Post by ki on Sept 6, 2018 22:26:06 GMT
Hi,
i developped a script that distributes incomming polyphonic midi into N monophonic midi channels.
One use-case is to setup N instances of a monophonic synth using the same preset receiving on different midi channels from Stream Byter and play this set of mono-synth with a single polyphonic midi source.
There is advanced logic for choosing the channel for new notes, including repetition on the same channel and note-hiding (temporary stealing) and unhiding in the opposite play order when releasing keys.
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.
I already did a lot of testing, but please report bugs or enhancements to the accompanying audiobus forum thread
# 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