Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

  • This test has a WPT meta file that expects 10 subtest issues.
  • This WPT test may be referenced by the following Test IDs:
<!DOCTYPE html>
<html>
<head>
<title>AudioBufferSourceNode: Negative PlaybackRate Edge Cases</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<script>
/**
* Helper to run a test on AudioBufferSourceNode.
* @param {number} renderFrames - Total frames to render in
* OfflineAudioContext.
* @param {number} bufferFrames - Number of frames in the AudioBuffer.
* @param {Function} setupSource - Callback to configure:
* (source, sampleRate) => void
* @param {Function} verifyResult - Callback to verify the output:
* (renderedData) => void
* @param {Array|null} bufferData - Optional initial buffer data. If null,
* populated with 1.0, 2.0, 3.0...
*/
async function runSourceNodeTest(
renderFrames, bufferFrames, setupSource, verifyResult,
bufferData = null) {
const sampleRate = 44100;
const context = new OfflineAudioContext(1, renderFrames, sampleRate);
const buffer = new AudioBuffer({
numberOfChannels: 1,
length: bufferFrames,
sampleRate: sampleRate
});
const channelData = buffer.getChannelData(0);
if (bufferData) {
channelData.set(bufferData);
} else {
// Populate with distinct values to easily verify interpolation.
for (let i = 0; i < bufferFrames; i++) {
channelData[i] = i + 1.0;
}
}
const source = new AudioBufferSourceNode(context, { buffer });
source.connect(context.destination);
setupSource(source, sampleRate);
const renderedBuffer = await context.startRendering();
verifyResult(renderedBuffer.getChannelData(0));
}
promise_test(async () => {
await runSourceNodeTest(
4, 4,
(source, sampleRate) => {
source.playbackRate.value = -1.0;
source.start(0, 3 / sampleRate);
},
(renderedData) => {
assert_equals(renderedData[0], 4.0, 'Frame 0 should be 4.0');
assert_equals(renderedData[1], 3.0, 'Frame 1 should be 3.0');
assert_equals(renderedData[2], 2.0, 'Frame 2 should be 2.0');
assert_equals(renderedData[3], 1.0, 'Frame 3 should be 1.0');
}
);
}, 'AudioBufferSourceNode supports negative playbackRate (-1.0)');
promise_test(async () => {
await runSourceNodeTest(
4, 4,
(source, sampleRate) => {
source.playbackRate.value = -2.0;
source.start(0, 3 / sampleRate);
},
(renderedData) => {
assert_equals(renderedData[0], 4.0, 'Frame 0 should be 4.0');
assert_equals(renderedData[1], 2.0, 'Frame 1 should be 2.0');
assert_equals(renderedData[2], 0.0, 'Frame 2 should be silence');
assert_equals(renderedData[3], 0.0, 'Frame 3 should be silence');
}
);
}, 'AudioBufferSourceNode supports high negative playbackRate (-2.0)');
promise_test(async () => {
await runSourceNodeTest(
4, 8,
(source, sampleRate) => {
source.playbackRate.value = -5.0;
source.start(0, 7 / sampleRate);
},
(renderedData) => {
assert_equals(renderedData[0], 8.0, 'Frame 0 should be 8.0');
assert_equals(renderedData[1], 3.0, 'Frame 1 should be 3.0');
assert_equals(renderedData[2], 0.0, 'Frame 2 should be silence');
assert_equals(renderedData[3], 0.0, 'Frame 3 should be silence');
}
);
}, 'AudioBufferSourceNode supports high negative playbackRate (-5.0)');
promise_test(async () => {
await runSourceNodeTest(
4, 2,
(source, sampleRate) => {
source.playbackRate.value = -0.5;
source.start(0, 1 / sampleRate);
},
(renderedData) => {
assert_equals(renderedData[0], 2.0, 'Frame 0 should be 2.0');
assert_equals(renderedData[1], 1.5, 'Frame 1 should be 1.5');
assert_equals(renderedData[2], 1.0, 'Frame 2 should be 1.0');
assert_equals(renderedData[3], 0.0, 'Frame 3 should be silence');
}
);
}, 'AudioBufferSourceNode supports fractional negative playbackRate');
promise_test(async () => {
await runSourceNodeTest(
4, 4,
(source, sampleRate) => {
source.playbackRate.value = -0.02;
source.start(0, 3 / sampleRate);
},
(renderedData) => {
assert_approx_equals(renderedData[0], 4.00, 1e-4, 'Frame 0');
assert_approx_equals(renderedData[1], 3.98, 1e-4, 'Frame 1');
assert_approx_equals(renderedData[2], 3.96, 1e-4, 'Frame 2');
assert_approx_equals(renderedData[3], 3.94, 1e-4, 'Frame 3');
}
);
}, 'AudioBufferSourceNode supports very low negative playbackRate (-0.02)');
// EDGE CASE: Offset post loopEnd
promise_test(async () => {
await runSourceNodeTest(
6, 8,
(source, sampleRate) => {
source.loop = true;
source.loopStart = 2 / sampleRate;
source.loopEnd = 4 / sampleRate;
source.playbackRate.value = -1.0;
source.start(0, 6 / sampleRate);
},
(renderedData) => {
const expected = [7.0, 6.0, 5.0, 4.0, 3.0, 4.0];
for (let i = 0; i < expected.length; i++) {
assert_equals(
renderedData[i], expected[i],
`Frame ${i} should be ${expected[i]}`);
}
}
);
}, 'AudioBufferSourceNode enters loop backwards from offset > loopEnd');
// EDGE CASE: offset < loopStart
promise_test(async () => {
await runSourceNodeTest(
4, 8,
(source, sampleRate) => {
source.loop = true;
source.loopStart = 4 / sampleRate;
source.loopEnd = 6 / sampleRate;
source.playbackRate.value = -1.0;
source.start(0, 2 / sampleRate);
},
(renderedData) => {
// Spec clamps offset to loopStart when playbackRate < 0.
const expected = [5.0, 6.0, 5.0, 6.0];
for (let i = 0; i < expected.length; i++) {
assert_equals(
renderedData[i], expected[i],
`Frame ${i} should be ${expected[i]}`);
}
}
);
}, 'AudioBufferSourceNode clamps offset to loopStart for playbackRate < 0');
// EDGE CASE: offset in loop
promise_test(async () => {
await runSourceNodeTest(
8, 4,
(source, sampleRate) => {
source.playbackRate.value = -1.0;
source.loop = true;
source.loopStart = 1 / sampleRate;
source.loopEnd = 3 / sampleRate;
source.start(0, 2 / sampleRate);
},
(renderedData) => {
const expected = [3.0, 2.0, 3.0, 2.0, 3.0, 2.0, 3.0, 2.0];
for (let i = 0; i < expected.length; i++) {
assert_equals(
renderedData[i], expected[i],
`Frame ${i} should be ${expected[i]}`);
}
}
);
}, 'AudioBufferSourceNode loops backwards with negative rate ' +
'(offset in loop)');
// EDGE CASE: offset = buffer.duration
promise_test(async () => {
await runSourceNodeTest(
4, 4,
(source, sampleRate) => {
source.playbackRate.value = -1.0;
source.start(0, 4 / sampleRate);
},
(renderedData) => {
const expected = [0.0, 4.0, 3.0, 2.0];
for (let i = 0; i < expected.length; i++) {
assert_equals(
renderedData[i], expected[i],
`Frame ${i} should be ${expected[i]}`);
}
}
);
}, 'AudioBufferSourceNode handles offset exactly at duration with ' +
'negative rate');
// EDGE CASE: offset > buffer.duration (out-of-bounds offset clamped)
promise_test(async () => {
await runSourceNodeTest(
4, 4,
(source, sampleRate) => {
source.playbackRate.value = -1.0;
source.start(0, 10 / sampleRate);
},
(renderedData) => {
assert_equals(renderedData[0], 0.0, 'Frame 0 should be silence');
assert_equals(renderedData[1], 4.0, 'Frame 1 should be 4.0');
assert_equals(renderedData[2], 3.0, 'Frame 2 should be 3.0');
assert_equals(renderedData[3], 2.0, 'Frame 3 should be 2.0');
}
);
}, 'AudioBufferSourceNode handles out-of-bounds start offset correctly');
// Sub-sample interpolation backwards
promise_test(async () => {
await runSourceNodeTest(
4, 4,
(source, sampleRate) => {
source.playbackRate.value = -1.0;
source.start(0, 2.5 / sampleRate);
},
(renderedData) => {
const expected = [3.5, 2.5, 1.5, 0.0];
for (let i = 0; i < expected.length; i++) {
assert_approx_equals(
renderedData[i], expected[i], 1e-4,
`Frame ${i} should be approx ${expected[i]}`);
}
}
);
}, 'AudioBufferSourceNode performs sub-sample interpolation backwards');
promise_test(async () => {
await runSourceNodeTest(
4, 4,
(source, sampleRate) => {
source.loop = true;
source.loopStart = 2 / sampleRate;
source.loopEnd = 2 / sampleRate;
source.playbackRate.value = 0;
source.start(0, 2 / sampleRate);
},
(renderedData) => {
assert_true(true, 'Test ran successfully!');
}
);
}, 'AudioBufferSourceNode ran successfully with zero delta and ' +
'playbackRate 0');
</script>
</body>
</html>