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:
- 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
- A device may only use the LSB value of the index but will use the higher resolution of the Data, resulting in 3 packets
- 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
- A device may send the packets in the order MSB then LSB, or the other way around
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
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.
ReplyDeleteHey 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