Wednesday 2 April 2014

Ok, finally something technical!

Last post I promised an update on my JACK setup (and it is coming, maybe another double post after this one and a snack), but I couldn't help myself...

The Bitwig API adventure begins!

So I was going to hold off until I at least had the skeleton for my pads, but I thought I'd share something useful for all you developers.

TL;DR: Socket I/O from your control script , with a node.js app to display messages and log them to file.

 I started on the skeleton for my Quneo mapping and got sidetracked of course. First things first, I like to see what's going on so immediately looked for the the println() function . Some meandering later I came across the connectToRemoteHost() function and started to play.

This function takes a host, port and callback function as arguments. The callback function is called when it connects, and receives a RemoteConnection object to work with. Note the send(byte[] data) function requires a byte array, but we'll add some magic to handle string conversion.

I've been messing with node.js at work so thought it'd be a simple way to test the waters. Below is a short script that will listen locally on port 58008:


// logsrv.js

var net = require('net');
var fs = require('fs');
var tmpfilepath = '/tmp/logsrv.txt';


// Create our socket
var server = net.createServer(
  function (socket) {




    // Callback when data is received
    socket.on('data', function(data) {


      var messagetxt = "";




      // Convert bytes to string      for(var i = 0; i < data.length; i++) {
        messagetxt += String.fromCharCode(data[i]);
      }


      // Print message to console
      console.log(messagetxt);



      // Write message to file
      fs.writeFile(tmpfilepath, messagetxt, function(err) {
        if(err) { console.error("Write error: %s", err); }
      });
    });
  }

);


// Start server
server.listen(58008, '127.0.0.1');



Run the script with:

$ node logsrv.js

This will listen for incoming data, convert the bytestream to a string, print the message and log it to /tmp/logsrv.txt .


To set up the scratchpad control script, copy the template to your private Bitwig directory. On a default install, it will be something like:

$ cd ~/Bitwig\ Studio/Controller\ Scripts/
$ mkdir scratchpad
$ cp -R /opt/bitwig-studio/resources/controllers/template/template.js ./scratchpad/scratchpad.control.js


Make sure you use .control.js as the extension or it won't show up in the list in Bitwig.

Edit scratchpad.control.js and update the defineController() arguments to something relevant to your controller. Include a UUID generated here. You should have something like the following:

host.defineController("sherman", "scratchpad", "1.0", "1dece780-ba4d-11e3-a5e2-0800200c9a66");

We'll add our socket code in the onMidi() callback function, which will fire every time a note event is received:

function onMidi(status, data1, data2)
{

  // Create connection with callback definition
  host.connectToRemoteHost('127.0.0.1', 58008, function(conn) {
 

    var messagetxt = "midi event - " + status + " " + data1 + " " + data2;
    conn.send(messagetxt.getBytes());
    conn.disconnect();

  });

}


Wait, we need to send bytes right? Javascript doesn't actually define String.getBytes() , but we can update the String prototype by including the following:

String.prototype.getBytes = function () {
  var bytes = [];
  for (var i = 0; i < this.length; ++i) {
    bytes.push(this.charCodeAt(i));
  }
  return bytes;
};


This extends String with the new interface, which should cover our needs.

So altogether, you should have something that looks like this:

// scratchpad.control.js

loadAPI(1);

host.defineController("sherman", "scratchpad", "1.0", "1dece780-ba4d-11e3-a5e2-0800200c9a66");
host.defineMidiPorts(1, 1);

String.prototype.getBytes = function () {
  var bytes = [];
  for (var i = 0; i < this.length; ++i) {
    bytes.push(this.charCodeAt(i));
  }
  return bytes;
};

//
// Callbacks
//

function init()
{


  println("sandbox - init()");

  host.getMidiInPort(0).setMidiCallback(onMidi);
  host.getMidiInPort(0).setSysexCallback(onSysex);

  host.showPopupNotification("scratchpad loaded");
}


function exit()
{
}


function onMidi(status, data1, data2)
{

  // Create connection with callback definition
  host.connectToRemoteHost('127.0.0.1', 58008, function(conn) {
 

    var messagetxt = "midi event - " + status + " " + data1 + " " + data2;

    println(messagetxt);
    conn.send(messagetxt.getBytes());
    conn.disconnect();

  });

}


function onSysex(data)
{
}


Now load up BWS and Show the Control Script Console from the View menu. This will show any output from the host println() function.

Make sure the node app is waiting for a connection, then open the Controllers tab of the Preferences screen. When you click Add Controller Manually you should now see the new control script in the list.

Add the device and select the midi input device. This will fire off the init() callback and a message should appear in the console. If there is a problem loading the script, an error message will be printed to help you track down what's wrong. Click OK to return to the main screen and test the script is working by playing a note.

You should see another message in the console with the midi event info. If everything worked, the node app should also display the message in its terminal. Finally, check the file at /tmp/logsrv.txt and make sure they were logged as expected.


I'll check the example files into my github repo. Feel free to copy, extend and do whatever you want with the code. If you have any questions or feedback, comment here or at github.


Logging was a useful but basic example of what the RemoteConnection offers for extending and interacting with BWS. In a Linux environment, this gives us an interface for working directly with other processes on the system, and tools such as node.js are a great place to start. A web service interface could even allow for things like "tweet that I started a new set" or even "render track and upload to soundcloud", all from a midi event!

If anyone has ideas for something a bit meatier, I can look at a more in depth example in the future. Two of my first thoughts are an OSC server, and an interface to the JACK daemon.

Oh, and if you've been working on anything cool, let me know :D I'm always keen to see what other people come up with too.

4 comments:

  1. Unfortunately on a mac, I'm getting the following in the console:

    > restart
    Called init()
    sandbox - init()
    Wrapped java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 (scratchpad.control.js#27)

    Any ideas? It looks like the failure is the index on:

    host.getMidiInPort(0).setMidiCallback(onMidi);
    (it's failing on getMidiInPort(0), 0 being out of bounds)

    I've even set the midi in port to my virtual game device.

    //Jamie

    ReplyDelete
    Replies
    1. Hey man, the midi ports should be set up when the script loads with the line:

      host.defineMidiPorts(1, 1);

      Note: this is called in the global scope, so run when BWS loads the script, before the call to init()

      Could you paste a copy of your script either here or at pastebin.com or similar, and I'll take a look?

      Better yet, drop into the IRC channel and we can help you get up and running:

      http://webchat.freenode.net/?channels=bitwig-dev

      Delete
    2. Success, we got this working in IRC for anyone following.

      Looks to be from starting without attaching the midi device, maybe Jamie will drop back and update if anyone else runs into the same.

      Delete
    3. Yep, I misunderstood what the 'virtual midi driver game controller' actually was.

      In order to avoid this error you need to plug in some hardware (making sure you select the correct in and out options).

      Also, make sure it's all correctly connected/wired up.

      Delete