<!DOCTYPE html>
<html>
  <head>
    <title>
      SetTarget Followed by Linear or Exponential Ramp Is Continuous
    </title>
    <script src="../../imported/w3c/web-platform-tests/resources/testharness.js"></script>
    <script src="../../resources/testharnessreport.js"></script>
    <script src="../resources/audit-util.js"></script>
    <script src="../resources/audit.js"></script>
    <script src="../resources/audioparam-testing.js"></script>
  </head>
  <body>
    <script id="layout-test-code">
      let sampleRate = 48000;
      let renderQuantum = 128;
      // Test doesn't need to run for very long.
      let renderDuration = 0.1;
      // Where the ramp should end
      let rampEndTime = renderDuration - .05;
      let renderFrames = renderDuration * sampleRate;
      let timeConstant = 0.01;

      let audit = Audit.createTaskRunner();

      // All of the tests start a SetTargetAtTime after one rendering quantum.
      // The following tests handle various cases where a linear or exponential
      // ramp is scheduled at or after SetTargetAtTime starts.

      audit.define('linear ramp replace', (task, should) => {
        // Schedule a linear ramp to start at the same time as SetTargetAtTime.
        // This effectively replaces the SetTargetAtTime as if it never existed.
        runTest(should, 'Linear ramp', {
          automationFunction: function(audioparam, endValue, endTime) {
            audioparam.linearRampToValueAtTime(endValue, endTime);
          },
          referenceFunction: linearResult,
          automationTime: renderQuantum / sampleRate,
          thresholdSetTarget: 0,
          thresholdRamp: 1.26765e-6
        }).then(() => task.done());
      });

      audit.define('delayed linear ramp', (task, should) => {
        // Schedule a linear ramp to start after the SetTargetAtTime has already
        // started rendering. This is the main test to verify that the linear
        // ramp is continuous with the SetTargetAtTime curve.
        runTest(should, 'Delayed linear ramp', {
          automationFunction: function(audioparam, endValue, endTime) {
            audioparam.linearRampToValueAtTime(endValue, endTime);
          },
          referenceFunction: linearResult,
          automationTime: 4 * renderQuantum / sampleRate,
          thresholdSetTarget: 3.43632e-7,
          thresholdRamp: 1.07972e-6
        }).then(() => task.done());
      });

      audit.define('expo ramp replace', (task, should) => {
        // Like "linear ramp replace", but with an exponential ramp instead.
        runTest(should, 'Exponential ramp', {
          automationFunction: function(audioparam, endValue, endTime) {
            audioparam.exponentialRampToValueAtTime(endValue, endTime);
          },
          referenceFunction: exponentialResult,
          automationTime: renderQuantum / sampleRate,
          thresholdSetTarget: 0,
          thresholdRamp: 1.14441e-5
        }).then(() => task.done());
      });

      audit.define('delayed expo ramp', (task, should) => {
        // Like "delayed linear ramp", but with an exponential ramp instead.
        runTest(should, 'Delayed exponential ramp', {
          automationFunction: function(audioparam, endValue, endTime) {
            audioparam.exponentialRampToValueAtTime(endValue, endTime);
          },
          referenceFunction: exponentialResult,
          automationTime: 4 * renderQuantum / sampleRate,
          thresholdSetTarget: 3.43632e-7,
          thresholdRamp: 4.29154e-6
        }).then(() => task.done());
      });

      audit.run();

      function computeExpectedResult(
          automationTime, timeConstant, endValue, endTime, rampFunction) {
        // The result is a constant value of 1 for one rendering quantum, then a
        // SetTarget event lasting to |automationTime|, at which point a ramp
        // starts which ends at |endValue| at |endTime|.  Then the rest of curve
        // should be held constant at |endValue|.
        let initialPart = new Array(renderQuantum);
        initialPart.fill(1);

        // Generate 1 extra frame so that we know where to start the linear
        // ramp.  The last sample of the array is where the ramp should start
        // from.
        let setTargetPart = createExponentialApproachArray(
            renderQuantum / sampleRate, automationTime + 1 / sampleRate, 1, 0,
            sampleRate, timeConstant);
        let setTargetLength = setTargetPart.length;

        // Generate the ramp starting at |automationTime| with a value from last
        // value of the SetTarget curve above.
        let rampPart = rampFunction(
            automationTime, endTime, setTargetPart[setTargetLength - 1],
            endValue, sampleRate);

        // Finally finish out the rest with a constant value of |endValue|, if
        // needed.
        let finalPart =
            new Array(Math.floor((renderDuration - endTime) * sampleRate));
        finalPart.fill(endValue);

        // Return the four parts separately for testing.
        return {
          initialPart: initialPart,
          setTargetPart: setTargetPart.slice(0, setTargetLength - 1),
          rampPart: rampPart,
          tailPart: finalPart
        };
      }

      function linearResult(automationTime, timeConstant, endValue, endTime) {
        return computeExpectedResult(
            automationTime, timeConstant, endValue, endTime,
            createLinearRampArray);
      }

      function exponentialResult(
          automationTime, timeConstant, endValue, endTime) {
        return computeExpectedResult(
            automationTime, timeConstant, endValue, endTime,
            createExponentialRampArray);
      }

      // Run test to verify that a SetTarget followed by a ramp produces a
      // continuous curve. |prefix| is a string to use as a prefix for the
      // messages. |options| is a dictionary describing how the test is run:
      //
      //   |options.automationFunction|
      //     The function to use to start the automation, which should be a
      //     linear or exponential ramp automation.  This function has three
      //     arguments:
      //       audioparam - the AudioParam to be automated
      //       endValue   - the end value of the ramp
      //       endTime    - the end time fo the ramp.
      //   |options.referenceFunction|
      //     The function to generated the expected result.  This function has
      //     four arguments:
      //       automationTime - the value of  |options.automationTime|
      //       timeConstant   - time constant used for SetTargetAtTime
      //       rampEndValue   - end value for the ramp (same value used for
      //       automationFunction) rampEndTime    - end time for the ramp (same
      //       value used for automationFunction)
      //   |options.automationTime|
      //     Time at which the |automationFunction| is called to start the
      //     automation.
      //   |options.thresholdSetTarget|
      //     Threshold to use for verifying that the initial (if any)
      //     SetTargetAtTime portion had the correct values.
      //   |options.thresholdRamp|
      //     Threshold to use for verifying that the ramp portion had the
      //     correct values.
      function runTest(should, prefix, options) {
        let automationFunction = options.automationFunction;
        let referenceFunction = options.referenceFunction;
        let automationTime = options.automationTime;
        let thresholdSetTarget = options.thresholdSetTarget || 0;
        let thresholdRamp = options.thresholdRamp || 0;

        // End value for the ramp.  Fairly arbitrary, but should be distinctly
        // different from the target value for SetTargetAtTime and the initial
        // value of gain.gain.
        let rampEndValue = 2;
        let context = new OfflineAudioContext(1, renderFrames, sampleRate);

        // A constant source of amplitude 1.
        let source = context.createBufferSource();
        source.buffer = createConstantBuffer(context, 1, 1);
        source.loop = true;

        let gain = context.createGain();

        // The SetTarget starts after one rendering quantum.
        gain.gain.setTargetAtTime(
            0, renderQuantum / context.sampleRate, timeConstant);

        // Schedule the ramp at |automationTime|.  If this time is past the
        // first rendering quantum, the SetTarget event will run for a bit
        // before running the ramp.  Otherwise, the SetTarget should be
        // completely replaced by the ramp.
        context.suspend(automationTime).then(function() {
          automationFunction(gain.gain, rampEndValue, rampEndTime);
          context.resume();
        });

        source.connect(gain);
        gain.connect(context.destination);

        source.start();

        return context.startRendering().then(function(resultBuffer) {
          let success = true;
          let result = resultBuffer.getChannelData(0);
          let expected = referenceFunction(
              automationTime, timeConstant, rampEndValue, rampEndTime);

          // Verify each part of the curve separately.
          let startIndex = 0;
          let length = expected.initialPart.length;

          // Verify that the initial part of the curve is constant.
          should(result.slice(0, length), prefix + ': Initial part')
              .beCloseToArray(expected.initialPart);

          // Verify the SetTarget part of the curve, if the SetTarget did
          // actually run.
          startIndex += length;
          length = expected.setTargetPart.length;
          if (length) {
            should(
                result.slice(startIndex, startIndex + length),
                prefix + ': SetTarget part')
                .beCloseToArray(
                    expected.setTargetPart,
                    {absoluteThreshold: thresholdSetTarget});
          } else {
            should(!length, prefix + ': SetTarget part')
                .message(
                    'was correctly replaced by the ramp',
                    'was incorrectly replaced by the ramp');
          }

          // Verify the ramp part of the curve
          startIndex += length;
          length = expected.rampPart.length;
          should(result.slice(startIndex, startIndex + length), prefix)
              .beCloseToArray(
                  expected.rampPart, {absoluteThreshold: thresholdRamp});

          // Verify that the end of the curve after the ramp (if any) is a
          // constant.
          startIndex += length;
          should(result.slice(startIndex), prefix + ': Tail part')
              .beCloseToArray(expected.tailPart);

        });
      }
    </script>
  </body>
</html>
