20th
scheduling
While I have yet to join in the Archaeopteryx zeitgeist, I have had a long interest in generative and permutative music sequencers. Mostly I have built these in Max/MSP, and even then several years ago.
I started building a sequencer that reflects my own philosophy of manipulating existing MIDI sequences instead of attempting to generate them, but ran into several problems. The first is that Max just isn’t that great of an environment for writing excessively complex logic, and the second was that all of a sudden I was poor and devoting all my time to starting a business. So that project fell by the wayside.
Recently I’ve started exploring the idea of rebuilding my old sequencer in Ruby, encouraged by the progress I’ve seen on Archaeopteryx and Ben Bleything’s midiator. From talking with both Ben and Giles however, one of the primary issues with Ruby has to do with scheduling, or, making sure that things happen at a certain time. Given ruby’s dynamic nature this is pretty understandable.
The recent release of Max/MSP 5 brought a lot of modernization to the 20+ year old environment, including a much improved timing engine, and I wanted to see if it would be possible to improve ruby’s scheduling by slaving it to Max. The most straightforward way of doing this is over UDP. Initial results looked good, so I wrote a quick little UDP Library and setup a max patch to talk to it.
Max’s scheduler can run from a given tempo (in this case, I used the default of 120bpm) and can run at “ticks” as fine as 480 pulses per quarter-note (ppq). I defaulted to running at 5 ticks (5.20833 milliseconds at 120bpm) or 1/384th note, a good interval grain for handling many odd timing lengths, down to 128th note triplets.
Then I ran some actual timing tests compared to Kernel#sleep. As the Digg headlines say, The Results May Surprise You
Stat Perfect Max->Ruby (UDP) Ruby (sleep)
======================================================
count 5760 5763 5458
sum 30000 29998.6429214478 11526.6110897064
mean 5.20833 5.20809772941801 2.11598587245962
median 5.20833 5.5539608001709 2.00986862182617
max 5.20833 31.1489105224609 15.2361392974854
min 5.20833 0.528097152709961 0.0209808349609375
stddev 0.0 1.71355416285846 1.6851694046022
The important ones here are median and max. While the UDP-based one has the highest outlier at 31.1ms, it’s median is a lot closer to the goal, and it’s mean is MUCH closer to the ideal.
Thinking things might improve for Kernel#sleep if I loosened up the time grain a bit, I set it to 20 ticks, which is a 96th note, or 20.833ms at 120bpm. Unfortunately Kernel#sleep fared a lot worse:
Stat Perfect Max->Ruby (UDP) Ruby (sleep)
======================================================
count 1440 1439 1427
sum 30000 29978.718996048 13650.7856845856
mean 20.8333 20.8330222349187 9.56607265913495
median 20.8333 20.3008651733398 9.51409339904785
max 20.8333 31.7790508270264 26.3772010803223
min 20.8333 10.0691318511963 0.0269412994384766
stddev 0.0 1.4683011270922 5.79134286901883
The other disadvantage to using Kernel#sleep is that it’s basically a bucket brigade: any errors in timing become cumulative, and unless the errors even themselves out it will get gradually more and more out of sync. Maybe it’s not a big deal, but if you’re dealing with outside forces (f.e. a real musician) this can become an issue.
I find these results promising, and am drawing up plans for a scheduling engine based on this. Max/MSP lets me compile standalone apps, that you can run without having any other software installed, and one of the benefits of using UDP is multicasting: you could have one time clock server running on a local network and slave several machines to it.