<!doctype html>
<html>
  <head>
    <title>k-rate AudioParams with inputs for AudioBufferSourceNode</title>
    <script src="/resources/testharness.js"></script>
    <script src="/resources/testharnessreport.js"></script>
    <script src="/webaudio/resources/audit.js"></script>
    <script src="/webaudio/resources/audit-util.js"></script>
  </head>

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

      // Fairly abitrary sampleRate and somewhat duration
      const sampleRate = 8000;
      const testDuration = 0.25;

      [['playbackRate', [1, 0], [2, testDuration]],
       ['detune', [-1200, 0], [1200, testDuration]]]
          .forEach(param => {
            audit.define(
                {label: param[0], description: `AudioBufferSource ${param[0]}`},
                async (task, should) => {
                  await doTest(should, {
                    prefix: task.label,
                    paramName: param[0],
                    startValue: param[1],
                    endValue: param[2]
                  });
                  task.done();
                });
          });

      audit.run();

      async function doTest(should, options) {
        // Test k-rate automation of AudioBufferSourceNode with connected
        // input.
        //
        // A reference source node is created with an automation on the
        // selected AudioParam. For simplicity, we just use a linear ramp from
        // the minValue to the maxValue of the AudioParam.
        //
        // The test node has an input signal connected to the AudioParam.  This
        // input signal is created to match the automation on the reference
        // node.
        //
        // Finally, the output from the two nodes must be identical if k-rate
        // inputs are working correctly.
        //
        // Options parameter is a dictionary with the following required
        // members:
        //   prefix    - prefix to use for the messages.
        //   paramName - Name of the AudioParam to be tested

        let {prefix, paramName, startValue, endValue} = options;

        let context = new OfflineAudioContext({
          numberOfChannels: 2,
          sampleRate: sampleRate,
          length: testDuration * sampleRate
        });

        let merger = new ChannelMergerNode(
            context, {numberOfInputs: context.destination.channelCount});
        merger.connect(context.destination);

        // Linear ramp to use for the buffer sources
        let ramp = createLinearRampBuffer(context, context.length);

        // Create the reference and test nodes.
        let refNode;
        let tstNode;

        const nodeOptions = {buffer: ramp};

        should(
            () => refNode = new AudioBufferSourceNode(context, nodeOptions),
            `${prefix}: refNode = new AudioBufferSourceNode(context, ${
                JSON.stringify(nodeOptions)})`)
            .notThrow();

        should(
            () => tstNode = new AudioBufferSourceNode(context, nodeOptions),
            `${prefix}: tstNode = new AudioBufferSourceNode(context, ${
                JSON.stringify(nodeOptions)})`)
            .notThrow();


        // Automate the AudioParam of the reference node with a linear ramp
        should(
            () => refNode[paramName].setValueAtTime(...startValue),
            `${prefix}: refNode[${paramName}].setValueAtTime(${
                startValue[0]}, ${startValue[1]})`)
            .notThrow();

        should(
            () => refNode[paramName].linearRampToValueAtTime(...endValue),
            `${prefix}: refNode[${paramName}].linearRampToValueAtTime(${
                endValue[0]}, ${endValue[1]})`)
            .notThrow();


        // Create the input node and automate it so that it's output when added
        // to the intrinsic value of the AudioParam we get the same values as
        // the automations on the reference node.

        // Compute the start and end values based on the defaultValue of the
        // param and the desired startValue and endValue.  The input is added to
        // the intrinsic value of the AudioParam, so we need to account for
        // that.

        let mod;
        should(
            () => mod = new ConstantSourceNode(context, {offset: 0}),
            `${prefix}: mod = new ConstantSourceNode(context, {offset: 0})`)
            .notThrow();

        let modStart = startValue[0] - refNode[paramName].defaultValue;
        let modEnd = endValue[0] - refNode[paramName].defaultValue;
        should(
            () => mod.offset.setValueAtTime(modStart, startValue[1]),
            `${prefix}: mod.offset.setValueAtTime(${modStart}, ${
                startValue[1]})`)
            .notThrow();
        should(
            () => mod.offset.linearRampToValueAtTime(modEnd, endValue[1]),
            `${prefix}: mod.offset.linearRampToValueAtTime(${modEnd}, ${
                endValue[1]})`)
            .notThrow();

        // Connect up everything.
        should(
            () => mod.connect(tstNode[paramName]),
            `${prefix}: mod.connect(tstNode[${paramName}])`)
            .notThrow();

        refNode.connect(merger, 0, 0);
        tstNode.connect(merger, 0, 1);

        // Go!
        refNode.start();
        tstNode.start();
        mod.start();

        const buffer = await context.startRendering();
        let expected = buffer.getChannelData(0);
        let actual = buffer.getChannelData(1);

        // Quick sanity check that output isn't zero.  This means we messed up
        // the connections or automations or the buffer source.
        should(expected, `Expected k-rate ${paramName} AudioParam with input`)
            .notBeConstantValueOf(0);
        should(actual, `Actual k-rate ${paramName} AudioParam with input`)
            .notBeConstantValueOf(0);

        // The expected and actual results must be EXACTLY the same.
        should(actual, `k-rate ${paramName} AudioParam with input`)
            .beCloseToArray(expected, {absoluteThreshold: 0});
      }
    </script>
  </body>
</html>
