summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-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-biquadfilternode-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-biquadfilternode-interface')
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-allpass.html42
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-automation.html406
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-bandpass.html44
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-basic.html134
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-getFrequencyResponse.html394
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-highpass.html42
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-highshelf.html43
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-lowpass.html45
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-lowshelf.html43
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-notch.html43
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-peaking.html46
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-tail.html71
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquadfilternode-basic.html64
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/ctor-biquadfilter.html86
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/no-dezippering.html288
15 files changed, 1791 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-allpass.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-allpass.html
new file mode 100644
index 0000000000..86618f9e46
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-allpass.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ biquad-allpass.html
+ </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/biquad-filters.js"></script>
+ <script src="/webaudio/resources/biquad-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ audit.define(
+ {label: 'test', description: 'Biquad allpass filter'},
+ function(task, should) {
+
+ // Create offline audio context.
+ let context = new OfflineAudioContext(
+ 2, sampleRate * renderLengthSeconds, sampleRate);
+
+ let filterParameters = [
+ {cutoff: 0, q: 10, gain: 1},
+ {cutoff: 1, q: 10, gain: 1},
+ {cutoff: .5, q: 0, gain: 1},
+ {cutoff: 0.25, q: 10, gain: 1},
+ ];
+ createTestAndRun(context, 'allpass', {
+ should: should,
+ threshold: 3.9337e-8,
+ filterParameters: filterParameters
+ }).then(task.done.bind(task));
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-automation.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-automation.html
new file mode 100644
index 0000000000..d459d16fb1
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-automation.html
@@ -0,0 +1,406 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ Biquad Automation Test
+ </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/biquad-filters.js"></script>
+ <script src="/webaudio/resources/audioparam-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ // Don't need to run these tests at high sampling rate, so just use a low
+ // one to reduce memory usage and complexity.
+ let sampleRate = 16000;
+
+ // How long to render for each test.
+ let renderDuration = 0.25;
+ // Where to end the automations. Fairly arbitrary, but must end before
+ // the renderDuration.
+ let automationEndTime = renderDuration / 2;
+
+ let audit = Audit.createTaskRunner();
+
+ // The definition of the linear ramp automation function.
+ function linearRamp(t, v0, v1, t0, t1) {
+ return v0 + (v1 - v0) * (t - t0) / (t1 - t0);
+ }
+
+ // Generate the filter coefficients for the specified filter using the
+ // given parameters for the given duration. |filterTypeFunction| is a
+ // function that returns the filter coefficients for one set of
+ // parameters. |parameters| is a property bag that contains the start and
+ // end values (as an array) for each of the biquad attributes. The
+ // properties are |freq|, |Q|, |gain|, and |detune|. |duration| is the
+ // number of seconds for which the coefficients are generated.
+ //
+ // A property bag with properties |b0|, |b1|, |b2|, |a1|, |a2|. Each
+ // propery is an array consisting of the coefficients for the time-varying
+ // biquad filter.
+ function generateFilterCoefficients(
+ filterTypeFunction, parameters, duration) {
+ let renderEndFrame = Math.ceil(renderDuration * sampleRate);
+ let endFrame = Math.ceil(duration * sampleRate);
+ let nCoef = renderEndFrame;
+ let b0 = new Float64Array(nCoef);
+ let b1 = new Float64Array(nCoef);
+ let b2 = new Float64Array(nCoef);
+ let a1 = new Float64Array(nCoef);
+ let a2 = new Float64Array(nCoef);
+
+ let k = 0;
+ // If the property is not given, use the defaults.
+ let freqs = parameters.freq || [350, 350];
+ let qs = parameters.Q || [1, 1];
+ let gains = parameters.gain || [0, 0];
+ let detunes = parameters.detune || [0, 0];
+
+ for (let frame = 0; frame <= endFrame; ++frame) {
+ // Apply linear ramp at frame |frame|.
+ let f =
+ linearRamp(frame / sampleRate, freqs[0], freqs[1], 0, duration);
+ let q = linearRamp(frame / sampleRate, qs[0], qs[1], 0, duration);
+ let g =
+ linearRamp(frame / sampleRate, gains[0], gains[1], 0, duration);
+ let d = linearRamp(
+ frame / sampleRate, detunes[0], detunes[1], 0, duration);
+
+ // Compute actual frequency parameter
+ f = f * Math.pow(2, d / 1200);
+
+ // Compute filter coefficients
+ let coef = filterTypeFunction(f / (sampleRate / 2), q, g);
+ b0[k] = coef.b0;
+ b1[k] = coef.b1;
+ b2[k] = coef.b2;
+ a1[k] = coef.a1;
+ a2[k] = coef.a2;
+ ++k;
+ }
+
+ // Fill the rest of the arrays with the constant value to the end of
+ // the rendering duration.
+ b0.fill(b0[endFrame], endFrame + 1);
+ b1.fill(b1[endFrame], endFrame + 1);
+ b2.fill(b2[endFrame], endFrame + 1);
+ a1.fill(a1[endFrame], endFrame + 1);
+ a2.fill(a2[endFrame], endFrame + 1);
+
+ return {b0: b0, b1: b1, b2: b2, a1: a1, a2: a2};
+ }
+
+ // Apply the given time-varying biquad filter to the given signal,
+ // |signal|. |coef| should be the time-varying coefficients of the
+ // filter, as returned by |generateFilterCoefficients|.
+ function timeVaryingFilter(signal, coef) {
+ let length = signal.length;
+ // Use double precision for the internal computations.
+ let y = new Float64Array(length);
+
+ // Prime the pump. (Assumes the signal has length >= 2!)
+ y[0] = coef.b0[0] * signal[0];
+ y[1] =
+ coef.b0[1] * signal[1] + coef.b1[1] * signal[0] - coef.a1[1] * y[0];
+
+ for (let n = 2; n < length; ++n) {
+ y[n] = coef.b0[n] * signal[n] + coef.b1[n] * signal[n - 1] +
+ coef.b2[n] * signal[n - 2];
+ y[n] -= coef.a1[n] * y[n - 1] + coef.a2[n] * y[n - 2];
+ }
+
+ // But convert the result to single precision for comparison.
+ return y.map(Math.fround);
+ }
+
+ // Configure the audio graph using |context|. Returns the biquad filter
+ // node and the AudioBuffer used for the source.
+ function configureGraph(context, toneFrequency) {
+ // The source is just a simple sine wave.
+ let src = context.createBufferSource();
+ let b =
+ context.createBuffer(1, renderDuration * sampleRate, sampleRate);
+ let data = b.getChannelData(0);
+ let omega = 2 * Math.PI * toneFrequency / sampleRate;
+ for (let k = 0; k < data.length; ++k) {
+ data[k] = Math.sin(omega * k);
+ }
+ src.buffer = b;
+ let f = context.createBiquadFilter();
+ src.connect(f);
+ f.connect(context.destination);
+
+ src.start();
+
+ return {filter: f, source: b};
+ }
+
+ function createFilterVerifier(
+ should, filterCreator, threshold, parameters, input, message) {
+ return function(resultBuffer) {
+ let actual = resultBuffer.getChannelData(0);
+ let coefs = generateFilterCoefficients(
+ filterCreator, parameters, automationEndTime);
+
+ reference = timeVaryingFilter(input, coefs);
+
+ should(actual, message).beCloseToArray(reference, {
+ absoluteThreshold: threshold
+ });
+ };
+ }
+
+ // Automate just the frequency parameter. A bandpass filter is used where
+ // the center frequency is swept across the source (which is a simple
+ // tone).
+ audit.define('automate-freq', (task, should) => {
+ let context =
+ new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+
+ // Center frequency of bandpass filter and also the frequency of the
+ // test tone.
+ let centerFreq = 10 * 440;
+
+ // Sweep the frequency +/- 5*440 Hz from the center. This should cause
+ // the output to be low at the beginning and end of the test where the
+ // tone is outside the pass band of the filter, but high in the middle
+ // of the automation time where the tone is near the center of the pass
+ // band. Make sure the frequency sweep stays inside the Nyquist
+ // frequency.
+ let parameters = {freq: [centerFreq - 5 * 440, centerFreq + 5 * 440]};
+ let graph = configureGraph(context, centerFreq);
+ let f = graph.filter;
+ let b = graph.source;
+
+ f.type = 'bandpass';
+ f.frequency.setValueAtTime(parameters.freq[0], 0);
+ f.frequency.linearRampToValueAtTime(
+ parameters.freq[1], automationEndTime);
+
+ context.startRendering()
+ .then(createFilterVerifier(
+ should, createBandpassFilter, 4.6455e-6, parameters,
+ b.getChannelData(0),
+ 'Output of bandpass filter with frequency automation'))
+ .then(() => task.done());
+ });
+
+ // Automate just the Q parameter. A bandpass filter is used where the Q
+ // of the filter is swept.
+ audit.define('automate-q', (task, should) => {
+ let context =
+ new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+
+ // The frequency of the test tone.
+ let centerFreq = 440;
+
+ // Sweep the Q paramter between 1 and 200. This will cause the output
+ // of the filter to pass most of the tone at the beginning to passing
+ // less of the tone at the end. This is because we set center frequency
+ // of the bandpass filter to be slightly off from the actual tone.
+ let parameters = {
+ Q: [1, 200],
+ // Center frequency of the bandpass filter is just 25 Hz above the
+ // tone frequency.
+ freq: [centerFreq + 25, centerFreq + 25]
+ };
+ let graph = configureGraph(context, centerFreq);
+ let f = graph.filter;
+ let b = graph.source;
+
+ f.type = 'bandpass';
+ f.frequency.value = parameters.freq[0];
+ f.Q.setValueAtTime(parameters.Q[0], 0);
+ f.Q.linearRampToValueAtTime(parameters.Q[1], automationEndTime);
+
+ context.startRendering()
+ .then(createFilterVerifier(
+ should, createBandpassFilter, 1.0133e-6, parameters,
+ b.getChannelData(0),
+ 'Output of bandpass filter with Q automation'))
+ .then(() => task.done());
+ });
+
+ // Automate just the gain of the lowshelf filter. A test tone will be in
+ // the lowshelf part of the filter. The output will vary as the gain of
+ // the lowshelf is changed.
+ audit.define('automate-gain', (task, should) => {
+ let context =
+ new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+
+ // Frequency of the test tone.
+ let centerFreq = 440;
+
+ // Set the cutoff frequency of the lowshelf to be significantly higher
+ // than the test tone. Sweep the gain from 20 dB to -20 dB. (We go from
+ // 20 to -20 to easily verify that the filter didn't go unstable.)
+ let parameters = {freq: [3500, 3500], gain: [20, -20]};
+ let graph = configureGraph(context, centerFreq);
+ let f = graph.filter;
+ let b = graph.source;
+
+ f.type = 'lowshelf';
+ f.frequency.value = parameters.freq[0];
+ f.gain.setValueAtTime(parameters.gain[0], 0);
+ f.gain.linearRampToValueAtTime(parameters.gain[1], automationEndTime);
+
+ context.startRendering()
+ .then(createFilterVerifier(
+ should, createLowShelfFilter, 2.7657e-5, parameters,
+ b.getChannelData(0),
+ 'Output of lowshelf filter with gain automation'))
+ .then(() => task.done());
+ });
+
+ // Automate just the detune parameter. Basically the same test as for the
+ // frequncy parameter but we just use the detune parameter to modulate the
+ // frequency parameter.
+ audit.define('automate-detune', (task, should) => {
+ let context =
+ new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+ let centerFreq = 10 * 440;
+ let parameters = {
+ freq: [centerFreq, centerFreq],
+ detune: [-10 * 1200, 10 * 1200]
+ };
+ let graph = configureGraph(context, centerFreq);
+ let f = graph.filter;
+ let b = graph.source;
+
+ f.type = 'bandpass';
+ f.frequency.value = parameters.freq[0];
+ f.detune.setValueAtTime(parameters.detune[0], 0);
+ f.detune.linearRampToValueAtTime(
+ parameters.detune[1], automationEndTime);
+
+ context.startRendering()
+ .then(createFilterVerifier(
+ should, createBandpassFilter, 3.1471e-5, parameters,
+ b.getChannelData(0),
+ 'Output of bandpass filter with detune automation'))
+ .then(() => task.done());
+ });
+
+ // Automate all of the filter parameters at once. This is a basic check
+ // that everything is working. A peaking filter is used because it uses
+ // all of the parameters.
+ audit.define('automate-all', (task, should) => {
+ let context =
+ new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+ let graph = configureGraph(context, 10 * 440);
+ let f = graph.filter;
+ let b = graph.source;
+
+ // Sweep all of the filter parameters. These are pretty much arbitrary.
+ let parameters = {
+ freq: [8000, 100],
+ Q: [f.Q.value, .0001],
+ gain: [f.gain.value, 20],
+ detune: [2400, -2400]
+ };
+
+ f.type = 'peaking';
+ // Set starting points for all parameters of the filter. Start at 10
+ // kHz for the center frequency, and the defaults for Q and gain.
+ f.frequency.setValueAtTime(parameters.freq[0], 0);
+ f.Q.setValueAtTime(parameters.Q[0], 0);
+ f.gain.setValueAtTime(parameters.gain[0], 0);
+ f.detune.setValueAtTime(parameters.detune[0], 0);
+
+ // Linear ramp each parameter
+ f.frequency.linearRampToValueAtTime(
+ parameters.freq[1], automationEndTime);
+ f.Q.linearRampToValueAtTime(parameters.Q[1], automationEndTime);
+ f.gain.linearRampToValueAtTime(parameters.gain[1], automationEndTime);
+ f.detune.linearRampToValueAtTime(
+ parameters.detune[1], automationEndTime);
+
+ context.startRendering()
+ .then(createFilterVerifier(
+ should, createPeakingFilter, 6.2907e-4, parameters,
+ b.getChannelData(0),
+ 'Output of peaking filter with automation of all parameters'))
+ .then(() => task.done());
+ });
+
+ // Test that modulation of the frequency parameter of the filter works. A
+ // sinusoid of 440 Hz is the test signal that is applied to a bandpass
+ // biquad filter. The frequency parameter of the filter is modulated by a
+ // sinusoid at 103 Hz, and the frequency modulation varies from 116 to 412
+ // Hz. (This test was taken from the description in
+ // https://github.com/WebAudio/web-audio-api/issues/509#issuecomment-94731355)
+ audit.define('modulation', (task, should) => {
+ let context =
+ new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+
+ // Create a graph with the sinusoidal source at 440 Hz as the input to a
+ // biquad filter.
+ let graph = configureGraph(context, 440);
+ let f = graph.filter;
+ let b = graph.source;
+
+ f.type = 'bandpass';
+ f.Q.value = 5;
+ f.frequency.value = 264;
+
+ // Create the modulation source, a sinusoid with frequency 103 Hz and
+ // amplitude 148. (The amplitude of 148 is added to the filter's
+ // frequency value of 264 to produce a sinusoidal modulation of the
+ // frequency parameter from 116 to 412 Hz.)
+ let mod = context.createBufferSource();
+ let mbuffer =
+ context.createBuffer(1, renderDuration * sampleRate, sampleRate);
+ let d = mbuffer.getChannelData(0);
+ let omega = 2 * Math.PI * 103 / sampleRate;
+ for (let k = 0; k < d.length; ++k) {
+ d[k] = 148 * Math.sin(omega * k);
+ }
+ mod.buffer = mbuffer;
+
+ mod.connect(f.frequency);
+
+ mod.start();
+ context.startRendering()
+ .then(function(resultBuffer) {
+ let actual = resultBuffer.getChannelData(0);
+ // Compute the filter coefficients using the mod sine wave
+
+ let endFrame = Math.ceil(renderDuration * sampleRate);
+ let nCoef = endFrame;
+ let b0 = new Float64Array(nCoef);
+ let b1 = new Float64Array(nCoef);
+ let b2 = new Float64Array(nCoef);
+ let a1 = new Float64Array(nCoef);
+ let a2 = new Float64Array(nCoef);
+
+ // Generate the filter coefficients when the frequency varies from
+ // 116 to 248 Hz using the 103 Hz sinusoid.
+ for (let k = 0; k < nCoef; ++k) {
+ let freq = f.frequency.value + d[k];
+ let c = createBandpassFilter(
+ freq / (sampleRate / 2), f.Q.value, f.gain.value);
+ b0[k] = c.b0;
+ b1[k] = c.b1;
+ b2[k] = c.b2;
+ a1[k] = c.a1;
+ a2[k] = c.a2;
+ }
+ reference = timeVaryingFilter(
+ b.getChannelData(0),
+ {b0: b0, b1: b1, b2: b2, a1: a1, a2: a2});
+
+ should(
+ actual,
+ 'Output of bandpass filter with sinusoidal modulation of bandpass center frequency')
+ .beCloseToArray(reference, {absoluteThreshold: 3.9787e-5});
+ })
+ .then(() => task.done());
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-bandpass.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-bandpass.html
new file mode 100644
index 0000000000..166aa9b3cb
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-bandpass.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ biquad-bandpass.html
+ </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/biquad-filters.js"></script>
+ <script src="/webaudio/resources/biquad-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ audit.define(
+ {label: 'test', description: 'Biquad bandpass filter.'},
+ function(task, should) {
+
+ // Create offline audio context.
+ let context = new OfflineAudioContext(
+ 2, sampleRate * renderLengthSeconds, sampleRate);
+
+ // The filters we want to test.
+ let filterParameters = [
+ {cutoff: 0, q: 0, gain: 1},
+ {cutoff: 1, q: 0, gain: 1},
+ {cutoff: 0.5, q: 0, gain: 1},
+ {cutoff: 0.25, q: 1, gain: 1},
+ ];
+
+ createTestAndRun(context, 'bandpass', {
+ should: should,
+ threshold: 2.2501e-8,
+ filterParameters: filterParameters
+ }).then(task.done.bind(task));
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-basic.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-basic.html
new file mode 100644
index 0000000000..441e98a251
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-basic.html
@@ -0,0 +1,134 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ Test Basic BiquadFilterNode Properties
+ </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>
+ </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.createBiquadFilter, 'context.createBiquadFilter')
+ .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.createBiquadFilter(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-createBiquadFilter', (task, should) => {
+ should(function() {
+ // Two args are required.
+ context.createBiquadFilter();
+ }, 'createBiquadFilter()').notThrow();
+
+ 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.createBiquadFilter(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-biquadfilternode-interface/biquad-getFrequencyResponse.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-getFrequencyResponse.html
new file mode 100644
index 0000000000..23222e4df9
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-getFrequencyResponse.html
@@ -0,0 +1,394 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ Test BiquadFilter getFrequencyResponse() functionality
+ </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/biquad-filters.js"></script>
+ <script src="/webaudio/resources/biquad-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ // Test the frequency response of a biquad filter. We compute the
+ // frequency response for a simple peaking biquad filter and compare it
+ // with the expected frequency response. The actual filter used doesn't
+ // matter since we're testing getFrequencyResponse and not the actual
+ // filter output. The filters are extensively tested in other biquad
+ // tests.
+
+ // The magnitude response of the biquad filter.
+ let magResponse;
+
+ // The phase response of the biquad filter.
+ let phaseResponse;
+
+ // Number of frequency samples to take.
+ let numberOfFrequencies = 1000;
+
+ // The filter parameters.
+ let filterCutoff = 1000; // Hz.
+ let filterQ = 1;
+ let filterGain = 5; // Decibels.
+
+ // The magnitudes and phases of the reference frequency response.
+ let expectedMagnitudes;
+ let expectedPhases;
+
+ // Convert frequency in Hz to a normalized frequency between 0 to 1 with 1
+ // corresponding to the Nyquist frequency.
+ function normalizedFrequency(freqHz, sampleRate) {
+ let nyquist = sampleRate / 2;
+ return freqHz / nyquist;
+ }
+
+ // Get the filter response at a (normalized) frequency |f| for the filter
+ // with coefficients |coef|.
+ function getResponseAt(coef, f) {
+ let b0 = coef.b0;
+ let b1 = coef.b1;
+ let b2 = coef.b2;
+ let a1 = coef.a1;
+ let a2 = coef.a2;
+
+ // H(z) = (b0 + b1 / z + b2 / z^2) / (1 + a1 / z + a2 / z^2)
+ //
+ // Compute H(exp(i * pi * f)). No native complex numbers in javascript,
+ // so break H(exp(i * pi * // f)) in to the real and imaginary parts of
+ // the numerator and denominator. Let omega = pi * f. Then the
+ // numerator is
+ //
+ // b0 + b1 * cos(omega) + b2 * cos(2 * omega) - i * (b1 * sin(omega) +
+ // b2 * sin(2 * omega))
+ //
+ // and the denominator is
+ //
+ // 1 + a1 * cos(omega) + a2 * cos(2 * omega) - i * (a1 * sin(omega) + a2
+ // * sin(2 * omega))
+ //
+ // Compute the magnitude and phase from the real and imaginary parts.
+
+ let omega = Math.PI * f;
+ let numeratorReal =
+ b0 + b1 * Math.cos(omega) + b2 * Math.cos(2 * omega);
+ let numeratorImag = -(b1 * Math.sin(omega) + b2 * Math.sin(2 * omega));
+ let denominatorReal =
+ 1 + a1 * Math.cos(omega) + a2 * Math.cos(2 * omega);
+ let denominatorImag =
+ -(a1 * Math.sin(omega) + a2 * Math.sin(2 * omega));
+
+ let magnitude = Math.sqrt(
+ (numeratorReal * numeratorReal + numeratorImag * numeratorImag) /
+ (denominatorReal * denominatorReal +
+ denominatorImag * denominatorImag));
+ let phase = Math.atan2(numeratorImag, numeratorReal) -
+ Math.atan2(denominatorImag, denominatorReal);
+
+ if (phase >= Math.PI) {
+ phase -= 2 * Math.PI;
+ } else if (phase <= -Math.PI) {
+ phase += 2 * Math.PI;
+ }
+
+ return {magnitude: magnitude, phase: phase};
+ }
+
+ // Compute the reference frequency response for the biquad filter |filter|
+ // at the frequency samples given by |frequencies|.
+ function frequencyResponseReference(filter, frequencies) {
+ let sampleRate = filter.context.sampleRate;
+ let normalizedFreq =
+ normalizedFrequency(filter.frequency.value, sampleRate);
+ let filterCoefficients = createFilter(
+ filter.type, normalizedFreq, filter.Q.value, filter.gain.value);
+
+ let magnitudes = [];
+ let phases = [];
+
+ for (let k = 0; k < frequencies.length; ++k) {
+ let response = getResponseAt(
+ filterCoefficients,
+ normalizedFrequency(frequencies[k], sampleRate));
+ magnitudes.push(response.magnitude);
+ phases.push(response.phase);
+ }
+
+ return {magnitudes: magnitudes, phases: phases};
+ }
+
+ // 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;
+ }
+
+ function linearToDecibels(x) {
+ if (x) {
+ return 20 * Math.log(x) / Math.LN10;
+ } else {
+ return -1000;
+ }
+ }
+
+ function decibelsToLinear(x) {
+ return Math.pow(10, x/20);
+ }
+
+ // Look through the array and find any NaN or infinity. Returns the index
+ // of the first occurence or -1 if none.
+ function findBadNumber(signal) {
+ for (let k = 0; k < signal.length; ++k) {
+ if (!isValidNumber(signal[k])) {
+ return k;
+ }
+ }
+ return -1;
+ }
+
+ // Compute absolute value of the difference between phase angles, taking
+ // into account the wrapping of phases.
+ function absolutePhaseDifference(x, y) {
+ let diff = Math.abs(x - y);
+
+ if (diff > Math.PI) {
+ diff = 2 * Math.PI - diff;
+ }
+ return diff;
+ }
+
+ // Compare the frequency response with our expected response.
+ //
+ // should - The |should| method provided by audit.define
+ // filter - The filter node used in the test
+ // frequencies - array of frequencies provided to |getFrequencyResponse|
+ // magResponse - mag response from |getFrequencyResponse|
+ // phaseResponse - phase response from |getFrequencyResponse|
+ // maxAllowedMagError - error threshold for mag response, in dB
+ // maxAllowedPhaseError - error threshold for phase response, in rad.
+ function compareResponses(
+ should, filter, frequencies, magResponse, phaseResponse,
+ maxAllowedMagError, maxAllowedPhaseError) {
+ let expectedResponse = frequencyResponseReference(filter, frequencies);
+
+ expectedMagnitudes = expectedResponse.magnitudes;
+ expectedPhases = expectedResponse.phases;
+
+ let n = magResponse.length;
+ let badResponse = false;
+
+ let maxMagError = -1;
+ let maxMagErrorIndex = -1;
+
+ let k;
+ let hasBadNumber;
+
+ hasBadNumber = findBadNumber(magResponse);
+ badResponse =
+ !should(
+ hasBadNumber >= 0 ? 1 : 0,
+ filter.type +
+ ': Number of non-finite values in magnitude response')
+ .beEqualTo(0);
+
+ hasBadNumber = findBadNumber(phaseResponse);
+ badResponse =
+ !should(
+ hasBadNumber >= 0 ? 1 : 0,
+ filter.type + ': Number of non-finte values in phase response')
+ .beEqualTo(0);
+
+ // These aren't testing the implementation itself. Instead, these are
+ // sanity checks on the reference. Failure here does not imply an error
+ // in the implementation.
+ hasBadNumber = findBadNumber(expectedMagnitudes);
+ badResponse =
+ !should(
+ hasBadNumber >= 0 ? 1 : 0,
+ filter.type +
+ ': Number of non-finite values in the expected magnitude response')
+ .beEqualTo(0);
+
+ hasBadNumber = findBadNumber(expectedPhases);
+ badResponse =
+ !should(
+ hasBadNumber >= 0 ? 1 : 0,
+ filter.type +
+ ': Number of non-finite values in expected phase response')
+ .beEqualTo(0);
+
+ // If we found a NaN or infinity, the following tests aren't very
+ // helpful, especially for NaN. We run them anyway, after printing a
+ // warning message.
+ should(
+ !badResponse,
+ filter.type +
+ ': Actual and expected results contained only finite values')
+ .beTrue();
+
+ for (k = 0; k < n; ++k) {
+ let error = Math.abs(
+ linearToDecibels(magResponse[k]) -
+ linearToDecibels(expectedMagnitudes[k]));
+ if (error > maxMagError) {
+ maxMagError = error;
+ maxMagErrorIndex = k;
+ }
+ }
+
+ should(
+ linearToDecibels(maxMagError),
+ filter.type + ': Max error (' + linearToDecibels(maxMagError) +
+ ' dB) of magnitude response at frequency ' +
+ frequencies[maxMagErrorIndex] + ' Hz')
+ .beLessThanOrEqualTo(linearToDecibels(maxAllowedMagError));
+ let maxPhaseError = -1;
+ let maxPhaseErrorIndex = -1;
+
+ for (k = 0; k < n; ++k) {
+ let error =
+ absolutePhaseDifference(phaseResponse[k], expectedPhases[k]);
+ if (error > maxPhaseError) {
+ maxPhaseError = error;
+ maxPhaseErrorIndex = k;
+ }
+ }
+
+ should(
+ radToDegree(maxPhaseError),
+ filter.type + ': Max error (' + radToDegree(maxPhaseError) +
+ ' deg) in phase response at frequency ' +
+ frequencies[maxPhaseErrorIndex] + ' Hz')
+ .beLessThanOrEqualTo(radToDegree(maxAllowedPhaseError));
+ }
+
+ function radToDegree(rad) {
+ // Radians to degrees
+ return rad * 180 / Math.PI;
+ }
+
+ // Test the getFrequencyResponse for each of filter types. Each entry in
+ // this array is a dictionary with these elements:
+ //
+ // type: filter type to be tested
+ // maxErrorInMagnitude: Allowed error in computed magnitude response
+ // maxErrorInPhase: Allowed error in computed magnitude phase
+ [{
+ type: 'lowpass',
+ maxErrorInMagnitude: decibelsToLinear(-73.0178),
+ maxErrorInPhase: 8.04360e-6
+ },
+ {
+ type: 'highpass',
+ maxErrorInMagnitude: decibelsToLinear(-117.5461),
+ maxErrorInPhase: 6.9691e-6
+ },
+ {
+ type: 'bandpass',
+ maxErrorInMagnitude: decibelsToLinear(-79.0139),
+ maxErrorInPhase: 4.9371e-6
+ },
+ {
+ type: 'lowshelf',
+ maxErrorInMagnitude: decibelsToLinear(-120.4038),
+ maxErrorInPhase: 4.0724e-6
+ },
+ {
+ type: 'highshelf',
+ maxErrorInMagnitude: decibelsToLinear(-120, 1303),
+ maxErrorInPhase: 4.0724e-6
+ },
+ {
+ type: 'peaking',
+ maxErrorInMagnitude: decibelsToLinear(-119.1176),
+ maxErrorInPhase: 6.4724e-8
+ },
+ {
+ type: 'notch',
+ maxErrorInMagnitude: decibelsToLinear(-87.0808),
+ maxErrorInPhase: 6.6300e-6
+ },
+ {
+ type: 'allpass',
+ maxErrorInMagnitude: decibelsToLinear(-265.3517),
+ maxErrorInPhase: 1.3260e-5
+ }].forEach(test => {
+ audit.define(
+ {label: test.type, description: 'Frequency response'},
+ (task, should) => {
+ let context = new AudioContext();
+
+ let filter = new BiquadFilterNode(context, {
+ type: test.type,
+ frequency: filterCutoff,
+ Q: filterQ,
+ gain: filterGain
+ });
+
+ let frequencies =
+ createFrequencies(numberOfFrequencies, context.sampleRate);
+ magResponse = new Float32Array(numberOfFrequencies);
+ phaseResponse = new Float32Array(numberOfFrequencies);
+
+ filter.getFrequencyResponse(
+ frequencies, magResponse, phaseResponse);
+ compareResponses(
+ should, filter, frequencies, magResponse, phaseResponse,
+ test.maxErrorInMagnitude, test.maxErrorInPhase);
+
+ 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 BiquadFilterNode(context);
+
+ // 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-biquadfilternode-interface/biquad-highpass.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-highpass.html
new file mode 100644
index 0000000000..45c335bc4b
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-highpass.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ biquad-highpass.html
+ </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/biquad-filters.js"></script>
+ <script src="/webaudio/resources/biquad-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ audit.define(
+ {label: 'test', description: 'Biquad highpass filter'},
+ function(task, should) {
+ // Create offline audio context.
+ let context = new OfflineAudioContext(
+ 2, sampleRate * renderLengthSeconds, sampleRate);
+
+ // The filters we want to test.
+ let filterParameters = [
+ {cutoff: 0, q: 1, gain: 1},
+ {cutoff: 1, q: 1, gain: 1},
+ {cutoff: 0.25, q: 1, gain: 1},
+ ];
+
+ createTestAndRun(context, 'highpass', {
+ should: should,
+ threshold: 1.5487e-8,
+ filterParameters: filterParameters
+ }).then(task.done.bind(task));
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-highshelf.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-highshelf.html
new file mode 100644
index 0000000000..345195f104
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-highshelf.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ biquad-highshelf.html
+ </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/biquad-filters.js"></script>
+ <script src="/webaudio/resources/biquad-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ audit.define(
+ {label: 'test', description: 'Biquad highshelf filter'},
+ function(task, should) {
+
+ // Create offline audio context.
+ let context = new OfflineAudioContext(
+ 2, sampleRate * renderLengthSeconds, sampleRate);
+
+ // The filters we want to test.
+ let filterParameters = [
+ {cutoff: 0, q: 10, gain: 10},
+ {cutoff: 1, q: 10, gain: 10},
+ {cutoff: 0.25, q: 10, gain: 10},
+ ];
+
+ createTestAndRun(context, 'highshelf', {
+ should: should,
+ threshold: 6.2577e-8,
+ filterParameters: filterParameters
+ }).then(task.done.bind(task));
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-lowpass.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-lowpass.html
new file mode 100644
index 0000000000..d20786e36b
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-lowpass.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ biquad-lowpass.html
+ </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/biquad-filters.js"></script>
+ <script src="/webaudio/resources/biquad-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ audit.define(
+ {label: 'test', description: 'Biquad lowpass filter'},
+ function(task, should) {
+
+ // Create offline audio context.
+ let context = new OfflineAudioContext(
+ 2, sampleRate * renderLengthSeconds, sampleRate);
+
+ // The filters we want to test.
+ let filterParameters = [
+ {cutoff: 0, q: 1, gain: 1},
+ {cutoff: 1, q: 1, gain: 1},
+ {cutoff: 0.25, q: 1, gain: 1},
+ {cutoff: 0.25, q: 1, gain: 1, detune: 100},
+ {cutoff: 0.01, q: 1, gain: 1, detune: -200},
+ ];
+
+ createTestAndRun(context, 'lowpass', {
+ should: should,
+ threshold: 9.7869e-8,
+ filterParameters: filterParameters
+ }).then(task.done.bind(task));
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-lowshelf.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-lowshelf.html
new file mode 100644
index 0000000000..ab76cefd4b
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-lowshelf.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ biquad-lowshelf.html
+ </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/biquad-filters.js"></script>
+ <script src="/webaudio/resources/biquad-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ audit.define(
+ {label: 'test', description: 'Biquad lowshelf filter'},
+ function(task, should) {
+
+ // Create offline audio context.
+ let context = new OfflineAudioContext(
+ 2, sampleRate * renderLengthSeconds, sampleRate);
+
+ // The filters we want to test.
+ let filterParameters = [
+ {cutoff: 0, q: 10, gain: 10},
+ {cutoff: 1, q: 10, gain: 10},
+ {cutoff: 0.25, q: 10, gain: 10},
+ ];
+
+ createTestAndRun(context, 'lowshelf', {
+ should: should,
+ threshold: 3.8349e-8,
+ filterParameters: filterParameters
+ }).then(task.done.bind(task));
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-notch.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-notch.html
new file mode 100644
index 0000000000..98e6e6e02c
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-notch.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ biquad-notch.html
+ </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/biquad-filters.js"></script>
+ <script src="/webaudio/resources/biquad-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ audit.define(
+ {label: 'test', description: 'Biquad notch filter'},
+ function(task, should) {
+
+ // Create offline audio context.
+ let context = new OfflineAudioContext(
+ 2, sampleRate * renderLengthSeconds, sampleRate);
+
+ let filterParameters = [
+ {cutoff: 0, q: 10, gain: 1},
+ {cutoff: 1, q: 10, gain: 1},
+ {cutoff: .5, q: 0, gain: 1},
+ {cutoff: 0.25, q: 10, gain: 1},
+ ];
+
+ createTestAndRun(context, 'notch', {
+ should: should,
+ threshold: 1.9669e-8,
+ filterParameters: filterParameters
+ }).then(task.done.bind(task));
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-peaking.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-peaking.html
new file mode 100644
index 0000000000..90b7c1546d
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-peaking.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ biquad-peaking.html
+ </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/biquad-filters.js"></script>
+ <script src="/webaudio/resources/biquad-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ audit.define(
+ {label: 'test', description: 'Biquad peaking filter'},
+ function(task, should) {
+
+ window.jsTestIsAsync = true;
+
+ // Create offline audio context.
+ let context = new OfflineAudioContext(
+ 2, sampleRate * renderLengthSeconds, sampleRate);
+
+ // The filters we want to test.
+ let filterParameters = [
+ {cutoff: 0, q: 10, gain: 10},
+ {cutoff: 1, q: 10, gain: 10},
+ {cutoff: .5, q: 0, gain: 10},
+ {cutoff: 0.25, q: 10, gain: 10},
+ ];
+
+ createTestAndRun(context, 'peaking', {
+ should: should,
+ threshold: 5.8234e-8,
+ filterParameters: filterParameters
+ }).then(task.done.bind(task));
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-tail.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-tail.html
new file mode 100644
index 0000000000..3141bf7ff3
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-tail.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ Test Biquad Tail Output
+ </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>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ // A high sample rate shows the issue more clearly.
+ let sampleRate = 192000;
+ // Some short duration because we don't need to run the test for very
+ // long.
+ let testDurationSec = 0.5;
+ let testDurationFrames = testDurationSec * sampleRate;
+
+ // Amplitude experimentally determined to give a biquad output close to 1.
+ // (No attempt was made to produce exactly 1; it's not needed.)
+ let sourceAmplitude = 100;
+
+ // The output of the biquad filter should not change by more than this
+ // much between output samples. Threshold was determined experimentally.
+ let glitchThreshold = 0.012968;
+
+ // Test that a Biquad filter doesn't have it's output terminated because
+ // the input has gone away. Generally, when a source node is finished, it
+ // disconnects itself from any downstream nodes. This is the correct
+ // behavior. Nodes that have no inputs (disconnected) are generally
+ // assumed to output zeroes. This is also desired behavior. However,
+ // biquad filters have memory so they should not suddenly output zeroes
+ // when the input is disconnected. This test checks to see if the output
+ // doesn't suddenly change to zero.
+ audit.define(
+ {label: 'test', description: 'Biquad Tail Output'},
+ function(task, should) {
+ let context =
+ new OfflineAudioContext(1, testDurationFrames, sampleRate);
+
+ // Create an impulse source.
+ let buffer = context.createBuffer(1, 1, context.sampleRate);
+ buffer.getChannelData(0)[0] = sourceAmplitude;
+ let source = context.createBufferSource();
+ source.buffer = buffer;
+
+ // Create the biquad filter. It doesn't really matter what kind, so
+ // the default filter type and parameters is fine. Connect the
+ // source to it.
+ let biquad = context.createBiquadFilter();
+ source.connect(biquad);
+ biquad.connect(context.destination);
+
+ source.start();
+
+ context.startRendering().then(function(result) {
+ // There should be no large discontinuities in the output
+ should(result.getChannelData(0), 'Biquad output')
+ .notGlitch(glitchThreshold);
+ task.done();
+ })
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquadfilternode-basic.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquadfilternode-basic.html
new file mode 100644
index 0000000000..7e71d07302
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquadfilternode-basic.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ biquadfilternode-basic.html
+ </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>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ audit.define(
+ {label: 'test', description: 'Basic tests for BiquadFilterNode'},
+ function(task, should) {
+
+ let context = new AudioContext();
+ let filter = context.createBiquadFilter();
+
+ should(filter.numberOfInputs, 'Number of inputs').beEqualTo(1);
+
+ should(filter.numberOfOutputs, 'Number of outputs').beEqualTo(1);
+
+ should(filter.type, 'Default filter type').beEqualTo('lowpass');
+
+ should(filter.frequency.value, 'Default frequency value')
+ .beEqualTo(350);
+
+ should(filter.Q.value, 'Default Q value').beEqualTo(1);
+
+ should(filter.gain.value, 'Default gain value').beEqualTo(0);
+
+ // Check that all legal filter types can be set.
+ let filterTypeArray = [
+ {type: 'lowpass'}, {type: 'highpass'}, {type: 'bandpass'},
+ {type: 'lowshelf'}, {type: 'highshelf'}, {type: 'peaking'},
+ {type: 'notch'}, {type: 'allpass'}
+ ];
+
+ for (let i = 0; i < filterTypeArray.length; ++i) {
+ should(
+ () => filter.type = filterTypeArray[i].type,
+ 'Setting filter.type to ' + filterTypeArray[i].type)
+ .notThrow();
+ should(filter.type, 'Filter type is')
+ .beEqualTo(filterTypeArray[i].type);
+ }
+
+
+ // Check that numerical values are no longer supported
+ filter.type = 99;
+ should(filter.type, 'Setting filter.type to (invalid) 99')
+ .notBeEqualTo(99);
+
+ task.done();
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/ctor-biquadfilter.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/ctor-biquadfilter.html
new file mode 100644
index 0000000000..e63479f985
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/ctor-biquadfilter.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ Test Constructor: BiquadFilter
+ </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, 'BiquadFilterNode', context);
+ task.done();
+ });
+
+ audit.define('default constructor', (task, should) => {
+ let prefix = 'node0';
+ let node = testDefaultConstructor(should, 'BiquadFilterNode', context, {
+ prefix: prefix,
+ numberOfInputs: 1,
+ numberOfOutputs: 1,
+ channelCount: 2,
+ channelCountMode: 'max',
+ channelInterpretation: 'speakers'
+ });
+
+ testDefaultAttributes(should, node, prefix, [
+ {name: 'type', value: 'lowpass'}, {name: 'Q', value: 1},
+ {name: 'detune', value: 0}, {name: 'frequency', value: 350},
+ {name: 'gain', value: 0.0}
+ ]);
+
+ task.done();
+ });
+
+ audit.define('test AudioNodeOptions', (task, should) => {
+ testAudioNodeOptions(should, context, 'BiquadFilterNode');
+ task.done();
+ });
+
+ audit.define('construct with options', (task, should) => {
+ let node;
+ let options = {
+ type: 'highpass',
+ frequency: 512,
+ detune: 1,
+ Q: 5,
+ gain: 3,
+ };
+
+ should(
+ () => {
+ node = new BiquadFilterNode(context, options);
+ },
+ 'node = new BiquadFilterNode(..., ' + JSON.stringify(options) + ')')
+ .notThrow();
+
+ // Test that attributes are set according to the option values.
+ should(node.type, 'node.type').beEqualTo(options.type);
+ should(node.frequency.value, 'node.frequency.value')
+ .beEqualTo(options.frequency);
+ should(node.detune.value, 'node.detuen.value')
+ .beEqualTo(options.detune);
+ should(node.Q.value, 'node.Q.value').beEqualTo(options.Q);
+ should(node.gain.value, 'node.gain.value').beEqualTo(options.gain);
+
+ task.done();
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/no-dezippering.html b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/no-dezippering.html
new file mode 100644
index 0000000000..79dc27035c
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/no-dezippering.html
@@ -0,0 +1,288 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ biquad-bandpass.html
+ </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/biquad-filters.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ // In the tests below, the initial values are not important, except that
+ // we wanted them to be all different so that the output contains
+ // different values for the first few samples. Otherwise, the actual
+ // values don't really matter. A peaking filter is used because the
+ // frequency, Q, gain, and detune parameters are used by this filter.
+ //
+ // Also, for the changeList option, the times and new values aren't really
+ // important. They just need to change so that we can verify that the
+ // outputs from the .value setter still matches the output from the
+ // corresponding setValueAtTime.
+ audit.define(
+ {label: 'Test 0', description: 'No dezippering for frequency'},
+ (task, should) => {
+ doTest(should, {
+ paramName: 'frequency',
+ initializer: {type: 'peaking', Q: 1, gain: 5},
+ changeList:
+ [{quantum: 2, newValue: 800}, {quantum: 7, newValue: 200}],
+ threshold: 3.0399e-6
+ }).then(() => task.done());
+ });
+
+ audit.define(
+ {label: 'Test 1', description: 'No dezippering for detune'},
+ (task, should) => {
+ doTest(should, {
+ paramName: 'detune',
+ initializer:
+ {type: 'peaking', frequency: 400, Q: 3, detune: 33, gain: 10},
+ changeList:
+ [{quantum: 2, newValue: 1000}, {quantum: 5, newValue: -400}],
+ threshold: 4.0532e-6
+ }).then(() => task.done());
+ });
+
+ audit.define(
+ {label: 'Test 2', description: 'No dezippering for Q'},
+ (task, should) => {
+ doTest(should, {
+ paramName: 'Q',
+ initializer: {type: 'peaking', Q: 5},
+ changeList:
+ [{quantum: 2, newValue: 10}, {quantum: 8, newValue: -10}]
+ }).then(() => task.done());
+ });
+
+ audit.define(
+ {label: 'Test 3', description: 'No dezippering for gain'},
+ (task, should) => {
+ doTest(should, {
+ paramName: 'gain',
+ initializer: {type: 'peaking', gain: 1},
+ changeList:
+ [{quantum: 2, newValue: 5}, {quantum: 6, newValue: -.3}],
+ threshold: 1.9074e-6
+ }).then(() => task.done());
+ });
+
+ // This test compares the filter output against a JS implementation of the
+ // filter. We're only testing a change in the frequency for a lowpass
+ // filter. This assumes we don't need to test other AudioParam changes
+ // with JS code because any mistakes would be exposed in the tests above.
+ audit.define(
+ {
+ label: 'Test 4',
+ description: 'No dezippering of frequency vs JS filter'
+ },
+ (task, should) => {
+ // Channel 0 is the source, channel 1 is the filtered output.
+ let context = new OfflineAudioContext(2, 2048, 16384);
+
+ let merger = new ChannelMergerNode(
+ context, {numberOfInputs: context.destination.channelCount});
+ merger.connect(context.destination);
+
+ let src = new OscillatorNode(context);
+ let f = new BiquadFilterNode(context, {type: 'lowpass'});
+
+ // Remember the initial filter parameters.
+ let initialFilter = {
+ type: f.type,
+ frequency: f.frequency.value,
+ gain: f.gain.value,
+ detune: f.detune.value,
+ Q: f.Q.value
+ };
+
+ src.connect(merger, 0, 0);
+ src.connect(f).connect(merger, 0, 1);
+
+ // Apply the filter change at frame |changeFrame| with a new
+ // frequency value of |newValue|.
+ let changeFrame = 2 * RENDER_QUANTUM_FRAMES;
+ let newValue = 750;
+
+ context.suspend(changeFrame / context.sampleRate)
+ .then(() => f.frequency.value = newValue)
+ .then(() => context.resume());
+
+ src.start();
+
+ context.startRendering()
+ .then(audio => {
+ let signal = audio.getChannelData(0);
+ let actual = audio.getChannelData(1);
+
+ // Get initial filter coefficients and updated coefficients
+ let nyquistFreq = context.sampleRate / 2;
+ let initialCoef = createFilter(
+ initialFilter.type, initialFilter.frequency / nyquistFreq,
+ initialFilter.Q, initialFilter.gain);
+
+ let finalCoef = createFilter(
+ f.type, f.frequency.value / nyquistFreq, f.Q.value,
+ f.gain.value);
+
+ let expected = new Float32Array(signal.length);
+
+ // Filter the initial part of the signal.
+ expected[0] =
+ filterSample(signal[0], initialCoef, 0, 0, 0, 0);
+ expected[1] = filterSample(
+ signal[1], initialCoef, expected[0], 0, signal[0], 0);
+
+ for (let k = 2; k < changeFrame; ++k) {
+ expected[k] = filterSample(
+ signal[k], initialCoef, expected[k - 1],
+ expected[k - 2], signal[k - 1], signal[k - 2]);
+ }
+
+ // Filter the rest of the input with the new coefficients
+ for (let k = changeFrame; k < signal.length; ++k) {
+ expected[k] = filterSample(
+ signal[k], finalCoef, expected[k - 1], expected[k - 2],
+ signal[k - 1], signal[k - 2]);
+ }
+
+ // The JS filter should match the actual output.
+ let match =
+ should(actual, 'Output from ' + f.type + ' filter')
+ .beCloseToArray(
+ expected, {absoluteThreshold: 6.8546e-7});
+ should(match, 'Output matches JS filter results').beTrue();
+ })
+ .then(() => task.done());
+ });
+
+ audit.define(
+ {label: 'Test 5', description: 'Test with modulation'},
+ (task, should) => {
+ doTest(should, {
+ prefix: 'Modulation: ',
+ paramName: 'frequency',
+ initializer: {type: 'peaking', Q: 5, gain: 5},
+ modulation: true,
+ changeList:
+ [{quantum: 2, newValue: 10}, {quantum: 8, newValue: -10}]
+ }).then(() => task.done());
+
+ });
+
+ audit.run();
+
+ // Run test, returning the promise from startRendering. |options|
+ // specifies the parameters for the test. |options.paramName| is the name
+ // of the AudioParam of the filter that is being tested.
+ // |options.initializer| is the initial value to be used in constructing
+ // the filter. |options.changeList| is an array consisting of dictionary
+ // with two members: |quantum| is the rendering quantum at which time we
+ // want to change the AudioParam value, and |newValue| is the value to be
+ // used.
+ function doTest(should, options) {
+ let paramName = options.paramName;
+ let newValue = options.newValue;
+ let prefix = options.prefix || '';
+
+ // Create offline audio context. The sample rate should be a power of
+ // two to eliminate any round-off errors in computing the time at which
+ // to suspend the context for the parameter change. The length is
+ // fairly arbitrary as long as it's big enough to the changeList
+ // values. There are two channels: channel 0 is output for the filter
+ // under test, and channel 1 is the output of referencef filter.
+ let context = new OfflineAudioContext(2, 2048, 16384);
+
+ let merger = new ChannelMergerNode(
+ context, {numberOfInputs: context.destination.channelCount});
+ merger.connect(context.destination);
+
+ let src = new OscillatorNode(context);
+
+ // |f0| is the filter under test that will have its AudioParam value
+ // changed. |f1| is the reference filter that uses setValueAtTime to
+ // update the AudioParam value.
+ let f0 = new BiquadFilterNode(context, options.initializer);
+ let f1 = new BiquadFilterNode(context, options.initializer);
+
+ src.connect(f0).connect(merger, 0, 0);
+ src.connect(f1).connect(merger, 0, 1);
+
+ // Modulate the AudioParam with an input signal, if requested.
+ if (options.modulation) {
+ // The modulation signal is a sine wave with amplitude 1/3 the cutoff
+ // frequency of the test filter. The amplitude is fairly arbitrary,
+ // but we want it to be a significant fraction of the cutoff so that
+ // the cutoff varies quite a bit in the test.
+ let mod =
+ new OscillatorNode(context, {type: 'sawtooth', frequency: 1000});
+ let modGain = new GainNode(context, {gain: f0.frequency.value / 3});
+ mod.connect(modGain);
+ modGain.connect(f0[paramName]);
+ modGain.connect(f1[paramName]);
+ mod.start();
+ }
+ // Output a message showing where we're starting from.
+ should(f0[paramName].value, prefix + `At time 0, ${paramName}`)
+ .beEqualTo(f0[paramName].value);
+
+ // Schedule all of the desired changes from |changeList|.
+ options.changeList.forEach(change => {
+ let changeTime =
+ change.quantum * RENDER_QUANTUM_FRAMES / context.sampleRate;
+ let value = change.newValue;
+
+ // Just output a message to show what we're doing.
+ should(value, prefix + `At time ${changeTime}, ${paramName}`)
+ .beEqualTo(value);
+
+ // Update the AudioParam value of each filter using setValueAtTime or
+ // the value setter.
+ f1[paramName].setValueAtTime(value, changeTime);
+ context.suspend(changeTime)
+ .then(() => f0[paramName].value = value)
+ .then(() => context.resume());
+ });
+
+ src.start();
+
+ return context.startRendering().then(audio => {
+ let actual = audio.getChannelData(0);
+ let expected = audio.getChannelData(1);
+
+ // The output from both filters MUST match exactly if dezippering has
+ // been properly removed.
+ let match = should(actual, `${prefix}Output from ${paramName} setter`)
+ .beCloseToArray(
+ expected, {absoluteThreshold: options.threshold});
+
+ // Just an extra message saying that what we're comparing, to make the
+ // output clearer. (Not really neceesary, but nice.)
+ should(
+ match,
+ `${prefix}Output from ${
+ paramName
+ } setter matches setValueAtTime output`)
+ .beTrue();
+ });
+ }
+
+ // Filter one sample:
+ //
+ // y[n] = b0 * x[n] + b1*x[n-1] + b2*x[n-2] - a1*y[n-1] - a2*y[n-2]
+ //
+ // where |x| is x[n], |xn1| is x[n-1], |xn2| is x[n-2], |yn1| is y[n-1],
+ // and |yn2| is y[n-2]. |coef| is a dictonary of the filter coefficients
+ // |b0|, |b1|, |b2|, |a1|, and |a2|.
+ function filterSample(x, coef, yn1, yn2, xn1, xn2) {
+ return coef.b0 * x + coef.b1 * xn1 + coef.b2 * xn2 - coef.a1 * yn1 -
+ coef.a2 * yn2;
+ }
+ </script>
+ </body>
+</html>