Search code examples
javascriptaudioweb-audio-apiaudiobuffer

How to create and properly schedule AudioBufferSource nodes?


I'm trying to develop a metronome using Javascript. I followed the instruction in this article and I created a scheduler that gets called every so often and schedules the 'ticks', that live into an array called ticksInQueue. I want the 'ticks' to be an actual sound of a metronome, not a frequency generated by an oscillator.

Oscillator

function scheduler() {
    while (nextTickTime < audioContext.currentTime + lookahead) { // while there are notes that will need to play before the next interval
      ticksInQueue.push({tick: currentTick, time: nextTickTime}); // push the note on the queue, even if we're not playing.
      //create an oscillator
      var osc = audioContext.createOscillator();
      osc.connect(audioContext.destination);
      osc.frequency.value = 440.0;
      osc.start(nextTickTime);
      osc.stop(nextTickTime + 0.1);
      nextTickTime += 60.0 / tempo;  // Add beat length to last beat time
      currentTick = currentTick + 1; // Advance the beat number, wrap to zero
      if (currentTick === 5) {
         currentTick = 0;
      }
   }
}

Following this video tutorial on how to create AudioBufferSource nodes, I wrote this piece of code that gets executed as the window is loaded.

audioContext = new AudioContext();
request = new XMLHttpRequest();
request.open("GET", "tick.mp3", true);
request.responseType = "arraybuffer";
function onDecoded(buffer) {
    bufferSource = audioContext.createBufferSource();
    bufferSource.buffer = buffer;
    bufferSource.connect(audioContext.destination);
}
request.onload = function () {
    // audio data is in request.response
    audioContext.decodeAudioData(request.response, onDecoded);
};
request.send();

However, if I substitute the oscillator with the buffer as you see below, the console tells me "Uncaught InvalidStateError: Failed to execute 'start' on 'AudioBufferSourceNode': cannot call start more than once."

AudioBufferSource

function scheduler() {
    while (nextTickTime < audioContext.currentTime + lookahead) {
      ticksInQueue.push({tick: currentTick, time: nextTickTime});
      //Changed code
      bufferSource.start(nextTickTime);
      bufferSource.stop(nextTickTime + 0.1);
      nextTickTime += 60.0 / tempo;  // Add beat length to last beat time
      currentTick = currentTick + 1; // Advance the beat number, wrap to zero
      if (currentTick === 5) {
         currentTick = 0;
      }
   }
}

Solution

  • Yup, you need to create a BufferSource for every "tick". (They share the buffer, but the buffersource is a single-use node.)

    So, something like this:

    var the_buffer=null;
    
    function onDecoded(buffer) {
        the_buffer=buffer;
    }
    
    function playTick() {
        if (the_buffer) {  // make sure it's been decoded
            var bufferSource = audioContext.createBufferSource();
            bufferSource.buffer = the_buffer;
            bufferSource.connect(audioContext.destination);
            bufferSource.start(0);
            // bufferSource will get auto-garbage-collected when it's done playing.
        }
    }
    

    now, replace your oscillator code in the scheduler with a call to playTick();