<!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 tryLegalOperationsOnClosedContext(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 tryLegalOperationsOnClosedContext(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 tryLegalOperationsOnClosedContext(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>
