<!DOCTYPE html>
<html>
  <head>
    <title>
      Test OscillatorNode Has No Dezippering
    </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>
  </head>
  <body>
    <script id="layout-test-code">
      // The sample rate must be a power of two to avoid any round-off errors in
      // computing when to suspend a context on a rendering quantum boundary.
      // Otherwise this is pretty arbitrary.
      let sampleRate = 16384;

      let audit = Audit.createTaskRunner();

      // Compare value setter with Javascript-generated reference for frequency.
      audit.define(
          {
            label: 'frequency',
            description: 'Test Oscillator frequency has no dezippering'
          },
          (task, should) => {
            let frequency0 = 128;
            let frequency1 = 440;
            let newValue = frequency1;

            testWithSine(should, {
              frequency0: frequency0,
              frequency1: frequency1,
              paramName: 'frequency',
              newValue: newValue,
              thresholds: [1.1921e-7, 1.7882e-7]
            }).then(() => task.done());
          });

      // Compare value setter with Javascript-generated reference for detune.
      audit.define(
          {
            label: 'detune',
            description: 'Test Oscillator detune has no dezippering'
          },
          (task, should) => {
            let frequency0 = 64;
            let detune = 600;
            // Compute new frequency this way to make the JS value match the
            // internal frequency better.
            let frequency1 = frequency0 *
                Math.fround(Math.pow(2, Math.fround(detune / 1200)));

            testWithSine(should, {
              frequency0: frequency0,
              frequency1: frequency1,
              paramName: 'detune',
              newValue: detune,
              thresholds: [1.1921e-7, 3.6657e-6],
            }).then(() => task.done());
          });

      audit.define(
          {
            label: 'setValueAtTime',
            description: 'Test Oscillator value setter against setValueAtTime'
          },
          (task, should) => {
            testWithSetValue(should, {
              initialValues: {frequency: 100},
              modulation: false,
              changeList: [{
                suspendQuantum: 2,
                changes: [
                  {paramName: 'frequency', paramValue: 440},
                  {paramName: 'detune', paramValue: 600}
                ]
              }]
            }).then(() => task.done());
          });

      audit.define(
          {
            label: 'modulation',
            description:
                'Test Oscillator value setter against setValueAtTime ' +
                'with modulation'
          },
          (task, should) => {
            testWithSetValue(should, {
              prefix: 'With modulation: ',
              initialValues: {frequency: 1000},
              changeList: [{
                suspendQuantum: 2,
                changes: [
                  {paramName: 'frequency', paramValue: 440},
                  {paramName: 'detune', paramValue: 600}
                ]
              }],
              modParams: [
                {
                  paramName: 'frequency',
                  initialValue: {frequency: 1000},
                  modGain: 100,
                },
                {
                  paramName: 'detune',
                  initialValue: {frequency: 1000},
                  modGain: 1000,
                }
              ]
            }).then(() => task.done());
          });

      audit.run();

      // Compute a sample sine wave of frequency |f| assuming a sample rate of
      // |sampleRate|.  The number of samples computed is |length|.
      function sineWave(f, sampleRate, length) {
        let omega = 2 * Math.PI * f / sampleRate;
        let data = new Float32Array(length);
        for (let k = 0; k < length; ++k) {
          data[k] = Math.sin(omega * k);
        }
        return data;
      }

      // Test oscillator against a Javascript reference. |optioos| is a
      // dictionary with the following items:
      //  frequency0 - initial frequency of the oscillator
      //  paramName  - name of oscillator attribute to modified
      //  newValue   - the new value of the attribute
      //  frequency1 - new value of oscillator, used for computing the reference
      //               value
      //  threshold  - array of thresholds use to compare against the JS
      //               reference
      //
      // The oscillator starts at |frequency0|.  After some time, the oscillator
      // attribute |paramName| is set to |newValue|.  The output from the
      // oscillator is compared against a Javascript reference.
      function testWithSine(should, options) {
        let context = new OfflineAudioContext(1, sampleRate, sampleRate);

        // Frequency of oscillator must be such that the period is a whole
        // number of render quanta.
        let frequency0 = options.frequency0;
        let frequency1 = options.frequency1;

        let periodFrames = sampleRate / frequency0;
        let period = periodFrames / sampleRate;

        // Sanity check that periodFrames is an integer and that it is a
        // multiple of 128 so that we suspend on a rendering boundary. We do
        // this to make the Javascript reference easier to compute so that when
        // the frequency changes, we start from the beginning of the sine wave,
        // not somewhere in between.
        should(
            periodFrames === Math.floor(periodFrames),
            `Oscillator period in frames (${periodFrames}) is an integer`)
            .beTrue();
        should(
            periodFrames / RENDER_QUANTUM_FRAMES ===
                Math.floor(periodFrames / RENDER_QUANTUM_FRAMES),
            'Oscillator period in frames (' + periodFrames +
                `) is a multiple of ${RENDER_QUANTUM_FRAMES}`)
            .beTrue();

        osc =
            new OscillatorNode(context, {type: 'sine', frequency: frequency0});

        osc.connect(context.destination);

        // After 1 oscillator period, change the frequency. This will happen
        // on a rendering boundary.
        context.suspend(period)
            .then(() => osc[options.paramName].value = options.newValue)
            .then(() => context.resume());

        osc.start();
        return context.startRendering().then(renderedBuffer => {
          let renderedData = renderedBuffer.getChannelData(0);

          // Compute expected results.  The first part should one period
          // of a sine wave with frequency |frequency0|.  The second
          // part should be a sine wave with frequency |frequency1|.
          let part0 = sineWave(frequency0, sampleRate, periodFrames);
          let part1 = sineWave(
              frequency1, sampleRate, renderedData.length - periodFrames);

          // Verify the two parts match.  Thresholds here are
          // experimentally determined.
          should(
              renderedData.slice(0, periodFrames),
              `Part 0 (sine wave at ${frequency0} Hz)`)
              .beCloseToArray(
                  part0, {absoluteThreshold: options.thresholds[0]});
          should(
              renderedData.slice(periodFrames),
              `Part 1 (sine wave at ${frequency1} Hz)`)
              .beCloseToArray(
                  part1, {absoluteThreshold: options.thresholds[1]});
        });
      }

      // Test oscillator using automation as a reference. |options| is a
      // dictionary with the following items:
      //
      //   prefix         - optional prefix for messages (to make messages
      //                    unique)
      //   initialValues  - initial values for the oscillator
      //   changeList     - an array specifying when and what should be changed.
      //   modParams      - an array specifying the modulation parameters. The
      //                    modulation is an oscillator that is connected to one
      //                    of the AudioParams of the oscillator.
      //
      // The |changeList| entry is a dictionary with the following items:
      //   suspendQuantum - render quantum at which the value is changed.
      //   changes        - an array of dictionaries specifying what oscillator
      //                    attribute should be changed and the correspond
      //                    value.  This is a dictionary with items |paramName|
      //                    and |paramValue|
      //
      // The |modParams| entry is an array of dictionaries, and each dictionary
      // has the following items:
      //   paramName      - name of the oscillator attribute to change
      //   initialValue   - initial value for the modulation oscillator
      //   modGain        - gain applied to the oscillator output before
      //                    connecting to the AudioParam of the test
      //                    oscillator.
      function testWithSetValue(should, options) {
        let context = new OfflineAudioContext(2, sampleRate, sampleRate);
        let merger = new ChannelMergerNode(context, {numberOfChannels: 2});
        merger.connect(context.destination);

        // |srcTest| is the oscillator to be tested using the value setter.
        // |srcRef| is an identical oscillator except that |setValueAtTime| will
        // be used to change the oscillator.
        let srcTest = new OscillatorNode(context, options.initialValues);
        let srcRef = new OscillatorNode(context, options.initialValues);

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

        // Apply each change given by |changeList|.
        options.changeList.forEach(change => {
          let changeTime = change.suspendQuantum * RENDER_QUANTUM_FRAMES /
              context.sampleRate;
          // Use setValue on the reference oscillator and also set the value of
          // the test oscillator.
          change.changes.forEach(item => {
            srcRef[item.paramName].setValueAtTime(item.paramValue, changeTime);
          });
          context.suspend(changeTime)
              .then(() => {
                change.changes.forEach(item => {
                  srcTest[item.paramName].value = item.paramValue;
                });
              })
              .then(() => context.resume());
        });

        if (options.modParams) {
          // If |modParams| is given, create an oscillator with an appropriate
          // gain for each entry and connect it to the specified AudioParam of
          // both the reference and test oscillators.
          options.modParams.forEach(item => {
            let mod = new OscillatorNode(context, item.initialValue);
            let modGain = new GainNode(context, {gain: item.modGain});
            mod.connect(modGain);
            modGain.connect(srcRef[item.paramName]);
            modGain.connect(srcTest[item.paramName]);
            mod.start();
          });
        }

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

        return context.startRendering().then(renderedBuffer => {
          let actual = renderedBuffer.getChannelData(0);
          let expected = renderedBuffer.getChannelData(1);

          let prefix = options.prefix || '';

          // The output using the value setter (|actual|) should be identical to
          // the output using |setValueAtTime| (|expected|).
          let match = should(actual, prefix + 'Output from .value setter')
                          .beEqualToArray(expected);
          should(
              match,
              prefix + 'Output from .value setter matches ' +
                  'setValueAtTime output')
              .beTrue();
        })
      }
    </script>
  </body>
</html>
