convolver-response-2-chan.html (14059B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title> 5 Test Convolver Channel Outputs for Response with 2 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 stereo response. 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: 2, length: 4, 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 stereoResponseTest({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 stereoResponseTest({numberOfInputes: 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 stereoResponseTest({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 stereoResponseTest({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 stereoResponseTest({numberOfInputs: 6, prefix: '5.1', 106 absoluteThreshold: threshold}, should) 107 .then(() => task.done()); 108 }); 109 110 audit.define( 111 { 112 label: '2-channel input, explicit mode', 113 description: 'produces 2-channel output' 114 }, 115 (task, should) => { 116 stereoResponseExplicitTest( 117 { 118 numberOfInputes: 2, 119 prefix: '2-in explicit mode' 120 }, 121 should) 122 .then(() => task.done()); 123 }); 124 125 audit.define( 126 { 127 label: '3-channel input explicit mode', 128 description: '3->1 downmix producing 2-channel output' 129 }, 130 (task, should) => { 131 stereoResponseExplicitTest( 132 { 133 numberOfInputs: 3, 134 prefix: '3-in explicit' 135 }, 136 should) 137 .then(() => task.done()); 138 }); 139 140 audit.define( 141 { 142 label: '4-channel input explicit mode', 143 description: '4->1 downmix producing 2-channel output' 144 }, 145 (task, should) => { 146 stereoResponseExplicitTest( 147 { 148 numberOfInputs: 4, 149 prefix: '4-in explicit' 150 }, 151 should) 152 .then(() => task.done()); 153 }); 154 155 audit.define( 156 { 157 label: '5.1-channel input explicit mode', 158 description: '5.1->1 downmix producing 2-channel output' 159 }, 160 (task, should) => { 161 // Scale tolerance by maximum amplitude expected in down-mix 162 // output. 163 let threshold = (Math.sqrt(0.5) * 2 + 2.0) * absoluteThreshold; 164 165 stereoResponseExplicitTest( 166 { 167 numberOfInputs: 6, 168 prefix: '5.1-in explicit', 169 absoluteThreshold: threshold 170 }, 171 should) 172 .then(() => task.done()); 173 }); 174 175 function stereoResponseTest(options, should) { 176 // Create an 4-channel offline context. The first two channels are for 177 // the stereo output of the convolver and the next two channels are for 178 // the reference stereo signal. 179 let context = new OfflineAudioContext(4, renderFrames, sampleRate); 180 context.destination.channelInterpretation = 'discrete'; 181 182 // Create oscillators for use as the input. The type and frequency is 183 // arbitrary except that oscillators must be different. 184 let src = new Array(options.numberOfInputs); 185 for (let k = 0; k < src.length; ++k) { 186 src[k] = new OscillatorNode( 187 context, {type: 'square', frequency: 440 + 220 * k}); 188 } 189 190 // Merger to combine the oscillators into one output stream. 191 let srcMerger = 192 new ChannelMergerNode(context, {numberOfInputs: src.length}); 193 194 for (let k = 0; k < src.length; ++k) { 195 src[k].connect(srcMerger, 0, k); 196 } 197 198 // Convolver under test. 199 let conv = new ConvolverNode( 200 context, {disableNormalization: true, buffer: response}); 201 srcMerger.connect(conv); 202 203 // Splitter to get individual channels of the convolver output so we can 204 // feed them (eventually) to the context in the right set of channels. 205 let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2}); 206 conv.connect(splitter); 207 208 // Reference graph consists of a delays node to simulate the response of 209 // the convolver. (The convolver response is designed this way.) 210 let delay = new Array(2); 211 for (let k = 0; k < delay.length; ++k) { 212 delay[k] = new DelayNode(context, { 213 delayTime: (k + 1) / context.sampleRate, 214 channelCount: 1, 215 channelCountMode: 'explicit' 216 }); 217 } 218 219 // Gain node to mix the sources to stereo in the desired way. (Could be 220 // done in the delay node, but let's keep the mixing separated from the 221 // functionality.) 222 let gainMixer = new GainNode( 223 context, {channelCount: 2, channelCountMode: 'explicit'}); 224 srcMerger.connect(gainMixer); 225 226 // Splitter to extract the channels of the reference signal. 227 let refSplitter = 228 new ChannelSplitterNode(context, {numberOfOutputs: 2}); 229 gainMixer.connect(refSplitter); 230 231 // Connect each channel to the delay nodes 232 for (let k = 0; k < delay.length; ++k) { 233 refSplitter.connect(delay[k], k); 234 } 235 236 // Final merger to bring back the individual channels from the convolver 237 // and the reference in the right order for the destination. 238 let finalMerger = new ChannelMergerNode( 239 context, {numberOfInputs: context.destination.channelCount}); 240 241 // First two channels are for the convolver output, and the next two are 242 // for the reference. 243 splitter.connect(finalMerger, 0, 0); 244 splitter.connect(finalMerger, 1, 1); 245 delay[0].connect(finalMerger, 0, 2); 246 delay[1].connect(finalMerger, 0, 3); 247 248 finalMerger.connect(context.destination); 249 250 // Start the sources at last. 251 for (let k = 0; k < src.length; ++k) { 252 src[k].start(); 253 } 254 255 return context.startRendering().then(audioBuffer => { 256 // Extract the various channels out 257 let actual0 = audioBuffer.getChannelData(0); 258 let actual1 = audioBuffer.getChannelData(1); 259 let expected0 = audioBuffer.getChannelData(2); 260 let expected1 = audioBuffer.getChannelData(3); 261 262 let threshold = options.absoluteThreshold ? 263 options.absoluteThreshold : absoluteThreshold; 264 265 // Verify that each output channel of the convolver matches 266 // the delayed signal from the reference 267 should(actual0, options.prefix + ': Channel 0') 268 .beCloseToArray(expected0, {absoluteThreshold: threshold}); 269 should(actual1, options.prefix + ': Channel 1') 270 .beCloseToArray(expected1, {absoluteThreshold: threshold}); 271 }); 272 } 273 274 function stereoResponseExplicitTest(options, should) { 275 // Create an 4-channel offline context. The first two channels are for 276 // the stereo output of the convolver and the next two channels are for 277 // the reference stereo signal. 278 let context = new OfflineAudioContext(4, renderFrames, sampleRate); 279 context.destination.channelInterpretation = 'discrete'; 280 281 // Create oscillators for use as the input. The type and frequency is 282 // arbitrary except that oscillators must be different. 283 let src = new Array(options.numberOfInputs); 284 for (let k = 0; k < src.length; ++k) { 285 src[k] = new OscillatorNode( 286 context, {type: 'square', frequency: 440 + 220 * k}); 287 } 288 289 // Merger to combine the oscillators into one output stream. 290 let srcMerger = 291 new ChannelMergerNode(context, {numberOfInputs: src.length}); 292 293 for (let k = 0; k < src.length; ++k) { 294 src[k].connect(srcMerger, 0, k); 295 } 296 297 // Convolver under test. 298 let conv = new ConvolverNode(context, { 299 channelCount: 1, 300 channelCountMode: 'explicit', 301 disableNormalization: true, 302 buffer: response 303 }); 304 srcMerger.connect(conv); 305 306 // Splitter to get individual channels of the convolver output so we can 307 // feed them (eventually) to the context in the right set of channels. 308 let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2}); 309 conv.connect(splitter); 310 311 // Reference graph consists of a delays node to simulate the response of 312 // the convolver. (The convolver response is designed this way.) 313 let delay = new Array(2); 314 for (let k = 0; k < delay.length; ++k) { 315 delay[k] = new DelayNode(context, { 316 delayTime: (k + 1) / context.sampleRate, 317 channelCount: 1, 318 channelCountMode: 'explicit' 319 }); 320 } 321 322 // Gain node to mix the sources in the same way as the convolver. 323 let gainMixer = new GainNode( 324 context, {channelCount: 1, channelCountMode: 'explicit'}); 325 srcMerger.connect(gainMixer); 326 327 // Connect each channel to the delay nodes 328 for (let k = 0; k < delay.length; ++k) { 329 gainMixer.connect(delay[k]); 330 } 331 332 // Final merger to bring back the individual channels from the convolver 333 // and the reference in the right order for the destination. 334 let finalMerger = new ChannelMergerNode( 335 context, {numberOfInputs: context.destination.channelCount}); 336 337 // First two channels are for the convolver output, and the next two are 338 // for the reference. 339 splitter.connect(finalMerger, 0, 0); 340 splitter.connect(finalMerger, 1, 1); 341 delay[0].connect(finalMerger, 0, 2); 342 delay[1].connect(finalMerger, 0, 3); 343 344 finalMerger.connect(context.destination); 345 346 // Start the sources at last. 347 for (let k = 0; k < src.length; ++k) { 348 src[k].start(); 349 } 350 351 return context.startRendering().then(audioBuffer => { 352 // Extract the various channels out 353 let actual0 = audioBuffer.getChannelData(0); 354 let actual1 = audioBuffer.getChannelData(1); 355 let expected0 = audioBuffer.getChannelData(2); 356 let expected1 = audioBuffer.getChannelData(3); 357 358 let threshold = options.absoluteThreshold ? 359 options.absoluteThreshold : absoluteThreshold; 360 361 // Verify that each output channel of the convolver matches 362 // the delayed signal from the reference 363 should(actual0, options.prefix + ': Channel 0') 364 .beCloseToArray(expected0, {absoluteThreshold: threshold}); 365 should(actual1, options.prefix + ': Channel 1') 366 .beCloseToArray(expected1, {absoluteThreshold: threshold}); 367 }); 368 } 369 370 audit.run(); 371 </script> 372 </body> 373 </html>