views:

64

answers:

2

Hi!

I'm trying to write a simple music-sequencer in Javascript.

Sounds will be played by SoundManager2

I quickly realised that setTimeout and setInterval are useless for this kind of timing. Their accuracy is simply not good enough.

So what I am trying instead is this:

Create a queue of sound-events (a 2d array [ note, time ] )

Process the queue in a while loop

In pseudo code it could look like this:

// The queue of time/note values (millisecs)
var q = [[0, C], [250, D], [500, E]]

var begin = (new Date).getTime()

while(q.length > 0 ){
    var now = (new Date).getTime()
    var eventTime = q[0][0] + begin

    if( now >= eventTime){

        playNote(q[0][1]) // Play the note  
        q.shift()       // Now that the note has been played, discard it.
    }
}

Through debugging I've found that this method seems precise enough (the playNote call is made at the exact time it's supposed to).

However while the 'sequence' is playing, all other Javascript (including the bit that actually MAKES the sound) is suspended which is hardly a surprise.

This means that I have silence for as long time it takes to run the sequence and then ALL of the calls to playNote are executed.

I've tried isolating the while-loop in it's own function and then call it through setTimeout, in the hopes that this would create a background thread executing it's own thing (namely playing the sequence) without halting all other JS execution. That doesn't work

I've also tried calling the playNote function using setTimeout, in the hopes that while indeed the UI is frozen at least the sequence is playing correctly, but that doesn't work either.

I've also tried a combination, but as you probably already guessed, that doesn't work either.

So my question is:

How can I have a queue of events processed with precise timing, while NOT shutting down all other code-execution during the run

+1  A: 

I don't know if there is a solution for older browers, but web workers are intented for doing parrallel execution in JavaScript.

They are supported in recent versions of Chrome and Firefox, I don't know about other browsers.

mikerobi
Older browsers are not relevant for me. This thing is going to end up as an online app where it will be fair to say 'If you wanna use this, you gotta DITCH NS4.6!' (and I won't need to support IE either). However if it can run in webkit on mobile (android - IOS) that would be awesome.
Acebone
@Acebone, I believe it is supported on both mobile platforms, but would have to google it to be sure.
mikerobi
Well, Webworkers will not instantiate flash or HTML5 Audio in the background, due to browser limitations. So my question still stands
Acebone
A: 

If you replace your while loop with a recursive function (it doesn't have to be self-executing) the browser wont freeze.

(function foo (q) {
    var now = (new Date).getTime(),
        eventTime = q[0][0] + begin;
    if (q.length > 0) {
        if (now >= eventTime) {
            playNote(q[0][1]); // Play the note
            q.shift(); // Discard played note
        }
        return foo(q); // Pass q into the next round
    } else {
        return; // End the "loop"
    }
}(q));

Edit: The problem with recursive calls + timer accuracy should be taken care of by setInterval:

var timer = setInterval(function foo () {
    var now = (new Date).getTime(),
        eventTime = q[0][0] + begin;
    if (q.length > 0) {
        if (now >= eventTime) {
            playNote(q[0][1]); // Play the note
            q.shift(); // Discard played note
        }
    } else {
        clearInterval(timer); // End the "loop"
    }
}, 10); // This is in ms (10 is just a random number)
indieinvader
Oh man! That's brilliant!!
Acebone
Can I ask what the enclosing parentheses are for?
Acebone
It's a self-executing function: The parens make it a function expression rather than a statement and the `()` operator calls the named-anonymous function and passes `q` as an argument. Writing it like this makes it act like `while` loop because it gets executed as soon as the JS engine gets to it.
indieinvader
Unfortunately this generates soo many recursive calls, that only one note is played and the whole thing get's shut down by the JSEngine (at least in Firefox).
Acebone
I've tried curbing the calls with setTimeout callsso that if there is more than X ms until next event, then wait for X - Y ms before recursing any further.The idea is that if Y > whateverimprecision setTimeout has, you could curb the number of recursive calls while still maintaining precisionIt doesn't work (or at least I can't make it work). All notes are played but any precision is lost
Acebone