diff options
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.html | 406 |
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> |