Generating Sound with the Audio Data API

17 April 2011

While it’s pretty easy to get started generating sound with the Web Audio API, using the Audio Data API takes a bit more work. Unlike the Web Audio API, which is callback driven, the Audio Data API doesn’t help you manage it’s buffers, so you are responsible for keeping them full.

Adapting the example from the Mozilla Wiki, we will write a function to be run every 100 milliseconds and fill the buffer.

var sampleRate = 44100;

var audio = new Audio();
audio.mozSetup(1, sampleRate);

The API uses the Audio object for output. We create one, and configure it for output at a sample rate of 44,100.

var writePosition = 0;

We need to keep track of how many samples we’ve written in order to know how many samples to write.

var bufferSize = sampleRate / 2;
var buffer = new Float32Array(bufferSize);
var currentBuffer = buffer.subarray(0);

We will try to keep ½ second of data in the output buffer, so we’ll never need to generate more than 44100 / 2 samples of data at a time. We allocate buffer to hold these samples. Samples that have been generated but not yet written will be kept in currentBuffer.

function write() {
    var playPosition = audio.mozCurrentSampleOffset();
    while (true) {
        var needed = playPosition + bufferSize - writePosition;
        if (needed <= 0) {
            break;
        }
        if (currentBuffer.length === 0) {
            currentBuffer = buffer.subarray(0, needed);
            generateData(currentBuffer);
        }
        var toWrite = currentBuffer.length;
        var written = audio.mozWriteAudio(currentBuffer);
        writePosition += written;
        currentBuffer = currentBuffer.subarray(written);
        if (written < toWrite) {
            break;
        }
    }
}

Each time the write() function is called, it loops until it has filled the output buffer with up to bufferSize samples of data. At any given time, the number of samples we want to have written to the output buffer is equal to playPosition + bufferSize. The first iteration through the loop, there may already be samples in currentBuffer. If this is the case, we can just write them to the output buffer. Otherwise, and on the second iteration of the loop, we need to generate some samples.

Once we have some samples, we write them to the output buffer, and keep track of the total number of samples written so far. The audio data remaining after the output buffer is filled will be kept in currentBuffer to be used on the next invocation. If we weren’t able to write as many samples as we wanted, we’re finished for this invocation.

var p = 0;
function generateData(buffer) {
    for (var i = 0; i < buffer.length; i++) {
        buffer[i] = Math.sin(p++);
    }
}

Finally, we can actually generate some audio data.

var interval;

function play() {
    interval = interval || setInterval(write, 100);
}

function pause() {
    interval = clearInterval(interval);
}

To control playback, we simply set or clear a timer that calls the write() function.