<!doctype html>
<html>
  <head>
    <title>Adding Events</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>
    <script src="/webaudio/resources/audio-param.js"></script>
  </head>

  <body>
    <script>
      let audit = Audit.createTaskRunner();

      // Arbitrary power of two to eliminate round-off in computing time from
      // frame.
      const sampleRate = 8192;

      audit.define(
          {
            label: 'linearRamp',
            description: 'Insert linearRamp after running for some time'
          },
          (task, should) => {
            testInsertion(should, {
              method: 'linearRampToValueAtTime',
              prefix: 'linearRamp'
            }).then(() => task.done());
          });

      audit.define(
          {
            label: 'expoRamp',
            description: 'Insert expoRamp after running for some time'
          },
          (task, should) => {
            testInsertion(should, {
              method: 'exponentialRampToValueAtTime',
              prefix: 'expoRamp'
            }).then(() => task.done());
          });

      // Test insertion of an event in the middle of rendering.
      //
      // options dictionary:
      //   method - automation method to test
      //   prefix - string to use for prefixing messages
      function testInsertion(should, options) {
        let {method, prefix} = options;

        // Channel 0 is the output for the test, and channel 1 is the
        // reference output.
        let context = new OfflineAudioContext(
            {numberOfChannels: 2, length: sampleRate, sampleRate: sampleRate});
        let merger = new ChannelMergerNode(
            context, {numberOfChannels: context.destination.channelCount});

        merger.connect(context.destination);

        // Initial value and final values of the source node
        let initialValue = 1;
        let finalValue = 2;

        // Set up the node for the automations under test
        let src = new ConstantSourceNode(context, {offset: initialValue});
        src.connect(merger, 0, 0);

        // Set initial event to occur at this time.  Keep it in the first
        // render quantum.
        const initialEventTime = 64 / context.sampleRate;
        should(
            () => src.offset.setValueAtTime(initialValue, initialEventTime),
            `${prefix}: setValueAtTime(${initialValue}, ${initialEventTime})`)
            .notThrow();

        // Let time pass and then add a new event with time in the future.
        let insertAtFrame = 512;
        let insertTime = insertAtFrame / context.sampleRate;
        let automationEndFrame = 1024 + 64;
        let automationEndTime = automationEndFrame / context.sampleRate;
        context.suspend(insertTime)
            .then(() => {
              should(
                  () => src.offset[method](finalValue, automationEndTime),
                  `${prefix}: At time ${insertTime} scheduling ${method}(${
                      finalValue}, ${automationEndTime})`)
                  .notThrow();
            })
            .then(() => context.resume());

        // Set up graph for the reference result.  Automate the source with
        // the events scheduled from the beginning.  Let the gain node
        // simulate the insertion of the event above.  This is done by
        // setting the gain to 1 at the insertion time.
        let srcRef = new ConstantSourceNode(context, {offset: 1});
        let g = new GainNode(context, {gain: 0});
        srcRef.connect(g).connect(merger, 0, 1);
        srcRef.offset.setValueAtTime(initialValue, initialEventTime);
        srcRef.offset[method](finalValue, automationEndTime);

        // Allow everything through after |insertFrame| frames.
        g.gain.setValueAtTime(1, insertTime);

        // Go!
        src.start();
        srcRef.start();

        return context.startRendering().then(audioBuffer => {
          let actual = audioBuffer.getChannelData(0);
          let expected = audioBuffer.getChannelData(1);

          // Verify that the output is 1 until we reach
          // insertAtFrame. Ignore the expected data because that always
          // produces 1.
          should(
              actual.slice(0, insertAtFrame),
              `${prefix}: output[0:${insertAtFrame - 1}]`)
              .beConstantValueOf(initialValue);

          // Verify ramp is correct by comparing it to the expected
          // data.
          should(
              actual.slice(
                  insertAtFrame, automationEndFrame - insertAtFrame + 1),
              `${prefix}: output[${insertAtFrame}:${
                  automationEndFrame - insertAtFrame}]`)
              .beCloseToArray(
                  expected.slice(
                      insertAtFrame, automationEndFrame - insertAtFrame + 1),
                  {absoluteThreshold: 0, numberOfArrayElements: 0});

          // Verify final output has the expected value
          should(
              actual.slice(automationEndFrame),
              `${prefix}: output[${automationEndFrame}:]`)
              .beConstantValueOf(finalValue);
        })
      }

      audit.run();
    </script>
  </body>
</html>
