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