Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
<!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>
Loading