diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface')
13 files changed, 2213 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/active-processing.https.html b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/active-processing.https.html new file mode 100644 index 0000000000..0712d6bcce --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/active-processing.https.html @@ -0,0 +1,93 @@ +<!doctype html> +<html> + <head> + <title> + Test Active Processing for ConvolverNode + </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> + </head> + + <body> + <script id="layout-test-code"> + // AudioProcessor that sends a message to its AudioWorkletNode whenver the + // number of channels on its input changes. + let filePath = + '../the-audioworklet-interface/processors/active-processing.js'; + + const audit = Audit.createTaskRunner(); + + let context; + + audit.define('initialize', (task, should) => { + // Create context and load the module + context = new AudioContext(); + should( + context.audioWorklet.addModule(filePath), + 'AudioWorklet module loading') + .beResolved() + .then(() => task.done()); + }); + + audit.define('test', (task, should) => { + const src = new OscillatorNode(context); + + const response = new AudioBuffer({numberOfChannels: 2, length: 150, + sampleRate: context.sampleRate}); + + const conv = new ConvolverNode(context, {buffer: response}); + + const testerNode = + new AudioWorkletNode(context, 'active-processing-tester', { + // Use as short a duration as possible to keep the test from + // taking too much time. + processorOptions: {testDuration: .5}, + }); + + // Expected number of output channels from the convolver node. We should + // start with the number of inputs, because the source (oscillator) is + // actively processing. When the source stops, the number of channels + // should change to 0. + const expectedValues = [2, 0]; + let index = 0; + + testerNode.port.onmessage = event => { + let count = event.data.channelCount; + let finished = event.data.finished; + + // If we're finished, end testing. + if (finished) { + // Verify that we got the expected number of changes. + should(index, 'Number of distinct values') + .beEqualTo(expectedValues.length); + + task.done(); + return; + } + + if (index < expectedValues.length) { + // Verify that the number of channels matches the expected number of + // channels. + should(count, `Test ${index}: Number of convolver output channels`) + .beEqualTo(expectedValues[index]); + } + + ++index; + }; + + // Create the graph and go + src.connect(conv).connect(testerNode).connect(context.destination); + src.start(); + + // Stop the source after a short time so we can test that the convolver + // changes to not actively processing and thus produces a single channel + // of silence. + src.stop(context.currentTime + .1); + }); + + audit.run(); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolution-mono-mono.html b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolution-mono-mono.html new file mode 100644 index 0000000000..570efebe22 --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolution-mono-mono.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<html> + <head> + <title> + convolution-mono-mono.html + </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"> + let audit = Audit.createTaskRunner(); + + // description("Tests ConvolverNode processing a mono channel with mono + // impulse response."); + + // To test the convolver, we convolve two square pulses together to + // produce a triangular pulse. To verify the result is correct we + // check several parts of the result. First, we make sure the initial + // part of the result is zero (due to the latency in the convolver). + // Next, the triangular pulse should match the theoretical result to + // within some roundoff. After the triangular pulse, the result + // should be exactly zero, but round-off prevents that. We make sure + // the part after the pulse is sufficiently close to zero. Finally, + // the result should be exactly zero because the inputs are exactly + // zero. + audit.define('test', function(task, should) { + // Create offline audio context. + let context = new OfflineAudioContext( + 2, sampleRate * renderLengthSeconds, sampleRate); + + let squarePulse = createSquarePulseBuffer(context, pulseLengthFrames); + let trianglePulse = + createTrianglePulseBuffer(context, 2 * pulseLengthFrames); + + let bufferSource = context.createBufferSource(); + bufferSource.buffer = squarePulse; + + let convolver = context.createConvolver(); + convolver.normalize = false; + convolver.buffer = squarePulse; + + bufferSource.connect(convolver); + convolver.connect(context.destination); + + bufferSource.start(0); + + context.startRendering() + .then(buffer => { + checkConvolvedResult(buffer, trianglePulse, should); + }) + .then(task.done.bind(task)); + ; + }); + + audit.run(); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-cascade.html b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-cascade.html new file mode 100644 index 0000000000..20bdfbdf4e --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-cascade.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html> + <head> + <title> + Test Cascade of Mono Convolvers + </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> + </head> + <body> + <script id="layout-test-code"> + let audit = Audit.createTaskRunner(); + + // Arbitrary sample rate and reasonably short duration + let sampleRate = 8000; + let duration = 0.25; + let renderFrames = duration * sampleRate; + + audit.define( + {label: 'cascade-mono', description: 'Cascaded mono convolvers'}, + (task, should) => { + // Cascade two convolvers with mono responses and verify that the + // output is not silent. + let context = new OfflineAudioContext(1, renderFrames, sampleRate); + + let b0 = + new AudioBuffer({length: 5, sampleRate: context.sampleRate}); + b0.getChannelData(0)[1] = 1; + let c0 = new ConvolverNode( + context, {disableNormalization: true, buffer: b0}); + + let b1 = + new AudioBuffer({length: 5, sampleRate: context.sampleRate}); + b1.getChannelData(0)[2] = 1; + + let c1 = new ConvolverNode( + context, {disableNormalization: true, buffer: b1}); + + let src = new OscillatorNode(context); + + src.connect(c0).connect(c1).connect(context.destination); + + src.start(); + + context.startRendering() + .then(audioBuffer => { + // Just verify the output is not silent + let audio = audioBuffer.getChannelData(0); + + should(audio, 'Output of cascaded mono convolvers') + .notBeConstantValueOf(0); + }) + .then(() => task.done()); + }); + + audit.run(); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-channels.html b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-channels.html new file mode 100644 index 0000000000..ac4f198d7c --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-channels.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<html> + <head> + <title> + Test Supported Number of Channels for ConvolverNode + </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> + </head> + <body> + <script id="layout-test-code"> + let audit = Audit.createTaskRunner(); + + audit.define('channel-count-test', (task, should) => { + // Just need a context to create nodes on, so any allowed length and + // rate is ok. + let context = new OfflineAudioContext(1, 1, 48000); + + let success = true; + + for (let count = 1; count <= 32; ++count) { + let convolver = context.createConvolver(); + let buffer = context.createBuffer(count, 1, context.sampleRate); + let message = 'ConvolverNode with buffer of ' + count + ' channels'; + + if (count == 1 || count == 2 || count == 4) { + // These are the only valid channel counts for the buffer. + should(() => convolver.buffer = buffer, message).notThrow(); + } else { + should(() => convolver.buffer = buffer, message) + .throw(DOMException, 'NotSupportedError'); + } + } + + task.done(); + }); + + audit.run(); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-response-1-chan.html b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-response-1-chan.html new file mode 100644 index 0000000000..e239a5e86f --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-response-1-chan.html @@ -0,0 +1,406 @@ +<!DOCTYPE html> +<html> + <head> + <title> + Test Convolver Channel Outputs for Response with 1 channel + </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> + </head> + <body> + <script id="layout-test-code"> + // Test various convolver configurations when the convolver response has + // one channel (mono). + + // This is somewhat arbitrary. It is the minimum value for which tests + // pass with both FFmpeg and KISS FFT implementations for 256 points. + // The value was similar for each implementation. + const absoluteThreshold = Math.pow(2, -21); + + // Fairly arbitrary sample rate, except that we want the rate to be a + // power of two so that 1/sampleRate is exactly representable as a + // single-precision float. + let sampleRate = 8192; + + // A fairly arbitrary number of frames, except the number of frames should + // be more than a few render quanta. + let renderFrames = 10 * 128; + + let audit = Audit.createTaskRunner(); + + // Convolver response + let response; + + audit.define( + { + label: 'initialize', + description: 'Convolver response with one channel' + }, + (task, should) => { + // Convolver response + should( + () => { + response = new AudioBuffer( + {numberOfChannels: 1, length: 2, sampleRate: sampleRate}); + response.getChannelData(0)[1] = 1; + }, + 'new AudioBuffer({numberOfChannels: 1, length: 2, sampleRate: ' + + sampleRate + '})') + .notThrow(); + + task.done(); + }); + + audit.define( + {label: '1-channel input', description: 'produces 1-channel output'}, + (task, should) => { + // Create a 3-channel context: channel 0 = convolver under test, + // channel 1: test that convolver output is not stereo, channel 2: + // expected output. The context MUST be discrete so that the + // channels don't get mixed in some unexpected way. + let context = new OfflineAudioContext(3, renderFrames, sampleRate); + context.destination.channelInterpretation = 'discrete'; + + let src = new OscillatorNode(context); + let conv = new ConvolverNode( + context, {disableNormalization: true, buffer: response}); + + // Splitter node to verify that the output of the convolver is mono. + // channelInterpretation must be 'discrete' so we don't do any + // mixing of the input to the node. + let splitter = new ChannelSplitterNode( + context, + {numberOfOutputs: 2, channelInterpretation: 'discrete'}); + + // Final merger to feed all of the individual channels into the + // destination. + let merger = new ChannelMergerNode(context, {numberOfInputs: 3}); + + src.connect(conv).connect(splitter); + splitter.connect(merger, 0, 0); + splitter.connect(merger, 1, 1); + + // The convolver response is a 1-sample delay. Use a delay node to + // implement this. + let delay = + new DelayNode(context, {delayTime: 1 / context.sampleRate}); + src.connect(delay); + delay.connect(merger, 0, 2); + + merger.connect(context.destination); + + src.start(); + + context.startRendering() + .then(audioBuffer => { + // Extract out the three channels + let actual = audioBuffer.getChannelData(0); + let c1 = audioBuffer.getChannelData(1); + let expected = audioBuffer.getChannelData(2); + + // c1 is expected to be zero. + should(c1, '1: Channel 1').beConstantValueOf(0); + + // The expected and actual results should be identical + should(actual, 'Convolver output') + .beCloseToArray(expected, + {absoluteThreshold: absoluteThreshold}); + }) + .then(() => task.done()); + }); + + audit.define( + {label: '2-channel input', description: 'produces 2-channel output'}, + (task, should) => { + downMixTest({numberOfInputs: 2, prefix: '2'}, should) + .then(() => task.done()); + }); + + audit.define( + { + label: '3-channel input', + description: '3->2 downmix producing 2-channel output' + }, + (task, should) => { + downMixTest({numberOfInputs: 3, prefix: '3'}, should) + .then(() => task.done()); + }); + + audit.define( + { + label: '4-channel input', + description: '4->2 downmix producing 2-channel output' + }, + (task, should) => { + downMixTest({numberOfInputs: 4, prefix: '4'}, should) + .then(() => task.done()); + }); + + audit.define( + { + label: '5.1-channel input', + description: '5.1->2 downmix producing 2-channel output' + }, + (task, should) => { + // Scale tolerance by maximum amplitude expected in down-mix + // output. + let threshold = (1.0 + Math.sqrt(0.5) * 2) * absoluteThreshold; + + downMixTest({numberOfInputs: 6, prefix: '5.1', + absoluteThreshold: threshold}, should) + .then(() => task.done()); + }); + + audit.define( + { + label: '3-channel input, explicit', + description: '3->2 explicit downmix producing 2-channel output' + }, + (task, should) => { + downMixTest( + { + channelCountMode: 'explicit', + numberOfInputs: 3, + prefix: '3 chan downmix explicit' + }, + should) + .then(() => task.done()); + }); + + audit.define( + { + label: '4-channel input, explicit', + description: '4->2 explicit downmix producing 2-channel output' + }, + (task, should) => { + downMixTest( + { + channelCountMode: 'explicit', + numberOfInputs: 4, + prefix: '4 chan downmix explicit' + }, + should) + .then(() => task.done()); + }); + + audit.define( + { + label: '5.1-channel input, explicit', + description: '5.1->2 explicit downmix producing 2-channel output' + }, + (task, should) => { + // Scale tolerance by maximum amplitude expected in down-mix + // output. + let threshold = (1.0 + Math.sqrt(0.5) * 2) * absoluteThreshold; + + downMixTest( + { + channelCountMode: 'explicit', + numberOfInputs: 6, + prefix: '5.1 chan downmix explicit', + absoluteThreshold: threshold + }, + should) + .then(() => task.done()); + }); + + audit.define( + { + label: 'mono-upmix-explicit', + description: '1->2 upmix, count mode explicit' + }, + (task, should) => { + upMixTest(should, {channelCountMode: 'explicit'}) + .then(buffer => { + let length = buffer.length; + let input = buffer.getChannelData(0); + let out0 = buffer.getChannelData(1); + let out1 = buffer.getChannelData(2); + + // The convolver is basically a one-sample delay. Verify that + // that each channel is delayed by one sample. + should(out0.slice(1), '1->2 explicit upmix: channel 0') + .beCloseToArray( + input.slice(0, length - 1), + {absoluteThreshold: absoluteThreshold}); + should(out1.slice(1), '1->2 explicit upmix: channel 1') + .beCloseToArray( + input.slice(0, length - 1), + {absoluteThreshold: absoluteThreshold}); + }) + .then(() => task.done()); + }); + + audit.define( + { + label: 'mono-upmix-clamped-max', + description: '1->2 upmix, count mode clamped-max' + }, + (task, should) => { + upMixTest(should, {channelCountMode: 'clamped-max'}) + .then(buffer => { + let length = buffer.length; + let input = buffer.getChannelData(0); + let out0 = buffer.getChannelData(1); + let out1 = buffer.getChannelData(2); + + // The convolver is basically a one-sample delay. With a mono + // input, the count set to 2, and a mode of 'clamped-max', the + // output should be mono + should(out0.slice(1), '1->2 clamped-max upmix: channel 0') + .beCloseToArray( + input.slice(0, length - 1), + {absoluteThreshold: absoluteThreshold}); + should(out1, '1->2 clamped-max upmix: channel 1') + .beConstantValueOf(0); + }) + .then(() => task.done()); + }); + + function downMixTest(options, should) { + // Create an 4-channel offline context. The first two channels are for + // the stereo output of the convolver and the next two channels are for + // the reference stereo signal. + let context = new OfflineAudioContext(4, renderFrames, sampleRate); + context.destination.channelInterpretation = 'discrete'; + + // Create oscillators for use as the input. The type and frequency is + // arbitrary except that oscillators must be different. + let src = new Array(options.numberOfInputs); + for (let k = 0; k < src.length; ++k) { + src[k] = new OscillatorNode( + context, {type: 'square', frequency: 440 + 220 * k}); + } + + // Merger to combine the oscillators into one output stream. + let srcMerger = + new ChannelMergerNode(context, {numberOfInputs: src.length}); + + for (let k = 0; k < src.length; ++k) { + src[k].connect(srcMerger, 0, k); + } + + // Convolver under test. + let conv = new ConvolverNode(context, { + disableNormalization: true, + buffer: response, + channelCountMode: options.channelCountMode + }); + srcMerger.connect(conv); + + // Splitter to get individual channels of the convolver output so we can + // feed them (eventually) to the context in the right set of channels. + let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2}); + conv.connect(splitter); + + // Reference graph consists of a delay node to simulate the response of + // the convolver. (The convolver response is designed this way.) + let delay = new DelayNode(context, {delayTime: 1 / context.sampleRate}); + + // Gain node to mix the sources to stereo in the desired way. (Could be + // done in the delay node, but let's keep the mixing separated from the + // functionality.) + let gainMixer = new GainNode( + context, {channelCount: 2, channelCountMode: 'explicit'}); + srcMerger.connect(gainMixer); + + // Splitter to extract the channels of the reference signal. + let refSplitter = + new ChannelSplitterNode(context, {numberOfOutputs: 2}); + gainMixer.connect(delay).connect(refSplitter); + + // Final merger to bring back the individual channels from the convolver + // and the reference in the right order for the destination. + let finalMerger = new ChannelMergerNode( + context, {numberOfInputs: context.destination.channelCount}); + + // First two channels are for the convolver output, and the next two are + // for the reference. + splitter.connect(finalMerger, 0, 0); + splitter.connect(finalMerger, 1, 1); + refSplitter.connect(finalMerger, 0, 2); + refSplitter.connect(finalMerger, 1, 3); + + finalMerger.connect(context.destination); + + // Start the sources at last. + for (let k = 0; k < src.length; ++k) { + src[k].start(); + } + + return context.startRendering().then(audioBuffer => { + // Extract the various channels out + let actual0 = audioBuffer.getChannelData(0); + let actual1 = audioBuffer.getChannelData(1); + let expected0 = audioBuffer.getChannelData(2); + let expected1 = audioBuffer.getChannelData(3); + + let threshold = options.absoluteThreshold ? + options.absoluteThreshold : absoluteThreshold; + + // Verify that each output channel of the convolver matches + // the delayed signal from the reference + should(actual0, options.prefix + ': Channel 0') + .beCloseToArray(expected0, {absoluteThreshold: threshold}); + should(actual1, options.prefix + ': Channel 1') + .beCloseToArray(expected1, {absoluteThreshold: threshold}); + }); + } + + function upMixTest(should, options) { + // Offline context with 3 channels: 0 = source + // 1 = convolver output, left, 2 = convolver output, right. Context + // destination must be discrete so that channels don't get mixed in + // unexpected ways. + let context = new OfflineAudioContext(3, renderFrames, sampleRate); + context.destination.channelInterpretation = 'discrete'; + + let merger = new ChannelMergerNode( + context, {numberOfInputs: context.destination.maxChannelCount}); + merger.connect(context.destination); + + let src = new OscillatorNode(context); + + // Mono response for convolver. Just a simple 1-frame delay. + let response = + new AudioBuffer({length: 2, sampleRate: context.sampleRate}); + response.getChannelData(0)[1] = 1; + + // Set mode to explicit and count to 2 so we manually force the + // convolver to produce stereo output. Without this, it would be + // mono input with mono response, which produces a mono output. + let conv; + + should( + () => {conv = new ConvolverNode(context, { + buffer: response, + disableNormalization: true, + channelCount: 2, + channelCountMode: options.channelCountMode + })}, + `new ConvolverNode({channelCountMode: '${ + options.channelCountMode}'})`) + .notThrow(); + + // Split output of convolver into individual channels. + let convSplit = new ChannelSplitterNode(context, {numberOfOutputs: 2}); + + src.connect(conv); + conv.connect(convSplit); + + // Connect signals to destination in the desired way. + src.connect(merger, 0, 0); + convSplit.connect(merger, 0, 1); + convSplit.connect(merger, 1, 2); + + src.start(); + + return context.startRendering(); + } + + audit.run(); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-response-2-chan.html b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-response-2-chan.html new file mode 100644 index 0000000000..a73eb3f8ab --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-response-2-chan.html @@ -0,0 +1,373 @@ +<!DOCTYPE html> +<html> + <head> + <title> + Test Convolver Channel Outputs for Response with 2 channels + </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> + </head> + <body> + <script id="layout-test-code"> + // Test various convolver configurations when the convolver response has + // a stereo response. + + // This is somewhat arbitrary. It is the minimum value for which tests + // pass with both FFmpeg and KISS FFT implementations for 256 points. + // The value was similar for each implementation. + const absoluteThreshold = Math.pow(2, -21); + + // Fairly arbitrary sample rate, except that we want the rate to be a + // power of two so that 1/sampleRate is exactly representable as a + // single-precision float. + let sampleRate = 8192; + + // A fairly arbitrary number of frames, except the number of frames should + // be more than a few render quanta. + let renderFrames = 10 * 128; + + let audit = Audit.createTaskRunner(); + + // Convolver response + let response; + + audit.define( + { + label: 'initialize', + description: 'Convolver response with one channel' + }, + (task, should) => { + // Convolver response + should( + () => { + response = new AudioBuffer( + {numberOfChannels: 2, length: 4, sampleRate: sampleRate}); + // Each channel of the response is a simple impulse (with + // different delay) so that we can use a DelayNode to simulate + // the convolver output. Channel k is delayed by k+1 frames. + for (let k = 0; k < response.numberOfChannels; ++k) { + response.getChannelData(k)[k + 1] = 1; + } + }, + 'new AudioBuffer({numberOfChannels: 2, length: 4, sampleRate: ' + + sampleRate + '})') + .notThrow(); + + task.done(); + }); + + audit.define( + {label: '1-channel input', description: 'produces 2-channel output'}, + (task, should) => { + stereoResponseTest({numberOfInputs: 1, prefix: '1'}, should) + .then(() => task.done()); + }); + + audit.define( + {label: '2-channel input', description: 'produces 2-channel output'}, + (task, should) => { + stereoResponseTest({numberOfInputes: 2, prefix: '2'}, should) + .then(() => task.done()); + }); + + audit.define( + { + label: '3-channel input', + description: '3->2 downmix producing 2-channel output' + }, + (task, should) => { + stereoResponseTest({numberOfInputs: 3, prefix: '3'}, should) + .then(() => task.done()); + }); + + audit.define( + { + label: '4-channel input', + description: '4->2 downmix producing 2-channel output' + }, + (task, should) => { + stereoResponseTest({numberOfInputs: 4, prefix: '4'}, should) + .then(() => task.done()); + }); + + audit.define( + { + label: '5.1-channel input', + description: '5.1->2 downmix producing 2-channel output' + }, + (task, should) => { + // Scale tolerance by maximum amplitude expected in down-mix + // output. + let threshold = (1.0 + Math.sqrt(0.5) * 2) * absoluteThreshold; + + stereoResponseTest({numberOfInputs: 6, prefix: '5.1', + absoluteThreshold: threshold}, should) + .then(() => task.done()); + }); + + audit.define( + { + label: '2-channel input, explicit mode', + description: 'produces 2-channel output' + }, + (task, should) => { + stereoResponseExplicitTest( + { + numberOfInputes: 2, + prefix: '2-in explicit mode' + }, + should) + .then(() => task.done()); + }); + + audit.define( + { + label: '3-channel input explicit mode', + description: '3->1 downmix producing 2-channel output' + }, + (task, should) => { + stereoResponseExplicitTest( + { + numberOfInputs: 3, + prefix: '3-in explicit' + }, + should) + .then(() => task.done()); + }); + + audit.define( + { + label: '4-channel input explicit mode', + description: '4->1 downmix producing 2-channel output' + }, + (task, should) => { + stereoResponseExplicitTest( + { + numberOfInputs: 4, + prefix: '4-in explicit' + }, + should) + .then(() => task.done()); + }); + + audit.define( + { + label: '5.1-channel input explicit mode', + description: '5.1->1 downmix producing 2-channel output' + }, + (task, should) => { + // Scale tolerance by maximum amplitude expected in down-mix + // output. + let threshold = (Math.sqrt(0.5) * 2 + 2.0) * absoluteThreshold; + + stereoResponseExplicitTest( + { + numberOfInputs: 6, + prefix: '5.1-in explicit', + absoluteThreshold: threshold + }, + should) + .then(() => task.done()); + }); + + function stereoResponseTest(options, should) { + // Create an 4-channel offline context. The first two channels are for + // the stereo output of the convolver and the next two channels are for + // the reference stereo signal. + let context = new OfflineAudioContext(4, renderFrames, sampleRate); + context.destination.channelInterpretation = 'discrete'; + + // Create oscillators for use as the input. The type and frequency is + // arbitrary except that oscillators must be different. + let src = new Array(options.numberOfInputs); + for (let k = 0; k < src.length; ++k) { + src[k] = new OscillatorNode( + context, {type: 'square', frequency: 440 + 220 * k}); + } + + // Merger to combine the oscillators into one output stream. + let srcMerger = + new ChannelMergerNode(context, {numberOfInputs: src.length}); + + for (let k = 0; k < src.length; ++k) { + src[k].connect(srcMerger, 0, k); + } + + // Convolver under test. + let conv = new ConvolverNode( + context, {disableNormalization: true, buffer: response}); + srcMerger.connect(conv); + + // Splitter to get individual channels of the convolver output so we can + // feed them (eventually) to the context in the right set of channels. + let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2}); + conv.connect(splitter); + + // Reference graph consists of a delays node to simulate the response of + // the convolver. (The convolver response is designed this way.) + let delay = new Array(2); + for (let k = 0; k < delay.length; ++k) { + delay[k] = new DelayNode(context, { + delayTime: (k + 1) / context.sampleRate, + channelCount: 1, + channelCountMode: 'explicit' + }); + } + + // Gain node to mix the sources to stereo in the desired way. (Could be + // done in the delay node, but let's keep the mixing separated from the + // functionality.) + let gainMixer = new GainNode( + context, {channelCount: 2, channelCountMode: 'explicit'}); + srcMerger.connect(gainMixer); + + // Splitter to extract the channels of the reference signal. + let refSplitter = + new ChannelSplitterNode(context, {numberOfOutputs: 2}); + gainMixer.connect(refSplitter); + + // Connect each channel to the delay nodes + for (let k = 0; k < delay.length; ++k) { + refSplitter.connect(delay[k], k); + } + + // Final merger to bring back the individual channels from the convolver + // and the reference in the right order for the destination. + let finalMerger = new ChannelMergerNode( + context, {numberOfInputs: context.destination.channelCount}); + + // First two channels are for the convolver output, and the next two are + // for the reference. + splitter.connect(finalMerger, 0, 0); + splitter.connect(finalMerger, 1, 1); + delay[0].connect(finalMerger, 0, 2); + delay[1].connect(finalMerger, 0, 3); + + finalMerger.connect(context.destination); + + // Start the sources at last. + for (let k = 0; k < src.length; ++k) { + src[k].start(); + } + + return context.startRendering().then(audioBuffer => { + // Extract the various channels out + let actual0 = audioBuffer.getChannelData(0); + let actual1 = audioBuffer.getChannelData(1); + let expected0 = audioBuffer.getChannelData(2); + let expected1 = audioBuffer.getChannelData(3); + + let threshold = options.absoluteThreshold ? + options.absoluteThreshold : absoluteThreshold; + + // Verify that each output channel of the convolver matches + // the delayed signal from the reference + should(actual0, options.prefix + ': Channel 0') + .beCloseToArray(expected0, {absoluteThreshold: threshold}); + should(actual1, options.prefix + ': Channel 1') + .beCloseToArray(expected1, {absoluteThreshold: threshold}); + }); + } + + function stereoResponseExplicitTest(options, should) { + // Create an 4-channel offline context. The first two channels are for + // the stereo output of the convolver and the next two channels are for + // the reference stereo signal. + let context = new OfflineAudioContext(4, renderFrames, sampleRate); + context.destination.channelInterpretation = 'discrete'; + + // Create oscillators for use as the input. The type and frequency is + // arbitrary except that oscillators must be different. + let src = new Array(options.numberOfInputs); + for (let k = 0; k < src.length; ++k) { + src[k] = new OscillatorNode( + context, {type: 'square', frequency: 440 + 220 * k}); + } + + // Merger to combine the oscillators into one output stream. + let srcMerger = + new ChannelMergerNode(context, {numberOfInputs: src.length}); + + for (let k = 0; k < src.length; ++k) { + src[k].connect(srcMerger, 0, k); + } + + // Convolver under test. + let conv = new ConvolverNode(context, { + channelCount: 1, + channelCountMode: 'explicit', + disableNormalization: true, + buffer: response + }); + srcMerger.connect(conv); + + // Splitter to get individual channels of the convolver output so we can + // feed them (eventually) to the context in the right set of channels. + let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2}); + conv.connect(splitter); + + // Reference graph consists of a delays node to simulate the response of + // the convolver. (The convolver response is designed this way.) + let delay = new Array(2); + for (let k = 0; k < delay.length; ++k) { + delay[k] = new DelayNode(context, { + delayTime: (k + 1) / context.sampleRate, + channelCount: 1, + channelCountMode: 'explicit' + }); + } + + // Gain node to mix the sources in the same way as the convolver. + let gainMixer = new GainNode( + context, {channelCount: 1, channelCountMode: 'explicit'}); + srcMerger.connect(gainMixer); + + // Connect each channel to the delay nodes + for (let k = 0; k < delay.length; ++k) { + gainMixer.connect(delay[k]); + } + + // Final merger to bring back the individual channels from the convolver + // and the reference in the right order for the destination. + let finalMerger = new ChannelMergerNode( + context, {numberOfInputs: context.destination.channelCount}); + + // First two channels are for the convolver output, and the next two are + // for the reference. + splitter.connect(finalMerger, 0, 0); + splitter.connect(finalMerger, 1, 1); + delay[0].connect(finalMerger, 0, 2); + delay[1].connect(finalMerger, 0, 3); + + finalMerger.connect(context.destination); + + // Start the sources at last. + for (let k = 0; k < src.length; ++k) { + src[k].start(); + } + + return context.startRendering().then(audioBuffer => { + // Extract the various channels out + let actual0 = audioBuffer.getChannelData(0); + let actual1 = audioBuffer.getChannelData(1); + let expected0 = audioBuffer.getChannelData(2); + let expected1 = audioBuffer.getChannelData(3); + + let threshold = options.absoluteThreshold ? + options.absoluteThreshold : absoluteThreshold; + + // Verify that each output channel of the convolver matches + // the delayed signal from the reference + should(actual0, options.prefix + ': Channel 0') + .beCloseToArray(expected0, {absoluteThreshold: threshold}); + should(actual1, options.prefix + ': Channel 1') + .beCloseToArray(expected1, {absoluteThreshold: threshold}); + }); + } + + audit.run(); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-response-4-chan.html b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-response-4-chan.html new file mode 100644 index 0000000000..f188d87b71 --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-response-4-chan.html @@ -0,0 +1,508 @@ +<!DOCTYPE html> +<html> + <head> + <title> + Test Convolver Channel Outputs for Response with 4 channels + </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> + </head> + <body> + <script id="layout-test-code"> + // Test various convolver configurations when the convolver response has + // a four channels. + + // This is somewhat arbitrary. It is the minimum value for which tests + // pass with both FFmpeg and KISS FFT implementations for 256 points. + // The value was similar for each implementation. + const absoluteThreshold = 3 * Math.pow(2, -22); + + // Fairly arbitrary sample rate, except that we want the rate to be a + // power of two so that 1/sampleRate is exactly representable as a + // single-precision float. + let sampleRate = 8192; + + // A fairly arbitrary number of frames, except the number of frames should + // be more than a few render quanta. + let renderFrames = 10 * 128; + + let audit = Audit.createTaskRunner(); + + // Convolver response + let response; + + audit.define( + { + label: 'initialize', + description: 'Convolver response with one channel' + }, + (task, should) => { + // Convolver response + should( + () => { + response = new AudioBuffer( + {numberOfChannels: 4, length: 8, sampleRate: sampleRate}); + // Each channel of the response is a simple impulse (with + // different delay) so that we can use a DelayNode to simulate + // the convolver output. Channel k is delayed by k+1 frames. + for (let k = 0; k < response.numberOfChannels; ++k) { + response.getChannelData(k)[k + 1] = 1; + } + }, + 'new AudioBuffer({numberOfChannels: 2, length: 4, sampleRate: ' + + sampleRate + '})') + .notThrow(); + + task.done(); + }); + + audit.define( + {label: '1-channel input', description: 'produces 2-channel output'}, + (task, should) => { + fourChannelResponseTest({numberOfInputs: 1, prefix: '1'}, should) + .then(() => task.done()); + }); + + audit.define( + {label: '2-channel input', description: 'produces 2-channel output'}, + (task, should) => { + fourChannelResponseTest({numberOfInputs: 2, prefix: '2'}, should) + .then(() => task.done()); + }); + + audit.define( + { + label: '3-channel input', + description: '3->2 downmix producing 2-channel output' + }, + (task, should) => { + fourChannelResponseTest({numberOfInputs: 3, prefix: '3'}, should) + .then(() => task.done()); + }); + + audit.define( + { + label: '4-channel input', + description: '4->2 downmix producing 2-channel output' + }, + (task, should) => { + fourChannelResponseTest({numberOfInputs: 4, prefix: '4'}, should) + .then(() => task.done()); + }); + + audit.define( + { + label: '5.1-channel input', + description: '5.1->2 downmix producing 2-channel output' + }, + (task, should) => { + // Scale tolerance by maximum amplitude expected in down-mix + // output. + let threshold = (1.0 + Math.sqrt(0.5) * 2) * absoluteThreshold; + + fourChannelResponseTest({numberOfInputs: 6, prefix: '5.1', + absoluteThreshold: threshold}, should) + .then(() => task.done()); + }); + + audit.define( + { + label: 'delayed buffer set', + description: 'Delayed set of 4-channel response' + }, + (task, should) => { + // Don't really care about the output for this test. It's to verify + // we don't crash in a debug build when setting the convolver buffer + // after creating the graph. + let context = new OfflineAudioContext(1, renderFrames, sampleRate); + let src = new OscillatorNode(context); + let convolver = + new ConvolverNode(context, {disableNormalization: true}); + let buffer = new AudioBuffer({ + numberOfChannels: 4, + length: 4, + sampleRate: context.sampleRate + }); + + // Impulse responses for the convolver aren't important, as long as + // it's not all zeroes. + for (let k = 0; k < buffer.numberOfChannels; ++k) { + buffer.getChannelData(k).fill(1); + } + + src.connect(convolver).connect(context.destination); + + // Set the buffer after a few render quanta have passed. The actual + // value must be least one, but is otherwise arbitrary. + context.suspend(512 / context.sampleRate) + .then(() => convolver.buffer = buffer) + .then(() => context.resume()); + + src.start(); + context.startRendering() + .then(audioBuffer => { + // Just make sure output is not silent. + should( + audioBuffer.getChannelData(0), + 'Output with delayed setting of convolver buffer') + .notBeConstantValueOf(0); + }) + .then(() => task.done()); + }); + + audit.define( + { + label: 'count 1, 2-channel in', + description: '2->1 downmix because channel count is 1' + }, + (task, should) => { + channelCount1ExplicitTest( + {numberOfInputs: 1, prefix: 'Convolver count 1, stereo in'}, + should) + .then(() => task.done()); + }); + + audit.define( + { + label: 'count 1, 4-channel in', + description: '4->1 downmix because channel count is 1' + }, + (task, should) => { + channelCount1ExplicitTest( + {numberOfInputs: 4, prefix: 'Convolver count 1, 4-channel in'}, + should) + .then(() => task.done()); + }); + + audit.define( + { + label: 'count 1, 5.1-channel in', + description: '5.1->1 downmix because channel count is 1' + }, + (task, should) => { + channelCount1ExplicitTest( + { + numberOfInputs: 6, + prefix: 'Convolver count 1, 5.1 channel in' + }, + should) + .then(() => task.done()); + }); + + audit.run(); + + function fourChannelResponseTest(options, should) { + // Create an 4-channel offline context. The first two channels are for + // the stereo output of the convolver and the next two channels are for + // the reference stereo signal. + let context = new OfflineAudioContext(4, renderFrames, sampleRate); + context.destination.channelInterpretation = 'discrete'; + + // Create oscillators for use as the input. The type and frequency is + // arbitrary except that oscillators must be different. + let src = new Array(options.numberOfInputs); + for (let k = 0; k < src.length; ++k) { + src[k] = new OscillatorNode( + context, {type: 'square', frequency: 440 + 220 * k}); + } + + // Merger to combine the oscillators into one output stream. + let srcMerger = + new ChannelMergerNode(context, {numberOfInputs: src.length}); + + for (let k = 0; k < src.length; ++k) { + src[k].connect(srcMerger, 0, k); + } + + // Convolver under test. + let conv = new ConvolverNode( + context, {disableNormalization: true, buffer: response}); + srcMerger.connect(conv); + + // Splitter to get individual channels of the convolver output so we can + // feed them (eventually) to the context in the right set of channels. + let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2}); + conv.connect(splitter); + + // Reference graph consists of a delays node to simulate the response of + // the convolver. (The convolver response is designed this way.) + let delay = new Array(4); + for (let k = 0; k < delay.length; ++k) { + delay[k] = new DelayNode(context, { + delayTime: (k + 1) / context.sampleRate, + channelCount: 1, + channelCountMode: 'explicit' + }); + } + + // Gain node to mix the sources to stereo in the desired way. (Could be + // done in the delay node, but let's keep the mixing separated from the + // functionality.) + let gainMixer = new GainNode( + context, {channelCount: 2, channelCountMode: 'explicit'}); + srcMerger.connect(gainMixer); + + // Splitter to extract the channels of the reference signal. + let refSplitter = + new ChannelSplitterNode(context, {numberOfOutputs: 2}); + gainMixer.connect(refSplitter); + + // Connect the left channel to the first two nodes and the right channel + // to the second two as required for "true" stereo matrix response. + for (let k = 0; k < 2; ++k) { + refSplitter.connect(delay[k], 0, 0); + refSplitter.connect(delay[k + 2], 1, 0); + } + + // Gain nodes to sum the responses to stereo + let gain = new Array(2); + for (let k = 0; k < gain.length; ++k) { + gain[k] = new GainNode(context, { + channelCount: 1, + channelCountMode: 'explicit', + channelInterpretation: 'discrete' + }); + } + + delay[0].connect(gain[0]); + delay[2].connect(gain[0]); + delay[1].connect(gain[1]); + delay[3].connect(gain[1]); + + // Final merger to bring back the individual channels from the convolver + // and the reference in the right order for the destination. + let finalMerger = new ChannelMergerNode( + context, {numberOfInputs: context.destination.channelCount}); + + // First two channels are for the convolver output, and the next two are + // for the reference. + splitter.connect(finalMerger, 0, 0); + splitter.connect(finalMerger, 1, 1); + gain[0].connect(finalMerger, 0, 2); + gain[1].connect(finalMerger, 0, 3); + + finalMerger.connect(context.destination); + + // Start the sources at last. + for (let k = 0; k < src.length; ++k) { + src[k].start(); + } + + return context.startRendering().then(audioBuffer => { + // Extract the various channels out + let actual0 = audioBuffer.getChannelData(0); + let actual1 = audioBuffer.getChannelData(1); + let expected0 = audioBuffer.getChannelData(2); + let expected1 = audioBuffer.getChannelData(3); + + let threshold = options.absoluteThreshold ? + options.absoluteThreshold : absoluteThreshold; + + // Verify that each output channel of the convolver matches + // the delayed signal from the reference + should(actual0, options.prefix + ': Channel 0') + .beCloseToArray(expected0, {absoluteThreshold: threshold}); + should(actual1, options.prefix + ': Channel 1') + .beCloseToArray(expected1, {absoluteThreshold: threshold}); + }); + } + + function fourChannelResponseExplicitTest(options, should) { + // Create an 4-channel offline context. The first two channels are for + // the stereo output of the convolver and the next two channels are for + // the reference stereo signal. + let context = new OfflineAudioContext(4, renderFrames, sampleRate); + context.destination.channelInterpretation = 'discrete'; + + // Create oscillators for use as the input. The type and frequency is + // arbitrary except that oscillators must be different. + let src = new Array(options.numberOfInputs); + for (let k = 0; k < src.length; ++k) { + src[k] = new OscillatorNode( + context, {type: 'square', frequency: 440 + 220 * k}); + } + + // Merger to combine the oscillators into one output stream. + let srcMerger = + new ChannelMergerNode(context, {numberOfInputs: src.length}); + + for (let k = 0; k < src.length; ++k) { + src[k].connect(srcMerger, 0, k); + } + + // Convolver under test. + let conv = new ConvolverNode( + context, {disableNormalization: true, buffer: response}); + srcMerger.connect(conv); + + // Splitter to get individual channels of the convolver output so we can + // feed them (eventually) to the context in the right set of channels. + let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2}); + conv.connect(splitter); + + // Reference graph consists of a delays node to simulate the response of + // the convolver. (The convolver response is designed this way.) + let delay = new Array(4); + for (let k = 0; k < delay.length; ++k) { + delay[k] = new DelayNode(context, { + delayTime: (k + 1) / context.sampleRate, + channelCount: 1, + channelCountMode: 'explicit' + }); + } + + // Gain node to mix the sources to stereo in the desired way. (Could be + // done in the delay node, but let's keep the mixing separated from the + // functionality.) + let gainMixer = new GainNode( + context, {channelCount: 2, channelCountMode: 'explicit'}); + srcMerger.connect(gainMixer); + + // Splitter to extract the channels of the reference signal. + let refSplitter = + new ChannelSplitterNode(context, {numberOfOutputs: 2}); + gainMixer.connect(refSplitter); + + // Connect the left channel to the first two nodes and the right channel + // to the second two as required for "true" stereo matrix response. + for (let k = 0; k < 2; ++k) { + refSplitter.connect(delay[k], 0, 0); + refSplitter.connect(delay[k + 2], 1, 0); + } + + // Gain nodes to sum the responses to stereo + let gain = new Array(2); + for (let k = 0; k < gain.length; ++k) { + gain[k] = new GainNode(context, { + channelCount: 1, + channelCountMode: 'explicit', + channelInterpretation: 'discrete' + }); + } + + delay[0].connect(gain[0]); + delay[2].connect(gain[0]); + delay[1].connect(gain[1]); + delay[3].connect(gain[1]); + + // Final merger to bring back the individual channels from the convolver + // and the reference in the right order for the destination. + let finalMerger = new ChannelMergerNode( + context, {numberOfInputs: context.destination.channelCount}); + + // First two channels are for the convolver output, and the next two are + // for the reference. + splitter.connect(finalMerger, 0, 0); + splitter.connect(finalMerger, 1, 1); + gain[0].connect(finalMerger, 0, 2); + gain[1].connect(finalMerger, 0, 3); + + finalMerger.connect(context.destination); + + // Start the sources at last. + for (let k = 0; k < src.length; ++k) { + src[k].start(); + } + + return context.startRendering().then(audioBuffer => { + // Extract the various channels out + let actual0 = audioBuffer.getChannelData(0); + let actual1 = audioBuffer.getChannelData(1); + let expected0 = audioBuffer.getChannelData(2); + let expected1 = audioBuffer.getChannelData(3); + + // Verify that each output channel of the convolver matches + // the delayed signal from the reference + should(actual0, options.prefix + ': Channel 0') + .beEqualToArray(expected0); + should(actual1, options.prefix + ': Channel 1') + .beEqualToArray(expected1); + }); + } + + function channelCount1ExplicitTest(options, should) { + // Create an 4-channel offline context. The first two channels are + // for the stereo output of the convolver and the next two channels + // are for the reference stereo signal. + let context = new OfflineAudioContext(4, renderFrames, sampleRate); + context.destination.channelInterpretation = 'discrete'; + // Final merger to bring back the individual channels from the + // convolver and the reference in the right order for the + // destination. + let finalMerger = new ChannelMergerNode( + context, {numberOfInputs: context.destination.channelCount}); + finalMerger.connect(context.destination); + + // Create source using oscillators + let src = new Array(options.numberOfInputs); + for (let k = 0; k < src.length; ++k) { + src[k] = new OscillatorNode( + context, {type: 'square', frequency: 440 + 220 * k}); + } + + // Merger to combine the oscillators into one output stream. + let srcMerger = + new ChannelMergerNode(context, {numberOfInputs: src.length}); + for (let k = 0; k < src.length; ++k) { + src[k].connect(srcMerger, 0, k); + } + + // Convolver under test + let conv = new ConvolverNode(context, { + channelCount: 1, + channelCountMode: 'explicit', + disableNormalization: true, + buffer: response + }); + srcMerger.connect(conv); + + // Splitter to extract the channels of the test signal. + let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2}); + conv.connect(splitter); + + // Reference convolver, with a gain node to do the desired mixing. The + // gain node should do the same thing that the convolver under test + // should do. + let gain = new GainNode( + context, {channelCount: 1, channelCountMode: 'explicit'}); + let convRef = new ConvolverNode( + context, {disableNormalization: true, buffer: response}); + + srcMerger.connect(gain).connect(convRef); + + // Splitter to extract the channels of the reference signal. + let refSplitter = + new ChannelSplitterNode(context, {numberOfOutputs: 2}); + + convRef.connect(refSplitter); + + // Merge all the channels into one + splitter.connect(finalMerger, 0, 0); + splitter.connect(finalMerger, 1, 1); + refSplitter.connect(finalMerger, 0, 2); + refSplitter.connect(finalMerger, 1, 3); + + // Start sources and render! + for (let k = 0; k < src.length; ++k) { + src[k].start(); + } + + return context.startRendering().then(buffer => { + // The output from the test convolver should be identical to + // the reference result. + let testOut0 = buffer.getChannelData(0); + let testOut1 = buffer.getChannelData(1); + let refOut0 = buffer.getChannelData(2); + let refOut1 = buffer.getChannelData(3); + + should(testOut0, `${options.prefix}: output 0`) + .beEqualToArray(refOut0); + should(testOut1, `${options.prefix}: output 1`) + .beEqualToArray(refOut1); + }) + } + </script> + </body> +</html> diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-setBuffer-already-has-value.html b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-setBuffer-already-has-value.html new file mode 100644 index 0000000000..ce2d5fcfe9 --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-setBuffer-already-has-value.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<html> + <head> + <title> + convolver-setBuffer-already-has-value.html + </title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/webaudio/resources/audit.js"></script> + </head> + <body> + <script id="layout-test-code"> + let audit = Audit.createTaskRunner(); + + audit.define('test', (task, should) => { + let context = new AudioContext(); + let audioBuffer = new AudioBuffer( + {numberOfChannels: 1, length: 1, sampleRate: context.sampleRate}); + let convolver = context.createConvolver(); + should(() => { + convolver.buffer = null; + }, 'Set buffer to null before set non-null').notThrow(); + + should(() => { + convolver.buffer = audioBuffer; + }, 'Set buffer first normally').notThrow(); + + should(() => { + convolver.buffer = audioBuffer; + }, 'Set buffer a second time').notThrow(); + + should(() => { + convolver.buffer = null; + }, 'Set buffer to null').notThrow(); + + should(() => { + convolver.buffer = null; + }, 'Set buffer to null again, to make sure').notThrow(); + + should(() => { + convolver.buffer = audioBuffer; + }, 'Set buffer to non-null to verify it is set') + .notThrow(); + + task.done(); + }); + + audit.run(); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-setBuffer-null.html b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-setBuffer-null.html new file mode 100644 index 0000000000..d35b8ec54b --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-setBuffer-null.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> + <head> + <title> + convolver-setBuffer-null.html + </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> + </head> + <body> + <script id="layout-test-code"> + let audit = Audit.createTaskRunner(); + + audit.define('test', function(task, should) { + let context = new AudioContext(); + let conv = context.createConvolver(); + + should(() => { + conv.buffer = null; + }, 'Setting ConvolverNode impulse response buffer to null').notThrow(); + should(conv.buffer === null, 'conv.buffer === null').beTrue(); + + task.done(); + }); + + audit.run(); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-upmixing-1-channel-response.html b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-upmixing-1-channel-response.html new file mode 100644 index 0000000000..b0b3a5965e --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-upmixing-1-channel-response.html @@ -0,0 +1,143 @@ +<!DOCTYPE html> +<title>Test that up-mixing signals in ConvolverNode processing is linear</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +const EPSILON = 3.0 * Math.pow(2, -22); +// sampleRate is a power of two so that delay times are exact in base-2 +// floating point arithmetic. +const SAMPLE_RATE = 32768; +// Length of stereo convolver input in frames (arbitrary): +const STEREO_FRAMES = 256; +// Length of mono signal in frames. This is more than two blocks to ensure +// that at least one block will be mono, even if interpolation in the +// DelayNode means that stereo is output one block earlier and later than +// if frames are delayed without interpolation. +const MONO_FRAMES = 384; +// Length of response buffer: +const RESPONSE_FRAMES = 256; + +function test_linear_upmixing(channelInterpretation, initial_mono_frames) +{ + let stereo_input_end = initial_mono_frames + STEREO_FRAMES; + // Total length: + let length = stereo_input_end + RESPONSE_FRAMES + MONO_FRAMES + STEREO_FRAMES; + // The first two channels contain signal where some up-mixing occurs + // internally to a ConvolverNode when a stereo signal is added and removed. + // The last two channels are expected to contain the same signal, but mono + // and stereo signals are convolved independently before up-mixing the mono + // output to mix with the stereo output. + let context = new OfflineAudioContext({numberOfChannels: 4, + length: length, + sampleRate: SAMPLE_RATE}); + + let response = new AudioBuffer({numberOfChannels: 1, + length: RESPONSE_FRAMES, + sampleRate: context.sampleRate}); + + // Two stereo channel splitters will collect test and reference outputs. + let destinationMerger = new ChannelMergerNode(context, {numberOfInputs: 4}); + destinationMerger.connect(context.destination); + let testSplitter = + new ChannelSplitterNode(context, {numberOfOutputs: 2}); + let referenceSplitter = + new ChannelSplitterNode(context, {numberOfOutputs: 2}); + testSplitter.connect(destinationMerger, 0, 0); + testSplitter.connect(destinationMerger, 1, 1); + referenceSplitter.connect(destinationMerger, 0, 2); + referenceSplitter.connect(destinationMerger, 1, 3); + + // A GainNode mixes reference stereo and mono signals because up-mixing + // cannot be performed at a channel splitter. + let referenceGain = new GainNode(context); + referenceGain.connect(referenceSplitter); + referenceGain.channelInterpretation = channelInterpretation; + + // The impulse response for convolution contains two impulses so as to test + // effects in at least two processing blocks. + response.getChannelData(0)[0] = 0.5; + response.getChannelData(0)[response.length - 1] = 0.5; + + let testConvolver = new ConvolverNode(context, {disableNormalization: true, + buffer: response}); + testConvolver.channelInterpretation = channelInterpretation; + let referenceMonoConvolver = new ConvolverNode(context, + {disableNormalization: true, + buffer: response}); + let referenceStereoConvolver = new ConvolverNode(context, + {disableNormalization: true, + buffer: response}); + // No need to set referenceStereoConvolver.channelInterpretation because + // input is either silent or stereo. + testConvolver.connect(testSplitter); + // Mix reference convolver output. + referenceMonoConvolver.connect(referenceGain); + referenceStereoConvolver.connect(referenceGain); + + // The DelayNode initially has a single channel of silence, which is used to + // switch the stereo signal in and out. The output of the delay node is + // first mono silence (if there is a non-zero initial_mono_frames), then + // stereo, then mono silence, and finally stereo again. maxDelayTime is + // used to generate the middle mono silence period from the initial silence + // in the DelayNode and then generate the final period of stereo from its + // initial input. + let maxDelayTime = (length - STEREO_FRAMES) / context.sampleRate; + let delay = + new DelayNode(context, + {maxDelayTime: maxDelayTime, + delayTime: initial_mono_frames / context.sampleRate}); + // Schedule an increase in the delay to return to mono silence. + delay.delayTime.setValueAtTime(maxDelayTime, + stereo_input_end / context.sampleRate); + delay.connect(testConvolver); + delay.connect(referenceStereoConvolver); + + let stereoMerger = new ChannelMergerNode(context, {numberOfInputs: 2}); + stereoMerger.connect(delay); + + // Three independent signals + let monoSignal = new OscillatorNode(context, {frequency: 440}); + let leftSignal = new OscillatorNode(context, {frequency: 450}); + let rightSignal = new OscillatorNode(context, {frequency: 460}); + monoSignal.connect(testConvolver); + monoSignal.connect(referenceMonoConvolver); + leftSignal.connect(stereoMerger, 0, 0); + rightSignal.connect(stereoMerger, 0, 1); + monoSignal.start(); + leftSignal.start(); + rightSignal.start(); + + return context.startRendering(). + then((buffer) => { + let maxDiff = -1.0; + let frameIndex = 0; + let channelIndex = 0; + for (let c = 0; c < 2; ++c) { + let testOutput = buffer.getChannelData(0 + c); + let referenceOutput = buffer.getChannelData(2 + c); + for (var i = 0; i < buffer.length; ++i) { + var diff = Math.abs(testOutput[i] - referenceOutput[i]); + if (diff > maxDiff) { + maxDiff = diff; + frameIndex = i; + channelIndex = c; + } + } + } + assert_approx_equals(buffer.getChannelData(0 + channelIndex)[frameIndex], + buffer.getChannelData(2 + channelIndex)[frameIndex], + EPSILON, + `output at ${frameIndex} ` + + `in channel ${channelIndex}` ); + }); +} + +promise_test(() => test_linear_upmixing("speakers", MONO_FRAMES), + "speakers, initially mono"); +promise_test(() => test_linear_upmixing("discrete", MONO_FRAMES), + "discrete"); +// Gecko uses a separate path for "speakers" up-mixing when the convolver's +// first input is stereo, so test that separately. +promise_test(() => test_linear_upmixing("speakers", 0), + "speakers, initially stereo"); +</script> diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/ctor-convolver.html b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/ctor-convolver.html new file mode 100644 index 0000000000..28a0fc1c3c --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/ctor-convolver.html @@ -0,0 +1,186 @@ +<!DOCTYPE html> +<html> + <head> + <title> + Test Constructor: Convolver + </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/audionodeoptions.js"></script> + </head> + <body> + <script id="layout-test-code"> + let context; + + let audit = Audit.createTaskRunner(); + + audit.define('initialize', (task, should) => { + context = initializeContext(should); + task.done(); + }); + + audit.define('invalid constructor', (task, should) => { + testInvalidConstructor(should, 'ConvolverNode', context); + task.done(); + }); + + audit.define('default constructor', (task, should) => { + let prefix = 'node0'; + let node = testDefaultConstructor(should, 'ConvolverNode', context, { + prefix: prefix, + numberOfInputs: 1, + numberOfOutputs: 1, + channelCount: 2, + channelCountMode: 'clamped-max', + channelInterpretation: 'speakers' + }); + + testDefaultAttributes( + should, node, prefix, + [{name: 'normalize', value: true}, {name: 'buffer', value: null}]); + + task.done(); + }); + + audit.define('test AudioNodeOptions', (task, should) => { + // Can't use testAudioNodeOptions because the constraints for this node + // are not supported there. + let node; + + // An array of tests. + [{ + // Test that we can set the channel count to 1 or 2 and that other + // channel counts throw an error. + attribute: 'channelCount', + tests: [ + {value: 1}, {value: 2}, {value: 0, error: 'NotSupportedError'}, + {value: 3, error: 'NotSupportedError'}, + {value: 99, error: 'NotSupportedError'} + ] + }, + { + // Test channelCountMode. A mode of "max" is illegal, but others are + // ok. But also throw an error of unknown values. + attribute: 'channelCountMode', + tests: [ + {value: 'clamped-max'}, {value: 'explicit'}, + {value: 'max', error: 'NotSupportedError'}, + {value: 'foobar', error: TypeError} + ] + }, + { + // Test channelInterpretation can be set for valid values and an + // error is thrown for others. + attribute: 'channelInterpretation', + tests: [ + {value: 'speakers'}, {value: 'discrete'}, + {value: 'foobar', error: TypeError} + ] + }].forEach(entry => { + entry.tests.forEach(testItem => { + let options = {}; + options[entry.attribute] = testItem.value; + + const testFunction = () => { + node = new ConvolverNode(context, options); + }; + const testDescription = + `new ConvolverNode(c, ${JSON.stringify(options)})`; + + if (testItem.error) { + testItem.error === TypeError + ? should(testFunction, testDescription).throw(TypeError) + : should(testFunction, testDescription) + .throw(DOMException, 'NotSupportedError'); + } else { + should(testFunction, testDescription).notThrow(); + should(node[entry.attribute], `node.${entry.attribute}`) + .beEqualTo(options[entry.attribute]); + } + }); + }); + + task.done(); + }); + + audit.define('nullable buffer', (task, should) => { + let node; + let options = {buffer: null}; + + should( + () => { + node = new ConvolverNode(context, options); + }, + 'node1 = new ConvolverNode(c, ' + JSON.stringify(options)) + .notThrow(); + + should(node.buffer, 'node1.buffer').beEqualTo(null); + + task.done(); + }); + audit.define('illegal sample-rate', (task, should) => { + let node; + let options = {buffer: context.createBuffer(1, 1, context.sampleRate / 2)}; + + should( + () => { + node = new ConvolverNode(context, options); + }, + 'node1 = new ConvolverNode(c, ' + JSON.stringify(options)) + .throw(DOMException, 'NotSupportedError'); + + task.done(); + }); + + audit.define('construct with options', (task, should) => { + let buf = context.createBuffer(1, 1, context.sampleRate); + let options = {buffer: buf, disableNormalization: false}; + + let message = + 'node = new ConvolverNode(c, ' + JSON.stringify(options) + ')'; + + let node; + should(() => { + node = new ConvolverNode(context, options); + }, message).notThrow(); + + should(node instanceof ConvolverNode, 'node1 instanceOf ConvolverNode') + .beEqualTo(true); + should(node.buffer === options.buffer, 'node1.buffer === <buf>') + .beEqualTo(true); + should(node.normalize, 'node1.normalize') + .beEqualTo(!options.disableNormalization); + + options.buffer = null; + options.disableNormalization = true; + + message = + 'node2 = new ConvolverNode(, ' + JSON.stringify(options) + ')'; + + should(() => { + node = new ConvolverNode(context, options); + }, message).notThrow(); + should(node.buffer, 'node2.buffer').beEqualTo(null); + should(node.normalize, 'node2.normalize') + .beEqualTo(!options.disableNormalization); + + options.disableNormalization = false; + message = 'node3 = new ConvolverNode(context, ' + + JSON.stringify(options) + ')'; + + should(() => { + node = new ConvolverNode(context, options); + }, message).notThrow(); + should(node.buffer, 'node3.buffer').beEqualTo(null); + should(node.normalize, 'node3.normalize') + .beEqualTo(!options.disableNormalization); + + task.done(); + }); + + audit.run(); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/realtime-conv.html b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/realtime-conv.html new file mode 100644 index 0000000000..8668e9d5ac --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/realtime-conv.html @@ -0,0 +1,149 @@ +<!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 = 77.03; + + // 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> diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/transferred-buffer-output.html b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/transferred-buffer-output.html new file mode 100644 index 0000000000..e37a98c386 --- /dev/null +++ b/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/transferred-buffer-output.html @@ -0,0 +1,107 @@ +<!doctype html> +<html> + <head> + <title> + Test Convolver Output with Transferred Buffer + </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> + </head> + + <body> + <script> + // Arbitrary sample rate. + const sampleRate = 16000; + + // Number of frames to render. Just need to have at least 2 render + // quanta. + const lengthInFrames = 10 * RENDER_QUANTUM_FRAMES; + + let audit = Audit.createTaskRunner(); + + // Buffer to use for the impulse response of a ConvolverNode. + let impulseBuffer; + + // This sets up a worker to receive one channel of an AudioBuffer. + function setUpWorkerForTest() { + impulseBuffer = new AudioBuffer({ + numberOfChannels: 2, + length: 2 * RENDER_QUANTUM_FRAMES, + sampleRate: sampleRate + }); + + // Just fill the buffer with a constant value; the contents shouldn't + // matter for this test since we're transferring one of the channels. + impulseBuffer.getChannelData(0).fill(1); + impulseBuffer.getChannelData(1).fill(2); + + // We're going to transfer channel 0 to the worker, making it + // unavailable for the convolver + let data = impulseBuffer.getChannelData(0).buffer; + + let string = [ + 'onmessage = function(e) {', ' postMessage(\'done\');', '};' + ].join('\n'); + + let blobURL = URL.createObjectURL(new Blob([string])); + let worker = new Worker(blobURL); + worker.onmessage = workerReply; + worker.postMessage(data, [data]); + } + + function workerReply() { + // Worker has received the message. Run the test. + audit.run(); + } + + audit.define( + { + label: 'Test Convolver with transferred buffer', + description: 'Output should be all zeroes' + }, + async (task, should) => { + // Two channels so we can capture the output of the convolver with a + // stereo convolver. + let context = new OfflineAudioContext({ + numberOfChannels: 2, + length: lengthInFrames, + sampleRate: sampleRate + }); + + // Use a simple constant source so we easily check that the + // convolver output is correct. + let source = new ConstantSourceNode(context); + + // Create the convolver with the desired impulse response and + // disable normalization so we can easily check the output. + let conv = new ConvolverNode( + context, {disableNormalization: true, buffer: impulseBuffer}); + + source.connect(conv).connect(context.destination); + + source.start(); + + let renderedBuffer = await context.startRendering(); + + // Get the actual data + let c0 = renderedBuffer.getChannelData(0); + let c1 = renderedBuffer.getChannelData(1); + + // Since one channel was transferred, we must behave as if all were + // transferred. Hence, the output should be all zeroes for both + // channels. + should(c0, `Convolver channel 0 output[0:${c0.length - 1}]`) + .beConstantValueOf(0); + + should(c1, `Convolver channel 1 output[0:${c1.length - 1}]`) + .beConstantValueOf(0); + + task.done(); + }); + + setUpWorkerForTest(); + </script> + </body> +</html> |