Distribute polyphonic midi to N channels with M voices each
Dec 4, 2018 17:38:59 GMT
midijam likes this
Post by ki on Dec 4, 2018 17:38:59 GMT
This StreamByter script is a followup/enhanced version of the PolyToNMono script.
There are two user-specified variables, one for defining the number of output synth instances (channels) and a second to specify the base polyphony (#voices) of each instance.
Feeding the script with polyphonic midi input will then distribute the concurrent notes to the available channels/voices. If more notes than the available (channels * #voices) are needed, ‚voice stealing‘ will happen by ‚hiding’ the ‚oldest’ notes and replacing them with the new ones. If later some notes are released and voices become available, the ‚hidden‘ notes are revealed again with their original velocity. Other midi events like pitch-bend and aftertouch are also distributed to the channels.
The script can be used to
The corresponding Audiosbus forum thread contains links to two Audiobus presets and two AUM sessions to showcase the script usage.
There are two user-specified variables, one for defining the number of output synth instances (channels) and a second to specify the base polyphony (#voices) of each instance.
Feeding the script with polyphonic midi input will then distribute the concurrent notes to the available channels/voices. If more notes than the available (channels * #voices) are needed, ‚voice stealing‘ will happen by ‚hiding’ the ‚oldest’ notes and replacing them with the new ones. If later some notes are released and voices become available, the ‚hidden‘ notes are revealed again with their original velocity. Other midi events like pitch-bend and aftertouch are also distributed to the channels.
The script can be used to
- Play multiple instances of monophonic synth (using the same preset) as if it was a polyphonic synth
- Enhance the polyphony of limited-polyphony synth as Moog Model D / Model 15 / Quanta
The corresponding Audiosbus forum thread contains links to two Audiobus presets and two AUM sessions to showcase the script usage.
# Distribute incomming polyphonic midi to N outgoing midi channels with M voices each
#
# Version: 4 / 04.12.2018
# Author: -ki https://forum.audiob.us/profile/_ki
#
# =============================
# User Setup:
IF LOAD
ASS I0D = 2 # Number of output midi channels/synth instances
ASS I0E = 4 # Number of voices per instance
# Use I0E=1 for monphonic synth (Ripplemaker, iVCS, Cyclops, Viking ....)
# Use I0E=4 for Model D, Model 15, EGSY01
# Use I0E=6 for Quanta
END
# =============================
# Documentation
#
# This script allows to enhance the polyphony of limited-polyphony synth by
# distributing the incomming polyphonic notes onto several instances.
# The script is an adaption of my „Distribute Poly To N Mono“ script.
#
# If a note is pressed, find an optimal output channel:
# - try to use the channed it used before if the channel is free (not playing)
# - try to use the free channel with the longest period of unuse
# - else use the active channel that has the longest period of unuse,
# hide the previous playing note and borrow that channel
# and play the incomming note on the computed output channel
#
# If a note is unpressed, stop playing and
# - Find the youngest hidden note and play/unhide with its previous,stored velocity
#
# Midi CCs, Aftertouch, Channelpressure and Pitchbend are send to all N channels.
# Polyphonic AT is changed send to the correct channel of the pressed note.
#
#
# The Streambyter Labels shows
# - LEFT: current number of playing channels
# - RIGHT: current hidden (hold) notes that will be played if a channel is available
# Changes
# =============================
# v 4 04.12.2018 Further cleanup, publish on Audeonic forum
# v 3 15.09.2018 Merge of both sources, renaming of vars, changed comment style,
# raised voice limit from 16 to 64 voices
# v 2 12.09.2018 Initial (internal) version of DistributePolyToNxMVoices
# v 1 06.09.2018 Initial realease of DistributePolyToNMono
# List of variables
# =============================
#
# J00-J7F noteInfo[note] for voice-idx of each note
# 00-0F voice-idx if note is playing
# 80-8F not playing, last used voice-idx + 80
# FF not playing, no voice info
#
# J80-JFF noteInfo[note+128] for velocity of each note
# 00-7F
#
# K00-K7F noteHold[note] sequence id to manage LRU of all held notes
# 00 note not hold down
# >01 sequenceId for note,based on nextHoldSeqId
#
# K80-KFF noteHold[note+128] used in re-indexing of noteHold[00-7F]
# 00-7F index into noteHold[]
#
# L00-L3F voiceInfo[voice] sequence id to manage LRU of voices
# 00 unused
# >01 sequenceId for voice, based on nextChannelSeqId
#
# L40-L7F voiceInfo[voice+64] for the note in a voice
# 00-7F is note of channel
# 80-FF is last note+80 of voice, voice is not playing
#
# L80-LBF voiceInfo[voice+128] used in re-indexing of voiceInfo[00-3F]
#
#
# I00 i Loop index
# I0D numChannels User specified number of synth instances/channels
# I0E numVoices User specified number of voices per instance/channel
# I0F totalVoices Computed number of voices of virtual poly synth
#
# I23 tmpIdx Temporary index offsetting the main index
# I24 newSeqId New sequenceId used for re-indexing the LRU
#
# I31 oldestEmptySeqId Oldest sequenceId for empty/non-playing channels
# I32 oldestSeqId Oldest sequenceId for any of the channels
# I33 oldestEmptyVoice Channel number of oldest empty voice, or FF
# I34 oldestVoice Channel number of oldest voice
# I35 noteIdx Index into note part of voiceInfo (ie i+64)
# I37 oldNote Previous playing note (to send a NoteOff in hiding)
# I38 midiCmd Computed midi command, usually (outputVoice%numChannels)+CMD
# I39 midiChannel Computed midi channel from virtual voice number
#
# I40 nextVoiceSeqId SequenceId for next used voice
# I41 nextHoldSeqId SequenceId for next pressed note
# I42 polyphony Number of playing voices
# I43 hidden Number of hidden notes still hold
#
# I50 useOldVoiceFlag Flag if the old channel of a note is available
# I52 unhideNote Note number of note to be unhidden, or FF if none
# I53 outputVoice The selected best voice for NoteOn or NoteOff
#
# I64 maxSeqId Highest found sequenceId when unhiding notes
#
# I70 loopEnd Loop end index in LRU re-Indexing
# I71 ptrA Offset to tmpIndex table part of voiceInfo for i
# I72 ptrB Offset to tmpIndex table part of voiceInfo for i+1
# I73 idxA Value of tmpIndex[i]
# I74 idxB Value of tmpIndex[i+1]
# I75 tmpVal Temporary storage for tmpIndex[] switching
# I77 elseFlag Temporary flag, 0 if IF was entered, 1 if not
# LRU Least recently used
# =============================
# For managing used voices and hold notes, two LRU caches are used. They
# store an auto-incemented sequence number, the oldest entry will have the
# lowest sequenceId and the youngest entry got the highest sequenceId. When
# notes are released, their seqId entry is cleared - there are only as many
# entries as held notes or playing voices. Depending on what is played,
# there will be gaps in the sequence, because new notes/voices always get
# a higher seqId which is removed when the note is unpressed.
#
# Since the number range is limited, the auto increment could overflow after
# a while - this is prevent by checking the currently highest sequenceId. If it
# reaches a threshold (4096), the LRU cache is re-indexed starting with 1,2,3...
# Empty entries (with seqId=0) will stay zero
#
# Re-indexing uses sorting of an indices into the value arrays. Because StreamByter
# does not support nested loops, a simple 'single loop sorting' is used.
#
# If you are still reading, you might wonder why i documented everything in such a
# depth...
# I wrote that much because the streambyter language is very ‚raw‘ but the used
# algorythms are not - having only 2 output lables for about 600 used variables
# is tough. Therefor i depeveloped/tested everything with a self-written
# simulation environment in Java (you see part of the code in the comments).
#
# If there were no comments and no list of which Ixx/J/K/L contains what, it gets
# compilated to enhance or debug the code.
#
# So the documentation is mainly for my future self ;-)
# // Remap NoteOn with vel=0 to NoteOff
9X XX 00 = 8X
IF MT == 90 # // ========== Handle NoteOn =================================
# // Check if note is not playing
IF JM01 >= 80 # if ( noteInfo[inputNote] >= 128 ) {
# // Check if old voice of note is free
ASS I50 = 0 # useOldVoiceFlag = 0
IF JM01 != FF # if ( noteInfo[inputNote] != 255 ) {
MAT I23 = JM01 - 40 # tmpIdx = noteInfo[inputNote] -128 + 64
IF LI23 >= 80 # if (voiceInfo[tmpIdx] >=128 ) {
MAT I53 = JM01 - 80 # outputVoice = noteInfo[inputNote] - 80
ASS I50 = 1 # useOldVoiceFlag = 1
END # }
END # }
# // When not using the old voice
IF I50 == 0 # if (useOldVoiceFlag == 0) {
# // Search for oldest voices
ASS I31 = 2000 # oldestEmptySeqId = 8192
ASS I32 = 2000 # oldestSeqId = 8192
ASS I33 = FF # oldestEmptyVoice = 255
ASS I34 = FF # oldestVoice = 255
ASS I00 = 0 # i = 0
IF I00 < I0F +L # while( i < totalVoices ) {
MAT I35 = I00 + 40 # noteIdx = i + 64
IF LI35 >= 80 # if (voiceInfo[noteIdx] >= 128) {
IF I31 > LI00 # if (oldestEmptySeqId > voiceInfo[i]) {
ASS I31 = LI00 # oldestEmptySeqId > voiceInfo[i]
ASS I33 = I00 # oldestEmptyVoice = i
END # }
END # }
IF I32 > LI00 # if (oldestSeqId > voiceInfo[i]) {
ASS I32 = LI00 # oldestSeqId > voiceInfo[i]
ASS I34 = I00 # oldestVoice = i
END # }
MAT I00 = I00 + 1 # i++
END # }
ASS I53 = I34 # outputVoice = oldestVoice
IF I33 != FF # if (oldestEmptyVoice != 255) {
ASS I53 = I33 # outputVoice = oldestEmptyVoice
END # }
# // Check if voice is already playing
MAT I35 = I53 + 40 # noteIdx = outputVoice + 64
IF LI35 < 80 # if (voiceInfo[noteIdx] < 128) {
# // Stop the old note
ASS I37 = LI35 # oldNote = voiceInfo[noteIdx]
MAT I39 = I53 % I0D # midiChannel = outputVoice % numChannels
MAT I38 = 80 + I39 # midiCmd = 80 + midiChannel
SND I38 I37 0 # send(midiCmd oldNote 0)
ASS JI37 = FF # noteInfo[oldNote] = 255
ASS LI53 = 0 # voiceInfo[outputVoice] = 0
MAT I42 = I42 - 1 # polyphony—
MAT I43 = I43 + 1 # hidden++
END # }
END # }
# // Check for re-indexing of voiceInfo
IF I40 > 2000 # if (nextVoiceSeqId > 4096) {
# // Prepare index table
ASS I00 = 0 # i = 0
IF I00 < I0F +L # while( i < totalVoices ) {
MAT I23 = I00 + 80 # tmpIdx = i + 128
ASS LI23 = I00 # voiceInfo[tmpIdx] = i
MAT I00 = I00 + 1 # i++
END # }
# // Single Loop Sort of seqId indices
ASS I00 = 0 # i = 0
MAT I70 = I0F - 1 # loopEnd = totalVoices -1
IF I00 < I70 +L # while(i < loopEnd ) {
MAT I71 = I00 + 80 # ptrA = i + 128
MAT I72 = I00 + 81 # ptrB = i + 129
ASS I73 = LI71 # idxA = voiceInfo[ ptrA ]
ASS I74 = LI72 # idxB = voiceInfo[ ptrB ]
ASS I77 = 1 # elseFlag = 1
IF LI74 < LI73 # if ( voiceInfo[idxB] < voiceInfo[idxA] ) {
ASS I75 = LI71 # tmpVal = voiceInfo[ptrA]
ASS LI71 = LI72 # voiceInfo[ptrA] = voiceInfo[ptrB]
ASS LI72 = I75 # voiceInfo[ptrB] = tmpVal
MAT I00 = I00 - 1 # i--
IF I00 == FFFF # if (i < 0) {
ASS I00 = 0 # i = 0
END # }
ASS I77 = 0 # elseFlag = 0
END # }
IF I77 == 1 # if (elseFlag == 1) {
MAT I00 = I00 + 1 # i++
END # }
END # }
# // re-index using sorted seqIndices
ASS I24 = 1 # newSeqId = 1
ASS I00 = 0 # i = 0
IF I00 < I0F +L # while( i < totalVoices) {
MAT I71 = I00 + 80 # ptrA = i + 128
ASS I73 = LI71 # idxA = voiceInfo[ ptrA ]
IF LI73 != 0 # if ( voiceInfo[idxA] != 0) {
ASS LI73 = I24 # voiceInfo[idxA] = newSeqId
MAT I24 = I24 + 1 # newSeqId++
END # }
MAT I00 = I00 + 1 # i++
END # }
ASS I40 = I24 # nextVoiceSeqId = newSeqId
END # }
# // Check for re-indexing of noteInfo
IF I41 > 1000 # if (nextHoldSeqId > 4096) {
# // Prepare index table
ASS I00 = 0 # i = 0
IF I00 < 80 +L # while( i < 80 ) {
MAT I23 = I00 + 80 # tmpIdx = i + 128
ASS KI23 = I00 # noteHold[tmpIdx] = i
MAT I00 = I00 + 1 # i++
END # }
# // Single Loop Sort of seqId indices
ASS I00 = 0 # i = 0
IF I00 < 7F +L # while(i < 127 ) {
MAT I71 = I00 + 80 # ptrA = i + 128
MAT I72 = I00 + 81 # ptrB = i + 129
ASS I73 = KI71 # idxA = noteHold[ ptrA ]
ASS I74 = KI72 # idxB = noteHold[ ptrB ]
ASS I77 = 1 # elseFlag = 1
IF KI74 < KI73 # if ( noteHold[idxB] < noteHold[idxA] ) {
ASS I75 = KI71 # tmpVal = noteHold[ptrA]
ASS KI71 = KI72 # noteHold[ptrA] = noteHold[ptrB]
ASS KI72 = I75 # noteHold[ptrB] = tmpVal
MAT I00 = I00 - 1 # i--
IF I00 == FFFF # if (i < 0) {
ASS I00 = 0 # i = 0
END # }
ASS I77 = 0 # elseFlag = 0
END # }
IF I77 == 1 # if (elseFlag == 1) {
MAT I00 = I00 + 1 # i++
END # }
END # }
# // re-index using sorted seqIndices
ASS I24 = 1 # newSeqId = 1
ASS I00 = 0 # i = 0
IF I00 <= FF +L # while( i <= 128) {
MAT I71 = I00 + 80 # ptrA = i + 128
ASS I73 = KI71 # idxA = noteHold[ ptrA ]
IF KI73 != 0 # if ( noteHold[idxA] != 0) {
ASS KI73 = I24 # noteHold[idxA] = newSeqId
MAT I24 = I24 + 1 # newSeqId++
END # }
MAT I00 = I00 + 1 # i++
END # }
ASS I41 = I24 # nextHoldSeqId = newSeqId
END # }
ASS JM01 = I53 # noteInfo[inputNote] = outputVoice
MAT I23 = M01 + 80 # tmpIdx = inputNote + 80
ASS JI23 = M02 # noteInfo[tmpIdx] = inputVelocity
ASS LI53 = I40 # voiceInfo[outputVoice] = nextVoiceSeqId
MAT I35 = I53 + 40 # noteIdx = outputVoice + 64
ASS LI35 = M01 # voiceInfo(noteIdx) = inputNote
ASS KM01 = I41 # noteHold[note] = nextHoldSeqId;
MAT I39 = I53 % I0D # midiChannel = outputVoice % numChannels
MAT I38 = 90 + I39 # midiCmd = 90 + midiChannel
SND I38 M01 M02 # send(midiCmd inputNote inputVel)
MAT I41 = I41 + 1 # nextHoldSeqId++;
MAT I40 = I40 + 1 # nextVoiceSeqId++
MAT I42 = I42 + 1 # polyphony++
END # }
IF I42 == 0 # if (polyphony == 0) {
SET LB0 S- # show( Label0 -)
END # }
IF I42 != 0 # if (polyphony != 0) {
SET LB0 I42 # show( Label0 polyphony )
END # }
IF I43 == 0 # if (hidden == 0) {
SET LB1 S- # show( Label1 -)
END # }
IF I43 != 0 # if (hidden != 0) {
SET LB1 I43 # show( Label1 hidden )
END # }
9X = XX +B # // Block arriving NoteOn
END
IF MT == 80 # // ========== Handle NoteOff ================================
IF KM01 > 0 # if (noteHold[inputNote] > 0) {
IF JM1 >= 80 # if noteInfo[inputNote] >=128) {
MAT I43 = I43 - 1 # hidden—-
END # }
END # }
ASS KM01 = 0 # noteHold[inputNote] = 0
# // Check if note is playing
IF JM01 < 80 # if ( noteInfo[inputNote] < 128 ) {
# // Stop playing the note
ASS I53 = JM01 # outputVoice = noteInfo[inputNote]
MAT I39 = I53 % I0D # midiChannel = outputVoice % numChannels
MAT I38 = 80 + I39 # midiCmd = 90 + midiChannel
SND I38 M01 0 # send(midiCmd inputChannel 0)
ASS LI53 = 0 # voiceInfo[outputVoice] = 0
MAT I35 = I53 + 40 # noteIdx = outputVoice + 64
MAT LI35 = M01 + 80 # voiceInfo[noteIdx] = inputNote + 128
MAT JM01 = JM01 + 80 # noteInfo[note] = noteInfo[inputNote] + $80;
MAT I42 = I42 - 1 # polyphony—-
# // Search empty voice
ASS I33 = FF # oldestEmptyVoice = 255
ASS I31 = 2000 # oldestEmptySeqId = 8192
ASS I00 = 0 # i = 0
IF I00 < I0F +L # while(i < totalVoices) {
MAT I35 = I00 + 40 # noteIdx = I + 64
IF LI35 >= 80 # if (voiceInfo[noteIdx] >= 128) {
IF I31 > LI00 # if (oldestEmptySeqId > voiceInfo[i]) {
ASS I31 = LI00 # oldestEmptySeqId = voiceInfo[i]
ASS I33 = I00 # oldestEmptyVoice = i
END # }
END # }
MAT I00 = I00 + 1 # i++
END # }
# // If there is a free voice, re-enable stolen note
IF I33 != FF # if (oldestEmptyVoice != 255) {
ASS I64 = 0 # maxSeqId = 0
ASS I52 = FF # unhideNote = 255
# // Find youngest non-playing note
ASS I00 = 0 # i = 0
IF I00 < 80 +L # while(i<128) {
IF JI00 >= 80 # if (noteInfo[i] >=128) {
IF KI00 > 0 # if (noteHold[i] > 0) {
IF I64 < KI00 # if (maxSeqId < noteHold[i])
ASS I64 = KI00 # maxSeqId = noteHold[i]
ASS I52 = I00 # unhideNote = i
END # }
END # }
END # }
MAT I00 = I00 + 1 # i++
END # }
# // If unhideNote was found, play on empty voice
IF I52 != FF # if (unhideNote!=255) {
ASS LI33 = I40 # voiceInfo[oldestEmptyVoice] = nextVoiceSeqId
MAT I35 = I33 + 40 # noteIdx = oldestEmptyVoice + 64
ASS LI35 = I52 # voiceInfo[noteIdx] = unhideNote
MAT I40 = I40 + 1 # nextVoiceSeqId++
ASS JI52 = I33 # noteInfo[unhideNote] = oldestEmptyVoice
MAT I39 = I33 % I0D # midiChannel = oldestEmptyVoice % numChannels
MAT I38 = 90 + I39 # midiCmd = 90 + midiChannel
MAT I23 = I52 + 80 # tmpIdx = unhideNote + 128
SND I38 I52 JI23 # send (midiCmd unhideNote noteInfo[tmpIdx])
MAT I42 = I42 + 1 # polyphony++
MAT I43 = I43 - 1 # hidden—-
END # }
END # }
END # }
IF I42 == 0 # if (polyphony == 0) {
SET LB0 S- # show( Label0 -)
END # }
IF I42 != 0 # if (polyphony != 0) {
SET LB0 I42 # show( Label0 polyphony )
END # }
IF I43 == 0 # if (hidden == 0) {
SET LB1 S- # show( Label1 -)
END # }
IF I43 != 0 # if (hidden != 0) {
SET LB1 I43 # show( Label1 hidden )
END # }
8X = XX +B # // Block the initially received NoteOff
END
IF MT == A0 # // ========== Handle Polyphonic Aftertouch ==================
#
IF JM1 < 80 # if (noteInfo[inputNote] < 80) {
IF I0E == 1 # if (numChannels==1) { // send as channel-AT
MAT I38 = JM1 + D0 # midiCmd = noteInfo[inputNote] + $D0
SND I38 M2 # send(midiCmd inputPressure)
END # }
IF I0E != 1 # else { // adapt channel of Poly-AT
MAT I38 = JM1 + A0 # midicmd = noteInfo[inputNote] + $A0
SND I38 M1 M2 # send(midiCmd inputNote inputPressure)
END # }
END # }
END #
IF MT >= B0 # // ========== Handle CCs; PCs, Aftertouch and PitchBend =====
# // forward to all channels
IF MT <= E0 #
ASS I00 = 0 # i = 0
IF I00 < I0E +L # while(i < numChannels) {
MAT I38 = I00 + MT # midiCmd = i + midiCmdWithoutChannel
SND I38 M1 M2 # send( midiCmd inputCCNum inputCCVal )
MAT I00 = I00 + 1 # i++
END # }
END #
END #
# // Block initially received Poly-AT, CCs, PCs, AT, PB and SysEX
AX = XX +B
BX = XX +B
CX = XX +B
DX = XX +B
EX = XX +B
FX = XX +B
# // ========== Initialization ================================
IF LOAD # Internal variables
ASS I40 = 1 # nextVoiceSeqId = 1
ASS I41 = 1 # nextHoldSeqId = 1
ASS I42 = 0 # polyphony = 0
ASS I43 = 0 # hidden = 0
# // Check user supplied numChannels
IF I0D < 2 # if (numChannels < 2) {
ASS I0D = 2 # numChannels = 2
SET LB0 SCH< # show( Label0 CH< )
SET LB1 SERR # show( Label0 ERR )
END # }
IF I0D > 10 # if ( numChannels > 10) {
ASS I0F = 10 # numChannels = 10
SET LB0 SCH> # show( Label0 CH> )
SET LB1 SERR # show( Label0 ERR )
END # }
# // Check user supplied numVoices
IF I0E < 1 # if (numVoices < 1) {
ASS I0D = 1 # numVoices = 1
SET LB0 SVO< # show( Label0 VO< )
SET LB1 SERR # show( Label0 ERR )
END # }
IF I0D > 10 # if ( numVoices > 16) {
ASS I0D = 10 # numVoices = 16
SET LB0 SVO> # show( Label0 VO> )
SET LB1 SERR # show( Label0 ERR )
END # }
MAT I0F = I0D * I0E # totalVoices = numChannels * numVoices
IF I0F > 40 # if ( totalVoices > 64) {
MAT I0E = 40 / I0D # numVoices = 64 / numChannels
MAT I0F = I0D * I0E # totalVoices = numChannels * numVoices
SET LB0 STO> # show( Label0 TO> )
SET LB1 SERR # show( Label0 ERR )
END # }
# // Lower part of voiceInfo, all with sequenceId=0
ASS I00 = 0 # i = 0
IF I00 < 40 +L # while( i<64 ) {
ASS LI00 = 0 # voiceInfo[i] = 0
MAT I00 = I00 + 1 # i++
END # }
# // Upper part of voiceInfo, all with 'empty' note
IF I00 < 80 +L # while( i<128 ) {
ASS LI00 = FF # voiceInfo[i] = FF
MAT I00 = I00 + 1 # i++
END # }
# // Lower part of noteInfo, all with 'empty' note
ASS I00 = 0 # i = 0
IF I00 < 80 +L # while( i<128 ) {
ASS JI00 = FF # noteInfo[i] = FF
MAT I00 = I00 + 1 # i++
END # }
# // Upper part of noteInfo, all with velocity = 0
IF I00 <= FF +L # while( i<=255 ) {
ASS JI00 = 0 # noteIndo[i] = 0
MAT I00 = I00 + 1 # i++
END # }
# // Lower part of noteHold, all with 'empty' seqId
ASS I00 = 0 # i = 0
IF I00 < 80 +L # while( i<128 ) {
ASS KI00 = 0 # noteHold[i] = 0
MAT I00 = I00 + 1 # i++
END # }
# // All NoteOff of on each channel
ASS I00 = 0 # i = 0
IF I00 < 80 +L # while( i< 128) {
SND 90 I00 0 # send( noteOffcmd i 0)
IF I0E > 1 # if (numChannels >= 2) {
SND 91 I00 0 # send(Channel_1_NoteOff- i 0)
END # }
IF I0E > 2 # if (numChannels >= 3) {
SND 92 I00 0 # send(Channel_1_NoteOff- i 0)
END # }
IF I0E > 3 # // ...
SND 93 I00 0
END
IF I0E > 4 # // I would have loved to use nested loops
SND 94 I00 0
END
IF I0E > 5
SND 95 I00 0 # // or loops running up to 16*128 times
END
IF I0E > 6
SND 96 I00 0
END
IF I0E > 7
SND 97 I00 0
END
IF I0E > 8
SND 98 I00 0
END
IF I0E > 9
SND 99 I00 0
END
IF I0E > A
SND 9A I00 0
END
IF I0E > B
SND 9B I00 0
END
IF I0E > C
SND 9C I00 0
END
IF I0E > D
SND 9D I00 0
END
IF I0E > E
SND 9E I00 0
END
IF I0E > F
SND 9F I00 0
END
MAT I00 = I00 + 1
END # }
END