Showing posts with label Max for Live. Show all posts
Showing posts with label Max for Live. Show all posts

Sunday, March 28, 2010

NRPNs Part 3: Sending with Ableton and M4L

I realized that I didn't finish the NRPN saga with Ableton quite yet. We know how to filter out NRPN changes and use the reconstructed 14 bit values, but how do we send an NRPN change back to the device?

We'll use the same method as before, javascript objects, to split up an NRPN change into 4 CC packets to send back out to the device. Here's a snapshot of the Max device with the addition of the javascript object js nrpn_to_cc.js:


We'll go through the path really quickly...

MIDI comes in through midiin and into midiparse which filters different types of MIDI data to each of it's different outlet. Outlet 2 forwards all CC messages, so we connect that directly to js nrpn_filter.js to filter out NRPN messages. Any non-NRPN related CC messages are sent to outlet 0, which are displayed in the two number objects control change ctrl and value. When we receive 2-4 CC changes that represent an NRPN change we forward the index and value out of outlet 1, which are displayed in the two number objects NRPN change index and value.

After we've unpacked the CC data and taken a look at it, it's repacked and send to midiformat inlet 2, which takes a list of two values and turns it into a MIDI CC event. We do the same with the NRPN data but we have to translate it into the correct MIDI values first in js nrpn_to_cc.js, which sends out 4 CC messages for each NRPN change. midiformat then sends a raw MIDI stream to midiout and to a print object which spits out the raw MIDI to the main Max window for debugging.

The code in nrpn_to_cc.js is much simpler than the filter so we'll just start with pseudocode:

When we receive 2 values
    Split value 0 into two 7 bit values
    Send value 0 high 7 bits as a CC change to CC#99
    Send value 0 low 7 bits as a CC change to CC#98
    Split value 1 into two 7 bit values
    Send value 1 high 7 bits as a CC change to CC#38
    Send value 1 low 7 bits as a CC change to CC#6

Pretty simple! Since we already have the values we don't need to do a lot of voodoo to deal with timing. We could handle Running Status here and optimize the MIDI stream quite a bit but for now we'll just make sure we can reconstruct the NRPN packets to send out. The only real interesting part of this code for the budding code junkie is bitwise operators, so we'll go over those quick, give the full code and call it a day.

To get the high and low 7 bits we can either do some convoluted math to convert from base 2 to base 10 or just use bit operators to do it lickity split. Going back to our first example we'll use the value 572:

572 = 0x023C = 0b 00 0010 0011 1100

First we'll get the bottom 7 bits. We use the bit operator AND to grab certain bits from the number and ignore the rest. To demonstrate we'll do a couple bit operations...

First AND against all ones:
00 0010 0011 1100
AND11 1111 1111 1111
=00 0010 0011 1100

And second AND against all zeros:
00 0010 0011 1100
AND00 0000 0000 0000
=00 0000 0000 0000
Well now that we know if we AND a 1 to our original value, we get exactly what we put in. If we AND a 0 to the value, we always get 0. So what we want is to get the original value for the low 7 bits and ignore all the rest.This is called a bitmask.

So we'll do this for the lower 7 bits:
00 0010 0011 11000x023C
AND00 0000 0111 11110x007F
=00 0000 0011 11000x003C

 And for the higher 7 bits:
00 0010 0011 11000x023C
AND11 1111 1000 00000x3F80
=00 0010 0000 00000x0200
So now we have the lower 7 bits exactly where we want them, but the higher 7 bits aren't in the right place. We need to move them over 7 places to get it in the right position. Luckily there's an operator perfectly suited for this... >> 7 moves all the bits over by 7 places:

0b00 0010 0000 0000 >> 7 = 0b00 0000 0000 0001
0x0200 >> 7 = 0x0001

Now everything is ready to send out! Here's the code for the javascript object:

// 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 = 1;

// global variables and arrays
var nrpn_index;            // Save last known rpn index
var data;
var output;                // Up to 8 Bytes to send

// Maxobj variables for scripting
var nrpn_index_lsb;
var nrpn_index_msb;
var data_lsb;
var data_msb;

// methods start here

// list - expects an RPN 14 bit Index + 14 bit Value
function list(val)
{
    if(arguments.length==2)
    {
        nrpn_index = arguments[0];
        data = arguments[1];

        nrpn_index_lsb = nrpn_index & 0x007F;
        nrpn_index_msb = (nrpn_index & 0x3F8) >> 7 ;
        data_lsb = data & 0x007F;
        data_msb = (data & 0x3F8) >> 7;
       
        outlet(0, CC_NRPN_MSB, nrpn_index_msb);
        outlet(0, CC_NRPN_LSB, nrpn_index_lsb);
        outlet(0, CC_DATA_MSB, data_msb);
        outlet(0, CC_DATA_LSB, data_lsb);
    }
}

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