blob: 8c627f90f22ca322daf890b9f2482fe0daad2176 [file] [log] [blame]
<!doctype html>
<html>
<head>
<title>
Test Sub-Sample Accurate Scheduling for ABSN
</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/webaudio/resources/audit-util.js"></script>
<script src="/webaudio/resources/audit.js"></script>
</head>
<body>
<script>
// Power of two so there's no roundoff converting from integer frames to
// time.
let sampleRate = 32768;
let audit = Audit.createTaskRunner();
audit.define('sub-sample accurate start', (task, should) => {
// There are two channels, one for each source. Only need to render
// quanta for this test.
let context = new OfflineAudioContext(
{numberOfChannels: 2, length: 8192, sampleRate: sampleRate});
let merger = new ChannelMergerNode(
context, {numberOfInputs: context.destination.channelCount});
merger.connect(context.destination);
// Use a simple linear ramp for the sources with integer steps starting
// at 1 to make it easy to verify and test that have sub-sample accurate
// start. Ramp starts at 1 so we can easily tell when the source
// starts.
let rampBuffer = new AudioBuffer(
{length: context.length, sampleRate: context.sampleRate});
let r = rampBuffer.getChannelData(0);
for (let k = 0; k < r.length; ++k) {
r[k] = k + 1;
}
const src0 = new AudioBufferSourceNode(context, {buffer: rampBuffer});
const src1 = new AudioBufferSourceNode(context, {buffer: rampBuffer});
// Frame where sources should start. This is pretty arbitrary, but one
// should be close to an integer and the other should be close to the
// next integer. We do this to catch the case where rounding of the
// start frame is being done. Rounding is incorrect.
const startFrame = 33;
const startFrame0 = startFrame + 0.1;
const startFrame1 = startFrame + 0.9;
src0.connect(merger, 0, 0);
src1.connect(merger, 0, 1);
src0.start(startFrame0 / context.sampleRate);
src1.start(startFrame1 / context.sampleRate);
context.startRendering()
.then(audioBuffer => {
const output0 = audioBuffer.getChannelData(0);
const output1 = audioBuffer.getChannelData(1);
// Compute the expected output by interpolating the ramp buffer of
// the sources if they started at the given frame.
const ramp = rampBuffer.getChannelData(0);
const expected0 = interpolateRamp(ramp, startFrame0);
const expected1 = interpolateRamp(ramp, startFrame1);
// Verify output0 has the correct values
// For information only
should(startFrame0, 'src0 start frame').beEqualTo(startFrame0);
// Output must be zero before the source start frame, and it must
// be interpolated correctly after the start frame. The
// absoluteThreshold below is currently set for Chrome which does
// linear interpolation. This needs to be updated eventually if
// other browsers do not user interpolation.
should(
output0.slice(0, startFrame + 1), `output0[0:${startFrame}]`)
.beConstantValueOf(0);
should(
output0.slice(startFrame + 1, expected0.length),
`output0[${startFrame + 1}:${expected0.length - 1}]`)
.beCloseToArray(
expected0.slice(startFrame + 1), {absoluteThreshold: 0});
// Verify output1 has the correct values. Same approach as for
// output0.
should(startFrame1, 'src1 start frame').beEqualTo(startFrame1);
should(
output1.slice(0, startFrame + 1), `output1[0:${startFrame}]`)
.beConstantValueOf(0);
should(
output1.slice(startFrame + 1, expected1.length),
`output1[${startFrame + 1}:${expected1.length - 1}]`)
.beCloseToArray(
expected1.slice(startFrame + 1), {absoluteThreshold: 0});
})
.then(() => task.done());
});
audit.define('sub-sample accurate stop', (task, should) => {
// There are threes channesl, one for each source. Only need to render
// quanta for this test.
let context = new OfflineAudioContext(
{numberOfChannels: 3, length: 128, sampleRate: sampleRate});
let merger = new ChannelMergerNode(
context, {numberOfInputs: context.destination.channelCount});
merger.connect(context.destination);
// The source can be as simple constant for this test.
let buffer = new AudioBuffer(
{length: context.length, sampleRate: context.sampleRate});
buffer.getChannelData(0).fill(1);
const src0 = new AudioBufferSourceNode(context, {buffer: buffer});
const src1 = new AudioBufferSourceNode(context, {buffer: buffer});
const src2 = new AudioBufferSourceNode(context, {buffer: buffer});
// Frame where sources should start. This is pretty arbitrary, but one
// should be an integer, one should be close to an integer and the other
// should be close to the next integer. This is to catch the case where
// rounding is used for the end frame. Rounding is incorrect.
const endFrame = 33;
const endFrame1 = endFrame + 0.1;
const endFrame2 = endFrame + 0.9;
src0.connect(merger, 0, 0);
src1.connect(merger, 0, 1);
src2.connect(merger, 0, 2);
src0.start(0);
src1.start(0);
src2.start(0);
src0.stop(endFrame / context.sampleRate);
src1.stop(endFrame1 / context.sampleRate);
src2.stop(endFrame2 / context.sampleRate);
context.startRendering()
.then(audioBuffer => {
let actual0 = audioBuffer.getChannelData(0);
let actual1 = audioBuffer.getChannelData(1);
let actual2 = audioBuffer.getChannelData(2);
// Just verify that we stopped at the right time.
// This is case where the end frame is an integer. Since the first
// output ends on an exact frame, the output must be zero at that
// frame number. We print the end frame for information only; it
// makes interpretation of the rest easier.
should(endFrame - 1, 'src0 end frame')
.beEqualTo(endFrame - 1);
should(actual0[endFrame - 1], `output0[${endFrame - 1}]`)
.notBeEqualTo(0);
should(actual0.slice(endFrame),
`output0[${endFrame}:]`)
.beConstantValueOf(0);
// The case where the end frame is just a little above an integer.
// The output must not be zero just before the end and must be zero
// after.
should(endFrame1, 'src1 end frame')
.beEqualTo(endFrame1);
should(actual1[endFrame], `output1[${endFrame}]`)
.notBeEqualTo(0);
should(actual1.slice(endFrame + 1),
`output1[${endFrame + 1}:]`)
.beConstantValueOf(0);
// The case where the end frame is just a little below an integer.
// The output must not be zero just before the end and must be zero
// after.
should(endFrame2, 'src2 end frame')
.beEqualTo(endFrame2);
should(actual2[endFrame], `output2[${endFrame}]`)
.notBeEqualTo(0);
should(actual2.slice(endFrame + 1),
`output2[${endFrame + 1}:]`)
.beConstantValueOf(0);
})
.then(() => task.done());
});
audit.define('sub-sample-grain', (task, should) => {
let context = new OfflineAudioContext(
{numberOfChannels: 2, length: 128, sampleRate: sampleRate});
let merger = new ChannelMergerNode(
context, {numberOfInputs: context.destination.channelCount});
merger.connect(context.destination);
// The source can be as simple constant for this test.
let buffer = new AudioBuffer(
{length: context.length, sampleRate: context.sampleRate});
buffer.getChannelData(0).fill(1);
let src0 = new AudioBufferSourceNode(context, {buffer: buffer});
let src1 = new AudioBufferSourceNode(context, {buffer: buffer});
src0.connect(merger, 0, 0);
src1.connect(merger, 0, 1);
// Start a short grain.
const src0StartGrain = 3.1;
const src0EndGrain = 37.2;
src0.start(
src0StartGrain / context.sampleRate, 0,
(src0EndGrain - src0StartGrain) / context.sampleRate);
const src1StartGrain = 5.8;
const src1EndGrain = 43.9;
src1.start(
src1StartGrain / context.sampleRate, 0,
(src1EndGrain - src1StartGrain) / context.sampleRate);
context.startRendering()
.then(audioBuffer => {
let output0 = audioBuffer.getChannelData(0);
let output1 = audioBuffer.getChannelData(1);
let expected = new Float32Array(context.length);
// Compute the expected output for output0 and verify the actual
// output matches.
expected.fill(1);
for (let k = 0; k <= Math.floor(src0StartGrain); ++k) {
expected[k] = 0;
}
for (let k = Math.ceil(src0EndGrain); k < expected.length; ++k) {
expected[k] = 0;
}
verifyGrain(should, output0, {
startGrain: src0StartGrain,
endGrain: src0EndGrain,
sourceName: 'src0',
outputName: 'output0'
});
verifyGrain(should, output1, {
startGrain: src1StartGrain,
endGrain: src1EndGrain,
sourceName: 'src1',
outputName: 'output1'
});
})
.then(() => task.done());
});
audit.define(
'sub-sample accurate start with playbackRate', (task, should) => {
// There are two channels, one for each source. Only need to render
// quanta for this test.
let context = new OfflineAudioContext(
{numberOfChannels: 2, length: 8192, sampleRate: sampleRate});
let merger = new ChannelMergerNode(
context, {numberOfInputs: context.destination.channelCount});
merger.connect(context.destination);
// Use a simple linear ramp for the sources with integer steps
// starting at 1 to make it easy to verify and test that have
// sub-sample accurate start. Ramp starts at 1 so we can easily
// tell when the source starts.
let buffer = new AudioBuffer(
{length: context.length, sampleRate: context.sampleRate});
let r = buffer.getChannelData(0);
for (let k = 0; k < r.length; ++k) {
r[k] = k + 1;
}
// Two sources with different playback rates
const src0 = new AudioBufferSourceNode(
context, {buffer: buffer, playbackRate: .25});
const src1 = new AudioBufferSourceNode(
context, {buffer: buffer, playbackRate: 4});
// Frame where sources start. Pretty arbitrary but should not be an
// integer.
const startFrame = 17.8;
src0.connect(merger, 0, 0);
src1.connect(merger, 0, 1);
src0.start(startFrame / context.sampleRate);
src1.start(startFrame / context.sampleRate);
context.startRendering()
.then(audioBuffer => {
const output0 = audioBuffer.getChannelData(0);
const output1 = audioBuffer.getChannelData(1);
const frameBefore = Math.floor(startFrame);
const frameAfter = frameBefore + 1;
// Informative message so we know what the following output
// indices really mean.
should(startFrame, 'Source start frame')
.beEqualTo(startFrame);
// Verify the output
// With a startFrame of 17.8, the first output is at frame 18,
// but the actual start is at 17.8. So we would interpolate
// the output 0.2 fraction of the way between 17.8 and 18, for
// an output of 1.2 for our ramp. But the playback rate is
// 0.25, so we're really only 1/4 as far along as we think so
// the output is .2*0.25 of the way between 1 and 2 or 1.05.
const ramp0 = buffer.getChannelData(0)[0];
const ramp1 = buffer.getChannelData(0)[1];
const src0Output = ramp0 +
(ramp1 - ramp0) * (frameAfter - startFrame) *
src0.playbackRate.value;
let playbackMessage =
`With playbackRate ${src0.playbackRate.value}:`;
should(
output0[frameBefore],
`${playbackMessage} output0[${frameBefore}]`)
.beEqualTo(0);
should(
output0[frameAfter],
`${playbackMessage} output0[${frameAfter}]`)
.beCloseTo(src0Output, {threshold: 4.542e-8});
const src1Output = ramp0 +
(ramp1 - ramp0) * (frameAfter - startFrame) *
src1.playbackRate.value;
playbackMessage =
`With playbackRate ${src1.playbackRate.value}:`;
should(
output1[frameBefore],
`${playbackMessage} output1[${frameBefore}]`)
.beEqualTo(0);
should(
output1[frameAfter],
`${playbackMessage} output1[${frameAfter}]`)
.beCloseTo(src1Output, {threshold: 4.542e-8});
})
.then(() => task.done());
});
audit.run();
// Given an input ramp in |rampBuffer|, interpolate the signal assuming
// this ramp is used for an ABSN that starts at frame |startFrame|, which
// is not necessarily an integer. For simplicity we just use linear
// interpolation here. The interpolation is not part of the spec but
// this should be pretty close to whatever interpolation is being done.
function interpolateRamp(rampBuffer, startFrame) {
// |start| is the last zero sample before the ABSN actually starts.
const start = Math.floor(startFrame);
// One less than the rampBuffer because we can't linearly interpolate
// the last frame.
let result = new Float32Array(rampBuffer.length - 1);
for (let k = 0; k <= start; ++k) {
result[k] = 0;
}
// Now start linear interpolation.
let frame = startFrame;
let index = 1;
for (let k = start + 1; k < result.length; ++k) {
let s0 = rampBuffer[index];
let s1 = rampBuffer[index - 1];
let delta = frame - k;
let s = s1 - delta * (s0 - s1);
result[k] = s;
++frame;
++index;
}
return result;
}
function verifyGrain(should, output, options) {
let {startGrain, endGrain, sourceName, outputName} = options;
let expected = new Float32Array(output.length);
// Compute the expected output for output and verify the actual
// output matches.
expected.fill(1);
for (let k = 0; k <= Math.floor(startGrain); ++k) {
expected[k] = 0;
}
for (let k = Math.ceil(endGrain); k < expected.length; ++k) {
expected[k] = 0;
}
should(startGrain, `${sourceName} grain start`).beEqualTo(startGrain);
should(endGrain - startGrain, `${sourceName} grain duration`)
.beEqualTo(endGrain - startGrain);
should(endGrain, `${sourceName} grain end`).beEqualTo(endGrain);
should(output, outputName).beEqualToArray(expected);
should(
output[Math.floor(startGrain)],
`${outputName}[${Math.floor(startGrain)}]`)
.beEqualTo(0);
should(
output[1 + Math.floor(startGrain)],
`${outputName}[${1 + Math.floor(startGrain)}]`)
.notBeEqualTo(0);
should(
output[Math.floor(endGrain)],
`${outputName}[${Math.floor(endGrain)}]`)
.notBeEqualTo(0);
should(
output[1 + Math.floor(endGrain)],
`${outputName}[${1 + Math.floor(endGrain)}]`)
.beEqualTo(0);
}
</script>
</body>
</html>