<!DOCTYPE html>
<html>
  <head>
    <title>
      Biquad Automation Test
    </title>
    <script src="/resources/testharness.js"></script>
    <script src="/resources/testharnessreport.js"></script>
    <script src="/webaudio/resources/audit-util.js"></script>
    <script src="/webaudio/resources/audit.js"></script>
    <script src="/webaudio/resources/biquad-filters.js"></script>
    <script src="/webaudio/resources/audioparam-testing.js"></script>
  </head>
  <body>
    <script id="layout-test-code">
      // Don't need to run these tests at high sampling rate, so just use a low
      // one to reduce memory usage and complexity.
      let sampleRate = 16000;

      // How long to render for each test.
      let renderDuration = 0.25;
      // Where to end the automations.  Fairly arbitrary, but must end before
      // the renderDuration.
      let automationEndTime = renderDuration / 2;

      let audit = Audit.createTaskRunner();

      // The definition of the linear ramp automation function.
      function linearRamp(t, v0, v1, t0, t1) {
        return v0 + (v1 - v0) * (t - t0) / (t1 - t0);
      }

      // Generate the filter coefficients for the specified filter using the
      // given parameters for the given duration.  |filterTypeFunction| is a
      // function that returns the filter coefficients for one set of
      // parameters.  |parameters| is a property bag that contains the start and
      // end values (as an array) for each of the biquad attributes.  The
      // properties are |freq|, |Q|, |gain|, and |detune|.  |duration| is the
      // number of seconds for which the coefficients are generated.
      //
      // A property bag with properties |b0|, |b1|, |b2|, |a1|, |a2|.  Each
      // propery is an array consisting of the coefficients for the time-varying
      // biquad filter.
      function generateFilterCoefficients(
          filterTypeFunction, parameters, duration) {
        let renderEndFrame = Math.ceil(renderDuration * sampleRate);
        let endFrame = Math.ceil(duration * sampleRate);
        let nCoef = renderEndFrame;
        let b0 = new Float64Array(nCoef);
        let b1 = new Float64Array(nCoef);
        let b2 = new Float64Array(nCoef);
        let a1 = new Float64Array(nCoef);
        let a2 = new Float64Array(nCoef);

        let k = 0;
        // If the property is not given, use the defaults.
        let freqs = parameters.freq || [350, 350];
        let qs = parameters.Q || [1, 1];
        let gains = parameters.gain || [0, 0];
        let detunes = parameters.detune || [0, 0];

        for (let frame = 0; frame <= endFrame; ++frame) {
          // Apply linear ramp at frame |frame|.
          let f =
              linearRamp(frame / sampleRate, freqs[0], freqs[1], 0, duration);
          let q = linearRamp(frame / sampleRate, qs[0], qs[1], 0, duration);
          let g =
              linearRamp(frame / sampleRate, gains[0], gains[1], 0, duration);
          let d = linearRamp(
              frame / sampleRate, detunes[0], detunes[1], 0, duration);

          // Compute actual frequency parameter
          f = f * Math.pow(2, d / 1200);

          // Compute filter coefficients
          let coef = filterTypeFunction(f / (sampleRate / 2), q, g);
          b0[k] = coef.b0;
          b1[k] = coef.b1;
          b2[k] = coef.b2;
          a1[k] = coef.a1;
          a2[k] = coef.a2;
          ++k;
        }

        // Fill the rest of the arrays with the constant value to the end of
        // the rendering duration.
        b0.fill(b0[endFrame], endFrame + 1);
        b1.fill(b1[endFrame], endFrame + 1);
        b2.fill(b2[endFrame], endFrame + 1);
        a1.fill(a1[endFrame], endFrame + 1);
        a2.fill(a2[endFrame], endFrame + 1);

        return {b0: b0, b1: b1, b2: b2, a1: a1, a2: a2};
      }

      // Apply the given time-varying biquad filter to the given signal,
      // |signal|.  |coef| should be the time-varying coefficients of the
      // filter, as returned by |generateFilterCoefficients|.
      function timeVaryingFilter(signal, coef) {
        let length = signal.length;
        // Use double precision for the internal computations.
        let y = new Float64Array(length);

        // Prime the pump. (Assumes the signal has length >= 2!)
        y[0] = coef.b0[0] * signal[0];
        y[1] =
            coef.b0[1] * signal[1] + coef.b1[1] * signal[0] - coef.a1[1] * y[0];

        for (let n = 2; n < length; ++n) {
          y[n] = coef.b0[n] * signal[n] + coef.b1[n] * signal[n - 1] +
              coef.b2[n] * signal[n - 2];
          y[n] -= coef.a1[n] * y[n - 1] + coef.a2[n] * y[n - 2];
        }

        // But convert the result to single precision for comparison.
        return y.map(Math.fround);
      }

      // Configure the audio graph using |context|.  Returns the biquad filter
      // node and the AudioBuffer used for the source.
      function configureGraph(context, toneFrequency) {
        // The source is just a simple sine wave.
        let src = context.createBufferSource();
        let b =
            context.createBuffer(1, renderDuration * sampleRate, sampleRate);
        let data = b.getChannelData(0);
        let omega = 2 * Math.PI * toneFrequency / sampleRate;
        for (let k = 0; k < data.length; ++k) {
          data[k] = Math.sin(omega * k);
        }
        src.buffer = b;
        let f = context.createBiquadFilter();
        src.connect(f);
        f.connect(context.destination);

        src.start();

        return {filter: f, source: b};
      }

      function createFilterVerifier(
          should, filterCreator, threshold, parameters, input, message) {
        return function(resultBuffer) {
          let actual = resultBuffer.getChannelData(0);
          let coefs = generateFilterCoefficients(
              filterCreator, parameters, automationEndTime);

          reference = timeVaryingFilter(input, coefs);

          should(actual, message).beCloseToArray(reference, {
            absoluteThreshold: threshold
          });
        };
      }

      // Automate just the frequency parameter.  A bandpass filter is used where
      // the center frequency is swept across the source (which is a simple
      // tone).
      audit.define('automate-freq', (task, should) => {
        let context =
            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);

        // Center frequency of bandpass filter and also the frequency of the
        // test tone.
        let centerFreq = 10 * 440;

        // Sweep the frequency +/- 5*440 Hz from the center.  This should cause
        // the output to be low at the beginning and end of the test where the
        // tone is outside the pass band of the filter, but high in the middle
        // of the automation time where the tone is near the center of the pass
        // band.  Make sure the frequency sweep stays inside the Nyquist
        // frequency.
        let parameters = {freq: [centerFreq - 5 * 440, centerFreq + 5 * 440]};
        let graph = configureGraph(context, centerFreq);
        let f = graph.filter;
        let b = graph.source;

        f.type = 'bandpass';
        f.frequency.setValueAtTime(parameters.freq[0], 0);
        f.frequency.linearRampToValueAtTime(
            parameters.freq[1], automationEndTime);

        context.startRendering()
            .then(createFilterVerifier(
                should, createBandpassFilter, 4.6455e-6, parameters,
                b.getChannelData(0),
                'Output of bandpass filter with frequency automation'))
            .then(() => task.done());
      });

      // Automate just the Q parameter.  A bandpass filter is used where the Q
      // of the filter is swept.
      audit.define('automate-q', (task, should) => {
        let context =
            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);

        // The frequency of the test tone.
        let centerFreq = 440;

        // Sweep the Q paramter between 1 and 200.  This will cause the output
        // of the filter to pass most of the tone at the beginning to passing
        // less of the tone at the end.  This is because we set center frequency
        // of the bandpass filter to be slightly off from the actual tone.
        let parameters = {
          Q: [1, 200],
          // Center frequency of the bandpass filter is just 25 Hz above the
          // tone frequency.
          freq: [centerFreq + 25, centerFreq + 25]
        };
        let graph = configureGraph(context, centerFreq);
        let f = graph.filter;
        let b = graph.source;

        f.type = 'bandpass';
        f.frequency.value = parameters.freq[0];
        f.Q.setValueAtTime(parameters.Q[0], 0);
        f.Q.linearRampToValueAtTime(parameters.Q[1], automationEndTime);

        context.startRendering()
            .then(createFilterVerifier(
                should, createBandpassFilter, 1.0133e-6, parameters,
                b.getChannelData(0),
                'Output of bandpass filter with Q automation'))
            .then(() => task.done());
      });

      // Automate just the gain of the lowshelf filter.  A test tone will be in
      // the lowshelf part of the filter.  The output will vary as the gain of
      // the lowshelf is changed.
      audit.define('automate-gain', (task, should) => {
        let context =
            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);

        // Frequency of the test tone.
        let centerFreq = 440;

        // Set the cutoff frequency of the lowshelf to be significantly higher
        // than the test tone. Sweep the gain from 20 dB to -20 dB.  (We go from
        // 20 to -20 to easily verify that the filter didn't go unstable.)
        let parameters = {freq: [3500, 3500], gain: [20, -20]};
        let graph = configureGraph(context, centerFreq);
        let f = graph.filter;
        let b = graph.source;

        f.type = 'lowshelf';
        f.frequency.value = parameters.freq[0];
        f.gain.setValueAtTime(parameters.gain[0], 0);
        f.gain.linearRampToValueAtTime(parameters.gain[1], automationEndTime);

        context.startRendering()
            .then(createFilterVerifier(
                should, createLowShelfFilter, 2.7657e-5, parameters,
                b.getChannelData(0),
                'Output of lowshelf filter with gain automation'))
            .then(() => task.done());
      });

      // Automate just the detune parameter.  Basically the same test as for the
      // frequncy parameter but we just use the detune parameter to modulate the
      // frequency parameter.
      audit.define('automate-detune', (task, should) => {
        let context =
            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
        let centerFreq = 10 * 440;
        let parameters = {
          freq: [centerFreq, centerFreq],
          detune: [-10 * 1200, 10 * 1200]
        };
        let graph = configureGraph(context, centerFreq);
        let f = graph.filter;
        let b = graph.source;

        f.type = 'bandpass';
        f.frequency.value = parameters.freq[0];
        f.detune.setValueAtTime(parameters.detune[0], 0);
        f.detune.linearRampToValueAtTime(
            parameters.detune[1], automationEndTime);

        context.startRendering()
            .then(createFilterVerifier(
                should, createBandpassFilter, 3.1471e-5, parameters,
                b.getChannelData(0),
                'Output of bandpass filter with detune automation'))
            .then(() => task.done());
      });

      // Automate all of the filter parameters at once.  This is a basic check
      // that everything is working.  A peaking filter is used because it uses
      // all of the parameters.
      audit.define('automate-all', (task, should) => {
        let context =
            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
        let graph = configureGraph(context, 10 * 440);
        let f = graph.filter;
        let b = graph.source;

        // Sweep all of the filter parameters.  These are pretty much arbitrary.
        let parameters = {
          freq: [8000, 100],
          Q: [f.Q.value, .0001],
          gain: [f.gain.value, 20],
          detune: [2400, -2400]
        };

        f.type = 'peaking';
        // Set starting points for all parameters of the filter.  Start at 10
        // kHz for the center frequency, and the defaults for Q and gain.
        f.frequency.setValueAtTime(parameters.freq[0], 0);
        f.Q.setValueAtTime(parameters.Q[0], 0);
        f.gain.setValueAtTime(parameters.gain[0], 0);
        f.detune.setValueAtTime(parameters.detune[0], 0);

        // Linear ramp each parameter
        f.frequency.linearRampToValueAtTime(
            parameters.freq[1], automationEndTime);
        f.Q.linearRampToValueAtTime(parameters.Q[1], automationEndTime);
        f.gain.linearRampToValueAtTime(parameters.gain[1], automationEndTime);
        f.detune.linearRampToValueAtTime(
            parameters.detune[1], automationEndTime);

        context.startRendering()
            .then(createFilterVerifier(
                should, createPeakingFilter, 6.2907e-4, parameters,
                b.getChannelData(0),
                'Output of peaking filter with automation of all parameters'))
            .then(() => task.done());
      });

      // Test that modulation of the frequency parameter of the filter works.  A
      // sinusoid of 440 Hz is the test signal that is applied to a bandpass
      // biquad filter.  The frequency parameter of the filter is modulated by a
      // sinusoid at 103 Hz, and the frequency modulation varies from 116 to 412
      // Hz.  (This test was taken from the description in
      // https://github.com/WebAudio/web-audio-api/issues/509#issuecomment-94731355)
      audit.define('modulation', (task, should) => {
        let context =
            new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);

        // Create a graph with the sinusoidal source at 440 Hz as the input to a
        // biquad filter.
        let graph = configureGraph(context, 440);
        let f = graph.filter;
        let b = graph.source;

        f.type = 'bandpass';
        f.Q.value = 5;
        f.frequency.value = 264;

        // Create the modulation source, a sinusoid with frequency 103 Hz and
        // amplitude 148.  (The amplitude of 148 is added to the filter's
        // frequency value of 264 to produce a sinusoidal modulation of the
        // frequency parameter from 116 to 412 Hz.)
        let mod = context.createBufferSource();
        let mbuffer =
            context.createBuffer(1, renderDuration * sampleRate, sampleRate);
        let d = mbuffer.getChannelData(0);
        let omega = 2 * Math.PI * 103 / sampleRate;
        for (let k = 0; k < d.length; ++k) {
          d[k] = 148 * Math.sin(omega * k);
        }
        mod.buffer = mbuffer;

        mod.connect(f.frequency);

        mod.start();
        context.startRendering()
            .then(function(resultBuffer) {
              let actual = resultBuffer.getChannelData(0);
              // Compute the filter coefficients using the mod sine wave

              let endFrame = Math.ceil(renderDuration * sampleRate);
              let nCoef = endFrame;
              let b0 = new Float64Array(nCoef);
              let b1 = new Float64Array(nCoef);
              let b2 = new Float64Array(nCoef);
              let a1 = new Float64Array(nCoef);
              let a2 = new Float64Array(nCoef);

              // Generate the filter coefficients when the frequency varies from
              // 116 to 248 Hz using the 103 Hz sinusoid.
              for (let k = 0; k < nCoef; ++k) {
                let freq = f.frequency.value + d[k];
                let c = createBandpassFilter(
                    freq / (sampleRate / 2), f.Q.value, f.gain.value);
                b0[k] = c.b0;
                b1[k] = c.b1;
                b2[k] = c.b2;
                a1[k] = c.a1;
                a2[k] = c.a2;
              }
              reference = timeVaryingFilter(
                  b.getChannelData(0),
                  {b0: b0, b1: b1, b2: b2, a1: a1, a2: a2});

              should(
                  actual,
                  'Output of bandpass filter with sinusoidal modulation of bandpass center frequency')
                  .beCloseToArray(reference, {absoluteThreshold: 3.9787e-5});
            })
            .then(() => task.done());
      });

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