k-rate-biquad-connection.html (18235B)
1 <!doctype html> 2 <html> 3 <head> 4 <title>Test k-rate AudioParam Inputs for BiquadFilterNode</title> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 <script src="/webaudio/resources/audit-util.js"></script> 8 <script src="/webaudio/resources/audit.js"></script> 9 </head> 10 11 <body> 12 <script> 13 // sampleRate and duration are fairly arbitrary. We use low values to 14 // limit the complexity of the test. 15 let sampleRate = 8192; 16 let testDuration = 0.5; 17 18 let audit = Audit.createTaskRunner(); 19 20 audit.define( 21 {label: 'Frequency AudioParam', description: 'k-rate input works'}, 22 async (task, should) => { 23 // Test frequency AudioParam using a lowpass filter whose bandwidth 24 // is initially larger than the oscillator frequency. Then automate 25 // the frequency to 0 so that the output of the filter is 0 (because 26 // the cutoff is 0). 27 let oscFrequency = 440; 28 29 let options = { 30 sampleRate: sampleRate, 31 paramName: 'frequency', 32 oscFrequency: oscFrequency, 33 testDuration: testDuration, 34 filterOptions: {type: 'lowpass', frequency: 0}, 35 autoStart: 36 {method: 'setValueAtTime', args: [2 * oscFrequency, 0]}, 37 autoEnd: { 38 method: 'linearRampToValueAtTime', 39 args: [0, testDuration / 4] 40 } 41 }; 42 43 let buffer = await doTest(should, options); 44 let expected = buffer.getChannelData(0); 45 let actual = buffer.getChannelData(1); 46 let halfLength = expected.length / 2; 47 48 // Sanity check. The expected output should not be zero for 49 // the first half, but should be zero for the second half 50 // (because the filter bandwidth is exactly 0). 51 const prefix = 'Expected k-rate frequency with automation'; 52 53 should( 54 expected.slice(0, halfLength), 55 `${prefix} output[0:${halfLength - 1}]`) 56 .notBeConstantValueOf(0); 57 should( 58 expected.slice(expected.length), 59 `${prefix} output[${halfLength}:]`) 60 .beConstantValueOf(0); 61 62 // Outputs should be the same. Break the message into two 63 // parts so we can see the expected outputs. 64 checkForSameOutput(should, options.paramName, actual, expected); 65 66 task.done(); 67 }); 68 69 audit.define( 70 {label: 'Q AudioParam', description: 'k-rate input works'}, 71 async (task, should) => { 72 // Test Q AudioParam. Use a bandpass filter whose center frequency 73 // is fairly far from the oscillator frequency. Then start with a Q 74 // value of 0 (so everything goes through) and then increase Q to 75 // some large value such that the out-of-band signals are basically 76 // cutoff. 77 let frequency = 440; 78 let oscFrequency = 4 * frequency; 79 80 let options = { 81 sampleRate: sampleRate, 82 oscFrequency: oscFrequency, 83 testDuration: testDuration, 84 paramName: 'Q', 85 filterOptions: {type: 'bandpass', frequency: frequency, Q: 0}, 86 autoStart: {method: 'setValueAtTime', args: [0, 0]}, 87 autoEnd: { 88 method: 'linearRampToValueAtTime', 89 args: [100, testDuration / 4] 90 } 91 }; 92 93 const buffer = await doTest(should, options); 94 let expected = buffer.getChannelData(0); 95 let actual = buffer.getChannelData(1); 96 97 // Outputs should be the same 98 checkForSameOutput(should, options.paramName, actual, expected); 99 100 task.done(); 101 }); 102 103 audit.define( 104 {label: 'Gain AudioParam', description: 'k-rate input works'}, 105 async (task, should) => { 106 // Test gain AudioParam. Use a peaking filter with a large Q so the 107 // peak is narrow with a center frequency the same as the oscillator 108 // frequency. Start with a gain of 0 so everything goes through and 109 // then ramp the gain down to -100 so that the oscillator is 110 // filtered out. 111 let oscFrequency = 4 * 440; 112 113 let options = { 114 sampleRate: sampleRate, 115 oscFrequency: oscFrequency, 116 testDuration: testDuration, 117 paramName: 'gain', 118 filterOptions: 119 {type: 'peaking', frequency: oscFrequency, Q: 100, gain: 0}, 120 autoStart: {method: 'setValueAtTime', args: [0, 0]}, 121 autoEnd: { 122 method: 'linearRampToValueAtTime', 123 args: [-100, testDuration / 4] 124 } 125 }; 126 127 const buffer = await doTest(should, options); 128 let expected = buffer.getChannelData(0); 129 let actual = buffer.getChannelData(1); 130 131 // Outputs should be the same 132 checkForSameOutput(should, options.paramName, actual, expected); 133 134 task.done(); 135 }); 136 137 audit.define( 138 {label: 'Detune AudioParam', description: 'k-rate input works'}, 139 async (task, should) => { 140 // Test detune AudioParam. The basic idea is the same as the 141 // frequency test above, but insteda of automating the frequency, we 142 // automate the detune value so that initially the filter cutuff is 143 // unchanged and then changing the detune until the cutoff goes to 1 144 // Hz, which would cause the oscillator to be filtered out. 145 let oscFrequency = 440; 146 let filterFrequency = 5 * oscFrequency; 147 148 // For a detune value d, the computed frequency, fc, of the filter 149 // is fc = f*2^(d/1200), where f is the frequency of the filter. Or 150 // d = 1200*log2(fc/f). Compute the detune value to produce a final 151 // cutoff frequency of 1 Hz. 152 let detuneEnd = 1200 * Math.log2(1 / filterFrequency); 153 154 let options = { 155 sampleRate: sampleRate, 156 oscFrequency: oscFrequency, 157 testDuration: testDuration, 158 paramName: 'detune', 159 filterOptions: { 160 type: 'lowpass', 161 frequency: filterFrequency, 162 detune: 0, 163 gain: 0 164 }, 165 autoStart: {method: 'setValueAtTime', args: [0, 0]}, 166 autoEnd: { 167 method: 'linearRampToValueAtTime', 168 args: [detuneEnd, testDuration / 4] 169 } 170 }; 171 172 const buffer = await doTest(should, options); 173 let expected = buffer.getChannelData(0); 174 let actual = buffer.getChannelData(1); 175 176 // Outputs should be the same 177 checkForSameOutput(should, options.paramName, actual, expected); 178 179 task.done(); 180 }); 181 182 audit.define('All k-rate inputs', async (task, should) => { 183 // Test the case where all AudioParams are set to k-rate with an input 184 // to each AudioParam. Similar to the above tests except all the params 185 // are k-rate. 186 let testFrames = testDuration * sampleRate; 187 let context = new OfflineAudioContext( 188 {numberOfChannels: 2, sampleRate: sampleRate, length: testFrames}); 189 190 let merger = new ChannelMergerNode( 191 context, {numberOfInputs: context.destination.channelCount}); 192 merger.connect(context.destination); 193 194 let src = new OscillatorNode(context); 195 196 // The peaking filter uses all four AudioParams, so this is the node to 197 // test. 198 let filterOptions = 199 {type: 'peaking', frequency: 0, detune: 0, gain: 0, Q: 0}; 200 let refNode; 201 should( 202 () => refNode = new BiquadFilterNode(context, filterOptions), 203 `Create: refNode = new BiquadFilterNode(context, ${ 204 JSON.stringify(filterOptions)})`) 205 .notThrow(); 206 207 let tstNode; 208 should( 209 () => tstNode = new BiquadFilterNode(context, filterOptions), 210 `Create: tstNode = new BiquadFilterNode(context, ${ 211 JSON.stringify(filterOptions)})`) 212 .notThrow(); 213 ; 214 215 // Make all the AudioParams k-rate. 216 ['frequency', 'Q', 'gain', 'detune'].forEach(param => { 217 should( 218 () => refNode[param].automationRate = 'k-rate', 219 `Set rate: refNode[${param}].automationRate = 'k-rate'`) 220 .notThrow(); 221 should( 222 () => tstNode[param].automationRate = 'k-rate', 223 `Set rate: tstNode[${param}].automationRate = 'k-rate'`) 224 .notThrow(); 225 }); 226 227 // One input for each AudioParam. 228 let mod = {}; 229 ['frequency', 'Q', 'gain', 'detune'].forEach(param => { 230 should( 231 () => mod[param] = new ConstantSourceNode(context, {offset: 0}), 232 `Create: mod[${ 233 param}] = new ConstantSourceNode(context, {offset: 0})`) 234 .notThrow(); 235 ; 236 should( 237 () => mod[param].offset.automationRate = 'a-rate', 238 `Set rate: mod[${param}].offset.automationRate = 'a-rate'`) 239 .notThrow(); 240 }); 241 242 // Set up automations for refNode. We want to start the filter with 243 // parameters that let the oscillator signal through more or less 244 // untouched. Then change the filter parameters to filter out the 245 // oscillator. What happens in between doesn't reall matter for this 246 // test. Hence, set the initial parameters with a center frequency well 247 // above the oscillator and a Q and gain of 0 to pass everthing. 248 [['frequency', [4 * src.frequency.value, 0]], ['Q', [0, 0]], 249 ['gain', [0, 0]], ['detune', [4 * 1200, 0]]] 250 .forEach(param => { 251 should( 252 () => refNode[param[0]].setValueAtTime(...param[1]), 253 `Automate 0: refNode.${param[0]}.setValueAtTime(${ 254 param[1][0]}, ${param[1][1]})`) 255 .notThrow(); 256 should( 257 () => mod[param[0]].offset.setValueAtTime(...param[1]), 258 `Automate 0: mod[${param[0]}].offset.setValueAtTime(${ 259 param[1][0]}, ${param[1][1]})`) 260 .notThrow(); 261 }); 262 263 // Now move the filter frequency to the oscillator frequency with a high 264 // Q and very low gain to remove the oscillator signal. 265 [['frequency', [src.frequency.value, testDuration / 4]], 266 ['Q', [40, testDuration / 4]], ['gain', [-100, testDuration / 4]], [ 267 'detune', [0, testDuration / 4] 268 ]].forEach(param => { 269 should( 270 () => refNode[param[0]].linearRampToValueAtTime(...param[1]), 271 `Automate 1: refNode[${param[0]}].linearRampToValueAtTime(${ 272 param[1][0]}, ${param[1][1]})`) 273 .notThrow(); 274 should( 275 () => mod[param[0]].offset.linearRampToValueAtTime(...param[1]), 276 `Automate 1: mod[${param[0]}].offset.linearRampToValueAtTime(${ 277 param[1][0]}, ${param[1][1]})`) 278 .notThrow(); 279 }); 280 281 // Connect everything 282 src.connect(refNode).connect(merger, 0, 0); 283 src.connect(tstNode).connect(merger, 0, 1); 284 285 src.start(); 286 for (let param in mod) { 287 should( 288 () => mod[param].connect(tstNode[param]), 289 `Connect: mod[${param}].connect(tstNode.${param})`) 290 .notThrow(); 291 } 292 293 for (let param in mod) { 294 should(() => mod[param].start(), `Start: mod[${param}].start()`) 295 .notThrow(); 296 } 297 298 const buffer = await context.startRendering(); 299 let expected = buffer.getChannelData(0); 300 let actual = buffer.getChannelData(1); 301 302 // Sanity check that the output isn't all zeroes. 303 should(actual, 'All k-rate AudioParams').notBeConstantValueOf(0); 304 should(actual, 'All k-rate AudioParams').beCloseToArray(expected, { 305 absoluteThreshold: 0 306 }); 307 308 task.done(); 309 }); 310 311 audit.run(); 312 313 async function doTest(should, options) { 314 // Test that a k-rate AudioParam with an input reads the input value and 315 // is actually k-rate. 316 // 317 // A refNode is created with an automation timeline. This is the 318 // expected output. 319 // 320 // The testNode is the same, but it has a node connected to the k-rate 321 // AudioParam. The input to the node is an a-rate ConstantSourceNode 322 // whose output is automated in exactly the same was as the refNode. If 323 // the test passes, the outputs of the two nodes MUST match exactly. 324 325 // The options argument MUST contain the following members: 326 // sampleRate - the sample rate for the offline context 327 // testDuration - duration of the offline context, in sec. 328 // paramName - the name of the AudioParam to be tested 329 // oscFrequency - frequency of oscillator source 330 // filterOptions - options used to construct the BiquadFilterNode 331 // autoStart - information about how to start the automation 332 // autoEnd - information about how to end the automation 333 // 334 // The autoStart and autoEnd options are themselves dictionaries with 335 // the following required members: 336 // method - name of the automation method to be applied 337 // args - array of arguments to be supplied to the method. 338 let { 339 sampleRate, 340 paramName, 341 oscFrequency, 342 autoStart, 343 autoEnd, 344 testDuration, 345 filterOptions 346 } = options; 347 348 let testFrames = testDuration * sampleRate; 349 let context = new OfflineAudioContext( 350 {numberOfChannels: 2, sampleRate: sampleRate, length: testFrames}); 351 352 let merger = new ChannelMergerNode( 353 context, {numberOfInputs: context.destination.channelCount}); 354 merger.connect(context.destination); 355 356 // Any calls to |should| are meant to be informational so we can see 357 // what nodes are created and the automations used. 358 let src; 359 360 // Create the source. 361 should( 362 () => { 363 src = new OscillatorNode(context, {frequency: oscFrequency}); 364 }, 365 `${paramName}: new OscillatorNode(context, {frequency: ${ 366 oscFrequency}})`) 367 .notThrow(); 368 369 // The refNode automates the AudioParam with k-rate automations, no 370 // inputs. 371 let refNode; 372 should( 373 () => { 374 refNode = new BiquadFilterNode(context, filterOptions); 375 }, 376 `Reference BiquadFilterNode(c, ${JSON.stringify(filterOptions)})`) 377 .notThrow(); 378 379 refNode[paramName].automationRate = 'k-rate'; 380 381 // Set up automations for the reference node. 382 should( 383 () => { 384 refNode[paramName][autoStart.method](...autoStart.args); 385 }, 386 `refNode.${paramName}.${autoStart.method}(${autoStart.args})`) 387 .notThrow(); 388 should( 389 () => { 390 refNode[paramName][autoEnd.method](...autoEnd.args); 391 }, 392 `refNode.${paramName}.${autoEnd.method}.(${autoEnd.args})`) 393 .notThrow(); 394 395 // The tstNode does the same automation, but it comes from the input 396 // connected to the AudioParam. 397 let tstNode; 398 should( 399 () => { 400 tstNode = new BiquadFilterNode(context, filterOptions); 401 }, 402 `Test BiquadFilterNode(context, ${JSON.stringify(filterOptions)})`) 403 .notThrow(); 404 tstNode[paramName].automationRate = 'k-rate'; 405 406 // Create the input to the AudioParam of the test node. The output of 407 // this node MUST have the same set of automations as the reference 408 // node, and MUST be a-rate to make sure we're handling k-rate inputs 409 // correctly. 410 let mod = new ConstantSourceNode(context); 411 mod.offset.automationRate = 'a-rate'; 412 should( 413 () => { 414 mod.offset[autoStart.method](...autoStart.args); 415 }, 416 `${paramName}: mod.offset.${autoStart.method}(${autoStart.args})`) 417 .notThrow(); 418 should( 419 () => { 420 mod.offset[autoEnd.method](...autoEnd.args); 421 }, 422 `${paramName}: mod.offset.${autoEnd.method}(${autoEnd.args})`) 423 .notThrow(); 424 425 // Create graph 426 mod.connect(tstNode[paramName]); 427 src.connect(refNode).connect(merger, 0, 0); 428 src.connect(tstNode).connect(merger, 0, 1); 429 430 // Run! 431 src.start(); 432 mod.start(); 433 return context.startRendering(); 434 } 435 436 function checkForSameOutput(should, paramName, actual, expected) { 437 let halfLength = expected.length / 2; 438 439 // Outputs should be the same. We break the check into halves so we can 440 // see the expected outputs. Mostly for a simple visual check that the 441 // output from the second half is small because the tests generally try 442 // to filter out the signal so that the last half of the output is 443 // small. 444 should( 445 actual.slice(0, halfLength), 446 `k-rate ${paramName} with input: output[0,${halfLength}]`) 447 .beCloseToArray( 448 expected.slice(0, halfLength), {absoluteThreshold: 0}); 449 should( 450 actual.slice(halfLength), 451 `k-rate ${paramName} with input: output[${halfLength}:]`) 452 .beCloseToArray(expected.slice(halfLength), {absoluteThreshold: 0}); 453 } 454 </script> 455 </body> 456 </html>