blob: 3df3083d1a58ab9b635424046fda7041f064c712 [file] [log] [blame]
var sampleRate = 44100;
// Information about the starting/ending times and starting/ending values for each time interval.
var timeValueInfo;
// The difference between starting values between each time interval.
var 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.
var startEndValueChange;
// Default threshold to use for detecting discontinuities that should appear at each time interval.
var discontinuityThreshold;
// Time interval between value changes. It is best if 1 / numberOfTests is not close to timeInterval.
var timeInterval = .03;
// 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.
var timeConstant = timeInterval / 3;
var gainNode;
var 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 buffer containing the same constant value.
function createConstantBuffer(context, constant, length) {
var buffer = context.createBuffer(1, length, context.sampleRate);
var n = buffer.length;
var data = buffer.getChannelData(0);
for (var k = 0; k < n; ++k) {
data[k] = constant;
}
return buffer;
}
// 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)
{
var startFrame = timeToSampleFrame(startTime, sampleRate);
var endFrame = timeToSampleFrame(endTime, sampleRate);
var length = endFrame - startFrame;
var buffer = createConstantBuffer(context, value, length);
return buffer.getChannelData(0);
}
// 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)
{
var startFrame = timeToSampleFrame(startTime, sampleRate);
var endFrame = timeToSampleFrame(endTime, sampleRate);
var length = endFrame - startFrame;
var array = new Array(length);
var step = (endValue - startValue) / length;
for (k = 0; k < length; ++k) {
array[k] = startValue + k * step;
}
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)
{
var startFrame = timeToSampleFrame(startTime, sampleRate);
var endFrame = timeToSampleFrame(endTime, sampleRate);
var length = endFrame - startFrame;
var array = new Array(length);
var multiplier = Math.pow(endValue / startValue, 1 / length);
for (var k = 0; k < length; ++k) {
array[k] = startValue * Math.pow(multiplier, k);
}
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)
{
var startFrame = timeToSampleFrame(startTime, sampleRate);
var endFrame = timeToSampleFrame(endTime, sampleRate);
var length = endFrame - startFrame;
var array = new Array(length);
var c = discreteTimeConstantForSampleRate(timeConstant, sampleRate);
var value = startValue;
for (var k = 0; k < length; ++k) {
array[k] = value;
value += (targetValue - value) * c;
}
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)
{
var length = timeToSampleFrame(durationSeconds, sampleRate);
var signal = new Float32Array(length);
var omega = 2 * Math.PI * freqHz / sampleRate;
var halfAmplitude = amplitude / 2;
for (var 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;
}
}
// 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(rendered, expectedFunction, startTime, endTime, valueInfo, sampleRate)
{
var startSample = timeToSampleFrame(startTime, sampleRate);
var expected = expectedFunction(startTime, endTime, valueInfo.startValue, valueInfo.endValue, sampleRate, timeConstant);
var n = expected.length;
var maxError = -1;
var maxErrorIndex = -1;
for (var 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;
testFailed("NaN or infinity for rendered data at " + maxErrorIndex);
break;
}
if (!isValidNumber(expected[k])) {
maxError = Infinity;
maxErrorIndex = startSample + k;
testFailed("Nan or infinity for reference data at " + maxErrorIndex);
break;
}
var error = Math.abs(rendered[startSample + k] - expected[k]);
if (error > maxError) {
maxError = error;
maxErrorIndex = k;
}
}
return {maxError : maxError, index : maxErrorIndex};
}
// 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(values, times, threshold)
{
var n = values.length;
var success = true;
var badLocations = 0;
var breaks = [];
// Find discontinuities.
for (var k = 1; k < n; ++k) {
if (Math.abs(values[k] - values[k - 1]) > threshold) {
breaks.push(k);
}
}
var 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;
testFailed("Found more discontinuities (" + breaks.length + ") than expected. Only comparing first " + testCount + "discontinuities.");
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 (var k = 0; k < testCount; ++k) {
var expectedSampleFrame = timeToSampleFrame(times[k + 1], sampleRate);
if (breaks[k] != expectedSampleFrame) {
success = false;
++badLocations;
testFailed("Expected discontinuity at " + expectedSampleFrame + " but got " + breaks[k]);
}
}
if (badLocations) {
testFailed(badLocations + " discontinuities at incorrect locations");
success = false;
} else {
if (breaks.length == numberOfTests - 1) {
testPassed("All " + numberOfTests + " tests started and ended at the correct time.");
} else {
testFailed("Found " + breaks.length + " discontinuities but expected " + (numberOfTests - 1));
success = false;
}
}
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(testName, maxError, renderedData, expectedFunction, timeValueInfo, breakThreshold)
{
var success = true;
var failedTestCount = 0;
var times = timeValueInfo.times;
var values = timeValueInfo.values;
var n = values.length;
success = verifyDiscontinuities(renderedData, times, breakThreshold);
for (var k = 0; k < n; ++k) {
var result = comparePartialSignals(renderedData, expectedFunction, times[k], times[k + 1], values[k], sampleRate);
if (result.maxError > maxError) {
testFailed("Incorrect value for test " + k + ". Max error = " + result.maxError + " at offset " + (result.index + timeToSampleFrame(times[k], sampleRate)));
++failedTestCount;
}
}
if (failedTestCount) {
testFailed(failedTestCount + " tests failed out of " + n);
success = false;
} else {
testPassed("All " + n + " tests passed within an acceptable tolerance.");
}
if (success) {
testPassed("AudioParam " + testName + " test passed.");
} else {
testFailed("AudioParam " + testName + " test failed.");
}
}
// 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(testName, error, referenceFunction, jumpThreshold)
{
return function(event) {
var buffer = event.renderedBuffer;
renderedData = buffer.getChannelData(0);
var threshold;
if (!jumpThreshold) {
threshold = discontinuityThreshold;
} else {
threshold = jumpThreshold;
}
compareSignals(testName, error, renderedData, referenceFunction, timeValueInfo, threshold);
finishJSTest();
}
}
// 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)
{
var timeInfo = [0];
var valueInfo = [];
var value = initialValue;
for (var k = 0; k < numberOfTests; ++k) {
var startTime = k * timeInterval;
var endTime = (k + 1) * timeInterval;
var 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(numberOfTests, initialValue, setValueFunction, automationFunction, testName, maxError, referenceFunction, jumpThreshold)
{
if (window.testRunner) {
testRunner.dumpAsText();
testRunner.waitUntilDone();
}
window.jsTestIsAsync = true;
// Create offline audio context.
context = new OfflineAudioContext(2, renderLength(numberOfTests), sampleRate);
var constantBuffer = createConstantBuffer(context, 1, renderLength(numberOfTests));
// 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();
var 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(testName,
maxError,
referenceFunction,
jumpThreshold);
context.startRendering();
}