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