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-biquadfilternode-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-biquadfilternode-interface')
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> |