audioparam-nominal-range.html (17931B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title> 5 Test AudioParam Nominal Range Values 6 </title> 7 <script src="/resources/testharness.js"></script> 8 <script src="/resources/testharnessreport.js"></script> 9 <script src="/webaudio/resources/audit-util.js"></script> 10 <script src="/webaudio/resources/audit.js"></script> 11 </head> 12 <body> 13 <script id="layout-test-code"> 14 // Some arbitrary sample rate for the offline context. 15 let sampleRate = 48000; 16 17 // The actual contexts to use. Generally use the offline context for 18 // testing except for the media nodes which require an AudioContext. 19 let offlineContext; 20 let audioContext; 21 22 // The set of all methods that we've tested for verifying that we tested 23 // all of the necessary objects. 24 let testedMethods = new Set(); 25 26 // The most positive single float value (the value just before infinity). 27 // Be careful when changing this value! Javascript only uses double 28 // floats, so the value here should be the max single-float value, 29 // converted directly to a double-float value. This also depends on 30 // Javascript reading this value and producing the desired double-float 31 // value correctly. 32 let mostPositiveFloat = 3.4028234663852886e38; 33 34 let audit = Audit.createTaskRunner(); 35 36 // Array describing the tests that should be run. |testOfflineConfigs| is 37 // for tests that can use an offline context. |testOnlineConfigs| is for 38 // tests that need to use an online context. Offline contexts are 39 // preferred when possible. 40 let testOfflineConfigs = [ 41 { 42 // The name of the method to create the particular node to be tested. 43 creator: 'createGain', 44 45 // Any args to pass to the creator function. 46 args: [], 47 48 // The min/max limits for each AudioParam of the node. This is a 49 // dictionary whose keys are 50 // the names of each AudioParam in the node. Don't define this if the 51 // node doesn't have any 52 // AudioParam attributes. 53 limits: { 54 gain: { 55 // The expected min and max values for this AudioParam. 56 minValue: -mostPositiveFloat, 57 maxValue: mostPositiveFloat 58 } 59 } 60 }, 61 { 62 creator: 'createDelay', 63 // Just specify a non-default value for the maximum delay so we can 64 // make sure the limits are 65 // set correctly. 66 args: [1.5], 67 limits: {delayTime: {minValue: 0, maxValue: 1.5}} 68 }, 69 { 70 creator: 'createBufferSource', 71 args: [], 72 limits: { 73 playbackRate: 74 {minValue: -mostPositiveFloat, maxValue: mostPositiveFloat}, 75 detune: {minValue: -mostPositiveFloat, maxValue: mostPositiveFloat} 76 } 77 }, 78 { 79 creator: 'createStereoPanner', 80 args: [], 81 limits: {pan: {minValue: -1, maxValue: 1}} 82 }, 83 { 84 creator: 'createDynamicsCompressor', 85 args: [], 86 // Do not set limits for reduction; it's currently an AudioParam but 87 // should be a float. 88 // So let the test fail for reduction. When reduction is changed, 89 // this test will then 90 // correctly pass. 91 limits: { 92 threshold: {minValue: -100, maxValue: 0}, 93 knee: {minValue: 0, maxValue: 40}, 94 ratio: {minValue: 1, maxValue: 20}, 95 attack: {minValue: 0, maxValue: 1}, 96 release: {minValue: 0, maxValue: 1} 97 } 98 }, 99 { 100 creator: 'createBiquadFilter', 101 args: [], 102 limits: { 103 gain: { 104 minValue: -mostPositiveFloat, 105 // This complicated expression is used to get all the arithmetic 106 // to round to the correct single-precision float value for the 107 // desired max. This also assumes that the implication computes 108 // the limit as 40 * log10f(std::numeric_limits<float>::max()). 109 maxValue: 110 Math.fround(40 * Math.fround(Math.log10(mostPositiveFloat))) 111 }, 112 Q: {minValue: -mostPositiveFloat, maxValue: mostPositiveFloat}, 113 frequency: {minValue: 0, maxValue: sampleRate / 2}, 114 detune: { 115 minValue: -Math.fround(1200 * Math.log2(mostPositiveFloat)), 116 maxValue: Math.fround(1200 * Math.log2(mostPositiveFloat)) 117 } 118 } 119 }, 120 { 121 creator: 'createOscillator', 122 args: [], 123 limits: { 124 frequency: {minValue: -sampleRate / 2, maxValue: sampleRate / 2}, 125 detune: { 126 minValue: -Math.fround(1200 * Math.log2(mostPositiveFloat)), 127 maxValue: Math.fround(1200 * Math.log2(mostPositiveFloat)) 128 } 129 } 130 }, 131 { 132 creator: 'createPanner', 133 args: [], 134 limits: { 135 positionX: { 136 minValue: -mostPositiveFloat, 137 maxValue: mostPositiveFloat, 138 }, 139 positionY: { 140 minValue: -mostPositiveFloat, 141 maxValue: mostPositiveFloat, 142 }, 143 positionZ: { 144 minValue: -mostPositiveFloat, 145 maxValue: mostPositiveFloat, 146 }, 147 orientationX: { 148 minValue: -mostPositiveFloat, 149 maxValue: mostPositiveFloat, 150 }, 151 orientationY: { 152 minValue: -mostPositiveFloat, 153 maxValue: mostPositiveFloat, 154 }, 155 orientationZ: { 156 minValue: -mostPositiveFloat, 157 maxValue: mostPositiveFloat, 158 } 159 }, 160 }, 161 { 162 creator: 'createConstantSource', 163 args: [], 164 limits: { 165 offset: {minValue: -mostPositiveFloat, maxValue: mostPositiveFloat} 166 } 167 }, 168 // These nodes don't have AudioParams, but we want to test them anyway. 169 // Any arguments for the 170 // constructor are pretty much arbitrary; they just need to be valid. 171 { 172 creator: 'createBuffer', 173 args: [1, 1, sampleRate], 174 }, 175 {creator: 'createIIRFilter', args: [[1, 2], [1, .9]]}, 176 { 177 creator: 'createWaveShaper', 178 args: [], 179 }, 180 { 181 creator: 'createConvolver', 182 args: [], 183 }, 184 { 185 creator: 'createAnalyser', 186 args: [], 187 }, 188 { 189 creator: 'createScriptProcessor', 190 args: [0], 191 }, 192 { 193 creator: 'createPeriodicWave', 194 args: [Float32Array.from([0, 0]), Float32Array.from([1, 0])], 195 }, 196 { 197 creator: 'createChannelSplitter', 198 args: [], 199 }, 200 { 201 creator: 'createChannelMerger', 202 args: [], 203 }, 204 ]; 205 206 let testOnlineConfigs = [ 207 {creator: 'createMediaElementSource', args: [new Audio()]}, 208 {creator: 'createMediaStreamDestination', args: []} 209 // Can't currently test MediaStreamSource because we're using an offline 210 // context. 211 ]; 212 213 // Create the contexts so we can use it in the following test. 214 audit.define('initialize', (task, should) => { 215 // Just any context so that we can create the nodes. 216 should(() => { 217 offlineContext = new OfflineAudioContext(1, 1, sampleRate); 218 }, 'Create offline context for tests').notThrow(); 219 should(() => { 220 onlineContext = new AudioContext(); 221 }, 'Create online context for tests').notThrow(); 222 task.done(); 223 }); 224 225 // Create a task for each entry in testOfflineConfigs 226 for (let test in testOfflineConfigs) { 227 let config = testOfflineConfigs[test] 228 audit.define('Offline ' + config.creator, (function(c) { 229 return (task, should) => { 230 let node = offlineContext[c.creator](...c.args); 231 testLimits(should, c.creator, node, c.limits); 232 task.done(); 233 }; 234 })(config)); 235 } 236 237 for (let test in testOnlineConfigs) { 238 let config = testOnlineConfigs[test] 239 audit.define('Online ' + config.creator, (function(c) { 240 return (task, should) => { 241 let node = onlineContext[c.creator](...c.args); 242 testLimits(should, c.creator, node, c.limits); 243 task.done(); 244 }; 245 })(config)); 246 } 247 248 // Test the AudioListener params that were added for the automated Panner 249 audit.define('AudioListener', (task, should) => { 250 testLimits(should, '', offlineContext.listener, { 251 positionX: { 252 minValue: -mostPositiveFloat, 253 maxValue: mostPositiveFloat, 254 }, 255 positionY: { 256 minValue: -mostPositiveFloat, 257 maxValue: mostPositiveFloat, 258 }, 259 positionZ: { 260 minValue: -mostPositiveFloat, 261 maxValue: mostPositiveFloat, 262 }, 263 forwardX: { 264 minValue: -mostPositiveFloat, 265 maxValue: mostPositiveFloat, 266 }, 267 forwardY: { 268 minValue: -mostPositiveFloat, 269 maxValue: mostPositiveFloat, 270 }, 271 forwardZ: { 272 minValue: -mostPositiveFloat, 273 maxValue: mostPositiveFloat, 274 }, 275 upX: { 276 minValue: -mostPositiveFloat, 277 maxValue: mostPositiveFloat, 278 }, 279 upY: { 280 minValue: -mostPositiveFloat, 281 maxValue: mostPositiveFloat, 282 }, 283 upZ: { 284 minValue: -mostPositiveFloat, 285 maxValue: mostPositiveFloat, 286 } 287 }); 288 task.done(); 289 }); 290 291 // Verify that we have tested all the create methods available on the 292 // context. 293 audit.define('verifyTests', (task, should) => { 294 let allNodes = new Set(); 295 // Create the set of all "create" methods from the context. 296 for (let method in offlineContext) { 297 if (typeof offlineContext[method] === 'function' && 298 method.substring(0, 6) === 'create') { 299 allNodes.add(method); 300 } 301 } 302 303 // Compute the difference between the set of all create methods on the 304 // context and the set of tests that we've run. 305 let diff = new Set([...allNodes].filter(x => !testedMethods.has(x))); 306 307 // Can't currently test a MediaStreamSourceNode, so remove it from the 308 // diff set. 309 diff.delete('createMediaStreamSource'); 310 311 // It's a test failure if we didn't test all of the create methods in 312 // the context (except createMediaStreamSource, of course). 313 let output = []; 314 if (diff.size) { 315 for (let item of diff) 316 output.push(' ' + item.substring(6)); 317 } 318 319 should(output.length === 0, 'Number of nodes not tested') 320 .message(': 0', ': ' + output); 321 322 task.done(); 323 }); 324 325 // Simple test of a few automation methods to verify we get warnings. 326 audit.define('automation', (task, should) => { 327 // Just use a DelayNode for testing because the audio param has finite 328 // limits. 329 should(() => { 330 let d = offlineContext.createDelay(); 331 332 // The console output should have the warnings that we're interested 333 // in. 334 d.delayTime.setValueAtTime(-1, 0); 335 d.delayTime.linearRampToValueAtTime(2, 1); 336 d.delayTime.exponentialRampToValueAtTime(3, 2); 337 d.delayTime.setTargetAtTime(-1, 3, .1); 338 d.delayTime.setValueCurveAtTime( 339 Float32Array.from([.1, .2, 1.5, -1]), 4, .1); 340 }, 'Test automations (check console logs)').notThrow(); 341 task.done(); 342 }); 343 344 audit.run(); 345 346 // Is |object| an AudioParam? We determine this by checking the 347 // constructor name. 348 function isAudioParam(object) { 349 return object && object.constructor.name === 'AudioParam'; 350 } 351 352 // Does |limitOptions| exist and does it have valid values for the 353 // expected min and max values? 354 function hasValidLimits(limitOptions) { 355 return limitOptions && (typeof limitOptions.minValue === 'number') && 356 (typeof limitOptions.maxValue === 'number'); 357 } 358 359 // Check the min and max values for the AudioParam attribute named 360 // |paramName| for the |node|. The expected limits is given by the 361 // dictionary |limits|. If some test fails, add the name of the failed 362 function validateAudioParamLimits(should, node, paramName, limits) { 363 let nodeName = node.constructor.name; 364 let parameter = node[paramName]; 365 let prefix = nodeName + '.' + paramName; 366 367 let success = true; 368 if (hasValidLimits(limits[paramName])) { 369 // Verify that the min and max values for the parameter are correct. 370 let isCorrect = should(parameter.minValue, prefix + '.minValue') 371 .beEqualTo(limits[paramName].minValue); 372 isCorrect = should(parameter.maxValue, prefix + '.maxValue') 373 .beEqualTo(limits[paramName].maxValue) && 374 isCorrect; 375 376 // Verify that the min and max attributes are read-only. |testValue| 377 // MUST be a number that can be represented exactly the same way as 378 // both a double and single float. A small integer works nicely. 379 const testValue = 42; 380 parameter.minValue = testValue; 381 let isReadOnly; 382 isReadOnly = 383 should(parameter.minValue, `${prefix}.minValue = ${testValue}`) 384 .notBeEqualTo(testValue); 385 386 should(isReadOnly, prefix + '.minValue is read-only').beEqualTo(true); 387 388 isCorrect = isReadOnly && isCorrect; 389 390 parameter.maxValue = testValue; 391 isReadOnly = 392 should(parameter.maxValue, `${prefix}.maxValue = ${testValue}`) 393 .notBeEqualTo(testValue); 394 should(isReadOnly, prefix + '.maxValue is read-only').beEqualTo(true); 395 396 isCorrect = isReadOnly && isCorrect; 397 398 // Now try to set the parameter outside the nominal range. 399 let newValue = 2 * limits[paramName].minValue - 1; 400 401 let isClipped = true; 402 let clippingTested = false; 403 // If the new value is beyond float the largest single-precision 404 // float, skip the test because Chrome throws an error. 405 if (newValue >= -mostPositiveFloat) { 406 parameter.value = newValue; 407 clippingTested = true; 408 isClipped = 409 should( 410 parameter.value, 'Set ' + prefix + '.value = ' + newValue) 411 .beEqualTo(parameter.minValue) && 412 isClipped; 413 } 414 415 newValue = 2 * limits[paramName].maxValue + 1; 416 417 if (newValue <= mostPositiveFloat) { 418 parameter.value = newValue; 419 clippingTested = true; 420 isClipped = 421 should( 422 parameter.value, 'Set ' + prefix + '.value = ' + newValue) 423 .beEqualTo(parameter.maxValue) && 424 isClipped; 425 } 426 427 if (clippingTested) { 428 should( 429 isClipped, 430 prefix + ' was clipped to lie within the nominal range') 431 .beEqualTo(true); 432 } 433 434 isCorrect = isCorrect && isClipped; 435 436 success = isCorrect && success; 437 } else { 438 // Test config didn't specify valid limits. Fail this test! 439 should( 440 clippingTested, 441 'Limits for ' + nodeName + '.' + paramName + 442 ' were correctly defined') 443 .beEqualTo(false); 444 445 success = false; 446 } 447 448 return success; 449 } 450 451 // Test all of the AudioParams for |node| using the expected values in 452 // |limits|. |creatorName| is the name of the method to create the node, 453 // and is used to keep trakc of which tests we've run. 454 function testLimits(should, creatorName, node, limits) { 455 let nodeName = node.constructor.name; 456 testedMethods.add(creatorName); 457 458 let success = true; 459 460 // List of all of the AudioParams that were tested. 461 let audioParams = []; 462 463 // List of AudioParams that failed the test. 464 let incorrectParams = []; 465 466 // Look through all of the keys for the node and extract just the 467 // AudioParams 468 Object.keys(node.__proto__).forEach(function(paramName) { 469 if (isAudioParam(node[paramName])) { 470 audioParams.push(paramName); 471 let isValid = validateAudioParamLimits( 472 should, node, paramName, limits, incorrectParams); 473 if (!isValid) 474 incorrectParams.push(paramName); 475 476 success = isValid && success; 477 } 478 }); 479 480 // Print an appropriate message depending on whether there were 481 // AudioParams defined or not. 482 if (audioParams.length) { 483 let message = 484 'Nominal ranges for AudioParam(s) of ' + node.constructor.name; 485 should(success, message) 486 .message('are correct', 'are incorrect for: ' + +incorrectParams); 487 return success; 488 } else { 489 should(!limits, nodeName) 490 .message( 491 'has no AudioParams as expected', 492 'has no AudioParams but test expected ' + limits); 493 } 494 } 495 </script> 496 </body> 497 </html>