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.

Working Around Firefox 4’s Subarray Bug

16 April 2011

Firefox 4 was the first browser to ship with built-in support for audio synthesis and manipulation with its Audio Data API. However, it also shipped with a bug which complicates writing JavaScript that uses the API. Fortunately, the nature of the bug makes it easy to detect and fix transparently.

The API relies heavily on Typed Arrays to work with audio data. Briefly, Typed Arrays provide fast, fixed-sized, and (obviously) typed arrays in JavaScript. Each array is backed by a mutable ArrayBuffer, which can be shared by multiple typed arrays.

Central to the API is the mozWriteAudio() method. This method adds an array of samples to the browser’s output buffer. It’s possible to fill this buffer completely, in which case some samples won’t be added and the return value will indicate the number of samples that actually were added. When this happens, it’s up to you to keep track of the remaining samples and make sure that they’re written eventually.

The Typed Array specification provides an easy way to do this with its subarray() method. This method works a lot like the regular Array.slice() method, but instead of creating a copy of the array elements, it simply creates a new window into the underlying region of memory which is what you want if you’re trying to write a low-latency audio application.

Ideally, we’d just take the return value from mozWriteAudio() and slice that many samples off the front of the sample array:

var numSamplesWritten = audio.mozWriteAudio(samples);
samples = samples.subarray(numSamplesWritten);

This is exactly what I tried, and then spent the better part of an hour trying to figure out why samples were being repeated, producing a stuttering effect. As it turns out, subarray() is just broken in Firefox 4.

The Problem

The bug is simple to illustrate.

var a = new Float32Array([0, 1, 2, 3, 4]);
while (a.length) {
    a = a.subarray(1);
    console.log(Array.prototype.join.call(a, ', '));
}

When run, the first value should be removed each time, giving

1, 2, 3, 4
2, 3, 4
3, 4
4

This is exactly what happens in recent WebKit. Firefox 4, however, gives

1, 2, 3, 4
1, 2, 3
1, 2
1

This makes it look like Firefox is slicing off the end, but what is actually happening is that the byte offset for the new array is always relative to the beginning of the underlying buffer, but the length is being calculated correctly.

The Solution

Fortunately, we can fix this. This method is easy enough to implement in JavaScript, so we can replace the broken implementation with our own, corrected version.

if (new Int8Array([0, 1, 0]).subarray(1).subarray(1)[0]) {
    function subarray (begin, end) {
        if (arguments.length === 0) {
            begin = 0;
            end = this.length;
        }
        else {
            if (begin < 0) {
                // relative to end
                begin += this.length;
            }
            // clamp to 0, length
            begin = Math.max(0, Math.min(this.length, begin));
            if (arguments.length === 1) {
                // slice to end
                end = this.length;
            }
            else {
                if (end < 0) {
                    // relative to end
                    end += this.length;
                }
                // clamp to begin, length
                end = Math.max(begin, Math.min(this.length, end));
            }
        }

        var byteOffset = this.byteOffset + begin * this.BYTES_PER_ELEMENT;
        return new this.constructor(this.buffer, byteOffset, end - begin);
    }

    var typedArrays = [Int8Array, Uint8Array, Int16Array, Uint16Array,
                       Int32Array, Uint32Array, Float32Array, Float64Array];
    typedArrays.forEach(function (cls) {
        cls.prototype.subarray = subarray;
    });
}

With this in place, Firefox behaves correctly and produces the same output as WebKit. You can write code that uses subarray() without worrying about the effects of the bug and when Mozilla ships a fix, this patch won’t be applied.

Notes

The bug was introduced in revision 8323a963fd6c. This commit significantly rewrote the Typed Array support and references bug 636078, which is protected.

Generating Sound with the Web Audio API

19 March 2011

The nascent Web Audio API is a neat addition to the HTML5 (6?) landscape. It opens the door to realtime audio synthesis directly in JavaScript, something that is currently only possible using Flash.

It’s easy to use. The following code generates a simple sine wave:

If you're running a build of Safari or Chromium with support for the API (available here), running the example and clicking “play” will produce a lovely tone.

Basic Vector Performance in AS3

24 July 2009

I started working with vectors in ActionScript 3 recently, and wanted to make sure I was using them in the most efficient way. I performed several rudimentary tests of various ways to fill a vector with values. None of the results were very surprising, but it's nice to have numbers to back up my assumptions.

Here’s what I discovered.

Don’t use push(). This is the slowest way I found to fill a vector. First, resizing the vector is going to have an impact on performance. Second, even if you need your vector to expand, push() is not the fastest option. Instead, always specify the index using a counter variable, e.g.  vector[index++] = item;. This can take under 25 percent of the time it takes to push the same items.

Create the vector with enough capacity to hold all your items. If you know your vector will have 100,000 items in it, create a vector with that capacity. This can be twice as fast as growing the vector for every item.

Fixing the size of the vector doesn’t help performance. If you create a vector of a given size, and don’t resize the vector, it makes no difference whether the vector is fixed size or not.

The tests I ran were very basic. For all, I ran a variation of

var vector:Vector.<int> = new Vector.<int>(5000000, true);

for (var i:int = 0; i < 5000000; i++) {
  vector[i] = i;
}

ten times, and averaged the execution time. Results could be different for smaller vectors.