LibWeb/WebAudio: Manage channelCountMode and channelCount for PannerNode

This commit is contained in:
Pavel Shliak 2024-12-17 01:10:55 +04:00 committed by Alexander Kalenik
parent 15a96e841b
commit 884599f1df
Notes: github-actions[bot] 2024-12-18 09:21:00 +00:00
7 changed files with 2555 additions and 0 deletions

View file

@ -163,4 +163,24 @@ WebIDL::ExceptionOr<void> PannerNode::set_orientation(float x, float y, float z)
return {};
}
// https://webaudio.github.io/web-audio-api/#dom-audionode-channelcountmode
WebIDL::ExceptionOr<void> PannerNode::set_channel_count_mode(Bindings::ChannelCountMode mode)
{
if (mode == Bindings::ChannelCountMode::Max) {
return WebIDL::NotSupportedError::create(realm(), "PannerNode does not support 'max' as channelCountMode."_string);
}
return AudioNode::set_channel_count_mode(mode);
}
// https://webaudio.github.io/web-audio-api/#dom-audionode-channelcount
WebIDL::ExceptionOr<void> PannerNode::set_channel_count(WebIDL::UnsignedLong channel_count)
{
if (channel_count > 2) {
return WebIDL::NotSupportedError::create(realm(), "PannerNode does not support channel count greater than 2"_string);
}
return AudioNode::set_channel_count(channel_count);
}
}

View file

@ -77,6 +77,10 @@ public:
WebIDL::ExceptionOr<void> set_position(float x, float y, float z);
WebIDL::ExceptionOr<void> set_orientation(float x, float y, float z);
// ^AudioNode
virtual WebIDL::ExceptionOr<void> set_channel_count(WebIDL::UnsignedLong) override;
virtual WebIDL::ExceptionOr<void> set_channel_count_mode(Bindings::ChannelCountMode) override;
protected:
PannerNode(JS::Realm&, GC::Ref<BaseAudioContext>, PannerOptions const& = {});

View file

@ -0,0 +1,131 @@
Harness status: OK
Found 125 tests
121 Pass
4 Fail
Pass # AUDIT TASK RUNNER STARTED.
Pass Executing "initialize"
Pass Executing "invalid constructor"
Pass Executing "default constructor"
Pass Executing "test AudioNodeOptions"
Pass Executing "constructor with options"
Pass Audit report
Pass > [initialize]
Pass context = new OfflineAudioContext(...) did not throw an exception.
Pass < [initialize] All assertions passed. (total 1 assertions)
Pass > [invalid constructor]
Pass new PannerNode() threw TypeError: "PannerNode() needs one argument".
Pass new PannerNode(1) threw TypeError: "Not an object of type BaseAudioContext".
Pass new PannerNode(context, 42) threw TypeError: "Not an object of type PannerOptions".
Pass < [invalid constructor] All assertions passed. (total 3 assertions)
Pass > [default constructor]
Pass node0 = new PannerNode(context) did not throw an exception.
Pass node0 instanceof PannerNode is equal to true.
Pass node0.numberOfInputs is equal to 1.
Pass node0.numberOfOutputs is equal to 1.
Pass node0.channelCount is equal to 2.
Pass node0.channelCountMode is equal to clamped-max.
Pass node0.channelInterpretation is equal to speakers.
Pass node0.panningModel is equal to equalpower.
Pass node0.positionX.value is equal to 0.
Pass node0.positionY.value is equal to 0.
Pass node0.positionZ.value is equal to 0.
Pass node0.orientationX.value is equal to 1.
Pass node0.orientationY.value is equal to 0.
Pass node0.orientationZ.value is equal to 0.
Pass node0.distanceModel is equal to inverse.
Pass node0.refDistance is equal to 1.
Pass node0.maxDistance is equal to 10000.
Pass node0.rolloffFactor is equal to 1.
Pass node0.coneInnerAngle is equal to 360.
Pass node0.coneOuterAngle is equal to 360.
Pass node0.coneOuterGain is equal to 0.
Pass context.listener.positionX.value is equal to 0.
Pass context.listener.positionY.value is equal to 0.
Pass context.listener.positionZ.value is equal to 0.
Pass context.listener.forwardX.value is equal to 0.
Pass context.listener.forwardY.value is equal to 0.
Pass context.listener.forwardZ.value is equal to -1.
Pass context.listener.upX.value is equal to 0.
Pass context.listener.upY.value is equal to 1.
Pass context.listener.upZ.value is equal to 0.
Pass < [default constructor] All assertions passed. (total 30 assertions)
Pass > [test AudioNodeOptions]
Pass node1 = new PannerNode(c, {"channelCount":1}) did not throw an exception.
Pass node1.channelCount is equal to 1.
Pass node2 = new PannerNode(c, {"channelCount":2}) did not throw an exception.
Pass node2.channelCount is equal to 2.
Pass new PannerNode(c, {"channelCount":0}) threw NotSupportedError: "Invalid channel count".
Pass node.channelCount = 0 threw NotSupportedError: "Invalid channel count".
Pass node.channelCount after setting to 0 is equal to 2.
Pass new PannerNode(c, {"channelCount":3}) threw NotSupportedError: "PannerNode does not support channel count greater than 2".
Pass node.channelCount = 3 threw NotSupportedError: "PannerNode does not support channel count greater than 2".
Pass node.channelCount after setting to 3 is equal to 2.
Pass new PannerNode(c, {"channelCount":99}) threw NotSupportedError: "PannerNode does not support channel count greater than 2".
Pass node.channelCount = 99 threw NotSupportedError: "PannerNode does not support channel count greater than 2".
Pass node.channelCount after setting to 99 is equal to 2.
Pass node3 = new PannerNode(c, {"channelCountMode":"clamped-max"}) did not throw an exception.
Pass node3.channelCountMode is equal to clamped-max.
Pass node4 = new PannerNode(c, {"channelCountMode":"explicit"}) did not throw an exception.
Pass node4.channelCountMode is equal to explicit.
Pass new PannerNode(c, {"channelCountMode":"max"}) threw NotSupportedError: "PannerNode does not support 'max' as channelCountMode.".
Pass node.channelCountMode = max threw NotSupportedError: "PannerNode does not support 'max' as channelCountMode.".
Pass node.channelCountMode after setting to max is equal to clamped-max.
Pass new PannerNode(c, " + JSON.stringify(options) + ") threw TypeError: "Invalid value 'foobar' for enumeration type 'ChannelCountMode'".
Pass node.channelCountMode = foobar did not throw an exception.
Pass node.channelCountMode after setting to foobar is equal to clamped-max.
Pass node5 = new PannerNode(c, {"channelInterpretation":"speakers"}) did not throw an exception.
Pass node5.channelInterpretation is equal to speakers.
Pass node6 = new PannerNode(c, {"channelInterpretation":"discrete"}) did not throw an exception.
Pass node6.channelInterpretation is equal to discrete.
Pass new PannerNode(c, {"channelInterpretation":"foobar"}) threw TypeError: "Invalid value 'foobar' for enumeration type 'ChannelInterpretation'".
Pass new PannerNode(c, {"maxDistance":-1}) threw RangeError: "maxDistance cannot be negative".
Pass node.maxDistance = -1 threw RangeError: "maxDistance cannot be negative".
Pass node.maxDistance after setting to -1 is equal to 10000.
Pass node7 = new PannerNode(c, {"maxDistance":100}) did not throw an exception.
Pass node7.maxDistance is equal to 100.
Pass new PannerNode(c, {"rolloffFactor":-1}) threw RangeError: "rolloffFactor cannot be negative".
Pass node.rolloffFactor = -1 threw RangeError: "rolloffFactor cannot be negative".
Pass node.rolloffFactor after setting to -1 is equal to 1.
Pass node8 = new PannerNode(c, {"rolloffFactor":0}) did not throw an exception.
Pass node8.rolloffFactor is equal to 0.
Pass node8 = new PannerNode(c, {"rolloffFactor":0.5}) did not throw an exception.
Pass node8.rolloffFactor is equal to 0.5.
Pass node8 = new PannerNode(c, {"rolloffFactor":100}) did not throw an exception.
Pass node8.rolloffFactor is equal to 100.
Pass new PannerNode(c, {"coneOuterGain":-1}) threw InvalidStateError: "coneOuterGain must be in the range of [0, 1]".
Pass node.coneOuterGain = -1 threw InvalidStateError: "coneOuterGain must be in the range of [0, 1]".
Pass node.coneOuterGain after setting to -1 is equal to 0.
Pass new PannerNode(c, {"coneOuterGain":1.1}) threw InvalidStateError: "coneOuterGain must be in the range of [0, 1]".
Pass node.coneOuterGain = 1.1 threw InvalidStateError: "coneOuterGain must be in the range of [0, 1]".
Pass node.coneOuterGain after setting to 1.1 is equal to 0.
Pass node9 = new PannerNode(c, {"coneOuterGain":0}) did not throw an exception.
Pass node9.coneOuterGain is equal to 0.
Pass node9 = new PannerNode(c, {"coneOuterGain":0.5}) did not throw an exception.
Pass node9.coneOuterGain is equal to 0.5.
Pass node9 = new PannerNode(c, {"coneOuterGain":1}) did not throw an exception.
Pass node9.coneOuterGain is equal to 1.
Pass < [test AudioNodeOptions] All assertions passed. (total 54 assertions)
Pass > [constructor with options]
Pass node = new PannerNode(c, {"panningModel":"HRTF","positionX":1.4142135623730951,"positionY":2.8284271247461903,"positionZ":4.242640687119286,"orientationX":-1.4142135623730951,"orientationY":-2.8284271247461903,"orientationZ":-4.242640687119286,"distanceModel":"linear","refDistance":3.141592653589793,"maxDistance":6.283185307179586,"rolloffFactor":9.42477796076938,"coneInnerAngle":12.566370614359172,"coneOuterAngle":15.707963267948966,"coneOuterGain":0.3141592653589793}) did not throw an exception.
Pass node instanceof PannerNode is equal to true.
Fail X node.panningModel is not equal to HRTF. Got equalpower.
Pass node.positionX.value is equal to 1.4142135381698608.
Pass node.positionY.value is equal to 2.8284270763397217.
Pass node.positionZ.value is equal to 4.242640495300293.
Pass node.orientationX.value is equal to -1.4142135381698608.
Pass node.orientationY.value is equal to -2.8284270763397217.
Pass node.orientationZ.value is equal to -4.242640495300293.
Fail X node.distanceModel is not equal to linear. Got inverse.
Pass node.refDistance is equal to 3.141592653589793.
Pass node.maxDistance is equal to 6.283185307179586.
Pass node.rolloffFactor is equal to 9.42477796076938.
Pass node.coneInnerAngle is equal to 12.566370614359172.
Pass node.coneOuterAngle is equal to 15.707963267948966.
Pass node.coneOuterGain is equal to 0.3141592653589793.
Pass node.channelCount is equal to 2.
Pass node.channelCountMode is equal to clamped-max.
Pass node.channelInterpretation is equal to speakers.
Fail < [constructor with options] 2 out of 19 assertions were failed.
Fail # AUDIT TASK RUNNER FINISHED: 1 out of 5 tasks were failed.

View file

@ -0,0 +1,292 @@
// Test that constructor for the node with name |nodeName| handles the
// various possible values for channelCount, channelCountMode, and
// channelInterpretation.
// The |should| parameter is the test function from new |Audit|.
function testAudioNodeOptions(should, context, nodeName, expectedNodeOptions) {
if (expectedNodeOptions === undefined)
expectedNodeOptions = {};
let node;
// Test that we can set channelCount and that errors are thrown for
// invalid values
let testChannelCount = 17;
if (expectedNodeOptions.channelCount) {
testChannelCount = expectedNodeOptions.channelCount.value;
}
should(
() => {
node = new window[nodeName](
context, Object.assign({}, expectedNodeOptions.additionalOptions, {
channelCount: testChannelCount
}));
},
'new ' + nodeName + '(c, {channelCount: ' + testChannelCount + '})')
.notThrow();
should(node.channelCount, 'node.channelCount').beEqualTo(testChannelCount);
if (expectedNodeOptions.channelCount &&
expectedNodeOptions.channelCount.isFixed) {
// The channel count is fixed. Verify that we throw an error if
// we try to change it. Arbitrarily set the count to be one more
// than the expected value.
testChannelCount = expectedNodeOptions.channelCount.value + 1;
should(
() => {
node = new window[nodeName](
context,
Object.assign(
{}, expectedNodeOptions.additionalOptions,
{channelCount: testChannelCount}));
},
'new ' + nodeName + '(c, {channelCount: ' + testChannelCount + '})')
.throw(DOMException,
expectedNodeOptions.channelCount.exceptionType);
// And test that setting it to the fixed value does not throw.
testChannelCount = expectedNodeOptions.channelCount.value;
should(
() => {
node = new window[nodeName](
context,
Object.assign(
{}, expectedNodeOptions.additionalOptions,
{channelCount: testChannelCount}));
node.channelCount = testChannelCount;
},
'(new ' + nodeName + '(c, {channelCount: ' + testChannelCount + '})).channelCount = ' + testChannelCount)
.notThrow();
} else {
// The channel count is not fixed. Try to set the count to invalid
// values and make sure an error is thrown.
[0, 99].forEach(testValue => {
should(() => {
node = new window[nodeName](
context, Object.assign({}, expectedNodeOptions.additionalOptions, {
channelCount: testValue
}));
}, `new ${nodeName}(c, {channelCount: ${testValue}})`)
.throw(DOMException, 'NotSupportedError');
});
}
// Test channelCountMode
let testChannelCountMode = 'max';
if (expectedNodeOptions.channelCountMode) {
testChannelCountMode = expectedNodeOptions.channelCountMode.value;
}
should(
() => {
node = new window[nodeName](
context, Object.assign({}, expectedNodeOptions.additionalOptions, {
channelCountMode: testChannelCountMode
}));
},
'new ' + nodeName + '(c, {channelCountMode: "' + testChannelCountMode +
'"}')
.notThrow();
should(node.channelCountMode, 'node.channelCountMode')
.beEqualTo(testChannelCountMode);
if (expectedNodeOptions.channelCountMode &&
expectedNodeOptions.channelCountMode.isFixed) {
// Channel count mode is fixed. Test setting to something else throws.
['max', 'clamped-max', 'explicit'].forEach(testValue => {
if (testValue !== expectedNodeOptions.channelCountMode.value) {
should(
() => {
node = new window[nodeName](
context,
Object.assign(
{}, expectedNodeOptions.additionalOptions,
{channelCountMode: testValue}));
},
`new ${nodeName}(c, {channelCountMode: "${testValue}"})`)
.throw(DOMException,
expectedNodeOptions.channelCountMode.exceptionType);
} else {
// Test that explicitly setting the the fixed value is allowed.
should(
() => {
node = new window[nodeName](
context,
Object.assign(
{}, expectedNodeOptions.additionalOptions,
{channelCountMode: testValue}));
node.channelCountMode = testValue;
},
`(new ${nodeName}(c, {channelCountMode: "${testValue}"})).channelCountMode = "${testValue}"`)
.notThrow();
}
});
} else {
// Mode is not fixed. Verify that we can set the mode to all valid
// values, and that we throw for invalid values.
let testValues = ['max', 'clamped-max', 'explicit'];
testValues.forEach(testValue => {
should(() => {
node = new window[nodeName](
context, Object.assign({}, expectedNodeOptions.additionalOptions, {
channelCountMode: testValue
}));
}, `new ${nodeName}(c, {channelCountMode: "${testValue}"})`).notThrow();
should(
node.channelCountMode, 'node.channelCountMode after valid setter')
.beEqualTo(testValue);
});
should(
() => {
node = new window[nodeName](
context,
Object.assign(
{}, expectedNodeOptions.additionalOptions,
{channelCountMode: 'foobar'}));
},
'new ' + nodeName + '(c, {channelCountMode: "foobar"}')
.throw(TypeError);
should(node.channelCountMode, 'node.channelCountMode after invalid setter')
.beEqualTo(testValues[testValues.length - 1]);
}
// Test channelInterpretation
if (expectedNodeOptions.channelInterpretation &&
expectedNodeOptions.channelInterpretation.isFixed) {
// The channel interpretation is fixed. Verify that we throw an
// error if we try to change it.
['speakers', 'discrete'].forEach(testValue => {
if (testValue !== expectedNodeOptions.channelInterpretation.value) {
should(
() => {
node = new window[nodeName](
context,
Object.assign(
{}, expectedNodeOptions.additionOptions,
{channelInterpretation: testValue}));
},
`new ${nodeName}(c, {channelInterpretation: "${testValue}"})`)
.throw(DOMException,
expectedNodeOptions.channelCountMode.exceptionType);
} else {
// Check that assigning the fixed value is OK.
should(
() => {
node = new window[nodeName](
context,
Object.assign(
{}, expectedNodeOptions.additionOptions,
{channelInterpretation: testValue}));
node.channelInterpretation = testValue;
},
`(new ${nodeName}(c, {channelInterpretation: "${testValue}"})).channelInterpretation = "${testValue}"`)
.notThrow();
}
});
} else {
// Channel interpretation is not fixed. Verify that we can set it
// to all possible values.
should(
() => {
node = new window[nodeName](
context,
Object.assign(
{}, expectedNodeOptions.additionalOptions,
{channelInterpretation: 'speakers'}));
},
'new ' + nodeName + '(c, {channelInterpretation: "speakers"})')
.notThrow();
should(node.channelInterpretation, 'node.channelInterpretation')
.beEqualTo('speakers');
should(
() => {
node = new window[nodeName](
context,
Object.assign(
{}, expectedNodeOptions.additionalOptions,
{channelInterpretation: 'discrete'}));
},
'new ' + nodeName + '(c, {channelInterpretation: "discrete"})')
.notThrow();
should(node.channelInterpretation, 'node.channelInterpretation')
.beEqualTo('discrete');
should(
() => {
node = new window[nodeName](
context,
Object.assign(
{}, expectedNodeOptions.additionalOptions,
{channelInterpretation: 'foobar'}));
},
'new ' + nodeName + '(c, {channelInterpretation: "foobar"})')
.throw(TypeError);
should(
node.channelInterpretation,
'node.channelInterpretation after invalid setter')
.beEqualTo('discrete');
}
}
function initializeContext(should) {
let c;
should(() => {
c = new OfflineAudioContext(1, 1, 48000);
}, 'context = new OfflineAudioContext(...)').notThrow();
return c;
}
function testInvalidConstructor(should, name, context) {
should(() => {
new window[name]();
}, 'new ' + name + '()').throw(TypeError);
should(() => {
new window[name](1);
}, 'new ' + name + '(1)').throw(TypeError);
should(() => {
new window[name](context, 42);
}, 'new ' + name + '(context, 42)').throw(TypeError);
}
function testDefaultConstructor(should, name, context, options) {
let node;
let message = options.prefix + ' = new ' + name + '(context';
if (options.constructorOptions)
message += ', ' + JSON.stringify(options.constructorOptions);
message += ')'
should(() => {
node = new window[name](context, options.constructorOptions);
}, message).notThrow();
should(node instanceof window[name], options.prefix + ' instanceof ' + name)
.beEqualTo(true);
should(node.numberOfInputs, options.prefix + '.numberOfInputs')
.beEqualTo(options.numberOfInputs);
should(node.numberOfOutputs, options.prefix + '.numberOfOutputs')
.beEqualTo(options.numberOfOutputs);
should(node.channelCount, options.prefix + '.channelCount')
.beEqualTo(options.channelCount);
should(node.channelCountMode, options.prefix + '.channelCountMode')
.beEqualTo(options.channelCountMode);
should(node.channelInterpretation, options.prefix + '.channelInterpretation')
.beEqualTo(options.channelInterpretation);
return node;
}
function testDefaultAttributes(should, node, prefix, items) {
items.forEach((item) => {
let attr = node[item.name];
if (attr instanceof AudioParam) {
should(attr.value, prefix + '.' + item.name + '.value')
.beEqualTo(item.value);
} else {
should(attr, prefix + '.' + item.name).beEqualTo(item.value);
}
});
}

View file

@ -0,0 +1,195 @@
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileOverview This file includes legacy utility functions for the layout
* test.
*/
// How many frames in a WebAudio render quantum.
let RENDER_QUANTUM_FRAMES = 128;
// Compare two arrays (commonly extracted from buffer.getChannelData()) with
// constraints:
// options.thresholdSNR: Minimum allowed SNR between the actual and expected
// signal. The default value is 10000.
// options.thresholdDiffULP: Maximum allowed difference between the actual
// and expected signal in ULP(Unit in the last place). The default is 0.
// options.thresholdDiffCount: Maximum allowed number of sample differences
// which exceeds the threshold. The default is 0.
// options.bitDepth: The expected result is assumed to come from an audio
// file with this number of bits of precision. The default is 16.
function compareBuffersWithConstraints(should, actual, expected, options) {
if (!options)
options = {};
// Only print out the message if the lengths are different; the
// expectation is that they are the same, so don't clutter up the
// output.
if (actual.length !== expected.length) {
should(
actual.length === expected.length,
'Length of actual and expected buffers should match')
.beTrue();
}
let maxError = -1;
let diffCount = 0;
let errorPosition = -1;
let thresholdSNR = (options.thresholdSNR || 10000);
let thresholdDiffULP = (options.thresholdDiffULP || 0);
let thresholdDiffCount = (options.thresholdDiffCount || 0);
// By default, the bit depth is 16.
let bitDepth = (options.bitDepth || 16);
let scaleFactor = Math.pow(2, bitDepth - 1);
let noisePower = 0, signalPower = 0;
for (let i = 0; i < actual.length; i++) {
let diff = actual[i] - expected[i];
noisePower += diff * diff;
signalPower += expected[i] * expected[i];
if (Math.abs(diff) > maxError) {
maxError = Math.abs(diff);
errorPosition = i;
}
// The reference file is a 16-bit WAV file, so we will almost never get
// an exact match between it and the actual floating-point result.
if (Math.abs(diff) > scaleFactor)
diffCount++;
}
let snr = 10 * Math.log10(signalPower / noisePower);
let maxErrorULP = maxError * scaleFactor;
should(snr, 'SNR').beGreaterThanOrEqualTo(thresholdSNR);
should(
maxErrorULP,
options.prefix + ': Maximum difference (in ulp units (' + bitDepth +
'-bits))')
.beLessThanOrEqualTo(thresholdDiffULP);
should(diffCount, options.prefix + ': Number of differences between results')
.beLessThanOrEqualTo(thresholdDiffCount);
}
// Create an impulse in a buffer of length sampleFrameLength
function createImpulseBuffer(context, sampleFrameLength) {
let audioBuffer =
context.createBuffer(1, sampleFrameLength, context.sampleRate);
let n = audioBuffer.length;
let dataL = audioBuffer.getChannelData(0);
for (let k = 0; k < n; ++k) {
dataL[k] = 0;
}
dataL[0] = 1;
return audioBuffer;
}
// Create a buffer of the given length with a linear ramp having values 0 <= x <
// 1.
function createLinearRampBuffer(context, sampleFrameLength) {
let audioBuffer =
context.createBuffer(1, sampleFrameLength, context.sampleRate);
let n = audioBuffer.length;
let dataL = audioBuffer.getChannelData(0);
for (let i = 0; i < n; ++i)
dataL[i] = i / n;
return audioBuffer;
}
// Create an AudioBuffer of length |sampleFrameLength| having a constant value
// |constantValue|. If |constantValue| is a number, the buffer has one channel
// filled with that value. If |constantValue| is an array, the buffer is created
// wit a number of channels equal to the length of the array, and channel k is
// filled with the k'th element of the |constantValue| array.
function createConstantBuffer(context, sampleFrameLength, constantValue) {
let channels;
let values;
if (typeof constantValue === 'number') {
channels = 1;
values = [constantValue];
} else {
channels = constantValue.length;
values = constantValue;
}
let audioBuffer =
context.createBuffer(channels, sampleFrameLength, context.sampleRate);
let n = audioBuffer.length;
for (let c = 0; c < channels; ++c) {
let data = audioBuffer.getChannelData(c);
for (let i = 0; i < n; ++i)
data[i] = values[c];
}
return audioBuffer;
}
// Create a stereo impulse in a buffer of length sampleFrameLength
function createStereoImpulseBuffer(context, sampleFrameLength) {
let audioBuffer =
context.createBuffer(2, sampleFrameLength, context.sampleRate);
let n = audioBuffer.length;
let dataL = audioBuffer.getChannelData(0);
let dataR = audioBuffer.getChannelData(1);
for (let k = 0; k < n; ++k) {
dataL[k] = 0;
dataR[k] = 0;
}
dataL[0] = 1;
dataR[0] = 1;
return audioBuffer;
}
// Convert time (in seconds) to sample frames.
function timeToSampleFrame(time, sampleRate) {
return Math.floor(0.5 + time * sampleRate);
}
// Compute the number of sample frames consumed by noteGrainOn with
// the specified |grainOffset|, |duration|, and |sampleRate|.
function grainLengthInSampleFrames(grainOffset, duration, sampleRate) {
let startFrame = timeToSampleFrame(grainOffset, sampleRate);
let endFrame = timeToSampleFrame(grainOffset + duration, sampleRate);
return endFrame - startFrame;
}
// True if the number is not an infinity or NaN
function isValidNumber(x) {
return !isNaN(x) && (x != Infinity) && (x != -Infinity);
}
// Compute the (linear) signal-to-noise ratio between |actual| and
// |expected|. The result is NOT in dB! If the |actual| and
// |expected| have different lengths, the shorter length is used.
function computeSNR(actual, expected) {
let signalPower = 0;
let noisePower = 0;
let length = Math.min(actual.length, expected.length);
for (let k = 0; k < length; ++k) {
let diff = actual[k] - expected[k];
signalPower += expected[k] * expected[k];
noisePower += diff * diff;
}
return signalPower / noisePower;
}

File diff suppressed because it is too large Load diff

View file

@ -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>