template<typename T, int MaxChannels = 8>
class dspark::Biquad< T, MaxChannels >
Biquad filter using Transposed Direct Form II (TDF-II) with thread-safe updates.
Implements a lock-free shadow buffering system (seqlock) to prevent torn reads when coefficients are updated by the UI thread concurrently with the audio thread. The shared staging slot is made of std::atomic<double> words (relaxed inside the seq-counter critical section, with a writer release / reader acquire fence pair), so the handoff is data-race free by the C++ memory model, not merely tear-free in practice; the audio thread promotes into its private, plain activeCoeffs_, so the per-sample recursion is untouched. Per-channel states are stored compactly so adjacent channels share cache lines during block processing.
The audio-thread side of that handoff is BOUNDED: applyPendingCoeffs() makes at most kSeqlockMaxAttempts validation attempts and then keeps the coefficients already in use, so the callback's worst case is a fixed multiple of one five-word copy rather than however long the control thread holds the counter odd. The fence-pairing derivation is unchanged by that bound, because the bound touches only how many times the derivation is applied: the accept test (even counter, unchanged across the copy) is byte-for-byte the same, and a read that fails it adopts nothing. The trade is stated where it is made – a coefficient update can land one call later under sustained contention.
Threading (SPSC model, see docs/threading.md):
The filter core (coefficients, history and recursion) is always double precision, independent of the sample type: see the file-level documentation for the measurements behind that decision. The realised response is therefore rate-independent down to the corners the designs already supported on paper.
- Template Parameters
-
| T | Sample type (float or double). |
| MaxChannels | Maximum number of independent filter channels. |
Definition at line 660 of file Biquad.h.
template<typename T , int MaxChannels = 8>
Processes a full audio buffer in-place.
Thread-safe block processing. Absorbs asynchronous coefficient updates at the start of the block to ensure atomic parameter changes.
The inner loop runs on a local copy of the coefficients: with no per-sample dirty-flag check in sight, the compiler keeps b0..a2 and the filter state in registers for the whole block (roughly 1.5-2x faster than routing every sample through processSample()).
Channels beyond MaxChannels are left untouched (pass-through): only the first min(numChannels, MaxChannels) channels are filtered.
- Parameters
-
| buffer | Audio buffer to process in-place. |
Definition at line 893 of file Biquad.h.
template<typename T , int MaxChannels = 8>
| T dspark::Biquad< T, MaxChannels >::processSample |
( |
T |
input, |
|
|
int |
channel |
|
) |
| |
|
inlinenoexcept |
Processes a single sample for a specific channel.
Transposed Direct Form II implementation. Self-sufficient: any setCoeffs() update from another thread is picked up here on the very next sample with virtually zero overhead; the fast-path is a relaxed load (a plain MOV on x86) compiled into a single branchless check.
No external sequencing is required. The caller is free to mix processSample() and processBlock() calls in any order, and concurrent setCoeffs() from the GUI thread always becomes audible deterministically within at most a couple of samples (single sample on x86/ARM).
- Precondition
- channel must be in [0, MaxChannels). Enforced by assert in debug builds; out-of-range access in release builds is undefined behaviour.
- Parameters
-
| input | Input sample. |
| channel | Channel index (0-based). |
- Returns
- Filtered output sample.
Definition at line 837 of file Biquad.h.
template<typename T , int MaxChannels = 8>
Sets the filter coefficients asynchronously (control thread).
Safely updates coefficients without locking. The audio thread will pick up the new coefficients safely on the next process call to avoid torn reads and filter blow-ups.
This is the CROSS-THREAD channel: it exists to move a coefficient set from the control thread to the audio thread. Coefficients computed on the processing thread for its own use (self-modulation) belong in setCoeffsNow() instead – routing them through here pays a full publish plus a gated re-adoption of the same thread's own write, inside the hot path (see docs/threading.md, the origin rule).
- Parameters
-
Definition at line 712 of file Biquad.h.
template<typename T , int MaxChannels = 8>
Stream-owner direct set: makes c the active set immediately.
Writes the audio-thread-private active coefficient set with no staging, no seqlock and no dirty flag. This is the setter for coefficients computed ON the processing thread for its own use – self-modulation such as a dynamic EQ's gain refresh or a smoothed filter glide – and for single-threaded / offline use. The staged channel (setCoeffs()) exists to cross threads; a stream owner must never self-publish through it (docs/threading.md, the origin rule).
Caller: the stream owner only – the thread that calls processSample() / processBlock() – or any thread in single-threaded / offline use; the same rule as reset(). NOT for control-thread updates concurrent with processing: those use setCoeffs().
Memory model: composes with the SPSC contract unchanged. The active set keeps exactly one writing thread (the audio thread, whether via staged adoption or via this setter) and the staging words keep exactly one writing thread (the control thread), so this method adds no cross-thread pair.
Dual-master semantics: on an instance that receives BOTH direct sets and cross-thread publications, a pending publication overwrites a direct set at the next adoption gate – the audio thread's last write wins, and an armed dirty flag is a later write. A class embedding Biquads must therefore designate ONE master per instance – control-published (setCoeffs()) or audio-modulated (setCoeffsNow()) – and say so in its threading block.
- Parameters
-
| c | New coefficient set, active from the next processed sample. |
Definition at line 771 of file Biquad.h.