| (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); |