DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
Biquad.h File Reference

Biquad filter with Transposed Direct Form II, Audio EQ Cookbook coefficients, and thread-safe updates. More...

#include <algorithm>
#include <array>
#include <cmath>
#include <numbers>
#include <span>
#include <atomic>
#include <cassert>
#include "AudioBuffer.h"
#include "DenormalGuard.h"
Include dependency graph for Biquad.h:
This graph shows which files directly or indirectly include this file:

Go to the source code of this file.

Classes

struct  dspark::BiquadCoeffs
 Stores normalised biquad coefficients (b0, b1, b2, a1, a2), always double. More...
 
class  dspark::Biquad< T, MaxChannels >
 Biquad filter using Transposed Direct Form II (TDF-II) with thread-safe updates. More...
 

Namespaces

namespace  dspark
 Main namespace for the DSPark framework.
 

Detailed Description

Biquad filter with Transposed Direct Form II, Audio EQ Cookbook coefficients, and thread-safe updates.

Provides two classes:

  • BiquadCoeffs: Coefficient set with static factory methods for all standard filter types. Formulas from Robert Bristow-Johnson's "Audio EQ Cookbook". Memory aligned to 32 bytes for SIMD operations.
  • Biquad<T, MaxChannels>: Per-channel filter state using Transposed Direct Form II (TDF-II). Features a lock-free shadow buffering system to allow safe coefficient updates from the GUI thread without tearing or blocking the audio thread.

Numerical design (why the filter core is double regardless of the sample type): a biquad whose corner sits far below the sample rate parks its poles a hair inside the unit circle - 1 - |z| is about pi*fc/(Q*fs), i.e. 5.8e-5 at 10 Hz / 768 kHz - so the denominator evaluated at DC, 1 + a1 + a2 = (2 - 2*cos w0)/a0, is a cancellation of terms of size 2 landing on 6.7e-9, far under the float resolution of the terms it is made of (1.2e-7). Realised in float that sum is no longer resolvable (measured 5.96e-08 against a true 2.68e-08 at 20 Hz / 768 kHz, Q = 2.5628), the recursion stops being contractive and its rounding error is integrated instead of decaying, which turns a high-pass into a DC SOURCE and, steep enough, into an oscillator. Measured on the float core this replaces, on a DC-free 1 kHz tone at -6 dBFS through FilterEngine: a 10 Hz 12 dB/oct corner published -1.46e-01 of sustained DC at 384 kHz with the peak inflated from 0.5068 to 0.6838 (+2.60 dB), a 20 Hz corner +6.62e-03 at 768 kHz (peak 0.5130 -> 0.5821, +1.10 dB), and the realised DC gain - the sum of the impulse response, which must be zero for a high-pass - reached +0.577 at 20 Hz / 768 kHz and -0.962 at 50 Hz / 768 kHz, i.e. the "high-pass" passed DC at unity. The impulse response says it plainly: 20 Hz at 768 kHz, peak |h| per half second, 24 dB/oct went 1.0 -> 4.1e-04 -> 4.8e-06 and then STOPPED decaying (2.7e-06, 1.6e-06, 8.6e-07: a pole on the circle), and 48 dB/oct went 1.0 -> 1.8e+19 -> 4.2e+37 -> non-finite - an unstable filter, reached from setHighPass(20, 0.707, 48) at a rate a host hits by oversampling 48 kHz by 16. In double the same cancellation carries an absolute error of about 2e-16, so the poles land within 1e-8 (relative) of the design; the same points now decay 1.0 -> 3.8e-04 -> 5.3e-15 -> 9.7e-26 and 1.0 -> 6.7e-04 -> 5.4e-10 -> 2.0e-15. Scalar double throughput matches float on x86-64 and ARM64, so coefficients, history and recursion are all double and only the buffer I/O is T; processSampleCore() exists so a cascade can keep the intermediate signal in the core precision instead of re-quantising it to T between stages.

Dependencies: C++20 standard library (<algorithm>, <array>, <atomic>, <cassert>, <cmath>, <numbers>, ).

Definition in file Biquad.h.