chromium/third_party/blink/web_tests/external/wpt/webaudio/the-audio-api/the-iirfilternode-interface/iirfilter.html

<!DOCTYPE html>
<html>
  <head>
    <title>
      Test Basic IIRFilterNode Operation
    </title>
    <script src="/resources/testharness.js"></script>
    <script src="/resources/testharnessreport.js"></script>
    <script src="../../resources/audit-util.js"></script>
    <script src="../../resources/audit.js"></script>
    <script src="../../resources/biquad-filters.js"></script>
  </head>
  <body>
    <script id="layout-test-code">
      let sampleRate = 24000;
      let testDurationSec = 0.25;
      let testFrames = testDurationSec * sampleRate;

      let audit = Audit.createTaskRunner();

      audit.define('coefficient-normalization', (task, should) => {
        // Test that the feedback coefficients are normalized.  Do this be
        // creating two IIRFilterNodes.  One has normalized coefficients, and
        // one doesn't.  Compute the difference and make sure they're the same.
        let context = new OfflineAudioContext(2, testFrames, sampleRate);

        // Use a simple impulse as the source.
        let buffer = context.createBuffer(1, 1, sampleRate);
        buffer.getChannelData(0)[0] = 1;
        let source = context.createBufferSource();
        source.buffer = buffer;

        // Gain node for computing the difference between the filters.
        let gain = context.createGain();
        gain.gain.value = -1;

        // The IIR filters.  Use a common feedforward array.
        let ff = [1];

        let fb1 = [1, .9];

        let fb2 = new Float64Array(2);
        // Scale the feedback coefficients by an arbitrary factor.
        let coefScaleFactor = 2;
        for (let k = 0; k < fb2.length; ++k) {
          fb2[k] = coefScaleFactor * fb1[k];
        }

        let iir1;
        let iir2;

        should(function() {
          iir1 = context.createIIRFilter(ff, fb1);
        }, 'createIIRFilter with normalized coefficients').notThrow();

        should(function() {
          iir2 = context.createIIRFilter(ff, fb2);
        }, 'createIIRFilter with unnormalized coefficients').notThrow();

        // Create the graph.  The output of iir1 (normalized coefficients) is
        // channel 0, and the output of iir2 (unnormalized coefficients), with
        // appropriate scaling, is channel 1.
        let merger = context.createChannelMerger(2);
        source.connect(iir1);
        source.connect(iir2);
        iir1.connect(merger, 0, 0);
        iir2.connect(gain);

        // The gain for the gain node should be set to compensate for the
        // scaling of the coefficients.  Since iir2 has scaled the coefficients
        // by coefScaleFactor, the output is reduced by the same factor, so
        // adjust the gain to scale the output of iir2 back up.
        gain.gain.value = coefScaleFactor;
        gain.connect(merger, 0, 1);

        merger.connect(context.destination);

        source.start();

        // Rock and roll!

        context.startRendering()
            .then(function(result) {
              // Find the max amplitude of the result, which should be near
              // zero.
              let iir1Data = result.getChannelData(0);
              let iir2Data = result.getChannelData(1);

              // Threshold isn't exactly zero because the arithmetic is done
              // differently between the IIRFilterNode and the BiquadFilterNode.
              should(
                  iir2Data,
                  'Output of IIR filter with unnormalized coefficients')
                  .beCloseToArray(iir1Data, {absoluteThreshold: 2.1958e-38});
            })
            .then(() => task.done());
      });

      audit.define('one-zero', (task, should) => {
        // Create a simple 1-zero filter and compare with the expected output.
        let context = new OfflineAudioContext(1, testFrames, sampleRate);

        // Use a simple impulse as the source
        let buffer = context.createBuffer(1, 1, sampleRate);
        buffer.getChannelData(0)[0] = 1;
        let source = context.createBufferSource();
        source.buffer = buffer;

        // The filter is y(n) = 0.5*(x(n) + x(n-1)), a simple 2-point moving
        // average.  This is rather arbitrary; keep it simple.

        let iir = context.createIIRFilter([0.5, 0.5], [1]);

        // Create the graph
        source.connect(iir);
        iir.connect(context.destination);

        // Rock and roll!
        source.start();

        context.startRendering()
            .then(function(result) {
              let actual = result.getChannelData(0);
              let expected = new Float64Array(testFrames);
              // The filter is a simple 2-point moving average of an impulse, so
              // the first two values are non-zero and the rest are zero.
              expected[0] = 0.5;
              expected[1] = 0.5;
              should(actual, 'IIR 1-zero output').beCloseToArray(expected, {
                absoluteThreshold: 0
              });
            })
            .then(() => task.done());
      });

      audit.define('one-pole', (task, should) => {
        // Create a simple 1-pole filter and compare with the expected output.

        // The filter is y(n) + c*y(n-1)= x(n).  The analytical response is
        // (-c)^n, so choose a suitable number of frames to run the test for
        // where the output isn't flushed to zero.
        let c = 0.9;
        let eps = 1e-20;
        let duration = Math.floor(Math.log(eps) / Math.log(Math.abs(c)));
        let context = new OfflineAudioContext(1, duration, sampleRate);

        // Use a simple impulse as the source
        let buffer = context.createBuffer(1, 1, sampleRate);
        buffer.getChannelData(0)[0] = 1;
        let source = context.createBufferSource();
        source.buffer = buffer;

        let iir = context.createIIRFilter([1], [1, c]);

        // Create the graph
        source.connect(iir);
        iir.connect(context.destination);

        // Rock and roll!
        source.start();

        context.startRendering()
            .then(function(result) {
              let actual = result.getChannelData(0);
              let expected = new Float64Array(actual.length);

              // The filter is a simple 1-pole filter: y(n) = -c*y(n-k)+x(n),
              // with an impulse as the input.
              expected[0] = 1;
              for (k = 1; k < testFrames; ++k) {
                expected[k] = -c * expected[k - 1];
              }

              // Threshold isn't exactly zero due to round-off in the
              // single-precision IIRFilterNode computations versus the
              // double-precision Javascript computations.
              should(actual, 'IIR 1-pole output').beCloseToArray(expected, {
                absoluteThreshold: 2.7657e-8
              });
            })
            .then(() => task.done());
      });

      // Return a function suitable for use as a defineTask function.  This
      // function creates an IIRFilterNode equivalent to the specified
      // BiquadFilterNode and compares the outputs.  The outputs from the two
      // filters should be virtually identical.
      function testWithBiquadFilter(filterType, errorThreshold, snrThreshold) {
        return (task, should) => {
          let context = new OfflineAudioContext(2, testFrames, sampleRate);

          // Use a constant (step function) as the source
          let buffer = createConstantBuffer(context, testFrames, 1);
          let source = context.createBufferSource();
          source.buffer = buffer;


          // Create the biquad.  Choose some rather arbitrary values for Q and
          // gain for the biquad so that the shelf filters aren't identical.
          let biquad = context.createBiquadFilter();
          biquad.type = filterType;
          biquad.Q.value = 10;
          biquad.gain.value = 10;

          // Create the equivalent IIR Filter node by computing the coefficients
          // of the given biquad filter type.
          let nyquist = sampleRate / 2;
          let coef = createFilter(
              filterType, biquad.frequency.value / nyquist, biquad.Q.value,
              biquad.gain.value);

          let iir = context.createIIRFilter(
              [coef.b0, coef.b1, coef.b2], [1, coef.a1, coef.a2]);

          let merger = context.createChannelMerger(2);
          // Create the graph
          source.connect(biquad);
          source.connect(iir);

          biquad.connect(merger, 0, 0);
          iir.connect(merger, 0, 1);

          merger.connect(context.destination);

          // Rock and roll!
          source.start();

          context.startRendering()
              .then(function(result) {
                // Find the max amplitude of the result, which should be near
                // zero.
                let expected = result.getChannelData(0);
                let actual = result.getChannelData(1);

                // On MacOSX, WebAudio uses an optimized Biquad implementation
                // that is different from the implementation used for Linux and
                // Windows.  This will cause the output to differ, even if the
                // threshold passes.  Thus, only print out a very small number
                // of elements of the array where we have tested that they are
                // consistent.
                should(actual, 'IIRFilter for Biquad ' + filterType)
                    .beCloseToArray(expected, errorThreshold);

                let snr = 10 * Math.log10(computeSNR(actual, expected));
                should(snr, 'SNR for IIRFIlter for Biquad ' + filterType)
                    .beGreaterThanOrEqualTo(snrThreshold);
              })
              .then(() => task.done());
        };
      }

      // Thresholds here are experimentally determined.
      let biquadTestConfigs = [
        {
          filterType: 'lowpass',
          snrThreshold: 91.221,
          errorThreshold: {relativeThreshold: 4.9834e-5}
        },
        {
          filterType: 'highpass',
          snrThreshold: 105.4590,
          errorThreshold: {absoluteThreshold: 2.9e-6, relativeThreshold: 3e-5}
        },
        {
          filterType: 'bandpass',
          snrThreshold: 104.060,
          errorThreshold: {absoluteThreshold: 2e-7, relativeThreshold: 8.7e-4}
        },
        {
          filterType: 'notch',
          snrThreshold: 91.312,
          errorThreshold: {absoluteThreshold: 0, relativeThreshold: 4.22e-5}
        },
        {
          filterType: 'allpass',
          snrThreshold: 91.319,
          errorThreshold: {absoluteThreshold: 0, relativeThreshold: 4.31e-5}
        },
        {
          filterType: 'lowshelf',
          snrThreshold: 90.609,
          errorThreshold: {absoluteThreshold: 0, relativeThreshold: 2.98e-5}
        },
        {
          filterType: 'highshelf',
          snrThreshold: 103.159,
          errorThreshold: {absoluteThreshold: 0, relativeThreshold: 1.24e-5}
        },
        {
          filterType: 'peaking',
          snrThreshold: 91.504,
          errorThreshold: {absoluteThreshold: 0, relativeThreshold: 5.05e-5}
        }
      ];

      // Create a set of tasks based on biquadTestConfigs.
      for (k = 0; k < biquadTestConfigs.length; ++k) {
        let config = biquadTestConfigs[k];
        let name = k + ': ' + config.filterType;
        audit.define(
            name,
            testWithBiquadFilter(
                config.filterType, config.errorThreshold, config.snrThreshold));
      }

      audit.define('multi-channel', (task, should) => {
        // Multi-channel test.  Create a biquad filter and the equivalent IIR
        // filter.  Filter the same multichannel signal and compare the results.
        let nChannels = 3;
        let context =
            new OfflineAudioContext(nChannels, testFrames, sampleRate);

        // Create a set of oscillators as the multi-channel source.
        let source = [];

        for (k = 0; k < nChannels; ++k) {
          source[k] = context.createOscillator();
          source[k].type = 'sawtooth';
          // The frequency of the oscillator is pretty arbitrary, but each
          // oscillator should have a different frequency.
          source[k].frequency.value = 100 + k * 100;
        }

        let merger = context.createChannelMerger(3);

        let biquad = context.createBiquadFilter();

        // Create the equivalent IIR Filter node.
        let nyquist = sampleRate / 2;
        let coef = createFilter(
            biquad.type, biquad.frequency.value / nyquist, biquad.Q.value,
            biquad.gain.value);
        let fb = [1, coef.a1, coef.a2];
        let ff = [coef.b0, coef.b1, coef.b2];

        let iir = context.createIIRFilter(ff, fb);
        // Gain node to compute the difference between the IIR and biquad
        // filter.
        let gain = context.createGain();
        gain.gain.value = -1;

        // Create the graph.
        for (k = 0; k < nChannels; ++k)
          source[k].connect(merger, 0, k);

        merger.connect(biquad);
        merger.connect(iir);
        iir.connect(gain);
        biquad.connect(context.destination);
        gain.connect(context.destination);

        for (k = 0; k < nChannels; ++k)
          source[k].start();

        context.startRendering()
            .then(function(result) {
              let errorThresholds = [3.7671e-5, 3.0071e-5, 2.6241e-5];

              // Check the difference signal on each channel
              for (channel = 0; channel < result.numberOfChannels; ++channel) {
                // Find the max amplitude of the result, which should be near
                // zero.
                let data = result.getChannelData(channel);
                let maxError =
                    data.reduce(function(reducedValue, currentValue) {
                      return Math.max(reducedValue, Math.abs(currentValue));
                    });

                should(
                    maxError,
                    'Max difference between IIR and Biquad on channel ' +
                        channel)
                    .beLessThanOrEqualTo(errorThresholds[channel]);
              }

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

      // Apply an IIRFilter to the given input signal.
      //
      // IIR filter in the time domain is
      //
      //   y[n] = sum(ff[k]*x[n-k], k, 0, M) - sum(fb[k]*y[n-k], k, 1, N)
      //
      function iirFilter(input, feedforward, feedback) {
        // For simplicity, create an x buffer that contains the input, and a y
        // buffer that contains the output.  Both of these buffers have an
        // initial work space to implement the initial memory of the filter.
        let workSize = Math.max(feedforward.length, feedback.length);
        let x = new Float32Array(input.length + workSize);

        // Float64 because we want to match the implementation that uses doubles
        // to minimize roundoff.
        let y = new Float64Array(input.length + workSize);

        // Copy the input over.
        for (let k = 0; k < input.length; ++k)
          x[k + feedforward.length] = input[k];

        // Run the filter
        for (let n = 0; n < input.length; ++n) {
          let index = n + workSize;
          let yn = 0;
          for (let k = 0; k < feedforward.length; ++k)
            yn += feedforward[k] * x[index - k];
          for (let k = 0; k < feedback.length; ++k)
            yn -= feedback[k] * y[index - k];

          y[index] = yn;
        }

        return y.slice(workSize).map(Math.fround);
      }

      // Cascade the two given biquad filters to create one IIR filter.
      function cascadeBiquads(f1Coef, f2Coef) {
        // The biquad filters are:
        //
        // f1 = (b10 + b11/z + b12/z^2)/(1 + a11/z + a12/z^2);
        // f2 = (b20 + b21/z + b22/z^2)/(1 + a21/z + a22/z^2);
        //
        // To cascade them, multiply the two transforms together to get a fourth
        // order IIR filter.

        let numProduct = [
          f1Coef.b0 * f2Coef.b0, f1Coef.b0 * f2Coef.b1 + f1Coef.b1 * f2Coef.b0,
          f1Coef.b0 * f2Coef.b2 + f1Coef.b1 * f2Coef.b1 + f1Coef.b2 * f2Coef.b0,
          f1Coef.b1 * f2Coef.b2 + f1Coef.b2 * f2Coef.b1, f1Coef.b2 * f2Coef.b2
        ];

        let denProduct = [
          1, f2Coef.a1 + f1Coef.a1,
          f2Coef.a2 + f1Coef.a1 * f2Coef.a1 + f1Coef.a2,
          f1Coef.a1 * f2Coef.a2 + f1Coef.a2 * f2Coef.a1, f1Coef.a2 * f2Coef.a2
        ];

        return {
          ff: numProduct, fb: denProduct
        }
      }

      // Find the magnitude of the root of the quadratic that has the maximum
      // magnitude.
      //
      // The quadratic is z^2 + a1 * z + a2 and we want the root z that has the
      // largest magnitude.
      function largestRootMagnitude(a1, a2) {
        let discriminant = a1 * a1 - 4 * a2;
        if (discriminant < 0) {
          // Complex roots:  -a1/2 +/- i*sqrt(-d)/2.  Thus the magnitude of each
          // root is the same and is sqrt(a1^2/4 + |d|/4)
          let d = Math.sqrt(-discriminant);
          return Math.hypot(a1 / 2, d / 2);
        } else {
          // Real roots
          let d = Math.sqrt(discriminant);
          return Math.max(Math.abs((-a1 + d) / 2), Math.abs((-a1 - d) / 2));
        }
      }

      audit.define('4th-order-iir', (task, should) => {
        // Cascade 2 lowpass biquad filters and compare that with the equivalent
        // 4th order IIR filter.

        let nyquist = sampleRate / 2;
        // Compute the coefficients of a lowpass filter.

        // First some preliminary stuff.  Compute the coefficients of the
        // biquad.  This is used to figure out how frames to use in the test.
        let biquadType = 'lowpass';
        let biquadCutoff = 350;
        let biquadQ = 5;
        let biquadGain = 1;

        let coef = createFilter(
            biquadType, biquadCutoff / nyquist, biquadQ, biquadGain);

        // Cascade the biquads together to create an equivalent IIR filter.
        let cascade = cascadeBiquads(coef, coef);

        // Since we're cascading two identical biquads, the root of denominator
        // of the IIR filter is repeated, so the root of the denominator with
        // the largest magnitude occurs twice.  The impulse response of the IIR
        // filter will be roughly c*(r*r)^n at time n, where r is the root of
        // largest magnitude.  This approximation gets better as n increases.
        // We can use this to get a rough idea of when the response has died
        // down to a small value.

        // This is the value we will use to determine how many frames to render.
        // Rendering too many is a waste of time and also makes it hard to
        // compare the actual result to the expected because the magnitudes are
        // so small that they could be mostly round-off noise.
        //
        // Find magnitude of the root with largest magnitude
        let rootMagnitude = largestRootMagnitude(coef.a1, coef.a2);

        // Find n such that |r|^(2*n) <= eps.  That is, n = log(eps)/(2*log(r)).
        // Somewhat arbitrarily choose eps = 1e-20;
        let eps = 1e-20;
        let framesForTest =
            Math.floor(Math.log(eps) / (2 * Math.log(rootMagnitude)));

        // We're ready to create the graph for the test.  The offline context
        // has two channels: channel 0 is the expected (cascaded biquad) result
        // and channel 1 is the actual IIR filter result.
        let context = new OfflineAudioContext(2, framesForTest, sampleRate);

        // Use a simple impulse with a large (arbitrary) amplitude as the source
        let amplitude = 1;
        let buffer = context.createBuffer(1, testFrames, sampleRate);
        buffer.getChannelData(0)[0] = amplitude;
        let source = context.createBufferSource();
        source.buffer = buffer;

        // Create the two biquad filters.  Doesn't really matter what, but for
        // simplicity we choose identical lowpass filters with the same
        // parameters.
        let biquad1 = context.createBiquadFilter();
        biquad1.type = biquadType;
        biquad1.frequency.value = biquadCutoff;
        biquad1.Q.value = biquadQ;

        let biquad2 = context.createBiquadFilter();
        biquad2.type = biquadType;
        biquad2.frequency.value = biquadCutoff;
        biquad2.Q.value = biquadQ;

        let iir = context.createIIRFilter(cascade.ff, cascade.fb);

        // Create the merger to get the signals into multiple channels
        let merger = context.createChannelMerger(2);

        // Create the graph, filtering the source through two biquads.
        source.connect(biquad1);
        biquad1.connect(biquad2);
        biquad2.connect(merger, 0, 0);

        source.connect(iir);
        iir.connect(merger, 0, 1);

        merger.connect(context.destination);

        // Now filter the source through the IIR filter.
        let y = iirFilter(buffer.getChannelData(0), cascade.ff, cascade.fb);

        // Rock and roll!
        source.start();

        context.startRendering()
            .then(function(result) {
              let expected = result.getChannelData(0);
              let actual = result.getChannelData(1);

              should(actual, '4-th order IIRFilter (biquad ref)')
                  .beCloseToArray(expected, {
                    // Thresholds experimentally determined.
                    absoluteThreshold: 1.59e-7,
                    relativeThreshold: 2.11e-5,
                  });

              let snr = 10 * Math.log10(computeSNR(actual, expected));
              should(snr, 'SNR of 4-th order IIRFilter (biquad ref)')
                  .beGreaterThanOrEqualTo(108.947);
            })
            .then(() => task.done());
      });

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