diff options
Diffstat (limited to 'testing/web-platform/tests/webaudio/resources/audioparam-testing.js')
-rw-r--r-- | testing/web-platform/tests/webaudio/resources/audioparam-testing.js | 554 |
1 files changed, 554 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webaudio/resources/audioparam-testing.js b/testing/web-platform/tests/webaudio/resources/audioparam-testing.js new file mode 100644 index 0000000000..bc90ddbef8 --- /dev/null +++ b/testing/web-platform/tests/webaudio/resources/audioparam-testing.js @@ -0,0 +1,554 @@ +(function(global) { + + // Information about the starting/ending times and starting/ending values for + // each time interval. + let timeValueInfo; + + // The difference between starting values between each time interval. + let startingValueDelta; + + // For any automation function that has an end or target value, the end value + // is based the starting value of the time interval. The starting value will + // be increased or decreased by |startEndValueChange|. We choose half of + // |startingValueDelta| so that the ending value will be distinct from the + // starting value for next time interval. This allows us to detect where the + // ramp begins and ends. + let startEndValueChange; + + // Default threshold to use for detecting discontinuities that should appear + // at each time interval. + let discontinuityThreshold; + + // Time interval between value changes. It is best if 1 / numberOfTests is + // not close to timeInterval. + let timeIntervalInternal = .03; + + let context; + + // Make sure we render long enough to capture all of our test data. + function renderLength(numberOfTests) { + return timeToSampleFrame((numberOfTests + 1) * timeInterval, sampleRate); + } + + // Create a constant reference signal with the given |value|. Basically the + // same as |createConstantBuffer|, but with the parameters to match the other + // create functions. The |endValue| is ignored. + function createConstantArray( + startTime, endTime, value, endValue, sampleRate) { + let startFrame = timeToSampleFrame(startTime, sampleRate); + let endFrame = timeToSampleFrame(endTime, sampleRate); + let length = endFrame - startFrame; + + let buffer = createConstantBuffer(context, length, value); + + return buffer.getChannelData(0); + } + + function getStartEndFrames(startTime, endTime, sampleRate) { + // Start frame is the ceiling of the start time because the ramp starts at + // or after the sample frame. End frame is the ceiling because it's the + // exclusive ending frame of the automation. + let startFrame = Math.ceil(startTime * sampleRate); + let endFrame = Math.ceil(endTime * sampleRate); + + return {startFrame: startFrame, endFrame: endFrame}; + } + + // Create a linear ramp starting at |startValue| and ending at |endValue|. The + // ramp starts at time |startTime| and ends at |endTime|. (The start and end + // times are only used to compute how many samples to return.) + function createLinearRampArray( + startTime, endTime, startValue, endValue, sampleRate) { + let frameInfo = getStartEndFrames(startTime, endTime, sampleRate); + let startFrame = frameInfo.startFrame; + let endFrame = frameInfo.endFrame; + let length = endFrame - startFrame; + let array = new Array(length); + + let step = Math.fround( + (endValue - startValue) / (endTime - startTime) / sampleRate); + let start = Math.fround( + startValue + + (endValue - startValue) * (startFrame / sampleRate - startTime) / + (endTime - startTime)); + + let slope = (endValue - startValue) / (endTime - startTime); + + // v(t) = v0 + (v1 - v0)*(t-t0)/(t1-t0) + for (k = 0; k < length; ++k) { + // array[k] = Math.fround(start + k * step); + let t = (startFrame + k) / sampleRate; + array[k] = startValue + slope * (t - startTime); + } + + return array; + } + + // Create an exponential ramp starting at |startValue| and ending at + // |endValue|. The ramp starts at time |startTime| and ends at |endTime|. + // (The start and end times are only used to compute how many samples to + // return.) + function createExponentialRampArray( + startTime, endTime, startValue, endValue, sampleRate) { + let deltaTime = endTime - startTime; + + let frameInfo = getStartEndFrames(startTime, endTime, sampleRate); + let startFrame = frameInfo.startFrame; + let endFrame = frameInfo.endFrame; + let length = endFrame - startFrame; + let array = new Array(length); + + let ratio = endValue / startValue; + + // v(t) = v0*(v1/v0)^((t-t0)/(t1-t0)) + for (let k = 0; k < length; ++k) { + let t = Math.fround((startFrame + k) / sampleRate); + array[k] = Math.fround( + startValue * Math.pow(ratio, (t - startTime) / deltaTime)); + } + + return array; + } + + function discreteTimeConstantForSampleRate(timeConstant, sampleRate) { + return 1 - Math.exp(-1 / (sampleRate * timeConstant)); + } + + // Create a signal that starts at |startValue| and exponentially approaches + // the target value of |targetValue|, using a time constant of |timeConstant|. + // The ramp starts at time |startTime| and ends at |endTime|. (The start and + // end times are only used to compute how many samples to return.) + function createExponentialApproachArray( + startTime, endTime, startValue, targetValue, sampleRate, timeConstant) { + let startFrameFloat = startTime * sampleRate; + let frameInfo = getStartEndFrames(startTime, endTime, sampleRate); + let startFrame = frameInfo.startFrame; + let endFrame = frameInfo.endFrame; + let length = Math.floor(endFrame - startFrame); + let array = new Array(length); + let c = discreteTimeConstantForSampleRate(timeConstant, sampleRate); + + let delta = startValue - targetValue; + + // v(t) = v1 + (v0 - v1) * exp(-(t-t0)/tau) + for (let k = 0; k < length; ++k) { + let t = (startFrame + k) / sampleRate; + let value = + targetValue + delta * Math.exp(-(t - startTime) / timeConstant); + array[k] = value; + } + + return array; + } + + // Create a sine wave of the specified duration. + function createReferenceSineArray( + startTime, endTime, startValue, endValue, sampleRate) { + // Ignore |startValue| and |endValue| for the sine wave. + let curve = createSineWaveArray( + endTime - startTime, freqHz, sineAmplitude, sampleRate); + // Sample the curve appropriately. + let frameInfo = getStartEndFrames(startTime, endTime, sampleRate); + let startFrame = frameInfo.startFrame; + let endFrame = frameInfo.endFrame; + let length = Math.floor(endFrame - startFrame); + let array = new Array(length); + + // v(t) = linearly interpolate between V[k] and V[k + 1] where k = + // floor((N-1)/duration*(t - t0)) + let f = (length - 1) / (endTime - startTime); + + for (let k = 0; k < length; ++k) { + let t = (startFrame + k) / sampleRate; + let indexFloat = f * (t - startTime); + let index = Math.floor(indexFloat); + if (index + 1 < length) { + let v0 = curve[index]; + let v1 = curve[index + 1]; + array[k] = v0 + (v1 - v0) * (indexFloat - index); + } else { + array[k] = curve[length - 1]; + } + } + + return array; + } + + // Create a sine wave of the given frequency and amplitude. The sine wave is + // offset by half the amplitude so that result is always positive. + function createSineWaveArray(durationSeconds, freqHz, amplitude, sampleRate) { + let length = timeToSampleFrame(durationSeconds, sampleRate); + let signal = new Float32Array(length); + let omega = 2 * Math.PI * freqHz / sampleRate; + let halfAmplitude = amplitude / 2; + + for (let k = 0; k < length; ++k) { + signal[k] = halfAmplitude + halfAmplitude * Math.sin(omega * k); + } + + return signal; + } + + // Return the difference between the starting value and the ending value for + // time interval |timeIntervalIndex|. We alternate between an end value that + // is above or below the starting value. + function endValueDelta(timeIntervalIndex) { + if (timeIntervalIndex & 1) { + return -startEndValueChange; + } else { + return startEndValueChange; + } + } + + // Relative error metric + function relativeErrorMetric(actual, expected) { + return (actual - expected) / Math.abs(expected); + } + + // Difference metric + function differenceErrorMetric(actual, expected) { + return actual - expected; + } + + // Return the difference between the starting value at |timeIntervalIndex| and + // the starting value at the next time interval. Since we started at a large + // initial value, we decrease the value at each time interval. + function valueUpdate(timeIntervalIndex) { + return -startingValueDelta; + } + + // Compare a section of the rendered data against our expected signal. + function comparePartialSignals( + should, rendered, expectedFunction, startTime, endTime, valueInfo, + sampleRate, errorMetric) { + let startSample = timeToSampleFrame(startTime, sampleRate); + let expected = expectedFunction( + startTime, endTime, valueInfo.startValue, valueInfo.endValue, + sampleRate, timeConstant); + + let n = expected.length; + let maxError = -1; + let maxErrorIndex = -1; + + for (let k = 0; k < n; ++k) { + // Make sure we don't pass these tests because a NaN has been generated in + // either the + // rendered data or the reference data. + if (!isValidNumber(rendered[startSample + k])) { + maxError = Infinity; + maxErrorIndex = startSample + k; + should( + isValidNumber(rendered[startSample + k]), + 'NaN or infinity for rendered data at ' + maxErrorIndex) + .beTrue(); + break; + } + if (!isValidNumber(expected[k])) { + maxError = Infinity; + maxErrorIndex = startSample + k; + should( + isValidNumber(expected[k]), + 'NaN or infinity for rendered data at ' + maxErrorIndex) + .beTrue(); + break; + } + let error = Math.abs(errorMetric(rendered[startSample + k], expected[k])); + if (error > maxError) { + maxError = error; + maxErrorIndex = k; + } + } + + return {maxError: maxError, index: maxErrorIndex, expected: expected}; + } + + // Find the discontinuities in the data and compare the locations of the + // discontinuities with the times that define the time intervals. There is a + // discontinuity if the difference between successive samples exceeds the + // threshold. + function verifyDiscontinuities(should, values, times, threshold) { + let n = values.length; + let success = true; + let badLocations = 0; + let breaks = []; + + // Find discontinuities. + for (let k = 1; k < n; ++k) { + if (Math.abs(values[k] - values[k - 1]) > threshold) { + breaks.push(k); + } + } + + let testCount; + + // If there are numberOfTests intervals, there are only numberOfTests - 1 + // internal interval boundaries. Hence the maximum number of discontinuties + // we expect to find is numberOfTests - 1. If we find more than that, we + // have no reference to compare against. We also assume that the actual + // discontinuities are close to the expected ones. + // + // This is just a sanity check when something goes really wrong. For + // example, if the threshold is too low, every sample frame looks like a + // discontinuity. + if (breaks.length >= numberOfTests) { + testCount = numberOfTests - 1; + should(breaks.length, 'Number of discontinuities') + .beLessThan(numberOfTests); + success = false; + } else { + testCount = breaks.length; + } + + // Compare the location of each discontinuity with the end time of each + // interval. (There is no discontinuity at the start of the signal.) + for (let k = 0; k < testCount; ++k) { + let expectedSampleFrame = timeToSampleFrame(times[k + 1], sampleRate); + if (breaks[k] != expectedSampleFrame) { + success = false; + ++badLocations; + should(breaks[k], 'Discontinuity at index') + .beEqualTo(expectedSampleFrame); + } + } + + if (badLocations) { + should(badLocations, 'Number of discontinuites at incorrect locations') + .beEqualTo(0); + success = false; + } else { + should( + breaks.length + 1, + 'Number of tests started and ended at the correct time') + .beEqualTo(numberOfTests); + } + + return success; + } + + // Compare the rendered data with the expected data. + // + // testName - string describing the test + // + // maxError - maximum allowed difference between the rendered data and the + // expected data + // + // rendererdData - array containing the rendered (actual) data + // + // expectedFunction - function to compute the expected data + // + // timeValueInfo - array containing information about the start and end times + // and the start and end values of each interval. + // + // breakThreshold - threshold to use for determining discontinuities. + function compareSignals( + should, testName, maxError, renderedData, expectedFunction, timeValueInfo, + breakThreshold, errorMetric) { + let success = true; + let failedTestCount = 0; + let times = timeValueInfo.times; + let values = timeValueInfo.values; + let n = values.length; + let expectedSignal = []; + + success = + verifyDiscontinuities(should, renderedData, times, breakThreshold); + + for (let k = 0; k < n; ++k) { + let result = comparePartialSignals( + should, renderedData, expectedFunction, times[k], times[k + 1], + values[k], sampleRate, errorMetric); + + expectedSignal = + expectedSignal.concat(Array.prototype.slice.call(result.expected)); + + should( + result.maxError, + 'Max error for test ' + k + ' at offset ' + + (result.index + timeToSampleFrame(times[k], sampleRate))) + .beLessThanOrEqualTo(maxError); + } + + should( + failedTestCount, + 'Number of failed tests with an acceptable relative tolerance of ' + + maxError) + .beEqualTo(0); + } + + // Create a function to test the rendered data with the reference data. + // + // testName - string describing the test + // + // error - max allowed error between rendered data and the reference data. + // + // referenceFunction - function that generates the reference data to be + // compared with the rendered data. + // + // jumpThreshold - optional parameter that specifies the threshold to use for + // detecting discontinuities. If not specified, defaults to + // discontinuityThreshold. + // + function checkResultFunction( + task, should, testName, error, referenceFunction, jumpThreshold, + errorMetric) { + return function(event) { + let buffer = event.renderedBuffer; + renderedData = buffer.getChannelData(0); + + let threshold; + + if (!jumpThreshold) { + threshold = discontinuityThreshold; + } else { + threshold = jumpThreshold; + } + + compareSignals( + should, testName, error, renderedData, referenceFunction, + timeValueInfo, threshold, errorMetric); + task.done(); + } + } + + // Run all the automation tests. + // + // numberOfTests - number of tests (time intervals) to run. + // + // initialValue - The initial value of the first time interval. + // + // setValueFunction - function that sets the specified value at the start of a + // time interval. + // + // automationFunction - function that sets the end value for the time + // interval. It specifies how the value approaches the end value. + // + // An object is returned containing an array of start times for each time + // interval, and an array giving the start and end values for the interval. + function doAutomation( + numberOfTests, initialValue, setValueFunction, automationFunction) { + let timeInfo = [0]; + let valueInfo = []; + let value = initialValue; + + for (let k = 0; k < numberOfTests; ++k) { + let startTime = k * timeInterval; + let endTime = (k + 1) * timeInterval; + let endValue = value + endValueDelta(k); + + // Set the value at the start of the time interval. + setValueFunction(value, startTime); + + // Specify the end or target value, and how we should approach it. + automationFunction(endValue, startTime, endTime); + + // Keep track of the start times, and the start and end values for each + // time interval. + timeInfo.push(endTime); + valueInfo.push({startValue: value, endValue: endValue}); + + value += valueUpdate(k); + } + + return {times: timeInfo, values: valueInfo}; + } + + // Create the audio graph for the test and then run the test. + // + // numberOfTests - number of time intervals (tests) to run. + // + // initialValue - the initial value of the gain at time 0. + // + // setValueFunction - function to set the value at the beginning of each time + // interval. + // + // automationFunction - the AudioParamTimeline automation function + // + // testName - string indicating the test that is being run. + // + // maxError - maximum allowed error between the rendered data and the + // reference data + // + // referenceFunction - function that generates the reference data to be + // compared against the rendered data. + // + // jumpThreshold - optional parameter that specifies the threshold to use for + // detecting discontinuities. If not specified, defaults to + // discontinuityThreshold. + // + function createAudioGraphAndTest( + task, should, numberOfTests, initialValue, setValueFunction, + automationFunction, testName, maxError, referenceFunction, jumpThreshold, + errorMetric) { + // Create offline audio context. + context = + new OfflineAudioContext(2, renderLength(numberOfTests), sampleRate); + let constantBuffer = + createConstantBuffer(context, renderLength(numberOfTests), 1); + + // We use an AudioGainNode here simply as a convenient way to test the + // AudioParam automation, since it's easy to pass a constant value through + // the node, automate the .gain attribute and observe the resulting values. + + gainNode = context.createGain(); + + let bufferSource = context.createBufferSource(); + bufferSource.buffer = constantBuffer; + bufferSource.connect(gainNode); + gainNode.connect(context.destination); + + // Set up default values for the parameters that control how the automation + // test values progress for each time interval. + startingValueDelta = initialValue / numberOfTests; + startEndValueChange = startingValueDelta / 2; + discontinuityThreshold = startEndValueChange / 2; + + // Run the automation tests. + timeValueInfo = doAutomation( + numberOfTests, initialValue, setValueFunction, automationFunction); + bufferSource.start(0); + + context.oncomplete = checkResultFunction( + task, should, testName, maxError, referenceFunction, jumpThreshold, + errorMetric || relativeErrorMetric); + context.startRendering(); + } + + // Export local references to global scope. All the new objects in this file + // must be exported through this if it is to be used in the actual test HTML + // page. + let exports = { + 'sampleRate': 44100, + 'gainNode': null, + 'timeInterval': timeIntervalInternal, + + // Some suitable time constant so that we can see a significant change over + // a timeInterval. This is only needed by setTargetAtTime() which needs a + // time constant. + 'timeConstant': timeIntervalInternal / 3, + + 'renderLength': renderLength, + 'createConstantArray': createConstantArray, + 'getStartEndFrames': getStartEndFrames, + 'createLinearRampArray': createLinearRampArray, + 'createExponentialRampArray': createExponentialRampArray, + 'discreteTimeConstantForSampleRate': discreteTimeConstantForSampleRate, + 'createExponentialApproachArray': createExponentialApproachArray, + 'createReferenceSineArray': createReferenceSineArray, + 'createSineWaveArray': createSineWaveArray, + 'endValueDelta': endValueDelta, + 'relativeErrorMetric': relativeErrorMetric, + 'differenceErrorMetric': differenceErrorMetric, + 'valueUpdate': valueUpdate, + 'comparePartialSignals': comparePartialSignals, + 'verifyDiscontinuities': verifyDiscontinuities, + 'compareSignals': compareSignals, + 'checkResultFunction': checkResultFunction, + 'doAutomation': doAutomation, + 'createAudioGraphAndTest': createAudioGraphAndTest + }; + + for (let reference in exports) { + global[reference] = exports[reference]; + } + +})(window); |