<!doctype html>
<html>
  <head>
    <title>
      Oscillator Detune Limits
    </title>
    <script src="/resources/testharness.js"></script>
    <script src="/resources/testharnessreport.js"></script>
    <script src="/webaudio/resources/audit.js"></script>
  </head>

  <body>
    <script>
      const sampleRate = 44100;
      const renderLengthSeconds = 0.125;

      let audit = Audit.createTaskRunner();

      audit.define(
          {
            label: 'detune limits',
            description:
                'Oscillator with detune and frequency at Nyquist or above'
          },
          (task, should) => {
            let context = new OfflineAudioContext(
                2, renderLengthSeconds * sampleRate, sampleRate);

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

            // For test oscillator, set the oscillator frequency to -Nyquist and
            // set detune to be a large number that would cause the detuned
            // frequency to be way above Nyquist.
            const oscFrequency = 1;
            const detunedFrequency = sampleRate;
            const detuneValue = Math.fround(1200 * Math.log2(detunedFrequency));

            let testOsc = new OscillatorNode(
                context, {frequency: oscFrequency, detune: detuneValue});
            testOsc.connect(merger, 0, 1);

            // For the reference oscillator, determine the computed oscillator
            // frequency using the values above and set that as the oscillator
            // frequency.
            let computedFreq = oscFrequency * Math.pow(2, detuneValue / 1200);

            let refOsc = new OscillatorNode(context, {frequency: computedFreq});
            refOsc.connect(merger, 0, 0);

            // Start 'em up and render
            testOsc.start();
            refOsc.start();

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

                  // Let user know about the smaple rate so following messages
                  // make more sense.
                  should(context.sampleRate, 'Context sample rate')
                    .beEqualTo(context.sampleRate);

                  // Since the frequency is at Nyquist, the reference oscillator
                  // output should be zero.
                  should(
                      refOsc.frequency.value, 'Reference oscillator frequency')
                      .beGreaterThanOrEqualTo(context.sampleRate / 2);
                  should(
                      expected, `Osc(freq: ${refOsc.frequency.value}) output`)
                      .beConstantValueOf(0);
                  // The output from each oscillator should be the same.
                  should(
                      actual,
                      'Osc(freq: ' + oscFrequency + ', detune: ' + detuneValue +
                          ') output')
                      .beCloseToArray(expected, {absoluteThreshold: 0});

                })
                .then(() => task.done());
          });

      audit.define(
          {
            label: 'detune automation',
            description:
                'Oscillator output with detune automation should be zero ' +
                'above Nyquist'
          },
          (task, should) => {
            let context = new OfflineAudioContext(
                1, renderLengthSeconds * sampleRate, sampleRate);

            const baseFrequency = 1;
            const rampEnd = renderLengthSeconds / 2;
            const detuneEnd = 1e7;

            let src = new OscillatorNode(context, {frequency: baseFrequency});
            src.detune.linearRampToValueAtTime(detuneEnd, rampEnd);

            src.connect(context.destination);

            src.start();

            context.startRendering()
                .then(renderedBuffer => {
                  let audio = renderedBuffer.getChannelData(0);

                  // At some point, the computed oscillator frequency will go
                  // above Nyquist.  Determine at what time this occurrs.  The
                  // computed frequency is f * 2^(d/1200) where |f| is the
                  // oscillator frequency and |d| is the detune value.  Thus,
                  // find |d| such that Nyquist = f*2^(d/1200). That is, d =
                  // 1200*log2(Nyquist/f)
                  let criticalDetune =
                      1200 * Math.log2(context.sampleRate / 2 / baseFrequency);

                  // Now figure out at what point on the linear ramp does the
                  // detune value reach this critical value.  For a linear ramp:
                  //
                  //   v(t) = V0+(V1-V0)*(t-T0)/(T1-T0)
                  //
                  // Thus,
                  //
                  //   t = ((T1-T0)*v(t) + T0*V1 - T1*V0)/(V1-V0)
                  //
                  // In this test, T0 = 0, V0 = 0, T1 = rampEnd, V1 =
                  // detuneEnd, and v(t) = criticalDetune
                  let criticalTime = (rampEnd * criticalDetune) / detuneEnd;
                  let criticalFrame =
                      Math.ceil(criticalTime * context.sampleRate);

                  should(
                      criticalFrame,
                      `Frame where detuned oscillator reaches Nyquist`)
                      .beEqualTo(criticalFrame);

                  should(
                      audio.slice(0, criticalFrame),
                      `osc[0:${criticalFrame - 1}]`)
                      .notBeConstantValueOf(0);

                  should(audio.slice(criticalFrame), `osc[${criticalFrame}:]`)
                      .beConstantValueOf(0);
                })
                .then(() => task.done());
          });

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