====== Mozaic: Include Snippet - Active Notes Tracker ======
{{tag>Mozaic midi_scripting}}
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.
==== Problem Solved by the Snippet ====
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.
==== Scripts using the Active Notes Tracker Snippet ====
* The [[https://patchstorage.com/active-notes-tracker/|Active Notes Tracker (Include) Demo]] is a fast asynchronus display of all active notes for all channels
* The [[https://patchstorage.com/note-statistics/|Note Statistics]] script displays the active count and total duration in 32th for all 12 root notes
* The [[https://patchstorage.com/mute-maschine/|Mute Maschine]] script allows to beat juggle with channel mutes (temporary mute/un-mute channels, ie gating sustained chords)
\\
===== Adding the 'Active Note Tracker' to own scripts ====
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.
* In the @OnLoad you need to call @ActiveNotesTracker_Init
* The event funciond @ActiveNotesTracker_NoteOn and @ActiveNotesTracker_NoteOff need to be called
either in @OnMidiNoteOn, @OnMidiNoteOff or in your @OnMidiNote or @OnMidiInput event.
* There are some array variables that you can check at any time:
* active_count[channel] : number of active notes on channel
* active_seqid[channel] : unique event sequence id per channel
* Wherever needed, you can call @ActiveNotesTracker_GetActive with the pChan parameter variable
set to the channel you want to query to update the following arrays:
* active_note[] : array of active notes
* active_velo[] : array of per note velocity
The number of entries is already specified in active_count[pChan]
==== Simple Usage Example ====
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
\\
\\
===== Active Notes Tracker Include Snippet ====
// ╔═╗┌─┐┌┬┐┬┬ ┬┌─┐ ╔╗╔┌─┐┌┬┐┌─┐┌─┐ ╔╦╗┬─┐┌─┐┌─┐┬┌─┌─┐┬─┐
// ╠═╣│ │ │└┐┌┘├┤ ║║║│ │ │ ├┤ └─┐ ║ ├┬┘├─┤│ ├┴┐├┤ ├┬┘
// ╩ ╩└─┘ ┴ ┴ └┘ └─┘ ╝╚╝└─┘ ┴ └─┘└─┘ ╩ ┴└─┴ ┴└─┘┴ ┴└─┘┴└─
// ==== 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