summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface
parentInitial commit. (diff)
downloadthunderbird-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')
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/active-processing.https.html93
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolution-mono-mono.html62
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-cascade.html61
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-channels.html43
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-response-1-chan.html406
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-response-2-chan.html373
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-response-4-chan.html508
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-setBuffer-already-has-value.html51
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-setBuffer-null.html31
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-upmixing-1-channel-response.html143
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/ctor-convolver.html186
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/realtime-conv.html149
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/transferred-buffer-output.html107
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>