audioparam-testing.js (19916B)
1 (function(global) { 2 3 // Information about the starting/ending times and starting/ending values for 4 // each time interval. 5 let timeValueInfo; 6 7 // The difference between starting values between each time interval. 8 let startingValueDelta; 9 10 // For any automation function that has an end or target value, the end value 11 // is based the starting value of the time interval. The starting value will 12 // be increased or decreased by |startEndValueChange|. We choose half of 13 // |startingValueDelta| so that the ending value will be distinct from the 14 // starting value for next time interval. This allows us to detect where the 15 // ramp begins and ends. 16 let startEndValueChange; 17 18 // Default threshold to use for detecting discontinuities that should appear 19 // at each time interval. 20 let discontinuityThreshold; 21 22 // Time interval between value changes. It is best if 1 / numberOfTests is 23 // not close to timeInterval. 24 let timeIntervalInternal = .03; 25 26 let context; 27 28 // Make sure we render long enough to capture all of our test data. 29 function renderLength(numberOfTests) { 30 return timeToSampleFrame((numberOfTests + 1) * timeInterval, sampleRate); 31 } 32 33 // Create a constant reference signal with the given |value|. Basically the 34 // same as |createConstantBuffer|, but with the parameters to match the other 35 // create functions. The |endValue| is ignored. 36 function createConstantArray( 37 startTime, endTime, value, endValue, sampleRate) { 38 let startFrame = timeToSampleFrame(startTime, sampleRate); 39 let endFrame = timeToSampleFrame(endTime, sampleRate); 40 let length = endFrame - startFrame; 41 42 let buffer = createConstantBuffer(context, length, value); 43 44 return buffer.getChannelData(0); 45 } 46 47 function getStartEndFrames(startTime, endTime, sampleRate) { 48 // Start frame is the ceiling of the start time because the ramp starts at 49 // or after the sample frame. End frame is the ceiling because it's the 50 // exclusive ending frame of the automation. 51 let startFrame = Math.ceil(startTime * sampleRate); 52 let endFrame = Math.ceil(endTime * sampleRate); 53 54 return {startFrame: startFrame, endFrame: endFrame}; 55 } 56 57 // Create a linear ramp starting at |startValue| and ending at |endValue|. The 58 // ramp starts at time |startTime| and ends at |endTime|. (The start and end 59 // times are only used to compute how many samples to return.) 60 function createLinearRampArray( 61 startTime, endTime, startValue, endValue, sampleRate) { 62 let frameInfo = getStartEndFrames(startTime, endTime, sampleRate); 63 let startFrame = frameInfo.startFrame; 64 let endFrame = frameInfo.endFrame; 65 let length = endFrame - startFrame; 66 let array = new Array(length); 67 68 let step = Math.fround( 69 (endValue - startValue) / (endTime - startTime) / sampleRate); 70 let start = Math.fround( 71 startValue + 72 (endValue - startValue) * (startFrame / sampleRate - startTime) / 73 (endTime - startTime)); 74 75 let slope = (endValue - startValue) / (endTime - startTime); 76 77 // v(t) = v0 + (v1 - v0)*(t-t0)/(t1-t0) 78 for (k = 0; k < length; ++k) { 79 // array[k] = Math.fround(start + k * step); 80 let t = (startFrame + k) / sampleRate; 81 array[k] = startValue + slope * (t - startTime); 82 } 83 84 return array; 85 } 86 87 // Create an exponential ramp starting at |startValue| and ending at 88 // |endValue|. The ramp starts at time |startTime| and ends at |endTime|. 89 // (The start and end times are only used to compute how many samples to 90 // return.) 91 function createExponentialRampArray( 92 startTime, endTime, startValue, endValue, sampleRate) { 93 let deltaTime = endTime - startTime; 94 95 let frameInfo = getStartEndFrames(startTime, endTime, sampleRate); 96 let startFrame = frameInfo.startFrame; 97 let endFrame = frameInfo.endFrame; 98 let length = endFrame - startFrame; 99 let array = new Array(length); 100 101 let ratio = endValue / startValue; 102 103 // v(t) = v0*(v1/v0)^((t-t0)/(t1-t0)) 104 for (let k = 0; k < length; ++k) { 105 let t = Math.fround((startFrame + k) / sampleRate); 106 array[k] = Math.fround( 107 startValue * Math.pow(ratio, (t - startTime) / deltaTime)); 108 } 109 110 return array; 111 } 112 113 function discreteTimeConstantForSampleRate(timeConstant, sampleRate) { 114 return 1 - Math.exp(-1 / (sampleRate * timeConstant)); 115 } 116 117 // Create a signal that starts at |startValue| and exponentially approaches 118 // the target value of |targetValue|, using a time constant of |timeConstant|. 119 // The ramp starts at time |startTime| and ends at |endTime|. (The start and 120 // end times are only used to compute how many samples to return.) 121 function createExponentialApproachArray( 122 startTime, endTime, startValue, targetValue, sampleRate, timeConstant) { 123 let startFrameFloat = startTime * sampleRate; 124 let frameInfo = getStartEndFrames(startTime, endTime, sampleRate); 125 let startFrame = frameInfo.startFrame; 126 let endFrame = frameInfo.endFrame; 127 let length = Math.floor(endFrame - startFrame); 128 let array = new Array(length); 129 let c = discreteTimeConstantForSampleRate(timeConstant, sampleRate); 130 131 let delta = startValue - targetValue; 132 133 // v(t) = v1 + (v0 - v1) * exp(-(t-t0)/tau) 134 for (let k = 0; k < length; ++k) { 135 let t = (startFrame + k) / sampleRate; 136 let value = 137 targetValue + delta * Math.exp(-(t - startTime) / timeConstant); 138 array[k] = value; 139 } 140 141 return array; 142 } 143 144 // Create a sine wave of the specified duration. 145 function createReferenceSineArray( 146 startTime, endTime, startValue, endValue, sampleRate) { 147 // Ignore |startValue| and |endValue| for the sine wave. 148 let curve = createSineWaveArray( 149 endTime - startTime, freqHz, sineAmplitude, sampleRate); 150 // Sample the curve appropriately. 151 let frameInfo = getStartEndFrames(startTime, endTime, sampleRate); 152 let startFrame = frameInfo.startFrame; 153 let endFrame = frameInfo.endFrame; 154 let length = Math.floor(endFrame - startFrame); 155 let array = new Array(length); 156 157 // v(t) = linearly interpolate between V[k] and V[k + 1] where k = 158 // floor((N-1)/duration*(t - t0)) 159 let f = (length - 1) / (endTime - startTime); 160 161 for (let k = 0; k < length; ++k) { 162 let t = (startFrame + k) / sampleRate; 163 let indexFloat = f * (t - startTime); 164 let index = Math.floor(indexFloat); 165 if (index + 1 < length) { 166 let v0 = curve[index]; 167 let v1 = curve[index + 1]; 168 array[k] = v0 + (v1 - v0) * (indexFloat - index); 169 } else { 170 array[k] = curve[length - 1]; 171 } 172 } 173 174 return array; 175 } 176 177 // Create a sine wave of the given frequency and amplitude. The sine wave is 178 // offset by half the amplitude so that result is always positive. 179 function createSineWaveArray(durationSeconds, freqHz, amplitude, sampleRate) { 180 let length = timeToSampleFrame(durationSeconds, sampleRate); 181 let signal = new Float32Array(length); 182 let omega = 2 * Math.PI * freqHz / sampleRate; 183 let halfAmplitude = amplitude / 2; 184 185 for (let k = 0; k < length; ++k) { 186 signal[k] = halfAmplitude + halfAmplitude * Math.sin(omega * k); 187 } 188 189 return signal; 190 } 191 192 // Return the difference between the starting value and the ending value for 193 // time interval |timeIntervalIndex|. We alternate between an end value that 194 // is above or below the starting value. 195 function endValueDelta(timeIntervalIndex) { 196 if (timeIntervalIndex & 1) { 197 return -startEndValueChange; 198 } else { 199 return startEndValueChange; 200 } 201 } 202 203 // Relative error metric 204 function relativeErrorMetric(actual, expected) { 205 return (actual - expected) / Math.abs(expected); 206 } 207 208 // Difference metric 209 function differenceErrorMetric(actual, expected) { 210 return actual - expected; 211 } 212 213 // Return the difference between the starting value at |timeIntervalIndex| and 214 // the starting value at the next time interval. Since we started at a large 215 // initial value, we decrease the value at each time interval. 216 function valueUpdate(timeIntervalIndex) { 217 return -startingValueDelta; 218 } 219 220 // Compare a section of the rendered data against our expected signal. 221 function comparePartialSignals( 222 rendered, expectedFunction, startTime, endTime, valueInfo, 223 sampleRateParam, errorMetric) { 224 let startSample = timeToSampleFrame(startTime, sampleRateParam); 225 let expected = expectedFunction( 226 startTime, endTime, valueInfo.startValue, valueInfo.endValue, 227 sampleRateParam, timeConstant); 228 229 let n = expected.length; 230 let maxError = -1; 231 let maxErrorIndex = -1; 232 233 for (let k = 0; k < n; ++k) { 234 // Make sure we don't pass these tests because a NaN has been generated in 235 // either the 236 // rendered data or the reference data. 237 if (!isValidNumber(rendered[startSample + k])) { 238 maxError = Infinity; 239 maxErrorIndex = startSample + k; 240 assert_true( 241 isValidNumber(rendered[startSample + k]), 242 `NaN or infinity for rendered data at ${maxErrorIndex}`); 243 break; 244 } 245 if (!isValidNumber(expected[k])) { 246 maxError = Infinity; 247 maxErrorIndex = startSample + k; 248 assert_true( 249 isValidNumber(expected[k]), 250 `NaN or infinity for rendered data at ${maxErrorIndex}`); 251 break; 252 } 253 let error = Math.abs(errorMetric(rendered[startSample + k], expected[k])); 254 if (error > maxError) { 255 maxError = error; 256 maxErrorIndex = k; 257 } 258 } 259 260 return {maxError: maxError, index: maxErrorIndex, expected: expected}; 261 }; 262 263 // Find the discontinuities in the data and compare the locations of the 264 // discontinuities with the times that define the time intervals. There is a 265 // discontinuity if the difference between successive samples exceeds the 266 // threshold. 267 function verifyDiscontinuities(values, times, threshold) { 268 let n = values.length; 269 let success = true; 270 let badLocations = 0; 271 let breaks = []; 272 273 // Find discontinuities. 274 for (let k = 1; k < n; ++k) { 275 if (Math.abs(values[k] - values[k - 1]) > threshold) { 276 breaks.push(k); 277 } 278 } 279 280 let testCount; 281 282 // If there are numberOfTests intervals, there are only numberOfTests - 1 283 // internal interval boundaries. Hence the maximum number of discontinuties 284 // we expect to find is numberOfTests - 1. If we find more than that, we 285 // have no reference to compare against. We also assume that the actual 286 // discontinuities are close to the expected ones. 287 // 288 // This is just a sanity check when something goes really wrong. For 289 // example, if the threshold is too low, every sample frame looks like a 290 // discontinuity. 291 if (breaks.length >= numberOfTests) { 292 testCount = numberOfTests - 1; 293 assert_less_than( 294 breaks.length, numberOfTests, 'Number of discontinuities'); 295 success = false; 296 } else { 297 testCount = breaks.length; 298 } 299 300 // Compare the location of each discontinuity with the end time of each 301 // interval. (There is no discontinuity at the start of the signal.) 302 for (let k = 0; k < testCount; ++k) { 303 let expectedSampleFrame = timeToSampleFrame(times[k + 1], sampleRate); 304 if (breaks[k] != expectedSampleFrame) { 305 success = false; 306 ++badLocations; 307 assert_equals(breaks[k], expectedSampleFrame, 'Discontinuity at index'); 308 } 309 } 310 311 if (badLocations) { 312 assert_equals( 313 badLocations, 0, 'Number of discontinuites at incorrect locations'); 314 success = false; 315 } else { 316 assert_equals( 317 breaks.length + 1, 318 numberOfTests, 319 'Number of tests started and ended at the correct time'); 320 } 321 322 return success; 323 }; 324 325 // Compare the rendered data with the expected data. 326 // 327 // testName - string describing the test 328 // 329 // maxError - maximum allowed difference between the rendered data and the 330 // expected data 331 // 332 // rendererdData - array containing the rendered (actual) data 333 // 334 // expectedFunction - function to compute the expected data 335 // 336 // timeValueInfo - array containing information about the start and end times 337 // and the start and end values of each interval. 338 // 339 // breakThreshold - threshold to use for determining discontinuities. 340 function compareSignals( 341 testName, maxError, renderedData, expectedFunction, timeValueInfo, 342 breakThreshold, errorMetric) { 343 let success = true; 344 const failedTestCount = 0; 345 const times = timeValueInfo.times; 346 const values = timeValueInfo.values; 347 const n = values.length; 348 let expectedSignal = []; 349 350 success = 351 verifyDiscontinuities(renderedData, times, breakThreshold); 352 353 for (let k = 0; k < n; ++k) { 354 const result = comparePartialSignals( 355 renderedData, expectedFunction, times[k], times[k + 1], 356 values[k], sampleRate, errorMetric); 357 358 expectedSignal = 359 expectedSignal.concat(Array.prototype.slice.call(result.expected)); 360 361 assert_less_than_equal( 362 result.maxError, 363 maxError, 364 `Max error for test ${k} at offset ` + 365 `${result.index + timeToSampleFrame(times[k], sampleRate)}`); 366 } 367 368 assert_equals( 369 failedTestCount, 370 0, 371 `Number of failed tests with an acceptable relative ` + 372 `tolerance of ${maxError}`); 373 }; 374 375 // Create a function to test the rendered data with the reference data. 376 // 377 // testName - string describing the test 378 // 379 // error - max allowed error between rendered data and the reference data. 380 // 381 // referenceFunction - function that generates the reference data to be 382 // compared with the rendered data. 383 // 384 // jumpThreshold - optional parameter that specifies the threshold to use for 385 // detecting discontinuities. If not specified, defaults to 386 // discontinuityThreshold. 387 // 388 function checkResultFunction( 389 task, testName, error, referenceFunction, jumpThreshold, 390 errorMetric) { 391 return function(event) { 392 let buffer = event.renderedBuffer; 393 renderedData = buffer.getChannelData(0); 394 395 let threshold; 396 397 if (!jumpThreshold) { 398 threshold = discontinuityThreshold; 399 } else { 400 threshold = jumpThreshold; 401 } 402 403 compareSignals( 404 testName, error, renderedData, referenceFunction, 405 timeValueInfo, threshold, errorMetric); 406 task.done(); 407 } 408 } 409 410 // Run all the automation tests. 411 // 412 // numberOfTests - number of tests (time intervals) to run. 413 // 414 // initialValue - The initial value of the first time interval. 415 // 416 // setValueFunction - function that sets the specified value at the start of a 417 // time interval. 418 // 419 // automationFunction - function that sets the end value for the time 420 // interval. It specifies how the value approaches the end value. 421 // 422 // An object is returned containing an array of start times for each time 423 // interval, and an array giving the start and end values for the interval. 424 function doAutomation( 425 numberOfTests, initialValue, setValueFunction, automationFunction) { 426 let timeInfo = [0]; 427 let valueInfo = []; 428 let value = initialValue; 429 430 for (let k = 0; k < numberOfTests; ++k) { 431 let startTime = k * timeInterval; 432 let endTime = (k + 1) * timeInterval; 433 let endValue = value + endValueDelta(k); 434 435 // Set the value at the start of the time interval. 436 setValueFunction(value, startTime); 437 438 // Specify the end or target value, and how we should approach it. 439 automationFunction(endValue, startTime, endTime); 440 441 // Keep track of the start times, and the start and end values for each 442 // time interval. 443 timeInfo.push(endTime); 444 valueInfo.push({startValue: value, endValue: endValue}); 445 446 value += valueUpdate(k); 447 } 448 449 return {times: timeInfo, values: valueInfo}; 450 } 451 452 // Create the audio graph for the test and then run the test. 453 // 454 // numberOfTests - number of time intervals (tests) to run. 455 // 456 // initialValue - the initial value of the gain at time 0. 457 // 458 // setValueFunction - function to set the value at the beginning of each time 459 // interval. 460 // 461 // automationFunction - the AudioParamTimeline automation function 462 // 463 // testName - string indicating the test that is being run. 464 // 465 // maxError - maximum allowed error between the rendered data and the 466 // reference data 467 // 468 // referenceFunction - function that generates the reference data to be 469 // compared against the rendered data. 470 // 471 // jumpThreshold - optional parameter that specifies the threshold to use for 472 // detecting discontinuities. If not specified, defaults to 473 // discontinuityThreshold. 474 function createAudioGraphAndTest( 475 task, should, numberOfTests, initialValue, setValueFunction, 476 automationFunction, testName, maxError, referenceFunction, jumpThreshold, 477 errorMetric) { 478 // Create offline audio context. 479 context = 480 new OfflineAudioContext(2, renderLength(numberOfTests), sampleRate); 481 let constantBuffer = 482 createConstantBuffer(context, renderLength(numberOfTests), 1); 483 484 // We use an AudioGainNode here simply as a convenient way to test the 485 // AudioParam automation, since it's easy to pass a constant value through 486 // the node, automate the .gain attribute and observe the resulting values. 487 488 gainNode = context.createGain(); 489 490 let bufferSource = context.createBufferSource(); 491 bufferSource.buffer = constantBuffer; 492 bufferSource.connect(gainNode); 493 gainNode.connect(context.destination); 494 495 // Set up default values for the parameters that control how the automation 496 // test values progress for each time interval. 497 startingValueDelta = initialValue / numberOfTests; 498 startEndValueChange = startingValueDelta / 2; 499 discontinuityThreshold = startEndValueChange / 2; 500 501 // Run the automation tests. 502 timeValueInfo = doAutomation( 503 numberOfTests, initialValue, setValueFunction, automationFunction); 504 bufferSource.start(0); 505 506 context.oncomplete = checkResultFunction( 507 task, testName, maxError, referenceFunction, jumpThreshold, 508 errorMetric || relativeErrorMetric); 509 context.startRendering(); 510 } 511 512 // Export local references to global scope. All the new objects in this file 513 // must be exported through this if it is to be used in the actual test HTML 514 // page. 515 let exports = { 516 'sampleRate': 44100, 517 'gainNode': null, 518 'timeInterval': timeIntervalInternal, 519 520 // Some suitable time constant so that we can see a significant change over 521 // a timeInterval. This is only needed by setTargetAtTime() which needs a 522 // time constant. 523 'timeConstant': timeIntervalInternal / 3, 524 525 'renderLength': renderLength, 526 'createConstantArray': createConstantArray, 527 'getStartEndFrames': getStartEndFrames, 528 'createLinearRampArray': createLinearRampArray, 529 'createExponentialRampArray': createExponentialRampArray, 530 'discreteTimeConstantForSampleRate': discreteTimeConstantForSampleRate, 531 'createExponentialApproachArray': createExponentialApproachArray, 532 'createReferenceSineArray': createReferenceSineArray, 533 'createSineWaveArray': createSineWaveArray, 534 'endValueDelta': endValueDelta, 535 'relativeErrorMetric': relativeErrorMetric, 536 'differenceErrorMetric': differenceErrorMetric, 537 'valueUpdate': valueUpdate, 538 'comparePartialSignals': comparePartialSignals, 539 'verifyDiscontinuities': verifyDiscontinuities, 540 'compareSignals': compareSignals, 541 'checkResultFunction': checkResultFunction, 542 'doAutomation': doAutomation, 543 'createAudioGraphAndTest': createAudioGraphAndTest 544 }; 545 546 for (let reference in exports) { 547 global[reference] = exports[reference]; 548 } 549 550 })(window);