DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
dspark::Biquad< T, MaxChannels > Class Template Reference

Biquad filter using Transposed Direct Form II (TDF-II) with thread-safe updates. More...

#include <Biquad.h>

Public Member Functions

 Biquad () noexcept=default
 
 Biquad (Biquad &&other) noexcept
 
Biquad & operator= (Biquad &&other) noexcept
 
 Biquad (const Biquad &)=delete
 
Biquad & operator= (const Biquad &)=delete
 
void setCoeffs (const BiquadCoeffs &c) noexcept
 Sets the filter coefficients asynchronously (control thread).
 
void setCoeffsNow (const BiquadCoeffs &c) noexcept
 Stream-owner direct set: makes c the active set immediately.
 
bool applyPendingCoeffs () noexcept
 Promotes any pending staged coefficients to active.
 
const BiquadCoeffs & getCoeffs () const noexcept
 Returns the active coefficient set currently in use by the DSP thread.
 
void reset () noexcept
 Resets all per-channel filter states to zero to avoid ringing/clicks.
 
T processSample (T input, int channel) noexcept
 Processes a single sample for a specific channel.
 
double processSampleCore (double input, int channel) noexcept
 One recursion step in the core's own precision (double).
 
void processBlock (AudioBufferView< T > buffer) noexcept
 Processes a full audio buffer in-place.
 

Detailed Description

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
TSample type (float or double).
MaxChannelsMaximum number of independent filter channels.

Definition at line 660 of file Biquad.h.

Constructor & Destructor Documentation

◆ Biquad() [1/3]

template<typename T , int MaxChannels = 8>
dspark::Biquad< T, MaxChannels >::Biquad ( )
defaultnoexcept

◆ Biquad() [2/3]

template<typename T , int MaxChannels = 8>
dspark::Biquad< T, MaxChannels >::Biquad ( Biquad< T, MaxChannels > &&  other)
inlinenoexcept

Definition at line 672 of file Biquad.h.

◆ Biquad() [3/3]

template<typename T , int MaxChannels = 8>
dspark::Biquad< T, MaxChannels >::Biquad ( const Biquad< T, MaxChannels > &  )
delete

Member Function Documentation

◆ applyPendingCoeffs()

template<typename T , int MaxChannels = 8>
bool dspark::Biquad< T, MaxChannels >::applyPendingCoeffs ( )
inlinenoexcept

Promotes any pending staged coefficients to active.

Real-time safe. Both processBlock() and processSample() invoke this automatically (the per-sample call is gated by a relaxed-load fast path so it is essentially free when there is no pending update).

Exposed publicly for the rare case where you have just called setCoeffs() on this same thread and want the change reflected immediately, e.g. for an offline / introspection getCoeffs() right after pushing new ones.

Returns
true if the active coefficients changed in this call. false means nothing was pending OR the adoption was DEFERRED: the read is bounded (see below), so under a concurrently publishing control thread it may give up, keep the coefficients already in use and re-arm the pending flag, and the update then lands on a later call. The single-thread use documented above is unaffected: with no concurrent writer the sequence counter is even and stable, so the first attempt always validates.

Definition at line 794 of file Biquad.h.

◆ getCoeffs()

template<typename T , int MaxChannels = 8>
const BiquadCoeffs & dspark::Biquad< T, MaxChannels >::getCoeffs ( ) const
inlinenoexcept

Returns the active coefficient set currently in use by the DSP thread.

Intended for the thread that owns processing (or single-threaded / offline use). A GUI thread reading this concurrently with a promotion may observe a mid-update set; for drawing response curves, keep your own copy of the coefficients you computed.

Definition at line 808 of file Biquad.h.

◆ operator=() [1/2]

template<typename T , int MaxChannels = 8>
Biquad & dspark::Biquad< T, MaxChannels >::operator= ( Biquad< T, MaxChannels > &&  other)
inlinenoexcept

Definition at line 681 of file Biquad.h.

◆ operator=() [2/2]

template<typename T , int MaxChannels = 8>
Biquad & dspark::Biquad< T, MaxChannels >::operator= ( const Biquad< T, MaxChannels > &  )
delete

◆ processBlock()

template<typename T , int MaxChannels = 8>
void dspark::Biquad< T, MaxChannels >::processBlock ( AudioBufferView< T >  buffer)
inlinenoexcept

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
bufferAudio buffer to process in-place.

Definition at line 893 of file Biquad.h.

◆ processSample()

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
inputInput sample.
channelChannel index (0-based).
Returns
Filtered output sample.

Definition at line 837 of file Biquad.h.

◆ processSampleCore()

template<typename T , int MaxChannels = 8>
double dspark::Biquad< T, MaxChannels >::processSampleCore ( double  input,
int  channel 
)
inlinenoexcept

One recursion step in the core's own precision (double).

Same filter, same state, no conversion at the boundary: a cascade calls this to keep the intermediate signal in the core precision instead of re-quantising it to T between stages (FilterEngine does). Identical contract to processSample() otherwise, including the lock-free pickup of pending coefficients.

Precondition
channel must be in [0, MaxChannels).
Parameters
inputInput sample.
channelChannel index (0-based).
Returns
Filtered output sample.

Definition at line 857 of file Biquad.h.

◆ reset()

template<typename T , int MaxChannels = 8>
void dspark::Biquad< T, MaxChannels >::reset ( )
inlinenoexcept

Resets all per-channel filter states to zero to avoid ringing/clicks.

Definition at line 811 of file Biquad.h.

◆ setCoeffs()

template<typename T , int MaxChannels = 8>
void dspark::Biquad< T, MaxChannels >::setCoeffs ( const BiquadCoeffs &  c)
inlinenoexcept

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
cNew coefficient set.

Definition at line 712 of file Biquad.h.

◆ setCoeffsNow()

template<typename T , int MaxChannels = 8>
void dspark::Biquad< T, MaxChannels >::setCoeffsNow ( const BiquadCoeffs &  c)
inlinenoexcept

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
cNew coefficient set, active from the next processed sample.

Definition at line 771 of file Biquad.h.


The documentation for this class was generated from the following file: