The Active Notes Tracker (Include) is a code snippet that at any time allows fast retrieval of a sorted list of active notes, their velocity and start-time for each of the midi channels. The sort-order is either oldest first or lowest note first. A per channel sequenceId allows to detect if the state of a channel changed. You just need to add 3 event calls and then can use the note trackers functionality to get an up-to-date and sorted note list.
In MIDI scripting, tracking the active notes is a required base functionality for implementing ARPs, chord detection, chord accompaniment and other advanced topics.
A naive approach would be to use the NoteState array to store the state of all notes for all channels - but retrieval would be very slow as the script would need to iterate through 128 notes on 16 channels, which would take too long and produce CPU spikes.
A better approach is to store the arriving notes in arrays (maybe separated by offsets per channel) - but the order in this array needs to change if notes of a chord are released, or when sorting lowest to highest ist needed and a new note is added to a held chord.
Nevertheless, implementing such a tracker is a complex task and its debugging will take a lot of time.
Using the Acive Notes Tracker snippet is a fast alternative - it's easy to add to own scripts, it's fast, feature-rich and well debugged. Internally the snippet maintains 16 double linked lists, therefore inserting and deleting central entries does not impose an overhead like when using linear arrays. Retrieval of all active notes of each channel is also quite fast.
You can either grab the snippet from one of the example scripts where you find the to-be-included snippet at the end of the scripts inbetween ⌄⌄⌄⌄⌄⌄⌄⌄ and ⌃⌃⌃⌃⌃⌃⌃⌃ .
Or you copy the code from the large code block at the end of this page to the end of your own script.
either in @OnMidiNoteOn, @OnMidiNoteOff or in your @OnMidiNote or @OnMidiInput event.
set to the channel you want to query to update the following arrays:
The number of entries is already specified in active_count[pChan]
The following code block contains a complete example allowing to log all active notes of each channel.
@OnLoad ShowLayout 2 LabelPads {Simple Active Note Tracker Demo} for p = 0 to 15 LabelPad p, {Log CH},p+1 endfor active_notes_sorting = YES Call @ActiveNotesTracker_Init FillArray lastSeq,-1,16 @End @OnMidiNoteOn Call @ActiveNotesTracker_NoteOn @End @OnMidiNoteOff Call @ActiveNotesTracker_NoteOff @End @OnPadDown pChan = LastPad Call @ActiveNotesTracker_GetActive if active_count[pChan] if lastSeq[pChan] <> active_seqid[pChan] lastSeq[pChan] = active_seqid[pChan] Log { } Log {==== CH },LastPad+1,{ < changed >} for n = 0 to active_count[pChan]-1 Log { },n+1,{: n},active_note[n],{ v},active_velo[n],{ t},active_time[n] endfor else Log {==== CH },LastPad+1,{ < same as last log > } endif else Log {==== CH },LastPad+1,{ < empty > } endif @End
// ╔═╗┌─┐┌┬┐┬┬ ┬┌─┐ ╔╗╔┌─┐┌┬┐┌─┐┌─┐ ╔╦╗┬─┐┌─┐┌─┐┬┌─┌─┐┬─┐ // ╠═╣│ │ │└┐┌┘├┤ ║║║│ │ │ ├┤ └─┐ ║ ├┬┘├─┤│ ├┴┐├┤ ├┬┘ // ╩ ╩└─┘ ┴ ┴ └┘ └─┘ ╝╚╝└─┘ ┴ └─┘└─┘ ╩ ┴└─┴ ┴└─┘┴ ┴└─┘┴└─ // ==== v1.0 ============================================= // Manages a list of active notes, velocity and duration for // each channel. // // Internally it maintains 16 double linked lists therefore // inserting and deleting entries does not impose an overhead. // Retrieval of all active notes of each channel is also quite fast. // // Notes are returned in order - either oldes first or lowest // note first, depending on the variable 'active_notes_sorting' @ActiveNotesTracker_Init if Unassigned active_notes_sorting active_notes_sorting = NO endif ANT_NONE = -1 // NoteOn info: FillArray ant_note, ANT_NONE // - note number FillArray ant_chan, ANT_NONE // - channel FillArray ant_velo, ANT_NONE // - velocity FillArray ant_time, ANT_NONE // - duration in mseconds // Double linked list: FillArray ant_prev, ANT_NONE // - prev index or ANT_NONE for head FillArray ant_next, ANT_NONE // - next index or ANT_NONE for last FillArray ant_store_a, ANT_NONE // Note state array substiture FillArray ant_store_b, ANT_NONE FillArray ant_head, ANT_NONE,16 // 16 linked list head pointers FillArray ant_tail, ANT_NONE,16 // 16 linked list tail pointers // Output arrays FillArray active_count,0,16 FillArray active_seqid,0,16 FillArray active_note,0,16 FillArray active_velo,0,16 FillArray active_time,0,16 ant_curr = ANT_NONE // Linked list, current entry for iteration ant_free = 0 // Next free entry Log {📔 ActiveNotesTracker v1.0: Initialized} @End @ActiveNotesTracker_GetActive // pChan ant_curr = ant_tail[pChan] ant_i = 0 while ant_curr <> ANT_NONE active_note[ant_i] = ant_note[ant_curr] active_velo[ant_i] = ant_velo[ant_curr] active_time[ant_i] = ant_time[ant_curr] Inc ant_i ant_curr = ant_prev[ant_curr] endwhile @End @ActiveNotesTracker_Reset FillArray ant_store_a, ANT_NONE FillArray ant_store_b, ANT_NONE @End @ActiveNotesTracker_NoteOn _off = MIDIChannel*128 + MIDINote if _off < 1024 _store = ant_store_a[_off] else _store = ant_store_b[_off-1024] endif if _store = ANT_NONE ant_note[ant_free] = MIDINote ant_chan[ant_free] = MIDIChannel ant_velo[ant_free] = MIDIVelocity ant_time[ant_free] = SystemTime active_seqid[MIDIChannel] = (Inc active_seqid[MIDIChannel]) % 4096 ant_hd = ant_head[MIDIChannel] if ant_hd = ANT_NONE ant_head[MIDIChannel] = ant_free ant_tail[MIDIChannel] = ant_free ant_prev[ant_free] = ANT_NONE ant_next[ant_free] = ANT_NONE elseif active_notes_sorting ant_tl = ant_tail[MIDIChannel] if ant_note[ant_tl] > MIDINote // new note lower than tail note ant_tail[MIDIChannel] = ant_free ant_next[ant_tl] = ant_free ant_prev[ant_free] = ant_tl ant_next[ant_free] = ANT_NONE elseif ant_note[ant_hd] < MIDINote // new note higher than head note ant_head[MIDIChannel] = ant_free ant_prev[ant_hd] = ant_free ant_next[ant_free] = ant_hd ant_prev[ant_free] = ANT_NONE else // search for position ant_curr = ant_prev[ant_tl] while ant_note[ant_curr] < MIDINote ant_curr = ant_prev[ant_curr] endwhile ant_nx = ant_next[ant_curr] ant_prev[ant_free] = ant_curr ant_next[ant_free] = ant_nx ant_next[ant_curr] = ant_free ant_prev[ant_nx] = ant_free endif else // In-Order linking ant_head[MIDIChannel] = ant_free ant_prev[ant_hd] = ant_free ant_next[ant_free] = ant_hd ant_prev[ant_free] = ANT_NONE endif if _off < 1024 ant_store_a[_off] = ant_free else ant_store_b[_off-1024] = ant_free endif Inc active_count[MIDIChannel] // Find enxt free entry _c = 0 repeat ant_free = (ant_free+1) % 1024 _c = _c + 1 until ant_note[ant_free] = ANT_NONE if _c > 15 Log {📔 ActiveNotesTracker v1.0: ‼️ Search or free-entry took },_c, { iterations} endif else Log {📔 ActiveNotesTracker v1.0: ❗️ Double NoteOn },{ c},MIDIChannel,{ n},MIDINote,{ detected} endif @End @ActiveNotesTracker_NoteOff // returns pDuration _off = MIDIChannel*128 + MIDINote if _off < 1024 _idx = ant_store_a[_off] else _idx = ant_store_b[_off-1024] endif if _idx <> ANT_NONE if ant_note[_idx] <> MIDINote or ant_chan[_idx] <> MIDIChannel Log { n},MIDINote,{ c},MIDIChannel,{ vs n},ant_note[_idx],{ c},ant_chan[_idx] Log {📔 ActiveNotesTracker v1.0: ‼️ Internal ERROR: NoteOff called with wrong _idx } endif active_seqid[MIDIChannel] = (Inc active_seqid[MIDIChannel]) % 4096 // free the entry ant_note[_idx] = ANT_NONE _time = ant_time[_idx] if _time = ANT_NONE pDuration = ANT_NONE else pDuration = SystemTime - _time endif // remove from list _prev = ant_prev[_idx] _next = ant_next[_idx] if _prev = ANT_NONE ant_head[MIDIChannel] = _next if _next <> ANT_NONE ant_prev[_next] = ANT_NONE else ant_tail[MIDIChannel] = ANT_NONE endif elseif _next = ANT_NONE ant_next[ _prev ] = _next ant_tail[MIDIChannel] = _prev else ant_next[ _prev ] = _next ant_prev[ _next ] = _prev endif Dec active_count[MIDIChannel] // Update internal note state array if _off < 1024 ant_store_a[_off] = ANT_NONE else ant_store_b[_off-1024] = ANT_NONE endif else Log {📔 ActiveNotesTracker v1.0: ❗️ Double NoteOff},{ c},MIDIChannel,{ n},MIDINote,{ detected} endif @End