DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
Biquad.h
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
53#include <algorithm>
54#include <array>
55#include <cmath>
56#include <numbers>
57#include <span>
58#include <atomic>
59#include <cassert>
60
61#include "AudioBuffer.h"
62#include "DenormalGuard.h"
63
64// Keeps a cold path out of a hot caller's stack frame. Needed exactly once here
65// (see Biquad::adoptStagedCoeffs), where inlining the coefficient adoption into
66// the per-sample entry points costs four instructions on EVERY sample. Locally
67// defined and guarded, the same way Core/SimdOps.h spells DSPARK_RESTRICT.
68#ifndef DSPARK_NOINLINE
69 #if defined(_MSC_VER)
70 #define DSPARK_NOINLINE __declspec(noinline)
71 #elif defined(__GNUC__) || defined(__clang__)
72 #define DSPARK_NOINLINE __attribute__((noinline))
73 #else
74 #define DSPARK_NOINLINE
75 #endif
76#endif
77
78namespace dspark {
79
80// ============================================================================
81// BiquadCoeffs -- Coefficient storage + factory methods
82// ============================================================================
83
105struct alignas(32) BiquadCoeffs
106{
107 double b0 = 1.0, b1 = 0.0, b2 = 0.0;
108 double a1 = 0.0, a2 = 0.0;
109
110 // -- Factory methods (Audio EQ Cookbook) ----------------------------------
111
118 [[nodiscard]] static BiquadCoeffs makeLowPass(double sampleRate, double freq, double Q = 0.7071067811865476) noexcept
119 {
120 freq = std::clamp(freq, 1.0, std::max(1.0, sampleRate * 0.499));
121 Q = std::max(Q, 0.001);
122 const double w0 = 2.0 * std::numbers::pi * freq / sampleRate;
123 const double cosw0 = std::cos(w0);
124 const double sinw0 = std::sin(w0);
125 const double alpha = sinw0 / (2.0 * Q);
126
127 const double a0 = 1.0 + alpha;
128 return normalise(a0,
129 (1.0 - cosw0) / 2.0,
130 1.0 - cosw0,
131 (1.0 - cosw0) / 2.0,
132 -2.0 * cosw0,
133 1.0 - alpha);
134 }
135
142 [[nodiscard]] static BiquadCoeffs makeHighPass(double sampleRate, double freq, double Q = 0.7071067811865476) noexcept
143 {
144 freq = std::clamp(freq, 1.0, std::max(1.0, sampleRate * 0.499));
145 Q = std::max(Q, 0.001);
146 const double w0 = 2.0 * std::numbers::pi * freq / sampleRate;
147 const double cosw0 = std::cos(w0);
148 const double sinw0 = std::sin(w0);
149 const double alpha = sinw0 / (2.0 * Q);
150
151 const double a0 = 1.0 + alpha;
152 return normalise(a0,
153 (1.0 + cosw0) / 2.0,
154 -(1.0 + cosw0),
155 (1.0 + cosw0) / 2.0,
156 -2.0 * cosw0,
157 1.0 - alpha);
158 }
159
171 [[nodiscard]] static BiquadCoeffs makeBandPass(double sampleRate, double freq, double Q = 0.7071067811865476) noexcept
172 {
173 freq = std::clamp(freq, 1.0, std::max(1.0, sampleRate * 0.499));
174 Q = std::max(Q, 0.001);
175 const double w0 = 2.0 * std::numbers::pi * freq / sampleRate;
176 const double cosw0 = std::cos(w0);
177 const double sinw0 = std::sin(w0);
178 const double alpha = sinw0 / (2.0 * Q);
179
180 const double a0 = 1.0 + alpha;
181 return normalise(a0,
182 alpha,
183 0.0,
184 -alpha,
185 -2.0 * cosw0,
186 1.0 - alpha);
187 }
188
200 [[nodiscard]] static BiquadCoeffs makePeak(double sampleRate, double freq, double Q, double gainDb) noexcept
201 {
202 freq = std::clamp(freq, 1.0, std::max(1.0, sampleRate * 0.499));
203 Q = std::max(Q, 0.001);
204 const double A = std::pow(10.0, gainDb / 40.0); // dB/40 = sqrt of linear gain
205 const double w0 = 2.0 * std::numbers::pi * freq / sampleRate;
206 const double cosw0 = std::cos(w0);
207 const double sinw0 = std::sin(w0);
208 const double alpha = sinw0 / (2.0 * Q);
209
210 const double a0 = 1.0 + alpha / A;
211 return normalise(a0,
212 1.0 + alpha * A,
213 -2.0 * cosw0,
214 1.0 - alpha * A,
215 -2.0 * cosw0,
216 1.0 - alpha / A);
217 }
218
247 [[nodiscard]] static BiquadCoeffs makePeakMatched(double sampleRate, double freq,
248 double Q, double gainDb) noexcept
249 {
250 freq = std::clamp(freq, 1.0, std::max(1.0, sampleRate * 0.499));
251 Q = std::max(Q, 0.001);
252 if (std::abs(gainDb) < 0.01)
253 return { 1.0, 0.0, 0.0, 0.0, 0.0 };
254
255 const double G = std::pow(10.0, gainDb / 20.0);
256 const double w0 = 2.0 * std::numbers::pi * freq / sampleRate;
257
258 // Poles: impulse invariance of s^2 + 2*q*w0*s + w0^2, q = 1/(2*Qpole)
259 // with the prototype's pole Q = sqrt(G) * Q.
260 const double q = 1.0 / (2.0 * Q * std::sqrt(G));
261 double a1 = 0.0;
262 const double a2 = std::exp(-2.0 * q * w0);
263 if (q <= 1.0)
264 {
265 a1 = -2.0 * std::exp(-q * w0) * std::cos(std::sqrt(1.0 - q * q) * w0);
266 }
267 else
268 {
269 // Two real poles z = e^((-q +/- r) w0). Evaluated one by one (each
270 // is <= 1) rather than as e^(-q w0) * cosh(r w0), whose cosh
271 // overflows for small Q; -q + r is taken as -1/(q + r) to avoid
272 // the cancellation.
273 const double r = std::sqrt(q * q - 1.0);
274 a1 = -(std::exp(-w0 / (q + r)) + std::exp(-(q + r) * w0));
275 }
276
277 // Magnitude matching in the paper's (A, B, phi) form: |H|^2 =
278 // (B0 phi0 + B1 phi1 + B2 phi2) / (A0 phi0 + A1 phi1 + A2 phi2).
279 const double A0 = (1.0 + a1 + a2) * (1.0 + a1 + a2);
280 const double A1 = (1.0 - a1 + a2) * (1.0 - a1 + a2);
281 const double A2 = -4.0 * a2;
282 const double s = std::sin(0.5 * w0);
283 const double phi1 = s * s;
284 const double phi0 = 1.0 - phi1;
285 const double phi2 = 4.0 * phi0 * phi1;
286
287 const double G2 = G * G;
288 const double B0 = A0; // unity at DC
289 const double R1 = (A0 * phi0 + A1 * phi1 + A2 * phi2) * G2; // |H(w0)| = G
290 const double R2 = (-A0 + A1 + 4.0 * (phi0 - phi1) * A2) * G2; // extremum at w0
291 const double B2 = (R1 - R2 * phi1 - B0) / (4.0 * phi1 * phi1);
292 const double B1 = R2 + B0 + 4.0 * (phi1 - phi0) * B2;
293
294 // Minimum-phase numerator from (B0, B1, B2).
295 const double sqrtB0 = std::sqrt(std::max(0.0, B0));
296 const double sqrtB1 = std::sqrt(std::max(0.0, B1));
297 const double W = 0.5 * (sqrtB0 + sqrtB1);
298 BiquadCoeffs c;
299 c.b0 = 0.5 * (W + std::sqrt(std::max(0.0, W * W + B2)));
300 c.b1 = 0.5 * (sqrtB0 - sqrtB1);
301 c.b2 = -B2 / (4.0 * c.b0);
302 c.a1 = a1;
303 c.a2 = a2;
304 return c;
305 }
306
314 [[nodiscard]] static BiquadCoeffs makeLowShelf(double sampleRate, double freq, double gainDb, double slope = 1.0) noexcept
315 {
316 freq = std::clamp(freq, 1.0, std::max(1.0, sampleRate * 0.499));
317 // Shelf slope S must stay in (0, 1]: for S > 1 the radicand below goes
318 // negative and std::sqrt yields NaN coefficients that poison the audio.
319 slope = std::clamp(slope, 0.0001, 1.0);
320 const double A = std::pow(10.0, gainDb / 40.0);
321 const double w0 = 2.0 * std::numbers::pi * freq / sampleRate;
322 const double cosw0 = std::cos(w0);
323 const double sinw0 = std::sin(w0);
324 const double radicand = (A + 1.0 / A) * (1.0 / slope - 1.0) + 2.0;
325 const double alpha = sinw0 / 2.0 * std::sqrt(std::max(0.0, radicand));
326 const double twoSqrtAAlpha = 2.0 * std::sqrt(A) * alpha;
327
328 const double a0 = (A + 1.0) + (A - 1.0) * cosw0 + twoSqrtAAlpha;
329 return normalise(a0,
330 A * ((A + 1.0) - (A - 1.0) * cosw0 + twoSqrtAAlpha),
331 2.0 * A * ((A - 1.0) - (A + 1.0) * cosw0),
332 A * ((A + 1.0) - (A - 1.0) * cosw0 - twoSqrtAAlpha),
333 -2.0 * ((A - 1.0) + (A + 1.0) * cosw0),
334 (A + 1.0) + (A - 1.0) * cosw0 - twoSqrtAAlpha);
335 }
336
344 [[nodiscard]] static BiquadCoeffs makeHighShelf(double sampleRate, double freq, double gainDb, double slope = 1.0) noexcept
345 {
346 freq = std::clamp(freq, 1.0, std::max(1.0, sampleRate * 0.499));
347 // Shelf slope S must stay in (0, 1]: for S > 1 the radicand below goes
348 // negative and std::sqrt yields NaN coefficients that poison the audio.
349 slope = std::clamp(slope, 0.0001, 1.0);
350 const double A = std::pow(10.0, gainDb / 40.0);
351 const double w0 = 2.0 * std::numbers::pi * freq / sampleRate;
352 const double cosw0 = std::cos(w0);
353 const double sinw0 = std::sin(w0);
354 const double radicand = (A + 1.0 / A) * (1.0 / slope - 1.0) + 2.0;
355 const double alpha = sinw0 / 2.0 * std::sqrt(std::max(0.0, radicand));
356 const double twoSqrtAAlpha = 2.0 * std::sqrt(A) * alpha;
357
358 const double a0 = (A + 1.0) - (A - 1.0) * cosw0 + twoSqrtAAlpha;
359 return normalise(a0,
360 A * ((A + 1.0) + (A - 1.0) * cosw0 + twoSqrtAAlpha),
361 -2.0 * A * ((A - 1.0) + (A + 1.0) * cosw0),
362 A * ((A + 1.0) + (A - 1.0) * cosw0 - twoSqrtAAlpha),
363 2.0 * ((A - 1.0) - (A + 1.0) * cosw0),
364 (A + 1.0) - (A - 1.0) * cosw0 - twoSqrtAAlpha);
365 }
366
373 [[nodiscard]] static BiquadCoeffs makeNotch(double sampleRate, double freq, double Q = 0.7071067811865476) noexcept
374 {
375 freq = std::clamp(freq, 1.0, std::max(1.0, sampleRate * 0.499));
376 Q = std::max(Q, 0.001);
377 const double w0 = 2.0 * std::numbers::pi * freq / sampleRate;
378 const double cosw0 = std::cos(w0);
379 const double sinw0 = std::sin(w0);
380 const double alpha = sinw0 / (2.0 * Q);
381
382 const double a0 = 1.0 + alpha;
383 return normalise(a0,
384 1.0,
385 -2.0 * cosw0,
386 1.0,
387 -2.0 * cosw0,
388 1.0 - alpha);
389 }
390
397 [[nodiscard]] static BiquadCoeffs makeAllPass(double sampleRate, double freq, double Q = 0.7071067811865476) noexcept
398 {
399 freq = std::clamp(freq, 1.0, std::max(1.0, sampleRate * 0.499));
400 Q = std::max(Q, 0.001);
401 const double w0 = 2.0 * std::numbers::pi * freq / sampleRate;
402 const double cosw0 = std::cos(w0);
403 const double sinw0 = std::sin(w0);
404 const double alpha = sinw0 / (2.0 * Q);
405
406 const double a0 = 1.0 + alpha;
407 return normalise(a0,
408 1.0 - alpha,
409 -2.0 * cosw0,
410 1.0 + alpha,
411 -2.0 * cosw0,
412 1.0 - alpha);
413 }
414
415 // -- First-order filter factory methods ------------------------------------
416
426 [[nodiscard]] static BiquadCoeffs makeFirstOrderLowPass(double sampleRate, double frequency) noexcept
427 {
428 frequency = std::clamp(frequency, 1.0, std::max(1.0, sampleRate * 0.499));
429 const double w = std::tan(std::numbers::pi * frequency / sampleRate);
430 const double n = 1.0 / (1.0 + w);
431 BiquadCoeffs c;
432 c.b0 = w * n;
433 c.b1 = w * n;
434 c.b2 = 0.0;
435 c.a1 = (w - 1.0) * n;
436 c.a2 = 0.0;
437 return c;
438 }
439
449 [[nodiscard]] static BiquadCoeffs makeFirstOrderHighPass(double sampleRate, double frequency) noexcept
450 {
451 frequency = std::clamp(frequency, 1.0, std::max(1.0, sampleRate * 0.499));
452 const double w = std::tan(std::numbers::pi * frequency / sampleRate);
453 const double n = 1.0 / (1.0 + w);
454 BiquadCoeffs c;
455 c.b0 = n;
456 c.b1 = -n;
457 c.b2 = 0.0;
458 c.a1 = (w - 1.0) * n;
459 c.a2 = 0.0;
460 return c;
461 }
462
475 [[nodiscard]] static BiquadCoeffs makeTilt(double sampleRate, double pivotFreq, double gainDb) noexcept
476 {
477 pivotFreq = std::clamp(pivotFreq, 1.0, std::max(1.0, sampleRate * 0.499));
478 const double g = std::pow(10.0, gainDb / 20.0);
479 const double sqrtG = std::sqrt(g);
480 const double c = std::tan(std::numbers::pi * pivotFreq / sampleRate);
481
482 // First-order tilt shelf via bilinear transform:
483 // DC gain = 1/sqrt(g), pivot gain = 1, Nyquist gain = sqrt(g)
484 // Total swing = gainDb (half below pivot, half above).
485 const double norm = 1.0 / (1.0 + sqrtG * c);
486
487 BiquadCoeffs coeffs;
488 coeffs.b0 = (sqrtG + c) * norm;
489 coeffs.b1 = (c - sqrtG) * norm;
490 coeffs.b2 = 0.0;
491 coeffs.a1 = (sqrtG * c - 1.0) * norm;
492 coeffs.a2 = 0.0;
493 return coeffs;
494 }
495
504 [[nodiscard]] static BiquadCoeffs makeKWeightingShelf(double sampleRate) noexcept
505 {
506 const double G = 3.999843853973347; // dB
507 const double Q = 0.7071752369554196;
508 const double fc = 1681.9744509555319; // Hz
509 const double K = std::tan(std::numbers::pi * fc / sampleRate);
510 const double Vh = std::pow(10.0, G / 20.0);
511 const double Vb = std::pow(Vh, 0.4996667741545416);
512 const double a0 = 1.0 + K / Q + K * K;
513 BiquadCoeffs c;
514 c.b0 = (Vh + Vb * K / Q + K * K) / a0;
515 c.b1 = 2.0 * (K * K - Vh) / a0;
516 c.b2 = (Vh - Vb * K / Q + K * K) / a0;
517 c.a1 = 2.0 * (K * K - 1.0) / a0;
518 c.a2 = (1.0 - K / Q + K * K) / a0;
519 return c;
520 }
521
529 [[nodiscard]] static BiquadCoeffs makeKWeightingHighPass(double sampleRate) noexcept
530 {
531 const double Q = 0.5003270373238773;
532 const double fc = 38.13547087602444; // Hz
533 const double K = std::tan(std::numbers::pi * fc / sampleRate);
534 const double a0 = 1.0 + K / Q + K * K;
535 BiquadCoeffs c;
536 c.b0 = 1.0;
537 c.b1 = -2.0;
538 c.b2 = 1.0;
539 c.a1 = 2.0 * (K * K - 1.0) / a0;
540 c.a2 = (1.0 - K / Q + K * K) / a0;
541 return c;
542 }
543
544 // -- Frequency response analysis -------------------------------------------
545
558 [[nodiscard]] double getMagnitude(double frequency, double sampleRate) const noexcept
559 {
560 const double w = 2.0 * std::numbers::pi * frequency / sampleRate;
561 const double cosW = std::cos(w);
562 const double cos2W = std::cos(2.0 * w);
563 const double sinW = std::sin(w);
564 const double sin2W = std::sin(2.0 * w);
565
566 const double nRe = b0 + b1 * cosW + b2 * cos2W;
567 const double nIm = -b1 * sinW - b2 * sin2W;
568 const double dRe = 1.0 + a1 * cosW + a2 * cos2W;
569 const double dIm = -a1 * sinW - a2 * sin2W;
570
571 const double numMag2 = nRe * nRe + nIm * nIm;
572 const double denMag2 = dRe * dRe + dIm * dIm;
573
574 return (denMag2 > 1e-30) ? std::sqrt(numMag2 / denMag2) : 0.0;
575 }
576
589 template <typename U>
590 void getMagnitudeForFrequencyArray(std::span<const U> frequencies,
591 std::span<U> magnitudes,
592 double sampleRate) const noexcept
593 {
594 assert(magnitudes.size() >= frequencies.size());
595 for (size_t i = 0; i < frequencies.size(); ++i)
596 magnitudes[i] = static_cast<U>(getMagnitude(static_cast<double>(frequencies[i]), sampleRate));
597 }
598
599private:
604 [[nodiscard]] static BiquadCoeffs normalise(double a0, double b0r, double b1r, double b2r,
605 double a1r, double a2r) noexcept
606 {
607 const double invA0 = 1.0 / a0;
608 return { b0r * invA0, b1r * invA0, b2r * invA0, a1r * invA0, a2r * invA0 };
609 }
610};
611
612// ============================================================================
613// Biquad -- Filter processor with per-channel state
614// ============================================================================
615
659template <typename T, int MaxChannels = 8>
660class alignas(32) Biquad
661{
662public:
663 Biquad() noexcept = default;
664
665 // std::atomic<bool> makes the default copy/move ops vanish. Provide manual
666 // move semantics so this class can live inside std::array / std::vector
667 // (e.g. CrossoverFilter::AllPassChain). Copying is left deleted on
668 // purpose: it would imply two threads observing the same staged coefficient
669 // dirty flag, which is semantically ambiguous and not needed in practice.
670 // Moves only happen during setup (single-threaded relocation into a
671 // container), so the seqlock counter is simply reset to an even value.
672 Biquad(Biquad&& other) noexcept
673 : activeCoeffs_(other.activeCoeffs_),
674 coeffsDirty_(other.coeffsDirty_.load(std::memory_order_relaxed)),
675 coeffsSeq_(0),
676 state_(other.state_)
677 {
678 copyStagedRelaxed(other); // Word-wise: the staging words are atomics.
679 }
680
681 Biquad& operator=(Biquad&& other) noexcept
682 {
683 if (this == &other) return *this;
684 activeCoeffs_ = other.activeCoeffs_;
685 copyStagedRelaxed(other); // Word-wise: the staging words are atomics.
686 coeffsDirty_.store(other.coeffsDirty_.load(std::memory_order_relaxed),
687 std::memory_order_relaxed);
688 coeffsSeq_.store(0, std::memory_order_relaxed);
689 state_ = other.state_;
690 return *this;
691 }
692
693 Biquad(const Biquad&) = delete;
694 Biquad& operator=(const Biquad&) = delete;
695
712 void setCoeffs(const BiquadCoeffs& c) noexcept
713 {
714 // Seqlock publish (single producer). An odd sequence number marks a
715 // write in progress; the audio thread retries its read while odd or if
716 // the counter moved, so a concurrent update can never be observed as a
717 // torn coefficient set (mixing a1 of one filter with a2 of another).
718 //
719 // Data-race freedom: the staging slot is 5 std::atomic<double> words
720 // stored relaxed inside the critical section, so every cross-thread
721 // word access is defined by the C++ memory model (the previous plain
722 // struct copy was formal UB -- the same defect class CI TSan flagged in
723 // FIRFilter). The release fence below pairs with the
724 // reader's acquire fence ([atomics.fences]/2): a reader that observes
725 // any mid-publish word must also observe the odd counter and retry
726 // (Boehm, "Can Seqlocks Get Along With Programming Language Memory
727 // Models?", MSPC 2012). Control-thread only: zero audio-path cost.
728 coeffsSeq_.fetch_add(1, std::memory_order_acq_rel); // -> odd
729 std::atomic_thread_fence(std::memory_order_release);
730 stagedCoeffs_.b0.store(c.b0, std::memory_order_relaxed);
731 stagedCoeffs_.b1.store(c.b1, std::memory_order_relaxed);
732 stagedCoeffs_.b2.store(c.b2, std::memory_order_relaxed);
733 stagedCoeffs_.a1.store(c.a1, std::memory_order_relaxed);
734 stagedCoeffs_.a2.store(c.a2, std::memory_order_relaxed);
735 coeffsSeq_.fetch_add(1, std::memory_order_release); // -> even
736 coeffsDirty_.store(true, std::memory_order_release);
737 }
738
771 void setCoeffsNow(const BiquadCoeffs& c) noexcept { activeCoeffs_ = c; }
772
794 bool applyPendingCoeffs() noexcept
795 {
796 if (!coeffsDirty_.exchange(false, std::memory_order_acquire)) return false;
797 return adoptStagedCoeffs();
798 }
799
808 [[nodiscard]] const BiquadCoeffs& getCoeffs() const noexcept { return activeCoeffs_; }
809
811 void reset() noexcept
812 {
813 for (auto& s : state_)
814 s = {};
815 }
816
837 T processSample(T input, int channel) noexcept
838 {
839 return static_cast<T>(processSampleCore(static_cast<double>(input), channel));
840 }
841
857 double processSampleCore(double input, int channel) noexcept
858 {
859 assert(channel >= 0 && channel < MaxChannels && "Channel index out of bounds");
860
861 // Lock-free fast path: relaxed load is free on every modern CPU.
862 // Branch is marked unlikely so the compiler keeps the hot DSP path
863 // straight-line; the dirty flag is only true on the first sample
864 // of a block where setCoeffs() landed since last process call.
865 if (coeffsDirty_.load(std::memory_order_relaxed)) [[unlikely]]
867
868 auto& s = state_[channel];
869
870 const double output = activeCoeffs_.b0 * input + s.z1;
871 s.z1 = activeCoeffs_.b1 * input - activeCoeffs_.a1 * output + s.z2;
872 s.z2 = activeCoeffs_.b2 * input - activeCoeffs_.a2 * output;
873
874 return output;
875 }
876
893 void processBlock(AudioBufferView<T> buffer) noexcept
894 {
895 DenormalGuard guard;
896
897 // Pick up any pending lock-free coefficient update from the GUI thread.
899
900 const int numChannels = std::min(buffer.getNumChannels(), MaxChannels);
901 const int numSamples = buffer.getNumSamples();
902
903 // Block-local coefficient copy: stable for the whole block by design
904 // (updates land at the next block boundary), and register-friendly.
905 const BiquadCoeffs c = activeCoeffs_;
906
907 for (int ch = 0; ch < numChannels; ++ch)
908 {
909 T* data = buffer.getChannel(ch);
910 auto& s = state_[ch];
911 double z1 = s.z1;
912 double z2 = s.z2;
913
914 for (int i = 0; i < numSamples; ++i)
915 {
916 const double input = static_cast<double>(data[i]);
917 const double output = c.b0 * input + z1;
918 z1 = c.b1 * input - c.a1 * output + z2;
919 z2 = c.b2 * input - c.a2 * output;
920 data[i] = static_cast<T>(output);
921 }
922
923 s.z1 = z1;
924 s.z2 = z2;
925 }
926 }
927
928private:
934 static constexpr int kSeqlockMaxAttempts = 3;
935
964 DSPARK_NOINLINE bool adoptStagedCoeffs() noexcept
965 {
966 BiquadCoeffs tmp;
967 bool adopted = false;
968 for (int attempt = 0; attempt < kSeqlockMaxAttempts; ++attempt)
969 {
970 const unsigned s0 = coeffsSeq_.load(std::memory_order_acquire);
971 // Odd counter: the writer is mid-publish, so do not copy at all.
972 // Testing this BEFORE the copy makes a collision cost one relaxed
973 // load instead of a full word-set copy that would be thrown away,
974 // which is what keeps the bound cheap enough for the per-sample
975 // entry point.
976 if ((s0 & 1u) != 0u) continue;
977 // Relaxed word loads: each access is race-free because the staging
978 // words are std::atomic<double>. The destination is a stack
979 // temporary, NOT the live activeCoeffs_ the hot path reads, so a
980 // give-up leaves the active set untouched (commit-on-validate).
981 tmp.b0 = stagedCoeffs_.b0.load(std::memory_order_relaxed);
982 tmp.b1 = stagedCoeffs_.b1.load(std::memory_order_relaxed);
983 tmp.b2 = stagedCoeffs_.b2.load(std::memory_order_relaxed);
984 tmp.a1 = stagedCoeffs_.a1.load(std::memory_order_relaxed);
985 tmp.a2 = stagedCoeffs_.a2.load(std::memory_order_relaxed);
986 // The fence keeps the copy above from sinking below the re-read of
987 // the counter. A plain acquire load only orders LATER accesses;
988 // without the fence, both the compiler and weakly-ordered CPUs
989 // (ARM) may complete the copy after the counter is re-read, and a
990 // torn copy would pass the equality check. It also pairs with the
991 // writer's release fence, so a copy that read any mid-publish word
992 // cannot pass validation.
993 std::atomic_thread_fence(std::memory_order_acquire);
994 if (s0 == coeffsSeq_.load(std::memory_order_relaxed))
995 {
996 adopted = true;
997 break;
998 }
999 }
1000 if (!adopted)
1001 {
1002 // Adopt nothing; keep the set in use and pick the update up later.
1003 coeffsDirty_.store(true, std::memory_order_release);
1004 return false;
1005 }
1006 activeCoeffs_ = tmp;
1007 return true;
1008 }
1009
1010 // Kept compact on purpose (no over-alignment): the states are only ever
1011 // touched by the processing thread, sequentially per channel, so packing
1012 // adjacent channels into the same cache line is strictly better than
1013 // padding each one out to its own.
1014 struct State
1015 {
1016 double z1 = 0.0;
1017 double z2 = 0.0;
1018 };
1019
1020 // Shared staging slot for the seqlock publish: written word-by-word by the
1021 // control thread inside the odd/even critical section and read word-by-word
1022 // by the audio thread's retry loop. Every word is std::atomic<double>
1023 // (relaxed) so each cross-thread access is defined by the C++ memory model
1024 // (no non-atomic concurrent read/write, i.e. no data-race UB); the seq
1025 // counter plus the writer release / reader acquire fence pair order and
1026 // tear-protect the 5-word set. std::atomic<double> is lock-free and one
1027 // machine word on every supported target, so the publish stays lock- and
1028 // allocation-free. Defaults mirror BiquadCoeffs{} (identity filter).
1029 struct StagedCoeffs
1030 {
1031 std::atomic<double> b0{1.0}, b1{0.0}, b2{0.0};
1032 std::atomic<double> a1{0.0}, a2{0.0};
1033 };
1034
1036 void copyStagedRelaxed(const Biquad& other) noexcept
1037 {
1038 stagedCoeffs_.b0.store(other.stagedCoeffs_.b0.load(std::memory_order_relaxed),
1039 std::memory_order_relaxed);
1040 stagedCoeffs_.b1.store(other.stagedCoeffs_.b1.load(std::memory_order_relaxed),
1041 std::memory_order_relaxed);
1042 stagedCoeffs_.b2.store(other.stagedCoeffs_.b2.load(std::memory_order_relaxed),
1043 std::memory_order_relaxed);
1044 stagedCoeffs_.a1.store(other.stagedCoeffs_.a1.load(std::memory_order_relaxed),
1045 std::memory_order_relaxed);
1046 stagedCoeffs_.a2.store(other.stagedCoeffs_.a2.load(std::memory_order_relaxed),
1047 std::memory_order_relaxed);
1048 }
1049
1050 BiquadCoeffs activeCoeffs_ {};
1051 StagedCoeffs stagedCoeffs_ {};
1052 std::atomic<bool> coeffsDirty_{false};
1053 std::atomic<unsigned> coeffsSeq_{0};
1054
1055 std::array<State, MaxChannels> state_ {};
1056};
1057
1058} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Biquad filter using Transposed Direct Form II (TDF-II) with thread-safe updates.
Definition Biquad.h:661
Biquad(const Biquad &)=delete
void setCoeffs(const BiquadCoeffs &c) noexcept
Sets the filter coefficients asynchronously (control thread).
Definition Biquad.h:712
void setCoeffsNow(const BiquadCoeffs &c) noexcept
Stream-owner direct set: makes c the active set immediately.
Definition Biquad.h:771
void reset() noexcept
Resets all per-channel filter states to zero to avoid ringing/clicks.
Definition Biquad.h:811
const BiquadCoeffs & getCoeffs() const noexcept
Returns the active coefficient set currently in use by the DSP thread.
Definition Biquad.h:808
bool applyPendingCoeffs() noexcept
Promotes any pending staged coefficients to active.
Definition Biquad.h:794
Biquad & operator=(Biquad &&other) noexcept
Definition Biquad.h:681
void processBlock(AudioBufferView< T > buffer) noexcept
Processes a full audio buffer in-place.
Definition Biquad.h:893
Biquad & operator=(const Biquad &)=delete
T processSample(T input, int channel) noexcept
Processes a single sample for a specific channel.
Definition Biquad.h:837
double processSampleCore(double input, int channel) noexcept
One recursion step in the core's own precision (double).
Definition Biquad.h:857
Biquad() noexcept=default
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
Main namespace for the DSPark framework.
Stores normalised biquad coefficients (b0, b1, b2, a1, a2), always double.
Definition Biquad.h:106
static BiquadCoeffs makeFirstOrderHighPass(double sampleRate, double frequency) noexcept
First-order (6 dB/oct) high-pass filter.
Definition Biquad.h:449
static BiquadCoeffs makePeakMatched(double sampleRate, double freq, double Q, double gainDb) noexcept
Analog-matched ("de-cramped") peaking filter (Vicanek design).
Definition Biquad.h:247
void getMagnitudeForFrequencyArray(std::span< const U > frequencies, std::span< U > magnitudes, double sampleRate) const noexcept
Computes magnitude responses for a batch of frequencies.
Definition Biquad.h:590
static BiquadCoeffs makeHighPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
High-pass filter.
Definition Biquad.h:142
static BiquadCoeffs makeAllPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
All-pass filter.
Definition Biquad.h:397
static BiquadCoeffs makeBandPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Band-pass filter (constant 0 dB peak gain).
Definition Biquad.h:171
static BiquadCoeffs makePeak(double sampleRate, double freq, double Q, double gainDb) noexcept
Peak (parametric EQ) filter.
Definition Biquad.h:200
static BiquadCoeffs makeKWeightingHighPass(double sampleRate) noexcept
ITU-R BS.1770 K-weighting, stage 2: the RLB high-pass.
Definition Biquad.h:529
double getMagnitude(double frequency, double sampleRate) const noexcept
Evaluates magnitude response |H(f)| at a single frequency.
Definition Biquad.h:558
static BiquadCoeffs makeFirstOrderLowPass(double sampleRate, double frequency) noexcept
First-order (6 dB/oct) low-pass filter.
Definition Biquad.h:426
static BiquadCoeffs makeTilt(double sampleRate, double pivotFreq, double gainDb) noexcept
Creates a first-order tilt filter.
Definition Biquad.h:475
static BiquadCoeffs makeKWeightingShelf(double sampleRate) noexcept
ITU-R BS.1770 K-weighting, stage 1: the head-related high shelf.
Definition Biquad.h:504
static BiquadCoeffs makeLowPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Low-pass filter.
Definition Biquad.h:118
static BiquadCoeffs makeLowShelf(double sampleRate, double freq, double gainDb, double slope=1.0) noexcept
Low-shelf filter.
Definition Biquad.h:314
static BiquadCoeffs makeHighShelf(double sampleRate, double freq, double gainDb, double slope=1.0) noexcept
High-shelf filter.
Definition Biquad.h:344
static BiquadCoeffs makeNotch(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Notch (band-reject) filter.
Definition Biquad.h:373