diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /testing/web-platform/tests/webaudio/the-audio-api/the-pannernode-interface | |
parent | Initial commit. (diff) | |
download | thunderbird-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')
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> |