зеркало из https://github.com/mozilla/gecko-dev.git
555 строки
20 KiB
JavaScript
555 строки
20 KiB
JavaScript
(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);
|