Bug 1265408 - Import blink IIRFilterNode tests; r=padenot draft
authorDan Minor <dminor@mozilla.com>
Tue, 03 May 2016 10:51:24 -0400
changeset 375238 a3ba651bef74036a2202484ed7b1165218b8c18c
parent 375237 4c0f4702d49fe10153a65dce24480cc633baab7f
child 375239 bdb76104a4417d0f595fd88afe7e2c11ea22d733
push id20196
push userdminor@mozilla.com
push dateFri, 03 Jun 2016 18:26:34 +0000
reviewerspadenot
bugs1265408
milestone49.0a1
Bug 1265408 - Import blink IIRFilterNode tests; r=padenot Imported from git revision 57f70919a0a3da5ba002b896778b580986343e08. MozReview-Commit-ID: 1HTS2AfgSEN
dom/media/webaudio/test/blink/biquad-filters.js
dom/media/webaudio/test/blink/iirfilter-getFrequencyResponse.html
dom/media/webaudio/test/blink/iirfilter.html
new file mode 100644
--- /dev/null
+++ b/dom/media/webaudio/test/blink/biquad-filters.js
@@ -0,0 +1,370 @@
+// Taken from WebKit/LayoutTests/webaudio/resources/biquad-filters.js
+
+// A biquad filter has a z-transform of
+// H(z) = (b0 + b1 / z + b2 / z^2) / (1 + a1 / z + a2 / z^2)
+//
+// The formulas for the various filters were taken from
+// http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt.
+
+
+// Lowpass filter.
+function createLowpassFilter(freq, q, gain) {
+    var b0;
+    var b1;
+    var b2;
+    var a1;
+    var a2;
+
+    if (freq == 1) {
+        // The formula below works, except for roundoff.  When freq = 1,
+        // the filter is just a wire, so hardwire the coefficients.
+        b0 = 1;
+        b1 = 0;
+        b2 = 0;
+        a1 = 0;
+        a2 = 0;
+    } else {
+        var g = Math.pow(10, q / 20);
+        var d = Math.sqrt((4 - Math.sqrt(16 - 16 / (g * g))) / 2);
+        var theta = Math.PI * freq;
+        var sn = d * Math.sin(theta) / 2;
+        var beta = 0.5 * (1 - sn) / (1 + sn);
+        var gamma = (0.5 + beta) * Math.cos(theta);
+        var alpha = 0.25 * (0.5 + beta - gamma);
+
+        b0 = 2 * alpha;
+        b1 = 4 * alpha;
+        b2 = 2 * alpha;
+        a1 = 2 * (-gamma);
+        a2 = 2 * beta;
+    }
+
+    return {b0 : b0, b1 : b1, b2 : b2, a1 : a1, a2 : a2};
+}
+
+function createHighpassFilter(freq, q, gain) {
+    var b0;
+    var b1;
+    var b2;
+    var a1;
+    var a2;
+
+    if (freq == 1) {
+        // The filter is 0
+        b0 = 0;
+        b1 = 0;
+        b2 = 0;
+        a1 = 0;
+        a2 = 0;
+    } else if (freq == 0) {
+        // The filter is 1.  Computation of coefficients below is ok, but
+        // there's a pole at 1 and a zero at 1, so round-off could make
+        // the filter unstable.
+        b0 = 1;
+        b1 = 0;
+        b2 = 0;
+        a1 = 0;
+        a2 = 0;
+    } else {
+        var g = Math.pow(10, q / 20);
+        var d = Math.sqrt((4 - Math.sqrt(16 - 16 / (g * g))) / 2);
+        var theta = Math.PI * freq;
+        var sn = d * Math.sin(theta) / 2;
+        var beta = 0.5 * (1 - sn) / (1 + sn);
+        var gamma = (0.5 + beta) * Math.cos(theta);
+        var alpha = 0.25 * (0.5 + beta + gamma);
+
+        b0 = 2 * alpha;
+        b1 = -4 * alpha;
+        b2 = 2 * alpha;
+        a1 = 2 * (-gamma);
+        a2 = 2 * beta;
+    }
+
+    return {b0 : b0, b1 : b1, b2 : b2, a1 : a1, a2 : a2};
+}
+
+function normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2) {
+    var scale = 1 / a0;
+
+    return {b0 : b0 * scale,
+            b1 : b1 * scale,
+            b2 : b2 * scale,
+            a1 : a1 * scale,
+            a2 : a2 * scale};
+}
+
+function createBandpassFilter(freq, q, gain) {
+    var b0;
+    var b1;
+    var b2;
+    var a0;
+    var a1;
+    var a2;
+    var coef;
+
+    if (freq > 0 && freq < 1) {
+        var w0 = Math.PI * freq;
+        if (q > 0) {
+            var alpha = Math.sin(w0) / (2 * q);
+            var k = Math.cos(w0);
+
+            b0 = alpha;
+            b1 = 0;
+            b2 = -alpha;
+            a0 = 1 + alpha;
+            a1 = -2 * k;
+            a2 = 1 - alpha;
+
+            coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
+        } else {
+            // q = 0, and frequency is not 0 or 1.  The above formula has a
+            // divide by zero problem.  The limit of the z-transform as q
+            // approaches 0 is 1, so set the filter that way.
+            coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+        }
+    } else {
+        // When freq = 0 or 1, the z-transform is identically 0,
+        // independent of q.
+        coef = {b0 : 0, b1 : 0, b2 : 0, a1 : 0, a2 : 0}
+    }
+
+    return coef;
+}
+
+function createLowShelfFilter(freq, q, gain) {
+    // q not used
+    var b0;
+    var b1;
+    var b2;
+    var a0;
+    var a1;
+    var a2;
+    var coef;
+
+    var S = 1;
+    var A = Math.pow(10, gain / 40);
+
+    if (freq == 1) {
+        // The filter is just a constant gain
+        coef = {b0 : A * A, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+    } else if (freq == 0) {
+        // The filter is 1
+        coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+    } else {
+        var w0 = Math.PI * freq;
+        var alpha = 1 / 2 * Math.sin(w0) * Math.sqrt((A + 1 / A) * (1 / S - 1) + 2);
+        var k = Math.cos(w0);
+        var k2 = 2 * Math.sqrt(A) * alpha;
+        var Ap1 = A + 1;
+        var Am1 = A - 1;
+
+        b0 = A * (Ap1 - Am1 * k + k2);
+        b1 = 2 * A * (Am1 - Ap1 * k);
+        b2 = A * (Ap1 - Am1 * k - k2);
+        a0 = Ap1 + Am1 * k + k2;
+        a1 = -2 * (Am1 + Ap1 * k);
+        a2 = Ap1 + Am1 * k - k2;
+        coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
+    }
+
+    return coef;
+}
+
+function createHighShelfFilter(freq, q, gain) {
+    // q not used
+    var b0;
+    var b1;
+    var b2;
+    var a0;
+    var a1;
+    var a2;
+    var coef;
+
+    var A = Math.pow(10, gain / 40);
+
+    if (freq == 1) {
+        // When freq = 1, the z-transform is 1
+        coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+    } else if (freq > 0) {
+        var w0 = Math.PI * freq;
+        var S = 1;
+        var alpha = 0.5 * Math.sin(w0) * Math.sqrt((A + 1 / A) * (1 / S - 1) + 2);
+        var k = Math.cos(w0);
+        var k2 = 2 * Math.sqrt(A) * alpha;
+        var Ap1 = A + 1;
+        var Am1 = A - 1;
+
+        b0 = A * (Ap1 + Am1 * k + k2);
+        b1 = -2 * A * (Am1 + Ap1 * k);
+        b2 = A * (Ap1 + Am1 * k - k2);
+        a0 = Ap1 - Am1 * k + k2;
+        a1 = 2 * (Am1 - Ap1*k);
+        a2 = Ap1 - Am1 * k-k2;
+
+        coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
+    } else {
+        // When freq = 0, the filter is just a gain
+        coef = {b0 : A * A, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+    }
+
+    return coef;
+}
+
+function createPeakingFilter(freq, q, gain) {
+    var b0;
+    var b1;
+    var b2;
+    var a0;
+    var a1;
+    var a2;
+    var coef;
+
+    var A = Math.pow(10, gain / 40);
+
+    if (freq > 0 && freq < 1) {
+        if (q > 0) {
+            var w0 = Math.PI * freq;
+            var alpha = Math.sin(w0) / (2 * q);
+            var k = Math.cos(w0);
+
+            b0 = 1 + alpha * A;
+            b1 = -2 * k;
+            b2 = 1 - alpha * A;
+            a0 = 1 + alpha / A;
+            a1 = -2 * k;
+            a2 = 1 - alpha / A;
+
+            coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
+        } else {
+            // q = 0, we have a divide by zero problem in the formulas
+            // above.  But if we look at the z-transform, we see that the
+            // limit as q approaches 0 is A^2.
+            coef = {b0 : A * A, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+        }
+    } else {
+        // freq = 0 or 1, the z-transform is 1
+        coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+    }
+
+    return coef;
+}
+
+function createNotchFilter(freq, q, gain) {
+    var b0;
+    var b1;
+    var b2;
+    var a0;
+    var a1;
+    var a2;
+    var coef;
+
+    if (freq > 0 && freq < 1) {
+        if (q > 0) {
+            var w0 = Math.PI * freq;
+            var alpha = Math.sin(w0) / (2 * q);
+            var k = Math.cos(w0);
+
+            b0 = 1;
+            b1 = -2 * k;
+            b2 = 1;
+            a0 = 1 + alpha;
+            a1 = -2 * k;
+            a2 = 1 - alpha;
+            coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
+        } else {
+            // When q = 0, we get a divide by zero above.  The limit of the
+            // z-transform as q approaches 0 is 0, so set the coefficients
+            // appropriately.
+            coef = {b0 : 0, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+        }
+    } else {
+        // When freq = 0 or 1, the z-transform is 1
+        coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+    }
+
+    return coef;
+}
+
+function createAllpassFilter(freq, q, gain) {
+    var b0;
+    var b1;
+    var b2;
+    var a0;
+    var a1;
+    var a2;
+    var coef;
+
+    if (freq > 0 && freq < 1) {
+        if (q > 0) {
+            var w0 = Math.PI * freq;
+            var alpha = Math.sin(w0) / (2 * q);
+            var k = Math.cos(w0);
+
+            b0 = 1 - alpha;
+            b1 = -2 * k;
+            b2 = 1 + alpha;
+            a0 = 1 + alpha;
+            a1 = -2 * k;
+            a2 = 1 - alpha;
+            coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
+        } else {
+            // q = 0
+            coef = {b0 : -1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+        }
+    } else {
+        coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+    }
+
+    return coef;
+}
+
+function filterData(filterCoef, signal, len) {
+    var y = new Array(len);
+    var b0 = filterCoef.b0;
+    var b1 = filterCoef.b1;
+    var b2 = filterCoef.b2;
+    var a1 = filterCoef.a1;
+    var a2 = filterCoef.a2;
+
+    // Prime the pump. (Assumes the signal has length >= 2!)
+    y[0] = b0 * signal[0];
+    y[1] = b0 * signal[1] + b1 * signal[0] - a1 * y[0];
+
+    // Filter all of the signal that we have.
+    for (var k = 2; k < Math.min(signal.length, len); ++k) {
+        y[k] = b0 * signal[k] + b1 * signal[k-1] + b2 * signal[k-2] - a1 * y[k-1] - a2 * y[k-2];
+    }
+
+    // If we need to filter more, but don't have any signal left,
+    // assume the signal is zero.
+    for (var k = signal.length; k < len; ++k) {
+        y[k] = - a1 * y[k-1] - a2 * y[k-2];
+    }
+
+    return y;
+}
+
+// Map the filter type name to a function that computes the filter coefficents for the given filter
+// type.
+var filterCreatorFunction = {"lowpass": createLowpassFilter,
+                             "highpass": createHighpassFilter,
+                             "bandpass": createBandpassFilter,
+                             "lowshelf": createLowShelfFilter,
+                             "highshelf": createHighShelfFilter,
+                             "peaking": createPeakingFilter,
+                             "notch": createNotchFilter,
+                             "allpass": createAllpassFilter};
+
+var filterTypeName = {"lowpass": "Lowpass filter",
+                      "highpass": "Highpass filter",
+                      "bandpass": "Bandpass filter",
+                      "lowshelf": "Lowshelf filter",
+                      "highshelf": "Highshelf filter",
+                      "peaking": "Peaking filter",
+                      "notch": "Notch filter",
+                      "allpass": "Allpass filter"};
+
+function createFilter(filterType, freq, q, gain) {
+    return filterCreatorFunction[filterType](freq, q, gain);
+}
new file mode 100644
--- /dev/null
+++ b/dom/media/webaudio/test/blink/iirfilter-getFrequencyResponse.html
@@ -0,0 +1,132 @@
+<!doctype html>
+<html>
+  <head>
+    <title>Test IIRFilter getFrequencyResponse() functionality</title>
+    <script src="../resources/js-test.js"></script>
+    <script src="resources/compatibility.js"></script>
+    <script src="resources/audio-testing.js"></script>
+    <script src="resources/biquad-filters.js"></script>
+  </head>
+
+  <body>
+    <script>
+      description("Test IIRFilter getFrequencyResponse() functionality");
+      window.jsTestIsAsync = true;
+
+      var sampleRate = 48000;
+      // Some short duration; we're not actually looking at the rendered output.
+      var testDurationSec = 0.01;
+
+      // Number of frequency samples to take.
+      var numberOfFrequencies = 1000;
+
+      var audit = Audit.createTaskRunner();
+
+
+      // Compute a set of linearly spaced frequencies.
+      function createFrequencies(nFrequencies, sampleRate)
+      {
+          var frequencies = new Float32Array(nFrequencies);
+          var nyquist = sampleRate / 2;
+          var freqDelta = nyquist / nFrequencies;
+
+          for (var k = 0; k < nFrequencies; ++k) {
+              frequencies[k] = k * freqDelta;
+          }
+
+          return frequencies;
+      }
+
+      audit.defineTask("1-pole IIR", function (done) {
+        var context = new OfflineAudioContext(1, testDurationSec * sampleRate, sampleRate);
+
+        var iir = context.createIIRFilter([1], [1, -0.9]);
+        var frequencies = createFrequencies(numberOfFrequencies, context.sampleRate);
+        
+        var iirMag = new Float32Array(numberOfFrequencies);
+        var iirPhase = new Float32Array(numberOfFrequencies);
+        var trueMag = new Float32Array(numberOfFrequencies);
+        var truePhase = new Float32Array(numberOfFrequencies);
+
+        // The IIR filter is
+        //   H(z) = 1/(1 - 0.9*z^(-1)).
+        //
+        // The frequency response is
+        //   H(exp(j*w)) = 1/(1 - 0.9*exp(-j*w)).
+        //
+        // Thus, the magnitude is
+        //   |H(exp(j*w))| = 1/sqrt(1.81-1.8*cos(w)).
+        //
+        // The phase is
+        //   arg(H(exp(j*w)) = atan(0.9*sin(w)/(.9*cos(w)-1))
+
+        var frequencyScale = Math.PI / (sampleRate / 2);
+
+        for (var k = 0; k < frequencies.length; ++k) {
+          var omega = frequencyScale * frequencies[k];
+          trueMag[k] = 1/Math.sqrt(1.81-1.8*Math.cos(omega));
+          truePhase[k] = Math.atan(0.9 * Math.sin(omega) / (0.9 * Math.cos(omega) - 1));
+        }
+
+        iir.getFrequencyResponse(frequencies, iirMag, iirPhase);
+
+        var success = true;
+
+        // Thresholds were experimentally determined.
+        success = Should("1-pole IIR Magnitude Response", iirMag).beCloseToArray(trueMag, 2.8611e-6);
+        success = Should("1-pole IIR Phase Response", iirPhase).beCloseToArray(truePhase, 1.7882e-7)
+          && success;
+        if (success)
+          testPassed("1-pole IIR response matched expected response.\n");
+        else
+          testFailed("1-pole IIR response did not match expected response.\n");
+
+        done();
+      });
+
+      audit.defineTask("compare IIR and biquad", function(done) {
+        // Create an IIR filter equivalent to the biquad filter. Compute the frequency response for
+        // both and verify that they are the same.
+        var context = new OfflineAudioContext(1, testDurationSec * sampleRate, sampleRate);
+
+        var biquad = context.createBiquadFilter();
+        var coef = createFilter(biquad.type,
+          biquad.frequency.value / (context.sampleRate / 2),
+          biquad.Q.value,
+          biquad.gain.value);
+
+        var iir = context.createIIRFilter([coef.b0, coef.b1, coef.b2], [1, coef.a1, coef.a2]);
+
+        var frequencies = createFrequencies(numberOfFrequencies, context.sampleRate);
+        var biquadMag = new Float32Array(numberOfFrequencies);
+        var biquadPhase = new Float32Array(numberOfFrequencies);
+        var iirMag = new Float32Array(numberOfFrequencies);
+        var iirPhase = new Float32Array(numberOfFrequencies);
+
+        biquad.getFrequencyResponse(frequencies, biquadMag, biquadPhase);
+        iir.getFrequencyResponse(frequencies, iirMag, iirPhase);
+
+        var success = true;
+
+        // Thresholds were experimentally determined.
+        success = Should("IIR Magnitude Response", iirMag).beCloseToArray(biquadMag, 2.7419e-5);
+        success = Should("IIR Phase Response", iirPhase).beCloseToArray(biquadPhase, 2.7657e-5) && success;
+
+        if (success)
+          testPassed("IIR response matched equivalent " + biquad.type + " Biquad response.\n");
+        else
+          testFailed("IIR response did not equivalent " + biquad.type + " Biquad response.\n");
+
+        done();
+      });
+
+      audit.defineTask("finish", function (done) {
+        finishJSTest();
+        done();
+      });
+
+      audit.runTasks();
+      successfullyParsed = true;
+    </script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/webaudio/test/blink/iirfilter.html
@@ -0,0 +1,574 @@
+<!doctype html>
+<html>
+  <head>
+    <title>Test Basic IIRFilterNode Operation</title>
+    <script src="../resources/js-test.js"></script>
+    <script src="resources/compatibility.js"></script>
+    <script src="resources/audio-testing.js"></script>
+    <script src="resources/biquad-filters.js"></script>
+  </head>
+
+  <body>
+    <script>
+      description("Test Basic IIRFilterNode Operation");
+      window.jsTestIsAsync = true;
+
+      var sampleRate = 48000;
+      var testDurationSec = 1;
+      var testFrames = testDurationSec * sampleRate;
+
+      var audit = Audit.createTaskRunner();
+
+      audit.defineTask("coefficient-normalization", function (done) {
+        // Test that the feedback coefficients are normalized.  Do this be creating two
+        // IIRFilterNodes.  One has normalized coefficients, and one doesn't.  Compute the
+        // difference and make sure they're the same.
+        var success = true;
+        var context = new OfflineAudioContext(2, testFrames, sampleRate);
+
+        // Use a simple impulse as the source.
+        var buffer = context.createBuffer(1, 1, sampleRate);
+        buffer.getChannelData(0)[0] = 1;
+        var source = context.createBufferSource();
+        source.buffer = buffer;
+
+        // Gain node for computing the difference between the filters.
+        var gain = context.createGain();
+        gain.gain.value = -1;
+
+        // The IIR filters.  Use a common feedforward array.
+        var ff = [1];
+
+        var fb1 = [1, .9];
+
+        var fb2 = new Float64Array(2);
+        // Scale the feedback coefficients by an arbitrary factor.
+        var coefScaleFactor = 2;
+        for (var k = 0; k < fb2.length; ++k) {
+          fb2[k] = coefScaleFactor * fb1[k];
+        }
+
+        var iir1;
+        var iir2;
+
+        success = Should("createIIRFilter with normalized coefficients", function () {
+          iir1 = context.createIIRFilter(ff, fb1);
+        }).notThrow() && success;
+
+        success = Should("createIIRFilter with unnormalized coefficients", function () {
+          iir2 = context.createIIRFilter(ff, fb2);
+        }).notThrow() && success;
+
+        // Create the graph.  The output of iir1 (normalized coefficients) is channel 0, and the
+        // output of iir2 (unnormalized coefficients), with appropriate scaling, is channel 1.
+        var merger = context.createChannelMerger(2);
+        source.connect(iir1);
+        source.connect(iir2);
+        iir1.connect(merger, 0, 0);
+        iir2.connect(gain);
+
+        // The gain for the gain node should be set to compensate for the scaling of the
+        // coefficients.  Since iir2 has scaled the coefficients by coefScaleFactor, the output is
+        // reduced by the same factor, so adjust the gain to scale the output of iir2 back up.
+        gain.gain.value = coefScaleFactor;
+        gain.connect(merger, 0, 1);
+
+        merger.connect(context.destination);
+
+        source.start();
+
+        // Rock and roll!
+
+        context.startRendering().then(function (result) {
+          // Find the max amplitude of the result, which should be near zero.
+          var iir1Data = result.getChannelData(0);
+          var iir2Data = result.getChannelData(1);
+
+          // Threshold isn't exactly zero because the arithmetic is done differently between the
+          // IIRFilterNode and the BiquadFilterNode.
+          success = Should("Output of IIR filter with unnormalized coefficients", iir2Data)
+            .beCloseToArray(iir1Data, 2.1958e-38) && success;
+          if (success)
+            testPassed("IIRFilter coefficients correctly normalized.\n");
+          else
+            testFailed("IIRFilter coefficients not correctly normalized.\n");
+        }).then(done);
+      });
+
+      audit.defineTask("one-zero", function (done) {
+        // Create a simple 1-zero filter and compare with the expected output.
+        var context = new OfflineAudioContext(1, testFrames, sampleRate);
+
+        // Use a simple impulse as the source
+        var buffer = context.createBuffer(1, 1, sampleRate);
+        buffer.getChannelData(0)[0] = 1;
+        var source = context.createBufferSource();
+        source.buffer = buffer;
+
+        // The filter is y(n) = 0.5*(x(n) + x(n-1)), a simple 2-point moving average.  This is
+        // rather arbitrary; keep it simple.
+
+        var iir = context.createIIRFilter([0.5, 0.5], [1]);
+
+        // Create the graph
+        source.connect(iir);
+        iir.connect(context.destination);
+
+        // Rock and roll!
+        source.start();
+
+        context.startRendering().then(function (result) {
+          var actual = result.getChannelData(0);
+          var expected = new Float64Array(testFrames);
+          // The filter is a simple 2-point moving average of an impulse, so the first two values
+          // are non-zero and the rest are zero.
+          expected[0] = 0.5;
+          expected[1] = 0.5;
+          Should('IIR 1-zero output', actual).beCloseToArray(expected, 0);
+        }).then(done);
+      });
+
+      audit.defineTask("one-pole", function (done) {
+        // Create a simple 1-pole filter and compare with the expected output.
+
+        // The filter is y(n) + c*y(n-1)= x(n).  The analytical response is (-c)^n, so choose a
+        // suitable number of frames to run the test for where the output isn't flushed to zero.
+        var c = 0.9;
+        var eps = 1e-20;
+        var duration = Math.floor(Math.log(eps) / Math.log(Math.abs(c)));
+        var context = new OfflineAudioContext(1, duration, sampleRate);
+
+        // Use a simple impulse as the source
+        var buffer = context.createBuffer(1, 1, sampleRate);
+        buffer.getChannelData(0)[0] = 1;
+        var source = context.createBufferSource();
+        source.buffer = buffer;
+
+        var iir = context.createIIRFilter([1], [1, c]);
+
+        // Create the graph
+        source.connect(iir);
+        iir.connect(context.destination);
+
+        // Rock and roll!
+        source.start();
+
+        context.startRendering().then(function (result) {
+          var actual = result.getChannelData(0);
+          var expected = new Float64Array(actual.length);
+
+          // The filter is a simple 1-pole filter: y(n) = -c*y(n-k)+x(n), with an impulse as the
+          // input.
+          expected[0] = 1;
+          for (k = 1; k < testFrames; ++k) {
+            expected[k] = -c * expected[k-1];
+          }
+
+          // Threshold isn't exactly zero due to round-off in the single-precision IIRFilterNode
+          // computations versus the double-precision Javascript computations.
+          Should('IIR 1-pole output', actual, {verbose: true})
+            .beCloseToArray(expected, {relativeThreshold: 5.723e-8});
+        }).then(done);
+      });
+
+      // Return a function suitable for use as a defineTask function.  This function creates an
+      // IIRFilterNode equivalent to the specified BiquadFilterNode and compares the outputs.  The
+      // outputs from the two filters should be virtually identical.
+      function testWithBiquadFilter (filterType, errorThreshold, snrThreshold) {
+        return function (done) {
+          var context = new OfflineAudioContext(2, testFrames, sampleRate);
+
+          // Use a constant (step function) as the source
+          var buffer = createConstantBuffer(context, testFrames, 1);
+          var source = context.createBufferSource();
+          source.buffer = buffer;
+
+      
+          // Create the biquad.  Choose some rather arbitrary values for Q and gain for the biquad
+          // so that the shelf filters aren't identical.
+          var biquad = context.createBiquadFilter();
+          biquad.type = filterType;
+          biquad.Q.value = 10;
+          biquad.gain.value = 10;
+
+          // Create the equivalent IIR Filter node by computing the coefficients of the given biquad
+          // filter type.
+          var nyquist = sampleRate / 2;
+          var coef = createFilter(filterType,
+                                  biquad.frequency.value / nyquist,
+                                  biquad.Q.value,
+                                  biquad.gain.value);
+
+          var iir = context.createIIRFilter([coef.b0, coef.b1, coef.b2], [1, coef.a1, coef.a2]);
+
+          var merger = context.createChannelMerger(2);
+          // Create the graph
+          source.connect(biquad);
+          source.connect(iir);
+
+          biquad.connect(merger, 0, 0);
+          iir.connect(merger, 0, 1);
+
+          merger.connect(context.destination);
+
+          // Rock and roll!
+          source.start();
+
+          context.startRendering().then(function (result) {
+            // Find the max amplitude of the result, which should be near zero.
+            var expected = result.getChannelData(0);
+            var actual = result.getChannelData(1);
+
+            // On MacOSX, WebAudio uses an optimized Biquad implementation that is different from
+            // the implementation used for Linux and Windows.  This will cause the output to differ,
+            // even if the threshold passes.  Thus, only print out a very small number of elements
+            // of the array where we have tested that they are consistent.
+            Should("IIRFilter for Biquad " + filterType, actual, {
+                precision: 5,
+                verbose: true
+              })
+              .beCloseToArray(expected, errorThreshold);
+
+            var snr = 10*Math.log10(computeSNR(actual, expected));
+            Should("SNR for IIRFIlter for Biquad " + filterType, snr).beGreaterThanOrEqualTo(snrThreshold);
+          }).then(done);
+        };
+      }
+
+      // Thresholds here are experimentally determined.
+      var biquadTestConfigs = [{
+        filterType: "lowpass",
+        snrThreshold: 91.222,
+        errorThreshold: {
+          relativeThreshold: 4.15e-5
+        }
+      }, {
+        filterType: "highpass",
+        snrThreshold: 107.246,
+        errorThreshold: {
+          absoluteThreshold: 2.9e-6,
+          relativeThreshold: 3e-5
+        }
+      }, {
+        filterType: "bandpass",
+        snrThreshold: 104.060,
+        errorThreshold: {
+          absoluteThreshold: 2e-7,
+          relativeThreshold: 8.7e-4
+        }
+      }, {
+        filterType: "notch",
+        snrThreshold: 91.312,
+        errorThreshold: {
+          absoluteThreshold: 0,
+          relativeThreshold: 4.22e-5
+        }
+      }, {
+        filterType: "allpass",
+        snrThreshold: 91.319,
+        errorThreshold: {
+          absoluteThreshold: 0,
+          relativeThreshold: 4.31e-5
+        }
+      }, {
+        filterType: "lowshelf",
+        snrThreshold: 90.609,
+        errorThreshold: {
+          absoluteThreshold: 0,
+          relativeThreshold: 2.98e-5
+        }
+      }, {
+        filterType: "highshelf",
+        snrThreshold: 103.159,
+        errorThreshold: {
+          absoluteThreshold: 0,
+          relativeThreshold: 1.24e-5
+        }
+      }, {
+        filterType: "peaking",
+        snrThreshold: 91.504,
+        errorThreshold: {
+          absoluteThreshold: 0,
+          relativeThreshold: 5.05e-5
+        }
+      }];
+
+      // Create a set of tasks based on biquadTestConfigs.
+      for (k = 0; k < biquadTestConfigs.length; ++k) {
+        var config = biquadTestConfigs[k];
+        var name = k + ": " + config.filterType;
+        audit.defineTask(name, testWithBiquadFilter(config.filterType, config.errorThreshold, config.snrThreshold));
+      }
+
+      audit.defineTask("multi-channel", function (done) {
+        // Multi-channel test.  Create a biquad filter and the equivalent IIR filter.  Filter the
+        // same multichannel signal and compare the results.
+        var nChannels = 3;
+        var context = new OfflineAudioContext(nChannels, testFrames, sampleRate);
+
+        // Create a set of oscillators as the multi-channel source.
+        var source = [];
+
+        for (k = 0; k < nChannels; ++k) {
+          source[k] = context.createOscillator();
+          source[k].type = "sawtooth";
+          // The frequency of the oscillator is pretty arbitrary, but each oscillator should have a
+          // different frequency.
+          source[k].frequency.value = 100 + k * 100;
+        }
+
+        var merger = context.createChannelMerger(3);
+
+        var biquad = context.createBiquadFilter();
+
+        // Create the equivalent IIR Filter node.
+        var nyquist = sampleRate / 2;
+        var coef = createFilter(biquad.type,
+          biquad.frequency.value / nyquist,
+          biquad.Q.value,
+          biquad.gain.value);
+        var fb = [1, coef.a1, coef.a2];
+        var ff = [coef.b0, coef.b1, coef.b2];
+
+        var iir = context.createIIRFilter(ff, fb);
+        // Gain node to compute the difference between the IIR and biquad filter.
+        var gain = context.createGain();
+        gain.gain.value = -1;
+
+        // Create the graph.
+        for (k = 0; k < nChannels; ++k)
+          source[k].connect(merger, 0, k);
+
+        merger.connect(biquad);
+        merger.connect(iir);
+        iir.connect(gain);
+        biquad.connect(context.destination);
+        gain.connect(context.destination);
+
+        for (k = 0; k < nChannels; ++k)
+          source[k].start();
+
+        context.startRendering().then(function (result) {
+          var success = true;
+          var errorThresholds = [3.7671e-5, 3.0071e-5, 2.6241e-5];
+
+          // Check the difference signal on each channel
+          for (channel = 0; channel < result.numberOfChannels; ++channel) {
+            // Find the max amplitude of the result, which should be near zero.
+            var data = result.getChannelData(channel);
+            var maxError = data.reduce(function(reducedValue, currentValue) {
+              return Math.max(reducedValue, Math.abs(currentValue));
+            });
+
+            success = Should("Max difference between IIR and Biquad on channel " + channel,
+              maxError).beLessThanOrEqualTo(errorThresholds[channel]);
+          }
+
+          if (success) {
+            testPassed("IIRFilter correctly processed " + result.numberOfChannels +
+              "-channel input.");
+          } else {
+            testFailed("IIRFilter failed to correctly process " + result.numberOfChannels +
+              "-channel input.");
+          }
+        }).then(done);
+      });
+
+      // Apply an IIRFilter to the given input signal.
+      //
+      // IIR filter in the time domain is
+      //
+      //   y[n] = sum(ff[k]*x[n-k], k, 0, M) - sum(fb[k]*y[n-k], k, 1, N)
+      //
+      function iirFilter(input, feedforward, feedback) {
+        // For simplicity, create an x buffer that contains the input, and a y buffer that contains
+        // the output.  Both of these buffers have an initial work space to implement the initial
+        // memory of the filter.
+        var workSize = Math.max(feedforward.length, feedback.length);
+        var x = new Float32Array(input.length + workSize);
+
+        // Float64 because we want to match the implementation that uses doubles to minimize
+        // roundoff.
+        var y = new Float64Array(input.length + workSize);
+
+        // Copy the input over.
+        for (var k = 0; k < input.length; ++k)
+          x[k + feedforward.length] = input[k];
+
+        // Run the filter
+        for (var n = 0; n < input.length; ++n) {
+          var index = n + workSize;
+          var yn = 0;
+          for (var k = 0; k < feedforward.length; ++k)
+            yn += feedforward[k] * x[index - k];
+          for (var k = 0; k < feedback.length; ++k)
+            yn -= feedback[k] * y[index - k];
+
+          y[index] = yn;
+        }
+
+        return y.slice(workSize).map(Math.fround);
+      }
+
+      // Cascade the two given biquad filters to create one IIR filter.
+      function cascadeBiquads(f1Coef, f2Coef) {
+        // The biquad filters are:
+        //
+        // f1 = (b10 + b11/z + b12/z^2)/(1 + a11/z + a12/z^2);
+        // f2 = (b20 + b21/z + b22/z^2)/(1 + a21/z + a22/z^2);
+        //
+        // To cascade them, multiply the two transforms together to get a fourth order IIR filter.
+
+        var numProduct = [f1Coef.b0 * f2Coef.b0,
+          f1Coef.b0 * f2Coef.b1 + f1Coef.b1 * f2Coef.b0,
+          f1Coef.b0 * f2Coef.b2 + f1Coef.b1 * f2Coef.b1 + f1Coef.b2 * f2Coef.b0,
+          f1Coef.b1 * f2Coef.b2 + f1Coef.b2 * f2Coef.b1,
+          f1Coef.b2 * f2Coef.b2
+        ];
+
+        var denProduct = [1,
+          f2Coef.a1 + f1Coef.a1,
+          f2Coef.a2 + f1Coef.a1 * f2Coef.a1 + f1Coef.a2,
+          f1Coef.a1 * f2Coef.a2 + f1Coef.a2 * f2Coef.a1,
+          f1Coef.a2 * f2Coef.a2
+        ];
+
+        return {
+          ff: numProduct,
+          fb: denProduct
+        }
+      }
+
+      // Find the magnitude of the root of the quadratic that has the maximum magnitude.
+      //
+      // The quadratic is z^2 + a1 * z + a2 and we want the root z that has the largest magnitude.
+      function largestRootMagnitude(a1, a2) {
+        var discriminant = a1 * a1 - 4 * a2;
+        if (discriminant < 0) {
+          // Complex roots:  -a1/2 +/- i*sqrt(-d)/2.  Thus the magnitude of each root is the same
+          // and is sqrt(a1^2/4 + |d|/4)
+          var d = Math.sqrt(-discriminant);
+          return Math.hypot(a1 / 2, d / 2);
+        } else {
+          // Real roots
+          var d = Math.sqrt(discriminant);
+          return Math.max(Math.abs((-a1 + d) / 2), Math.abs((-a1 - d) / 2));
+        }
+      }
+
+      audit.defineTask("4th-order-iir", function(done) {
+        // Cascade 2 lowpass biquad filters and compare that with the equivalent 4th order IIR
+        // filter.
+
+        var nyquist = sampleRate / 2;
+        // Compute the coefficients of a lowpass filter.
+
+        // First some preliminary stuff.  Compute the coefficients of the biquad.  This is used to
+        // figure out how frames to use in the test.
+        var biquadType = "lowpass";
+        var biquadCutoff = 350;
+        var biquadQ = 5;
+        var biquadGain = 1;
+
+        var coef = createFilter(biquadType,
+          biquadCutoff / nyquist,
+          biquadQ,
+          biquadGain);
+
+        // Cascade the biquads together to create an equivalent IIR filter.
+        var cascade = cascadeBiquads(coef, coef);
+
+        // Since we're cascading two identical biquads, the root of denominator of the IIR filter is
+        // repeated, so the root of the denominator with the largest magnitude occurs twice.  The
+        // impulse response of the IIR filter will be roughly c*(r*r)^n at time n, where r is the
+        // root of largest magnitude.  This approximation gets better as n increases.  We can use
+        // this to get a rough idea of when the response has died down to a small value.
+
+        // This is the value we will use to determine how many frames to render.  Rendering too many
+        // is a waste of time and also makes it hard to compare the actual result to the expected
+        // because the magnitudes are so small that they could be mostly round-off noise.
+        //
+        // Find magnitude of the root with largest magnitude
+        var rootMagnitude = largestRootMagnitude(coef.a1, coef.a2);
+
+        // Find n such that |r|^(2*n) <= eps.  That is, n = log(eps)/(2*log(r)).  Somewhat
+        // arbitrarily choose eps = 1e-20;
+        var eps = 1e-20;
+        var framesForTest = Math.floor(Math.log(eps) / (2 * Math.log(rootMagnitude)));
+
+        // We're ready to create the graph for the test.  The offline context has two channels:
+        // channel 0 is the expected (cascaded biquad) result and channel 1 is the actual IIR filter
+        // result.
+        var context = new OfflineAudioContext(2, framesForTest, sampleRate);
+
+        // Use a simple impulse with a large (arbitrary) amplitude as the source
+        var amplitude = 1;
+        var buffer = context.createBuffer(1, testFrames, sampleRate);
+        buffer.getChannelData(0)[0] = amplitude;
+        var source = context.createBufferSource();
+        source.buffer = buffer;
+
+        // Create the two biquad filters.  Doesn't really matter what, but for simplicity we choose
+        // identical lowpass filters with the same parameters.
+        var biquad1 = context.createBiquadFilter();
+        biquad1.type = biquadType;
+        biquad1.frequency.value = biquadCutoff;
+        biquad1.Q.value = biquadQ;
+
+        var biquad2 = context.createBiquadFilter();
+        biquad2.type = biquadType;
+        biquad2.frequency.value = biquadCutoff;
+        biquad2.Q.value = biquadQ;
+
+        var iir = context.createIIRFilter(cascade.ff, cascade.fb);
+
+        // Create the merger to get the signals into multiple channels
+        var merger = context.createChannelMerger(2);
+
+        // Create the graph, filtering the source through two biquads.
+        source.connect(biquad1);
+        biquad1.connect(biquad2);
+        biquad2.connect(merger, 0, 0);
+
+        source.connect(iir);
+        iir.connect(merger, 0, 1);
+
+        merger.connect(context.destination);
+
+        // Now filter the source through the IIR filter.
+        var y = iirFilter(buffer.getChannelData(0), cascade.ff, cascade.fb);
+
+        // Rock and roll!
+        source.start();
+
+        context.startRendering().then(function(result) {
+          var expected = result.getChannelData(0);
+          var actual = result.getChannelData(1);
+
+          Should("4-th order IIRFilter (biquad ref)",
+              actual, {
+                verbose: true,
+                precision: 5
+              })
+            .beCloseToArray(expected, {
+              // Thresholds experimentally determined.
+              absoluteThreshold: 8.4e-8,
+              relativeThreshold: 5e-7,
+            });
+
+          var snr = 10*Math.log10(computeSNR(actual, expected));
+          Should("SNR of 4-th order IIRFilter (biquad ref)", snr)
+            .beGreaterThanOrEqualTo(110.684);
+        }).then(done);
+      });
+
+      audit.defineTask("finish", function (done) {
+        finishJSTest();
+        done();
+      });
+
+      audit.runTasks();
+      successfullyParsed = true;
+    </script>
+  </body>
+</html>