<!DOCTYPE html>
<html>
<head>
<title>
SetTarget Followed by Linear or Exponential Ramp Is Continuous
</title>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../resources/audit-util.js"></script>
<script src="../resources/audit.js"></script>
<script src="../resources/audioparam-testing.js"></script>
</head>
<body>
<script id="layout-test-code">
let sampleRate = 48000;
let renderQuantum = 128;
// Test doesn't need to run for very long.
let renderDuration = 0.1;
// Where the ramp should end
let rampEndTime = renderDuration - .05;
let renderFrames = renderDuration * sampleRate;
let timeConstant = 0.01;
let audit = Audit.createTaskRunner();
// All of the tests start a SetTargetAtTime after one rendering quantum.
// The following tests handle various cases where a linear or exponential
// ramp is scheduled at or after SetTargetAtTime starts.
audit.define('linear ramp replace', (task, should) => {
// Schedule a linear ramp to start at the same time as SetTargetAtTime.
// This effectively replaces the SetTargetAtTime as if it never existed.
runTest(should, 'Linear ramp', {
automationFunction: function(audioparam, endValue, endTime) {
audioparam.linearRampToValueAtTime(endValue, endTime);
},
referenceFunction: linearResult,
automationTime: renderQuantum / sampleRate,
thresholdSetTarget: 0,
thresholdRamp: 1.26765e-6
}).then(() => task.done());
});
audit.define('delayed linear ramp', (task, should) => {
// Schedule a linear ramp to start after the SetTargetAtTime has already
// started rendering. This is the main test to verify that the linear
// ramp is continuous with the SetTargetAtTime curve.
runTest(should, 'Delayed linear ramp', {
automationFunction: function(audioparam, endValue, endTime) {
audioparam.linearRampToValueAtTime(endValue, endTime);
},
referenceFunction: linearResult,
automationTime: 4 * renderQuantum / sampleRate,
thresholdSetTarget: 3.43632e-7,
thresholdRamp: 1.07972e-6
}).then(() => task.done());
});
audit.define('expo ramp replace', (task, should) => {
// Like "linear ramp replace", but with an exponential ramp instead.
runTest(should, 'Exponential ramp', {
automationFunction: function(audioparam, endValue, endTime) {
audioparam.exponentialRampToValueAtTime(endValue, endTime);
},
referenceFunction: exponentialResult,
automationTime: renderQuantum / sampleRate,
thresholdSetTarget: 0,
thresholdRamp: 1.14441e-5
}).then(() => task.done());
});
audit.define('delayed expo ramp', (task, should) => {
// Like "delayed linear ramp", but with an exponential ramp instead.
runTest(should, 'Delayed exponential ramp', {
automationFunction: function(audioparam, endValue, endTime) {
audioparam.exponentialRampToValueAtTime(endValue, endTime);
},
referenceFunction: exponentialResult,
automationTime: 4 * renderQuantum / sampleRate,
thresholdSetTarget: 3.43632e-7,
thresholdRamp: 4.29154e-6
}).then(() => task.done());
});
audit.run();
function computeExpectedResult(
automationTime, timeConstant, endValue, endTime, rampFunction) {
// The result is a constant value of 1 for one rendering quantum, then a
// SetTarget event lasting to |automationTime|, at which point a ramp
// starts which ends at |endValue| at |endTime|. Then the rest of curve
// should be held constant at |endValue|.
let initialPart = new Array(renderQuantum);
initialPart.fill(1);
// Generate 1 extra frame so that we know where to start the linear
// ramp. The last sample of the array is where the ramp should start
// from.
let setTargetPart = createExponentialApproachArray(
renderQuantum / sampleRate, automationTime + 1 / sampleRate, 1, 0,
sampleRate, timeConstant);
let setTargetLength = setTargetPart.length;
// Generate the ramp starting at |automationTime| with a value from last
// value of the SetTarget curve above.
let rampPart = rampFunction(
automationTime, endTime, setTargetPart[setTargetLength - 1],
endValue, sampleRate);
// Finally finish out the rest with a constant value of |endValue|, if
// needed.
let finalPart =
new Array(Math.floor((renderDuration - endTime) * sampleRate));
finalPart.fill(endValue);
// Return the four parts separately for testing.
return {
initialPart: initialPart,
setTargetPart: setTargetPart.slice(0, setTargetLength - 1),
rampPart: rampPart,
tailPart: finalPart
};
}
function linearResult(automationTime, timeConstant, endValue, endTime) {
return computeExpectedResult(
automationTime, timeConstant, endValue, endTime,
createLinearRampArray);
}
function exponentialResult(
automationTime, timeConstant, endValue, endTime) {
return computeExpectedResult(
automationTime, timeConstant, endValue, endTime,
createExponentialRampArray);
}
// Run test to verify that a SetTarget followed by a ramp produces a
// continuous curve. |prefix| is a string to use as a prefix for the
// messages. |options| is a dictionary describing how the test is run:
//
// |options.automationFunction|
// The function to use to start the automation, which should be a
// linear or exponential ramp automation. This function has three
// arguments:
// audioparam - the AudioParam to be automated
// endValue - the end value of the ramp
// endTime - the end time fo the ramp.
// |options.referenceFunction|
// The function to generated the expected result. This function has
// four arguments:
// automationTime - the value of |options.automationTime|
// timeConstant - time constant used for SetTargetAtTime
// rampEndValue - end value for the ramp (same value used for
// automationFunction) rampEndTime - end time for the ramp (same
// value used for automationFunction)
// |options.automationTime|
// Time at which the |automationFunction| is called to start the
// automation.
// |options.thresholdSetTarget|
// Threshold to use for verifying that the initial (if any)
// SetTargetAtTime portion had the correct values.
// |options.thresholdRamp|
// Threshold to use for verifying that the ramp portion had the
// correct values.
function runTest(should, prefix, options) {
let automationFunction = options.automationFunction;
let referenceFunction = options.referenceFunction;
let automationTime = options.automationTime;
let thresholdSetTarget = options.thresholdSetTarget || 0;
let thresholdRamp = options.thresholdRamp || 0;
// End value for the ramp. Fairly arbitrary, but should be distinctly
// different from the target value for SetTargetAtTime and the initial
// value of gain.gain.
let rampEndValue = 2;
let context = new OfflineAudioContext(1, renderFrames, sampleRate);
// A constant source of amplitude 1.
let source = context.createBufferSource();
source.buffer = createConstantBuffer(context, 1, 1);
source.loop = true;
let gain = context.createGain();
// The SetTarget starts after one rendering quantum.
gain.gain.setTargetAtTime(
0, renderQuantum / context.sampleRate, timeConstant);
// Schedule the ramp at |automationTime|. If this time is past the
// first rendering quantum, the SetTarget event will run for a bit
// before running the ramp. Otherwise, the SetTarget should be
// completely replaced by the ramp.
context.suspend(automationTime).then(function() {
automationFunction(gain.gain, rampEndValue, rampEndTime);
context.resume();
});
source.connect(gain);
gain.connect(context.destination);
source.start();
return context.startRendering().then(function(resultBuffer) {
let success = true;
let result = resultBuffer.getChannelData(0);
let expected = referenceFunction(
automationTime, timeConstant, rampEndValue, rampEndTime);
// Verify each part of the curve separately.
let startIndex = 0;
let length = expected.initialPart.length;
// Verify that the initial part of the curve is constant.
should(result.slice(0, length), prefix + ': Initial part')
.beCloseToArray(expected.initialPart);
// Verify the SetTarget part of the curve, if the SetTarget did
// actually run.
startIndex += length;
length = expected.setTargetPart.length;
if (length) {
should(
result.slice(startIndex, startIndex + length),
prefix + ': SetTarget part')
.beCloseToArray(
expected.setTargetPart,
{absoluteThreshold: thresholdSetTarget});
} else {
should(!length, prefix + ': SetTarget part')
.message(
'was correctly replaced by the ramp',
'was incorrectly replaced by the ramp');
}
// Verify the ramp part of the curve
startIndex += length;
length = expected.rampPart.length;
should(result.slice(startIndex, startIndex + length), prefix)
.beCloseToArray(
expected.rampPart, {absoluteThreshold: thresholdRamp});
// Verify that the end of the curve after the ramp (if any) is a
// constant.
startIndex += length;
should(result.slice(startIndex), prefix + ': Tail part')
.beCloseToArray(expected.tailPart);
});
}
</script>
</body>
</html>