DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
DeEsser.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
37#include "../Core/AudioBuffer.h"
38#include "../Core/AudioSpec.h"
39#include "../Core/Biquad.h"
40#include "../Core/DspMath.h"
41#include "../Core/StateBlob.h"
42
43#include <algorithm>
44#include <array>
45#include <atomic>
46#include <cmath>
47#include <cstddef>
48#include <cstdint>
49#include <vector>
50
51namespace dspark {
52
63template <FloatType T>
65{
66public:
67 enum class DetectionMode
68 {
69 Bandpass,
71 };
72
81 void prepare(const AudioSpec& spec)
82 {
83 if (!spec.isValid()) return; // release-safe: keep previous state
84
85 sampleRate_ = spec.sampleRate;
86 numChannels_ = spec.numChannels;
87
88 updateEnvelopeCoeffs();
89 coefSmoothCoeff_ = static_cast<T>(1.0 - std::exp(-1.0 / (sampleRate_ * 0.001)));
90
91 reset();
92
93 filterParamsDirty_.store(true, std::memory_order_release);
94 updateFiltersIfDirty();
95 }
96
101 void processBlock(AudioBufferView<T> buffer) noexcept
102 {
103 int numCh = std::min(buffer.getNumChannels(), std::min(numChannels_, kMaxChannels));
104 int numSamples = buffer.getNumSamples();
105 if (numSamples == 0 || numCh == 0) return;
106
107 updateFiltersIfDirty();
108
109 if (envCoefsDirty_.exchange(false, std::memory_order_acquire))
110 updateEnvelopeCoeffs();
111
112 T thresh = threshold_.load(std::memory_order_relaxed);
113 T maxRed = maxReduction_.load(std::memory_order_relaxed);
114 auto mode = detectionMode_.load(std::memory_order_relaxed);
115
116 constexpr int kCoefRefreshInterval = 16;
117 const T coefSmooth = coefSmoothCoeff_;
118 T blockMaxGr = T(0);
119
120 // Branch hoisting: Process samples depending on the mode to avoid inner-loop conditionals.
121 if (mode == DetectionMode::Derivative)
122 {
123 processInternal<true>(buffer, numCh, numSamples, thresh, maxRed, coefSmooth, kCoefRefreshInterval, blockMaxGr);
124 }
125 else
126 {
127 processInternal<false>(buffer, numCh, numSamples, thresh, maxRed, coefSmooth, kCoefRefreshInterval, blockMaxGr);
128 }
129
130 gainReduction_.store(blockMaxGr, std::memory_order_relaxed);
131 }
132
134 void reset() noexcept
135 {
136 for (int ch = 0; ch < kMaxChannels; ++ch)
137 {
138 detector_[ch].reset();
139 reduction_[ch].reset();
140 smoothedGrDb_[ch] = T(0);
141 derivShift_[ch].fill(T(0));
142 }
143 envelope_ = T(0);
144 gainReduction_.store(T(0), std::memory_order_relaxed);
145 }
146
147 // Setters
148
150 void setAttack(T ms) noexcept
151 {
152 if (!std::isfinite(ms)) return;
153 attackMs_.store(std::clamp(ms, T(0.1), T(20)), std::memory_order_relaxed);
154 envCoefsDirty_.store(true, std::memory_order_release);
155 }
156
158 void setRelease(T ms) noexcept
159 {
160 if (!std::isfinite(ms)) return;
161 releaseMs_.store(std::clamp(ms, T(1), T(500)), std::memory_order_relaxed);
162 envCoefsDirty_.store(true, std::memory_order_release);
163 }
164
171 void setFrequency(T hz) noexcept
172 {
173 if (!std::isfinite(hz)) return;
174 frequency_.store(hz, std::memory_order_relaxed);
175 filterParamsDirty_.store(true, std::memory_order_release);
176 }
177
179 void setBandwidth(T octaves) noexcept
180 {
181 if (!std::isfinite(octaves)) return;
182 T bw = std::max(octaves, T(0.1));
183 T q = T(1) / (T(2) * std::sinh(T(0.34657359) * bw));
184 bandwidth_.store(q, std::memory_order_relaxed);
185 filterParamsDirty_.store(true, std::memory_order_release);
186 }
187
189 void setThreshold(T db) noexcept
190 {
191 if (!std::isfinite(db)) return;
192 threshold_.store(db, std::memory_order_relaxed);
193 }
194
196 void setReduction(T db) noexcept
197 {
198 if (!std::isfinite(db)) return;
199 maxReduction_.store(std::abs(db), std::memory_order_relaxed);
200 }
201
203 void setDetectionMode(DetectionMode mode) noexcept
204 {
205 const int v = std::clamp(static_cast<int>(mode),
206 static_cast<int>(DetectionMode::Bandpass),
207 static_cast<int>(DetectionMode::Derivative));
208 detectionMode_.store(static_cast<DetectionMode>(v), std::memory_order_relaxed);
209 }
210
211 // Getters
212 [[nodiscard]] T getGainReductionDb() const noexcept { return gainReduction_.load(std::memory_order_relaxed); }
213 [[nodiscard]] T getFrequency() const noexcept { return frequency_.load(std::memory_order_relaxed); }
214 [[nodiscard]] T getThreshold() const noexcept { return threshold_.load(std::memory_order_relaxed); }
215 [[nodiscard]] DetectionMode getDetectionMode() const noexcept { return detectionMode_.load(std::memory_order_relaxed); }
216 [[nodiscard]] T getAttack() const noexcept { return attackMs_.load(std::memory_order_relaxed); }
217 [[nodiscard]] T getRelease() const noexcept { return releaseMs_.load(std::memory_order_relaxed); }
218
220 [[nodiscard]] T getBandwidth() const noexcept
221 {
222 return static_cast<T>(std::asinh(1.0 / (2.0 * static_cast<double>(
223 bandwidth_.load(std::memory_order_relaxed)))) / 0.34657359);
224 }
225
227 [[nodiscard]] T getReduction() const noexcept { return maxReduction_.load(std::memory_order_relaxed); }
228
230 [[nodiscard]] std::vector<uint8_t> getState() const
231 {
232 // The blob stores float (setState reads float back); the explicit
233 // casts also keep this overload resolvable when T is double.
234 StateWriter w(stateId("DEES"), 1);
235 w.write("frequency", static_cast<float>(frequency_.load(std::memory_order_relaxed)));
236 // bandwidth_ stores Q = 1/(2 sinh(ln2/2 * oct)); serialize back in octaves.
237 w.write("bandwidth", static_cast<float>(getBandwidth()));
238 w.write("threshold", static_cast<float>(threshold_.load(std::memory_order_relaxed)));
239 w.write("reduction", static_cast<float>(maxReduction_.load(std::memory_order_relaxed)));
240 w.write("attack", static_cast<float>(attackMs_.load(std::memory_order_relaxed)));
241 w.write("release", static_cast<float>(releaseMs_.load(std::memory_order_relaxed)));
242 w.write("detection", static_cast<int32_t>(detectionMode_.load(std::memory_order_relaxed)));
243 return w.blob();
244 }
245
247 bool setState(const uint8_t* data, size_t size)
248 {
249 StateReader r(data, size);
250 if (!r.isValid() || r.processorId() != stateId("DEES")) return false;
251 setFrequency(static_cast<T>(r.read("frequency", 7000.0f)));
252 setBandwidth(static_cast<T>(r.read("bandwidth", 2.0f)));
253 setThreshold(static_cast<T>(r.read("threshold", -20.0f)));
254 setReduction(static_cast<T>(r.read("reduction", 12.0f)));
255 setAttack(static_cast<T>(r.read("attack", 0.5f)));
256 setRelease(static_cast<T>(r.read("release", 20.0f)));
257 setDetectionMode(static_cast<DetectionMode>(r.read("detection", 0)));
258 return true;
259 }
260
261private:
262 template <bool IsDerivative>
263 void processInternal(AudioBufferView<T>& buffer, int numCh, int numSamples,
264 T thresh, T maxRed, T coefSmooth, int refreshInterval, T& blockMaxGr) noexcept
265 {
266 for (int i = 0; i < numSamples; ++i)
267 {
268 T maxLevel = T(0);
269
270 // 1. Stereo-linked detection
271 for (int ch = 0; ch < numCh; ++ch)
272 {
273 T dataIn = buffer.getChannel(ch)[i];
274 T level = T(0);
275
276 if constexpr (IsDerivative)
277 {
278 auto& sr = derivShift_[ch];
279 for (int k = kDerivLen - 1; k > 0; --k) sr[k] = sr[k - 1];
280 sr[0] = detector_[ch].processSample(dataIn, 0);
281
282 T sumDiffs = T(0);
283 for (int k = 0; k < kDerivLen - 1; ++k)
284 {
285 sumDiffs += std::abs(sr[k] - sr[k + 1]);
286 }
287 level = sumDiffs / T(kDerivLen - 1);
288 }
289 else
290 {
291 level = std::abs(detector_[ch].processSample(dataIn, 0));
292 }
293
294 if (level > maxLevel) maxLevel = level;
295 }
296
297 // 2. Single linked envelope
298 T coeff = (maxLevel > envelope_) ? attackCoeff_ : releaseCoeff_;
299 envelope_ += coeff * (maxLevel - envelope_);
300
301 // 3. Compute GR
302 T envDb = gainToDecibels(envelope_ + T(1e-30));
303 T overDb = envDb - thresh;
304 T grDb = (overDb > T(0)) ? -std::min(overDb, maxRed) : T(0);
305
306 if (-grDb > blockMaxGr) blockMaxGr = -grDb;
307
308 // 4. Apply processing per channel
309 for (int ch = 0; ch < numCh; ++ch)
310 {
311 T& smoothedGr = smoothedGrDb_[ch];
312 smoothedGr += coefSmooth * (grDb - smoothedGr);
313
314 // Refresh coefficients without trig functions
315 if ((i & (refreshInterval - 1)) == 0)
316 {
317 updateDynamicPeakCoeffs(ch, smoothedGr);
318 }
319
320 T* channelData = buffer.getChannel(ch);
321 channelData[i] = reduction_[ch].processSample(channelData[i], 0);
322 }
323 }
324 }
325
330 void updateFiltersIfDirty() noexcept
331 {
332 if (!filterParamsDirty_.exchange(false, std::memory_order_acquire))
333 return;
334
335 T freq = frequency_.load(std::memory_order_relaxed);
336 T bw = bandwidth_.load(std::memory_order_relaxed);
337
338 // One shared effective frequency for BOTH filters. The bandpass
339 // factory clamps internally, but the precomputed peak terms did not:
340 // beyond Nyquist sin(w0) goes negative, alpha flips sign and the
341 // dynamic bell turns unstable under gain reduction.
342 const double fEff = std::clamp(static_cast<double>(freq), 1.0,
343 std::max(1.0, sampleRate_ * 0.499));
344
345 auto c = BiquadCoeffs::makeBandPass(sampleRate_, fEff, static_cast<double>(bw));
346 for (int ch = 0; ch < kMaxChannels; ++ch)
347 detector_[ch].setCoeffs(c);
348
349 // Precompute trig terms for the Peak filter to avoid math in the inner
350 // loop. Double, like the rest of the coefficient path: the design is
351 // only ever as good as the arithmetic that builds it.
352 double w0 = twoPi<double> * fEff / sampleRate_;
353 precomputedCos_ = std::cos(w0);
354 precomputedAlpha_ = std::sin(w0) / (2.0 * static_cast<double>(bw));
355 }
356
362 void updateDynamicPeakCoeffs(int ch, T gainDb) noexcept
363 {
364 // A = 10^(gainDb / 40)
365 const double A = std::pow(10.0, static_cast<double>(gainDb) / 40.0);
366
367 const double b0 = 1.0 + precomputedAlpha_ * A;
368 const double b1 = -2.0 * precomputedCos_;
369 const double b2 = 1.0 - precomputedAlpha_ * A;
370 const double a0 = 1.0 + precomputedAlpha_ / A;
371 const double a1 = -2.0 * precomputedCos_;
372 const double a2 = 1.0 - precomputedAlpha_ / A;
373
374 // Normalization
375 const double a0Inv = 1.0 / a0;
376
377 BiquadCoeffs coeffs;
378 coeffs.b0 = b0 * a0Inv;
379 coeffs.b1 = b1 * a0Inv;
380 coeffs.b2 = b2 * a0Inv;
381 coeffs.a1 = a1 * a0Inv;
382 coeffs.a2 = a2 * a0Inv;
383
384 reduction_[ch].setCoeffs(coeffs);
385 }
386
387 void updateEnvelopeCoeffs() noexcept
388 {
389 if (sampleRate_ <= 0.0) return;
390 const double atkSec = static_cast<double>(attackMs_.load(std::memory_order_relaxed)) * 0.001;
391 const double relSec = static_cast<double>(releaseMs_.load(std::memory_order_relaxed)) * 0.001;
392 attackCoeff_ = static_cast<T>(1.0 - std::exp(-1.0 / (sampleRate_ * std::max(atkSec, 1e-6))));
393 releaseCoeff_ = static_cast<T>(1.0 - std::exp(-1.0 / (sampleRate_ * std::max(relSec, 1e-6))));
394 }
395
396 static constexpr int kMaxChannels = 2;
397 static constexpr int kDerivLen = 8;
398
399 double sampleRate_ = 44100.0;
400 int numChannels_ = 0;
401
402 std::atomic<T> frequency_ { T(7000) };
403 std::atomic<T> bandwidth_ { T(2) };
404 std::atomic<T> threshold_ { T(-20) };
405 std::atomic<T> maxReduction_ { T(12) };
406 std::atomic<T> attackMs_ { T(0.5) };
407 std::atomic<T> releaseMs_ { T(20) };
408 std::atomic<DetectionMode> detectionMode_ { DetectionMode::Bandpass };
409
410 std::atomic<bool> envCoefsDirty_ { false };
411 std::atomic<bool> filterParamsDirty_ { true };
412
413 T attackCoeff_ = T(0);
414 T releaseCoeff_ = T(0);
415 T coefSmoothCoeff_ = T(0);
416 std::atomic<T> gainReduction_ { T(0) };
417
418 Biquad<T, 1> detector_[kMaxChannels]{};
419 Biquad<T, 1> reduction_[kMaxChannels]{};
420
421 T envelope_{T(0)}; // Stereo-linked envelope
422 T smoothedGrDb_[kMaxChannels]{};
423
424 std::array<std::array<T, kDerivLen>, kMaxChannels> derivShift_ {};
425
426 // Precomputed components for fast Biquad Peak modulation
427 double precomputedCos_ = 0.0;
428 double precomputedAlpha_ = 0.0;
429};
430
431} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
void setCoeffs(const BiquadCoeffs &c) noexcept
Sets the filter coefficients asynchronously.
Definition Biquad.h:599
void reset() noexcept
Resets all per-channel filter states to zero to avoid ringing/clicks.
Definition Biquad.h:668
T processSample(T input, int channel) noexcept
Processes a single sample for a specific channel.
Definition Biquad.h:694
Stereo-linked, CPU-optimized dynamic-EQ de-esser.
Definition DeEsser.h:65
T getAttack() const noexcept
Definition DeEsser.h:216
DetectionMode getDetectionMode() const noexcept
Definition DeEsser.h:215
T getReduction() const noexcept
Definition DeEsser.h:227
T getThreshold() const noexcept
Definition DeEsser.h:214
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
Definition DeEsser.h:247
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Definition DeEsser.h:230
void setThreshold(T db) noexcept
Sets the detection threshold in dBFS. Non-finite values are ignored.
Definition DeEsser.h:189
void setReduction(T db) noexcept
Sets the maximum gain reduction in dB (sign is ignored). Non-finite values are ignored.
Definition DeEsser.h:196
void setAttack(T ms) noexcept
Sets the detector attack, clamped to [0.1, 20] ms. Non-finite values are ignored.
Definition DeEsser.h:150
void setDetectionMode(DetectionMode mode) noexcept
Sets the detection mode. Out-of-range values are clamped so the getter stays honest.
Definition DeEsser.h:203
void prepare(const AudioSpec &spec)
Prepares the de-esser state and filters.
Definition DeEsser.h:81
void reset() noexcept
Resets internal DSP state.
Definition DeEsser.h:134
T getGainReductionDb() const noexcept
Definition DeEsser.h:212
void processBlock(AudioBufferView< T > buffer) noexcept
Processes audio in-place. Stereo-linked to preserve imaging.
Definition DeEsser.h:101
T getRelease() const noexcept
Definition DeEsser.h:217
void setRelease(T ms) noexcept
Sets the detector release, clamped to [1, 500] ms. Non-finite values are ignored.
Definition DeEsser.h:158
void setBandwidth(T octaves) noexcept
Sets the detection/cut bandwidth in octaves (floored at 0.1). Non-finite values are ignored.
Definition DeEsser.h:179
void setFrequency(T hz) noexcept
Sets the sibilance centre frequency in Hz.
Definition DeEsser.h:171
@ Bandpass
Standard bandpass filter detection.
@ Derivative
Sum of absolute derivatives. Sensitive to sustained high frequencies.
T getFrequency() const noexcept
Definition DeEsser.h:213
T getBandwidth() const noexcept
Definition DeEsser.h:220
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.
T gainToDecibels(T gain, T minusInfinityDb=T(-100)) noexcept
Converts a linear gain value to decibels.
Definition DspMath.h:78
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
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:67
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 makeBandPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Band-pass filter (constant 0 dB peak gain).
Definition Biquad.h:156