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