<!doctype html>
<html>
  <head>
    <title>Test AudioParam events very close in time</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>
      const audit = Audit.createTaskRunner();

      // Largest sample rate that is required to be supported and is a power of
      // two, to eliminate round-off as much as possible.
      const sampleRate = 65536;

      // Only need one render quantum for testing.
      const testFrames = 128;

      // Largest representable single-float number
      const floatMax = Math.fround(3.4028234663852886e38);

      // epspos is the smallest x such that 1 + x != 1
      const epspos = 1.1102230246251568e-16;
      // epsneg is the smallest x such that 1 - x != 1
      const epsneg = 5.551115123125784e-17;

      audit.define(
          {label: 'no-nan', description: 'NaN does not occur'},
          (task, should) => {
            const context = new OfflineAudioContext({
              numberOfChannels: 1,
              sampleRate: sampleRate,
              length: testFrames
            });

            const src0 = new ConstantSourceNode(context, {offset: 0});

            // This should always succeed.  We just want to print out a message
            // that |src0| is a constant source node for the following
            // processing.
            should(src0, 'src0 = new ConstantSourceNode(context, {offset: 0})')
                .beEqualTo(src0);

            src0.connect(context.destination);

            // Values for the first event (setValue).  |time1| MUST be 0.
            const time1 = 0;
            const value1 = 10;

            // Values for the second event (linearRamp).  |value2| must be huge,
            // and |time2| must be small enough that 1/|time2| overflows a
            // single float. This value is the least positive single float.
            const value2 = floatMax;
            const time2 = 1.401298464324817e-45;

            // These should always succeed; the messages are just informational
            // to show the events that we scheduled.
            should(
                src0.offset.setValueAtTime(value1, time1),
                `src0.offset.setValueAtTime(${value1}, ${time1})`)
                .beEqualTo(src0.offset);
            should(
                src0.offset.linearRampToValueAtTime(value2, time2),
                `src0.offset.linearRampToValueAtTime(${value2}, ${time2})`)
                .beEqualTo(src0.offset);

            src0.start();

            context.startRendering()
                .then(buffer => {
                  const output = buffer.getChannelData(0);

                  // Since time1 = 0, the output at frame 0 MUST be value1.
                  should(output[0], 'output[0]').beEqualTo(value1);

                  // Since time2 < 1, output from frame 1 and later must be a
                  // constant.
                  should(output.slice(1), 'output[1]')
                      .beConstantValueOf(value2);
                })
                .then(() => task.done());
          });

      audit.define(
          {label: 'interpolation', description: 'Interpolation of linear ramp'},
          (task, should) => {
            const context = new OfflineAudioContext({
              numberOfChannels: 1,
              sampleRate: sampleRate,
              length: testFrames
            });

            const src1 = new ConstantSourceNode(context, {offset: 0});

            // This should always succeed.  We just want to print out a message
            // that |src1| is a constant source node for the following
            // processing.
            should(src1, 'src1 = new ConstantSourceNode(context, {offset: 0})')
                .beEqualTo(src1);

            src1.connect(context.destination);

            const frame = 1;

            // These time values are arranged so that time1 < frame/sampleRate <
            // time2.  This means we need to interpolate to get a value at given
            // frame.
            //
            // The values are not so important, but |value2| should be huge.
            const time1 = frame * (1 - epsneg) / context.sampleRate;
            const value1 = 1e15;

            const time2 = frame * (1 + epspos) / context.sampleRate;
            const value2 = floatMax;

            should(
                src1.offset.setValueAtTime(value1, time1),
                `src1.offset.setValueAtTime(${value1}, ${time1})`)
                .beEqualTo(src1.offset);
            should(
                src1.offset.linearRampToValueAtTime(value2, time2),
                `src1.offset.linearRampToValueAtTime(${value2}, ${time2})`)
                .beEqualTo(src1.offset);

            src1.start();

            context.startRendering()
                .then(buffer => {
                  const output = buffer.getChannelData(0);

                  // Sanity check
                  should(time2 - time1, 'Event time difference')
                      .notBeEqualTo(0);

                  // Because 0 < time1 < 1, output must be 0 at time 0.
                  should(output[0], 'output[0]').beEqualTo(0);

                  // Because time1 < 1/sampleRate < time2, we need to
                  // interpolate the value between these times to determine the
                  // output at frame 1.
                  const t = frame / context.sampleRate;
                  const v = value1 +
                      (value2 - value1) * (t - time1) / (time2 - time1);

                  should(output[1], 'output[1]').beCloseTo(v, {threshold: 0});

                  // Because 1 < time2 < 2, the output at frame 2 and higher is
                  // constant.
                  should(output.slice(2), 'output[2:]')
                      .beConstantValueOf(value2);
                })
                .then(() => task.done());
          });

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