diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /testing/web-platform/tests/webaudio/the-audio-api/the-iirfilternode-interface | |
parent | Initial commit. (diff) | |
download | thunderbird-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')
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> |