rename from dom/media/webaudio/test/blink/iirfilter.html
rename to dom/media/webaudio/test/blink/test_iirFilterNode.html
--- a/dom/media/webaudio/test/blink/iirfilter.html
+++ b/dom/media/webaudio/test/blink/test_iirFilterNode.html
@@ -1,574 +1,467 @@
-<!doctype html>
+<!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>
+<head>
+ <title>Test IIRFilterNode GetFrequencyResponse</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <script type="text/javascript" src="biquad-filters.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
- <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);
+SimpleTest.waitForExplicitFinish();
- // 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];
+addLoadEvent(function() {
+ var sampleRate = 48000;
+ var testDurationSec = 1;
+ var testFrames = testDurationSec * sampleRate;
- var fb1 = [1, .9];
+ var testPromises = []
+ testPromises.push(function () {
+ // 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 context = new OfflineAudioContext(2, testFrames, sampleRate);
- 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;
+ // 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;
- success = Should("createIIRFilter with unnormalized coefficients", function () {
- iir2 = context.createIIRFilter(ff, fb2);
- }).notThrow() && success;
+ // Gain node for computing the difference between the filters.
+ var gain = context.createGain();
+ gain.gain.value = -1;
- // 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 IIR filters. Use a common feedforward array.
+ var ff = [1];
+
+ var fb1 = [1, .9];
- // 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);
+ 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];
+ }
- 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);
+ var iir1 = context.createIIRFilter(ff, fb1);
+ var iir2 = context.createIIRFilter(ff, fb2);
- // 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);
+ // 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);
- // 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 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);
- // 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.
+ merger.connect(context.destination);
- var iir = context.createIIRFilter([0.5, 0.5], [1]);
+ source.start();
- // Create the graph
- source.connect(iir);
- iir.connect(context.destination);
+ // Rock and roll!
- // Rock and roll!
- source.start();
+ return 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);
- 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);
- });
+ // Threshold isn't exactly zero because the arithmetic is done differently between the
+ // IIRFilterNode and the BiquadFilterNode.
+ compareChannels(iir1Data, iir2Data);
+ });
+ }());
- 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);
+ testPromises.push(function () {
+ // 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;
+ // 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();
+ // 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.
- context.startRendering().then(function (result) {
- var actual = result.getChannelData(0);
- var expected = new Float64Array(actual.length);
+ var iir = context.createIIRFilter([0.5, 0.5], [1]);
- // 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];
- }
+ // Create the graph
+ source.connect(iir);
+ iir.connect(context.destination);
- // 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);
- });
+ // Rock and roll!
+ source.start();
- // 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;
+ return 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;
+ compareChannels(actual, expected);
+ });
+ }());
-
- // 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;
+ testPromises.push(function () {
+ // Create a simple 1-pole filter and compare with the expected output.
- // 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]);
+ // 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);
- var merger = context.createChannelMerger(2);
- // Create the graph
- source.connect(biquad);
- source.connect(iir);
+ // 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;
- biquad.connect(merger, 0, 0);
- iir.connect(merger, 0, 1);
+ var iir = context.createIIRFilter([1], [1, c]);
- merger.connect(context.destination);
-
- // Rock and roll!
- source.start();
+ // Create the graph
+ source.connect(iir);
+ iir.connect(context.destination);
- 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);
+ // Rock and roll!
+ source.start();
- // 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);
+ return context.startRendering().then(function (result) {
+ var actual = result.getChannelData(0);
+ var expected = new Float64Array(actual.length);
- var snr = 10*Math.log10(computeSNR(actual, expected));
- Should("SNR for IIRFIlter for Biquad " + filterType, snr).beGreaterThanOrEqualTo(snrThreshold);
- }).then(done);
- };
+ // 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];
}
- // 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
- }
- }];
+ compareChannels(actual, expected);
+ });
+ }());
+
+ // 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) {
+ var context = new OfflineAudioContext(2, testFrames, sampleRate);
+
+ // Use a constant (step function) as the source
+ var buffer = context.createBuffer(1, testFrames, context.sampleRate);
+ for (var i = 0; i < testFrames; ++i) {
+ buffer.getChannelData(0)[i] = 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();
+
+ return 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);
+ compareChannels(actual, expected);
+ });
+ }
+
+ biquadFilterTypes = ["lowpass", "highpass", "bandpass", "notch",
+ "allpass", "lowshelf", "highshelf", "peaking"];
+
+ // Create a set of tasks based on biquadTestConfigs.
+ for (var i = 0; i < biquadFilterTypes.length; ++i) {
+ testPromises.push(testWithBiquadFilter(biquadFilterTypes[i]));
+ }
+
+ testPromises.push(function () {
+ // 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 = [];
- // 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));
+ 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();
+
+ return context.startRendering().then(function (result) {
+ 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));
+ });
+
+ ok(maxError <= errorThresholds[channel], "Max difference between IIR and Biquad on channel " + channel);
+ }
+ });
+ }());
+
+ testPromises.push(function () {
+ // 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;
}
- 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 = [];
+ return y.slice(workSize).map(Math.fround);
+ }
- 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];
+ // 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 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);
+ 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
+ ];
- 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];
+ 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
+ ];
- // 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]);
- }
+ return {
+ ff: numProduct,
+ fb: denProduct
+ }
+ }
- if (success) {
- testPassed("IIRFilter correctly processed " + result.numberOfChannels +
- "-channel input.");
- } else {
- testFailed("IIRFilter failed to correctly process " + result.numberOfChannels +
- "-channel input.");
- }
- }).then(done);
- });
+ // 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));
+ }
+ }
- // 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];
+ // Cascade 2 lowpass biquad filters and compare that with the equivalent 4th order IIR
+ // filter.
- // 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];
+ var nyquist = sampleRate / 2;
+ // Compute the coefficients of a lowpass filter.
- y[index] = yn;
- }
-
- return y.slice(workSize).map(Math.fround);
- }
+ // 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;
- // 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 coef = createFilter(biquadType,
+ biquadCutoff / nyquist,
+ biquadQ,
+ biquadGain);
- 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
- ];
+ // Cascade the biquads together to create an equivalent IIR filter.
+ var cascade = cascadeBiquads(coef, coef);
- 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
- ];
+ // 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.
- return {
- ff: numProduct,
- fb: denProduct
- }
- }
+ // 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 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));
- }
- }
+ // 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)));
- 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.
+ // 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);
- // 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);
+ // 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;
- // 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.
+ // 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;
- // 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);
+ var biquad2 = context.createBiquadFilter();
+ biquad2.type = biquadType;
+ biquad2.frequency.value = biquadCutoff;
+ biquad2.Q.value = biquadQ;
- // 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);
+ var iir = context.createIIRFilter(cascade.ff, cascade.fb);
- // 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 merger to get the signals into multiple channels
+ var merger = context.createChannelMerger(2);
- // 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;
+ // Create the graph, filtering the source through two biquads.
+ source.connect(biquad1);
+ biquad1.connect(biquad2);
+ biquad2.connect(merger, 0, 0);
- var biquad2 = context.createBiquadFilter();
- biquad2.type = biquadType;
- biquad2.frequency.value = biquadCutoff;
- biquad2.Q.value = biquadQ;
+ source.connect(iir);
+ iir.connect(merger, 0, 1);
- var iir = context.createIIRFilter(cascade.ff, cascade.fb);
-
- // Create the merger to get the signals into multiple channels
- var merger = context.createChannelMerger(2);
+ merger.connect(context.destination);
- // Create the graph, filtering the source through two biquads.
- source.connect(biquad1);
- biquad1.connect(biquad2);
- biquad2.connect(merger, 0, 0);
+ // Now filter the source through the IIR filter.
+ var y = iirFilter(buffer.getChannelData(0), cascade.ff, cascade.fb);
- source.connect(iir);
- iir.connect(merger, 0, 1);
-
- merger.connect(context.destination);
+ // Rock and roll!
+ source.start();
- // Now filter the source through the IIR filter.
- var y = iirFilter(buffer.getChannelData(0), cascade.ff, cascade.fb);
+ return context.startRendering().then(function(result) {
+ var expected = result.getChannelData(0);
+ var actual = result.getChannelData(1);
- // Rock and roll!
- source.start();
+ compareChannels(actual, expected);
- 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>
+ // Wait for all tests
+ Promise.all(testPromises).then(function () {
+ SimpleTest.finish();
+ }, function () {
+ SimpleTest.finish();
+ });
+});
+</script>
+</pre>
+</body>
</html>
rename from dom/media/webaudio/test/blink/iirfilter-getFrequencyResponse.html
rename to dom/media/webaudio/test/blink/test_iirFilterNodeGetFrequencyResponse.html
--- a/dom/media/webaudio/test/blink/iirfilter-getFrequencyResponse.html
+++ b/dom/media/webaudio/test/blink/test_iirFilterNodeGetFrequencyResponse.html
@@ -1,132 +1,97 @@
-<!doctype html>
+<!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;
+<head>
+ <title>Test IIRFilterNode GetFrequencyResponse</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <script type="text/javascript" src="biquad-filters.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ // Modified from WebKit/LayoutTests/webaudio/iirfilter-getFrequencyResponse.html
+ var sampleRate = 48000;
+ var testDurationSec = 0.01;
- for (var k = 0; k < nFrequencies; ++k) {
- frequencies[k] = k * freqDelta;
- }
+ // Compute a set of linearly spaced frequencies.
+ function createFrequencies(nFrequencies, sampleRate)
+ {
+ var frequencies = new Float32Array(nFrequencies);
+ var nyquist = sampleRate / 2;
+ var freqDelta = nyquist / nFrequencies;
- return frequencies;
- }
+ for (var k = 0; k < nFrequencies; ++k) {
+ frequencies[k] = k * freqDelta;
+ }
- audit.defineTask("1-pole IIR", function (done) {
- var context = new OfflineAudioContext(1, testDurationSec * sampleRate, sampleRate);
+ return frequencies;
+ }
- 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);
+ // Number of frequency samples to take.
+ var numberOfFrequencies = 1000;
+
+ var context = new OfflineAudioContext(1, testDurationSec * sampleRate, sampleRate);
+
+ var frequencies = createFrequencies(numberOfFrequencies, context.sampleRate);
- // 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))
+ // 1-Pole IIR Filter
+ var iir = context.createIIRFilter([1], [1, -0.9]);
- var frequencyScale = Math.PI / (sampleRate / 2);
+ var iirMag = new Float32Array(numberOfFrequencies);
+ var iirPhase = new Float32Array(numberOfFrequencies);
+ var trueMag = new Float32Array(numberOfFrequencies);
+ var truePhase = new Float32Array(numberOfFrequencies);
- 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;
+ // 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))
- // 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");
+ var frequencyScale = Math.PI / (sampleRate / 2);
- done();
- });
+ 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));
+ }
- 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);
+ iir.getFrequencyResponse(frequencies, iirMag, iirPhase);
+ compareChannels(iirMag, trueMag);
+ compareChannels(iirPhase, truePhase);
- 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);
+ // Compare IIR and Biquad Filter
+ // Create an IIR filter equivalent to the biquad filter. Compute the frequency response for both and verify that they are the same.
+ var biquad = context.createBiquadFilter();
+ var coef = createFilter(biquad.type,
+ biquad.frequency.value / (context.sampleRate / 2),
+ biquad.Q.value,
+ biquad.gain.value);
- 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;
+ var iir = context.createIIRFilter([coef.b0, coef.b1, coef.b2], [1, coef.a1, coef.a2]);
- 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();
- });
+ var biquadMag = new Float32Array(numberOfFrequencies);
+ var biquadPhase = new Float32Array(numberOfFrequencies);
+ var iirMag = new Float32Array(numberOfFrequencies);
+ var iirPhase = new Float32Array(numberOfFrequencies);
- audit.defineTask("finish", function (done) {
- finishJSTest();
- done();
- });
+ biquad.getFrequencyResponse(frequencies, biquadMag, biquadPhase);
+ iir.getFrequencyResponse(frequencies, iirMag, iirPhase);
+ compareChannels(iirMag, biquadMag);
+ compareChannels(iirPhase, biquadPhase);
- audit.runTasks();
- successfullyParsed = true;
- </script>
- </body>
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
</html>