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.