| 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 |