test_convolverNodeChannelInterpretationChanges.html (7323B)
1 <!DOCTYPE html> 2 <title>Test up-mixing in ConvolverNode after ChannelInterpretation change</title> 3 <script src="/resources/testharness.js"></script> 4 <script src="/resources/testharnessreport.js"></script> 5 <script> 6 // This test is not in wpt because it requires that multiple changes to the 7 // nodes in an AudioContext during a single event will be processed by the 8 // audio thread in a single transaction. Gecko provides that, but this is not 9 // currently required by the Web Audio API. 10 11 const EPSILON = Math.pow(2, -23); 12 // sampleRate is a power of two so that delay times are exact in base-2 13 // floating point arithmetic. 14 const SAMPLE_RATE = 32768; 15 // Length of initial mono signal in frames, if the test has an initial mono 16 // signal. This is more than one block to ensure that at least one block 17 // will be mono, even if interpolation in the DelayNode means that stereo is 18 // output one block earlier than if frames are delayed without interpolation. 19 const MONO_FRAMES = 256; 20 // Length of response buffer. This is greater than 1 to ensure that the 21 // convolver has stereo output at least one block after stereo input is 22 // disconnected. 23 const RESPONSE_FRAMES = 2; 24 25 function test_interpretation_change(t, initialInterpretation, initialMonoFrames) 26 { 27 let context = new AudioContext({sampleRate: SAMPLE_RATE}); 28 29 // Three independent signals. These are constant so that results are 30 // independent of the timing of the `ended` event. 31 let monoOffset = 0.25 32 let monoSource = new ConstantSourceNode(context, {offset: monoOffset}); 33 let leftOffset = 0.125; 34 let rightOffset = 0.5; 35 let leftSource = new ConstantSourceNode(context, {offset: leftOffset}); 36 let rightSource = new ConstantSourceNode(context, {offset: rightOffset}); 37 monoSource.start(); 38 leftSource.start(); 39 rightSource.start(); 40 41 let stereoMerger = new ChannelMergerNode(context, {numberOfInputs: 2}); 42 leftSource.connect(stereoMerger, 0, 0); 43 rightSource.connect(stereoMerger, 0, 1); 44 45 // The DelayNode initially has a single channel of silence, and so the 46 // output of the delay node is first mono silence (if there is a non-zero 47 // initialMonoFrames), then stereo. In Gecko, this triggers a convolver 48 // configuration that is different for different channelInterpretations. 49 let delay = 50 new DelayNode(context, 51 {maxDelayTime: MONO_FRAMES / context.sampleRate, 52 delayTime: initialMonoFrames / context.sampleRate}); 53 stereoMerger.connect(delay); 54 55 // Two convolvers with the same impulse response. The test convolver will 56 // process a mix of stereo and mono signals. The reference convolver will 57 // always process stereo, including the up-mixed mono signal. 58 let response = new AudioBuffer({numberOfChannels: 1, 59 length: RESPONSE_FRAMES, 60 sampleRate: context.sampleRate}); 61 response.getChannelData(0)[response.length - 1] = 1; 62 63 let testConvolver = new ConvolverNode(context, 64 {disableNormalization: true, 65 buffer: response}); 66 testConvolver.channelInterpretation = initialInterpretation; 67 let referenceConvolver = new ConvolverNode(context, 68 {disableNormalization: true, 69 buffer: response}); 70 // No need to set referenceConvolver.channelInterpretation because 71 // input is always stereo, due to up-mixing at gain node. 72 let referenceMixer = new GainNode(context); 73 referenceMixer.channelCount = 2; 74 referenceMixer.channelCountMode = "explicit"; 75 referenceMixer.channelInterpretation = initialInterpretation; 76 referenceMixer.connect(referenceConvolver); 77 78 delay.connect(testConvolver); 79 delay.connect(referenceMixer); 80 81 monoSource.connect(testConvolver); 82 monoSource.connect(referenceMixer); 83 84 // A timer sends 'ended' when the convolvers are known to be processing 85 // stereo. 86 let timer = new ConstantSourceNode(context); 87 timer.start(); 88 timer.stop((initialMonoFrames + 1) / context.sampleRate); 89 90 timer.onended = t.step_func(() => { 91 let changedInterpretation = 92 initialInterpretation == "speakers" ? "discrete" : "speakers"; 93 94 // Switch channelInterpretation in test and reference paths. 95 testConvolver.channelInterpretation = changedInterpretation; 96 referenceMixer.channelInterpretation = changedInterpretation; 97 98 // Disconnect the stereo input from both test and reference convolvers. 99 // The disconnected convolvers will continue to output stereo for at least 100 // one frame. The test convolver will up-mix its mono input into its two 101 // buffers. 102 delay.disconnect(); 103 104 // Capture the outputs in a script processor. 105 // 106 // The first two channels contain signal where some up-mixing occurs 107 // internally to the test convolver. 108 // 109 // The last two channels are expected to contain the same signal, but 110 // up-mixing was performed at a GainNode prior to convolution. 111 // 112 // Two stereo splitters will collect test and reference outputs. 113 let testSplitter = 114 new ChannelSplitterNode(context, {numberOfOutputs: 2}); 115 let referenceSplitter = 116 new ChannelSplitterNode(context, {numberOfOutputs: 2}); 117 testConvolver.connect(testSplitter); 118 referenceConvolver.connect(referenceSplitter); 119 120 let outputMerger = new ChannelMergerNode(context, {numberOfInputs: 4}); 121 testSplitter.connect(outputMerger, 0, 0); 122 testSplitter.connect(outputMerger, 1, 1); 123 referenceSplitter.connect(outputMerger, 0, 2); 124 referenceSplitter.connect(outputMerger, 1, 3); 125 126 let processor = context.createScriptProcessor(256, 4, 0); 127 outputMerger.connect(processor); 128 129 processor.onaudioprocess = t.step_func_done((e) => { 130 e.target.onaudioprocess = null; 131 outputMerger.disconnect(); 132 133 // The test convolver output is stereo for the first block. 134 let length = 128; 135 136 let buffer = e.inputBuffer; 137 let maxDiff = -1.0; 138 let frameIndex = 0; 139 let channelIndex = 0; 140 for (let c = 0; c < 2; ++c) { 141 let testOutput = buffer.getChannelData(0 + c); 142 let referenceOutput = buffer.getChannelData(2 + c); 143 for (var i = 0; i < length; ++i) { 144 var diff = Math.abs(testOutput[i] - referenceOutput[i]); 145 if (diff > maxDiff) { 146 maxDiff = diff; 147 frameIndex = i; 148 channelIndex = c; 149 } 150 } 151 } 152 assert_approx_equals(buffer.getChannelData(0 + channelIndex)[frameIndex], 153 buffer.getChannelData(2 + channelIndex)[frameIndex], 154 EPSILON, 155 `output at ${frameIndex} ` + 156 `in channel ${channelIndex}` ); 157 }); 158 }); 159 } 160 161 async_test((t) => test_interpretation_change(t, "speakers", MONO_FRAMES), 162 "speakers to discrete, initially mono"); 163 async_test((t) => test_interpretation_change(t, "discrete", MONO_FRAMES), 164 "discrete to speakers"); 165 // Gecko uses a separate path for "speakers" initial up-mixing when the 166 // convolver's first input is stereo, so test that separately. 167 async_test((t) => test_interpretation_change(t, "speakers", 0), 168 "speakers to discrete, initially stereo"); 169 </script>