blob: 45b27496d71b38ee01e92373e1c3827978cdec73 [file] [log] [blame]
<!DOCTYPE html>
<html>
<head>
<title>
Test AudioParam Nominal Range Values
</title>
<script src="../../imported/w3c/web-platform-tests/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">
// Some arbitrary sample rate for the offline context.
let sampleRate = 48000;
// The actual contexts to use. Generally use the offline context for
// testing except for the media nodes which require an AudioContext.
let offlineContext;
let audioContext;
// The set of all methods that we've tested for verifying that we tested
// all of the necessary objects.
let testedMethods = new Set();
// The most positive single float value (the value just before infinity).
// Be careful when changing this value! Javascript only uses double
// floats, so the value here should be the max single-float value,
// converted directly to a double-float value. This also depends on
// Javascript reading this value and producing the desired double-float
// value correctly.
let mostPositiveFloat = 3.4028234663852886e38;
let audit = Audit.createTaskRunner();
// Array describing the tests that should be run. |testOfflineConfigs| is
// for tests that can use an offline context. |testOnlineConfigs| is for
// tests that need to use an online context. Offline contexts are
// preferred when possible.
let testOfflineConfigs = [
{
// The name of the method to create the particular node to be tested.
creator: 'createGain',
// Any args to pass to the creator function.
args: [],
// The min/max limits for each AudioParam of the node. This is a
// dictionary whose keys are
// the names of each AudioParam in the node. Don't define this if the
// node doesn't have any
// AudioParam attributes.
limits: {
gain: {
// The expected min and max values for this AudioParam.
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat
}
}
},
{
creator: 'createDelay',
// Just specify a non-default value for the maximum delay so we can
// make sure the limits are
// set correctly.
args: [1.5],
limits: {delayTime: {minValue: 0, maxValue: 1.5}}
},
{
creator: 'createBufferSource',
args: [],
limits: {
playbackRate:
{minValue: -mostPositiveFloat, maxValue: mostPositiveFloat},
detune: {minValue: -mostPositiveFloat, maxValue: mostPositiveFloat}
}
},
{
creator: 'createStereoPanner',
args: [],
limits: {pan: {minValue: -1, maxValue: 1}}
},
{
creator: 'createDynamicsCompressor',
args: [],
// Do not set limits for reduction; it's currently an AudioParam but
// should be a float.
// So let the test fail for reduction. When reduction is changed,
// this test will then
// correctly pass.
limits: {
threshold: {minValue: -100, maxValue: 0},
knee: {minValue: 0, maxValue: 40},
ratio: {minValue: 1, maxValue: 20},
attack: {minValue: 0, maxValue: 1},
release: {minValue: 0, maxValue: 1}
}
},
{
creator: 'createBiquadFilter',
args: [],
limits: {
gain: {
minValue: -mostPositiveFloat,
// This complicated expression is used to get all the arithmetic
// to round to the correct single-precision float value for the
// desired max. This also assumes that the implication computes
// the limit as 40 * log10f(std::numeric_limits<float>::max()).
maxValue:
Math.fround(40 * Math.fround(Math.log10(mostPositiveFloat)))
},
Q: {minValue: -mostPositiveFloat, maxValue: mostPositiveFloat},
frequency: {minValue: 0, maxValue: sampleRate / 2},
detune: {
minValue: -Math.fround(1200 * Math.log2(mostPositiveFloat)),
maxValue: Math.fround(1200 * Math.log2(mostPositiveFloat))
}
}
},
{
creator: 'createOscillator',
args: [],
limits: {
frequency: {minValue: -sampleRate / 2, maxValue: sampleRate / 2},
detune: {
minValue: -Math.fround(1200 * Math.log2(mostPositiveFloat)),
maxValue: Math.fround(1200 * Math.log2(mostPositiveFloat))
}
}
},
{
creator: 'createPanner',
args: [],
limits: {
positionX: {
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat,
},
positionY: {
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat,
},
positionZ: {
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat,
},
orientationX: {
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat,
},
orientationY: {
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat,
},
orientationZ: {
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat,
}
},
},
{
creator: 'createConstantSource',
args: [],
limits: {
offset: {minValue: -mostPositiveFloat, maxValue: mostPositiveFloat}
}
},
// These nodes don't have AudioParams, but we want to test them anyway.
// Any arguments for the
// constructor are pretty much arbitrary; they just need to be valid.
{
creator: 'createBuffer',
args: [1, 1, sampleRate],
},
{creator: 'createIIRFilter', args: [[1, 2], [1, .9]]},
{
creator: 'createWaveShaper',
args: [],
},
{
creator: 'createConvolver',
args: [],
},
{
creator: 'createAnalyser',
args: [],
},
{
creator: 'createScriptProcessor',
args: [0],
},
{
creator: 'createPeriodicWave',
args: [Float32Array.from([0, 0]), Float32Array.from([1, 0])],
},
{
creator: 'createChannelSplitter',
args: [],
},
{
creator: 'createChannelMerger',
args: [],
},
];
let testOnlineConfigs = [
{creator: 'createMediaElementSource', args: [new Audio()]},
{creator: 'createMediaStreamDestination', args: []}
// Can't currently test MediaStreamSource because we're using an offline
// context.
];
// Create the contexts so we can use it in the following test.
audit.define('initialize', (task, should) => {
// Just any context so that we can create the nodes.
should(() => {
offlineContext = new OfflineAudioContext(1, 1, sampleRate);
}, 'Create offline context for tests').notThrow();
should(() => {
onlineContext = new AudioContext();
}, 'Create online context for tests').notThrow();
task.done();
});
// Create a task for each entry in testOfflineConfigs
for (let test in testOfflineConfigs) {
let config = testOfflineConfigs[test]
audit.define('Offline ' + config.creator, (function(c) {
return (task, should) => {
let node = offlineContext[c.creator](...c.args);
testLimits(should, c.creator, node, c.limits);
task.done();
};
})(config));
}
for (let test in testOnlineConfigs) {
let config = testOnlineConfigs[test]
audit.define('Online ' + config.creator, (function(c) {
return (task, should) => {
let node = onlineContext[c.creator](...c.args);
testLimits(should, c.creator, node, c.limits);
task.done();
};
})(config));
}
// Test the AudioListener params that were added for the automated Panner
audit.define('AudioListener', (task, should) => {
testLimits(should, '', offlineContext.listener, {
positionX: {
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat,
},
positionY: {
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat,
},
positionZ: {
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat,
},
forwardX: {
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat,
},
forwardY: {
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat,
},
forwardZ: {
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat,
},
upX: {
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat,
},
upY: {
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat,
},
upZ: {
minValue: -mostPositiveFloat,
maxValue: mostPositiveFloat,
}
});
task.done();
});
// Verify that we have tested all the create methods available on the
// context.
audit.define('verifyTests', (task, should) => {
let allNodes = new Set();
// Create the set of all "create" methods from the context.
for (let method in offlineContext) {
if (typeof offlineContext[method] === 'function' &&
method.substring(0, 6) === 'create') {
allNodes.add(method);
}
}
// Compute the difference between the set of all create methods on the
// context and the set of tests that we've run.
let diff = new Set([...allNodes].filter(x => !testedMethods.has(x)));
// Can't currently test a MediaStreamSourceNode, so remove it from the
// diff set.
diff.delete('createMediaStreamSource');
// It's a test failure if we didn't test all of the create methods in
// the context (except createMediaStreamSource, of course).
let output = [];
if (diff.size) {
for (let item of diff)
output.push(' ' + item.substring(6));
}
should(output.length === 0, 'Number of nodes not tested')
.message(': 0', ': ' + output);
task.done();
});
// Simple test of a few automation methods to verify we get warnings.
audit.define('automation', (task, should) => {
// Just use a DelayNode for testing because the audio param has finite
// limits.
should(() => {
let d = offlineContext.createDelay();
// The console output should have the warnings that we're interested
// in.
d.delayTime.setValueAtTime(-1, 0);
d.delayTime.linearRampToValueAtTime(2, 1);
d.delayTime.exponentialRampToValueAtTime(3, 2);
d.delayTime.setTargetAtTime(-1, 3, .1);
d.delayTime.setValueCurveAtTime(
Float32Array.from([.1, .2, 1.5, -1]), 4, .1);
}, 'Test automations (check console logs)').notThrow();
task.done();
});
audit.run();
// Is |object| an AudioParam? We determine this by checking the
// constructor name.
function isAudioParam(object) {
return object && object.constructor.name === 'AudioParam';
}
// Does |limitOptions| exist and does it have valid values for the
// expected min and max values?
function hasValidLimits(limitOptions) {
return limitOptions && (typeof limitOptions.minValue === 'number') &&
(typeof limitOptions.maxValue === 'number');
}
// Check the min and max values for the AudioParam attribute named
// |paramName| for the |node|. The expected limits is given by the
// dictionary |limits|. If some test fails, add the name of the failed
function validateAudioParamLimits(should, node, paramName, limits) {
let nodeName = node.constructor.name;
let parameter = node[paramName];
let prefix = nodeName + '.' + paramName;
let success = true;
if (hasValidLimits(limits[paramName])) {
// Verify that the min and max values for the parameter are correct.
let isCorrect = should(parameter.minValue, prefix + '.minValue')
.beEqualTo(limits[paramName].minValue);
isCorrect = should(parameter.maxValue, prefix + '.maxValue')
.beEqualTo(limits[paramName].maxValue) &&
isCorrect;
// Verify that the min and max attributes are read-only. |testValue|
// MUST be a number that can be represented exactly the same way as
// both a double and single float. A small integer works nicely.
const testValue = 42;
parameter.minValue = testValue;
let isReadOnly;
isReadOnly =
should(parameter.minValue, `${prefix}.minValue = ${testValue}`)
.notBeEqualTo(testValue);
should(isReadOnly, prefix + '.minValue is read-only').beEqualTo(true);
isCorrect = isReadOnly && isCorrect;
parameter.maxValue = testValue;
isReadOnly =
should(parameter.maxValue, `${prefix}.maxValue = ${testValue}`)
.notBeEqualTo(testValue);
should(isReadOnly, prefix + '.maxValue is read-only').beEqualTo(true);
isCorrect = isReadOnly && isCorrect;
// Now try to set the parameter outside the nominal range.
let newValue = 2 * limits[paramName].minValue - 1;
let isClipped = true;
let clippingTested = false;
// If the new value is beyond float the largest single-precision
// float, skip the test because Chrome throws an error.
if (newValue >= -mostPositiveFloat) {
parameter.value = newValue;
clippingTested = true;
isClipped =
should(
parameter.value, 'Set ' + prefix + '.value = ' + newValue)
.beEqualTo(parameter.minValue) &&
isClipped;
}
newValue = 2 * limits[paramName].maxValue + 1;
if (newValue <= mostPositiveFloat) {
parameter.value = newValue;
clippingTested = true;
isClipped =
should(
parameter.value, 'Set ' + prefix + '.value = ' + newValue)
.beEqualTo(parameter.maxValue) &&
isClipped;
}
if (clippingTested) {
should(
isClipped,
prefix + ' was clipped to lie within the nominal range')
.beEqualTo(true);
}
isCorrect = isCorrect && isClipped;
success = isCorrect && success;
} else {
// Test config didn't specify valid limits. Fail this test!
should(
clippingTested,
'Limits for ' + nodeName + '.' + paramName +
' were correctly defined')
.beEqualTo(false);
success = false;
}
return success;
}
// Test all of the AudioParams for |node| using the expected values in
// |limits|. |creatorName| is the name of the method to create the node,
// and is used to keep trakc of which tests we've run.
function testLimits(should, creatorName, node, limits) {
let nodeName = node.constructor.name;
testedMethods.add(creatorName);
let success = true;
// List of all of the AudioParams that were tested.
let audioParams = [];
// List of AudioParams that failed the test.
let incorrectParams = [];
// Look through all of the keys for the node and extract just the
// AudioParams
Object.keys(node.__proto__).forEach(function(paramName) {
if (isAudioParam(node[paramName])) {
audioParams.push(paramName);
let isValid = validateAudioParamLimits(
should, node, paramName, limits, incorrectParams);
if (!isValid)
incorrectParams.push(paramName);
success = isValid && success;
}
});
// Print an appropriate message depending on whether there were
// AudioParams defined or not.
if (audioParams.length) {
let message =
'Nominal ranges for AudioParam(s) of ' + node.constructor.name;
should(success, message)
.message('are correct', 'are incorrect for: ' + +incorrectParams);
return success;
} else {
should(!limits, nodeName)
.message(
'has no AudioParams as expected',
'has no AudioParams but test expected ' + limits);
}
}
</script>
</body>
</html>