Digital Biquad Filters


Second order recursive linear filtering

Brief:


Biquad refers to the fact that the transfer function of the filter is the ratio of two quadratic functions:

\(H(z) = {b_{0} + b_{1}z^{-1} + b_{2}z^{-2} \over {a_{0} + a_{1}z^{-1} + a_{2}z^{-2}}}\)

Higher-order IIR filters are prone to instability due to coefficient quantization, so they are often implemented as cascaded biquad sections, ensuring all poles remain inside the unit circle in the Z-domain for stability.

Second order filters are also very simple to implement, as they only require managing six coefficient values and four state values. Implementing a biquad filter is as simple as defining the difference equation:

\( y_{n} = {b_{0} \over a_{0}}x_{n} + {b_{1} \over a_{0}}x_{n - 1} + {b_{2} \over a_{0}}x_{n - 2} - {a_{1} \over a_{0}}y_{n - 1} - {a_{2} \over a_{0}}y_{n - 2}\)


C++ Implementation:


/// DigitalBiquadFilter.h

/**
Copyright © 2025 Alex Parisi

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

#ifndef DIGITAL_BIQUAD_FILTER_H
#define DIGITAL_BIQUAD_FILTER_H

#include <array>
#include <concepts>
#include <optional>
#include <vector>

#include "Logging.h"

/**
 * @brief Coefficients Struct
 * @details A struct containing the coefficients of a digital biquad filter
 */
template<std::floating_point T>
struct Coefficients {
    T b0, b1, b2, a0, a1, a2;
};

/**
 * @brief State Struct
 * @details A struct containing the state variables of a digital biquad filter
 */
template<std::floating_point T>
struct State {
    T x1, x2, y1, y2;
};

/**
 * @brief Digital Biquad Filter
 * @details A digital biquad filter is a type of infinite impulse response
 * filter. The transfer function is defined by the following equation: H(z) =
 * (b0 + b1*z^-1 + b2*z^-2) / (a0 + a1*z^-1 + a2*z^-2) The filter is implemented
 * by applying the following difference equation: y[n] = b0*x[n] + b1*x[n-1] +
 * b2*x[n-2] - a1*y[n-1] - a2*y[n-2]
 */
template<std::floating_point T, size_t blockSize = 0>
class DigitalBiquadFilter {
public:
    /**
     * @brief Public Constructor
     * @details Creates a filter with specified coefficients
     * @param coefficients A Coefficients struct containing the filter
     * coefficients: b0, b1 b2, a0, a1, a2
     * @see Coefficients
     */
    static auto create(const Coefficients<T> &coefficients)
            -> std::optional<DigitalBiquadFilter> {
        if (coefficients.a0 == 0.0) {
            LOG_E("DigitalBiquadFilter",
                  "Error creating DigitalBiquadFilter object: the a0 "
                  "coefficient cannot be zero");
            return std::nullopt;
        }
        return DigitalBiquadFilter(coefficients);
    }
    /**
     * @brief Process a sample
     * @details Processes a single sample of audio data
     * @param sample The input sample
     */
    auto process(const T &sample) -> T {
        const T output =
                ((m_coefficients.b0 / m_coefficients.a0) * sample) +
                ((m_coefficients.b1 / m_coefficients.a0) * m_state.x1) +
                ((m_coefficients.b2 / m_coefficients.a0) * m_state.x2) -
                ((m_coefficients.a1 / m_coefficients.a0) * m_state.y1) -
                ((m_coefficients.a2 / m_coefficients.a0) * m_state.y2);
        m_state.x2 = m_state.x1;
        m_state.x1 = sample;
        m_state.y2 = m_state.y1;
        m_state.y1 = output;

        m_iter++;
        return output;
    }
    /**
     * @brief Process a block of samples
     * @details Processes a block of samples of audio data
     * @param samples A pointer to the block of samples. The samples are edited
     * in-place. This assumes that the pointer has enough memory allocated to
     * pull out blockSize samples and is inherently unsafe. For block,
     * processing, it's safer to use the block process methods with either a
     * std::vector or std::array.
     */
    auto process(T *samples) -> void {
        if (blockSize <= 0) {
            LOG_E("DigitalBiquadFilter",
                  "Error processing block of samples: blockSize must be "
                  "greater than zero.");
            return;
        }
        if (samples == nullptr) {
            LOG_E("DigitalBiquadFilter",
                  "Error processing block of samples: samples pointer is null");
            return;
        }
        for (size_t i = 0; i < blockSize; ++i) {
            samples[i] = process(samples[i]);
        }
    }
    /**
     * @brief Process a block of samples
     * @details Processes a block of samples of audio data. Since this method
     * uses a std::vector as input, we don't need to rely on blockSize being
     * passed in. The samples are edited in place.
     * @param samples A vector of samples
     */
    auto process(std::vector<T> &samples) -> void {
        if (samples.empty()) {
            LOG_E("DigitalBiquadFilter",
                  "Error processing block of samples: samples vector is empty");
            return;
        }
        for (auto &sample: samples) {
            sample = process(sample);
        }
    }
    /**
     * @brief Process a block of samples
     * @details Processes a block of samples of audio data. The samples are
     * edited in place.
     * @param samples An array of samples
     */
    auto process(std::array<T, blockSize> &samples) -> void {
        if (blockSize <= 0) {
            LOG_E("DigitalBiquadFilter", "Error processing block of samples: "
                                         "blockSize must be greater than zero");
            return;
        }
        for (size_t i = 0; i < blockSize; ++i) {
            samples[i] = process(samples[i]);
        }
    }
    /**
     * @brief Set the filter coefficients
     * @details Sets the filter coefficients to custom values
     * @param coefficients A Coefficients struct containing the filter
     * coefficients: b0, b1 b2, a0, a1, a2
     * @see Coefficients
     */
    auto set_coefficients(const Coefficients<T> &coefficients) -> void {
        m_coefficients = coefficients;
        reset();
    }
    /**
     * @brief Reset the filter
     * @details Resets the filter state variables
     */
    auto reset() -> void {
        m_state.x1 = 0.0;
        m_state.x2 = 0.0;
        m_state.y1 = 0.0;
        m_state.y2 = 0.0;
        m_iter = 0;
    }

private:
    /**
     * @brief Private Constructor
     * @details Initializes the filter with specified coefficients
     * @param coefficients A Coefficients struct containing the filter
     * coefficients: b0, b1 b2, a0, a1, a2
     * @see Coefficients
     */
    explicit DigitalBiquadFilter(const Coefficients<T> &coefficients) :
        m_coefficients(coefficients) {}

    /** The filter coefficients */
    Coefficients<T> m_coefficients = {};

    /** The filter state variables */
    State<T> m_state = {};

    /** The number of iterations */
    long long m_iter = 0LL;
};

#endif // DIGITAL_BIQUAD_FILTER_H

Notes:


  • There is a concept applied to the templated coefficients struct, state struct, and filter class: std::floating_point. This ensures that the DigitalBiquadFilter class can only be templated with a float or double type.
  • To reduce the chance of encountering quantization noise, it's recommended to always template the DigitalBiquadFilter class with a double type.