<!doctype html>
<html>
  <head>
    <title>
      k-rate AudioParams with inputs for OscillatorNode
    </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();

      // Sample rate must be a power of two to eliminate round-off when
      // computing time from frames and vice versa.  Using a non-power of two
      // will work, but the thresholds below will not be zero.  They're probably
      // closer to 1e-5 or so, but if everything is working correctly, the
      // outputs really should be exactly equal.
      const sampleRate = 8192;

      // Fairly arbitrary but short duration to limit runtime.
      const testFrames = 5 * RENDER_QUANTUM_FRAMES;
      const testDuration = testFrames / sampleRate;

      audit.define(
          {label: 'Test 1', description: 'k-rate frequency input'},
          async (task, should) => {
            // Test that an input to the frequency AudioParam set to k-rate
            // works.

            // Fairly arbitrary start and end frequencies for the automation.
            const freqStart = 100;
            const freqEnd = 2000;

            let refSetup = (context) => {
              let srcRef = new OscillatorNode(context, {frequency: 0});

              should(
                  () => srcRef.frequency.automationRate = 'k-rate',
                  `${task.label}: srcRef.frequency.automationRate = 'k-rate'`)
                  .notThrow();
              should(
                  () => srcRef.frequency.setValueAtTime(freqStart, 0),
                  `${task.label}: srcRef.frequency.setValueAtTime(${
                      freqStart}, 0)`)
                  .notThrow();
              should(
                  () => srcRef.frequency.linearRampToValueAtTime(
                      freqEnd, testDuration),
                  `${task.label}: srcRef.frequency.linearRampToValueAtTime(${
                      freqEnd}, ${testDuration})`)
                  .notThrow();

              return srcRef;
            };

            let testSetup = (context) => {
              let srcTest = new OscillatorNode(context, {frequency: 0});
              should(
                  () => srcTest.frequency.automationRate = 'k-rate',
                  `${task.label}: srcTest.frequency.automationRate = 'k-rate'`)
                  .notThrow();

              return srcTest;
            };

            let modSetup = (context) => {
              let mod = new ConstantSourceNode(context, {offset: 0});

              should(
                  () => mod.offset.setValueAtTime(freqStart, 0),
                  `${task.label}: modFreq.offset.setValueAtTime(${
                      freqStart}, 0)`)
                  .notThrow();
              should(
                  () =>
                      mod.offset.linearRampToValueAtTime(freqEnd, testDuration),
                  `${task.label}: modFreq.offset.linearRampToValueAtTime(${
                      freqEnd}, ${testDuration})`)
                  .notThrow();

              // This node is going to be connected to the frequency AudioParam.
              return {frequency: mod};
            };

            await testParams(should, {
              prefix: task.label,
              summary: 'k-rate frequency with input',
              setupRefOsc: refSetup,
              setupTestOsc: testSetup,
              setupMod: modSetup
            });

            task.done();
          });

      audit.define(
          {label: 'Test 2', description: 'k-rate detune input'},
          async (task, should) => {
            // Test that an input to the detune AudioParam set to k-rate works.
            // Threshold experimentally determined.  It should be probably not
            // be much larger than 5e-5. or something is not right.

            // Fairly arbitrary start and end detune values for automation.
            const detuneStart = 0;
            const detuneEnd = 2000;

            let refSetup = (context) => {
              let srcRef = new OscillatorNode(context, {detune: 0});

              should(
                  () => srcRef.detune.automationRate = 'k-rate',
                  `${task.label}: srcRef.detune.automationRate = 'k-rate'`)
                  .notThrow();

              should(
                  () => srcRef.detune.setValueAtTime(detuneStart, 0),
                  `${task.label}: srcRef.detune.setValueAtTime(${
                      detuneStart}, 0)`)
                  .notThrow();
              should(
                  () => srcRef.detune.linearRampToValueAtTime(
                      detuneEnd, testDuration),
                  `${task.label}: srcRef.detune.linearRampToValueAtTime(${
                      detuneEnd}, ${testDuration})`)
                  .notThrow();

              return srcRef;
            };

            let testSetup = (context) => {
              let srcTest = new OscillatorNode(context, {detune: 0});

              should(
                  () => srcTest.detune.automationRate = 'k-rate',
                  `${task.label}: srcTest.detune.automationRate = 'k-rate'`)
                  .notThrow();

              return srcTest;
            };

            let modSetup = (context) => {
              let mod = new ConstantSourceNode(context, {offset: 0});

              should(
                  () => mod.offset.setValueAtTime(detuneStart, 0),
                  `${task.label}: modDetune.offset.setValueAtTime(${
                      detuneStart}, 0)`)
                  .notThrow();
              should(
                  () => mod.offset.linearRampToValueAtTime(
                      detuneEnd, testDuration),
                  `${task.label}: modDetune.offset.linearRampToValueAtTime(${
                      detuneEnd}, ${testDuration})`)
                  .notThrow();

              return {detune: mod};
            };

            await testParams(should, {
              prefix: task.label,
              summary: 'k-rate detune with input',
              setupRefOsc: refSetup,
              setupTestOsc: testSetup,
              setupMod: modSetup
            });

            task.done();
          });

      audit.define(
          {
            label: 'Test 3',
            description: 'k-rate frequency input with a-rate detune'
          },
          async (task, should) => {
            // Test OscillatorNode with a k-rate frequency with input and an
            // a-rate detune iwth automations.

            // Fairly arbitrary start and end values for the frequency and
            // detune automations.
            const freqStart = 100;
            const freqEnd = 2000;
            const detuneStart = 0;
            const detuneEnd = -2000;

            let refSetup = (context) => {
              let node = new OscillatorNode(context, {frequency: 0});

              // Set up k-rate frequency and a-rate detune
              should(
                  () => node.frequency.automationRate = 'k-rate',
                  `${task.label}: srcRef.frequency.automationRate = 'k-rate'`)
                  .notThrow();
              should(
                  () => node.frequency.setValueAtTime(freqStart, 0),
                  `${task.label}: srcRef.frequency.setValueAtTime(${
                      freqStart}, 0)`)
                  .notThrow();
              should(
                  () => node.frequency.linearRampToValueAtTime(
                      2000, testDuration),
                  `${task.label}: srcRef.frequency.linearRampToValueAtTime(${
                      freqEnd}, ${testDuration})`)
                  .notThrow();
              should(
                  () => node.detune.setValueAtTime(detuneStart, 0),
                  `${task.label}: srcRef.detune.setValueAtTime(${
                      detuneStart}, 0)`)
                  .notThrow();
              should(
                  () => node.detune.linearRampToValueAtTime(
                      detuneEnd, testDuration),
                  `${task.label}: srcRef.detune.linearRampToValueAtTime(${
                      detuneEnd}, ${testDuration})`)
                  .notThrow();

              return node;
            };

            let testSetup = (context) => {
              let node = new OscillatorNode(context, {frequency: 0});

              should(
                  () => node.frequency.automationRate = 'k-rate',
                  `${task.label}: srcTest.frequency.automationRate = 'k-rate'`)
                  .notThrow();
              should(
                  () => node.detune.setValueAtTime(detuneStart, 0),
                  `${task.label}: srcTest.detune.setValueAtTime(${
                      detuneStart}, 0)`)
                  .notThrow();
              should(
                  () => node.detune.linearRampToValueAtTime(
                      detuneEnd, testDuration),
                  `${task.label}: srcTest.detune.linearRampToValueAtTime(${
                      detuneEnd}, ${testDuration})`)
                  .notThrow();

              return node;
            };

            let modSetup = (context) => {
              let mod = {};
              mod['frequency'] = new ConstantSourceNode(context, {offset: 0});

              should(
                  () => mod['frequency'].offset.setValueAtTime(freqStart, 0),
                  `${task.label}: modFreq.offset.setValueAtTime(${
                      freqStart}, 0)`)
                  .notThrow();

              should(
                  () => mod['frequency'].offset.linearRampToValueAtTime(
                      2000, testDuration),
                  `${task.label}: modFreq.offset.linearRampToValueAtTime(${
                      freqEnd}, ${testDuration})`)
                  .notThrow();

              return mod;
            };

            await testParams(should, {
              prefix: task.label,
              summary: 'k-rate frequency input with a-rate detune',
              setupRefOsc: refSetup,
              setupTestOsc: testSetup,
              setupMod: modSetup
            });

            task.done();
          });

      audit.define(
          {
            label: 'Test 4',
            description: 'a-rate frequency with k-rate detune input'
          },
          async (task, should) => {
            // Test OscillatorNode with an a-rate frequency with automations and
            // a k-rate detune with input.

            // Fairly arbitrary start and end values for the frequency and
            // detune automations.
            const freqStart = 100;
            const freqEnd = 2000;
            const detuneStart = 0;
            const detuneEnd = -2000;

            let refSetup = (context) => {
              let node = new OscillatorNode(context, {detune: 0});

              // Set up a-rate frequency and k-rate detune
              should(
                  () => node.frequency.setValueAtTime(freqStart, 0),
                  `${task.label}: srcRef.frequency.setValueAtTime(${
                      freqStart}, 0)`)
                  .notThrow();
              should(
                  () => node.frequency.linearRampToValueAtTime(
                      2000, testDuration),
                  `${task.label}: srcRef.frequency.linearRampToValueAtTime(${
                      freqEnd}, ${testDuration})`)
                  .notThrow();
              should(
                  () => node.detune.automationRate = 'k-rate',
                  `${task.label}: srcRef.detune.automationRate = 'k-rate'`)
                  .notThrow();
              should(
                  () => node.detune.setValueAtTime(detuneStart, 0),
                  `${task.label}: srcRef.detune.setValueAtTime(${
                      detuneStart}, 0)`)
                  .notThrow();
              should(
                  () => node.detune.linearRampToValueAtTime(
                      detuneEnd, testDuration),
                  `${task.label}: srcRef.detune.linearRampToValueAtTime(${
                      detuneEnd}, ${testDuration})`)
                  .notThrow();

              return node;
            };

            let testSetup = (context) => {
              let node = new OscillatorNode(context, {detune: 0});

              should(
                  () => node.detune.automationRate = 'k-rate',
                  `${task.label}: srcTest.detune.automationRate = 'k-rate'`)
                  .notThrow();
              should(
                  () => node.frequency.setValueAtTime(freqStart, 0),
                  `${task.label}: srcTest.frequency.setValueAtTime(${
                      freqStart}, 0)`)
                  .notThrow();
              should(
                  () => node.frequency.linearRampToValueAtTime(
                      freqEnd, testDuration),
                  `${task.label}: srcTest.frequency.linearRampToValueAtTime(${
                      freqEnd}, ${testDuration})`)
                  .notThrow();

              return node;
            };

            let modSetup = (context) => {
              let mod = {};
              const name = 'detune';

              mod['detune'] = new ConstantSourceNode(context, {offset: 0});
              should(
                  () => mod[name].offset.setValueAtTime(detuneStart, 0),
                  `${task.label}: modDetune.offset.setValueAtTime(${
                      detuneStart}, 0)`)
                  .notThrow();

              should(
                  () => mod[name].offset.linearRampToValueAtTime(
                      detuneEnd, testDuration),
                  `${task.label}: modDetune.offset.linearRampToValueAtTime(${
                      detuneEnd}, ${testDuration})`)
                  .notThrow();

              return mod;
            };

            await testParams(should, {
              prefix: task.label,
              summary: 'k-rate detune input with a-rate frequency',
              setupRefOsc: refSetup,
              setupTestOsc: testSetup,
              setupMod: modSetup
            });

            task.done();
          });

      audit.define(
          {
            label: 'Test 5',
            description: 'k-rate inputs for frequency and detune'
          },
          async (task, should) => {
            // Test OscillatorNode with k-rate frequency and detune with inputs
            // on both.

            // Fairly arbitrary start and end values for the frequency and
            // detune automations.
            const freqStart = 100;
            const freqEnd = 2000;
            const detuneStart = 0;
            const detuneEnd = -2000;

            let refSetup = (context) => {
              let node = new OscillatorNode(context, {frequency: 0, detune: 0});

              should(
                  () => node.frequency.automationRate = 'k-rate',
                  `${task.label}: srcRef.frequency.automationRate = 'k-rate'`)
                  .notThrow();
              should(
                  () => node.frequency.setValueAtTime(freqStart, 0),
                  `${task.label}: srcRef.setValueAtTime(${freqStart}, 0)`)
                  .notThrow();
              should(
                  () => node.frequency.linearRampToValueAtTime(
                      freqEnd, testDuration),
                  `${task.label}: srcRef;.frequency.linearRampToValueAtTime(${
                      freqEnd}, ${testDuration})`)
                  .notThrow();
              should(
                  () => node.detune.automationRate = 'k-rate',
                  `${task.label}: srcRef.detune.automationRate = 'k-rate'`)
                  .notThrow();
              should(
                  () => node.detune.setValueAtTime(detuneStart, 0),
                  `${task.label}: srcRef.detune.setValueAtTime(${
                      detuneStart}, 0)`)
                  .notThrow();
              should(
                  () => node.detune.linearRampToValueAtTime(
                      detuneEnd, testDuration),
                  `${task.label}: srcRef.detune.linearRampToValueAtTime(${
                      detuneEnd}, ${testDuration})`)
                  .notThrow();

              return node;
            };

            let testSetup = (context) => {
              let node = new OscillatorNode(context, {frequency: 0, detune: 0});

              should(
                  () => node.frequency.automationRate = 'k-rate',
                  `${task.label}: srcTest.frequency.automationRate = 'k-rate'`)
                  .notThrow();
              should(
                  () => node.detune.automationRate = 'k-rate',
                  `${task.label}: srcTest.detune.automationRate = 'k-rate'`)
                  .notThrow();

              return node;
            };

            let modSetup = (context) => {
              let modF = new ConstantSourceNode(context, {offset: 0});

              should(
                  () => modF.offset.setValueAtTime(freqStart, 0),
                  `${task.label}: modFreq.offset.setValueAtTime(${
                      freqStart}, 0)`)
                  .notThrow();
              should(
                  () => modF.offset.linearRampToValueAtTime(
                      freqEnd, testDuration),
                  `${task.label}: modFreq.offset.linearRampToValueAtTime(${
                      freqEnd}, ${testDuration})`)
                  .notThrow();

              let modD = new ConstantSourceNode(context, {offset: 0});

              should(
                  () => modD.offset.setValueAtTime(detuneStart, 0),
                  `${task.label}: modDetune.offset.setValueAtTime(${
                      detuneStart}, 0)`)
                  .notThrow();
              should(
                  () => modD.offset.linearRampToValueAtTime(
                      detuneEnd, testDuration),
                  `${task.label}: modDetune.offset.linearRampToValueAtTime(${
                      detuneEnd}, ${testDuration})`)
                  .notThrow();

              return {frequency: modF, detune: modD};
            };

            await testParams(should, {
              prefix: task.label,
              summary: 'k-rate inputs for both frequency and detune',
              setupRefOsc: refSetup,
              setupTestOsc: testSetup,
              setupMod: modSetup
            });

            task.done();
          });

      audit.run();

      async function testParams(should, options) {
        // Test a-rate and k-rate AudioParams of an OscillatorNode.
        //
        // |options| should be a dictionary with these members:
        //   prefix       - prefix to use for messages
        //   summary      - message to be printed with the final results
        //   setupRefOsc  - function returning the reference oscillator
        //   setupTestOsc - function returning the test oscillator
        //   setupMod     - function returning nodes to be connected to the
        //   AudioParams.
        //
        // |setupRefOsc| and |setupTestOsc| are given the context and each
        // method is expected to create an OscillatorNode with the appropriate
        // automations for testing.  The constructed OscillatorNode is returned.
        //
        // The reference oscillator
        // should automate the desired AudioParams at the appropriate automation
        // rate, and the output is the expected result.
        //
        // The test oscillator should set up the AudioParams but expect the
        // AudioParam(s) have an input that matches the automation for the
        // reference oscillator.
        //
        // |setupMod| must create one or two ConstantSourceNodes with exactly
        // the same automations as used for the reference oscillator.  This node
        // is used as the input to an AudioParam of the test oscillator.  This
        // function returns a dictionary whose members are named 'frequency' and
        // 'detune'.  The name indicates which AudioParam the constant source
        // node should be connected to.

        // Two channels: 0 = reference signal, 1 = test signal
        let context = new OfflineAudioContext({
          numberOfChannels: 2,
          sampleRate: sampleRate,
          length: testDuration * sampleRate
        });

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

        // The reference oscillator.
        let srcRef = options.setupRefOsc(context);

        // The test oscillator.
        let srcTest = options.setupTestOsc(context);

        // Inputs to AudioParam.
        let mod = options.setupMod(context);

        if (mod['frequency']) {
          should(
              () => mod['frequency'].connect(srcTest.frequency),
              `${options.prefix}: modFreq.connect(srcTest.frequency)`)
              .notThrow();
          mod['frequency'].start()
        }

        if (mod['detune']) {
          should(
              () => mod['detune'].connect(srcTest.detune),
              `${options.prefix}: modDetune.connect(srcTest.detune)`)
              .notThrow();
          mod['detune'].start()
        }

        srcRef.connect(merger, 0, 0);
        srcTest.connect(merger, 0, 1);

        srcRef.start();
        srcTest.start();

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

        // The output of the reference and test oscillator should be
        // exactly equal because the AudioParam values should be exactly
        // equal.
        should(actual, options.summary).beCloseToArray(expected, {
          absoluteThreshold: 0
        });
      }
    </script>
  </body>
</html>
