4

I followed the instructions in this article and created a Javascript metronome. It makes use of the Web Audio API and has audioContext.currentTime at its core for precise timing.

My version, available at this plunker, is a very simplified version of the original, made by Chris Wilson and available here. In order for mine to work, since it uses an actual audio file and doesn't synthesize sounds through an oscillator, you need to download the plunker and this audio file, placing it in the root folder (it's a metronome 'tick' sound, but you could use any sound you want).

It works like a charm - if it weren't for the fact that if the user minimizes the window, the otherwise very accurate metronome starts hiccupping instantly and awfully. I really don't understand what is the problem, here.

Javascript

var context, request, buffer;
var tempo = 120;
var tickTime;

function ticking() {
    var source = context.createBufferSource();
    source.buffer = buffer;
    source.connect(context.destination);
    source.start(tickTime);
}

function scheduler() {
    while (tickTime < context.currentTime + 0.1) {  //while there are notes to schedule, play the last scheduled note and advance the pointer
        ticking();
        tickTime += 60 / tempo;
    }
}

function loadTick() {
    request = new XMLHttpRequest();                 //Asynchronous http request (you'll need a local server) 
    request.open('GET', 'tick.wav', true);          //You need to download the file @ http://s000.tinyupload.com/index.php?file_id=89415137224761217947
    request.responseType = 'arraybuffer';
    request.onload = function () {
        context.decodeAudioData(request.response, function (theBuffer) {
            buffer = theBuffer;
        });
    };
    request.send();
}

function start() {
    tickTime = context.currentTime;
    scheduleTimer = setInterval(function () {
        scheduler();
   }, 25);
}

window.onload = function () {
    window.AudioContext = window.AudioContext || window.webkitAudioContext;
    context = new AudioContext();
    loadTick();
    start();
};

1 Answers1

2

Yeah, this is because the browsers throttle setTimeout and setInterval to once per second when the window loses focus. (This was done to circumvent CPU/power drain due to developers using setTimeout/setInterval for visual animation, and not pausing the animation when the tab lost focus.)

There are two ways around this:

1) increase the "look-ahead" (in your example, 0.1 second) to greater than one second - like, 1.1s. Unfortunately, this would mean you couldn't change things (like stopping the playback, or changing the tempo) without a more-than-one-second lag in the change; so you'd probably want to only increase that value when the blur event was fired on the window, and change it back to 0.1 when the window focus event fired. Still not ideal.

2) Circumvent the throttling. :) It turns out you can do this, because setTimeout/setInterval are NOT throttled in Web Workers! (This approach was originally suggested by someone in the comment thread of my original article at http://www.html5rocks.com/en/tutorials/audio/scheduling/#disqus_thread.) I implemented this for the metronome code in https://github.com/cwilso/metronome: take a look at the js/metronome.js and js/metronomeworker.js. The Worker basically just maintains the timer, and marshals a message back across to the main thread; take a look at https://github.com/cwilso/metronome/blob/master/js/metronome.js#L153, in particular, to see how it's kicked off. You could modify that snippet of code and use metronomeworker.js as-is to fix this.

cwilso
  • 13,610
  • 1
  • 30
  • 35