summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-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-pannernode-interface
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.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-pannernode-interface')
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/automation-changes.html140
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/ctor-panner.html468
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/distance-exponential.html34
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/distance-inverse.html28
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/distance-linear.html30
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-automation-basic.html298
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-automation-equalpower-stereo.html47
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-automation-position.html265
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-azimuth.html51
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-distance-clamping.html227
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-equalpower-stereo.html44
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-equalpower.html139
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-rolloff-clamping.html98
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/pannernode-basic.window.js71
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/pannernode-setposition-throws.html37
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/test-pannernode-automation.html36
16 files changed, 2013 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/automation-changes.html b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/automation-changes.html
new file mode 100644
index 0000000000..8aa73552aa
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/automation-changes.html
@@ -0,0 +1,140 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Panner Node Automation</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../resources/audit-util.js"></script>
+ <script src="../../resources/audit.js"></script>
+ </head>
+
+ <body>
+ <script>
+ // Use a power-of-two to eliminate some round-off; otherwise, this isn't
+ // really important.
+ const sampleRate = 16384;
+
+ // Render enough for the test; we don't need a lot.
+ const renderFrames = 2048;
+
+ // Initial panner positionX and final positionX for listener.
+ const positionX = 2000;
+
+ const audit = Audit.createTaskRunner();
+
+ // Test that listener.positionX.value setter does the right thing.
+ audit.define('Set Listener.positionX.value', (task, should) => {
+ const context = new OfflineAudioContext(2, renderFrames, sampleRate);
+
+ createGraph(context);
+
+ // Frame at which the listener instantaneously moves to a new location.
+ const moveFrame = 512;
+
+ context.suspend(moveFrame / context.sampleRate)
+ .then(() => {
+ context.listener.positionX.value = positionX;
+ })
+ .then(() => context.resume());
+
+ verifyOutput(context, moveFrame, should, 'listenr.positionX.value')
+ .then(() => task.done());
+ });
+
+ // Test that listener.positionX.setValueAtTime() does the right thing.
+ audit.define('Listener.positionX.setValue', (task, should) => {
+ const context = new OfflineAudioContext(2, renderFrames, sampleRate);
+
+ createGraph(context);
+
+ // Frame at which the listener instantaneously moves to a new location.
+ const moveFrame = 512;
+
+ context.listener.positionX.setValueAtTime(
+ positionX, moveFrame / context.sampleRate);
+
+ verifyOutput(
+ context, moveFrame, should, 'listener.positionX.setValueATTime')
+ .then(() => task.done());
+ });
+
+ // Test that listener.setPosition() does the right thing.
+ audit.define('Listener.setPosition', (task, should) => {
+ const context = new OfflineAudioContext(2, renderFrames, sampleRate);
+
+ createGraph(context);
+
+ // Frame at which the listener instantaneously moves to a new location.
+ const moveFrame = 512;
+
+ context.suspend(moveFrame / context.sampleRate)
+ .then(() => {
+ context.listener.setPosition(positionX, 0, 0);
+ })
+ .then(() => context.resume());
+
+ verifyOutput(context, moveFrame, should, 'listener.setPostion')
+ .then(() => task.done());
+ });
+
+ audit.run();
+
+
+ // Create the basic graph for testing which consists of an oscillator node
+ // connected to a panner node.
+ function createGraph(context) {
+ const listener = context.listener;
+
+ listener.positionX.value = 0;
+ listener.positionY.value = 0;
+ listener.positionZ.value = 0;
+
+ const src = new OscillatorNode(context);
+
+ const panner = new PannerNode(context, {
+ distanceModel: 'linear',
+ refDistance: 1,
+ maxDistance: 3000,
+ positionX: positionX,
+ positionY: 0,
+ positionZ: 0
+ });
+ src.connect(panner).connect(context.destination);
+
+ src.start();
+ }
+
+
+ // Verify the output from the panner is correct.
+ function verifyOutput(context, moveFrame, should, prefix) {
+ return context.startRendering().then(resultBuffer => {
+ // Get the outputs (left and right)
+ const c0 = resultBuffer.getChannelData(0);
+ const c1 = resultBuffer.getChannelData(1);
+
+ // The src/listener set up is such that audio should only come
+ // from the right for until |moveFrame|. Hence the left channel
+ // should be 0 (or very nearly 0).
+ const zero = new Float32Array(moveFrame);
+
+ should(
+ c0.slice(0, moveFrame), `${prefix}: output0[0:${moveFrame - 1}]`)
+ .beCloseToArray(zero, {absoluteThreshold: 1e-16});
+ should(
+ c1.slice(0, moveFrame), `${prefix}: output1[0:${moveFrame - 1}]`)
+ .notBeConstantValueOf(0);
+
+ // At |moveFrame| and beyond, the listener and source are at the
+ // same position, so the outputs from the left and right should be
+ // identical, and the left channel should not be 0 anymore.
+
+ should(c0.slice(moveFrame), `${prefix}: output0[${moveFrame}:]`)
+ .notBeConstantValueOf(0);
+ should(c1.slice(moveFrame), `${prefix}: output1[${moveFrame}:]`)
+ .beCloseToArray(c0.slice(moveFrame));
+ });
+ }
+ </script>
+ </body>
+</html>
+
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/ctor-panner.html b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/ctor-panner.html
new file mode 100644
index 0000000000..c434aa8c6a
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/ctor-panner.html
@@ -0,0 +1,468 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ Test Constructor: Panner
+ </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, 'PannerNode', context);
+ task.done();
+ });
+
+ audit.define('default constructor', (task, should) => {
+ let prefix = 'node0';
+ let node = testDefaultConstructor(should, 'PannerNode', context, {
+ prefix: prefix,
+ numberOfInputs: 1,
+ numberOfOutputs: 1,
+ channelCount: 2,
+ channelCountMode: 'clamped-max',
+ channelInterpretation: 'speakers'
+ });
+
+ testDefaultAttributes(should, node, prefix, [
+ {name: 'panningModel', value: 'equalpower'},
+ {name: 'positionX', value: 0}, {name: 'positionY', value: 0},
+ {name: 'positionZ', value: 0}, {name: 'orientationX', value: 1},
+ {name: 'orientationY', value: 0}, {name: 'orientationZ', value: 0},
+ {name: 'distanceModel', value: 'inverse'},
+ {name: 'refDistance', value: 1}, {name: 'maxDistance', value: 10000},
+ {name: 'rolloffFactor', value: 1},
+ {name: 'coneInnerAngle', value: 360},
+ {name: 'coneOuterAngle', value: 360},
+ {name: 'coneOuterGain', value: 0}
+ ]);
+
+ // Test the listener too, while we're at it.
+ let listenerAttributes = [
+ {name: 'positionX', value: 0},
+ {name: 'positionY', value: 0},
+ {name: 'positionZ', value: 0},
+ {name: 'forwardX', value: 0},
+ {name: 'forwardY', value: 0},
+ {name: 'forwardZ', value: -1},
+ {name: 'upX', value: 0},
+ {name: 'upY', value: 1},
+ {name: 'upZ', value: 0},
+ ];
+
+ listenerAttributes.forEach((item) => {
+ should(
+ context.listener[item.name].value,
+ 'context.listener.' + item.name + '.value')
+ .beEqualTo(item.value);
+ });
+
+ task.done();
+ });
+
+ audit.define('test AudioNodeOptions', (task, should) => {
+ // Can't use testAudioNodeOptions because the constraints for this node
+ // are not supported there.
+ let node;
+ let success = true;
+
+ // Test that we can set the channel count to 1 or 2.
+ let options = {channelCount: 1};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'node1 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .notThrow();
+ should(node.channelCount, 'node1.channelCount')
+ .beEqualTo(options.channelCount);
+
+ options = {channelCount: 2};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'node2 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .notThrow();
+ should(node.channelCount, 'node2.channelCount')
+ .beEqualTo(options.channelCount);
+
+ // Test that other channel counts throw an error
+ options = {channelCount: 0};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .throw(DOMException, 'NotSupportedError');
+ should(
+ () => {
+ node = new PannerNode(context);
+ node.channelCount = options.channelCount;
+ },
+ `node.channelCount = ${options.channelCount}`)
+ .throw(DOMException, "NotSupportedError");
+ should(node.channelCount,
+ `node.channelCount after setting to ${options.channelCount}`)
+ .beEqualTo(2);
+
+ options = {channelCount: 3};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .throw(DOMException, 'NotSupportedError');
+ should(
+ () => {
+ node = new PannerNode(context);
+ node.channelCount = options.channelCount;
+ },
+ `node.channelCount = ${options.channelCount}`)
+ .throw(DOMException, "NotSupportedError");
+ should(node.channelCount,
+ `node.channelCount after setting to ${options.channelCount}`)
+ .beEqualTo(2);
+
+ options = {channelCount: 99};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .throw(DOMException, 'NotSupportedError');
+ should(
+ () => {
+ node = new PannerNode(context);
+ node.channelCount = options.channelCount;
+ },
+ `node.channelCount = ${options.channelCount}`)
+ .throw(DOMException, "NotSupportedError");
+ should(node.channelCount,
+ `node.channelCount after setting to ${options.channelCount}`)
+ .beEqualTo(2);
+
+ // Test channelCountMode. A mode of "max" is illegal, but others are
+ // ok.
+ options = {channelCountMode: 'clamped-max'};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'node3 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .notThrow();
+ should(node.channelCountMode, 'node3.channelCountMode')
+ .beEqualTo(options.channelCountMode);
+
+ options = {channelCountMode: 'explicit'};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'node4 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .notThrow();
+ should(node.channelCountMode, 'node4.channelCountMode')
+ .beEqualTo(options.channelCountMode);
+
+ options = {channelCountMode: 'max'};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .throw(DOMException, 'NotSupportedError');
+ should(
+ () => {
+ node = new PannerNode(context);
+ node.channelCountMode = options.channelCountMode;
+ },
+ `node.channelCountMode = ${options.channelCountMode}`)
+ .throw(DOMException, "NotSupportedError");
+ should(node.channelCountMode,
+ `node.channelCountMode after setting to ${options.channelCountMode}`)
+ .beEqualTo("clamped-max");
+
+ options = {channelCountMode: 'foobar'};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'new PannerNode(c, " + JSON.stringify(options) + ")')
+ .throw(TypeError);
+ should(
+ () => {
+ node = new PannerNode(context);
+ node.channelCountMode = options.channelCountMode;
+ },
+ `node.channelCountMode = ${options.channelCountMode}`)
+ .notThrow(); // Invalid assignment to enum-valued attrs does not throw.
+ should(node.channelCountMode,
+ `node.channelCountMode after setting to ${options.channelCountMode}`)
+ .beEqualTo("clamped-max");
+
+ // Test channelInterpretation.
+ options = {channelInterpretation: 'speakers'};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'node5 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .notThrow();
+ should(node.channelInterpretation, 'node5.channelInterpretation')
+ .beEqualTo(options.channelInterpretation);
+
+ options = {channelInterpretation: 'discrete'};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'node6 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .notThrow();
+ should(node.channelInterpretation, 'node6.channelInterpretation')
+ .beEqualTo(options.channelInterpretation);
+
+ options = {channelInterpretation: 'foobar'};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .throw(TypeError);
+
+ // Test maxDistance
+ options = {maxDistance: -1};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .throw(RangeError);
+ should(
+ () => {
+ node = new PannerNode(context);
+ node.maxDistance = options.maxDistance;
+ },
+ `node.maxDistance = ${options.maxDistance}`)
+ .throw(RangeError);
+ should(node.maxDistance,
+ `node.maxDistance after setting to ${options.maxDistance}`)
+ .beEqualTo(10000);
+
+ options = {maxDistance: 100};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'node7 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .notThrow();
+ should(node.maxDistance, 'node7.maxDistance')
+ .beEqualTo(options.maxDistance);
+
+ // Test rolloffFactor
+ options = {rolloffFactor: -1};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .throw(RangeError);
+ should(
+ () => {
+ node = new PannerNode(context);
+ node.rolloffFactor = options.rolloffFactor;
+ },
+ `node.rolloffFactor = ${options.rolloffFactor}`)
+ .throw(RangeError);
+ should(node.rolloffFactor,
+ `node.rolloffFactor after setting to ${options.rolloffFactor}`)
+ .beEqualTo(1);
+
+ options = {rolloffFactor: 0};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'node8 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .notThrow();
+ should(node.rolloffFactor, 'node8.rolloffFactor')
+ .beEqualTo(options.rolloffFactor);
+
+ options = {rolloffFactor: 0.5};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'node8 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .notThrow();
+ should(node.rolloffFactor, 'node8.rolloffFactor')
+ .beEqualTo(options.rolloffFactor);
+
+ options = {rolloffFactor: 100};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'node8 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .notThrow();
+ should(node.rolloffFactor, 'node8.rolloffFactor')
+ .beEqualTo(options.rolloffFactor);
+
+ // Test coneOuterGain
+ options = {coneOuterGain: -1};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .throw(DOMException, 'InvalidStateError');
+ should(
+ () => {
+ node = new PannerNode(context);
+ node.coneOuterGain = options.coneOuterGain;
+ },
+ `node.coneOuterGain = ${options.coneOuterGain}`)
+ .throw(DOMException, 'InvalidStateError');
+ should(node.coneOuterGain,
+ `node.coneOuterGain after setting to ${options.coneOuterGain}`)
+ .beEqualTo(0);
+
+ options = {coneOuterGain: 1.1};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .throw(DOMException, 'InvalidStateError');
+ should(
+ () => {
+ node = new PannerNode(context);
+ node.coneOuterGain = options.coneOuterGain;
+ },
+ `node.coneOuterGain = ${options.coneOuterGain}`)
+ .throw(DOMException, 'InvalidStateError');
+ should(node.coneOuterGain,
+ `node.coneOuterGain after setting to ${options.coneOuterGain}`)
+ .beEqualTo(0);
+
+ options = {coneOuterGain: 0.0};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'node9 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .notThrow();
+ should(node.coneOuterGain, 'node9.coneOuterGain')
+ .beEqualTo(options.coneOuterGain);
+ options = {coneOuterGain: 0.5};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'node9 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .notThrow();
+ should(node.coneOuterGain, 'node9.coneOuterGain')
+ .beEqualTo(options.coneOuterGain);
+
+ options = {coneOuterGain: 1.0};
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'node9 = new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .notThrow();
+ should(node.coneOuterGain, 'node9.coneOuterGain')
+ .beEqualTo(options.coneOuterGain);
+
+ task.done();
+ });
+
+ audit.define('constructor with options', (task, should) => {
+ let node;
+ let success = true;
+ let options = {
+ panningModel: 'HRTF',
+ // We use full double float values here to verify also that the actual
+ // AudioParam value is properly rounded to a float. The actual value
+ // is immaterial as long as x != Math.fround(x).
+ positionX: Math.SQRT2,
+ positionY: 2 * Math.SQRT2,
+ positionZ: 3 * Math.SQRT2,
+ orientationX: -Math.SQRT2,
+ orientationY: -2 * Math.SQRT2,
+ orientationZ: -3 * Math.SQRT2,
+ distanceModel: 'linear',
+ // We use full double float values here to verify also that the actual
+ // attribute is a double float. The actual value is immaterial as
+ // long as x != Math.fround(x).
+ refDistance: Math.PI,
+ maxDistance: 2 * Math.PI,
+ rolloffFactor: 3 * Math.PI,
+ coneInnerAngle: 4 * Math.PI,
+ coneOuterAngle: 5 * Math.PI,
+ coneOuterGain: 0.1 * Math.PI
+ };
+
+ should(
+ () => {
+ node = new PannerNode(context, options);
+ },
+ 'node = new PannerNode(c, ' + JSON.stringify(options) + ')')
+ .notThrow();
+ should(node instanceof PannerNode, 'node instanceof PannerNode')
+ .beEqualTo(true);
+
+ should(node.panningModel, 'node.panningModel')
+ .beEqualTo(options.panningModel);
+ should(node.positionX.value, 'node.positionX.value')
+ .beEqualTo(Math.fround(options.positionX));
+ should(node.positionY.value, 'node.positionY.value')
+ .beEqualTo(Math.fround(options.positionY));
+ should(node.positionZ.value, 'node.positionZ.value')
+ .beEqualTo(Math.fround(options.positionZ));
+ should(node.orientationX.value, 'node.orientationX.value')
+ .beEqualTo(Math.fround(options.orientationX));
+ should(node.orientationY.value, 'node.orientationY.value')
+ .beEqualTo(Math.fround(options.orientationY));
+ should(node.orientationZ.value, 'node.orientationZ.value')
+ .beEqualTo(Math.fround(options.orientationZ));
+ should(node.distanceModel, 'node.distanceModel')
+ .beEqualTo(options.distanceModel);
+ should(node.refDistance, 'node.refDistance')
+ .beEqualTo(options.refDistance);
+ should(node.maxDistance, 'node.maxDistance')
+ .beEqualTo(options.maxDistance);
+ should(node.rolloffFactor, 'node.rolloffFactor')
+ .beEqualTo(options.rolloffFactor);
+ should(node.coneInnerAngle, 'node.coneInnerAngle')
+ .beEqualTo(options.coneInnerAngle);
+ should(node.coneOuterAngle, 'node.coneOuterAngle')
+ .beEqualTo(options.coneOuterAngle);
+ should(node.coneOuterGain, 'node.coneOuterGain')
+ .beEqualTo(options.coneOuterGain);
+
+ should(node.channelCount, 'node.channelCount').beEqualTo(2);
+ should(node.channelCountMode, 'node.channelCountMode')
+ .beEqualTo('clamped-max');
+ should(node.channelInterpretation, 'node.channelInterpretation')
+ .beEqualTo('speakers');
+
+ task.done();
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/distance-exponential.html b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/distance-exponential.html
new file mode 100644
index 0000000000..383e2c67b6
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/distance-exponential.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ distance-exponential.html
+ </title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../resources/audit-util.js"></script>
+ <script src="../../resources/audit.js"></script>
+ <script src="../../resources/distance-model-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ audit.define(
+ {
+ label: 'test',
+ description: 'Exponential distance model for PannerNode'
+ },
+ (task, should) => {
+ // Create offline audio context.
+ context = new OfflineAudioContext(
+ 2, sampleRate * renderLengthSeconds, sampleRate);
+
+ createTestAndRun(context, 'exponential', should)
+ .then(() => task.done());
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/distance-inverse.html b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/distance-inverse.html
new file mode 100644
index 0000000000..a4ff984e09
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/distance-inverse.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ distance-inverse.html
+ </title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../resources/audit-util.js"></script>
+ <script src="../../resources/audit.js"></script>
+ <script src="../../resources/distance-model-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ audit.define('test', (task, should) => {
+ // Create offline audio context.
+ context = new OfflineAudioContext(
+ 2, sampleRate * renderLengthSeconds, sampleRate);
+
+ createTestAndRun(context, 'inverse', should).then(() => task.done());
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/distance-linear.html b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/distance-linear.html
new file mode 100644
index 0000000000..812fea3eba
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/distance-linear.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ distance-linear.html
+ </title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../resources/audit-util.js"></script>
+ <script src="../../resources/audit.js"></script>
+ <script src="../../resources/distance-model-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ audit.define(
+ {label: 'test', description: 'Linear distance model PannerNode'},
+ (task, should) => {
+ // Create offline audio context.
+ context = new OfflineAudioContext(
+ 2, sampleRate * renderLengthSeconds, sampleRate);
+
+ createTestAndRun(context, 'linear', should).then(() => task.done());
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-automation-basic.html b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-automation-basic.html
new file mode 100644
index 0000000000..5c3df0e6fd
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-automation-basic.html
@@ -0,0 +1,298 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ Test Basic PannerNode with Automation Position Properties
+ </title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../resources/audit-util.js"></script>
+ <script src="../../resources/audit.js"></script>
+ <script src="../../resources/panner-formulas.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let sampleRate = 48000;
+
+ // These tests are quite slow, so don't run for many frames. 256 frames
+ // should be enough to demonstrate that automations are working.
+ let renderFrames = 256;
+ let renderDuration = renderFrames / sampleRate;
+
+ let audit = Audit.createTaskRunner();
+
+ // Array of tests for setting the panner positions. These tests basically
+ // verify that the position setters for the panner and listener are
+ // working correctly.
+ let testConfig = [
+ {
+ setter: 'positionX',
+ },
+ {
+ setter: 'positionY',
+ },
+ {
+ setter: 'positionZ',
+ }
+ ];
+
+ // Create tests for the panner position setters. Both mono and steroe
+ // sources are tested.
+ for (let k = 0; k < testConfig.length; ++k) {
+ let config = testConfig[k];
+ // Function to create the test to define the test.
+ let tester = function(config, channelCount) {
+ return (task, should) => {
+ let nodes = createGraph(channelCount);
+ let {context, source, panner} = nodes;
+
+ let message = channelCount == 1 ? 'Mono' : 'Stereo';
+ message += ' panner.' + config.setter;
+
+ testPositionSetter(should, {
+ nodes: nodes,
+ pannerSetter: panner[config.setter],
+ message: message
+ }).then(() => task.done());
+ }
+ };
+
+ audit.define('Stereo panner.' + config.setter, tester(config, 2));
+ audit.define('Mono panner.' + config.setter, tester(config, 1));
+ }
+
+ // Create tests for the listener position setters. Both mono and steroe
+ // sources are tested.
+ for (let k = 0; k < testConfig.length; ++k) {
+ let config = testConfig[k];
+ // Function to create the test to define the test.
+ let tester = function(config, channelCount) {
+ return (task, should) => {
+ let nodes = createGraph(channelCount);
+ let {context, source, panner} = nodes;
+
+ let message = channelCount == 1 ? 'Mono' : 'Stereo';
+ message += ' listener.' + config.setter;
+
+ // Some relatively arbitrary (non-default) position for the source
+ // location.
+ panner.setPosition(1, 0, 1);
+
+ testPositionSetter(should, {
+ nodes: nodes,
+ pannerSetter: context.listener[config.setter],
+ message: message
+ }).then(() => task.done());
+ }
+ };
+
+ audit.define('Stereo listener.' + config.setter, tester(config, 2));
+ audit.define('Mono listener.' + config.setter, tester(config, 1));
+ }
+
+ // Test setPosition method.
+ audit.define('setPosition', (task, should) => {
+ let {context, panner, source} = createGraph(2);
+
+ // Initialize source position (values don't really matter).
+ panner.setPosition(1, 1, 1);
+
+ // After some (unimportant) time, move the panner to a (any) new
+ // location.
+ let suspendFrame = 128;
+ context.suspend(suspendFrame / sampleRate)
+ .then(function() {
+ panner.setPosition(-100, 2000, 8000);
+ })
+ .then(context.resume.bind(context));
+
+ context.startRendering()
+ .then(function(resultBuffer) {
+ verifyPannerOutputChanged(
+ should, resultBuffer,
+ {message: 'setPosition', suspendFrame: suspendFrame});
+ })
+ .then(() => task.done());
+ });
+
+ audit.define('orientation setter', (task, should) => {
+ let {context, panner, source} = createGraph(2);
+
+ // For orientation to matter, we need to make the source directional,
+ // and also move away from the listener (because the default location is
+ // 0,0,0).
+ panner.setPosition(0, 0, 1);
+ panner.coneInnerAngle = 0;
+ panner.coneOuterAngle = 360;
+ panner.coneOuterGain = .001;
+
+ // After some (unimportant) time, change the panner orientation to a new
+ // orientation. The only constraint is that the orientation changes
+ // from before.
+ let suspendFrame = 128;
+ context.suspend(suspendFrame / sampleRate)
+ .then(function() {
+ panner.orientationX.value = -100;
+ panner.orientationY.value = 2000;
+ panner.orientationZ.value = 8000;
+ })
+ .then(context.resume.bind(context));
+
+ context.startRendering()
+ .then(function(resultBuffer) {
+ verifyPannerOutputChanged(should, resultBuffer, {
+ message: 'panner.orientation{XYZ}',
+ suspendFrame: suspendFrame
+ });
+ })
+ .then(() => task.done());
+ });
+
+ audit.define('forward setter', (task, should) => {
+ let {context, panner, source} = createGraph(2);
+
+ // For orientation to matter, we need to make the source directional,
+ // and also move away from the listener (because the default location is
+ // 0,0,0).
+ panner.setPosition(0, 0, 1);
+ panner.coneInnerAngle = 0;
+ panner.coneOuterAngle = 360;
+ panner.coneOuterGain = .001;
+
+ // After some (unimportant) time, change the panner orientation to a new
+ // orientation. The only constraint is that the orientation changes
+ // from before.
+ let suspendFrame = 128;
+ context.suspend(suspendFrame / sampleRate)
+ .then(function() {
+ context.listener.forwardX.value = -100;
+ context.listener.forwardY.value = 2000;
+ context.listener.forwardZ.value = 8000;
+ })
+ .then(context.resume.bind(context));
+
+ context.startRendering()
+ .then(function(resultBuffer) {
+ verifyPannerOutputChanged(should, resultBuffer, {
+ message: 'listener.forward{XYZ}',
+ suspendFrame: suspendFrame
+ });
+ })
+ .then(() => task.done());
+ });
+
+ audit.define('up setter', (task, should) => {
+ let {context, panner, source} = createGraph(2);
+
+ // For orientation to matter, we need to make the source directional,
+ // and also move away from the listener (because the default location is
+ // 0,0,0).
+ panner.setPosition(0, 0, 1);
+ panner.coneInnerAngle = 0;
+ panner.coneOuterAngle = 360;
+ panner.coneOuterGain = .001;
+ panner.setPosition(1, 0, 1);
+
+ // After some (unimportant) time, change the panner orientation to a new
+ // orientation. The only constraint is that the orientation changes
+ // from before.
+ let suspendFrame = 128;
+ context.suspend(suspendFrame / sampleRate)
+ .then(function() {
+ context.listener.upX.value = 100;
+ context.listener.upY.value = 100;
+ context.listener.upZ.value = 100;
+ ;
+ })
+ .then(context.resume.bind(context));
+
+ context.startRendering()
+ .then(function(resultBuffer) {
+ verifyPannerOutputChanged(
+ should, resultBuffer,
+ {message: 'listener.up{XYZ}', suspendFrame: suspendFrame});
+ })
+ .then(() => task.done());
+ });
+
+ audit.run();
+
+ function createGraph(channelCount) {
+ let context = new OfflineAudioContext(2, renderFrames, sampleRate);
+ let panner = context.createPanner();
+ let source = context.createBufferSource();
+ source.buffer =
+ createConstantBuffer(context, 1, channelCount == 1 ? 1 : [1, 2]);
+ source.loop = true;
+
+ source.connect(panner);
+ panner.connect(context.destination);
+
+ source.start();
+ return {context: context, source: source, panner: panner};
+ }
+
+ function testPositionSetter(should, options) {
+ let {nodes, pannerSetter, message} = options;
+
+ let {context, source, panner} = nodes;
+
+ // Set panner x position. (Value doesn't matter);
+ pannerSetter.value = 1;
+
+ // Wait a bit and set a new position. (Actual time and position doesn't
+ // matter).
+ let suspendFrame = 128;
+ context.suspend(suspendFrame / sampleRate)
+ .then(function() {
+ pannerSetter.value = 10000;
+ })
+ .then(context.resume.bind(context));
+
+ return context.startRendering().then(function(resultBuffer) {
+ verifyPannerOutputChanged(
+ should, resultBuffer,
+ {message: message, suspendFrame: suspendFrame});
+ });
+ }
+
+ function verifyPannerOutputChanged(should, resultBuffer, options) {
+ let {message, suspendFrame} = options;
+ // Verify that the first part of output is constant. (Doesn't matter
+ // what.)
+ let data0 = resultBuffer.getChannelData(0);
+ let data1 = resultBuffer.getChannelData(1);
+
+ let middle = '[0, ' + suspendFrame + ') ';
+ should(
+ data0.slice(0, suspendFrame),
+ message + '.value frame ' + middle + 'channel 0')
+ .beConstantValueOf(data0[0]);
+ should(
+ data1.slice(0, suspendFrame),
+ message + '.value frame ' + middle + 'channel 1')
+ .beConstantValueOf(data1[0]);
+
+ // The rest after suspendTime should be constant and different from the
+ // first part.
+ middle = '[' + suspendFrame + ', ' + renderFrames + ') ';
+ should(
+ data0.slice(suspendFrame),
+ message + '.value frame ' + middle + 'channel 0')
+ .beConstantValueOf(data0[suspendFrame]);
+ should(
+ data1.slice(suspendFrame),
+ message + '.value frame ' + middle + 'channel 1')
+ .beConstantValueOf(data1[suspendFrame]);
+ should(
+ data0[suspendFrame],
+ message + ': Output at frame ' + suspendFrame + ' channel 0')
+ .notBeEqualTo(data0[0]);
+ should(
+ data1[suspendFrame],
+ message + ': Output at frame ' + suspendFrame + ' channel 1')
+ .notBeEqualTo(data1[0]);
+ }
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-automation-equalpower-stereo.html b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-automation-equalpower-stereo.html
new file mode 100644
index 0000000000..7afc9c2a39
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-automation-equalpower-stereo.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ panner-automation-equalpower-stereo.html
+ </title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../resources/audit-util.js"></script>
+ <script src="../../resources/audit.js"></script>
+ <script src="../../resources/panner-model-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ // To test the panner, we create a number of panner nodes
+ // equally spaced on a semicircle at unit distance. The
+ // semicircle covers the azimuth range from -90 to 90 deg,
+ // covering full left to full right. Each source is an impulse
+ // turning at a different time and we check that the rendered
+ // impulse has the expected gain.
+ audit.define(
+ {
+ label: 'test',
+ description:
+ 'Equal-power panner model of AudioPannerNode with stereo source',
+ },
+ (task, should) => {
+ // Create offline audio context.
+ context = new OfflineAudioContext(
+ 2, sampleRate * renderLengthSeconds, sampleRate);
+
+ createTestAndRun(
+ context, should, nodesToCreate, 2,
+ function(panner, x, y, z) {
+ panner.positionX.value = x;
+ panner.positionY.value = y;
+ panner.positionZ.value = z;
+ })
+ .then(() => task.done());
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-automation-position.html b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-automation-position.html
new file mode 100644
index 0000000000..8e09e869ac
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-automation-position.html
@@ -0,0 +1,265 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ Test Automation of PannerNode Positions
+ </title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../resources/audit-util.js"></script>
+ <script src="../../resources/audit.js"></script>
+ <script src="../../resources/panner-formulas.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let sampleRate = 48000;
+ // These tests are quite slow, so don't run for many frames. 256 frames
+ // should be enough to demonstrate that automations are working.
+ let renderFrames = 256;
+ let renderDuration = renderFrames / sampleRate;
+
+ let context;
+ let panner;
+
+ let audit = Audit.createTaskRunner();
+
+ // Set of tests for the panner node with automations applied to the
+ // position of the source.
+ let testConfigs = [
+ {
+ // Distance model parameters for the panner
+ distanceModel: {model: 'inverse', rolloff: 1},
+ // Initial location of the source
+ startPosition: [0, 0, 1],
+ // Final position of the source. For this test, we only want to move
+ // on the z axis which
+ // doesn't change the azimuth angle.
+ endPosition: [0, 0, 10000],
+ },
+ {
+ distanceModel: {model: 'inverse', rolloff: 1},
+ startPosition: [0, 0, 1],
+ // An essentially random end position, but it should be such that
+ // azimuth angle changes as
+ // we move from the start to the end.
+ endPosition: [20000, 30000, 10000],
+ errorThreshold: [
+ {
+ // Error threshold for 1-channel case
+ relativeThreshold: 4.8124e-7
+ },
+ {
+ // Error threshold for 2-channel case
+ relativeThreshold: 4.3267e-7
+ }
+ ],
+ },
+ {
+ distanceModel: {model: 'exponential', rolloff: 1.5},
+ startPosition: [0, 0, 1],
+ endPosition: [20000, 30000, 10000],
+ errorThreshold:
+ [{relativeThreshold: 5.0783e-7}, {relativeThreshold: 5.2180e-7}]
+ },
+ {
+ distanceModel: {model: 'linear', rolloff: 1},
+ startPosition: [0, 0, 1],
+ endPosition: [20000, 30000, 10000],
+ errorThreshold: [
+ {relativeThreshold: 6.5324e-6}, {relativeThreshold: 6.5756e-6}
+ ]
+ }
+ ];
+
+ for (let k = 0; k < testConfigs.length; ++k) {
+ let config = testConfigs[k];
+ let tester = function(c, channelCount) {
+ return (task, should) => {
+ runTest(should, c, channelCount).then(() => task.done());
+ }
+ };
+
+ let baseTestName = config.distanceModel.model +
+ ' rolloff: ' + config.distanceModel.rolloff;
+
+ // Define tasks for both 1-channel and 2-channel
+ audit.define(k + ': 1-channel ' + baseTestName, tester(config, 1));
+ audit.define(k + ': 2-channel ' + baseTestName, tester(config, 2));
+ }
+
+ audit.run();
+
+ function runTest(should, options, channelCount) {
+ // Output has 5 channels: channels 0 and 1 are for the stereo output of
+ // the panner node. Channels 2-5 are the for automation of the x,y,z
+ // coordinate so that we have actual coordinates used for the panner
+ // automation.
+ context = new OfflineAudioContext(5, renderFrames, sampleRate);
+
+ // Stereo source for the panner.
+ let source = context.createBufferSource();
+ source.buffer = createConstantBuffer(
+ context, renderFrames, channelCount == 1 ? 1 : [1, 2]);
+
+ panner = context.createPanner();
+ panner.distanceModel = options.distanceModel.model;
+ panner.rolloffFactor = options.distanceModel.rolloff;
+ panner.panningModel = 'equalpower';
+
+ // Source and gain node for the z-coordinate calculation.
+ let dist = context.createBufferSource();
+ dist.buffer = createConstantBuffer(context, 1, 1);
+ dist.loop = true;
+ let gainX = context.createGain();
+ let gainY = context.createGain();
+ let gainZ = context.createGain();
+ dist.connect(gainX);
+ dist.connect(gainY);
+ dist.connect(gainZ);
+
+ // Set the gain automation to match the z-coordinate automation of the
+ // panner.
+
+ // End the automation some time before the end of the rendering so we
+ // can verify that automation has the correct end time and value.
+ let endAutomationTime = 0.75 * renderDuration;
+
+ gainX.gain.setValueAtTime(options.startPosition[0], 0);
+ gainX.gain.linearRampToValueAtTime(
+ options.endPosition[0], endAutomationTime);
+ gainY.gain.setValueAtTime(options.startPosition[1], 0);
+ gainY.gain.linearRampToValueAtTime(
+ options.endPosition[1], endAutomationTime);
+ gainZ.gain.setValueAtTime(options.startPosition[2], 0);
+ gainZ.gain.linearRampToValueAtTime(
+ options.endPosition[2], endAutomationTime);
+
+ dist.start();
+
+ // Splitter and merger to map the panner output and the z-coordinate
+ // automation to the correct channels in the destination.
+ let splitter = context.createChannelSplitter(2);
+ let merger = context.createChannelMerger(5);
+
+ source.connect(panner);
+ // Split the output of the panner to separate channels
+ panner.connect(splitter);
+
+ // Merge the panner outputs and the z-coordinate output to the correct
+ // destination channels.
+ splitter.connect(merger, 0, 0);
+ splitter.connect(merger, 1, 1);
+ gainX.connect(merger, 0, 2);
+ gainY.connect(merger, 0, 3);
+ gainZ.connect(merger, 0, 4);
+
+ merger.connect(context.destination);
+
+ // Initialize starting point of the panner.
+ panner.positionX.setValueAtTime(options.startPosition[0], 0);
+ panner.positionY.setValueAtTime(options.startPosition[1], 0);
+ panner.positionZ.setValueAtTime(options.startPosition[2], 0);
+
+ // Automate z coordinate to move away from the listener
+ panner.positionX.linearRampToValueAtTime(
+ options.endPosition[0], 0.75 * renderDuration);
+ panner.positionY.linearRampToValueAtTime(
+ options.endPosition[1], 0.75 * renderDuration);
+ panner.positionZ.linearRampToValueAtTime(
+ options.endPosition[2], 0.75 * renderDuration);
+
+ source.start();
+
+ // Go!
+ return context.startRendering().then(function(renderedBuffer) {
+ // Get the panner outputs
+ let data0 = renderedBuffer.getChannelData(0);
+ let data1 = renderedBuffer.getChannelData(1);
+ let xcoord = renderedBuffer.getChannelData(2);
+ let ycoord = renderedBuffer.getChannelData(3);
+ let zcoord = renderedBuffer.getChannelData(4);
+
+ // We're doing a linear ramp on the Z axis with the equalpower panner,
+ // so the equalpower panning gain remains constant. We only need to
+ // model the distance effect.
+
+ // Compute the distance gain
+ let distanceGain = new Float32Array(xcoord.length);
+ ;
+
+ if (panner.distanceModel === 'inverse') {
+ for (let k = 0; k < distanceGain.length; ++k) {
+ distanceGain[k] =
+ inverseDistance(panner, xcoord[k], ycoord[k], zcoord[k])
+ }
+ } else if (panner.distanceModel === 'linear') {
+ for (let k = 0; k < distanceGain.length; ++k) {
+ distanceGain[k] =
+ linearDistance(panner, xcoord[k], ycoord[k], zcoord[k])
+ }
+ } else if (panner.distanceModel === 'exponential') {
+ for (let k = 0; k < distanceGain.length; ++k) {
+ distanceGain[k] =
+ exponentialDistance(panner, xcoord[k], ycoord[k], zcoord[k])
+ }
+ }
+
+ // Compute the expected result. Since we're on the z-axis, the left
+ // and right channels pass through the equalpower panner unchanged.
+ // Only need to apply the distance gain.
+ let buffer0 = source.buffer.getChannelData(0);
+ let buffer1 =
+ channelCount == 2 ? source.buffer.getChannelData(1) : buffer0;
+
+ let azimuth = new Float32Array(buffer0.length);
+
+ for (let k = 0; k < data0.length; ++k) {
+ azimuth[k] = calculateAzimuth(
+ [xcoord[k], ycoord[k], zcoord[k]],
+ [
+ context.listener.positionX.value,
+ context.listener.positionY.value,
+ context.listener.positionZ.value
+ ],
+ [
+ context.listener.forwardX.value,
+ context.listener.forwardY.value,
+ context.listener.forwardZ.value
+ ],
+ [
+ context.listener.upX.value, context.listener.upY.value,
+ context.listener.upZ.value
+ ]);
+ }
+
+ let expected = applyPanner(azimuth, buffer0, buffer1, channelCount);
+ let expected0 = expected.left;
+ let expected1 = expected.right;
+
+ for (let k = 0; k < expected0.length; ++k) {
+ expected0[k] *= distanceGain[k];
+ expected1[k] *= distanceGain[k];
+ }
+
+ let info = options.distanceModel.model +
+ ', rolloff: ' + options.distanceModel.rolloff;
+ let prefix = channelCount + '-channel ' +
+ '[' + options.startPosition[0] + ', ' + options.startPosition[1] +
+ ', ' + options.startPosition[2] + '] -> [' +
+ options.endPosition[0] + ', ' + options.endPosition[1] + ', ' +
+ options.endPosition[2] + ']: ';
+
+ let errorThreshold = 0;
+
+ if (options.errorThreshold)
+ errorThreshold = options.errorThreshold[channelCount - 1]
+
+ should(data0, prefix + 'distanceModel: ' + info + ', left channel')
+ .beCloseToArray(expected0, {absoluteThreshold: errorThreshold});
+ should(data1, prefix + 'distanceModel: ' + info + ', right channel')
+ .beCloseToArray(expected1, {absoluteThreshold: errorThreshold});
+ });
+ }
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-azimuth.html b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-azimuth.html
new file mode 100644
index 0000000000..d09f2ec352
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-azimuth.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test Panner Azimuth Calculation</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../resources/audit.js"></script>
+ </head>
+
+ <body>
+ <script>
+ const audit = Audit.createTaskRunner();
+
+ // Fairly arbitrary sample rate
+ const sampleRate = 16000;
+
+ audit.define('Azimuth calculation', (task, should) => {
+ // Two channels for the context so we can see each channel of the
+ // panner node.
+ let context = new OfflineAudioContext(2, sampleRate, sampleRate);
+
+ let src = new ConstantSourceNode(context);
+ let panner = new PannerNode(context);
+
+ src.connect(panner).connect(context.destination);
+
+ // The source is still pointed directly at the listener, but is now
+ // directly above. The audio should be the same in both the left and
+ // right channels.
+ panner.positionY.value = 1;
+
+ src.start();
+
+ context.startRendering()
+ .then(audioBuffer => {
+ // The left and right channels should contain the same signal.
+ let c0 = audioBuffer.getChannelData(0);
+ let c1 = audioBuffer.getChannelData(1);
+
+ let expected = Math.fround(Math.SQRT1_2);
+
+ should(c0, 'Left channel').beConstantValueOf(expected);
+ should(c1, 'Righteft channel').beConstantValueOf(expected);
+ })
+ .then(() => task.done());
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-distance-clamping.html b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-distance-clamping.html
new file mode 100644
index 0000000000..78c1ec6dc2
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-distance-clamping.html
@@ -0,0 +1,227 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ Test Clamping of Distance for PannerNode
+ </title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../resources/audit-util.js"></script>
+ <script src="../../resources/audit.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ // Arbitrary sample rate and render length.
+ let sampleRate = 48000;
+ let renderFrames = 128;
+
+ let audit = Audit.createTaskRunner();
+
+ audit.define('ref-distance-error', (task, should) => {
+ testDistanceLimits(should, {name: 'refDistance', isZeroAllowed: true});
+ task.done();
+ });
+
+ audit.define('max-distance-error', (task, should) => {
+ testDistanceLimits(should, {name: 'maxDistance', isZeroAllowed: false});
+ task.done();
+ });
+
+ function testDistanceLimits(should, options) {
+ // Verify that exceptions are thrown for invalid values of refDistance.
+ let context = new OfflineAudioContext(1, renderFrames, sampleRate);
+
+ let attrName = options.name;
+ let prefix = 'new PannerNode(c, {' + attrName + ': ';
+
+ should(function() {
+ let nodeOptions = {};
+ nodeOptions[attrName] = -1;
+ new PannerNode(context, nodeOptions);
+ }, prefix + '-1})').throw(RangeError);
+
+ if (options.isZeroAllowed) {
+ should(function() {
+ let nodeOptions = {};
+ nodeOptions[attrName] = 0;
+ new PannerNode(context, nodeOptions);
+ }, prefix + '0})').notThrow();
+ } else {
+ should(function() {
+ let nodeOptions = {};
+ nodeOptions[attrName] = 0;
+ new PannerNode(context, nodeOptions);
+ }, prefix + '0})').throw(RangeError);
+ }
+
+ // The smallest representable positive single float.
+ let leastPositiveDoubleFloat = 4.9406564584124654e-324;
+
+ should(function() {
+ let nodeOptions = {};
+ nodeOptions[attrName] = leastPositiveDoubleFloat;
+ new PannerNode(context, nodeOptions);
+ }, prefix + leastPositiveDoubleFloat + '})').notThrow();
+
+ prefix = 'panner.' + attrName + ' = ';
+ panner = new PannerNode(context);
+ should(function() {
+ panner[attrName] = -1;
+ }, prefix + '-1').throw(RangeError);
+
+ if (options.isZeroAllowed) {
+ should(function() {
+ panner[attrName] = 0;
+ }, prefix + '0').notThrow();
+ } else {
+ should(function() {
+ panner[attrName] = 0;
+ }, prefix + '0').throw(RangeError);
+ }
+
+ should(function() {
+ panner[attrName] = leastPositiveDoubleFloat;
+ }, prefix + leastPositiveDoubleFloat).notThrow();
+ }
+
+ audit.define('min-distance', async (task, should) => {
+ // Test clamping of panner distance to refDistance for all of the
+ // distance models. The actual distance is arbitrary as long as it's
+ // less than refDistance. We test default and non-default values for
+ // the panner's refDistance and maxDistance.
+ // correctly.
+ await runTest(should, {
+ distance: 0.01,
+ distanceModel: 'linear',
+ });
+ await runTest(should, {
+ distance: 0.01,
+ distanceModel: 'exponential',
+ });
+ await runTest(should, {
+ distance: 0.01,
+ distanceModel: 'inverse',
+ });
+ await runTest(should, {
+ distance: 2,
+ distanceModel: 'linear',
+ maxDistance: 1000,
+ refDistance: 10,
+ });
+ await runTest(should, {
+ distance: 2,
+ distanceModel: 'exponential',
+ maxDistance: 1000,
+ refDistance: 10,
+ });
+ await runTest(should, {
+ distance: 2,
+ distanceModel: 'inverse',
+ maxDistance: 1000,
+ refDistance: 10,
+ });
+ task.done();
+ });
+
+ audit.define('max-distance', async (task, should) => {
+ // Like the "min-distance" task, but for clamping to the max
+ // distance. The actual distance is again arbitrary as long as it is
+ // greater than maxDistance.
+ await runTest(should, {
+ distance: 20000,
+ distanceModel: 'linear',
+ });
+ await runTest(should, {
+ distance: 21000,
+ distanceModel: 'exponential',
+ });
+ await runTest(should, {
+ distance: 23000,
+ distanceModel: 'inverse',
+ });
+ await runTest(should, {
+ distance: 5000,
+ distanceModel: 'linear',
+ maxDistance: 1000,
+ refDistance: 10,
+ });
+ await runTest(should, {
+ distance: 5000,
+ distanceModel: 'exponential',
+ maxDistance: 1000,
+ refDistance: 10,
+ });
+ await runTest(should, {
+ distance: 5000,
+ distanceModel: 'inverse',
+ maxDistance: 1000,
+ refDistance: 10,
+ });
+ task.done();
+ });
+
+ function runTest(should, options) {
+ let context = new OfflineAudioContext(2, renderFrames, sampleRate);
+ let src = new OscillatorNode(context, {
+ type: 'sawtooth',
+ frequency: 20 * 440,
+ });
+
+ // Set panner options. Use a non-default rolloffFactor so that the
+ // various distance models look distinctly different.
+ let pannerOptions = {};
+ Object.assign(pannerOptions, options, {rolloffFactor: 0.5});
+
+ let pannerRef = new PannerNode(context, pannerOptions);
+ let pannerTest = new PannerNode(context, pannerOptions);
+
+ // Split the panner output so we can grab just one of the output
+ // channels.
+ let splitRef = new ChannelSplitterNode(context, {numberOfOutputs: 2});
+ let splitTest = new ChannelSplitterNode(context, {numberOfOutputs: 2});
+
+ // Merge the panner outputs back into one stereo stream for the
+ // destination.
+ let merger = new ChannelMergerNode(context, {numberOfInputs: 2});
+
+ src.connect(pannerTest).connect(splitTest).connect(merger, 0, 0);
+ src.connect(pannerRef).connect(splitRef).connect(merger, 0, 1);
+
+ merger.connect(context.destination);
+
+ // Move the panner some distance away. Arbitrarily select the x
+ // direction. For the reference panner, manually clamp the distance.
+ // All models clamp the distance to a minimum of refDistance. Only the
+ // linear model also clamps to a maximum of maxDistance.
+ let xRef = Math.max(options.distance, pannerRef.refDistance);
+
+ if (pannerRef.distanceModel === 'linear') {
+ xRef = Math.min(xRef, pannerRef.maxDistance);
+ }
+
+ let xTest = options.distance;
+
+ pannerRef.positionZ.setValueAtTime(xRef, 0);
+ pannerTest.positionZ.setValueAtTime(xTest, 0);
+
+ src.start();
+
+ return context.startRendering().then(function(resultBuffer) {
+ let actual = resultBuffer.getChannelData(0);
+ let expected = resultBuffer.getChannelData(1);
+
+ should(
+ xTest < pannerRef.refDistance || xTest > pannerRef.maxDistance,
+ 'Model: ' + options.distanceModel + ': Distance (' + xTest +
+ ') is outside the range [' + pannerRef.refDistance + ', ' +
+ pannerRef.maxDistance + ']')
+ .beEqualTo(true);
+ should(actual, 'Test panner output ' + JSON.stringify(options))
+ .beEqualToArray(expected);
+ });
+ }
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-equalpower-stereo.html b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-equalpower-stereo.html
new file mode 100644
index 0000000000..2a0225b3f6
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-equalpower-stereo.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ panner-equalpower-stereo.html
+ </title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../resources/audit-util.js"></script>
+ <script src="../../resources/audit.js"></script>
+ <script src="../../resources/panner-model-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ // To test the panner, we create a number of panner nodes
+ // equally spaced on a semicircle at unit distance. The
+ // semicircle covers the azimuth range from -90 to 90 deg,
+ // covering full left to full right. Each source is an impulse
+ // turning at a different time and we check that the rendered
+ // impulse has the expected gain.
+ audit.define(
+ {
+ label: 'test',
+ description:
+ 'Equal-power panner model of AudioPannerNode with stereo source'
+ },
+ (task, should) => {
+ context = new OfflineAudioContext(
+ 2, sampleRate * renderLengthSeconds, sampleRate);
+
+ createTestAndRun(
+ context, should, nodesToCreate, 2,
+ function(panner, x, y, z) {
+ panner.setPosition(x, y, z);
+ })
+ .then(() => task.done());
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-equalpower.html b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-equalpower.html
new file mode 100644
index 0000000000..3ff21b651f
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-equalpower.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ panner-equalpower.html
+ </title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../resources/audit-util.js"></script>
+ <script src="../../resources/audit.js"></script>
+ <script src="../../resources/panner-model-testing.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ let audit = Audit.createTaskRunner();
+
+ // To test the panner, we create a number of panner nodes
+ // equally spaced on a semicircle at unit distance. The
+ // semicircle covers the azimuth range from -90 to 90 deg,
+ // covering full left to full right. Each source is an impulse
+ // turning at a different time and we check that the rendered
+ // impulse has the expected gain.
+ audit.define(
+ {
+ label: 'test',
+ description: 'Equal-power panner model of AudioPannerNode',
+ },
+ (task, should) => {
+ // Create offline audio context.
+ context = new OfflineAudioContext(
+ 2, sampleRate * renderLengthSeconds, sampleRate);
+
+ createTestAndRun(
+ context, should, nodesToCreate, 1,
+ function(panner, x, y, z) {
+ panner.setPosition(x, y, z);
+ })
+ .then(() => task.done());
+ ;
+ });
+
+ // Test that a mono source plays out on both the left and right channels
+ // when the source and listener positions are the same.
+ audit.define(
+ {
+ label: 'mono source=listener',
+ description: 'Source and listener at the same position'
+ },
+ (task, should) => {
+ // Must be stereo to verify output and only need a short duration
+ let context =
+ new OfflineAudioContext(2, 0.25 * sampleRate, sampleRate);
+
+ // Arbitrary position for source and listener. Just so we don't use
+ // defaults positions.
+ let x = 1;
+ let y = 2;
+ let z = 3;
+
+ context.listener.setPosition(x, y, z);
+
+ let src = new OscillatorNode(context);
+ let panner = new PannerNode(context, {
+ panningModel: 'equalpower',
+ positionX: x,
+ positionY: y,
+ positionZ: z
+ });
+
+ src.connect(panner).connect(context.destination);
+
+ src.start();
+
+ context.startRendering()
+ .then(renderedBuffer => {
+ // Verify that both channels have the same data because they
+ // should when the source and listener are at the same
+ // position
+ let c0 = renderedBuffer.getChannelData(0);
+ let c1 = renderedBuffer.getChannelData(1);
+ should(c0, 'Mono: Left and right channels').beEqualToArray(c1);
+ })
+ .then(() => task.done());
+ });
+
+ // Test that a stereo source plays out on both the left and right channels
+ // when the source and listener positions are the same.
+ audit.define(
+ {
+ label: 'stereo source=listener',
+ description: 'Source and listener at the same position'
+ },
+ (task, should) => {
+ // Must be stereo to verify output and only need a short duration.
+ let context =
+ new OfflineAudioContext(2, 0.25 * sampleRate, sampleRate);
+
+ // Arbitrary position for source and listener. Just so we don't use
+ // defaults positions.
+ let x = 1;
+ let y = 2;
+ let z = 3;
+
+ context.listener.setPosition(x, y, z);
+
+ let src = new OscillatorNode(context);
+ let merger = new ChannelMergerNode(context, {numberOfInputs: 2});
+ let panner = new PannerNode(context, {
+ panningModel: 'equalpower',
+ positionX: x,
+ positionY: y,
+ positionZ: z
+ });
+
+ // Make the oscillator a stereo signal (with identical signals on
+ // each channel).
+ src.connect(merger, 0, 0);
+ src.connect(merger, 0, 1);
+
+ merger.connect(panner).connect(context.destination);
+
+ src.start();
+
+ context.startRendering()
+ .then(renderedBuffer => {
+ // Verify that both channels have the same data because they
+ // should when the source and listener are at the same
+ // position.
+ let c0 = renderedBuffer.getChannelData(0);
+ let c1 = renderedBuffer.getChannelData(1);
+ should(c0, 'Stereo: Left and right channels').beEqualToArray(c1);
+ })
+ .then(() => task.done());
+ });
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-rolloff-clamping.html b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-rolloff-clamping.html
new file mode 100644
index 0000000000..387f873010
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/panner-rolloff-clamping.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>
+ Test Clamping of PannerNode rolloffFactor
+ </title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="../../resources/audit-util.js"></script>
+ <script src="../../resources/audit.js"></script>
+ </head>
+ <body>
+ <script id="layout-test-code">
+ // Fairly arbitrary sample rate and render frames.
+ let sampleRate = 16000;
+ let renderFrames = 2048;
+
+ let audit = Audit.createTaskRunner();
+
+ audit.define(
+ {
+ label: 'linear-clamp-high',
+ description: 'rolloffFactor clamping for linear distance model'
+ },
+ (task, should) => {
+ runTest(should, {
+ distanceModel: 'linear',
+ // Fairly arbitrary value outside the nominal range
+ rolloffFactor: 2,
+ clampedRolloff: 1
+ }).then(() => task.done());
+ });
+
+ // Test clamping of the rolloffFactor. The test is done by comparing the
+ // output of a panner with the rolloffFactor set outside the nominal range
+ // against the output of a panner with the rolloffFactor clamped to the
+ // nominal range. The outputs should be the same.
+ //
+ // The |options| dictionary should contain the members
+ // distanceModel - The distance model to use for the panners
+ // rolloffFactor - The desired rolloffFactor. Should be outside the
+ // nominal range of the distance model.
+ // clampedRolloff - The rolloffFactor (above) clamped to the nominal
+ // range for the given distance model.
+ function runTest(should, options) {
+ // Offline context with two channels. The first channel is the panner
+ // node under test. The second channel is the reference panner node.
+ let context = new OfflineAudioContext(2, renderFrames, sampleRate);
+
+ // The source for the panner nodes. This is fairly arbitrary.
+ let src = new OscillatorNode(context, {type: 'sawtooth'});
+
+ // Create the test panner with the specified rolloff factor. The
+ // position is fairly arbitrary, but something that is not the default
+ // is good to show the distance model had some effect.
+ let pannerTest = new PannerNode(context, {
+ rolloffFactor: options.rolloffFactor,
+ distanceModel: options.distanceModel,
+ positionX: 5000
+ });
+
+ // Create the reference panner with the rolloff factor clamped to the
+ // appropriate limit.
+ let pannerRef = new PannerNode(context, {
+ rolloffFactor: options.clampedRolloff,
+ distanceModel: options.distanceModel,
+ positionX: 5000
+ });
+
+
+ // Connect the source to the panners to the destination appropriately.
+ let merger = new ChannelMergerNode(context, {numberOfInputs: 2});
+
+
+ src.connect(pannerTest).connect(merger, 0, 0);
+ src.connect(pannerRef).connect(merger, 0, 1);
+
+ merger.connect(context.destination);
+
+ src.start();
+
+ return context.startRendering().then(function(resultBuffer) {
+ // The two channels should be the same due to the clamping. Verify
+ // that they are the same.
+ let actual = resultBuffer.getChannelData(0);
+ let expected = resultBuffer.getChannelData(1);
+
+ let message = 'Panner distanceModel: "' + options.distanceModel +
+ '", rolloffFactor: ' + options.rolloffFactor;
+
+ should(actual, message).beEqualToArray(expected);
+ });
+ }
+
+ audit.run();
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/pannernode-basic.window.js b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/pannernode-basic.window.js
new file mode 100644
index 0000000000..298fce0f20
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/pannernode-basic.window.js
@@ -0,0 +1,71 @@
+test((t) => {
+ const context = new AudioContext();
+ const source = new ConstantSourceNode(context);
+ const panner = new PannerNode(context);
+ source.connect(panner).connect(context.destination);
+
+ // Basic parameters
+ assert_equals(panner.numberOfInputs,1);
+ assert_equals(panner.numberOfOutputs,1);
+ assert_equals(panner.refDistance, 1);
+ panner.refDistance = 270.5;
+ assert_equals(panner.refDistance, 270.5);
+ assert_equals(panner.maxDistance, 10000);
+ panner.maxDistance = 100.5;
+ assert_equals(panner.maxDistance, 100.5);
+ assert_equals(panner.rolloffFactor, 1);
+ panner.rolloffFactor = 0.75;
+ assert_equals(panner.rolloffFactor, 0.75);
+ assert_equals(panner.coneInnerAngle, 360);
+ panner.coneInnerAngle = 240.5;
+ assert_equals(panner.coneInnerAngle, 240.5);
+ assert_equals(panner.coneOuterAngle, 360);
+ panner.coneOuterAngle = 166.5;
+ assert_equals(panner.coneOuterAngle, 166.5);
+ assert_equals(panner.coneOuterGain, 0);
+ panner.coneOuterGain = 0.25;
+ assert_equals(panner.coneOuterGain, 0.25);
+ assert_equals(panner.panningModel, 'equalpower');
+ assert_equals(panner.distanceModel, 'inverse');
+
+ // Position/orientation AudioParams
+ assert_equals(panner.positionX.value, 0);
+ assert_equals(panner.positionY.value, 0);
+ assert_equals(panner.positionZ.value, 0);
+ assert_equals(panner.orientationX.value, 1);
+ assert_equals(panner.orientationY.value, 0);
+ assert_equals(panner.orientationZ.value, 0);
+
+ // AudioListener
+ assert_equals(context.listener.positionX.value, 0);
+ assert_equals(context.listener.positionY.value, 0);
+ assert_equals(context.listener.positionZ.value, 0);
+ assert_equals(context.listener.forwardX.value, 0);
+ assert_equals(context.listener.forwardY.value, 0);
+ assert_equals(context.listener.forwardZ.value, -1);
+ assert_equals(context.listener.upX.value, 0);
+ assert_equals(context.listener.upY.value, 1);
+ assert_equals(context.listener.upZ.value, 0);
+
+ panner.panningModel = 'equalpower';
+ assert_equals(panner.panningModel, 'equalpower');
+ panner.panningModel = 'HRTF';
+ assert_equals(panner.panningModel, 'HRTF');
+ panner.panningModel = 'invalid';
+ assert_equals(panner.panningModel, 'HRTF');
+
+ // Check that numerical values are no longer supported. We shouldn't
+ // throw and the value shouldn't be changed.
+ panner.panningModel = 1;
+ assert_equals(panner.panningModel, 'HRTF');
+
+ panner.distanceModel = 'linear';
+ assert_equals(panner.distanceModel, 'linear');
+ panner.distanceModel = 'inverse';
+ assert_equals(panner.distanceModel, 'inverse');
+ panner.distanceModel = 'exponential';
+ assert_equals(panner.distanceModel, 'exponential');
+
+ panner.distanceModel = 'invalid';
+ assert_equals(panner.distanceModel, 'exponential');
+}, 'Test the PannerNode interface');
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/pannernode-setposition-throws.html b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/pannernode-setposition-throws.html
new file mode 100644
index 0000000000..2053411943
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/pannernode-setposition-throws.html
@@ -0,0 +1,37 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test PannerNode.setPosition() throws with parameter out of range of float</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+// https://webaudio.github.io/web-audio-api/#dom-pannernode-setposition
+// setPosition(x, y, z) "is equivalent to setting positionX.value,
+// positionY.value, and positionZ.value directly with the given x, y, and z
+// values, respectively." setPosition() parameters are double, but the
+// AudioParam value setter has a float parameter, so out of range values
+// throw.
+const FLT_MAX = 3.40282e+38;
+let panner;
+setup(() => {
+ const ctx = new OfflineAudioContext({length: 1, sampleRate: 24000});
+ panner = ctx.createPanner();
+});
+test(() => {
+ assert_throws_js(TypeError, () => panner.setPosition(2 * FLT_MAX, 0, 0));
+}, "setPosition x");
+test(() => {
+ assert_throws_js(TypeError, () => panner.setPosition(0, -2 * FLT_MAX, 0));
+}, "setPosition y");
+test(() => {
+ assert_throws_js(TypeError, () => panner.setPosition(0, 0, 2 * FLT_MAX));
+}, "setPosition z");
+test(() => {
+ assert_throws_js(TypeError, () => panner.setOrientation(-2 * FLT_MAX, 0, 0));
+}, "setOrientation x");
+test(() => {
+ assert_throws_js(TypeError, () => panner.setOrientation(0, 2 * FLT_MAX, 0));
+}, "setOrientation y");
+test(() => {
+ assert_throws_js(TypeError, () => panner.setOrientation(0, 0, -2 * FLT_MAX));
+}, "setOrientation z");
+</script>
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/test-pannernode-automation.html b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/test-pannernode-automation.html
new file mode 100644
index 0000000000..ce474b10b5
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface/test-pannernode-automation.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+
+// This value is purposefuly not aligned on a 128-block boundary so that we test
+// that the PannerNode position audioparam is a-rate.
+const POSITION_CHANGE_FRAME = 1111;
+
+promise_test(function(t) {
+ var ac = new OfflineAudioContext(2, 2048, 44100);
+ var panner = ac.createPanner();
+ panner.positionX.value = -1;
+ panner.positionY.value = -1;
+ panner.positionZ.value = 1;
+ panner.positionX.setValueAtTime(1, POSITION_CHANGE_FRAME/ac.sampleRate);
+ var osc = ac.createOscillator();
+ osc.connect(panner);
+ panner.connect(ac.destination);
+ osc.start()
+ return ac.startRendering().then(function(buffer) {
+ var left = buffer.getChannelData(0);
+ var right = buffer.getChannelData(1);
+ for (var i = 0; i < 2048; ++i) {
+ if (i < POSITION_CHANGE_FRAME) {
+ assert_true(Math.abs(left[i]) >= Math.abs(right[i]), "index " + i + " should be on the left");
+ } else {
+ assert_true(Math.abs(left[i]) < Math.abs(right[i]), "index " + i + " should be on the right");
+ }
+ }
+ });
+}, "PannerNode AudioParam automation works properly");
+
+</script>