blob: f3985202dd48b63f5cf7410b1ed8395058829ae6 [file] [log] [blame]
<!DOCTYPE html>
<html>
<head>
<title>
Test CancelValuesAndHoldAtTime
</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>
</head>
<body>
<script id="layout-test-code">
let sampleRate = 48000;
let renderDuration = 0.5;
let audit = Audit.createTaskRunner();
// The first few tasks test the cancellation of each relevant automation
// function. For the test, a simple linear ramp from 0 to 1 is used to
// start things off. Then the automation to be tested is scheduled and
// cancelled.
audit.define(
{label: 'linear', description: 'Cancel linearRampToValueAtTime'},
function(task, should) {
cancelTest(should, linearRampTest('linearRampToValueAtTime'), {
valueThreshold: 8.3998e-5,
curveThreshold: 0
}).then(task.done.bind(task));
});
audit.define(
{label: 'exponential', description: 'Cancel exponentialRampAtTime'},
function(task, should) {
// Cancel an exponential ramp. The thresholds are experimentally
// determined.
cancelTest(should, function(g, v0, t0, cancelTime) {
// Initialize values to 0.
g[0].gain.setValueAtTime(0, 0);
g[1].gain.setValueAtTime(0, 0);
// Schedule a short linear ramp to start things off.
g[0].gain.linearRampToValueAtTime(v0, t0);
g[1].gain.linearRampToValueAtTime(v0, t0);
// After the linear ramp, schedule an exponential ramp to the end.
// (This is the event that will be be cancelled.)
let v1 = 0.001;
let t1 = renderDuration;
g[0].gain.exponentialRampToValueAtTime(v1, t1);
g[1].gain.exponentialRampToValueAtTime(v1, t1);
expectedConstant = Math.fround(
v0 * Math.pow(v1 / v0, (cancelTime - t0) / (t1 - t0)));
return {
expectedConstant: expectedConstant,
autoMessage: 'exponentialRampToValue(' + v1 + ', ' + t1 + ')',
summary: 'exponentialRampToValueAtTime',
};
}, {
valueThreshold: 1.8664e-6,
curveThreshold: 5.9605e-8
}).then(task.done.bind(task));
});
audit.define(
{label: 'setTarget', description: 'Cancel setTargetAtTime'},
function(task, should) {
// Cancel a setTarget event.
cancelTest(should, function(g, v0, t0, cancelTime) {
// Initialize values to 0.
g[0].gain.setValueAtTime(0, 0);
g[1].gain.setValueAtTime(0, 0);
// Schedule a short linear ramp to start things off.
g[0].gain.linearRampToValueAtTime(v0, t0);
g[1].gain.linearRampToValueAtTime(v0, t0);
// At the end of the linear ramp, schedule a setTarget. (This is
// the event that will be cancelled.)
let v1 = 0;
let t1 = t0;
let timeConstant = 0.05;
g[0].gain.setTargetAtTime(v1, t1, timeConstant);
g[1].gain.setTargetAtTime(v1, t1, timeConstant);
expectedConstant = Math.fround(
v1 + (v0 - v1) * Math.exp(-(cancelTime - t0) / timeConstant));
return {
expectedConstant: expectedConstant,
autoMessage: 'setTargetAtTime(' + v1 + ', ' + t1 + ', ' +
timeConstant + ')',
summary: 'setTargetAtTime',
};
}, {
valueThreshold: 4.5267e-7, // 1.1317e-7,
curveThreshold: 0
}).then(task.done.bind(task));
});
audit.define(
{label: 'setValueCurve', description: 'Cancel setValueCurveAtTime'},
function(task, should) {
// Cancel a setValueCurve event.
cancelTest(should, function(g, v0, t0, cancelTime) {
// Initialize values to 0.
g[0].gain.setValueAtTime(0, 0);
g[1].gain.setValueAtTime(0, 0);
// Schedule a short linear ramp to start things off.
g[0].gain.linearRampToValueAtTime(v0, t0);
g[1].gain.linearRampToValueAtTime(v0, t0);
// After the linear ramp, schedule a setValuesCurve. (This is the
// event that will be cancelled.)
let v1 = 0;
let duration = renderDuration - t0;
// For simplicity, a 2-point curve so we get a linear interpolated
// result.
let curve = Float32Array.from([v0, 0]);
g[0].gain.setValueCurveAtTime(curve, t0, duration);
g[1].gain.setValueCurveAtTime(curve, t0, duration);
let index =
Math.floor((curve.length - 1) / duration * (cancelTime - t0));
let curvePointsPerFrame =
(curve.length - 1) / duration / sampleRate;
let virtualIndex =
(cancelTime - t0) * sampleRate * curvePointsPerFrame;
let delta = virtualIndex - index;
expectedConstant = curve[0] + (curve[1] - curve[0]) * delta;
return {
expectedConstant: expectedConstant,
autoMessage: 'setValueCurveAtTime([' + curve + '], ' + t0 +
', ' + duration + ')',
summary: 'setValueCurveAtTime',
};
}, {
valueThreshold: 9.5368e-9,
curveThreshold: 0
}).then(task.done.bind(task));
});
audit.define(
{
label: 'setValueCurve after end',
description: 'Cancel setValueCurveAtTime after the end'
},
function(task, should) {
cancelTest(should, function(g, v0, t0, cancelTime) {
// Initialize values to 0.
g[0].gain.setValueAtTime(0, 0);
g[1].gain.setValueAtTime(0, 0);
// Schedule a short linear ramp to start things off.
g[0].gain.linearRampToValueAtTime(v0, t0);
g[1].gain.linearRampToValueAtTime(v0, t0);
// After the linear ramp, schedule a setValuesCurve. (This is the
// event that will be cancelled.) Make sure the curve ends before
// the cancellation time.
let v1 = 0;
let duration = cancelTime - t0 - 0.125;
// For simplicity, a 2-point curve so we get a linear interpolated
// result.
let curve = Float32Array.from([v0, 0]);
g[0].gain.setValueCurveAtTime(curve, t0, duration);
g[1].gain.setValueCurveAtTime(curve, t0, duration);
expectedConstant = curve[1];
return {
expectedConstant: expectedConstant,
autoMessage: 'setValueCurveAtTime([' + curve + '], ' + t0 +
', ' + duration + ')',
summary: 'setValueCurveAtTime',
};
}, {
valueThreshold: 0,
curveThreshold: 0
}).then(task.done.bind(task));
});
// Special case where we schedule a setTarget and there is no earlier
// automation event. This tests that we pick up the starting point
// correctly from the last setting of the AudioParam value attribute.
audit.define(
{
label: 'initial setTarget',
description: 'Cancel with initial setTargetAtTime'
},
function(task, should) {
cancelTest(should, function(g, v0, t0, cancelTime) {
let v1 = 0;
let timeConstant = 0.1;
g[0].gain.value = 1;
g[0].gain.setTargetAtTime(v1, t0, timeConstant);
g[1].gain.value = 1;
g[1].gain.setTargetAtTime(v1, t0, timeConstant);
let expectedConstant = Math.fround(
v1 + (v0 - v1) * Math.exp(-(cancelTime - t0) / timeConstant));
return {
expectedConstant: expectedConstant,
autoMessage: 'setTargetAtTime(' + v1 + ', ' + t0 + ', ' +
timeConstant + ')',
summary: 'Initial setTargetAtTime',
};
}, {
valueThreshold: 1.2320e-6,
curveThreshold: 0
}).then(task.done.bind(task));
});
// Test automations scheduled after the call to cancelAndHoldAtTime.
// Very similar to the above tests, but we also schedule an event after
// cancelAndHoldAtTime and verify that curve after cancellation has
// the correct values.
audit.define(
{
label: 'post cancel: Linear',
description: 'LinearRamp after cancelling'
},
function(task, should) {
// Run the cancel test using a linearRamp as the event to be
// cancelled. Then schedule another linear ramp after the
// cancellation.
cancelTest(
should,
linearRampTest('Post cancellation linearRampToValueAtTime'),
{valueThreshold: 8.3998e-5, curveThreshold: 0},
function(g, cancelTime, expectedConstant) {
// Schedule the linear ramp on g[0], and do the same for g[2],
// using the starting point given by expectedConstant.
let v2 = 2;
let t2 = cancelTime + 0.125;
g[0].gain.linearRampToValueAtTime(v2, t2);
g[2].gain.setValueAtTime(expectedConstant, cancelTime);
g[2].gain.linearRampToValueAtTime(v2, t2);
return {
constantEndTime: cancelTime,
message: 'Post linearRamp(' + v2 + ', ' + t2 + ')'
};
})
.then(task.done.bind(task));
});
audit.define(
{
label: 'post cancel: Exponential',
description: 'ExponentialRamp after cancelling'
},
function(task, should) {
// Run the cancel test using a linearRamp as the event to be
// cancelled. Then schedule an exponential ramp after the
// cancellation.
cancelTest(
should,
linearRampTest('Post cancel exponentialRampToValueAtTime'),
{valueThreshold: 8.3998e-5, curveThreshold: 0},
function(g, cancelTime, expectedConstant) {
// Schedule the exponential ramp on g[0], and do the same for
// g[2], using the starting point given by expectedConstant.
let v2 = 2;
let t2 = cancelTime + 0.125;
g[0].gain.exponentialRampToValueAtTime(v2, t2);
g[2].gain.setValueAtTime(expectedConstant, cancelTime);
g[2].gain.exponentialRampToValueAtTime(v2, t2);
return {
constantEndTime: cancelTime,
message: 'Post exponentialRamp(' + v2 + ', ' + t2 + ')'
};
})
.then(task.done.bind(task));
});
audit.define('post cancel: ValueCurve', function(task, should) {
// Run the cancel test using a linearRamp as the event to be cancelled.
// Then schedule a setValueCurve after the cancellation.
cancelTest(
should, linearRampTest('Post cancel setValueCurveAtTime'),
{valueThreshold: 8.3998e-5, curveThreshold: 0},
function(g, cancelTime, expectedConstant) {
// Schedule the exponential ramp on g[0], and do the same for
// g[2], using the starting point given by expectedConstant.
let t2 = cancelTime + 0.125;
let duration = 0.125;
let curve = Float32Array.from([.125, 2]);
g[0].gain.setValueCurveAtTime(curve, t2, duration);
g[2].gain.setValueAtTime(expectedConstant, cancelTime);
g[2].gain.setValueCurveAtTime(curve, t2, duration);
return {
constantEndTime: cancelTime,
message: 'Post setValueCurve([' + curve + '], ' + t2 + ', ' +
duration + ')',
errorThreshold: 8.3998e-5
};
})
.then(task.done.bind(task));
});
audit.define('post cancel: setTarget', function(task, should) {
// Run the cancel test using a linearRamp as the event to be cancelled.
// Then schedule a setTarget after the cancellation.
cancelTest(
should, linearRampTest('Post cancel setTargetAtTime'),
{valueThreshold: 8.3998e-5, curveThreshold: 0},
function(g, cancelTime, expectedConstant) {
// Schedule the exponential ramp on g[0], and do the same for
// g[2], using the starting point given by expectedConstant.
let v2 = 0.125;
let t2 = cancelTime + 0.125;
let timeConstant = 0.1;
g[0].gain.setTargetAtTime(v2, t2, timeConstant);
g[2].gain.setValueAtTime(expectedConstant, cancelTime);
g[2].gain.setTargetAtTime(v2, t2, timeConstant);
return {
constantEndTime: cancelTime + 0.125,
message: 'Post setTargetAtTime(' + v2 + ', ' + t2 + ', ' +
timeConstant + ')',
errorThreshold: 8.4037e-5
};
})
.then(task.done.bind(task));
});
audit.define('post cancel: setValue', function(task, should) {
// Run the cancel test using a linearRamp as the event to be cancelled.
// Then schedule a setTarget after the cancellation.
cancelTest(
should, linearRampTest('Post cancel setValueAtTime'),
{valueThreshold: 8.3998e-5, curveThreshold: 0},
function(g, cancelTime, expectedConstant) {
// Schedule the exponential ramp on g[0], and do the same for
// g[2], using the starting point given by expectedConstant.
let v2 = 0.125;
let t2 = cancelTime + 0.125;
g[0].gain.setValueAtTime(v2, t2);
g[2].gain.setValueAtTime(expectedConstant, cancelTime);
g[2].gain.setValueAtTime(v2, t2);
return {
constantEndTime: cancelTime + 0.125,
message: 'Post setValueAtTime(' + v2 + ', ' + t2 + ')'
};
})
.then(task.done.bind(task));
});
audit.run();
// Common function for doing a linearRamp test. This just does a linear
// ramp from 0 to v0 at from time 0 to t0. Then another linear ramp is
// scheduled from v0 to 0 from time t0 to t1. This is the ramp that is to
// be cancelled.
function linearRampTest(message) {
return function(g, v0, t0, cancelTime) {
g[0].gain.setValueAtTime(0, 0);
g[1].gain.setValueAtTime(0, 0);
g[0].gain.linearRampToValueAtTime(v0, t0);
g[1].gain.linearRampToValueAtTime(v0, t0);
let v1 = 0;
let t1 = renderDuration;
g[0].gain.linearRampToValueAtTime(v1, t1);
g[1].gain.linearRampToValueAtTime(v1, t1);
expectedConstant =
Math.fround(v0 + (v1 - v0) * (cancelTime - t0) / (t1 - t0));
return {
expectedConstant: expectedConstant,
autoMessage:
message + ': linearRampToValue(' + v1 + ', ' + t1 + ')',
summary: message,
};
}
}
// Run the cancellation test. A set of automations is created and
// canceled.
//
// |testFunction| is a function that generates the automation to be
// tested. It is given an array of 3 gain nodes, the value and time of an
// initial linear ramp, and the time where the cancellation should occur.
// The function must do the automations for the first two gain nodes. It
// must return a dictionary with |expectedConstant| being the value at the
// cancellation time, |autoMessage| for message to describe the test, and
// |summary| for general summary message to be printed at the end of the
// test.
//
// |thresholdOptions| is a property bag that specifies the error threshold
// to use. |thresholdOptions.valueThreshold| is the error threshold for
// comparing the actual constant output after cancelling to the expected
// value. |thresholdOptions.curveThreshold| is the error threshold for
// comparing the actual and expected automation curves before the
// cancelation point.
//
// For cancellation tests, |postCancelTest| is a function that schedules
// some automation after the cancellation. It takes 3 arguments: an array
// of the gain nodes, the cancellation time, and the expected value at the
// cancellation time. This function must return a dictionary consisting
// of |constantEndtime| indicating when the held constant from
// cancellation stops being constant, |message| giving a summary of what
// automation is being used, and |errorThreshold| that is the error
// threshold between the expected curve and the actual curve.
//
function cancelTest(
should, testerFunction, thresholdOptions, postCancelTest) {
// Create a context with three channels. Channel 0 is the test channel
// containing the actual output that includes the cancellation of
// events. Channel 1 is the expected data upto the cancellation so we
// can verify the cancellation produced the correct result. Channel 2
// is for verifying events inserted after the cancellation so we can
// verify that automations are correctly generated after the
// cancellation point.
let context =
new OfflineAudioContext(3, renderDuration * sampleRate, sampleRate);
// Test source is a constant signal
let src = context.createBufferSource();
src.buffer = createConstantBuffer(context, 1, 1);
src.loop = true;
// We'll do the automation tests with three gain nodes. One (g0) will
// have cancelAndHoldAtTime and the other (g1) will not. g1 is
// used as the expected result for that automation up to the
// cancellation point. They should be the same. The third node (g2) is
// used for testing automations inserted after the cancellation point,
// if any. g2 is the expected result from the cancellation point to the
// end of the test.
let g0 = context.createGain();
let g1 = context.createGain();
let g2 = context.createGain();
let v0 = 1;
let t0 = 0.01;
let cancelTime = renderDuration / 2;
// Test automation here. The tester function is responsible for setting
// up the gain nodes with the desired automation for testing.
autoResult = testerFunction([g0, g1, g2], v0, t0, cancelTime);
let expectedConstant = autoResult.expectedConstant;
let autoMessage = autoResult.autoMessage;
let summaryMessage = autoResult.summary;
// Cancel scheduled events somewhere in the middle of the test
// automation.
g0.gain.cancelAndHoldAtTime(cancelTime);
let constantEndTime;
if (postCancelTest) {
postResult =
postCancelTest([g0, g1, g2], cancelTime, expectedConstant);
constantEndTime = postResult.constantEndTime;
}
// Connect everything together (with a merger to make a two-channel
// result). Channel 0 is the test (with cancelAndHoldAtTime) and
// channel 1 is the reference (without cancelAndHoldAtTime).
// Channel 1 is used to verify that everything up to the cancellation
// has the correct values.
src.connect(g0);
src.connect(g1);
src.connect(g2);
let merger = context.createChannelMerger(3);
g0.connect(merger, 0, 0);
g1.connect(merger, 0, 1);
g2.connect(merger, 0, 2);
merger.connect(context.destination);
// Go!
src.start();
return context.startRendering().then(function(buffer) {
let actual = buffer.getChannelData(0);
let expected = buffer.getChannelData(1);
// The actual output should be a constant from the cancel time to the
// end. We use the last value of the actual output as the constant,
// but we also want to compare that with what we thought it should
// really be.
let cancelFrame = Math.ceil(cancelTime * sampleRate);
// Verify that the curves up to the cancel time are "identical". The
// should be but round-off may make them differ slightly due to the
// way cancelling is done.
let endFrame = Math.floor(cancelTime * sampleRate);
should(
actual.slice(0, endFrame),
autoMessage + ' up to time ' + cancelTime)
.beCloseToArray(
expected.slice(0, endFrame),
{absoluteThreshold: thresholdOptions.curveThreshold});
// Verify the output after the cancellation is a constant.
let actualTail;
let constantEndFrame;
if (postCancelTest) {
constantEndFrame = Math.ceil(constantEndTime * sampleRate);
actualTail = actual.slice(cancelFrame, constantEndFrame);
} else {
actualTail = actual.slice(cancelFrame);
}
let actualConstant = actual[cancelFrame];
should(
actualTail,
'Cancelling ' + autoMessage + ' at time ' + cancelTime)
.beConstantValueOf(actualConstant);
// Verify that the constant is the value we expect.
should(
actualConstant,
'Expected value for cancelling ' + autoMessage + ' at time ' +
cancelTime)
.beCloseTo(
expectedConstant,
{threshold: thresholdOptions.valueThreshold});
// Verify the curve after the constantEndTime matches our
// expectations.
if (postCancelTest) {
let c2 = buffer.getChannelData(2);
should(actual.slice(constantEndFrame), postResult.message)
.beCloseToArray(
c2.slice(constantEndFrame),
{absoluteThreshold: postResult.errorThreshold || 0});
}
});
}
</script>
</body>
</html>