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