summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webaudio/the-audio-api/the-audiocontext-interface/audiocontext-suspend-resume-close.html
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/webaudio/the-audio-api/the-audiocontext-interface/audiocontext-suspend-resume-close.html')
-rw-r--r--testing/web-platform/tests/webaudio/the-audio-api/the-audiocontext-interface/audiocontext-suspend-resume-close.html406
1 files changed, 406 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webaudio/the-audio-api/the-audiocontext-interface/audiocontext-suspend-resume-close.html b/testing/web-platform/tests/webaudio/the-audio-api/the-audiocontext-interface/audiocontext-suspend-resume-close.html
new file mode 100644
index 0000000000..192317dda2
--- /dev/null
+++ b/testing/web-platform/tests/webaudio/the-audio-api/the-audiocontext-interface/audiocontext-suspend-resume-close.html
@@ -0,0 +1,406 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script type="module">
+"use strict";
+
+function tryToCreateNodeOnClosedContext(ctx) {
+ assert_equals(ctx.state, "closed", "The context is in closed state");
+
+ [
+ { name: "createBufferSource" },
+ {
+ name: "createMediaStreamDestination",
+ onOfflineAudioContext: false,
+ },
+ { name: "createScriptProcessor" },
+ { name: "createStereoPanner" },
+ { name: "createAnalyser" },
+ { name: "createGain" },
+ { name: "createDelay" },
+ { name: "createBiquadFilter" },
+ { name: "createWaveShaper" },
+ { name: "createPanner" },
+ { name: "createConvolver" },
+ { name: "createChannelSplitter" },
+ { name: "createChannelMerger" },
+ { name: "createDynamicsCompressor" },
+ { name: "createOscillator" },
+ {
+ name: "createMediaElementSource",
+ args: [new Audio()],
+ onOfflineAudioContext: false,
+ },
+ {
+ name: "createMediaStreamSource",
+ args: [new AudioContext().createMediaStreamDestination().stream],
+ onOfflineAudioContext: false,
+ },
+ ].forEach(function (e) {
+ if (
+ e.onOfflineAudioContext == false &&
+ ctx instanceof OfflineAudioContext
+ ) {
+ return;
+ }
+
+ try {
+ ctx[e.name].apply(ctx, e.args);
+ } catch (err) {
+ assert_true(false, "unexpected exception thrown for " + e.name);
+ }
+ });
+}
+
+function loadFile(url, callback) {
+ return new Promise((resolve) => {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.responseType = "arraybuffer";
+ xhr.onload = function () {
+ resolve(xhr.response);
+ };
+ xhr.send();
+ });
+}
+
+// createBuffer, createPeriodicWave and decodeAudioData should work on a context
+// that has `state` == "closed"
+async function tryLegalOpeerationsOnClosedContext(ctx) {
+ assert_equals(ctx.state, "closed", "The context is in closed state");
+
+ [
+ { name: "createBuffer", args: [1, 44100, 44100] },
+ {
+ name: "createPeriodicWave",
+ args: [new Float32Array(10), new Float32Array(10)],
+ },
+ ].forEach(function (e) {
+ try {
+ ctx[e.name].apply(ctx, e.args);
+ } catch (err) {
+ assert_true(false, "unexpected exception thrown");
+ }
+ });
+ var buf = await loadFile("/webaudio/resources/sin_440Hz_-6dBFS_1s.wav");
+ return ctx
+ .decodeAudioData(buf)
+ .then(function (decodedBuf) {
+ assert_true(
+ true,
+ "decodeAudioData on a closed context should work, it did."
+ );
+ })
+ .catch(function (e) {
+ assert_true(
+ false,
+ "decodeAudioData on a closed context should work, it did not"
+ );
+ });
+}
+
+// Test that MediaStreams that are the output of a suspended AudioContext are
+// producing silence
+// ac1 produce a sine fed to a MediaStreamAudioDestinationNode
+// ac2 is connected to ac1 with a MediaStreamAudioSourceNode, and check that
+// there is silence when ac1 is suspended
+async function testMultiContextOutput() {
+ var ac1 = new AudioContext(),
+ ac2 = new AudioContext();
+
+ await new Promise((resolve) => (ac1.onstatechange = resolve));
+
+ ac1.onstatechange = null;
+ await ac1.suspend();
+ assert_equals(ac1.state, "suspended", "ac1 is suspended");
+ var osc1 = ac1.createOscillator(),
+ mediaStreamDestination1 = ac1.createMediaStreamDestination();
+
+ var mediaStreamAudioSourceNode2 = ac2.createMediaStreamSource(
+ mediaStreamDestination1.stream
+ ),
+ sp2 = ac2.createScriptProcessor(),
+ silentBuffersInARow = 0;
+
+ osc1.connect(mediaStreamDestination1);
+ mediaStreamAudioSourceNode2.connect(sp2);
+ osc1.start();
+
+ let e = await new Promise((resolve) => (sp2.onaudioprocess = resolve));
+
+ while (true) {
+ let e = await new Promise(
+ (resolve) => (sp2.onaudioprocess = resolve)
+ );
+ var input = e.inputBuffer.getChannelData(0);
+ var silent = true;
+ for (var i = 0; i < input.length; i++) {
+ if (input[i] != 0.0) {
+ silent = false;
+ }
+ }
+
+ if (silent) {
+ silentBuffersInARow++;
+ if (silentBuffersInARow == 10) {
+ assert_true(
+ true,
+ "MediaStreams produce silence when their input is blocked."
+ );
+ break;
+ }
+ } else {
+ assert_equals(
+ silentBuffersInARow,
+ 0,
+ "No non silent buffer inbetween silent buffers."
+ );
+ }
+ }
+
+ sp2.onaudioprocess = null;
+ ac1.close();
+ ac2.close();
+}
+
+// Test that there is no buffering between contexts when connecting a running
+// AudioContext to a suspended AudioContext. Gecko's ScriptProcessorNode does some
+// buffering internally, so we ensure this by using a very very low frequency
+// on a sine, and oberve that the phase has changed by a big enough margin.
+async function testMultiContextInput() {
+ var ac1 = new AudioContext(),
+ ac2 = new AudioContext();
+
+ await new Promise((resolve) => (ac1.onstatechange = resolve));
+ ac1.onstatechange = null;
+
+ var osc1 = ac1.createOscillator(),
+ mediaStreamDestination1 = ac1.createMediaStreamDestination(),
+ sp1 = ac1.createScriptProcessor();
+
+ var mediaStreamAudioSourceNode2 = ac2.createMediaStreamSource(
+ mediaStreamDestination1.stream
+ ),
+ sp2 = ac2.createScriptProcessor(),
+ eventReceived = 0;
+
+ osc1.frequency.value = 0.0001;
+ osc1.connect(mediaStreamDestination1);
+ osc1.connect(sp1);
+ mediaStreamAudioSourceNode2.connect(sp2);
+ osc1.start();
+
+ var e = await new Promise((resolve) => (sp2.onaudioprocess = resolve));
+ var inputBuffer1 = e.inputBuffer.getChannelData(0);
+ sp2.value = inputBuffer1[inputBuffer1.length - 1];
+ await ac2.suspend();
+ await ac2.resume();
+
+ while (true) {
+ var e = await new Promise(
+ (resolve) => (sp2.onaudioprocess = resolve)
+ );
+ var inputBuffer = e.inputBuffer.getChannelData(0);
+ if (eventReceived++ == 3) {
+ var delta = Math.abs(inputBuffer[1] - sp2.value),
+ theoreticalIncrement =
+ (2048 * 3 * Math.PI * 2 * osc1.frequency.value) /
+ ac1.sampleRate;
+ assert_true(
+ delta >= theoreticalIncrement,
+ "Buffering did not occur when the context was suspended (delta:" +
+ delta +
+ " increment: " +
+ theoreticalIncrement +
+ ")"
+ );
+ break;
+ }
+ }
+ ac1.close();
+ ac2.close();
+ sp1.onaudioprocess = null;
+ sp2.onaudioprocess = null;
+}
+
+// Take an AudioContext, make sure it switches to running when the audio starts
+// flowing, and then, call suspend, resume and close on it, tracking its state.
+async function testAudioContext() {
+ var ac = new AudioContext();
+ assert_equals(
+ ac.state,
+ "suspended",
+ "AudioContext should start in suspended state."
+ );
+ var stateTracker = {
+ previous: ac.state,
+ // no promise for the initial suspended -> running
+ initial: { handler: false },
+ suspend: { promise: false, handler: false },
+ resume: { promise: false, handler: false },
+ close: { promise: false, handler: false },
+ };
+
+ await new Promise((resolve) => (ac.onstatechange = resolve));
+
+ assert_true(
+ stateTracker.previous == "suspended" && ac.state == "running",
+ 'AudioContext should switch to "running" when the audio hardware is' +
+ " ready."
+ );
+
+ stateTracker.previous = ac.state;
+ stateTracker.initial.handler = true;
+
+ let promise_statechange_suspend = new Promise((resolve) => {
+ ac.onstatechange = resolve;
+ }).then(() => {
+ stateTracker.suspend.handler = true;
+ });
+ await ac.suspend();
+ assert_true(
+ !stateTracker.suspend.handler,
+ "Promise should be resolved before the callback."
+ );
+ assert_equals(
+ ac.state,
+ "suspended",
+ 'AudioContext should switch to "suspended" when the audio stream is ' +
+ "suspended."
+ );
+ await promise_statechange_suspend;
+ stateTracker.previous = ac.state;
+
+ let promise_statechange_resume = new Promise((resolve) => {
+ ac.onstatechange = resolve;
+ }).then(() => {
+ stateTracker.resume.handler = true;
+ });
+ await ac.resume();
+ assert_true(
+ !stateTracker.resume.handler,
+ "Promise should be resolved before the callback."
+ );
+ assert_equals(
+ ac.state,
+ "running",
+ 'AudioContext should switch to "running" when the audio stream is ' +
+ "resumed."
+ );
+ await promise_statechange_resume;
+ stateTracker.previous = ac.state;
+
+ let promise_statechange_close = new Promise((resolve) => {
+ ac.onstatechange = resolve;
+ }).then(() => {
+ stateTracker.close.handler = true;
+ });
+ await ac.close();
+ assert_true(
+ !stateTracker.close.handler,
+ "Promise should be resolved before the callback."
+ );
+ assert_equals(
+ ac.state,
+ "closed",
+ 'AudioContext should switch to "closed" when the audio stream is ' +
+ "closed."
+ );
+ await promise_statechange_close;
+ stateTracker.previous = ac.state;
+
+ tryToCreateNodeOnClosedContext(ac);
+ await tryLegalOpeerationsOnClosedContext(ac);
+}
+
+async function testOfflineAudioContext() {
+ var o = new OfflineAudioContext(1, 44100, 44100);
+ assert_equals(
+ o.state,
+ "suspended",
+ "OfflineAudioContext should start in suspended state."
+ );
+
+ var previousState = o.state,
+ finishedRendering = false;
+
+ o.startRendering().then(function (buffer) {
+ finishedRendering = true;
+ });
+
+ await new Promise((resolve) => (o.onstatechange = resolve));
+
+ assert_true(
+ previousState == "suspended" && o.state == "running",
+ "onstatechanged" +
+ "handler is called on state changed, and the new state is running"
+ );
+ previousState = o.state;
+ await new Promise((resolve) => (o.onstatechange = resolve));
+ assert_true(
+ previousState == "running" && o.state == "closed",
+ "onstatechanged handler is called when rendering finishes, " +
+ "and the new state is closed"
+ );
+ assert_true(
+ finishedRendering,
+ "The Promise that is resolved when the rendering is " +
+ "done should be resolved earlier than the state change."
+ );
+ previousState = o.state;
+ function afterRenderingFinished() {
+ assert_true(
+ false,
+ "There should be no transition out of the closed state."
+ );
+ }
+ o.onstatechange = afterRenderingFinished;
+
+ tryToCreateNodeOnClosedContext(o);
+ await tryLegalOpeerationsOnClosedContext(o);
+}
+
+async function testSuspendResumeEventLoop() {
+ var ac = new AudioContext();
+ var source = ac.createBufferSource();
+ source.buffer = ac.createBuffer(1, 44100, 44100);
+ await new Promise((resolve) => (ac.onstatechange = resolve));
+ ac.onstatechange = null;
+ assert_true(ac.state == "running", "initial state is running");
+ await ac.suspend();
+ source.start();
+ ac.resume();
+ await new Promise((resolve) => (source.onended = resolve));
+ assert_true(true, "The AudioContext did resume");
+}
+
+function testResumeInStateChangeForResumeCallback() {
+ return new Promise((resolve) => {
+ var ac = new AudioContext();
+ ac.onstatechange = function () {
+ ac.resume().then(() => {
+ assert_true(true, "resume promise resolved as expected.");
+ resolve();
+ });
+ };
+ });
+}
+
+var tests = [
+ testOfflineAudioContext,
+ testMultiContextOutput,
+ testMultiContextInput,
+ testSuspendResumeEventLoop,
+ testResumeInStateChangeForResumeCallback,
+ testAudioContext,
+];
+
+tests.forEach(function (f) {
+ promise_test(f, f.name);
+});
+ </script>
+ </head>
+</html>