DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
DCBlocker.h
Go to the documentation of this file.
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
49#include "../Core/DspMath.h"
50#include "../Core/Biquad.h"
51#include "../Core/AudioSpec.h"
52#include "../Core/AudioBuffer.h"
53#include "../Core/StateBlob.h"
54
55#include <algorithm>
56#include <array>
57#include <atomic>
58#include <cassert>
59#include <cmath>
60#include <cstddef>
61#include <cstdint>
62#include <numbers>
63#include <vector>
64
65namespace dspark {
66
85template <FloatType T>
87{
88public:
89 static constexpr int kMaxBiquadStages = 5;
90 static constexpr int kMaxChannels = 16;
91
94
95 ~DCBlocker() = default; // Non-virtual to avoid vtable injection
96
109 void prepare(double sampleRate, int numChannels = 2, double cutoffHz = -1.0)
110 {
111 if (!(sampleRate > 0.0)) return;
112 sampleRate_ = sampleRate;
113 numChannels_ = std::clamp(numChannels, 1, kMaxChannels);
114
115 reset();
116
117 // Use the thread-safe setter to clamp and initialize safely
118 if (cutoffHz > 0.0)
119 setCutoff(static_cast<T>(cutoffHz));
121 }
122
124 void prepare(const AudioSpec& spec)
125 {
126 prepare(spec.sampleRate, spec.numChannels);
127 }
128
144 void setOrder(int order) noexcept
145 {
146 order_.store(std::clamp(order, 1, 10), std::memory_order_relaxed);
147 }
148
150 [[nodiscard]] int getOrder() const noexcept
151 {
152 return order_.load(std::memory_order_relaxed);
153 }
154
166 void setCutoff(T hz) noexcept
167 {
168 if (!std::isfinite(hz)) return;
169 cutoffHz_.store(std::max(hz, T(1)), std::memory_order_relaxed);
170 }
171
173 [[nodiscard]] T getCutoff() const noexcept
174 {
175 return cutoffHz_.load(std::memory_order_relaxed);
176 }
177
186 void processBlock(AudioBufferView<T> buffer) noexcept
187 {
189
190 // lastOrder_ is the order the live coefficients were designed for.
191 // Reloading the atomic here could pick up a setOrder() that landed
192 // after the rebuild and enable stages that hold no coefficients yet.
193 const int currentOrder = lastOrder_;
194 const int nCh = std::min(buffer.getNumChannels(), numChannels_);
195 const int nS = buffer.getNumSamples();
196 if (nS <= 0) return;
197
198 if (currentOrder <= 1)
199 {
200 for (int ch = 0; ch < nCh; ++ch)
201 runOnePole(buffer.getChannel(ch), nS, ch);
202 }
203 else
204 {
205 const int numStages = std::clamp(currentOrder / 2, 1, kMaxBiquadStages);
206 for (int ch = 0; ch < nCh; ++ch)
207 runCascade(buffer.getChannel(ch), nS, ch, numStages);
208 }
209 }
210
220 void processBlock(int channel, T* data, int numSamples) noexcept
221 {
222 if (channel < 0 || channel >= kMaxChannels) return;
223 if (data == nullptr || numSamples <= 0) return;
225 const int currentOrder = lastOrder_;
226
227 if (currentOrder <= 1)
228 runOnePole(data, numSamples, channel);
229 else
230 runCascade(data, numSamples, channel,
231 std::clamp(currentOrder / 2, 1, kMaxBiquadStages));
232 }
233
252 [[nodiscard]] T processSample(int channel, T input) noexcept
253 {
254 if (channel < 0 || channel >= kMaxChannels) return input;
256 const auto ch = static_cast<size_t>(channel);
257 const double x = static_cast<double>(input);
258
259 if (lastOrder_ <= 1)
260 return static_cast<T>(stepOnePole(onePoleR_, onePole_[ch], x));
261
262 const int numStages = std::clamp(lastOrder_ / 2, 1, kMaxBiquadStages);
263 double v = x;
264 for (int s = 0; s < numStages; ++s)
265 {
266 auto& sec = sections_[static_cast<size_t>(s)];
267 v = stepSection(sec.b0, sec.a1, sec.a2, sec.state[ch], v);
268 }
269 return static_cast<T>(v);
270 }
271
275 void reset() noexcept
276 {
277 onePole_.fill(OnePoleState{});
278 for (auto& s : sections_)
279 s.state.fill(SectionState{});
280 }
281
282
284 [[nodiscard]] std::vector<uint8_t> getState() const
285 {
286 StateWriter w(stateId("DCBL"), 1);
287 w.write("order", order_.load(std::memory_order_relaxed));
288 w.write("cutoff", static_cast<float>(cutoffHz_.load(std::memory_order_relaxed)));
289 return w.blob();
290 }
291
293 bool setState(const uint8_t* data, size_t size)
294 {
295 StateReader r(data, size);
296 if (!r.isValid() || r.processorId() != stateId("DCBL")) return false;
297 setOrder(r.read("order", 1));
298 setCutoff(static_cast<T>(r.read("cutoff", 5.0f)));
299 return true;
300 }
301
302protected:
304 {
305 // The ORDER is part of the design, not just a stage count: each order
306 // has its own Q table row, and a larger order enables stages that may
307 // never have received coefficients. Rebuilding only on cutoff changes
308 // left a live setOrder() running a broken hybrid (old-Q first stage +
309 // identity stages) until the cutoff happened to move.
310 const T cutoff = cutoffHz_.load(std::memory_order_relaxed);
311 const int order = order_.load(std::memory_order_relaxed);
312 if (cutoff != lastCutoff_ || order != lastOrder_)
313 {
314 forceUpdateCoefficients(cutoff);
315 }
316 }
317
318 void forceUpdateCoefficients(T explicitCutoff = T(0)) noexcept
319 {
320 const T cutoff = explicitCutoff > T(0) ? explicitCutoff : cutoffHz_.load(std::memory_order_relaxed);
321 const double fc = static_cast<double>(cutoff);
322 // Loaded ONCE: reading order_ again below could pick up a concurrent
323 // setOrder() and record a design (lastOrder_) whose stages were never
324 // built, which the processing paths would then run as silent sections.
325 const int order = order_.load(std::memory_order_relaxed);
326
327 // 1-Pole update
328 onePoleR_ = std::exp(-std::numbers::pi * 2.0 * fc / sampleRate_);
329
330 // Biquad updates
331 static constexpr float qTable[6][kMaxBiquadStages] = {
332 {}, // index 0 (unused)
333 { 0.7071f }, // order 2
334 { 0.5412f, 1.3066f }, // order 4
335 { 0.5177f, 0.7071f, 1.9319f }, // order 6
336 { 0.5098f, 0.6013f, 0.8999f, 2.5628f }, // order 8
337 { 0.5062f, 0.5612f, 0.7071f, 1.1013f, 3.1962f } // order 10
338 };
339
340 const int tableIdx = std::clamp(order / 2, 1, 5);
341 for (int s = 0; s < tableIdx; ++s)
342 {
343 // Designed in double and used as b0 * (1 - z^-1)^2 / A(z): the RBJ
344 // high-pass numerator IS b0 * (1, -2, 1) - exactly so in binary
345 // floating point, since halving and doubling are exact and all
346 // three share the same 1/a0 factor - which is what lets the DC
347 // zero be applied structurally in the inner loop.
348 const auto c = BiquadCoeffs::makeHighPass(
349 sampleRate_, fc, static_cast<double>(qTable[tableIdx][s]));
350 assert(c.b1 == -2.0 * c.b0 && c.b2 == c.b0
351 && "RBJ high-pass numerator is no longer b0 * (1, -2, 1)");
352
353 auto& sec = sections_[static_cast<size_t>(s)];
354 sec.b0 = c.b0;
355 sec.a1 = c.a1;
356 sec.a2 = c.a2;
357 }
358
359 lastCutoff_ = cutoff;
360 lastOrder_ = order;
361 }
362
363 // -- Double-precision filter core -----------------------------------------
364
365 struct OnePoleState { double x1 = 0.0, y1 = 0.0; };
366 struct SectionState { double x1 = 0.0, x2 = 0.0, y1 = 0.0, y2 = 0.0; };
367
369 struct Section
370 {
371 double b0 = 0.0, a1 = 0.0, a2 = 0.0;
372 std::array<SectionState, kMaxChannels> state {};
373 };
374
376 static double stepOnePole(double r, OnePoleState& z, double x) noexcept
377 {
378 const double y = (x - z.x1) + r * z.y1;
379 z.x1 = x;
380 z.y1 = y;
381 return y;
382 }
383
385 static double stepSection(double b0, double a1, double a2, SectionState& z, double x) noexcept
386 {
387 // (x - x1) - (x1 - x2) is exactly zero for a constant input in IEEE
388 // arithmetic, so steady DC never reaches the recursion at all.
389 const double d = (x - z.x1) - (z.x1 - z.x2);
390 const double y = b0 * d - a1 * z.y1 - a2 * z.y2;
391 z.x2 = z.x1; z.x1 = x;
392 z.y2 = z.y1; z.y1 = y;
393 return y;
394 }
395
407 static constexpr double kDenormalFloor = 1e-30;
408
409 static void flushDenormals(OnePoleState& z) noexcept
410 {
411 if (std::abs(z.y1) + std::abs(z.x1) < kDenormalFloor)
412 z = OnePoleState{};
413 }
414
415 static void flushDenormals(SectionState& z) noexcept
416 {
417 if (std::abs(z.y1) + std::abs(z.y2) + std::abs(z.x1) + std::abs(z.x2) < kDenormalFloor)
418 z = SectionState{};
419 }
420
421 void runOnePole(T* data, int numSamples, int channel) noexcept
422 {
423 const double r = onePoleR_;
424 OnePoleState z = onePole_[static_cast<size_t>(channel)];
425 for (int i = 0; i < numSamples; ++i)
426 data[i] = static_cast<T>(stepOnePole(r, z, static_cast<double>(data[i])));
427 flushDenormals(z);
428 onePole_[static_cast<size_t>(channel)] = z;
429 }
430
431 void runCascade(T* data, int numSamples, int channel, int numStages) noexcept
432 {
433 // Coefficients and history are lifted into locals for the whole block:
434 // the intermediate signal then stays in double across the cascade and
435 // the compiler is free to keep the state in registers (writing to
436 // data[i] would otherwise be assumed to alias the member state).
437 double b0[kMaxBiquadStages] {}, a1[kMaxBiquadStages] {}, a2[kMaxBiquadStages] {};
438 SectionState z[kMaxBiquadStages] {};
439 const auto ch = static_cast<size_t>(channel);
440 for (int s = 0; s < numStages; ++s)
441 {
442 const auto& sec = sections_[static_cast<size_t>(s)];
443 b0[s] = sec.b0; a1[s] = sec.a1; a2[s] = sec.a2;
444 z[s] = sec.state[ch];
445 }
446
447 for (int i = 0; i < numSamples; ++i)
448 {
449 double v = static_cast<double>(data[i]);
450 for (int s = 0; s < numStages; ++s)
451 v = stepSection(b0[s], a1[s], a2[s], z[s], v);
452 data[i] = static_cast<T>(v);
453 }
454
455 for (int s = 0; s < numStages; ++s)
456 {
457 flushDenormals(z[s]);
458 sections_[static_cast<size_t>(s)].state[ch] = z[s];
459 }
460 }
461
462 double sampleRate_ = 48000.0;
463 int numChannels_ = 2;
464 double onePoleR_ = 0.0;
465
466 std::atomic<int> order_ { 1 };
467 std::atomic<T> cutoffHz_ { T(5) };
468 T lastCutoff_ = T(-1);
469 int lastOrder_ = -1;
470
471 // Fixed-size per-channel state (scalar loops: no SIMD alignment needed).
472 std::array<OnePoleState, kMaxChannels> onePole_ {};
473 std::array<Section, kMaxBiquadStages> sections_ {};
474};
475
476} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
DC blocking filter with configurable Butterworth order (1-10).
Definition DCBlocker.h:87
void prepare(double sampleRate, int numChannels=2, double cutoffHz=-1.0)
Prepares the DC blocker, resetting internal states and precalculating coefficients.
Definition DCBlocker.h:109
void setOrder(int order) noexcept
Sets the filter order (1-10). Thread-safe.
Definition DCBlocker.h:144
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Definition DCBlocker.h:284
std::array< OnePoleState, kMaxChannels > onePole_
Definition DCBlocker.h:472
std::atomic< T > cutoffHz_
Definition DCBlocker.h:467
void runOnePole(T *data, int numSamples, int channel) noexcept
Definition DCBlocker.h:421
static constexpr int kMaxBiquadStages
Definition DCBlocker.h:89
DCBlocker() noexcept
Builds a filter coherent with the documented defaults (5 Hz @ 48 kHz).
Definition DCBlocker.h:93
~DCBlocker()=default
T processSample(int channel, T input) noexcept
Processes a single sample for a given channel.
Definition DCBlocker.h:252
void forceUpdateCoefficients(T explicitCutoff=T(0)) noexcept
Definition DCBlocker.h:318
void setCutoff(T hz) noexcept
Requests a cutoff frequency update.
Definition DCBlocker.h:166
void reset() noexcept
Clears the internal history states to zero.
Definition DCBlocker.h:275
std::atomic< int > order_
Definition DCBlocker.h:466
static void flushDenormals(OnePoleState &z) noexcept
Definition DCBlocker.h:409
int getOrder() const noexcept
Returns the current filter order.
Definition DCBlocker.h:150
double onePoleR_
Derived from the documented defaults in the constructor.
Definition DCBlocker.h:464
void runCascade(T *data, int numSamples, int channel, int numStages) noexcept
Definition DCBlocker.h:431
void updateCoefficientsIfNeeded() noexcept
Definition DCBlocker.h:303
static void flushDenormals(SectionState &z) noexcept
Definition DCBlocker.h:415
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an AudioBufferView in-place.
Definition DCBlocker.h:186
void processBlock(int channel, T *data, int numSamples) noexcept
Processes a block of samples for one channel in-place.
Definition DCBlocker.h:220
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
Definition DCBlocker.h:293
static double stepSection(double b0, double a1, double a2, SectionState &z, double x) noexcept
Direct Form I with the numerator applied as a second difference.
Definition DCBlocker.h:385
std::array< Section, kMaxBiquadStages > sections_
Definition DCBlocker.h:473
void prepare(const AudioSpec &spec)
Prepares from AudioSpec (unified API); keeps the configured cutoff.
Definition DCBlocker.h:124
static double stepOnePole(double r, OnePoleState &z, double x) noexcept
y[n] = (x[n] - x[n-1]) + R*y[n-1]; the difference zeroes DC exactly.
Definition DCBlocker.h:376
static constexpr int kMaxChannels
Definition DCBlocker.h:90
T getCutoff() const noexcept
Returns the requested cutoff frequency in Hz.
Definition DCBlocker.h:173
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:160
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:203
bool isValid() const noexcept
Definition StateBlob.h:198
uint32_t processorId() const noexcept
Definition StateBlob.h:199
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:52
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:104
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:70
Main namespace for the DSPark framework.
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:639
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:56
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43
static BiquadCoeffs makeHighPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
High-pass filter.
Definition Biquad.h:127
Second-order section: b0 * (1 - z^-1)^2 / (1 + a1 z^-1 + a2 z^-2).
Definition DCBlocker.h:370