convolver-response-4-chan.html (19753B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title> 5 Test Convolver Channel Outputs for Response with 4 channels 6 </title> 7 <script src="/resources/testharness.js"></script> 8 <script src="/resources/testharnessreport.js"></script> 9 <script src="/webaudio/resources/audit-util.js"></script> 10 <script src="/webaudio/resources/audit.js"></script> 11 </head> 12 <body> 13 <script id="layout-test-code"> 14 // Test various convolver configurations when the convolver response has 15 // a four channels. 16 17 // This is somewhat arbitrary. It is the minimum value for which tests 18 // pass with both FFmpeg and KISS FFT implementations for 256 points. 19 // The value was similar for each implementation. 20 const absoluteThreshold = 3 * Math.pow(2, -22); 21 22 // Fairly arbitrary sample rate, except that we want the rate to be a 23 // power of two so that 1/sampleRate is exactly representable as a 24 // single-precision float. 25 let sampleRate = 8192; 26 27 // A fairly arbitrary number of frames, except the number of frames should 28 // be more than a few render quanta. 29 let renderFrames = 10 * 128; 30 31 let audit = Audit.createTaskRunner(); 32 33 // Convolver response 34 let response; 35 36 audit.define( 37 { 38 label: 'initialize', 39 description: 'Convolver response with one channel' 40 }, 41 (task, should) => { 42 // Convolver response 43 should( 44 () => { 45 response = new AudioBuffer( 46 {numberOfChannels: 4, length: 8, sampleRate: sampleRate}); 47 // Each channel of the response is a simple impulse (with 48 // different delay) so that we can use a DelayNode to simulate 49 // the convolver output. Channel k is delayed by k+1 frames. 50 for (let k = 0; k < response.numberOfChannels; ++k) { 51 response.getChannelData(k)[k + 1] = 1; 52 } 53 }, 54 'new AudioBuffer({numberOfChannels: 2, length: 4, sampleRate: ' + 55 sampleRate + '})') 56 .notThrow(); 57 58 task.done(); 59 }); 60 61 audit.define( 62 {label: '1-channel input', description: 'produces 2-channel output'}, 63 (task, should) => { 64 fourChannelResponseTest({numberOfInputs: 1, prefix: '1'}, should) 65 .then(() => task.done()); 66 }); 67 68 audit.define( 69 {label: '2-channel input', description: 'produces 2-channel output'}, 70 (task, should) => { 71 fourChannelResponseTest({numberOfInputs: 2, prefix: '2'}, should) 72 .then(() => task.done()); 73 }); 74 75 audit.define( 76 { 77 label: '3-channel input', 78 description: '3->2 downmix producing 2-channel output' 79 }, 80 (task, should) => { 81 fourChannelResponseTest({numberOfInputs: 3, prefix: '3'}, should) 82 .then(() => task.done()); 83 }); 84 85 audit.define( 86 { 87 label: '4-channel input', 88 description: '4->2 downmix producing 2-channel output' 89 }, 90 (task, should) => { 91 fourChannelResponseTest({numberOfInputs: 4, prefix: '4'}, should) 92 .then(() => task.done()); 93 }); 94 95 audit.define( 96 { 97 label: '5.1-channel input', 98 description: '5.1->2 downmix producing 2-channel output' 99 }, 100 (task, should) => { 101 // Scale tolerance by maximum amplitude expected in down-mix 102 // output. 103 let threshold = (1.0 + Math.sqrt(0.5) * 2) * absoluteThreshold; 104 105 fourChannelResponseTest({numberOfInputs: 6, prefix: '5.1', 106 absoluteThreshold: threshold}, should) 107 .then(() => task.done()); 108 }); 109 110 audit.define( 111 { 112 label: 'delayed buffer set', 113 description: 'Delayed set of 4-channel response' 114 }, 115 (task, should) => { 116 // Don't really care about the output for this test. It's to verify 117 // we don't crash in a debug build when setting the convolver buffer 118 // after creating the graph. 119 let context = new OfflineAudioContext(1, renderFrames, sampleRate); 120 let src = new OscillatorNode(context); 121 let convolver = 122 new ConvolverNode(context, {disableNormalization: true}); 123 let buffer = new AudioBuffer({ 124 numberOfChannels: 4, 125 length: 4, 126 sampleRate: context.sampleRate 127 }); 128 129 // Impulse responses for the convolver aren't important, as long as 130 // it's not all zeroes. 131 for (let k = 0; k < buffer.numberOfChannels; ++k) { 132 buffer.getChannelData(k).fill(1); 133 } 134 135 src.connect(convolver).connect(context.destination); 136 137 // Set the buffer after a few render quanta have passed. The actual 138 // value must be least one, but is otherwise arbitrary. 139 context.suspend(512 / context.sampleRate) 140 .then(() => convolver.buffer = buffer) 141 .then(() => context.resume()); 142 143 src.start(); 144 context.startRendering() 145 .then(audioBuffer => { 146 // Just make sure output is not silent. 147 should( 148 audioBuffer.getChannelData(0), 149 'Output with delayed setting of convolver buffer') 150 .notBeConstantValueOf(0); 151 }) 152 .then(() => task.done()); 153 }); 154 155 audit.define( 156 { 157 label: 'count 1, 2-channel in', 158 description: '2->1 downmix because channel count is 1' 159 }, 160 (task, should) => { 161 channelCount1ExplicitTest( 162 {numberOfInputs: 1, prefix: 'Convolver count 1, stereo in'}, 163 should) 164 .then(() => task.done()); 165 }); 166 167 audit.define( 168 { 169 label: 'count 1, 4-channel in', 170 description: '4->1 downmix because channel count is 1' 171 }, 172 (task, should) => { 173 channelCount1ExplicitTest( 174 {numberOfInputs: 4, prefix: 'Convolver count 1, 4-channel in'}, 175 should) 176 .then(() => task.done()); 177 }); 178 179 audit.define( 180 { 181 label: 'count 1, 5.1-channel in', 182 description: '5.1->1 downmix because channel count is 1' 183 }, 184 (task, should) => { 185 channelCount1ExplicitTest( 186 { 187 numberOfInputs: 6, 188 prefix: 'Convolver count 1, 5.1 channel in' 189 }, 190 should) 191 .then(() => task.done()); 192 }); 193 194 audit.run(); 195 196 function fourChannelResponseTest(options, should) { 197 // Create an 4-channel offline context. The first two channels are for 198 // the stereo output of the convolver and the next two channels are for 199 // the reference stereo signal. 200 let context = new OfflineAudioContext(4, renderFrames, sampleRate); 201 context.destination.channelInterpretation = 'discrete'; 202 203 // Create oscillators for use as the input. The type and frequency is 204 // arbitrary except that oscillators must be different. 205 let src = new Array(options.numberOfInputs); 206 for (let k = 0; k < src.length; ++k) { 207 src[k] = new OscillatorNode( 208 context, {type: 'square', frequency: 440 + 220 * k}); 209 } 210 211 // Merger to combine the oscillators into one output stream. 212 let srcMerger = 213 new ChannelMergerNode(context, {numberOfInputs: src.length}); 214 215 for (let k = 0; k < src.length; ++k) { 216 src[k].connect(srcMerger, 0, k); 217 } 218 219 // Convolver under test. 220 let conv = new ConvolverNode( 221 context, {disableNormalization: true, buffer: response}); 222 srcMerger.connect(conv); 223 224 // Splitter to get individual channels of the convolver output so we can 225 // feed them (eventually) to the context in the right set of channels. 226 let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2}); 227 conv.connect(splitter); 228 229 // Reference graph consists of a delays node to simulate the response of 230 // the convolver. (The convolver response is designed this way.) 231 let delay = new Array(4); 232 for (let k = 0; k < delay.length; ++k) { 233 delay[k] = new DelayNode(context, { 234 delayTime: (k + 1) / context.sampleRate, 235 channelCount: 1, 236 channelCountMode: 'explicit' 237 }); 238 } 239 240 // Gain node to mix the sources to stereo in the desired way. (Could be 241 // done in the delay node, but let's keep the mixing separated from the 242 // functionality.) 243 let gainMixer = new GainNode( 244 context, {channelCount: 2, channelCountMode: 'explicit'}); 245 srcMerger.connect(gainMixer); 246 247 // Splitter to extract the channels of the reference signal. 248 let refSplitter = 249 new ChannelSplitterNode(context, {numberOfOutputs: 2}); 250 gainMixer.connect(refSplitter); 251 252 // Connect the left channel to the first two nodes and the right channel 253 // to the second two as required for "true" stereo matrix response. 254 for (let k = 0; k < 2; ++k) { 255 refSplitter.connect(delay[k], 0, 0); 256 refSplitter.connect(delay[k + 2], 1, 0); 257 } 258 259 // Gain nodes to sum the responses to stereo 260 let gain = new Array(2); 261 for (let k = 0; k < gain.length; ++k) { 262 gain[k] = new GainNode(context, { 263 channelCount: 1, 264 channelCountMode: 'explicit', 265 channelInterpretation: 'discrete' 266 }); 267 } 268 269 delay[0].connect(gain[0]); 270 delay[2].connect(gain[0]); 271 delay[1].connect(gain[1]); 272 delay[3].connect(gain[1]); 273 274 // Final merger to bring back the individual channels from the convolver 275 // and the reference in the right order for the destination. 276 let finalMerger = new ChannelMergerNode( 277 context, {numberOfInputs: context.destination.channelCount}); 278 279 // First two channels are for the convolver output, and the next two are 280 // for the reference. 281 splitter.connect(finalMerger, 0, 0); 282 splitter.connect(finalMerger, 1, 1); 283 gain[0].connect(finalMerger, 0, 2); 284 gain[1].connect(finalMerger, 0, 3); 285 286 finalMerger.connect(context.destination); 287 288 // Start the sources at last. 289 for (let k = 0; k < src.length; ++k) { 290 src[k].start(); 291 } 292 293 return context.startRendering().then(audioBuffer => { 294 // Extract the various channels out 295 let actual0 = audioBuffer.getChannelData(0); 296 let actual1 = audioBuffer.getChannelData(1); 297 let expected0 = audioBuffer.getChannelData(2); 298 let expected1 = audioBuffer.getChannelData(3); 299 300 let threshold = options.absoluteThreshold ? 301 options.absoluteThreshold : absoluteThreshold; 302 303 // Verify that each output channel of the convolver matches 304 // the delayed signal from the reference 305 should(actual0, options.prefix + ': Channel 0') 306 .beCloseToArray(expected0, {absoluteThreshold: threshold}); 307 should(actual1, options.prefix + ': Channel 1') 308 .beCloseToArray(expected1, {absoluteThreshold: threshold}); 309 }); 310 } 311 312 function fourChannelResponseExplicitTest(options, should) { 313 // Create an 4-channel offline context. The first two channels are for 314 // the stereo output of the convolver and the next two channels are for 315 // the reference stereo signal. 316 let context = new OfflineAudioContext(4, renderFrames, sampleRate); 317 context.destination.channelInterpretation = 'discrete'; 318 319 // Create oscillators for use as the input. The type and frequency is 320 // arbitrary except that oscillators must be different. 321 let src = new Array(options.numberOfInputs); 322 for (let k = 0; k < src.length; ++k) { 323 src[k] = new OscillatorNode( 324 context, {type: 'square', frequency: 440 + 220 * k}); 325 } 326 327 // Merger to combine the oscillators into one output stream. 328 let srcMerger = 329 new ChannelMergerNode(context, {numberOfInputs: src.length}); 330 331 for (let k = 0; k < src.length; ++k) { 332 src[k].connect(srcMerger, 0, k); 333 } 334 335 // Convolver under test. 336 let conv = new ConvolverNode( 337 context, {disableNormalization: true, buffer: response}); 338 srcMerger.connect(conv); 339 340 // Splitter to get individual channels of the convolver output so we can 341 // feed them (eventually) to the context in the right set of channels. 342 let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2}); 343 conv.connect(splitter); 344 345 // Reference graph consists of a delays node to simulate the response of 346 // the convolver. (The convolver response is designed this way.) 347 let delay = new Array(4); 348 for (let k = 0; k < delay.length; ++k) { 349 delay[k] = new DelayNode(context, { 350 delayTime: (k + 1) / context.sampleRate, 351 channelCount: 1, 352 channelCountMode: 'explicit' 353 }); 354 } 355 356 // Gain node to mix the sources to stereo in the desired way. (Could be 357 // done in the delay node, but let's keep the mixing separated from the 358 // functionality.) 359 let gainMixer = new GainNode( 360 context, {channelCount: 2, channelCountMode: 'explicit'}); 361 srcMerger.connect(gainMixer); 362 363 // Splitter to extract the channels of the reference signal. 364 let refSplitter = 365 new ChannelSplitterNode(context, {numberOfOutputs: 2}); 366 gainMixer.connect(refSplitter); 367 368 // Connect the left channel to the first two nodes and the right channel 369 // to the second two as required for "true" stereo matrix response. 370 for (let k = 0; k < 2; ++k) { 371 refSplitter.connect(delay[k], 0, 0); 372 refSplitter.connect(delay[k + 2], 1, 0); 373 } 374 375 // Gain nodes to sum the responses to stereo 376 let gain = new Array(2); 377 for (let k = 0; k < gain.length; ++k) { 378 gain[k] = new GainNode(context, { 379 channelCount: 1, 380 channelCountMode: 'explicit', 381 channelInterpretation: 'discrete' 382 }); 383 } 384 385 delay[0].connect(gain[0]); 386 delay[2].connect(gain[0]); 387 delay[1].connect(gain[1]); 388 delay[3].connect(gain[1]); 389 390 // Final merger to bring back the individual channels from the convolver 391 // and the reference in the right order for the destination. 392 let finalMerger = new ChannelMergerNode( 393 context, {numberOfInputs: context.destination.channelCount}); 394 395 // First two channels are for the convolver output, and the next two are 396 // for the reference. 397 splitter.connect(finalMerger, 0, 0); 398 splitter.connect(finalMerger, 1, 1); 399 gain[0].connect(finalMerger, 0, 2); 400 gain[1].connect(finalMerger, 0, 3); 401 402 finalMerger.connect(context.destination); 403 404 // Start the sources at last. 405 for (let k = 0; k < src.length; ++k) { 406 src[k].start(); 407 } 408 409 return context.startRendering().then(audioBuffer => { 410 // Extract the various channels out 411 let actual0 = audioBuffer.getChannelData(0); 412 let actual1 = audioBuffer.getChannelData(1); 413 let expected0 = audioBuffer.getChannelData(2); 414 let expected1 = audioBuffer.getChannelData(3); 415 416 // Verify that each output channel of the convolver matches 417 // the delayed signal from the reference 418 should(actual0, options.prefix + ': Channel 0') 419 .beEqualToArray(expected0); 420 should(actual1, options.prefix + ': Channel 1') 421 .beEqualToArray(expected1); 422 }); 423 } 424 425 function channelCount1ExplicitTest(options, should) { 426 // Create an 4-channel offline context. The first two channels are 427 // for the stereo output of the convolver and the next two channels 428 // are for the reference stereo signal. 429 let context = new OfflineAudioContext(4, renderFrames, sampleRate); 430 context.destination.channelInterpretation = 'discrete'; 431 // Final merger to bring back the individual channels from the 432 // convolver and the reference in the right order for the 433 // destination. 434 let finalMerger = new ChannelMergerNode( 435 context, {numberOfInputs: context.destination.channelCount}); 436 finalMerger.connect(context.destination); 437 438 // Create source using oscillators 439 let src = new Array(options.numberOfInputs); 440 for (let k = 0; k < src.length; ++k) { 441 src[k] = new OscillatorNode( 442 context, {type: 'square', frequency: 440 + 220 * k}); 443 } 444 445 // Merger to combine the oscillators into one output stream. 446 let srcMerger = 447 new ChannelMergerNode(context, {numberOfInputs: src.length}); 448 for (let k = 0; k < src.length; ++k) { 449 src[k].connect(srcMerger, 0, k); 450 } 451 452 // Convolver under test 453 let conv = new ConvolverNode(context, { 454 channelCount: 1, 455 channelCountMode: 'explicit', 456 disableNormalization: true, 457 buffer: response 458 }); 459 srcMerger.connect(conv); 460 461 // Splitter to extract the channels of the test signal. 462 let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2}); 463 conv.connect(splitter); 464 465 // Reference convolver, with a gain node to do the desired mixing. The 466 // gain node should do the same thing that the convolver under test 467 // should do. 468 let gain = new GainNode( 469 context, {channelCount: 1, channelCountMode: 'explicit'}); 470 let convRef = new ConvolverNode( 471 context, {disableNormalization: true, buffer: response}); 472 473 srcMerger.connect(gain).connect(convRef); 474 475 // Splitter to extract the channels of the reference signal. 476 let refSplitter = 477 new ChannelSplitterNode(context, {numberOfOutputs: 2}); 478 479 convRef.connect(refSplitter); 480 481 // Merge all the channels into one 482 splitter.connect(finalMerger, 0, 0); 483 splitter.connect(finalMerger, 1, 1); 484 refSplitter.connect(finalMerger, 0, 2); 485 refSplitter.connect(finalMerger, 1, 3); 486 487 // Start sources and render! 488 for (let k = 0; k < src.length; ++k) { 489 src[k].start(); 490 } 491 492 return context.startRendering().then(buffer => { 493 // The output from the test convolver should be identical to 494 // the reference result. 495 let testOut0 = buffer.getChannelData(0); 496 let testOut1 = buffer.getChannelData(1); 497 let refOut0 = buffer.getChannelData(2); 498 let refOut1 = buffer.getChannelData(3); 499 500 should(testOut0, `${options.prefix}: output 0`) 501 .beEqualToArray(refOut0); 502 should(testOut1, `${options.prefix}: output 1`) 503 .beEqualToArray(refOut1); 504 }) 505 } 506 </script> 507 </body> 508 </html>