# Distribute incomming polyphonic midi to N outgoing mono midi channels # # Version: 1 / 06.09.2018 # Author: -ki https://forum.audiob.us/profile/_ki # # ============================= # User Setup of polyphony distribution: # Number of output midi channels/mono synth instances ASS I0F = 4 # BEWARE - this number is specified in hex, so use 0A for 10 dec, 0B for 11, # 0C for 12, 0D for 13, 0E for 14, 0F for 15. And 10 for 16 dec is the # maximum of supported midi channels. The minimum is 2. # ============================= # Documentation # # This script allows to use N mono synth instances (using the same preset) to be # played as a single polyphonic synth. # # 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 into channel Aftertouch and send to the note’s channel # # The Streambyter Labels shows # - LEFT: current number of playing channels # - RIGHT: current hidden (hold) notes that will be played if a channel is available # List of variables # ============================= # # J00-J7F noteInfo[note] for channel of each note # 00-0F channel if note is playing # 80-8F not playing, last used channel + 80 # FF not playing, no channel 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-L0F channelInfo[channel] sequence id to manage LRU of channels # 00 unused # >01 sequenceId for channel, based on nextChannelSeqId # # L10-L1F channelInfo[channel+16] for the note in a channel # 00-7F is note of channel # 80-FF is last note+80 of channel, channel not playing # # L20-L2F channelInfo[channel+32] used in re-indexing of channelInfo[00-0F] # # # I00 i Loopindex # I0F maxChannel User specified number of channels/mono synth instances # # 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 oldestTotalSeqId Oldest sequenceId for any of the channels # I33 oldestEmptyChannel Channel number of oldest empty channel, or FF # I34 oldestTotalChannel Channel number of oldest channel # I35 noteIdx Index into note part of channelInfo (ie i+16) # I37 oldNote Previous playing note (to send a NoteOff in hiding) # I38 midiCmd Computed midi command (usually outputChannel+CMD) # # I40 nextChannelSeqId SequenceId for next used channel # I41 nextHoldSeqId SequenceId for next pressed note # I42 polyphony Number of playing channels # I43 hidden Number of hidden notes still hold # # I50 useOldChannelFlag Flag if the old channel of a note is available # I52 unhideNote Note number of note to be unhidden, or FF if none # I53 outputChannel The computed output channel 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 channelInfo for i # I72 ptrB Offset to tmpIndex table part of channelInfo 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 channels 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 channels. Depending on what is played, # there will be gaps in the sequence, because new notes/channels 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 channel of note is free ASS I50 = 0 # useOldChannelFlag = 0 IF JM01 != FF # if ( noteInfo[inputNote] != 255 ) MAT I23 = JM01 - 70 # tmpIdx = noteInfo[inputNote] - 70 IF LI23 >= 80 # if (channelInfo[tmpIdx] >=128 ) MAT I53 = JM01 - 80 # outputChannel = noteInfo[inputNote] - 80 ASS I50 = 1 # useOldChannelFlag = 1 END END # // When not using the old channel IF I50 == 0 # if (useOldChannelFlag == 0) # // Search for oldes channels ASS I31 = 2000 # oldestEmptySeqId = 8192 ASS I32 = 2000 # oldestTotalSeqId = 8192 ASS I33 = FF # oldestEmptyChannel = 255 ASS I34 = FF # oldestTotalChannel = 255 ASS I00 = 0 # i = 0 IF I00 < I0F +L # while( i < maxChannel ) MAT I35 = I00 + 10 # noteIdx = i + 16 IF LI35 >= 80 # if (channelInfo[noteIdx] >= 128) IF I31 > LI00 # if (oldestEmptySeqId > channelInfo[i]) ASS I31 = LI00 # oldestEmptySeqId > channelInfo[i] ASS I33 = I00 # oldestEmptyChannel = i END END IF I32 > LI00 # if (oldestTotalSeqId > channelInfo[i]) ASS I32 = LI00 # oldestTotalSeqId > channelInfo[i] ASS I34 = I00 # oldestTotalChannel = i END MAT I00 = I00 + 1 # i++ END ASS I53 = I34 # outputChannel = oldestTotalChannel IF I33 != FF # if (oldestEmptyChannel != 255) ASS I53 = I33 # outputChannel = oldestEmptyChannel END # // Check if channel is already playing MAT I35 = I53 + 10 # noteIdx = outputChannel + 16 IF LI35 < 80 # if (channelInfo[noteIdx] < 128) # // Stop the old note ASS I37 = LI35 # oldNote = channelInfo[noteIdx] MAT I38 = 80 + I53 # midiCmd = $80 + outputChannel SND I38 I37 0 # send(midiCmd oldNote 0) ASS JI37 = FF # noteInfo[oldNote] = 255 ASS LI53 = 0 # channelInfo[outputChannel] = 0 MAT I42 = I42 - 1 # polyphony— MAT I43 = I43 + 1 # hidden++ END END # // Check for re-indexing of channelInfo IF I40 > 2000 # if (nextChannelSeqId > 4096) # // Prepare index table ASS I00 = 0 # i = 0 IF I00 < I0F +L # while( i < maxChannel ) MAT I23 = I00 + 20 # tmpIdx = i + 32 ASS LI23 = I00 # channelInfo[tmpIdx] = i MAT I00 = I00 + 1 # i++ END # // Single Loop Sort of seqId indices ASS I00 = 0 # i = 0 MAT I70 = I0F - 1 # loopEnd = maxChannel -1 IF I00 < I70 +L # while(i < loopEnd ) MAT I71 = I00 + 20 # ptrA = i + 32 MAT I72 = I00 + 21 # ptrB = i + 33 ASS I73 = LI71 # idxA = channelInfo[ ptrA ] ASS I74 = LI72 # idxB = channelInfo[ ptrB ] ASS I77 = 1 # elseFlag = 1 IF LI74 < LI73 # if ( channelInfo[idxB] < channelInfo[idxA] ) ASS I75 = LI71 # tmpVal = channelInfo[ptrA] ASS LI71 = LI72 # channelInfo[ptrA] = channelInfo[ptrB] ASS LI72 = I75 # channelInfo[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 < maxChannel) MAT I71 = I00 + 20 # ptrA = i + 32 ASS I73 = LI71 # idxA = channelInfo[ ptrA ] IF LI73 != 0 # if ( channelInfo[idxA] != 0) ASS LI73 = I24 # channelInfo[idxA] = newSeqId MAT I24 = I24 + 1 # newSeqId++ END MAT I00 = I00 + 1 # i++ END ASS I40 = I24 # nextChannelSeqId = 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] = outputChannel MAT I23 = M01 + 80 # tmpIdx = inputNote + 80 ASS JI23 = M02 # noteInfo[tmpIdx] = inputVelocity ASS LI53 = I40 # channelInfo[outputChannel] = nextChannelSeqId MAT I35 = I53 + 10 # noteIdx = outputChannel + 16 ASS LI35 = M01 # channelInfo(noteIdx) = inputNote ASS KM01 = I41 # noteHold[note] = nextHoldSeqId; MAT I38 = 90 + I53 # midiCmd = $90 + outputChannel SND I38 M01 M02 # send(midiCmd inputNote inputVel) MAT I41 = I41 + 1 # nextHoldSeqId++; MAT I40 = I40 + 1 # nextChannelSeqId++ 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 # outputChannel = noteInfo[inputNote] MAT I38 = I53 + 80 # midiCmd = outputChannel + $80; SND I38 M01 0 # send(midiCmd inputChannel 0) ASS LI53 = 0 # channelInfo[outputChannel] = 0 MAT I35 = I53 + 10 # noteIdx = outputChannel + 16 MAT LI35 = M01 + 80 # channelInfo[noteIdx] = inputNote + 128 MAT JM01 = JM01 + 80 # noteInfo[note] = noteInfo[inputNote] + $80; MAT I42 = I42 - 1 # polyphony—- # // Search empty channel ASS I33 = FF # oldestEmptyChannel = 255 ASS I31 = 2000 # oldestEmptySeqId = 8192 ASS I00 = 0 # i = 0 IF I00 < I0F +L # while(i < maxChannel) MAT I35 = I00 + 10 # noteIdx = I + 16 IF LI35 >= 80 # if (channelInfo[noteIdx] >= 128) IF I31 > LI00 # if (oldestEmptySeqId > channelInfo[i]) ASS I31 = LI00 # oldestEmptySeqId = channelInfo[i] ASS I33 = I00 # oldestEmptyChannel = i END END MAT I00 = I00 + 1 # i++ END # // If there is a free channel, re-enable stolen note IF I33 != FF # if (oldestEmptyChannel != 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 channel IF I52 != FF # if (unhideNote!=255) ASS LI33 = I40 # channelInfo[oldestEmptyChannel] = nextChannelSeqId MAT I35 = I33 + 10 # noteIdx = oldestEmptyChannel + 16 ASS LI35 = I52 # channelInfo[noteIdx] = unhideNote MAT I40 = I40 + 1 # nextChannelSeqId++ ASS JI52 = I33 # noteInfo[unhideNote] = oldestEmptyChannel MAT I38 = 90 + I33 # midiCmd = $90 + oldestEmptyChannel 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 ================== # // map to channel aftertouch IF JM1 < 80 # if (noteInfo[inputNote] < 80) MAT I38 = JM1 + D0 # midiCmd = noteInfo[inputNote] + $D0 SND I38 M2 # send(midiCmd inputPressure) 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 < I0F +L # while(i < maxChannel) 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 # nextChannelSeqId = 1 ASS I41 = 1 # nextHoldSeqId = 1 ASS I42 = 0 # polyphony = 0 ASS I43 = 0 # hidden = 0 # // Check user supplied maxChannel IF I0F < 2 # if (maxChannel < 2) ASS I0F = 2 # maxChannel = 2 SET LB0 SCH< # show( Label0 CH< ) SET LB1 SERR # show( Label0 ERR ) END IF I0F > 10 # if ( maxChannel > 10) ASS I0F = 10 # maxChannel = 10 SET LB0 SCH> # show( Label0 CH> ) SET LB1 SERR # show( Label0 ERR ) END # // Lower part of channelInfo, all with sequenceId=0 ASS I00 = 0 # i = 0 IF I00 < 10 +L # while( i<16 ) ASS LI00 = 0 # channelInfo[i] = 0 MAT I00 = I00 + 1 # i++ END # // Upper part of channelInfo, all with 'empty' note IF I00 < 20 +L # while( i<32 ) ASS LI00 = FF # channelInfo[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 # // Upper part of noteHold, used as tmpArray # // 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 I0F > 1 # if (maxChannel >= 2) SND 91 I00 0 # send(Channel_1_NoteOff- i 0) END IF I0F > 2 # if (maxChannel >= 3) SND 92 I00 0 # send(Channel_1_NoteOff- i 0) END IF I0F > 3 # // ... SND 93 I00 0 END IF I0F > 4 # // I would have loved to use nested loops SND 94 I00 0 END IF I0F > 5 SND 95 I00 0 # // or loops running up to 16*128 times END IF I0F > 6 SND 96 I00 0 END IF I0F > 7 SND 97 I00 0 END IF I0F > 8 SND 98 I00 0 END IF I0F > 9 SND 99 I00 0 END IF I0F > A SND 9A I00 0 END IF I0F > B SND 9B I00 0 END IF I0F > C SND 9C I00 0 END IF I0F > D SND 9D I00 0 END IF I0F > E SND 9E I00 0 END IF I0F > F SND 9F I00 0 END MAT I00 = I00 + 1 END END