Post by uncledave on Jun 19, 2022 17:26:08 GMT
OK, here we go. This version has everything we've discussed: single note output, optional Note Off, CC input for notes and clock, using local BPM value for delay. I've tested this with all combinations of options and it seems to work. I've tried to include lots of comments, so you should be able to look through it and underatand what it is doing. The options default to the original implementation: notes output on channel 13, no Note Off. You can configure each instance (in AUM or MidiFire) as you wish, and save the session or preset in that mode.
#TickDelayLarge04
# Stores incoming notes 0..5 in a buffer, with a delay of a certain
# number of MIDI clock ticks. Clock messages are input, and each
# note is sent when its delay expires. Individual delays can be set
# for each note, and a master delay is also applied.
# There are several optional modes, enabled on the second GUI page.
# Tap the GUI icon (circle with sliders) in the lower right to cycle through
# Page 1, Page 2, and program text.
# Note that, if BPM mode is enabled, the clock input is not expected and
# should not be provided. Notes (and clock) will continue to work when
# CCs are enabled. CCs are just converted to notes and clock when they
# arrive.
# Slider 7 displays current buffer usage. Move it right and left
# after running to see stats in the monitor log. Note that there is no
# buffer usage in BPM mode. Notes are sent immediately, with delay.
# Options:
# - Output one note for all. Input note still selects the delay.
# - Delay or ignore Note Off.
# - MIDI channel for output.
# - Use CCs, B0 for clock, B1 for note on, B2 for off.
# - Use BPM to generate delay, replacing clock.
# - Debug mode displays buffer usage on slider.
# Update for note off:
# Only need 1 bit to distinguish off and on. Pack note and
# velocity into 14 bits, use bit 15 (4000) for note off.
# Uses the W array, with 2048 unsigned 16-bit words, allowing a
# buffer of 1024 notes. SB limits loops to 128 iterations, so some
# trickery is required to handle large amounts of data.
# I have tested this by sending 6-note chords from my keyboard as fast
# as I could, into a whole note delay at 120 bpm. The highest number of
# active entries was 150, and the highest cell used was 340, corresponding
# to 170 total entries. This was delaying both Note On and Note Off
# messages.
If load
Alias $0 testOffset # should be 0 for normal use
Alias $1024 maxCount # max number of entries
Set Name Delay
# Screen Controllers ———————————————————————————
# Page 1 .......
# Delay times are selected by an integer ranging from 0..11, indexing a table
# of tick counts for typical note values. When you move a slider, the
# selected note and number of ticks is displayed in the labels at the
# bottom.
Alias 6 maxNote
Alias 7 maxSlider
Alias $11 maxSelect
# Note that the Qi are published as AUv3 parameters (scaled 0..127)
# so you could control them externally that way. Or, of course, you
# could implement CCs to adjust them directly.
Set Q0 BD 0 maxSelect
Set Q1 HH 0 maxSelect
Set Q2 SD 0 maxSelect
Set Q3 RD 0 maxSelect
Set Q4 CL 0 maxSelect
Set Q5 TM 0 maxSelect
Set Q6 Master_Select 0 maxSelect
# you can manually set theCount to zero to reset buffer
Alias Q7 theCount
Alias 7 indCount
Set Q7 Count/Reset 0 maxCount
# Note that the script requires that Q 0..5 contain the delay for notes
# 0..5. Do not change this.
# Page 2 ........
# These aliases are used in the code, so it is generic. We can revise
# the screen layout here by changing the Qi, without affecting the
# rest of the code.
Alias Q8 theUseSingleNote # enable single note output
Alias Q9 theUseNoteOff # include note off
Alias 9 indUseNoteOff
Alias QA theUseCC
Alias A indUseCC
Alias QB theUseBPM
Set Q8 Use_SingleNote +Toggle
Set Q9 Use_Note_Off +Toggle
Set QA Use_CC +Toggle
Set QB Use_BPM +Toggle
Alias QC theNoteValue # single note value to use
Alias QD theOutChannel # output MIDI channel number
Alias D indOutChannel
Alias QF theDebugMode
Set QC Note_Value 0 7F +Menu
Set QD Out_Channel 1 10 +Menu
Set QE +Hide
Set QF Debug_Mode +Toggle
Ass Q0 = 0 0 0 0 0 0 0 0 +P
Ass Q8 = 0 0 0 0 0 D 0 1 +P
Set SLIDER_DISPLAY 1
# End Screen Controllers —————————————————————————
# Returns the number of ms to delay for given number of ticks.
# SB BPM is beats in 100 min, 6000 sec, or 6000000 ms
# ppqn * BPM is number of ticks in 100 min
# Dividing these gives ms per tick. Multiply by number of ticks to
# delay gives delay in ms. Division step is last for maximum precision.
# Result is rounded to nearest integer ms.
# Using 32-bit P registers for these large numbers.
Alias $24 ppqn # number of ticks in a quarter note
Sub ComputeDelayTime pResult pNumbTicks
Mat P00 = $6000000 * pNumbTicks # numerator
Mat P01 = ppqn * BPM # denominator
Mat P02 = P01 / 2
Mat P00 = P00 + P02 # add half divisor for rounding
Mat P00 = P00 / P01
Ass pResult = P00
End
# delay values in clock ticks, 24 ppqn (pulses per quarter note)
# array indices 0 to maxSelect
Ass K0 = 0 6 8 9 $12 $16 $18 $24 $36 $48 $72 $96 +P
# replacements for CCs on chs 0, 1, and 2
Ass K10 = F8 90 80 0 0 0 0 0 +P
Alias 10 mapTableBase # start of this table
# Delays in ticks: Whole 96, Half dot 72, Half 48, Qtr dot 36, Qtr 24,
# Qtr3 16, 8th dot 18, 8th 12, 8th3 8, 16th dot 9, 16th 6.
# this tedious method is the only way to convert a number into an
# arbitrary string
Sub SayTickDelay pTicks
Set LB1 pTicks +D
If pTicks == 0
Set LB0 S0
End
If pTicks == $96
Set LB0 SWhole
End
If pTicks == $72
Set LB0 SDotted_Half
End
If pTicks == $48
Set LB0 SHalf
End
If pTicks == $36
Set LB0 SDotted_Qtr
End
If pTicks == $24
Set LB0 SQtr
End
If pTicks == $16
Set LB0 SQtr_Triplet
End
If pTicks == $18
Set LB0 SDotted_8th
End
If pTicks == $12
Set LB0 S8th
End
If pTicks == $8
Set LB0 S8th_Triplet
End
If pTicks == $9
Set LB0 SDotted_16th
End
If pTicks == $6
Set LB0 S16th
End
End # SayTickDelay
# returns the total delay for the given note in pResult
# Delay is the sum of the master delay, and the delay for the given note.
Sub GetDelay pResult pNote
# Master delay
Ass I20 = maxNote
Ass I20 = QI20 # get Master slider value
Ass pResult = KI20 # index the table of ticks
Mat I20 = pNote - testOffset # allows using normal notes to test
If I20 < maxNote
# add delay for the note
Ass I20 = QI20
Mat pResult = pResult + KI20
End
End
# W000..W800 is a buffer of 1024 2-word entries (800 is $2048).
# Each entry contains the delay (counting down) and the note
# and its velocity, packed into one word. Times are counted down
# on each tick, and each note is sent when its count reaches 0.
# Notes are added to the first free slot. A Note Off is marked by
# the 4000 bit in the note word.
# buffer parameters
Alias 2 blockSize # number of words in each entry
Alias 4000 noteOffFlag # set in notes for note off
# control values in IFx registers
Alias IF0 firstFree # position of first free slot
Alias IF1 currCount # number of active entries
Alias IF2 maxPos # last array index+1
Alias IF3 currMax # position of entry off the end
Alias IF4 countMax # highest count seen
Alias IF5 maxMax # highest buffer position used
Alias IF6 sendNoteCommand # note on and channel
Alias IF7 enableNoteOff # message type for note off
Alias IF8 enableCCInput # message type for CC input
Ass IF0 = 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Mat maxPos = maxCount * blockSize
# combine selected MIDI channel with note on command
Sub UpdateChannel
Mat sendNoteCommand = theOutChannel - 1
Mat sendNoteCommand = 90 + sendNoteCommand
End
# These enable flags are either zero or the correct message type.
# This makes the event handling logic really simple, since we can
# check the message type and the enable flag in one test.
Sub UpdateNoteOff
Mat enableNoteOff = 80 * theUseNoteOff
End
Sub UpdateCCs
Mat enableCCInput = B0 * theUseCC
End
# sends a note on or off message, delayed by the number of ticks
Sub DelayNote pTicks pMode pNote pVel
ComputeDelayTime I20 pTicks # convert ticks to ms delay time
Ass I23 = sendNoteCommand
If pMode == 80
Mat I23 = I23 ^ 10 # converts note on to note off
End
Send I23 pNote pVel +DI20 # send the delayed note
End # DelayNote
# returns the first free slot. This will normally be firstFree, unless
# multiple notes arrive between ticks.
Sub FindFreeSlot pResult
Ass pResult = firstFree
Ass I20 = firstFree
If WI20 > 0
# here when firstFree is taken.
Ass pResult = maxPos # assume failure
Mat I20 = I20 + blockSize # advance, since first item is taken
# search for free entry
# double loops avoid SB limitation of 128 max iterations.
While I20 < maxPos
Mat I21 = maxPos - I20 # number of cells remaining
If I21 > 7F # limit to 127
Ass I21 = 7F
End
While I21 > 0 # this inner loop limited to 127 steps
Mat I21 = I21 - 1
If WI20 == 0
Ass pResult = I20
Ass I20 = maxPos # exit loops on success
Ass I21 = 0
End
Mat I20 = I20 + blockSize # advance to next block
End # inner loop
End # while
End
End # FindFreeSlot
# adds the given note to the table, with the delay time
# Items are added to open slots, in no particular order. This keeps the
# table relatively tightly packed, shortening the tick update loop.
Sub AddNoteTable pTicks pMode pNote pVel
FindFreeSlot I18
If I18 < maxPos # only add if space available
# here with table slot to use
If currMax == I18 # update maximum when extending buffer
Mat currMax = currMax + blockSize
If maxMax < currMax # remember highwater level
Ass maxMax = currMax
End
End
# new entry fills position I18 and I18+1 of W
Ass WI18 = pTicks # delay in ticks
Mat I18 = I18 + 1
# Note and velocity packed in one word
Mat I19 = pNote * 80 # note value
Mat I19 = I19 | pVel # velocity
If pMode == 80
Mat I19 = I19 | noteOffFlag # add flag for note off
End
Ass WI18 = I19 # store note, velocity, and flag
Mat currCount = currCount + 1 # update active entry count
If countMax < currCount # remember max number of active items
Ass countMax = currCount
End
If theDebugMode > 0
Ass theCount = currCount # update display slider
# If currCount > 7F
# Log Add_Count currCount +D
# End
End
Else
# here when no slots available
Set LB1 SFull # displays "Full" if could not add note
Log Buffer_Full # log msg appears in monitor
End
End # AddNoteTable
# processes one note, adding it to the table or just delaying it
# pMode is 90 for note on, 80 for note off
Sub ProcessNote pMode pNote pVel
GetDelay I11 pNote
If I11 > 0 # skip if delay is 0
Set LB0 pNote +D # show note and delay when adding
Set LB1 I11 +D
# convert all notes to single value
If theUseSingleNote == 1
Ass I12 = theNoteValue
Else
Ass I12 = pNote
End
# use simple delay if BPM enabled
If theUseBPM == 1
DelayNote I11 pMode I12 pVel
Else
AddNoteTable I11 pMode I12 pVel
End
End
End # ProcessNote
# sends the current note. pWhere is index of the count.
# Note that division by a power of 2 works like shift in SB.
Sub SendDelayedNote pWhere
Mat I20 = pWhere + 1 # access note data
Mat I23 = WI20 / noteOffFlag # access note off flag
Mat I23 = I23 * 10 # value is 0 or 10
# this will change 90 to 80 for note off, sneaky but concise
Mat I23 = sendNoteCommand ^ I23
Mat I21 = WI20 / 80 # unpack note and velocity
Mat I21 = I21 & 7F # mask off the flag
Mat I22 = WI20 & 7F
Send I23 I21 I22 # send note on or off message
End # SendDelayedNote
# decrement the count for each active entry, sending notes
# when their delay expires. Called for each clock tick.
# Also identifies firstFree slot for next insertion.
Sub UpdateTicks
Ass I10 = 0 # walks the buffer from the beginning
Ass I11 = currCount # I11 counts down the active entries
Ass I12 = 0 # enables updating Count slider
Ass I14 = 0 # set when firstFree has been found
Ass firstFree = currMax
# loop over active buffer entries
# note that I11 only changes for active entries, so inactive
# entries off the end are ignored automatically
While I11 > 0
# this double loop is required in case of more than 128 entries.
# SB loops are limited to 128 iterations.
Ass I13 = 7F
While I13 > 0 # inner loop runs only 127 iterations
Mat I13 = I13 - 1
If WI10 > 0 # skip notes already sent
# here with an active entry
Mat I11 = I11 - 1 # decrement active counter
If I11 == 0 # passing last active entry exits both loops
Ass I13 = 0
End
# decrement count for this note
Mat WI10 = WI10 - 1 # decrease tick count
If WI10 == 0 # first time count reaches 0
SendDelayedNote I10
Mat currCount = currCount - 1
Ass I12 = theDebugMode # remember change
End
End # processing active entry
# I14 is zero while we're looking for the first free position
# then it's set very large, so this test always fails. And
# WI10 is zero for a free slot. This happens after sending,
# so the slot may be one that was just freed.
If WI10 == I14
Ass firstFree = I10 # save position of first free slot
Ass I14 = FFFF # this will not happen again
End
Mat I10 = I10 + blockSize # advance through buffer
End # inner while
End # while
# remember position beyond last active entry
Ass currMax = I10
# update count display if anything has changed
If I12 > 0
Ass theCount = currCount
# If currCount > 7F
# Log Scan_Count currCount +D
# End
End
End # UpdateTicks
# dump active entries for diagnosis
# also shows peak levels seen since reset
Sub DumpActive
Ass I10 = 0
Log Dump_Count currCount +D
Log countMax countMax +D
Log maxMax maxMax +D
Ass I11 = currCount
While I11 > 0
Ass I14 = 7F
While I14 > 0
Mat I14 = I14 - 1
If WI10 > 0
Mat I11 = I11 - 1
If I11 <= 0
Ass I14 = 0
End
Mat I13 = I10 + 1
Mat I13 = WI13 / 80
Mat I12 = 100 * WI10
Mat I12 = I12 + I13
# log will show 2 hex nybbles with time and note
Log Buffer I12
End
Mat I10 = I10 + blockSize
End
End
End # DumpActive
# maximum loop count is 128, so need this double logic.
# two nested loops is good up to 16384.
Sub ClearBuffer
Ass I10 = 0
While I10 < maxPos
Mat I11 = maxPos - I10
If I11 > 80
Ass I11 = 80
End
# inner loop advances I10 by $128 or less.
While I11 > 0
Ass WI10 = 0
Mat I10 = I10 + 1
Mat I11 = I11 - 1
End # inner loop
End # while
Log Last_Pos I10 +D
End
# initialize all data
Sub ResetBuffer
Set LB0 SReset
DumpActive
Ass theCount = 0
Ass firstFree = 0
Ass currCount = 0
Ass currMax = 0
Ass countMax = 0
Ass maxMax = 0
UpdateChannel
UpdateNoteOff
UpdateCCs
ClearBuffer
End
# handle slider and button changes. pIndex is the slider index
# from the internal event message.
Sub UpdateControls pIndex
If pIndex < maxSlider # M3 is the index of the changed Qi
# displays the delay based on the selection
Ass I00 = QM3
SayTickDelay KI00
End
If pIndex == indCount
# move slider up and back to 0 to trigger reset
If theCount == 0
ResetBuffer
End
End
If pIndex == indOutChannel
UpdateChannel
End
If pIndex == indUseNoteOff
UpdateNoteOff
End
If pIndex == indUseCC
UpdateCCs
End
End
ResetBuffer # initialize data on load
End # Initialization ———————————————————————————
# enableCCInput is zero if we're not using CCs, B0 if we are
If MT == enableCCInput
If MC < 3
# replace CC on ch 0..2 with clock, note msgs
Mat I00 = MC + mapTableBase
Ass M0 = KI00
End
End
If M0 == F8 # process clock first, since it is most frequent
If currCount > 0 # check count first to save time
UpdateTicks
End
Block
Exit # exit here for quickest handling of F8
End
If MT < 10 # ignore this debugging code; it will never run
If M1 == $48 # using low C to trigger diagnostic output
If MT == 90
DumpActive
End
Block
End
End
If MT == 90 # process Note On
# you could modify incoming values here as needed
Ass I00 = M1 M2
ProcessNote MT I00 I01
Else
If MT == enableNoteOff # value is 0 or 80
ProcessNote MT M1 M2
End
End
# these events only happen when a control is changed
If M0 == F0 7D 01 # handle control change
UpdateControls M3
End
# all input messages blocked here. Only delayed notes are sent.
Block