OLD | NEW |
| (Empty) |
1 <!doctype html> | |
2 <html> | |
3 <head> | |
4 <title>Biquad Automation Test</title> | |
5 <script src="../resources/js-test.js"></script> | |
6 <script src="resources/compatibility.js"></script> | |
7 <script src="resources/audit-util.js"></script> | |
8 <script src="resources/audio-testing.js"></script> | |
9 <script src="resources/biquad-filters.js"></script> | |
10 <script src="resources/audioparam-testing.js"></script> | |
11 </head> | |
12 <body> | |
13 <script> | |
14 description("Test Automation of Biquad Filters"); | |
15 | |
16 window.jsTestIsAsync = true; | |
17 | |
18 // Don't need to run these tests at high sampling rate, so just use a low
one to reduce memory | |
19 // usage and complexity. | |
20 var sampleRate = 16000; | |
21 | |
22 // How long to render for each test. | |
23 var renderDuration = 1; | |
24 // Where to end the automations. Fairly arbitrary, but must end before | |
25 // the renderDuration. | |
26 var automationEndTime = renderDuration / 2; | |
27 | |
28 var audit = Audit.createTaskRunner(); | |
29 | |
30 // The definition of the linear ramp automation function. | |
31 function linearRamp(t, v0, v1, t0, t1) { | |
32 return v0 + (v1 - v0) * (t - t0) / (t1 - t0); | |
33 } | |
34 | |
35 // Generate the filter coefficients for the specified filter using the giv
en parameters for | |
36 // the given duration. |filterTypeFunction| is a function that returns th
e filter | |
37 // coefficients for one set of parameters. |parameters| is a property bag
that contains the | |
38 // start and end values (as an array) for each of the biquad attributes.
The properties are | |
39 // |freq|, |Q|, |gain|, and |detune|. |duration| is the number of seconds
for which the | |
40 // coefficients are generated. | |
41 // | |
42 // A property bag with properties |b0|, |b1|, |b2|, |a1|, |a2|. Each prop
ery is an array | |
43 // consisting of the coefficients for the time-varying biquad filter. | |
44 function generateFilterCoefficients(filterTypeFunction, parameters, durati
on) { | |
45 var renderEndFrame = Math.ceil(renderDuration * sampleRate); | |
46 var endFrame = Math.ceil(duration * sampleRate); | |
47 var nCoef = renderEndFrame; | |
48 var b0 = new Float64Array(nCoef); | |
49 var b1 = new Float64Array(nCoef); | |
50 var b2 = new Float64Array(nCoef); | |
51 var a1 = new Float64Array(nCoef); | |
52 var a2 = new Float64Array(nCoef); | |
53 | |
54 var k = 0; | |
55 // If the property is not given, use the defaults. | |
56 var freqs = parameters.freq || [350, 350]; | |
57 var qs = parameters.Q || [1, 1]; | |
58 var gains = parameters.gain || [0, 0]; | |
59 var detunes = parameters.detune || [0, 0]; | |
60 | |
61 for (var frame = 0; frame <= endFrame; ++frame) { | |
62 // Apply linear ramp at frame |frame|. | |
63 var f = linearRamp(frame / sampleRate, freqs[0], freqs[1], 0, durati
on); | |
64 var q = linearRamp(frame / sampleRate, qs[0], qs[1], 0, duration); | |
65 var g = linearRamp(frame / sampleRate, gains[0], gains[1], 0, durati
on); | |
66 var d = linearRamp(frame / sampleRate, detunes[0], detunes[1], 0, du
ration); | |
67 | |
68 // Compute actual frequency parameter | |
69 f = f * Math.pow(2, d / 1200); | |
70 | |
71 // Compute filter coefficients | |
72 var coef = filterTypeFunction(f / (sampleRate / 2), q, g); | |
73 b0[k] = coef.b0; | |
74 b1[k] = coef.b1; | |
75 b2[k] = coef.b2; | |
76 a1[k] = coef.a1; | |
77 a2[k] = coef.a2; | |
78 ++k; | |
79 } | |
80 | |
81 // Fill the rest of the arrays with the constant value to the end of | |
82 // the rendering duration. | |
83 b0.fill(b0[endFrame], endFrame + 1); | |
84 b1.fill(b1[endFrame], endFrame + 1); | |
85 b2.fill(b2[endFrame], endFrame + 1); | |
86 a1.fill(a1[endFrame], endFrame + 1); | |
87 a2.fill(a2[endFrame], endFrame + 1); | |
88 | |
89 return {b0: b0, b1: b1, b2: b2, a1: a1, a2: a2}; | |
90 } | |
91 | |
92 // Apply the given time-varying biquad filter to the given signal, |signal
|. |coef| should be | |
93 // the time-varying coefficients of the filter, as returned by |generateFi
lterCoefficients|. | |
94 function timeVaryingFilter(signal, coef) { | |
95 var length = signal.length; | |
96 // Use double precision for the internal computations. | |
97 var y = new Float64Array(length); | |
98 | |
99 // Prime the pump. (Assumes the signal has length >= 2!) | |
100 y[0] = coef.b0[0] * signal[0]; | |
101 y[1] = coef.b0[1] * signal[1] + coef.b1[1] * signal[0] - coef.a1[1] * y[
0]; | |
102 | |
103 for (var n = 2; n < length; ++n) { | |
104 y[n] = coef.b0[n] * signal[n] + coef.b1[n] * signal[n-1] + coef.b2[n]
* signal[n-2]; | |
105 y[n] -= coef.a1[n] * y[n-1] + coef.a2[n] * y[n-2]; | |
106 } | |
107 | |
108 // But convert the result to single precision for comparison. | |
109 return y.map(Math.fround); | |
110 } | |
111 | |
112 // Configure the audio graph using |context|. Returns the biquad filter n
ode and the | |
113 // AudioBuffer used for the source. | |
114 function configureGraph(context, toneFrequency) { | |
115 // The source is just a simple sine wave. | |
116 var src = context.createBufferSource(); | |
117 var b = context.createBuffer(1, renderDuration * sampleRate, sampleRate)
; | |
118 var data = b.getChannelData(0); | |
119 var omega = 2 * Math.PI * toneFrequency / sampleRate; | |
120 for (var k = 0; k < data.length; ++k) { | |
121 data[k] = Math.sin(omega * k); | |
122 } | |
123 src.buffer = b; | |
124 var f = context.createBiquadFilter(); | |
125 src.connect(f); | |
126 f.connect(context.destination); | |
127 | |
128 src.start(); | |
129 | |
130 return {filter: f, source: b}; | |
131 } | |
132 | |
133 function createFilterVerifier(filterCreator, threshold, parameters, input,
message) { | |
134 return function (resultBuffer) { | |
135 var actual = resultBuffer.getChannelData(0); | |
136 var coefs = generateFilterCoefficients(filterCreator, parameters, auto
mationEndTime); | |
137 | |
138 reference = timeVaryingFilter(input, coefs); | |
139 | |
140 Should(message, actual, { | |
141 verbose: true | |
142 }).beCloseToArray(reference, threshold); | |
143 }; | |
144 } | |
145 | |
146 // Automate just the frequency parameter. A bandpass filter is used where
the center | |
147 // frequency is swept across the source (which is a simple tone). | |
148 audit.defineTask("automate-freq", function (done) { | |
149 var context = new OfflineAudioContext(1, renderDuration * sampleRate, sa
mpleRate); | |
150 | |
151 // Center frequency of bandpass filter and also the frequency of the tes
t tone. | |
152 var centerFreq = 10*440; | |
153 | |
154 // Sweep the frequency +/- 5*440 Hz from the center. This should cause | |
155 // the output to be low at the beginning and end of the test where the | |
156 // tone is outside the pass band of the filter, but high in the middle | |
157 // of the automation time where the tone is near the center of the pass | |
158 // band. Make sure the frequency sweep stays inside the Nyquist | |
159 // frequency. | |
160 var parameters = { | |
161 freq: [centerFreq - 5*440, centerFreq + 5*440] | |
162 } | |
163 var graph = configureGraph(context, centerFreq); | |
164 var f = graph.filter; | |
165 var b = graph.source; | |
166 | |
167 f.type = "bandpass"; | |
168 f.frequency.setValueAtTime(parameters.freq[0], 0); | |
169 f.frequency.linearRampToValueAtTime(parameters.freq[1], automationEndTim
e); | |
170 | |
171 context.startRendering() | |
172 .then(createFilterVerifier(createBandpassFilter, 4.8429e-6, parameters
, b.getChannelData(0), | |
173 "Output of bandpass filter with frequency automation")) | |
174 .then(done); | |
175 }); | |
176 | |
177 // Automate just the Q parameter. A bandpass filter is used where the Q o
f the filter is | |
178 // swept. | |
179 audit.defineTask("automate-q", function (done) { | |
180 var context = new OfflineAudioContext(1, renderDuration * sampleRate, sa
mpleRate); | |
181 | |
182 // The frequency of the test tone. | |
183 var centerFreq = 440; | |
184 | |
185 // Sweep the Q paramter between 1 and 200. This will cause the output o
f the filter to pass | |
186 // most of the tone at the beginning to passing less of the tone at the
end. This is | |
187 // because we set center frequency of the bandpass filter to be slightly
off from the actual | |
188 // tone. | |
189 var parameters = { | |
190 Q: [1, 200], | |
191 // Center frequency of the bandpass filter is just 25 Hz above the ton
e frequency. | |
192 freq: [centerFreq + 25, centerFreq + 25] | |
193 }; | |
194 var graph = configureGraph(context, centerFreq); | |
195 var f = graph.filter; | |
196 var b = graph.source; | |
197 | |
198 f.type = "bandpass"; | |
199 f.frequency.value = parameters.freq[0]; | |
200 f.Q.setValueAtTime(parameters.Q[0], 0); | |
201 f.Q.linearRampToValueAtTime(parameters.Q[1], automationEndTime); | |
202 | |
203 context.startRendering() | |
204 .then(createFilterVerifier(createBandpassFilter, 1.1062e-6, parameters
, b.getChannelData(0), | |
205 "Output of bandpass filter with Q automation")) | |
206 .then(done); | |
207 }); | |
208 | |
209 // Automate just the gain of the lowshelf filter. A test tone will be in
the lowshelf part of | |
210 // the filter. The output will vary as the gain of the lowshelf is change
d. | |
211 audit.defineTask("automate-gain", function (done) { | |
212 var context = new OfflineAudioContext(1, renderDuration * sampleRate, sa
mpleRate); | |
213 | |
214 // Frequency of the test tone. | |
215 var centerFreq = 440; | |
216 | |
217 // Set the cutoff frequency of the lowshelf to be significantly higher t
han the test tone. | |
218 // Sweep the gain from 20 dB to -20 dB. (We go from 20 to -20 to easily
verify that the | |
219 // filter didn't go unstable.) | |
220 var parameters = { | |
221 freq: [3500, 3500], | |
222 gain: [20, -20] | |
223 } | |
224 var graph = configureGraph(context, centerFreq); | |
225 var f = graph.filter; | |
226 var b = graph.source; | |
227 | |
228 f.type = "lowshelf"; | |
229 f.frequency.value = parameters.freq[0]; | |
230 f.gain.setValueAtTime(parameters.gain[0], 0); | |
231 f.gain.linearRampToValueAtTime(parameters.gain[1], automationEndTime); | |
232 | |
233 context.startRendering() | |
234 .then(createFilterVerifier(createLowShelfFilter, 1.4306e-5, parameters
, b.getChannelData(0), | |
235 "Output of lowshelf filter with gain automation")) | |
236 .then(done); | |
237 }); | |
238 | |
239 // Automate just the detune parameter. Basically the same test as for the
frequncy parameter | |
240 // but we just use the detune parameter to modulate the frequency paramete
r. | |
241 audit.defineTask("automate-detune", function (done) { | |
242 var context = new OfflineAudioContext(1, renderDuration * sampleRate, sa
mpleRate); | |
243 var centerFreq = 10*440; | |
244 var parameters = { | |
245 freq: [centerFreq, centerFreq], | |
246 detune: [-10*1200, 10*1200] | |
247 }; | |
248 var graph = configureGraph(context, centerFreq); | |
249 var f = graph.filter; | |
250 var b = graph.source; | |
251 | |
252 f.type = "bandpass"; | |
253 f.frequency.value = parameters.freq[0]; | |
254 f.detune.setValueAtTime(parameters.detune[0], 0); | |
255 f.detune.linearRampToValueAtTime(parameters.detune[1], automationEndTime
); | |
256 | |
257 context.startRendering() | |
258 .then(createFilterVerifier(createBandpassFilter, 2.9535e-5, parameters
, b.getChannelData(0), | |
259 "Output of bandpass filter with detune automation")) | |
260 .then(done); | |
261 }); | |
262 | |
263 // Automate all of the filter parameters at once. This is a basic check t
hat everything is | |
264 // working. A peaking filter is used because it uses all of the parameter
s. | |
265 audit.defineTask("automate-all", function (done) { | |
266 var context = new OfflineAudioContext(1, renderDuration * sampleRate, sa
mpleRate); | |
267 var graph = configureGraph(context, 10*440); | |
268 var f = graph.filter; | |
269 var b = graph.source; | |
270 | |
271 // Sweep all of the filter parameters. These are pretty much arbitrary. | |
272 var parameters = { | |
273 freq: [8000, 100], | |
274 Q: [f.Q.value, .0001], | |
275 gain: [f.gain.value, 20], | |
276 detune: [2400, -2400] | |
277 }; | |
278 | |
279 f.type = "peaking"; | |
280 // Set starting points for all parameters of the filter. Start at 10 kH
z for the center | |
281 // frequency, and the defaults for Q and gain. | |
282 f.frequency.setValueAtTime(parameters.freq[0], 0); | |
283 f.Q.setValueAtTime(parameters.Q[0], 0); | |
284 f.gain.setValueAtTime(parameters.gain[0], 0); | |
285 f.detune.setValueAtTime(parameters.detune[0], 0); | |
286 | |
287 // Linear ramp each parameter | |
288 f.frequency.linearRampToValueAtTime(parameters.freq[1], automationEndTim
e); | |
289 f.Q.linearRampToValueAtTime(parameters.Q[1], automationEndTime); | |
290 f.gain.linearRampToValueAtTime(parameters.gain[1], automationEndTime); | |
291 f.detune.linearRampToValueAtTime(parameters.detune[1], automationEndTime
); | |
292 | |
293 context.startRendering() | |
294 .then(createFilterVerifier(createPeakingFilter, 6.2907e-4, parameters,
b.getChannelData(0), | |
295 "Output of peaking filter with automation of all parameters")) | |
296 .then(done); | |
297 }); | |
298 | |
299 // Test that modulation of the frequency parameter of the filter works. A
sinusoid of 440 Hz | |
300 // is the test signal that is applied to a bandpass biquad filter. The fr
equency parameter of | |
301 // the filter is modulated by a sinusoid at 103 Hz, and the frequency modu
lation varies from | |
302 // 116 to 412 Hz. (This test was taken from the description in | |
303 // https://github.com/WebAudio/web-audio-api/issues/509#issuecomment-94731
355) | |
304 audit.defineTask("modulation", function (done) { | |
305 var context = new OfflineAudioContext(1, renderDuration * sampleRate, sa
mpleRate); | |
306 | |
307 // Create a graph with the sinusoidal source at 440 Hz as the input to a
biquad filter. | |
308 var graph = configureGraph(context, 440); | |
309 var f = graph.filter; | |
310 var b = graph.source; | |
311 | |
312 f.type = "bandpass"; | |
313 f.Q.value = 5; | |
314 f.frequency.value = 264; | |
315 | |
316 // Create the modulation source, a sinusoid with frequency 103 Hz and am
plitude 148. (The | |
317 // amplitude of 148 is added to the filter's frequency value of 264 to p
roduce a sinusoidal | |
318 // modulation of the frequency parameter from 116 to 412 Hz.) | |
319 var mod = context.createBufferSource(); | |
320 var mbuffer = context.createBuffer(1, renderDuration * sampleRate, sampl
eRate); | |
321 var d = mbuffer.getChannelData(0); | |
322 var omega = 2 * Math.PI * 103 / sampleRate; | |
323 for (var k = 0; k < d.length; ++k) { | |
324 d[k] = 148 * Math.sin(omega * k); | |
325 } | |
326 mod.buffer = mbuffer; | |
327 | |
328 mod.connect(f.frequency); | |
329 | |
330 mod.start(); | |
331 context.startRendering() | |
332 .then(function (resultBuffer) { | |
333 var actual = resultBuffer.getChannelData(0); | |
334 // Compute the filter coefficients using the mod sine wave | |
335 | |
336 var endFrame = Math.ceil(renderDuration * sampleRate); | |
337 var nCoef = endFrame; | |
338 var b0 = new Float64Array(nCoef); | |
339 var b1 = new Float64Array(nCoef); | |
340 var b2 = new Float64Array(nCoef); | |
341 var a1 = new Float64Array(nCoef); | |
342 var a2 = new Float64Array(nCoef); | |
343 | |
344 // Generate the filter coefficients when the frequency varies from
116 to 248 Hz using | |
345 // the 103 Hz sinusoid. | |
346 for (var k = 0; k < nCoef; ++k) { | |
347 var freq = f.frequency.value + d[k]; | |
348 var c = createBandpassFilter(freq / (sampleRate / 2), f.Q.value,
f.gain.value); | |
349 b0[k] = c.b0; | |
350 b1[k] = c.b1; | |
351 b2[k] = c.b2; | |
352 a1[k] = c.a1; | |
353 a2[k] = c.a2; | |
354 } | |
355 reference = timeVaryingFilter(b.getChannelData(0), | |
356 {b0: b0, b1: b1, b2: b2, a1: a1, a2: a2}); | |
357 | |
358 Should("Output of bandpass filter with sinusoidal modulation of ban
dpass center frequency", | |
359 actual) | |
360 .beCloseToArray(reference, 3.9787e-5); | |
361 }) | |
362 .then(done); | |
363 }); | |
364 | |
365 // All done! | |
366 audit.defineTask("finish", function (done) { | |
367 finishJSTest(); | |
368 done(); | |
369 }); | |
370 | |
371 audit.runTasks(); | |
372 </script> | |
373 </body> | |
374 </html> | |
OLD | NEW |