Keeping track of held MIDI notes?

submo

Active member
Hello,

So I have a MIDI project where I need to keep track of what MIDI notes are currently being held down.

For instance if notes 40, 50, 60 were pressed (in that order) I would like to be able to quickly retrieve this info. Then say if note 50 was released, then I would get just notes 40 & 60. I'd obviously like to be able to manage more than just three notes (was thinking up to 16), but I've been scratching my head for a few days trying to figure out the best or most optimal way to do this and have not got very far. I don't want to be iterating over an entire array of 128 notes just to find which ones are currently being held, and I'd also like to be able to figure out the order that the notes were pressed. I'm normally fairly good with MIDI stuff, but my coding skills are average at best.

I've been looking at linked lists, maps etc.... but I can't help think that they might be a bit overkill, and I'm missing a trick somewhere... Any advice or thoughts would be grand!

TIA
 
If you want to use a standard container object, definitely not a map, and a set would probably be better than a list. You can insert() or erase() by value, as opposed to a list where you have to find an element before you remove it.
 
How's this:
Code:
#include <bitset>

std::bitset<128> note_state;
uint8_t notes_on[128];
uint8_t n_notes_on = 0;

When note x is pressed:
Code:
note_state[x] = true;
notes_on[n_notes_on++] = x;

When note x is released:
Code:
note_state[x] = false;
for (int i=0; i < n_notes_on; i++) {
    if (notes_on[i] == x) {
        --n_notes_on;
        memmove(notes_on+i, notes_on+i+1, n_notes_on - i);
        break;
    }
}
 
You can use a doubly linked list to do the operations O(1), which might be optimal time-wise but its probably not worth the effort.

The code should be a bit more defensive, for instance if the same note is pressed twice without being released in between you should avoid putting the note into the list twice. Otherwise a single lost Note-Off message would break the list state indefinitely.

So scan the entire list for both Note-On and Note-Off, then you can keep it in a sane condition in the face of errors.

If the list size is smaller than the number of notes you'll have to have a strategy for when more notes are pressed than the list can hold. Either kill least recent note (to allow new note to sound) or block new notes until existing one is released...
 
In terms of defensive coding (and besides accounting for Note-On with velocity 0 as Note-Off) you might also want to consider All Notes Off (123) and All Sound Off (120). See the chapter "CHANNEL MODE MESSAGES" in the MIDI 1.0 spec.

If your MIDI stream could have come from a MIDI merger, you could also encounter Note-Off without the corresponding Note-On.
 
Thanks everyone!

@jmarsh Thank you. I will definitely give this a go, but it looks like this is the trick that I was missing.

@MarkT I will definitely be adding some defensive code in there. I know how finicky MIDI can be sometimes, and have been caught out before, so I will be adding a few checks in there.

@Nantonos Good call on Note On with Velocity 0. I'm pretty sure that the default Teensy Midi library, and also usbMidi will flag a Note On with Velocity zero as a Note Off but I will be double checking this.

I'll post up my solution when I get it sorted.

Thanks again!
 
Hello again.

I seem to have this all working. Slightly modified so I can keep track of held notes across all Midi Channels, and added a couple of defensive checks in there. Also double checked about Note On Velocity Zero, and both the MIDI & usbMidi libraries automatically account for this.
Took me a little bit to figure out how to get memmove to work correctly with multi dimensional arrays (first time doing this). Not sure if what I've done is the best way to go about it, but it seems to work OK.....
Code:
std::bitset<128> note_state_ch[16];  // bitset array for Note States for Midi Channels 1-16
uint8_t notes_on_ch[16][128];        // Currently held Notes for Midi Channels 1-16
uint8_t num_notes_on_ch[16];         // Number of held Notes for Midi Channels 1- 16

if (MIDI_A.read()) {  // Get a MIDI message from MIDI DIN A if available
    byte type = MIDI_A.getType();
    byte channel = MIDI_A.getChannel();
    byte data1 = MIDI_A.getData1();
    byte data2 = MIDI_A.getData2();

    if (channel) {                                    // If a Channel message
      byte ch_index = channel - 1;                    // Convert Channel number (1-16) to index (0-15)
      if (type == midi::NoteOn) {                                      // If a Note On Message
        if (!note_state_ch[ch_index].test(data1)) {                    // Check if Note number is not already held
          note_state_ch[ch_index].set(data1);                          // If not held, flag as held
          notes_on_ch[ch_index][num_notes_on_ch[ch_index]++] = data1;  // Add Note number to Held Notes table & Inc number of held notes.
        }
      } else if (type == midi::NoteOff) {                            // Else If a Note Off Message
        if (note_state_ch[ch_index].test(data1)) {                   // Check if Note number is already held
          note_state_ch[ch_index].reset(data1);                      // If held, flag as not held
          for (uint8_t i = 0; i < num_notes_on_ch[ch_index]; i++) {  // Scan currently held Notes
            if (notes_on_ch[ch_index][i] == data1) {                 // If Note On found
              num_notes_on_ch[ch_index]--;                           // Dec number of notes held
              memmove(&notes_on_ch[ch_index][0] + i,                 // Remove from Held Note table & sort
                      &notes_on_ch[ch_index][0] + i + 1,
                      num_notes_on_ch[ch_index] - i);
            }
          }
        }
      }
    }

Thanks again everyone! Hopefully I haven't made any silly mistakes here, and welcome any improvements if you have any.
 
Just to make things a bit neater I would recommend using a struct array and hoisting out the channel index stuff, like so:
Code:
typedef struct {
    std::bitset<128> state;
    uint8_t on[128];
    uint8_t num;
} note_states;

note_states channel_notes[16];

if (MIDI_A.read()) {  // Get a MIDI message from MIDI DIN A if available
    byte type = MIDI_A.getType();
    byte channel = MIDI_A.getChannel();
    byte data1 = MIDI_A.getData1();
    byte data2 = MIDI_A.getData2();

    if (channel && channel<=16) {                 // If a Channel message
      note_states& ns = channel_notes[channel-1]; // Convert Channel number (1-16) to index (0-15), get reference to channel's struct
      if (type == midi::NoteOn) {                 // If a Note On Message
        if (!ns.state.test(data1)) {              // Check if Note number is not already held
          ns.state.set(data1);                    // If not held, flag as held
          ns.on[ns.num++] = data1;                // Add Note number to Held Notes table & Inc number of held notes.
        }
      } else if (type == midi::NoteOff) {         // Else If a Note Off Message
        if (ns.state.test(data1)) {               // Check if Note number is already held
          ns.state.reset(data1);                  // If held, flag as not held
          for (uint8_t i = 0; i < ns.num; i++) {  // Scan currently held Notes
            if (ns.on[i] == data1) {              // If Note On found
              ns.num--;                           // Dec number of notes held
              memmove(ns.on + i,                  // Remove from Held Note table & sort
                      ns.on + i + 1,
                      ns.num - i);
              /* it should be impossible for a value to be in this array more than once,
               * plus we've moved things around so the next entry is going to be skipped...
               */
              break;
            }
          }
        }
      }
    }
(I also put the break statement back in, because otherwise you're just wasting cpu cycles looking for a value that can't exist.)
 
I'm very late to the game, but AI chat bots really excel at this sort of thing. You could have gotten an efficient class from one of them. You would just need to craft a good paragraph describing what you wanted.
 
Back
Top