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;
}
}
}
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();