<!DOCTYPE html>
<html>
  <head>
    <title>
      Test Interpolation for AudioParam.setValueCurveAtTime
    </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>
    <title>
      Test Interpolation for AudioParam.setValueCurveAtTime
    </title>
  </head>
  <body>
    <script id="layout-test-code">
      // Play a constant signal through a gain node that is automated using
      // setValueCurveAtTime with a 2-element curve.  The output should be a
      // linear change.

      // Choose a sample rate that is a multiple of 128, the rendering quantum
      // size.  This makes the math work out to be nice numbers.
      let sampleRate = 25600;
      let testDurationSec = 1;
      let testDurationFrames = testDurationSec * sampleRate;

      // Where the curve starts and its duration.  This MUST be less than the
      // total rendering time.
      let curveStartTime = 256 / sampleRate;
      let curveDuration = 300 / sampleRate;
      ;
      let curveValue = 0.75;

      // At this time, the gain node goes to gain 1.  This is used to make sure
      // the value curve is propagated correctly until the next event.
      let fullGainTime = 0.75;

      // Thresholds use to determine if the test passes; these are
      // experimentally determined.  The SNR between the actual and expected
      // result should be at least |snrThreshold|.  The maximum difference
      // betwen them should not exceed |maxErrorThreshold|.
      let snrThreshold = 10000;
      let maxErrorThreshold = 0;

      let context;
      let actualResult;
      let expectedResult;

      let audit = Audit.createTaskRunner();

      // Array of test configs.  Each config must specify curveStartTime,
      // curveDuration, curveLength, fullGainTime, maxErrorThreshold, and
      // snrThreshold.
      let testConfigs = [
        {
          // The main test
          curveStartTime: 256 / sampleRate,
          curveDuration: 300 / sampleRate,
          curveLength: 2,
          fullGainTime: 0.75,
          maxErrorThreshold: 5.9605e-8,
          snrThreshold: 171.206
        },
        {
          // Increase the curve length
          curveStartTime: 256 / sampleRate,
          curveDuration: 300 / sampleRate,
          curveLength: 3,
          fullGainTime: 0.75,
          maxErrorThreshold: 5.9605e-8,
          snrThreshold: 171.206
        },
        {
          // Increase the curve length
          curveStartTime: 256 / sampleRate,
          curveDuration: 300 / sampleRate,
          curveLength: 16,
          fullGainTime: 0.75,
          maxErrorThreshold: 5.9605e-8,
          snrThreshold: 170.892
        },
        {
          // Increase the curve length
          curveStartTime: 256 / sampleRate,
          curveDuration: 300 / sampleRate,
          curveLength: 100,
          fullGainTime: 0.75,
          maxErrorThreshold: 1.1921e-7,
          snrThreshold: 168.712
        },
        {
          // Corner case with duration less than a frame!
          curveStartTime: 256 / sampleRate,
          curveDuration: 0.25 / sampleRate,
          curveLength: 2,
          fullGainTime: 0.75,
          maxErrorThreshold: 0,
          snrThreshold: 10000
        },
        {
          // Short duration test
          curveStartTime: 256 / sampleRate,
          curveDuration: 2 / sampleRate,
          curveLength: 2,
          fullGainTime: 0.75,
          maxErrorThreshold: 0,
          snrThreshold: 10000
        },
        {
          // Short duration test with many points.
          curveStartTime: 256 / sampleRate,
          curveDuration: 2 / sampleRate,
          curveLength: 8,
          fullGainTime: 0.75,
          maxErrorThreshold: 0,
          snrThreshold: 10000
        },
        {
          // Long duration, big curve
          curveStartTime: 256 / sampleRate,
          curveDuration: .5,
          curveLength: 1000,
          fullGainTime: 0.75,
          maxErrorThreshold: 5.9605e-8,
          snrThreshold: 152.784
        }
      ];

      // Creates a function based on the test config that is suitable for use by
      // defineTask().
      function createTaskFunction(config) {
        return function(task, should) {
          runTest(should, config).then(() => task.done());
        };
      }

      // Define a task for each config, in the order listed in testConfigs.
      for (let k = 0; k < testConfigs.length; ++k) {
        let config = testConfigs[k];
        let name = k + ':curve=' + config.curveLength +
            ',duration=' + (config.curveDuration * sampleRate);
        audit.define(name, createTaskFunction(config));
      }

      // Simple test from crbug.com/441471.  Makes sure the end points and the
      // middle point are interpolated correctly.
      audit.define('crbug-441471', (task, should) => {
        // Any sample rate should work; we pick something small such that the
        // time end points are on a sampling point.
        let context = new OfflineAudioContext(1, 5000, 5000)

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

        let gain = context.createGain();

        let startTime = 0.7;
        let duration = 0.2;

        // Create the curve.  The interpolated result should be just a straight
        // line from -1 to 1 from time startTime to startTime + duration.

        let c = new Float32Array(3);
        c[0] = -1;
        c[1] = 0;
        c[2] = 1;
        gain.gain.setValueCurveAtTime(c, startTime, duration);
        source.connect(gain);
        gain.connect(context.destination);
        source.start();

        context.startRendering()
            .then(function(renderedBuffer) {
              let data = renderedBuffer.getChannelData(0);
              let endTime = startTime + duration;
              let midPoint = (startTime + endTime) / 2;

              should(
                  data[timeToSampleFrame(startTime, context.sampleRate)],
                  'Curve value at time ' + startTime)
                  .beEqualTo(c[0]);
              // Due to round-off, the value at the midpoint is not exactly zero
              // on arm64.  See crbug.com/558563.  The current value is
              // experimentally determined.
              should(
                  data[timeToSampleFrame(midPoint, context.sampleRate)],
                  'Curve value at time ' + midPoint)
                  .beCloseTo(0, {threshold: Math.pow(2, -51)});
              should(
                  data[timeToSampleFrame(endTime, context.sampleRate)],
                  'Curve value at time ' + endTime)
                  .beEqualTo(c[2]);
            })
            .then(() => task.done());
      });

      function runTest(should, config) {
        context = new OfflineAudioContext(1, testDurationFrames, sampleRate);

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

        // The value curve for testing.  Just to make things easy for testing,
        // make the curve a simple ramp up to curveValue.
        // TODO(rtoy): Maybe allow more complicated curves?
        let curve = new Float32Array(config.curveLength);
        for (let k = 0; k < config.curveLength; ++k) {
          curve[k] = curveValue / (config.curveLength - 1) * k;
        }

        // A gain node that is to be automated using setValueCurveAtTime.
        let gain = context.createGain();
        gain.gain.value = 0;
        gain.gain.setValueCurveAtTime(
            curve, config.curveStartTime, config.curveDuration);
        // This is to verify that setValueCurveAtTime ends appropriately.
        gain.gain.setValueAtTime(1, config.fullGainTime);

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

        // Some consistency checks on the test parameters
        let prefix = 'Length ' + config.curveLength + ', duration ' +
            config.curveDuration;
        should(
            config.curveStartTime + config.curveDuration,
            prefix + ': Check: Curve end time')
            .beLessThanOrEqualTo(testDurationSec);
        should(config.fullGainTime, prefix + ': Check: Full gain start time')
            .beLessThanOrEqualTo(testDurationSec);
        should(config.fullGainTime, prefix + ': Check: Full gain start time')
            .beGreaterThanOrEqualTo(
                config.curveStartTime + config.curveDuration);

        // Rock and roll!
        return context.startRendering().then(checkResult(should, config));
      }

      // Return a function to check that the rendered result matches the
      // expected result.
      function checkResult(should, config) {
        return function(renderedBuffer) {
          let success = true;

          actualResult = renderedBuffer.getChannelData(0);
          expectedResult = computeExpectedResult(config);

          // Compute the SNR and max absolute difference between the actual and
          // expected result.
          let SNR = 10 * Math.log10(computeSNR(actualResult, expectedResult));
          let maxDiff = -1;
          let posn = -1;

          for (let k = 0; k < actualResult.length; ++k) {
            let diff = Math.abs(actualResult[k] - expectedResult[k]);
            if (maxDiff < diff) {
              maxDiff = diff;
              posn = k;
            }
          }

          let prefix = 'Curve length ' + config.curveLength + ', duration ' +
              config.curveDuration;
          should(SNR, prefix + ': SNR')
              .beGreaterThanOrEqualTo(config.snrThreshold);

          should(maxDiff, prefix + ': Max difference')
              .beLessThanOrEqualTo(config.maxErrorThreshold);
        }
      }

      // Compute the expected result based on the config settings.
      function computeExpectedResult(config) {
        // The automation curve starts at |curveStartTime| and has duration
        // |curveDuration|.  So, the output should be zero until curveStartTime,
        // linearly ramp up from there to |curveValue|, and then be constant 1
        // from then to the end of the buffer.

        let expected = new Float32Array(testDurationFrames);

        let curveStartFrame = config.curveStartTime * sampleRate;
        let curveEndFrame =
            (config.curveStartTime + config.curveDuration) * sampleRate;
        let fullGainFrame = config.fullGainTime * sampleRate;

        let k;

        // Zero out the start.
        for (k = 0; k < curveStartFrame; ++k)
          expected[k] = 0;

        // Linearly ramp now.  This assumes that the actual curve used is a
        // linear ramp, even if there are many curve points.
        let stepSize = curveValue / (config.curveDuration * sampleRate);
        for (; k < curveEndFrame; ++k)
          expected[k] = stepSize * (k - curveStartFrame);

        // Hold it constant until the next event
        for (; k < fullGainFrame; ++k)
          expected[k] = curveValue;

        // Amplitude is one for the rest of the test.
        for (; k < testDurationFrames; ++k)
          expected[k] = 1;

        return expected;
      }

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