| <!DOCTYPE html> |
| <html> |
| <head> |
| <title> |
| Test Convolver on Real-time Context |
| </title> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="/webaudio/resources/audit-util.js"></script> |
| <script src="/webaudio/resources/audit.js"></script> |
| <script src="/webaudio/resources/convolution-testing.js"></script> |
| </head> |
| <body> |
| <script id="layout-test-code"> |
| const audit = Audit.createTaskRunner(); |
| // Choose a length that is larger enough to cause multiple threads to be |
| // used in the convolver. For browsers that don't support this, this |
| // value doesn't matter. |
| const pulseLength = 16384; |
| |
| // The computed SNR should be at least this large. This value depends on |
| // teh platform and browser. Don't set this value to be to much lower |
| // than this. It probably indicates a fairly inaccurate convolver or |
| // constant source node automations that should be fixed instead. |
| const minRequiredSNR = 83; |
| |
| // To test the real-time convolver, we convolve two square pulses together |
| // to produce a triangular pulse. To verify the result is correct we |
| // compare it against a constant source node configured to generate the |
| // expected ramp. |
| audit.define( |
| {label: 'test', description: 'Test convolver with real-time context'}, |
| (task, should) => { |
| // Use a power of two for the sample rate to eliminate round-off in |
| // computing times from frames. |
| const context = new AudioContext({sampleRate: 16384}); |
| |
| // Square pulse for the convolver impulse response. |
| const squarePulse = new AudioBuffer( |
| {length: pulseLength, sampleRate: context.sampleRate}); |
| squarePulse.getChannelData(0).fill(1); |
| |
| const convolver = new ConvolverNode( |
| context, {buffer: squarePulse, disableNormalization: true}); |
| |
| // Square pulse for testing |
| const srcSquare = new ConstantSourceNode(context, {offset: 0}); |
| srcSquare.connect(convolver); |
| |
| // Reference ramp. Automations on this constant source node will |
| // generate the desired ramp. |
| const srcRamp = new ConstantSourceNode(context, {offset: 0}); |
| |
| // Use these gain nodes to compute the difference between the |
| // convolver output and the expected ramp to create the error |
| // signal. |
| const inverter = new GainNode(context, {gain: -1}); |
| const sum = new GainNode(context, {gain: 1}); |
| convolver.connect(sum); |
| srcRamp.connect(inverter).connect(sum); |
| |
| // Square the error signal using this gain node. |
| const squarer = new GainNode(context, {gain: 0}); |
| sum.connect(squarer); |
| sum.connect(squarer.gain); |
| |
| // Merge the error signal and the square source so we can integrate |
| // the error signal to find an SNR. |
| const merger = new ChannelMergerNode(context, {numberOfInputs: 2}); |
| |
| squarer.connect(merger, 0, 0); |
| srcSquare.connect(merger, 0, 1); |
| |
| // For simplicity, use a ScriptProcessor to integrate the error |
| // signal. The square pulse signal is used as a gate over which the |
| // integration is done. When the pulse ends, the SNR is computed |
| // and the test ends. |
| |
| // |doSum| is used to determine when to integrate and when it |
| // becomes false, it signals the end of integration. |
| let doSum = false; |
| |
| // |signalSum| is the energy in the square pulse. |errorSum| is the |
| // energy in the error signal. |
| let signalSum = 0; |
| let errorSum = 0; |
| |
| let spn = context.createScriptProcessor(0, 2, 1); |
| spn.onaudioprocess = (event) => { |
| // Sum the values on the first channel when the second channel is |
| // not zero. When the second channel goes from non-zero to 0, |
| // dump the value out and terminate the test. |
| let c0 = event.inputBuffer.getChannelData(0); |
| let c1 = event.inputBuffer.getChannelData(1); |
| |
| for (let k = 0; k < c1.length; ++k) { |
| if (c1[k] == 0) { |
| if (doSum) { |
| doSum = false; |
| // Square wave is now silent and we were integration, so we |
| // can stop now and verify the SNR. |
| should(10 * Math.log10(signalSum / errorSum), 'SNR') |
| .beGreaterThanOrEqualTo(minRequiredSNR); |
| spn.onaudioprocess = null; |
| task.done(); |
| } |
| } else { |
| // Signal is non-zero so sum up the values. |
| doSum = true; |
| errorSum += c0[k]; |
| signalSum += c1[k] * c1[k]; |
| } |
| } |
| }; |
| |
| merger.connect(spn).connect(context.destination); |
| |
| // Schedule everything to start a bit in the futurefrom now, and end |
| // pulseLength frames later. |
| let now = context.currentTime; |
| |
| // |startFrame| is the number of frames to schedule ahead for |
| // testing. |
| const startFrame = 512; |
| const startTime = startFrame / context.sampleRate; |
| const pulseDuration = pulseLength / context.sampleRate; |
| |
| // Create a square pulse in the constant source node. |
| srcSquare.offset.setValueAtTime(1, now + startTime); |
| srcSquare.offset.setValueAtTime(0, now + startTime + pulseDuration); |
| |
| // Create the reference ramp. |
| srcRamp.offset.setValueAtTime(1, now + startTime); |
| srcRamp.offset.linearRampToValueAtTime( |
| pulseLength, |
| now + startTime + pulseDuration - 1 / context.sampleRate); |
| srcRamp.offset.linearRampToValueAtTime( |
| 0, |
| now + startTime + 2 * pulseDuration - 1 / context.sampleRate); |
| |
| // Start the ramps! |
| srcRamp.start(); |
| srcSquare.start(); |
| }); |
| |
| audit.run(); |
| </script> |
| </body> |
| </html> |