summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-automation.html
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-automation.html')
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-biquadfilternode-interface/biquad-automation.html406
1 files changed, 406 insertions, 0 deletions
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>