|
Post by mikefloutier on Mar 5, 2022 17:44:51 GMT
Many thanks Dave, I’ll look into it and give it go.
Doubtless I’ll be back with some specific SB related questions IDC.
|
|
|
Post by uncledave on Mar 6, 2022 18:19:39 GMT
Here's what I promised about using the MIDI Clock messages. You can use these to trigger events just before or just after the start of a bar, which I think you wanted for your drum patterns. It's just a demo, but it has all the functionality you'd need. Attachments:ClockDemo.zip (86.88 KB)
|
|
|
Post by mikefloutier on Mar 6, 2022 18:57:01 GMT
Here's what I promised about using the MIDI Clock messages. You can use these to trigger events just before or just after the start of a bar, which I think you wanted for your drum patterns. It's just a demo, but it has all the functionality you'd need. Thanks so much Dave! I’m just finishing up an attempt to modify the script to cater for my “story”. However, without the ability to interrogate the transport position (which I’m assuming your demo illustrates) it will rely on me Tapping or Holding at the right moment, so not ideal. I will finish this current attempt first though, as it’s good training. Also, it might do my head in to add additional info to the mix. 😂
|
|
|
Post by uncledave on Mar 7, 2022 14:53:54 GMT
Hi Mike, You should hold the presses on needing Mozaic. Our mentor Nick has described what he thinks Mozaic is doing, and how we can match that in SB. So I'll put that together in a bit and post it.
|
|
|
Post by mikefloutier on Mar 7, 2022 16:04:24 GMT
Hi Mike, You should hold the presses on needing Mozaic. Our mentor Nick has described what he thinks Mozaic is doing, and how we can match that in SB. So I'll put that together in a bit and post it. Many thanks Dave, that sounds great!
|
|
|
Post by uncledave on Mar 7, 2022 19:49:43 GMT
Here's the updated code for generating clock pulses from inside SB. It works by sending an internal message delayed by one clock interval. I've tried to minimize the operstions between receiving that message and sending the next one, to keep the clock interval as accurate as possible. It turned out to be a little tricky because the desired ms between ticks really needs to be a decimal fraction, and SB can only use integers. So I had to remember an old trick that keeps track of the shortfall and inserts an extra ms whenever possible. Try it in Audiobus with AR-909 to get a feel for how it works. There are really two parts to the code, one part that generates the clock ticks, and the other part that counts the ticks and converts them into useful info. You will be able to revise or adapt the second part to do what you need with respect to the song. #TickDemo # Converts metronome pulses into beats and bars. # Using 4 pulses per quarter note (ppqn) to limit computing. # For 4 beats in a bar, this results in 16 pulses per # bar. The script runs the clockCount from 0 to 15, so you # can determine exactly where you are in a bar. # Load AR-909 with a reliable pattern, hit Play and watch the # SB labels to confirm that this works.
If load Alias $4 ppqn # ticks/quarter note Alias 4 beatLimit # beats in a bar Alias $8 barLimit # bars in a phrase (arbitrary) # You could make barLimit 8 for a phrase.
# this internal SysEx message uses an arbitrary index (42) # to ensure uniqueness. define tick_event F0 7D 42 F7
# using J00 to J06 for these. Change them to avoid conflicts. Alias J00 clockCount # clock count runs 0..15 Alias J01 beatCount # beat count runs 0..3 Alias J02 barCount # bar count runs 1 to whatever Alias J03 clockLimit # calculated pulses per bar Alias J04 ticking # set when transport running Alias J05 tickTime # nominal ms between ticks Alias J06 nextTickTime # delay to use for next tick Ass J00 = 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
# these are large numbers, so we use P00 array. Alias P00 tickDiv Alias P01 tickRem Alias P02 tickAccum
Mat clockLimit = ppqn * beatLimit
# this is just demo code identifying beats and bars from the ticks. # replace with application code as required. Sub ProcessTickEvent Mat clockCount = clockCount + 1 If clockCount >= clockLimit Ass clockCount = 0 End
# this makes the Beat pulse. It's just for show. 2 is half ppqn. Mat I00 = clockCount + 2 Mat I00 = I00 % ppqn If I00 == 0 # blank display halfway thru beat Set LB0 S_ End
# update beat and bar counters # % is the modulo operator, gives the remainder after division, # so here, I00 runs from 0 to 3. Mat I00 = clockCount % ppqn If I00 == 0 # declare new beat every ppqn pulses Mat beatCount = beatCount + 1 Set LB0 SBeat End
If beatCount >= beatLimit # declare new bar Ass beatCount = 0 Mat barCount = barCount + 1 If barCount > barLimit # bar number is 1-based, like normal Ass barCount = 1 End Set LB0 SBar # Bar overwrites Beat Set LB1 barCount +D End End # ProcessTickEvent
# Calculates the delay time for the next tick. The tickAccum accumulates # the time lost by using an integer number of ms for the delay. Every # time it equals or exceeds the tickDiv, we can add an extra ms to the # nextTickTime. Sub UpdateTickTime Calc tickAccum = tickAccum + tickRem If tickAccum >= tickDiv Calc tickAccum = tickAccum - tickDiv Calc nextTickTime = tickTime + 1 Else Ass nextTickTime = tickTime End End
# Issues delayed mesage as early as possible, to keep time. sub issue_tick # message will arrive after one tick delay Ass I00 = nextTickTime # using I00 keeps aliases clean send tick_event +I +DI00 # prepare for next tick UpdateTickTime # do normal tick stuff ProcessTickEvent end
# initialize tick parameters from the system bpm # This locks in the bpm value when you hit play. Saves the time required # to recompute the delay on every tick. Sub PrepareTickParameters # SB bpm value is beats in 100 minutes (6000 sec) # 6,000,000/bpm is ms/beat, divide by ppqn to get ms/tick Calc tickDiv = bpm * ppqn # ticks in 6000 sec calc tickTime = $6000000 / tickDiv # base tick time in ms # tickRem/tickDiv is the fractional ms that we cannot use Calc tickRem = $6000000 % tickDiv # init accumulator and time for first tick Ass tickAccum = tickRem Ass nextTickTime = tickTime End
Sub Reset # force all counts to overflow on first pulse Ass clockCount = clockLimit Ass beatCount = beatLimit Ass barCount = barLimit PrepareTickParameters Set LB1 S_ Set LB0 S_ End
End # Initialization ——————————————————————————
# this receives the delayed message and immediately sends another # It should be the first event handler, since it happens very often. # Exit minimizes execution time. Setting ticking=0 stops the cycle. if M0 == tick_event if ticking == 1 issue_tick end Exit end
# SB provides these internal messages on transport start and stop. # We use them to start on the right foot. If M0 == F0 7D 01 # handle system events If M3 == 7A # Start Reset Ass ticking = 1 issue_tick End If M3 == 7C # Stop Reset Ass ticking = 0 End End
Edit: After some provocation from user @wim in the Audiobus forum, I've tested this on some longer runs. The longest run was 85 minutes at 120 bpm and 24 ppqn. These values were chosen to ensure a large number of ticks would be required. The beat and bar were still in sync with the Audiobus beat ticker and an AR-909 pattern. So it should be stable in normal running. It would be wise to restart the transport between songs to avoid any possible build up of error.
I'm going to repackage this code to be a little more modular, so it will be easier to merge it into an existing script.
|
|
|
Post by mikefloutier on Mar 9, 2022 10:50:21 GMT
Thanks Dave, I’ll wait for it, still “perfecting” my analogue version.
By the way, am I right in thinking that +D delays can only be applied to Send and not to other rules, such as subroutine calls?
|
|
|
Post by uncledave on Mar 9, 2022 11:14:24 GMT
Thanks Dave, I’ll wait for it, still “perfecting” my analogue version. By the way, am I right in thinking that +D delays can only be applied to Send and not to other rules, such as subroutine calls? Yes. +Dnnn is only for Send. +D (no nnn) means "Decimal" when applied to Set label and Log. You should hold fire on my clock stuff. I'll be posting a more modular version which will be easier to drop into an existing script. (And it still works after a pretty comprehensive rehash!)
|
|
|
Post by mikefloutier on Mar 9, 2022 17:26:36 GMT
Hi Dave, I love this, especially the use of the labels. Looking forward to the modular version. Definitely going to incorporate it into my AR-909 handler once I understand it (May take me a while but I’m enjoying the process 😂).
I thought this would give you a laugh, it’s my analogue version (to your digital) already incorporated. Having said that, you deserve 99% of the credit. All the foolishness is down to me.
# AR-909 Pattern selector - using a single sustain pedal.
# patterns (pat) selected with C-1...F-1 notes ($12...15) # pat 1 is Main, pat 2 is Main fill, pat 3 is Alt, pat 4 is Alt fill. # there are two modes of operation # 1. On click - seq is pats 1,2,3 (or 3,4,1) # 2. On hold - seq is pats 1,2,1 (or 3,4,3) # timing is not important in 2. as 1st halves of pats 1&2 (3&4) are identical... # ...although fill won’t start until hold # in 1. all of pats 1&3 (2&4) are different, so click must be half way through... # ...to ensure good timing (ie. switching triggered just before beat 3) # the D+ figure is based on 100bpm and can be tied to the host’s BPM by... # ...dividing it by the current BPM (taken as BP/100, ignoring modulus) # NB. Patterns 1-4 in AR-909 are 0-3 in the script # The pattern switching timings are highly inexact and rely on the musician... # ...initiating them at the correct time. This may seem careless, however, # ...musicians are always doing this sort of thing; it’s where art meets science
If load Set name selpat Alias $12 patternBase # first note number for patterns Alias 3 maxPattern # patterns numbered 0..3 # using the J array for saved values, I for temp values Alias J01 currentPattern Alias J02 theDelay # delay in ms needed for auto pat switch Ass J00 = 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 Alias I10 temp0
Sub SelectPattern thePattern Delay # set pattern note number & delay Mat temp0 = patternBase + thePattern Snd 91 temp0 3F +DDelay # pattern is switched, on time Snd 81 temp0 0 +D500 End Sub UpdatePattern # 1st update is always +1, ie. to a fill Mat currentPattern = currentPattern + 1 # 2nd can be either + or - (if a hold) If currentPattern == 4 # deals with overflow Ass currentPattern = 0 End SelectPattern currentPattern theDelay # does the business End
Sub UpdatePatternRevert # handles the 25% of -1 updates Mat currentPattern = currentPattern - 1 # back to main or alt SelectPattern currentPattern theDelay # after transport is 1/2 thru fill End End
If M0 == B0 $21 # detect CC21 on MIDI channel 1 Ass I00 = T00 # read timer If M2 == 0 # CC value is 127 on press, 0 on release If I00 < $300 # short click, hence pat seq 0,1,2 or 2,3,0 Ass theDelay = 0 # zero delay UpdatePattern # switch immediately to fill, from Pat 0 or 2 Ass theDelay = $1400 # Ass a 1.4sec delay to next pat select UpdatePattern # delay allows for cymbal crash on beat 1 else # assuming a Hold Ass theDelay = 0 # revert to main or alt, eg. 0,1,0 or 2,3,2 UpdatePattern # immediate switch to fill (delay 0). Ass theDelay = $1600 # just after end of fill UpdatePatternRevert # revert to main or alt End End End Block
|
|
|
Post by uncledave on Mar 10, 2022 2:27:57 GMT
OK. I'll give yours a look over. I notice you've already found something in SB I thought was impossible. So that's cool.
Here's my best and final version of the clock tick generator. I split the application demo code away from the clock tick functionality, so you should be able to merge it with your other code. I kept one counter which directly gives the position in the current bar. If you run this in Audiobus along with AR-909, you will see it calling out bars and beats in tempo. You don't need to connect it to anything else; it just sits there and keeps time, like a little metrognome. #TickMaster
# This code uses PF0 to PFF. Using only one array to minimize impact # on other code. All variable and sub names begin with (Tt)ick. # Add the block of tick management code to If load, add the event # handlers to the beginning of the main part of the script, so they are always # seen first. Replace or edit UserResetData and UserProcessTick to # implement any tick- or run-related functions.
If load Alias $4 tickPpqn # ticks/quarter note. Configure as desired.
# Start of tick management code. Do not change. ————————————— # this internal SysEx message uses an arbitrary index (42) # to ensure uniqueness. define tickEvent F0 7D 42 F7
# Some of these are large numbers, so we use P00 array. Alias PF0 tickNextTime Alias PF1 tickTime Alias PF2 tickRunning Alias PF3 tickDiv Alias PF4 tickRem Alias PF5 tickAccum Alias PF6 tickCount Alias PF7 tickBarLength Alias PF8 tickBeatsPerBar Ass PF0 = 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 # If you change tickBeatsPerBar before Run, the new value # will be used. Ass tickBeatsPerBar = 4
Alias $6000000 tickPeriod # number of ms in 100 minutes
# Calculates the delay time for the next tick. The tickAccum accumulates # the time lost by using an integer number of ms for the delay. Every # time it equals or exceeds the tickDiv, we can add an extra ms to the # nextTickTime. Sub TickUpdateTime Ass tickNextTime = tickTime Calc tickAccum = tickAccum + tickRem If tickAccum >= tickDiv # roll counter down and add 1 ms to next delay. Calc tickAccum = tickAccum - tickDiv Calc tickNextTime = tickNextTime + 1 End End
# Issues delayed message as early as possible, to keep time. Sub TickGenerate If tickRunning == 1 # message will arrive after one tick delay # PF0 is tickNextTime send tickEvent +I +DPF0 Calc tickCount = tickCount + 1 If tickCount >= tickBarLength Ass tickCount = 0 End # prepare for next tick TickUpdateTime End end
# initialize tick parameters from the system bpm # This locks in the bpm value when you hit play. Saves the time required # to recompute the delay on every tick. # SB bpm value is beats in 100 minutes (6000 sec). # bpm * ppqn is the number of ticks in 6000 sec., or 6,000,000 ms Sub TickPrepareParameters pMode Calc tickDiv = bpm * tickPpqn # ticks in 6000 sec calc tickTime = tickPeriod / tickDiv # base tick time in ms # tickRem is the remainder from the tickTime division # tickRem/tickDiv is the fractional ms that we cannot use Calc tickRem = tickPeriod % tickDiv # init accumulator and time for first tick # Starting tickAccum half full makes the tick events bracket # the correct time. Initialize to 0 to make ticks always early, # or to tickDiv to make ticks always late. Calc tickAccum = tickDiv / 2 Ass tickNextTime = tickTime Ass tickRunning = pMode Calc tickBarLength = tickBeatsPerBar * tickPpqn Ass tickCount = tickBarLength # count will roll over on first tick If tickRunning == 1 TickGenerate End End
# End of tick management code. ——————————————————————
# this demo sub flashes SB label 0 with Bar and Beat Sub DemoShowBeat If tickCount == 0 Set LB0 SBar Else Mat I00 = tickCount % tickPpqn If I00 == 0 Set LB0 SBeat End End # clear the label after half a beat Mat I00 = tickPpqn / 2 Mat I00 = tickCount + I00 Mat I00 = I00 % tickPpqn If I00 == 0 Set LB0 S_ End End
# Add user tick code to subroutines UserResetData and UserProcessTick.
# Called on Start and Stop. Add user reset actions here. # Parameter pMode is 1 for start, 0 for stop Sub UserResetData pMode End
# Called on every tick. Add user tick actions here. # You can use tickCount, which runs from 0 to tickBarLength-1. Sub UserProcessTick DemoShowBeat End
End # Initialization ——————————————————————————
# Tick-related event handlers. —————————————————————— # Keep them at the top of the main code.
# this receives the delayed message and immediately sends another # It should be the first event handler, since it happens very often. # Exit minimizes execution time. if M0 == tickEvent TickGenerate UserProcessTick Exit end
# SB provides these internal messages on transport start and stop. # We use them to start on the right foot. If M0 == F0 7D 01 # handle system events If M3 == 7A # Start TickPrepareParameters 1 UserResetData 1 UserProcessTick End If M3 == 7C # Stop TickPrepareParameters 0 UserResetData 0 End End # End of tick-related event handlers. ———————————————————
# Add other event handlers here.
The marker lines show the parts you should be able to drop into your code.
|
|
|
Post by uncledave on Mar 10, 2022 18:36:34 GMT
Hi Mike,
I have reviewed your code, and it looks fine except for one little niggle. In SelectPattern, the Note Off (81) is always being sent with a delay of 500 ms. But the Note On may be delayed by as much as 1600 ms. The Note Off is supposed to follow the Note on by 500 ms. You could do something like this Mat I11 = Delay + $500 # note the $ Snd 81 temp0 0 +DI11
Also, the Note On/Off (91, 81) are using MIDI channel 2. I guess that's intentional.
UpdatePatternRevert should protect currentPattern from going negative. You should only subtract one if currentPattern > 0. Since SB variables are unsigned, subtracting 1 from 0 produces a huge number (65535), which would be pretty bad. You may know that this will never happen, but that's why bad stuff happens.
And the check in UpdatePattern should be if currentPattern >= 4, not just == 4. That ensures it cannot run away.
I guess that was three niggles. But this helps keep the program safe when we turn it loose in the wild.
Cheers, Dave
|
|
|
Post by mikefloutier on Mar 11, 2022 8:25:20 GMT
In SelectPattern, the Note Off (81) is always being sent with a delay of 500 ms. But the Note On may be delayed by as much as 1600 ms. The Note Off is supposed to follow the Note on by 500 ms. You could do something like this Mat I11 = Delay + $500 # note the $ Snd 81 temp0 0 +DI11
Thanks Dave, to be honest I didn’t really understand the delayed note off thing, I just took your word for it. I should have either deleted it or modified it in the manner you suggest. Also, the Note On/Off (91, 81) are using MIDI channel 2. I guess that's intentional. Good spot, I should have put this back to Ch 1. I chose Ch 2. initially when I thought that BlueBoard could assign different channels to each of its own Banks, which would have made it much more flexible. I’ve taken this up with IK Multimedia but no response so far. UpdatePatternRevert should protect currentPattern from going negative. You should only subtract one if currentPattern > 0. Since SB variables are unsigned, subtracting 1 from 0 produces a huge number (65535), which would be pretty bad. You may know that this will never happen, but that's why bad stuff happens. And the check in UpdatePattern should be if currentPattern >= 4, not just == 4. That ensures it cannot run away. Yes, good practise (in the wild) but, as you say, I know I will never trigger the script in those circumstances so, for my use, and to keep it short, I think I’ll leave it I’m now working on the rules for tailoring theDelay to the current BPM. The whole integer/modulus issue has been doing my head in a bit, but since I only need a rough figure anyway I guess it shouldn’t be too hard. Again, just trying to keep it short and simple. Any suggestions would be welcome. Many thanks, Mike
|
|
|
Post by uncledave on Mar 11, 2022 10:56:25 GMT
To convert BPM to time, look at the first two lines of TickPrepareParameters in my last version of the clock code. It computes the pulse time in ms for a given ppqn (pulses per quarter note, or beat). Use ppqn=1 to get the beat time. This value will always be slightly less than correct. Add tickDiv/2 to the numerator before dividing to get a rounded result. This is equivalent to adding 0.5 to a decimal value before truncating.
Regarding my suggested changes, they don't cost anything, and they protect you from what might just be a slip of the foot. Pretty embarassing to have your program mess up during a performance. My company had a funny story from back when Basic was big. In a full-on customer demo, the program was in a runaway loop. Thinking quickly, they paused the program, patched the bad line, and continued running. Whew!
|
|
|
Post by mikefloutier on Mar 11, 2022 12:57:33 GMT
Thanks Dave, I’ll bear that in mind and test thoroughly before using in anger.
Regarding the issue of adjusting my delays in light of changing BPMs, I’ve just done this horrible little bit of script which gives good enough accuracy for my purposes.
My only real question (subject, of course, to your own suggestions) is regarding use of the “P” variables I’ve heard of. I guessed they might help in some way but I’ve no idea how, and I couldn't see anything in the StreamByter Pages.
# Calculating the relevant delay for a variable BPM
Alias J1 theDelay # related to current bpm, derived from basisDelay (re 100 BPM) Alias J2 basisDelay # relates to 100bpm (set by user whilst BPM is $100) Alias J3 interDelay # used in calculating theDelay Alias J4 realBPM # BPM/100 Alias J5 interDelay2 # used in calculating theDelay
Ass basisDelay = $1400 # eg. From program; NB. keep under $3250
If M0 == 90 Mat realBPM = BPM / $100 # kept low to avoid division issues Mat interDelay = basisDelay * $20 # increased to help next division Mat interDelay2 = interDelay / realBPM # adjusts to account for current BPM Mat theDelay = interDelay2 * $5 # restores from interDelay calculations Set LB0 interDelay Set LB1 theDelay End
|
|
|
Post by uncledave on Mar 11, 2022 16:25:53 GMT
OK, if that works for you.
The normal variables (I, J, K, L) in SB are 16-bit, unsigned. That means they can store only positive values up to 65535 (2^16 - 1). That's good for most stuff, but a little small for precise calculations. The code I referenced uses the P array, which stores 32-bit signed integers. That means it can handle very large numbers, both positive and negative. These are the usual integers in modern computers. That lets me directly divide 6000000 by the SB value of BPM to get the ms per beat. (Although that value itself is small. Not sure how SB would handle the arithmetic without P though.) The P array is described in one sentence of the manual; easy to overlook.
|
|