summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webaudio/the-audio-api/the-audiobuffersourcenode-interface/sub-sample-scheduling.html
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/webaudio/the-audio-api/the-audiobuffersourcenode-interface/sub-sample-scheduling.html')
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-audiobuffersourcenode-interface/sub-sample-scheduling.html423
1 files changed, 423 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-audiobuffersourcenode-interface/sub-sample-scheduling.html b/testing/web-platform/tests/webaudio/the-audio-api/the-audiobuffersourcenode-interface/sub-sample-scheduling.html
new file mode 100644
index 0000000000..8c627f90f2
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-audiobuffersourcenode-interface/sub-sample-scheduling.html
@@ -0,0 +1,423 @@
+<!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>