Re: Reflections on writing a sequencer

HI Adam,

I think one general way to structure sequencer playback is as follows -- I've used this approach with WebAudio successfully in the past:

1. Just before starting playback, take note of the AudioContext's currentTime property.  Add a small time offset to it, say 100 ms.  The result will be your performance start time, corresponding to time offset zero in your sequencer data.  (The time offset provides a short window in which to schedule the first events in the sequence).

2. Create a scheduler function that will run periodically, which examines the AudioContext's currentTime and subtracts the previously captured startTime. That gives you a "current performance time" at the moment the callback occurs, expressed in terms of your sequencer data.  Then create AudioNodes representing all sequencer events that occur within an arbitrary time window after this current performance time (say, several seconds) and schedule them with noteOn/noteOff.

3. Call the function immediately, and also use setInterval() or setTimeout() to schedule callbacks to the above function on some reasonable basis, say every 100-200 ms. The exact interval is not important and can be tuned for best performance.

This approach is relatively insensitive to callback timing and in general allows audio to be scheduled an arbitrary interval in advance of its being played.

...Joe


On Jul 24, 2012, at 11:40 AM, Adam Goode wrote:

> Hi,
> 
> Yesterday I tried to write an extremely simple sequencer using webaudio. My goal was to have a tone play periodically, at a user-selectable low frequency interval.
> 
> The main problem I ran into was the difficulties in scheduling events synchronized with the a-rate clock.
> 
> If I want to play a tone twice per second, I want to call this code in a loop, indefinitely:
> 
> var startTime = ....
> var o = c.createOscillator();
> o.connect(c.destination);
> o.noteOn(startTime);
> o.noteOff(startTime + 0.1);
> 
> I can't just put it in a loop, I need to schedule this in a callback, when necessary to fill the event queue. But what callback to use? setInterval is not appropriate, since the setInterval clock will skew quickly from c.currentTime. And busy looping with setInterval(0) will consume a lot of CPU and gets throttled when switching tabs (try putting the drum machine demo in a background tab and see).
> 
> My solution was this:
> 
> var controlOscillator = c.createOscillator();
> controlOscillator.frequency.value = 2;
> var js = c.createJavaScriptNode(256, 1, 0);
> controlOscillator.connect(js);
> 
> js.onaudioprocess = function(e) {
>   ... detect positive zero crossing from control oscillator ...
>   if (zeroCross) {
>     var o = c.createOscillator();
>     o.connect(c.destination);
>     var startTime = ... zero crossing offset + playbackTime ...
>     o.noteOn(startTime);
>     o.noteOff(startTime + 0.1);
>   }
> };
> 
> 
> This does work (except for missing playbackTime https://bugs.webkit.org/show_bug.cgi?id=61524, needing to connect the javascript node to destination, and another bug on chrome http://crbug.com/138646), but is awkward. There is also the question of having a disconnected graph: I am sending control data, not audio data, so I don't want to connect it to destination.
> 
> I essentially want to have a callback for getting new control data, to keep the event pipeline filled without overflowing any noteOn buffer or falling behind. Is the javascript node appropriate for this? I feel like there could be something more explicit, like a setInterval off of the audio context.
> 
> 
> 
> Adam
> 
> 

... .  .    .       Joe

Joe Berkovitz
President

Noteflight LLC
84 Hamilton St, Cambridge, MA 02139
phone: +1 978 314 6271
www.noteflight.com

Received on Tuesday, 24 July 2012 16:40:32 UTC