diff options
Diffstat (limited to 'testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/no-dezippering.html')
-rw-r--r-- | testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/no-dezippering.html | 288 |
1 files changed, 288 insertions, 0 deletions
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> |