summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface')
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/ctor-iirfilter.html126
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/iirfilter-basic.html204
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/iirfilter-getFrequencyResponse.html159
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/iirfilter.html572
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/test-iirfilternode.html59
5 files changed, 1120 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/ctor-iirfilter.html b/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/ctor-iirfilter.html
new file mode 100644
index 0000000000..e884d487af
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/ctor-iirfilter.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ Test Constructor: IIRFilter
+ </title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/webaudio/resources/audit-util.js"></script>
+ <script src="/webaudio/resources/audit.js"></script>
+ <script src="/webaudio/resources/audionodeoptions.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let context;
+
+ let audit = Audit.createTaskRunner();
+
+ audit.define('initialize', (task, should) => {
+ context = initializeContext(should);
+ task.done();
+ });
+
+ audit.define('invalid constructor', (task, should) => {
+ testInvalidConstructor(should, 'IIRFilterNode', context);
+ task.done();
+ });
+
+ audit.define('default constructor', (task, should) => {
+ let prefix = 'node0';
+ let node = testDefaultConstructor(should, 'IIRFilterNode', context, {
+ prefix: prefix,
+ numberOfInputs: 1,
+ numberOfOutputs: 1,
+ channelCount: 2,
+ channelCountMode: 'max',
+ channelInterpretation: 'speakers',
+ constructorOptions: {feedforward: [1], feedback: [1, -.9]}
+ });
+
+ task.done();
+ });
+
+ audit.define('test AudioNodeOptions', (task, should) => {
+ testAudioNodeOptions(
+ should, context, 'IIRFilterNode',
+ {additionalOptions: {feedforward: [1, 1], feedback: [1, .5]}});
+ task.done();
+ });
+
+ audit.define('constructor options', (task, should) => {
+ let node;
+
+ let options = {feedback: [1, .5]};
+ should(
+ () => {
+ node = new IIRFilterNode(context, options);
+ },
+ 'node = new IIRFilterNode(, ' + JSON.stringify(options) + ')')
+ .throw(TypeError);
+
+ options = {feedforward: [1, 0.5]};
+ should(
+ () => {
+ node = new IIRFilterNode(context, options);
+ },
+ 'node = new IIRFilterNode(c, ' + JSON.stringify(options) + ')')
+ .throw(TypeError);
+
+ task.done();
+ });
+
+ // Test functionality of constructor. This is needed because we have no
+ // way of determining if the filter coefficients were were actually set
+ // appropriately.
+
+ // TODO(rtoy): This functionality test should be moved out to a separate
+ // file.
+ audit.define('functionality', (task, should) => {
+ let options = {feedback: [1, .5], feedforward: [1, 1]};
+
+ // Create two-channel offline context; sample rate and length are fairly
+ // arbitrary. Channel 0 contains the test output and channel 1 contains
+ // the expected output.
+ let sampleRate = 48000;
+ let renderLength = 0.125;
+ let testContext =
+ new OfflineAudioContext(2, renderLength * sampleRate, sampleRate);
+
+ // The test node uses the constructor. The reference node creates the
+ // same filter but uses the old factory method.
+ let testNode = new IIRFilterNode(testContext, options);
+ let refNode = testContext.createIIRFilter(
+ Float32Array.from(options.feedforward),
+ Float32Array.from(options.feedback));
+
+ let source = testContext.createOscillator();
+ source.connect(testNode);
+ source.connect(refNode);
+
+ let merger = testContext.createChannelMerger(
+ testContext.destination.channelCount);
+
+ testNode.connect(merger, 0, 0);
+ refNode.connect(merger, 0, 1);
+
+ merger.connect(testContext.destination);
+
+ source.start();
+ testContext.startRendering()
+ .then(function(resultBuffer) {
+ let actual = resultBuffer.getChannelData(0);
+ let expected = resultBuffer.getChannelData(1);
+
+ // The output from the two channels should be exactly equal
+ // because exactly the same IIR filter should have been created.
+ should(actual, 'Output of filter using new IIRFilter(...)')
+ .beEqualToArray(expected);
+ })
+ .then(() => task.done());
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/iirfilter-basic.html b/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/iirfilter-basic.html
new file mode 100644
index 0000000000..7828f05226
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/iirfilter-basic.html
@@ -0,0 +1,204 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ Test Basic IIRFilterNode Properties
+ </title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../resources/audit-util.js"></script>
+ <script src="../../resources/audit.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let sampleRate = 48000;
+ let testFrames = 100;
+
+ // Global context that can be used by the individual tasks. It must be
+ // defined by the initialize task.
+ let context;
+
+ let audit = Audit.createTaskRunner();
+
+ audit.define('initialize', (task, should) => {
+ should(() => {
+ context = new OfflineAudioContext(1, testFrames, sampleRate);
+ }, 'Initialize context for testing').notThrow();
+ task.done();
+ });
+
+ audit.define('existence', (task, should) => {
+ should(context.createIIRFilter, 'context.createIIRFilter').exist();
+ task.done();
+ });
+
+ audit.define('parameters', (task, should) => {
+ // Create a really simple IIR filter. Doesn't much matter what.
+ let coef = Float32Array.from([1]);
+
+ let f = context.createIIRFilter(coef, coef);
+
+ should(f.numberOfInputs, 'numberOfInputs').beEqualTo(1);
+ should(f.numberOfOutputs, 'numberOfOutputs').beEqualTo(1);
+ should(f.channelCountMode, 'channelCountMode').beEqualTo('max');
+ should(f.channelInterpretation, 'channelInterpretation')
+ .beEqualTo('speakers');
+
+ task.done();
+ });
+
+ audit.define('exceptions-createIIRFilter', (task, should) => {
+ should(function() {
+ // Two args are required.
+ context.createIIRFilter();
+ }, 'createIIRFilter()').throw(TypeError);
+
+ should(function() {
+ // Two args are required.
+ context.createIIRFilter(new Float32Array(1));
+ }, 'createIIRFilter(new Float32Array(1))').throw(TypeError);
+
+ should(function() {
+ // null is not valid
+ context.createIIRFilter(null, null);
+ }, 'createIIRFilter(null, null)').throw(TypeError);
+
+ should(function() {
+ // There has to be at least one coefficient.
+ context.createIIRFilter([], []);
+ }, 'createIIRFilter([], [])').throw(DOMException, 'NotSupportedError');
+
+ should(function() {
+ // There has to be at least one coefficient.
+ context.createIIRFilter([1], []);
+ }, 'createIIRFilter([1], [])').throw(DOMException, 'NotSupportedError');
+
+ should(function() {
+ // There has to be at least one coefficient.
+ context.createIIRFilter([], [1]);
+ }, 'createIIRFilter([], [1])').throw(DOMException, 'NotSupportedError');
+
+ should(
+ function() {
+ // Max allowed size for the coefficient arrays.
+ let fb = new Float32Array(20);
+ fb[0] = 1;
+ context.createIIRFilter(fb, fb);
+ },
+ 'createIIRFilter(new Float32Array(20), new Float32Array(20))')
+ .notThrow();
+
+ should(
+ function() {
+ // Max allowed size for the feedforward coefficient array.
+ let coef = new Float32Array(21);
+ coef[0] = 1;
+ context.createIIRFilter(coef, [1]);
+ },
+ 'createIIRFilter(new Float32Array(21), [1])')
+ .throw(DOMException, 'NotSupportedError');
+
+ should(
+ function() {
+ // Max allowed size for the feedback coefficient array.
+ let coef = new Float32Array(21);
+ coef[0] = 1;
+ context.createIIRFilter([1], coef);
+ },
+ 'createIIRFilter([1], new Float32Array(21))')
+ .throw(DOMException, 'NotSupportedError');
+
+ should(
+ function() {
+ // First feedback coefficient can't be 0.
+ context.createIIRFilter([1], new Float32Array(2));
+ },
+ 'createIIRFilter([1], new Float32Array(2))')
+ .throw(DOMException, 'InvalidStateError');
+
+ should(
+ function() {
+ // feedforward coefficients can't all be zero.
+ context.createIIRFilter(new Float32Array(10), [1]);
+ },
+ 'createIIRFilter(new Float32Array(10), [1])')
+ .throw(DOMException, 'InvalidStateError');
+
+ should(function() {
+ // Feedback coefficients must be finite.
+ context.createIIRFilter([1], [1, Infinity, NaN]);
+ }, 'createIIRFilter([1], [1, NaN, Infinity])').throw(TypeError);
+
+ should(function() {
+ // Feedforward coefficients must be finite.
+ context.createIIRFilter([1, Infinity, NaN], [1]);
+ }, 'createIIRFilter([1, NaN, Infinity], [1])').throw(TypeError);
+
+ should(function() {
+ // Test that random junk in the array is converted to NaN.
+ context.createIIRFilter([1, 'abc', []], [1]);
+ }, 'createIIRFilter([1, \'abc\', []], [1])').throw(TypeError);
+
+ task.done();
+ });
+
+ audit.define('exceptions-getFrequencyData', (task, should) => {
+ // Create a really simple IIR filter. Doesn't much matter what.
+ let coef = Float32Array.from([1]);
+
+ let f = context.createIIRFilter(coef, coef);
+
+ should(
+ function() {
+ // frequencyHz can't be null.
+ f.getFrequencyResponse(
+ null, new Float32Array(1), new Float32Array(1));
+ },
+ 'getFrequencyResponse(null, new Float32Array(1), new Float32Array(1))')
+ .throw(TypeError);
+
+ should(
+ function() {
+ // magResponse can't be null.
+ f.getFrequencyResponse(
+ new Float32Array(1), null, new Float32Array(1));
+ },
+ 'getFrequencyResponse(new Float32Array(1), null, new Float32Array(1))')
+ .throw(TypeError);
+
+ should(
+ function() {
+ // phaseResponse can't be null.
+ f.getFrequencyResponse(
+ new Float32Array(1), new Float32Array(1), null);
+ },
+ 'getFrequencyResponse(new Float32Array(1), new Float32Array(1), null)')
+ .throw(TypeError);
+
+ should(
+ function() {
+ // magResponse array must the same length as frequencyHz
+ f.getFrequencyResponse(
+ new Float32Array(10), new Float32Array(1),
+ new Float32Array(20));
+ },
+ 'getFrequencyResponse(new Float32Array(10), new Float32Array(1), new Float32Array(20))')
+ .throw(DOMException, 'InvalidAccessError');
+
+ should(
+ function() {
+ // phaseResponse array must be the same length as frequencyHz
+ f.getFrequencyResponse(
+ new Float32Array(10), new Float32Array(20),
+ new Float32Array(1));
+ },
+ 'getFrequencyResponse(new Float32Array(10), new Float32Array(20), new Float32Array(1))')
+ .throw(DOMException, 'InvalidAccessError');
+
+ task.done();
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/iirfilter-getFrequencyResponse.html b/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/iirfilter-getFrequencyResponse.html
new file mode 100644
index 0000000000..c98555f161
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/iirfilter-getFrequencyResponse.html
@@ -0,0 +1,159 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ Test IIRFilter getFrequencyResponse() functionality
+ </title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../resources/audit-util.js"></script>
+ <script src="../../resources/audit.js"></script>
+ <script src="../../resources/biquad-filters.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let sampleRate = 48000;
+ // Some short duration; we're not actually looking at the rendered output.
+ let testDurationSec = 0.01;
+
+ // Number of frequency samples to take.
+ let numberOfFrequencies = 1000;
+
+ let audit = Audit.createTaskRunner();
+
+
+ // Compute a set of linearly spaced frequencies.
+ function createFrequencies(nFrequencies, sampleRate) {
+ let frequencies = new Float32Array(nFrequencies);
+ let nyquist = sampleRate / 2;
+ let freqDelta = nyquist / nFrequencies;
+
+ for (let k = 0; k < nFrequencies; ++k) {
+ frequencies[k] = k * freqDelta;
+ }
+
+ return frequencies;
+ }
+
+ audit.define('1-pole IIR', (task, should) => {
+ let context = new OfflineAudioContext(
+ 1, testDurationSec * sampleRate, sampleRate);
+
+ let iir = context.createIIRFilter([1], [1, -0.9]);
+ let frequencies =
+ createFrequencies(numberOfFrequencies, context.sampleRate);
+
+ let iirMag = new Float32Array(numberOfFrequencies);
+ let iirPhase = new Float32Array(numberOfFrequencies);
+ let trueMag = new Float32Array(numberOfFrequencies);
+ let 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))
+
+ let frequencyScale = Math.PI / (sampleRate / 2);
+
+ for (let k = 0; k < frequencies.length; ++k) {
+ let 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);
+
+ // Thresholds were experimentally determined.
+ should(iirMag, '1-pole IIR Magnitude Response')
+ .beCloseToArray(trueMag, {absoluteThreshold: 2.8611e-6});
+ should(iirPhase, '1-pole IIR Phase Response')
+ .beCloseToArray(truePhase, {absoluteThreshold: 1.7882e-7});
+
+ task.done();
+ });
+
+ audit.define('compare IIR and biquad', (task, should) => {
+ // Create an IIR filter equivalent to the biquad filter. Compute the
+ // frequency response for both and verify that they are the same.
+ let context = new OfflineAudioContext(
+ 1, testDurationSec * sampleRate, sampleRate);
+
+ let biquad = context.createBiquadFilter();
+ let coef = createFilter(
+ biquad.type, biquad.frequency.value / (context.sampleRate / 2),
+ biquad.Q.value, biquad.gain.value);
+
+ let iir = context.createIIRFilter(
+ [coef.b0, coef.b1, coef.b2], [1, coef.a1, coef.a2]);
+
+ let frequencies =
+ createFrequencies(numberOfFrequencies, context.sampleRate);
+ let biquadMag = new Float32Array(numberOfFrequencies);
+ let biquadPhase = new Float32Array(numberOfFrequencies);
+ let iirMag = new Float32Array(numberOfFrequencies);
+ let iirPhase = new Float32Array(numberOfFrequencies);
+
+ biquad.getFrequencyResponse(frequencies, biquadMag, biquadPhase);
+ iir.getFrequencyResponse(frequencies, iirMag, iirPhase);
+
+ // Thresholds were experimentally determined.
+ should(iirMag, 'IIR Magnitude Response').beCloseToArray(biquadMag, {
+ absoluteThreshold: 2.7419e-5
+ });
+ should(iirPhase, 'IIR Phase Response').beCloseToArray(biquadPhase, {
+ absoluteThreshold: 2.7657e-5
+ });
+
+ task.done();
+ });
+
+ audit.define(
+ {
+ label: 'getFrequencyResponse',
+ description: 'Test out-of-bounds frequency values'
+ },
+ (task, should) => {
+ let context = new OfflineAudioContext(1, 1, sampleRate);
+ let filter = new IIRFilterNode(
+ context, {feedforward: [1], feedback: [1, -.9]});
+
+ // Frequencies to test. These are all outside the valid range of
+ // frequencies of 0 to Nyquist.
+ let freq = new Float32Array(2);
+ freq[0] = -1;
+ freq[1] = context.sampleRate / 2 + 1;
+
+ let mag = new Float32Array(freq.length);
+ let phase = new Float32Array(freq.length);
+
+ filter.getFrequencyResponse(freq, mag, phase);
+
+ // Verify that the returned magnitude and phase entries are alL NaN
+ // since the frequencies are outside the valid range
+ for (let k = 0; k < mag.length; ++k) {
+ should(mag[k],
+ 'Magnitude response at frequency ' + freq[k])
+ .beNaN();
+ }
+
+ for (let k = 0; k < phase.length; ++k) {
+ should(phase[k],
+ 'Phase response at frequency ' + freq[k])
+ .beNaN();
+ }
+
+ task.done();
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/iirfilter.html b/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/iirfilter.html
new file mode 100644
index 0000000000..aa38a6bfca
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/iirfilter.html
@@ -0,0 +1,572 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ Test Basic IIRFilterNode Operation
+ </title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../resources/audit-util.js"></script>
+ <script src="../../resources/audit.js"></script>
+ <script src="../../resources/biquad-filters.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let sampleRate = 24000;
+ let testDurationSec = 0.25;
+ let testFrames = testDurationSec * sampleRate;
+
+ let audit = Audit.createTaskRunner();
+
+ audit.define('coefficient-normalization', (task, should) => {
+ // 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.
+ let context = new OfflineAudioContext(2, testFrames, sampleRate);
+
+ // Use a simple impulse as the source.
+ let buffer = context.createBuffer(1, 1, sampleRate);
+ buffer.getChannelData(0)[0] = 1;
+ let source = context.createBufferSource();
+ source.buffer = buffer;
+
+ // Gain node for computing the difference between the filters.
+ let gain = context.createGain();
+ gain.gain.value = -1;
+
+ // The IIR filters. Use a common feedforward array.
+ let ff = [1];
+
+ let fb1 = [1, .9];
+
+ let fb2 = new Float64Array(2);
+ // Scale the feedback coefficients by an arbitrary factor.
+ let coefScaleFactor = 2;
+ for (let k = 0; k < fb2.length; ++k) {
+ fb2[k] = coefScaleFactor * fb1[k];
+ }
+
+ let iir1;
+ let iir2;
+
+ should(function() {
+ iir1 = context.createIIRFilter(ff, fb1);
+ }, 'createIIRFilter with normalized coefficients').notThrow();
+
+ should(function() {
+ iir2 = context.createIIRFilter(ff, fb2);
+ }, 'createIIRFilter with unnormalized coefficients').notThrow();
+
+ // 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.
+ let 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.
+ let iir1Data = result.getChannelData(0);
+ let iir2Data = result.getChannelData(1);
+
+ // Threshold isn't exactly zero because the arithmetic is done
+ // differently between the IIRFilterNode and the BiquadFilterNode.
+ should(
+ iir2Data,
+ 'Output of IIR filter with unnormalized coefficients')
+ .beCloseToArray(iir1Data, {absoluteThreshold: 2.1958e-38});
+ })
+ .then(() => task.done());
+ });
+
+ audit.define('one-zero', (task, should) => {
+ // Create a simple 1-zero filter and compare with the expected output.
+ let context = new OfflineAudioContext(1, testFrames, sampleRate);
+
+ // Use a simple impulse as the source
+ let buffer = context.createBuffer(1, 1, sampleRate);
+ buffer.getChannelData(0)[0] = 1;
+ let 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.
+
+ let 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) {
+ let actual = result.getChannelData(0);
+ let 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(actual, 'IIR 1-zero output').beCloseToArray(expected, {
+ absoluteThreshold: 0
+ });
+ })
+ .then(() => task.done());
+ });
+
+ audit.define('one-pole', (task, should) => {
+ // 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.
+ let c = 0.9;
+ let eps = 1e-20;
+ let duration = Math.floor(Math.log(eps) / Math.log(Math.abs(c)));
+ let context = new OfflineAudioContext(1, duration, sampleRate);
+
+ // Use a simple impulse as the source
+ let buffer = context.createBuffer(1, 1, sampleRate);
+ buffer.getChannelData(0)[0] = 1;
+ let source = context.createBufferSource();
+ source.buffer = buffer;
+
+ let 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) {
+ let actual = result.getChannelData(0);
+ let 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(actual, 'IIR 1-pole output').beCloseToArray(expected, {
+ absoluteThreshold: 2.7657e-8
+ });
+ })
+ .then(() => task.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 (task, should) => {
+ let context = new OfflineAudioContext(2, testFrames, sampleRate);
+
+ // Use a constant (step function) as the source
+ let buffer = createConstantBuffer(context, testFrames, 1);
+ let 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.
+ let 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.
+ let nyquist = sampleRate / 2;
+ let coef = createFilter(
+ filterType, biquad.frequency.value / nyquist, biquad.Q.value,
+ biquad.gain.value);
+
+ let iir = context.createIIRFilter(
+ [coef.b0, coef.b1, coef.b2], [1, coef.a1, coef.a2]);
+
+ let 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.
+ let expected = result.getChannelData(0);
+ let 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(actual, 'IIRFilter for Biquad ' + filterType)
+ .beCloseToArray(expected, errorThreshold);
+
+ let snr = 10 * Math.log10(computeSNR(actual, expected));
+ should(snr, 'SNR for IIRFIlter for Biquad ' + filterType)
+ .beGreaterThanOrEqualTo(snrThreshold);
+ })
+ .then(() => task.done());
+ };
+ }
+
+ // Thresholds here are experimentally determined.
+ let biquadTestConfigs = [
+ {
+ filterType: 'lowpass',
+ snrThreshold: 91.221,
+ errorThreshold: {relativeThreshold: 4.9834e-5}
+ },
+ {
+ filterType: 'highpass',
+ snrThreshold: 105.4590,
+ 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) {
+ let config = biquadTestConfigs[k];
+ let name = k + ': ' + config.filterType;
+ audit.define(
+ name,
+ testWithBiquadFilter(
+ config.filterType, config.errorThreshold, config.snrThreshold));
+ }
+
+ audit.define('multi-channel', (task, should) => {
+ // Multi-channel test. Create a biquad filter and the equivalent IIR
+ // filter. Filter the same multichannel signal and compare the results.
+ let nChannels = 3;
+ let context =
+ new OfflineAudioContext(nChannels, testFrames, sampleRate);
+
+ // Create a set of oscillators as the multi-channel source.
+ let 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;
+ }
+
+ let merger = context.createChannelMerger(3);
+
+ let biquad = context.createBiquadFilter();
+
+ // Create the equivalent IIR Filter node.
+ let nyquist = sampleRate / 2;
+ let coef = createFilter(
+ biquad.type, biquad.frequency.value / nyquist, biquad.Q.value,
+ biquad.gain.value);
+ let fb = [1, coef.a1, coef.a2];
+ let ff = [coef.b0, coef.b1, coef.b2];
+
+ let iir = context.createIIRFilter(ff, fb);
+ // Gain node to compute the difference between the IIR and biquad
+ // filter.
+ let 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) {
+ let 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.
+ let data = result.getChannelData(channel);
+ let maxError =
+ data.reduce(function(reducedValue, currentValue) {
+ return Math.max(reducedValue, Math.abs(currentValue));
+ });
+
+ should(
+ maxError,
+ 'Max difference between IIR and Biquad on channel ' +
+ channel)
+ .beLessThanOrEqualTo(errorThresholds[channel]);
+ }
+
+ })
+ .then(() => task.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.
+ let workSize = Math.max(feedforward.length, feedback.length);
+ let x = new Float32Array(input.length + workSize);
+
+ // Float64 because we want to match the implementation that uses doubles
+ // to minimize roundoff.
+ let y = new Float64Array(input.length + workSize);
+
+ // Copy the input over.
+ for (let k = 0; k < input.length; ++k)
+ x[k + feedforward.length] = input[k];
+
+ // Run the filter
+ for (let n = 0; n < input.length; ++n) {
+ let index = n + workSize;
+ let yn = 0;
+ for (let k = 0; k < feedforward.length; ++k)
+ yn += feedforward[k] * x[index - k];
+ for (let 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.
+
+ let 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
+ ];
+
+ let 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) {
+ let 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)
+ let d = Math.sqrt(-discriminant);
+ return Math.hypot(a1 / 2, d / 2);
+ } else {
+ // Real roots
+ let d = Math.sqrt(discriminant);
+ return Math.max(Math.abs((-a1 + d) / 2), Math.abs((-a1 - d) / 2));
+ }
+ }
+
+ audit.define('4th-order-iir', (task, should) => {
+ // Cascade 2 lowpass biquad filters and compare that with the equivalent
+ // 4th order IIR filter.
+
+ let 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.
+ let biquadType = 'lowpass';
+ let biquadCutoff = 350;
+ let biquadQ = 5;
+ let biquadGain = 1;
+
+ let coef = createFilter(
+ biquadType, biquadCutoff / nyquist, biquadQ, biquadGain);
+
+ // Cascade the biquads together to create an equivalent IIR filter.
+ let 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
+ let 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;
+ let eps = 1e-20;
+ let 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.
+ let context = new OfflineAudioContext(2, framesForTest, sampleRate);
+
+ // Use a simple impulse with a large (arbitrary) amplitude as the source
+ let amplitude = 1;
+ let buffer = context.createBuffer(1, testFrames, sampleRate);
+ buffer.getChannelData(0)[0] = amplitude;
+ let 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.
+ let biquad1 = context.createBiquadFilter();
+ biquad1.type = biquadType;
+ biquad1.frequency.value = biquadCutoff;
+ biquad1.Q.value = biquadQ;
+
+ let biquad2 = context.createBiquadFilter();
+ biquad2.type = biquadType;
+ biquad2.frequency.value = biquadCutoff;
+ biquad2.Q.value = biquadQ;
+
+ let iir = context.createIIRFilter(cascade.ff, cascade.fb);
+
+ // Create the merger to get the signals into multiple channels
+ let 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.
+ let y = iirFilter(buffer.getChannelData(0), cascade.ff, cascade.fb);
+
+ // Rock and roll!
+ source.start();
+
+ context.startRendering()
+ .then(function(result) {
+ let expected = result.getChannelData(0);
+ let actual = result.getChannelData(1);
+
+ should(actual, '4-th order IIRFilter (biquad ref)')
+ .beCloseToArray(expected, {
+ // Thresholds experimentally determined.
+ absoluteThreshold: 1.59e-7,
+ relativeThreshold: 2.11e-5,
+ });
+
+ let snr = 10 * Math.log10(computeSNR(actual, expected));
+ should(snr, 'SNR of 4-th order IIRFilter (biquad ref)')
+ .beGreaterThanOrEqualTo(108.947);
+ })
+ .then(() => task.done());
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/test-iirfilternode.html b/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/test-iirfilternode.html
new file mode 100644
index 0000000000..001a2a6172
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface/test-iirfilternode.html
@@ -0,0 +1,59 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test the IIRFilterNode Interface</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+test(function(t) {
+ var ac = new AudioContext();
+
+ function check_args(arg1, arg2, err, desc) {
+ test(function() {
+ assert_throws_dom(err, function() {
+ ac.createIIRFilter(arg1, arg2)
+ })
+ }, desc)
+ }
+
+ check_args([], [1.0], 'NotSupportedError',
+ 'feedforward coefficients can not be empty');
+
+ check_args([1.0], [], 'NotSupportedError',
+ 'feedback coefficients can not be empty');
+
+ var coeff = new Float32Array(21)
+ coeff[0] = 1.0;
+
+ check_args(coeff, [1.0], 'NotSupportedError',
+ 'more than 20 feedforward coefficients can not be used');
+
+ check_args([1.0], coeff, 'NotSupportedError',
+ 'more than 20 feedback coefficients can not be used');
+
+ check_args([0.0, 0.0], [1.0], 'InvalidStateError',
+ 'at least one feedforward coefficient must be non-zero');
+
+ check_args([0.5, 0.5], [0.0], 'InvalidStateError',
+ 'the first feedback coefficient must be non-zero');
+
+}, "IIRFilterNode coefficients are checked properly");
+
+test(function(t) {
+ var ac = new AudioContext();
+
+ var frequencies = new Float32Array([-1.0, ac.sampleRate*0.5 - 1.0, ac.sampleRate]);
+ var magResults = new Float32Array(3);
+ var phaseResults = new Float32Array(3);
+
+ var filter = ac.createIIRFilter([0.5, 0.5], [1.0]);
+ filter.getFrequencyResponse(frequencies, magResults, phaseResults);
+
+ assert_true(isNaN(magResults[0]), "Invalid input frequency should give NaN magnitude response");
+ assert_true(!isNaN(magResults[1]), "Valid input frequency should not give NaN magnitude response");
+ assert_true(isNaN(magResults[2]), "Invalid input frequency should give NaN magnitude response");
+ assert_true(isNaN(phaseResults[0]), "Invalid input frequency should give NaN phase response");
+ assert_true(!isNaN(phaseResults[1]), "Valid input frequency should not give NaN phase response");
+ assert_true(isNaN(phaseResults[2]), "Invalid input frequency should give NaN phase response");
+
+}, "IIRFilterNode getFrequencyResponse handles invalid frequencies properly");
+</script>