Arduino Multi MIDI Merge – Part 2

I’ve built a number of MIDI merge projects so far (for Arduino and Pico), but they’ve always taken a relatively simple approach to the merge.  They had all ensured that MIDI messages won’t be corrupted as they are merged – i.e. only complete messages will be passed on – but they’ve done nothing to interpret the messages in any way.

This project looks at how NoteOn and NoteOff messages might be handled slightly more intelligently by building on the code from the Arduino Multi MIDI Merge.

IMG_6237

Warning! I strongly recommend using old or second hand equipment for your experiments.  I am not responsible for any damage to expensive instruments!

These are the key Arduino tutorials for the main concepts used in this project:

If you are new to Arduino, see the Getting Started pages.

Parts list

  • Arduino Uno, Nano, Micro, Pro Mega or other depending on requirements.
  • Several MIDI Shields or alternative MIDI interface (DIY or ready-made)
  • MIDI note sources and MIDI sound source.

The Circuit

As this is using the code from the Arduino Multi MIDI Merge as a basis, then in principle any of the configurations from that previous project are plausible here too.

But for this demonstration I’m using two setups as follows:

  • An Arduino Uno with MIDI Shield and additional serial MIDI module.
  • A “Pro Mega” third party ATmega2560 based board with two serial MIDI modules.

The Uno is connected as follows:

  • TX/RX to the MIDI Shield MIDI OUT/IN.
  • D2 to the additional module MIDI IN.

The Pro 2560 is connected as follows:

  • D18/D19 (TXD1/RXD1) to a MIDI module MIDI OUT/IN.
  • D17 (RXD2) to a MIDI module MIDI IN.

In the case of the 2560, this leaves RXD0/TXD0 free for a debug console accessed over USB.

The Code

The main code is fully described in Arduino Multi MIDI Merge, with details of how to configure the various options for hardware serial, software serial, USB MIDI device or USB MIDI host depending on which board and additional hardware is in use.

The two configurations I’m using are as follows:

For the Uno:

#define MIDI_HW_SERIAL 1
#define MIDI_SW_SERIAL 5
#define MIDI_OUT MIDI_HW_SERIAL

For the Pro Mega:

#define MIDI_HW_SERIAL2 2
#define MIDI_HW_SERIAL3 3
#define MIDI_OUT MIDI_HW_SERIAL2

My approach to “Intelligent” Merging

The previous code simply passed on any complete MIDI messages out to the merged channel. This works fine for most things, but can be problematical for note messages.  Merging MIDI streams is never really that simple, so there will always be competing messages sent.  If both inputs send a Program Change message for example, which should take effect – the first or last one received?

But with NoteOn/NoteOff messages, where there is an assumed pairing, a little more intelligence is possible.  I’ve illustrated three scenarios below.

Arduino MIDI Merge - Issue

The first is fairly straightforward.  Two notes are played, one after the other and the MIDI NoteOn and NoteOff messages appear in sequence and two distinct notes are heard on the merged channel.

In the second scenario, the notes overlap.  With a simple implementation of MIDI merge, the NoteOn and NoteOff messages would be forwarded on in the order they are received, which would result in the note being turned off on the merged channel when the first NoteOff message is received.

In the last scenario, the merging is more knowledgeable of the MIDI protocols and is able to know that the notes are overlapping and doesn’t send any NoteOff messages until the last NoteOff has been received.

Which scenario works best is probably dependent on the application but in general if a MIDI stream has assumed a note was playing, then it probably ought to remain playing until the last NoteOff has been received.  This is the approach I’m aiming for in this code.

To achieve this, the merge unit has to count the NoteOn and NoteOff messages and only actually send a NoteOff when the final one has been received.  This sounds fairly straightforward.  Until you remember there are 128 notes to keep track of.  On 16 channels…

The simplest way would be a two dimensional array: [16][128].  Using 8-bit values (the smallest natural data type) this requires 16×128 = 2048 bytes.  That doesn’t sound like much until you realise that this is the total amount of working memory on an ATmega328!  That isn’t going to work 🙂

So in the code, I’ve allowed for two approaches. If there is plenty of memory (in this case that means using an ATmega2560) then I use the two dimensional array of counters.

If however, there isn’t much memory (e.g. with an ATmega328 or an ATmega32U4) then I use two one-dimensional arrays of 16-bit values as follows:

#define NUM_NOTES 128
uint16_t note1[NUM_NOTES];
uint16_t note2[NUM_NOTES];

The simplest approach would be to use them as a flag to know that up to two NoteOn messages have been received.  As there are 16 MIDI channels, each bit in each entry can represent one MIDI channel playing that note, so with two arrays, I can track two notes.

However, with a little more code (although not much) I can treat these two arrays as two bits of a two-bit number, which then gives me four different states, which I can use to track up to three notes as follows:

note2 note1
  0     0  = No notes being played
  0     1  = One note already being played
  1     0  = Two notes already being played
  1     1  = Three notes already being played

Rather than attempt any calculated bit manipulation, in the interests of speed, I just hard-code in four “if” statements to check for the various states and act accordingly.  Remember that the required bit in each value is determined by the MIDI channel (once translated over to 0 to 15 form).  Each of the if/elseif/elseif/else blocks is basically checking for the two-bit binary values 00, 01, 10, 11, in that order.

uint16_t chmask = (1<<(channel-1));
uint16_t n1 = (note1[note] & chmask);
uint16_t n2 = (note2[note] & chmask);

if (cmd == midi::NoteOn) {
  if (!n2 && !n1) {
    note1[note] |= chmask;
  } else if (!n2 && n1) {
    note1[note] &= ~chmask;
    note2[note] |= chmask;
  } else if (n2 && !n1) {
    note1[note] |= chmask;
  } else {
    // Ignore
  }
  return true;
} else if (cmd == midi::NoteOff) {
  if (!n2 && !n1) {
    return true;
  } else if (!n2 && n1) {
    note1[note] &= ~chmask;
    return true;
  } else if (n2 && !n1) {
    note1[note] |= chmask;
    note2[note] &= ~chmask;
    return false;
  } else {
    note1[note] &= ~chmask;
    return false;
  }
}

Essentially each state does the equivalent of “count = count + 1” on NoteOn for the two-bit binary number, and “count = count – 1” for NoteOff.  Whilst it always sends a NoteOn whenever one is received (even if I’ve run out of spaces to count them), it will only send a NoteOff if the counter is zero.

It is using bit manipulation throughout, so “|= mask” will set the bit defined by the mask; “&= ~mask” will clear it; “& mask” will test it; and so on.  I’ve been a little lazy in that I don’t care what the “non zero” value is for any of the tests.  This means that, say, for channel 7, “false” will be zero, as you’d expect, but “true” would be b0000 0000 0010 0000 – i.e. bit 6 is set (which is decimal value 32) – remember MIDI channels are 1 to 16, but they need translating over to 0 to 15 to be used in code.  The same is true for any other channels, but it doesn’t matter as I’m only testing for “value” and !”value” (“not value”) and don’t care what value is.

Note that a NoteOn is always sent.  I figured it was more important to allow the receiver to ‘retrigger’ if required than not. If however I receive more NoteOff’s than I’ve been able to count, I just pass those on too and don’t worry about it.

It isn’t perfect, but then it is hard to define some generic behaviour for any kind of “intelligent” MIDI merge, so I thought this isn’t a bad compromise.

Find it on GitHub here.

Closing Thoughts

I really wasn’t sure that the memory limitations of the more basic Arduino boards would actually let me do anything more than just a simple merge, but I’m pleasantly surprised I’ve been able to do something here.

My initial thought was to use a bit-field that allowed a tradeoff between the number of channels monitored (not all 16 would need to cope with polyphony); the number of notes watched (most channels won’t use anything like the full 128 notes!); and the level of polyphony to track.  I think it would be possible, but it requires a lot of bit manipulation to map specific bits onto channels and notes, and means keeping track of odd numbers of bits.  For example, for 8-note polyphony, 3 bits per note per channel are required.  I think it would be doable, and would allow for different builds to compromise in different ways according to the application.  But the performance is likely to be pretty poor with all that bit calculating going on.

As usual, we have to trade off either memory or computation.  With more memory, the computing is simple – just use 2048 counters to cover the entire 16 channels of 128 notes.  With less memory, the computational overhead increases.  I think my compromise, taking advantage of the fact there can be only 16 channels, and there is a native 16-bit data type, is a pretty good half-way solution.

It would be possible to add a third bit, and only require an additional 256 bytes (another 128 16-bit values), which would allow me to track up to 7 note polyphony. It would need twice the number of if/ifelse statements of course.  But that would probably only really be plausible on an ATmega32U4, with its 2.5K of memory, and if I was using a 32U4, then I’d probably want some of that extra memory for a USB stack.

I also think it highly likely that there would only be a couple of channels if using an Uno or ATmega32U4, due to the fact they only have one or two onboard UART serial ports.  So it is unlikely to be a requirement to track many notes anyway.  I’m working on the assumption that each MIDI link is unlikely to already be a merged or multiplexed link. If a single link will be sending overlapping NoteOn/NoteOff messages itself, then that is really a problem for the up-stream merging!

No, if more than 3-note polyphony is required, then I think it is fair to upgrade to a 2560 based board or jump away from Arduino completely.  This kind of thing could work fine on a Raspberry Pi Pico for example.

Kevin

Leave a comment