convolver-response-1-chan.html (15710B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title> 5 Test Convolver Channel Outputs for Response with 1 channel 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 // one channel (mono). 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 = Math.pow(2, -21); 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: 1, length: 2, sampleRate: sampleRate}); 47 response.getChannelData(0)[1] = 1; 48 }, 49 'new AudioBuffer({numberOfChannels: 1, length: 2, sampleRate: ' + 50 sampleRate + '})') 51 .notThrow(); 52 53 task.done(); 54 }); 55 56 audit.define( 57 {label: '1-channel input', description: 'produces 1-channel output'}, 58 (task, should) => { 59 // Create a 3-channel context: channel 0 = convolver under test, 60 // channel 1: test that convolver output is not stereo, channel 2: 61 // expected output. The context MUST be discrete so that the 62 // channels don't get mixed in some unexpected way. 63 let context = new OfflineAudioContext(3, renderFrames, sampleRate); 64 context.destination.channelInterpretation = 'discrete'; 65 66 let src = new OscillatorNode(context); 67 let conv = new ConvolverNode( 68 context, {disableNormalization: true, buffer: response}); 69 70 // Splitter node to verify that the output of the convolver is mono. 71 // channelInterpretation must be 'discrete' so we don't do any 72 // mixing of the input to the node. 73 let splitter = new ChannelSplitterNode( 74 context, 75 {numberOfOutputs: 2, channelInterpretation: 'discrete'}); 76 77 // Final merger to feed all of the individual channels into the 78 // destination. 79 let merger = new ChannelMergerNode(context, {numberOfInputs: 3}); 80 81 src.connect(conv).connect(splitter); 82 splitter.connect(merger, 0, 0); 83 splitter.connect(merger, 1, 1); 84 85 // The convolver response is a 1-sample delay. Use a delay node to 86 // implement this. 87 let delay = 88 new DelayNode(context, {delayTime: 1 / context.sampleRate}); 89 src.connect(delay); 90 delay.connect(merger, 0, 2); 91 92 merger.connect(context.destination); 93 94 src.start(); 95 96 context.startRendering() 97 .then(audioBuffer => { 98 // Extract out the three channels 99 let actual = audioBuffer.getChannelData(0); 100 let c1 = audioBuffer.getChannelData(1); 101 let expected = audioBuffer.getChannelData(2); 102 103 // c1 is expected to be zero. 104 should(c1, '1: Channel 1').beConstantValueOf(0); 105 106 // The expected and actual results should be identical 107 should(actual, 'Convolver output') 108 .beCloseToArray(expected, 109 {absoluteThreshold: absoluteThreshold}); 110 }) 111 .then(() => task.done()); 112 }); 113 114 audit.define( 115 {label: '2-channel input', description: 'produces 2-channel output'}, 116 (task, should) => { 117 downMixTest({numberOfInputs: 2, prefix: '2'}, should) 118 .then(() => task.done()); 119 }); 120 121 audit.define( 122 { 123 label: '3-channel input', 124 description: '3->2 downmix producing 2-channel output' 125 }, 126 (task, should) => { 127 downMixTest({numberOfInputs: 3, prefix: '3'}, should) 128 .then(() => task.done()); 129 }); 130 131 audit.define( 132 { 133 label: '4-channel input', 134 description: '4->2 downmix producing 2-channel output' 135 }, 136 (task, should) => { 137 downMixTest({numberOfInputs: 4, prefix: '4'}, should) 138 .then(() => task.done()); 139 }); 140 141 audit.define( 142 { 143 label: '5.1-channel input', 144 description: '5.1->2 downmix producing 2-channel output' 145 }, 146 (task, should) => { 147 // Scale tolerance by maximum amplitude expected in down-mix 148 // output. 149 let threshold = (1.0 + Math.sqrt(0.5) * 2) * absoluteThreshold; 150 151 downMixTest({numberOfInputs: 6, prefix: '5.1', 152 absoluteThreshold: threshold}, should) 153 .then(() => task.done()); 154 }); 155 156 audit.define( 157 { 158 label: '3-channel input, explicit', 159 description: '3->2 explicit downmix producing 2-channel output' 160 }, 161 (task, should) => { 162 downMixTest( 163 { 164 channelCountMode: 'explicit', 165 numberOfInputs: 3, 166 prefix: '3 chan downmix explicit' 167 }, 168 should) 169 .then(() => task.done()); 170 }); 171 172 audit.define( 173 { 174 label: '4-channel input, explicit', 175 description: '4->2 explicit downmix producing 2-channel output' 176 }, 177 (task, should) => { 178 downMixTest( 179 { 180 channelCountMode: 'explicit', 181 numberOfInputs: 4, 182 prefix: '4 chan downmix explicit' 183 }, 184 should) 185 .then(() => task.done()); 186 }); 187 188 audit.define( 189 { 190 label: '5.1-channel input, explicit', 191 description: '5.1->2 explicit downmix producing 2-channel output' 192 }, 193 (task, should) => { 194 // Scale tolerance by maximum amplitude expected in down-mix 195 // output. 196 let threshold = (1.0 + Math.sqrt(0.5) * 2) * absoluteThreshold; 197 198 downMixTest( 199 { 200 channelCountMode: 'explicit', 201 numberOfInputs: 6, 202 prefix: '5.1 chan downmix explicit', 203 absoluteThreshold: threshold 204 }, 205 should) 206 .then(() => task.done()); 207 }); 208 209 audit.define( 210 { 211 label: 'mono-upmix-explicit', 212 description: '1->2 upmix, count mode explicit' 213 }, 214 (task, should) => { 215 upMixTest(should, {channelCountMode: 'explicit'}) 216 .then(buffer => { 217 let length = buffer.length; 218 let input = buffer.getChannelData(0); 219 let out0 = buffer.getChannelData(1); 220 let out1 = buffer.getChannelData(2); 221 222 // The convolver is basically a one-sample delay. Verify that 223 // that each channel is delayed by one sample. 224 should(out0.slice(1), '1->2 explicit upmix: channel 0') 225 .beCloseToArray( 226 input.slice(0, length - 1), 227 {absoluteThreshold: absoluteThreshold}); 228 should(out1.slice(1), '1->2 explicit upmix: channel 1') 229 .beCloseToArray( 230 input.slice(0, length - 1), 231 {absoluteThreshold: absoluteThreshold}); 232 }) 233 .then(() => task.done()); 234 }); 235 236 audit.define( 237 { 238 label: 'mono-upmix-clamped-max', 239 description: '1->2 upmix, count mode clamped-max' 240 }, 241 (task, should) => { 242 upMixTest(should, {channelCountMode: 'clamped-max'}) 243 .then(buffer => { 244 let length = buffer.length; 245 let input = buffer.getChannelData(0); 246 let out0 = buffer.getChannelData(1); 247 let out1 = buffer.getChannelData(2); 248 249 // The convolver is basically a one-sample delay. With a mono 250 // input, the count set to 2, and a mode of 'clamped-max', the 251 // output should be mono 252 should(out0.slice(1), '1->2 clamped-max upmix: channel 0') 253 .beCloseToArray( 254 input.slice(0, length - 1), 255 {absoluteThreshold: absoluteThreshold}); 256 should(out1, '1->2 clamped-max upmix: channel 1') 257 .beConstantValueOf(0); 258 }) 259 .then(() => task.done()); 260 }); 261 262 function downMixTest(options, should) { 263 // Create an 4-channel offline context. The first two channels are for 264 // the stereo output of the convolver and the next two channels are for 265 // the reference stereo signal. 266 let context = new OfflineAudioContext(4, renderFrames, sampleRate); 267 context.destination.channelInterpretation = 'discrete'; 268 269 // Create oscillators for use as the input. The type and frequency is 270 // arbitrary except that oscillators must be different. 271 let src = new Array(options.numberOfInputs); 272 for (let k = 0; k < src.length; ++k) { 273 src[k] = new OscillatorNode( 274 context, {type: 'square', frequency: 440 + 220 * k}); 275 } 276 277 // Merger to combine the oscillators into one output stream. 278 let srcMerger = 279 new ChannelMergerNode(context, {numberOfInputs: src.length}); 280 281 for (let k = 0; k < src.length; ++k) { 282 src[k].connect(srcMerger, 0, k); 283 } 284 285 // Convolver under test. 286 let conv = new ConvolverNode(context, { 287 disableNormalization: true, 288 buffer: response, 289 channelCountMode: options.channelCountMode 290 }); 291 srcMerger.connect(conv); 292 293 // Splitter to get individual channels of the convolver output so we can 294 // feed them (eventually) to the context in the right set of channels. 295 let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2}); 296 conv.connect(splitter); 297 298 // Reference graph consists of a delay node to simulate the response of 299 // the convolver. (The convolver response is designed this way.) 300 let delay = new DelayNode(context, {delayTime: 1 / context.sampleRate}); 301 302 // Gain node to mix the sources to stereo in the desired way. (Could be 303 // done in the delay node, but let's keep the mixing separated from the 304 // functionality.) 305 let gainMixer = new GainNode( 306 context, {channelCount: 2, channelCountMode: 'explicit'}); 307 srcMerger.connect(gainMixer); 308 309 // Splitter to extract the channels of the reference signal. 310 let refSplitter = 311 new ChannelSplitterNode(context, {numberOfOutputs: 2}); 312 gainMixer.connect(delay).connect(refSplitter); 313 314 // Final merger to bring back the individual channels from the convolver 315 // and the reference in the right order for the destination. 316 let finalMerger = new ChannelMergerNode( 317 context, {numberOfInputs: context.destination.channelCount}); 318 319 // First two channels are for the convolver output, and the next two are 320 // for the reference. 321 splitter.connect(finalMerger, 0, 0); 322 splitter.connect(finalMerger, 1, 1); 323 refSplitter.connect(finalMerger, 0, 2); 324 refSplitter.connect(finalMerger, 1, 3); 325 326 finalMerger.connect(context.destination); 327 328 // Start the sources at last. 329 for (let k = 0; k < src.length; ++k) { 330 src[k].start(); 331 } 332 333 return context.startRendering().then(audioBuffer => { 334 // Extract the various channels out 335 let actual0 = audioBuffer.getChannelData(0); 336 let actual1 = audioBuffer.getChannelData(1); 337 let expected0 = audioBuffer.getChannelData(2); 338 let expected1 = audioBuffer.getChannelData(3); 339 340 let threshold = options.absoluteThreshold ? 341 options.absoluteThreshold : absoluteThreshold; 342 343 // Verify that each output channel of the convolver matches 344 // the delayed signal from the reference 345 should(actual0, options.prefix + ': Channel 0') 346 .beCloseToArray(expected0, {absoluteThreshold: threshold}); 347 should(actual1, options.prefix + ': Channel 1') 348 .beCloseToArray(expected1, {absoluteThreshold: threshold}); 349 }); 350 } 351 352 function upMixTest(should, options) { 353 // Offline context with 3 channels: 0 = source 354 // 1 = convolver output, left, 2 = convolver output, right. Context 355 // destination must be discrete so that channels don't get mixed in 356 // unexpected ways. 357 let context = new OfflineAudioContext(3, renderFrames, sampleRate); 358 context.destination.channelInterpretation = 'discrete'; 359 360 let merger = new ChannelMergerNode( 361 context, {numberOfInputs: context.destination.maxChannelCount}); 362 merger.connect(context.destination); 363 364 let src = new OscillatorNode(context); 365 366 // Mono response for convolver. Just a simple 1-frame delay. 367 let response = 368 new AudioBuffer({length: 2, sampleRate: context.sampleRate}); 369 response.getChannelData(0)[1] = 1; 370 371 // Set mode to explicit and count to 2 so we manually force the 372 // convolver to produce stereo output. Without this, it would be 373 // mono input with mono response, which produces a mono output. 374 let conv; 375 376 should( 377 () => {conv = new ConvolverNode(context, { 378 buffer: response, 379 disableNormalization: true, 380 channelCount: 2, 381 channelCountMode: options.channelCountMode 382 })}, 383 `new ConvolverNode({channelCountMode: '${ 384 options.channelCountMode}'})`) 385 .notThrow(); 386 387 // Split output of convolver into individual channels. 388 let convSplit = new ChannelSplitterNode(context, {numberOfOutputs: 2}); 389 390 src.connect(conv); 391 conv.connect(convSplit); 392 393 // Connect signals to destination in the desired way. 394 src.connect(merger, 0, 0); 395 convSplit.connect(merger, 0, 1); 396 convSplit.connect(merger, 1, 2); 397 398 src.start(); 399 400 return context.startRendering(); 401 } 402 403 audit.run(); 404 </script> 405 </body> 406 </html>