utils/biquad.js

/*
 * This file is part of AUX.
 *
 * AUX is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * AUX is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General
 * Public License along with this program; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301  USA
 */

/**
 * This module contains functions for deadling with biquad filters and
 * also implementation of common filters.
 * The formulae for 'standard' biquad filter coefficients are taken
 * from
 *  "Cookbook formulae for audio EQ biquad filter coefficients"
 *  by Robert Bristow-Johnson.
 *
 * @module utils/biquad
 */

/**
 * Trivial filter.
 *
 * @implements {@link BiquadTransform}
 */
export function NULL(O) {
  /* this biquad does not do anything */
  return {
    b0: 1,
    b1: 1,
    b2: 1,
    a0: 1,
    a1: 1,
    a2: 1,
    sample_rate: O.sample_rate,
  };
}

/**
 * Low-Shelf filter.
 *
 * @implements {@link BiquadTransform}
 */
export function lowShelf(O) {
  const cos = Math.cos,
    sqrt = Math.sqrt,
    A = Math.pow(10, O.gain / 40),
    w0 = (2 * Math.PI * O.freq) / O.sample_rate,
    alpha = Math.sin(w0) / (2 * O.q);
  return {
    b0: A * (A + 1 - (A - 1) * cos(w0) + 2 * sqrt(A) * alpha),
    b1: 2 * A * (A - 1 - (A + 1) * cos(w0)),
    b2: A * (A + 1 - (A - 1) * cos(w0) - 2 * sqrt(A) * alpha),
    a0: A + 1 + (A - 1) * cos(w0) + 2 * sqrt(A) * alpha,
    a1: -2 * (A - 1 + (A + 1) * cos(w0)),
    a2: A + 1 + (A - 1) * cos(w0) - 2 * sqrt(A) * alpha,
    sample_rate: O.sample_rate,
  };
}

/**
 * High-Shelf filter.
 *
 * @implements {@link BiquadTransform}
 */
export function highShelf(O) {
  const cos = Math.cos;
  const sqrt = Math.sqrt;
  const A = Math.pow(10, O.gain / 40);
  const w0 = (2 * Math.PI * O.freq) / O.sample_rate;
  const alpha = Math.sin(w0) / (2 * O.q);
  return {
    b0: A * (A + 1 + (A - 1) * cos(w0) + 2 * sqrt(A) * alpha),
    b1: -2 * A * (A - 1 + (A + 1) * cos(w0)),
    b2: A * (A + 1 + (A - 1) * cos(w0) - 2 * sqrt(A) * alpha),
    a0: A + 1 - (A - 1) * cos(w0) + 2 * sqrt(A) * alpha,
    a1: 2 * (A - 1 - (A + 1) * cos(w0)),
    a2: A + 1 - (A - 1) * cos(w0) - 2 * sqrt(A) * alpha,
    sample_rate: O.sample_rate,
  };
}

/**
 * Peak filter.
 *
 * @implements {@link BiquadTransform}
 */
export function peaking(O) {
  const cos = Math.cos;
  const A = Math.pow(10, O.gain / 40);
  const w0 = (2 * Math.PI * O.freq) / O.sample_rate;
  const alpha = Math.sin(w0) / (2 * O.q);
  return {
    b0: 1 + alpha * A,
    b1: -2 * cos(w0),
    b2: 1 - alpha * A,
    a0: 1 + alpha / A,
    a1: -2 * cos(w0),
    a2: 1 - alpha / A,
    sample_rate: O.sample_rate,
  };
}

/**
 * notch filter.
 *
 * @implements {@link BiquadTransform}
 */
export function notch(O) {
  const cos = Math.cos;
  const w0 = (2 * Math.PI * O.freq) / O.sample_rate;
  const alpha = Math.sin(w0) / (2 * O.q);
  return {
    b0: 1,
    b1: -2 * cos(w0),
    b2: 1,
    a0: 1 + alpha,
    a1: -2 * cos(w0),
    a2: 1 - alpha,
    sample_rate: O.sample_rate,
  };
}

/**
 * This is a standard lowpass filter with transfer function
 * H(s) = 1/(1+s).
 *
 * @implements {@link BiquadTransform}
 */
export function lowPass1(O) {
  const w0 = (2 * Math.PI * O.freq) / O.sample_rate,
    s0 = Math.sin(w0),
    c0 = Math.cos(w0);
  return {
    b0: 1 - c0,
    b1: 2 * (1 - c0),
    b2: 1 - c0,
    a0: 1 - c0 + s0,
    a1: 2 * (1 - c0),
    a2: 1 - c0 - s0,
    sample_rate: O.sample_rate,
  };
}

/**
 * @implements {@link BiquadTransform}
 */
export function lowPass2(O) {
  const cos = Math.cos;
  const w0 = (2 * Math.PI * O.freq) / O.sample_rate;
  const alpha = Math.sin(w0) / (2 * O.q);
  return {
    b0: (1 - cos(w0)) / 2,
    b1: 1 - cos(w0),
    b2: (1 - cos(w0)) / 2,
    a0: 1 + alpha,
    a1: -2 * cos(w0),
    a2: 1 - alpha,
    sample_rate: O.sample_rate,
  };
}

/**
 * @implements {@link BiquadTransform}
 */
export function lowPass4(O) {
  O = lowPass2(O);
  O.factor = 2;
  return O;
}

/**
 * This is a standard highpass filter with transfer function
 * H(s) = s/(1+s).
 *
 * @implements {@link BiquadTransform}
 */
export function highPass1(O) {
  const w0 = (2 * Math.PI * O.freq) / O.sample_rate,
    s0 = Math.sin(w0),
    c0 = Math.cos(w0);
  return {
    b0: s0,
    b1: 0,
    b2: -s0,
    a0: 1 - c0 + s0,
    a1: 2 * (1 - c0),
    a2: 1 - c0 - s0,
    sample_rate: O.sample_rate,
  };
}

/**
 * @implements {@link BiquadTransform}
 */
export function highPass2(O) {
  const cos = Math.cos;
  const w0 = (2 * Math.PI * O.freq) / O.sample_rate;
  const alpha = Math.sin(w0) / (2 * O.q);
  return {
    b0: (1 + cos(w0)) / 2,
    b1: -(1 + cos(w0)),
    b2: (1 + cos(w0)) / 2,
    a0: 1 + alpha,
    a1: -2 * cos(w0),
    a2: 1 - alpha,
    sample_rate: O.sample_rate,
  };
}

/**
 * @implements {@link BiquadTransform}
 */
export function highPass4(O) {
  O = highPass2(O);
  O.factor = 2;
  return O;
}

/**
 * A set of standard filters.
 *
 * @property {Filter~filter_factory} null - The trivial filter, which does not
 *      change the gain.
 * @property {Filter~filter_factory} parametric - peaking.
 * @property {Filter~filter_factory} notch -  notch.
 * @property {Filter~filter_factory} low-shelf - lowShelf.
 * @property {Filter~filter_factory} high-shelf - highShelf.
 * @property {Filter~filter_factory} lowpass1 - lowPass1.
 * @property {Filter~filter_factory} lowpass2 - lowPass2.
 * @property {Filter~filter_factory} lowpass3 - lowPass3.
 * @property {Filter~filter_factory} lowpass4 - lowPass4.
 * @property {Filter~filter_factory} highpass1 - highPass1.
 * @property {Filter~filter_factory} highpass2 - highPass2.
 * @property {Filter~filter_factory} highpass3 - highPass3.
 * @property {Filter~filter_factory} highpass4 - highPass4.
 */
export const standardBiquadFilters = {
  null: biquadFilter(NULL),
  'low-shelf': biquadFilter(lowShelf),
  'high-shelf': biquadFilter(highShelf),
  parametric: biquadFilter(peaking),
  notch: biquadFilter(notch),
  lowpass1: biquadFilter(lowPass1),
  lowpass2: biquadFilter(lowPass2),
  lowpass3: biquadFilter(lowPass1, lowPass2),
  lowpass4: biquadFilter(lowPass4),
  highpass1: biquadFilter(highPass1),
  highpass2: biquadFilter(highPass2),
  highpass3: biquadFilter(highPass1, highPass2),
  highpass4: biquadFilter(highPass4),
};

function nullFilter() {
  return 0;
}

//function bilinearModule(O) {
//var log = Math.log;
//var sin = Math.sin;

//var LN10_10 = ((O.factor || 1.0) * 10) / Math.LN10;
//var PI = +(Math.PI / O.sample_rate);
//var Ra = +(((O.a0 + O.a1) * (O.a0 + O.a1)) / 4);
//var Rb = +(((O.b0 + O.b1) * (O.b0 + O.b1)) / 4);
//var Ya = +(O.a1 * O.a0);
//var Yb = +(O.b1 * O.b0);

//if (Ra === Rb && Ya === Yb) return nullModule;

//function frequencyToGain(f) {
//f = +f;
//var S = +sin(PI * f);
//S *= S;
//return LN10_10 * log((Rb - S * Yb) / (Ra - S * Ya));
//}

//return frequencyToGain;
//}

function biquadModule(O) {
  const log = Math.log;
  const sin = Math.sin;

  const LN10_10 = ((O.factor || 1.0) * 10) / Math.LN10;
  const PI = +(Math.PI / O.sample_rate);
  const Ra = +(((O.a0 + O.a1 + O.a2) * (O.a0 + O.a1 + O.a2)) / 4);
  const Rb = +(((O.b0 + O.b1 + O.b2) * (O.b0 + O.b1 + O.b2)) / 4);
  const Xa = +(4 * O.a0 * O.a2);
  const Ya = +(O.a1 * (O.a0 + O.a2));
  const Xb = +(4 * O.b0 * O.b2);
  const Yb = +(O.b1 * (O.b0 + O.b2));

  if (Ra === Rb && Ya === Yb && Xa === Xb) return nullFilter;

  return function (f) {
    f = +f;
    let S = +sin(PI * f);
    S *= S;
    return (
      LN10_10 *
      log((Rb - S * (Xb * (1 - S) + Yb)) / (Ra - S * (Xa * (1 - S) + Ya)))
    );
  };
}

function biquadFilter1(trafo) {
  function factory(O) {
    return biquadModule(trafo(O));
  }

  return factory;
}

function biquadFilterN(trafos) {
  function factory(O) {
    const A = new Array(trafos.length);
    let i;

    for (i = 0; i < trafos.length; i++) {
      A[i] = biquadModule(trafos[i](O));
    }

    return function (f) {
      let ret = 0.0;
      let j;

      for (j = 0; j < A.length; j++) {
        ret += A[j](f);
      }

      return ret;
    };
  }

  return factory;
}

/**
 * @callback biquadTransform
 *
 * @param {Object} options - The filter parameters.
 * @param {number} options.freq - The current frequency, i.e. the x position in
 *      the equalizer graph.
 * @param {number} options.gain - The current gain, i.e. the y position in the
 *      equalizer graph.
 * @param {number} options.q - The Q of the filter, i.e. the z position in the
 *      equalizer graph.
 * @param {number} options.sample_rate - The sample rate.
 * @returns {Object} - An object containing the biquad coefficients `a0`, `a1`,
 * `a2`, `b0`, `b1`, `b2` and the sample rate `sample_rate`.
 */

/**
 * This function can be used to turn a series of biquad filter transformations
 * into an object which implements the {@link EqFilter} interface. In other
 * words, this method will calculate the frequency response of generic biquad
 * filters based on their biquad coefficients.
 *
 * @param {...Function} transforms - The list of biquad transformations. Each of
 *      these functions is expected to implement the {@link BiquadTransform}
 *      interface.
 * @returns {EqFilter} - The filter object.
 */
export function biquadFilter() {
  if (arguments.length === 1) return biquadFilter1(arguments[0]);

  return biquadFilterN.call(this, Array.prototype.slice.call(arguments));
}