DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
DeEsser.h
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 // Missing key: the constructor default, Q = 2 (0.714 octaves). The
253 // fallback used to be 2 octaves, a different filter.
254 setBandwidth(static_cast<T>(r.read("bandwidth", 0.7140373f)));
255 setThreshold(static_cast<T>(r.read("threshold", -20.0f)));
256 setReduction(static_cast<T>(r.read("reduction", 12.0f)));
257 setAttack(static_cast<T>(r.read("attack", 0.5f)));
258 setRelease(static_cast<T>(r.read("release", 20.0f)));
259 setDetectionMode(static_cast<DetectionMode>(r.read("detection", 0)));
260 return true;
261 }
262
263private:
264 template <bool IsDerivative>
265 void processInternal(AudioBufferView<T>& buffer, int numCh, int numSamples,
266 T thresh, T maxRed, T coefSmooth, int refreshInterval, T& blockMaxGr) noexcept
267 {
268 for (int i = 0; i < numSamples; ++i)
269 {
270 T maxLevel = T(0);
271
272 // 1. Stereo-linked detection
273 for (int ch = 0; ch < numCh; ++ch)
274 {
275 T dataIn = buffer.getChannel(ch)[i];
276 T level = T(0);
277
278 if constexpr (IsDerivative)
279 {
280 auto& sr = derivShift_[ch];
281 for (int k = kDerivLen - 1; k > 0; --k) sr[k] = sr[k - 1];
282 sr[0] = detector_[ch].processSample(dataIn, 0);
283
284 T sumDiffs = T(0);
285 for (int k = 0; k < kDerivLen - 1; ++k)
286 {
287 sumDiffs += std::abs(sr[k] - sr[k + 1]);
288 }
289 level = sumDiffs / T(kDerivLen - 1);
290 }
291 else
292 {
293 level = std::abs(detector_[ch].processSample(dataIn, 0));
294 }
295
296 if (level > maxLevel) maxLevel = level;
297 }
298
299 // 2. Single linked envelope
300 T coeff = (maxLevel > envelope_) ? attackCoeff_ : releaseCoeff_;
301 envelope_ += coeff * (maxLevel - envelope_);
302
303 // 3. Compute GR
304 T envDb = gainToDecibels(envelope_ + T(1e-30));
305 T overDb = envDb - thresh;
306 T grDb = (overDb > T(0)) ? -std::min(overDb, maxRed) : T(0);
307
308 if (-grDb > blockMaxGr) blockMaxGr = -grDb;
309
310 // 4. Apply processing per channel
311 for (int ch = 0; ch < numCh; ++ch)
312 {
313 T& smoothedGr = smoothedGrDb_[ch];
314 smoothedGr += coefSmooth * (grDb - smoothedGr);
315
316 // Refresh coefficients without trig functions
317 if ((i & (refreshInterval - 1)) == 0)
318 {
319 updateDynamicPeakCoeffs(ch, smoothedGr);
320 }
321
322 T* channelData = buffer.getChannel(ch);
323 channelData[i] = reduction_[ch].processSample(channelData[i], 0);
324 }
325 }
326 }
327
332 void updateFiltersIfDirty() noexcept
333 {
334 if (!filterParamsDirty_.exchange(false, std::memory_order_acquire))
335 return;
336
337 T freq = frequency_.load(std::memory_order_relaxed);
338 T bw = bandwidth_.load(std::memory_order_relaxed);
339
340 // One shared effective frequency for BOTH filters. The bandpass
341 // factory clamps internally, but the precomputed peak terms did not:
342 // beyond Nyquist sin(w0) goes negative, alpha flips sign and the
343 // dynamic bell turns unstable under gain reduction.
344 const double fEff = std::clamp(static_cast<double>(freq), 1.0,
345 std::max(1.0, sampleRate_ * 0.499));
346
347 auto c = BiquadCoeffs::makeBandPass(sampleRate_, fEff, static_cast<double>(bw));
348 for (int ch = 0; ch < kMaxChannels; ++ch)
349 detector_[ch].setCoeffs(c);
350
351 // Precompute trig terms for the Peak filter to avoid math in the inner
352 // loop. Double, like the rest of the coefficient path: the design is
353 // only ever as good as the arithmetic that builds it.
354 double w0 = twoPi<double> * fEff / sampleRate_;
355 precomputedCos_ = std::cos(w0);
356 precomputedAlpha_ = std::sin(w0) / (2.0 * static_cast<double>(bw));
357 }
358
364 void updateDynamicPeakCoeffs(int ch, T gainDb) noexcept
365 {
366 // A = 10^(gainDb / 40)
367 const double A = std::pow(10.0, static_cast<double>(gainDb) / 40.0);
368
369 const double b0 = 1.0 + precomputedAlpha_ * A;
370 const double b1 = -2.0 * precomputedCos_;
371 const double b2 = 1.0 - precomputedAlpha_ * A;
372 const double a0 = 1.0 + precomputedAlpha_ / A;
373 const double a1 = -2.0 * precomputedCos_;
374 const double a2 = 1.0 - precomputedAlpha_ / A;
375
376 // Normalization
377 const double a0Inv = 1.0 / a0;
378
379 BiquadCoeffs coeffs;
380 coeffs.b0 = b0 * a0Inv;
381 coeffs.b1 = b1 * a0Inv;
382 coeffs.b2 = b2 * a0Inv;
383 coeffs.a1 = a1 * a0Inv;
384 coeffs.a2 = a2 * a0Inv;
385
386 reduction_[ch].setCoeffs(coeffs);
387 }
388
389 void updateEnvelopeCoeffs() noexcept
390 {
391 if (sampleRate_ <= 0.0) return;
392 const double atkSec = static_cast<double>(attackMs_.load(std::memory_order_relaxed)) * 0.001;
393 const double relSec = static_cast<double>(releaseMs_.load(std::memory_order_relaxed)) * 0.001;
394 attackCoeff_ = static_cast<T>(1.0 - std::exp(-1.0 / (sampleRate_ * std::max(atkSec, 1e-6))));
395 releaseCoeff_ = static_cast<T>(1.0 - std::exp(-1.0 / (sampleRate_ * std::max(relSec, 1e-6))));
396 }
397
398 static constexpr int kMaxChannels = 2;
399 static constexpr int kDerivLen = 8;
400
401 double sampleRate_ = 44100.0;
402 int numChannels_ = 0;
403
404 std::atomic<T> frequency_ { T(7000) };
405 std::atomic<T> bandwidth_ { T(2) };
406 std::atomic<T> threshold_ { T(-20) };
407 std::atomic<T> maxReduction_ { T(12) };
408 std::atomic<T> attackMs_ { T(0.5) };
409 std::atomic<T> releaseMs_ { T(20) };
410 std::atomic<DetectionMode> detectionMode_ { DetectionMode::Bandpass };
411
412 std::atomic<bool> envCoefsDirty_ { false };
413 std::atomic<bool> filterParamsDirty_ { true };
414
415 T attackCoeff_ = T(0);
416 T releaseCoeff_ = T(0);
417 T coefSmoothCoeff_ = T(0);
418 std::atomic<T> gainReduction_ { T(0) };
419
420 Biquad<T, 1> detector_[kMaxChannels]{};
421 Biquad<T, 1> reduction_[kMaxChannels]{};
422
423 T envelope_{T(0)}; // Stereo-linked envelope
424 T smoothedGrDb_[kMaxChannels]{};
425
426 std::array<std::array<T, kDerivLen>, kMaxChannels> derivShift_ {};
427
428 // Precomputed components for fast Biquad Peak modulation
429 double precomputedCos_ = 0.0;
430 double precomputedAlpha_ = 0.0;
431};
432
433} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
void setCoeffs(const BiquadCoeffs &c) noexcept
Sets the filter coefficients asynchronously (control thread).
Definition Biquad.h:712
void reset() noexcept
Resets all per-channel filter states to zero to avoid ringing/clicks.
Definition Biquad.h:811
T processSample(T input, int channel) noexcept
Processes a single sample for a specific channel.
Definition Biquad.h:837
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: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
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:89
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
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:71
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 makeBandPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Band-pass filter (constant 0 dB peak gain).
Definition Biquad.h:171