Table of Contents

Mozaic: Include Snippet - Active Notes Tracker

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


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.

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]

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