<!doctype html>
<html>
  <head>
    <title>k-rate AudioParams with inputs for DynamicsCompressorNode</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 = 48000;
      const testDuration = 0.25;

      ['attack', 'knee', 'ratio', 'release', 'threshold'].forEach(param => {
        audit.define(
            {label: param, description: `Dynamics compressor ${param}`},
            async (task, should) => {
              await doTest(should, {prefix: task.label, paramName: param});
              task.done();
            });
      });

      audit.run();

      async function doTest(should, options) {
        // Test k-rate automation of DynamicsCompressorNode with connected
        // input.
        //
        // A reference compressor 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} = 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);

        // Use an oscillator for the source.  Pretty arbitrary parameters.
        let src =
            new OscillatorNode(context, {type: 'sawtooth', frequency: 440});

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

        should(
            () => refNode = new DynamicsCompressorNode(context),
            `${prefix}: refNode = new DynamicsCompressorNode(context)`)
            .notThrow();

        let tstOptions = {};
        tstOptions[paramName] = refNode[paramName].minValue;
        should(
            () => tstNode = new DynamicsCompressorNode(context, tstOptions),
            `${prefix}: tstNode = new DynamicsCompressorNode(context, ${
                JSON.stringify(tstOptions)})`)
            .notThrow();


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

        should(
            () => refNode[paramName].linearRampToValueAtTime(
                refNode[paramName].maxValue, testDuration),
            `${prefix}: refNode[${paramName}].linearRampToValueAtTime(refNode[${
                paramName}].minValue, ${testDuration})`)
            .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 ference node.  We need to do it this way
        // because the ratio AudioParam has a nominal range of [1, 20] so we
        // can't just set the value to 0, which is what we'd normally do.
        let mod;
        should(
            () => mod = new ConstantSourceNode(context, {offset: 0}),
            `${prefix}: mod = new ConstantSourceNode(context, {offset: 0})`)
            .notThrow();
        let endValue =
            refNode[paramName].maxValue - refNode[paramName].minValue;
        should(
            () => mod.offset.setValueAtTime(0, 0),
            `${prefix}: mod.offset.setValueAtTime(0, 0)`)
            .notThrow();
        should(
            () => mod.offset.linearRampToValueAtTime(endValue, testDuration),
            `${prefix}: mod.offset.linearRampToValueAtTime(${endValue}, ${
                testDuration})`)
            .notThrow();

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

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

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

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

        // 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>
