Sunday, March 28, 2010

NRPNs Part 2: Filtering NRPNs in Ableton with M4L

Since the whole goal is getting Ableton controlling the Mopho and Tetra the next step is to figure out how the heck we can use NRPNs with Ableton. On the positive side, NRPNs are just CCs so Ableton doesn't filter them out like it does with Sysex. On the negative side, Ableton handles NRPNs changes as 4 separate CC changes which makes it darned near impossible to automate the parameters efficiently.

Enter Max for Live. M4L doesn't handle NRPNs natively but it does give us an easy way to intercept the stream of MIDI CCs we see and reconstruct it into 14 bit index + value pairs. There's a couple ways we can handle the NRPN translation in M4L: Create an object that watches for each CC change we are interested and constructs the data, use Javascript as a js object in Max to filter out the CCs, or write a module in C using the Max SDK. Since I like the flexibility of code with this type of problem and I'm not quite ready to explore the SDK (maybe in a few weeks) we're going to stick to javascript this time around.

Going from one of the MIDI tutorials I put together a Max patch that sends all the CC MIDI data from one input into a java script with 1 inlet and 2 outlets, intending to have outlet 0 forward all the CC data we didn't filter, and have each complete NRPN change sent to outlet 1.



Since we have some important CC values we're interested in  I saved them as constants in the beginning of nrpn_filter.js

const CC_DATA_MSB = 6;
const CC_DATA_LSB = 38;
const CC_NRPN_LSB = 98;
const CC_NRPN_MSB = 99;


Then you need to declare the number inlets and outlets for the Max object:

inlets = 1;
outlets = 2;


 At this point we have a totally useless object that looks like it did in the picture above. Time to get it to do something useful. Since the js nrpn_filter.js object will be getting data from the midiparse object I checked through the Max docs to find that midiparse, like many other objects, sends data out in the list format. In Max Javascript, if you create a function list(val) it will run every time you get a new input in the list format. That's pretty much what I want, to run some code to check the CC number and assemble the values every time we get a new CC change. So in nrpn_filter.js we'll create a skeleton for the list function:

function list(val)
{

}

Since I'm trying to be a better programmer lately I'm going to focus on taking small steps that create a working demo, first step will be just sending all the CC data out outlet 0 and doing a basic check to make sure we get what we expect. I also added a post() statement for debugging so I can see exactly what values go into the function printed to the main Max screen. Now our list functions looks like this:

function list(val)
{

    cc_index = arguments[0];
    cc_value = arguments[1];

    post(cc_index, cc_value);

    outlet(0, cc_index, cc_value);



}

After plugging in the Mopho Keyboard to USB and hooking this Max patch up to it, it's spitting out all the CCs I expect to see from each NRPN change. Great! To reconstruct the NRPN from CC messages we need to understand a little bit more about the way MIDI constructs it's data, in specific a shortcut MIDI uses to prevent redundant information called running status.

Running Status holds the last command that was sent over the MIDI bus so we don't have to send a new 0xB0 byte for each individual CC change on MIDI channel 1, we just send one 0xB0 and every data byte after that is interpreted as if it was a CC change on MIDI channel 1. So let's go back to the raw MIDI stream of the NRPN change we used in Part 1 (NRPN# 572 set to 163):

     B0 63 04 B0 62 3C B0 06 01 B0 38 23

If the device was using Running Status, the exact same command could look like this:

     B0 63 04 62 3C 06 01 38 23

Since MIDI is limited by the hardware specifications of the time it was created to send one 3 byte packet of data can take 1ms to transmit. Since we can only transmit one message at a time, if we're changing 10 CCs at once, it will be a minimum of 10ms from the first change to the last change, which could easily be noticed by the human ear. It's worse if during those changes you start inputting notes, which are even easier to hear than the generally more subtle CC changes. Luckily USB takes care of this since it's hundreds of times faster and you can bundle many MIDI messages in a single USB packet, and Max/Ableton will take care of that low level running status... so why am I bringing this up?

When we reconstruct the NRPN packet so far we've assumed that we're going to see 4 packets of data for each change. There a few cases that we may see:
  1. A device may only use the LSB values, not caring about the extra resolution of the MSB, so it will only send 2 packets per change
  2. A device may only use the LSB value of the index but will use the higher resolution of the Data, resulting in 3 packets
  3. A device may send 1-2 packets to indicate which NRPN it intends to change and then only send data changes from there on out, expecting that each new data packet will be interpreted as the same NRPN data change
  4. A device may send the packets in the order MSB then LSB, or the other way around
Even though I know exactly how the Mopho/Tetra are sending out MIDI we're going to assume that whatever device might be sending MIDI to this patch isn't as stable and we'll have to consider those edge cases. Since we're not entirely sure what order things will come in, we need to figure out what we are SURE is going to happen. The two things we can assume is that we will receive the NRPN index messages BEFORE the data and that the LSB of the data will change every time we get an NRPN change. So we'll use those rules to construct the algorithm. Here's the high level pseudocode for the reconstruction before we translate it into raw javascript:

If we receive an NRPN MSB or LSB index message
    Save the new NRPN MSB or LSB index
    Construct the new NRPN (even if we've only received one of the two messages)
If we receive an NRPN MSB data message
    If we have a valid NRPN index
        Save the new NRPN MSB data value
        If we have a valid NRPN LSB data value
            Construct the 14 bit data value  from the data MSB and data LSB
            Send a new NRPN message to outlet 1
    Else if we do NOT have a valid NRPN index
        It is not a valid NRPN data packet, forward the CC data to outlet 0
If we receive an NRPN LSB data message
    If we have a valid NRPN index
        Construct the 14 bit data value from the data MSB and data LSB
        Send a new NRPN message to outlet 1
    Else if we do NOT have a valid NRPN index
        It is not a valid NRPN data packet, forward the CC data to outlet 0
If we receive any other type of CC message
    Forward it to outlet 0

There's a catch here though. If we receive the data MSB and LSB out of order, we will end up sending 2 NRPN changes out. Since each one is 4 bytes, every time that happens we're adding almost 3ms of latency to a hardware MIDI bus. So in the interest of latency we'll assume that the data will arrive in MSB then LSB order.To determine when we've sent a valid NRPN message, since we are not sure if the MSB will change, we need to reset the data LSB to an invalid state every time we send a new NRPN change out.

Each time we send an NRPN message
    Reset the NRPN LSB data message to invalid

Here's the resulting nrpn_filter.js file:

// MIDI CC constants
const CC_DATA_MSB = 6;
const CC_DATA_LSB = 38;
const CC_NRPN_LSB = 98;
const CC_NRPN_MSB = 99;

// inlets and outlets
inlets = 1;
outlets = 2;

// global variables and arrays
var nrpn_index;

var value;
var nrpn_index_lsb;
var nrpn_index_msb = 0;
var value_lsb;
var value_msb = 0;
var cc_index;
var cc_value;

// methods start here

// list - expects a CC Index + Value as argument
// filters out NRPN and RPN values and assigns to variables
// Passes through all other CCs
function list(val)
{
    if(arguments.length==2)
    {
        cc_index = arguments[0];
        cc_value = arguments[1];

        switch(cc_index)
        {
        case CC_DATA_MSB:
            // If we have a valid NRPN index, then the data is valid
            if(nrpn_index)
            {
                value_msb = cc_value << 7;
                if(value_lsb)
                {
                    value = value_msb | value_lsb;
                    // We are now ready to send an index and value out!
                    send_nrpn(nrpn_index, value);
                }
            }
            // If we don't have an index it's invalid
            else
            {
                // So forward the value as a normal CC
                send_cc(cc_index, cc_value);
            }
            break;
        case CC_DATA_LSB:
            // If we have a valid NRPN index, then the data is valid
            if(nrpn_index)
            {
                value_lsb = cc_value;
                value = value_msb | value_lsb;
                // We are now ready to send an index and value out!
                send_nrpn(nrpn_index, value);
            }
            // If we don't have an index it's invalid
            else
            {
                // So forward the value as a normal CC
                send_cc(cc_index, cc_value);
            }
            break;
        // NRPN Index MSB - 7 high bits
        case CC_NRPN_MSB:
            // Save 7 high bits
            nrpn_index_msb = cc_value << 7;
            // Create the 14 bit NRPN index
            nrpn_index = nrpn_index_msb | nrpn_index_lsb;
            break;
       // NRPN Index LSB - 7 low bits
        case CC_NRPN_LSB:
            // Save 7 low bits
            nrpn_index_lsb = cc_value;
            // Create the 14 bit NRPN index
            nrpn_index = nrpn_index_msb | nrpn_index_lsb;
            break;
        default:
            send_cc(cc_index, cc_value);
            break;
        }
    }
}

// Send CC: Forward CC index and value to output 0
function send_cc(i, v)
{
    outlet(0, i, v);
}

// Send NRPN: Send NRPN index and value to output 1
function send_nrpn(i, v)
{
    outlet(1, i, v);
    value = null;        // reset the parsed value
    value_lsb = null;    // reset the LSB of the value
}

As a last word, I wouldn't consider myself a particularly good coder. I am aware that a lot of things here are redundant or inefficient but I tried to err on the side of readability. If you need a faster or more efficient handling (for a very large data stream for example) you'll probably want to rewrite this as a C module using the Max SDK.

If you spot any bugs or things that don't make sense, please let me know!

That's it for now, if I can make some progress on the Sysex side of things we'll dive into that next

2 comments:

  1. Chris, I'm having an issue when I try to build this device. I copied your js code into my own js object, but it only shows one output in the Max environment. I am completely new to Max4Live so I may be missing something obvious. Any help would be greatly appreciated.

    ReplyDelete
  2. Hey Justin, know it's a bit late and hope you found some help in the forums. I know you have to have the file in the same directory to call it using the js command directly (eg. "js file.js"). that might be your problem? Otherwise not entirely sure, I'm still having some small issues with M4L myself getting things to run smoothly, hence the lack of updates on this stuff lately

    ReplyDelete