DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
Limiter.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
6#include "../Core/detail/BoxAverage.h"
7
55#include "../Core/DspMath.h"
56#include "../Core/AudioSpec.h"
57#include "../Core/AudioBuffer.h"
58#include "../Core/RingBuffer.h"
59#include "../Core/SmoothedValue.h"
60#include "../Core/DenormalGuard.h"
61#include "../Core/TruePeakDetector.h"
62#include "../Core/StateBlob.h"
63
64#include <algorithm>
65#include <atomic>
66#include <cmath>
67#include <cstddef>
68#include <cstdint>
69#include <vector>
70
71// Keeps the gain computer's rare paths (once per hold block, once per 2^16
72// frames) out of the per-sample loop, so the hot part inlines. Guarded the same
73// way as in Core/Biquad.h.
74#ifndef DSPARK_NOINLINE
75 #if defined(_MSC_VER)
76 #define DSPARK_NOINLINE __declspec(noinline)
77 #elif defined(__GNUC__) || defined(__clang__)
78 #define DSPARK_NOINLINE __attribute__((noinline))
79 #else
80 #define DSPARK_NOINLINE
81 #endif
82#endif
83
84namespace dspark {
85
92template <FloatType T>
94{
95public:
96 ~Limiter() = default; // non-virtual: leaf class (no virtual dispatch)
97
98 // -- Lifecycle --------------------------------------------------------------
99
116 void prepare(double sampleRate, int numChannels = 2, double initialLookaheadMs = -1.0)
117 {
118 if (!(sampleRate > 0.0)) return; // NaN-safe validity gate: keep previous state
119 prepared_ = false; // basic guarantee while (re)allocating
120
121 sampleRate_ = sampleRate;
122 // The true-peak detector state is a fixed kMaxChannels array; clamp so
123 // detection can never index it out of bounds for high channel counts.
124 numChannels_ = std::clamp(numChannels, 1, kMaxChannels);
125
126 // Caching inverse sample rate for fast math in hot paths
127 invSampleRate_ = T(1) / static_cast<T>(sampleRate_);
128
129 // Allocate for maximum possible lookahead to prevent RT-allocations
130 // later. The sample count is capped in double before the cast (a cast
131 // of an out-of-int-range double is undefined behaviour).
132 const double maxLaExact = sampleRate_ * kMaxLookaheadMs / 1000.0;
133 const int maxLookaheadSamples = static_cast<int>(std::min(maxLaExact, 1.0e7)) + 1;
134
135 // Use the clamped count: a degenerate numChannels (<= 0, or negative cast
136 // to size_t, or > kMaxChannels) must neither under-allocate (which would
137 // make processBlock index delayLines_ out of bounds) nor over-allocate.
138 delayLines_.resize(static_cast<size_t>(numChannels_));
139 for (auto& dl : delayLines_)
140 dl.prepare(maxLookaheadSamples * 2 + kChunk); // lookahead glide + one processBlock() chunk
141
142 // Gain-computer histories: power-of-two rings covering the longest
143 // hold window (L + 1) plus the moving-average lookback.
144 int hist = 4;
145 while (hist < maxLookaheadSamples + 4) hist <<= 1;
146 histSize_ = hist;
147 histMask_ = hist - 1;
148 eHist_.assign(static_cast<size_t>(hist), 1.0);
149 avgHist_.assign(static_cast<size_t>(hist), 1.0);
150 rHist_.assign(static_cast<size_t>(hist), 1.0);
151 sufMin_.assign(static_cast<size_t>(hist) + 1, 1.0);
152
153 if (std::isfinite(initialLookaheadMs) && initialLookaheadMs > 0.0)
154 lookaheadMs_.store(std::clamp(static_cast<T>(initialLookaheadMs),
155 T(0.5), static_cast<T>(kMaxLookaheadMs)),
156 std::memory_order_relaxed);
157 (void)lookaheadDirty_.exchange(false, std::memory_order_acquire);
159 lookaheadCurrent_ = static_cast<T>(lookaheadSamples_);
160
161 // Duration cap for the adaptive-release counter (2 seconds).
162 maxLimitSamples_ = static_cast<int>(std::min(sampleRate_ * 2.0, 1.0e9));
163
164 // Ceiling smoother setup
165 const T ceilDb = ceilingDb_.load(std::memory_order_relaxed);
166 lastCeilingDb_ = ceilDb;
167 const T ceilLinear = decibelsToGain(ceilDb);
168 ceilingSmooth_.prepare(sampleRate, 30.0);
169 ceilingSmooth_.reset(ceilLinear);
170
171 lastReleaseMs_ = std::max(releaseMs_.load(std::memory_order_relaxed), T(1));
173 lastHoldMs_ = holdMs_.load(std::memory_order_relaxed);
175
176 reset();
177 prepared_ = true;
178 }
179
187 void prepare(const AudioSpec& spec)
188 {
189 prepare(spec.sampleRate, spec.numChannels);
190 }
191
203 void processBlock(AudioBufferView<T> buffer) noexcept
204 {
205 if (!prepared_) return;
206 DenormalGuard guard;
207
208 const int nCh = std::min(buffer.getNumChannels(), numChannels_);
209 const int nS = buffer.getNumSamples();
210
211 if (lookaheadDirty_.exchange(false, std::memory_order_acquire))
214
215 const bool isp = truePeakEnabled_.load(std::memory_order_relaxed);
216 const bool adaptive = adaptiveRelease_.load(std::memory_order_relaxed);
217 const bool safetyClip = safetyClipEnabled_.load(std::memory_order_relaxed);
218 const T relMs = std::max(releaseMs_.load(std::memory_order_relaxed), T(1));
219 const T lookTarget = static_cast<T>(lookaheadSamples_);
220
221 T* chData[kMaxChannels] = {};
222 for (int ch = 0; ch < nCh; ++ch)
223 chData[ch] = buffer.getChannel(ch);
224
225 // Three passes per chunk: detection (the true-peak FIR, throughput
226 // bound), the serial gain computer (latency bound) and the gain
227 // application. Kept apart, the out-of-order core no longer stalls the
228 // FIR work behind the gain recursion. The per-sample arithmetic is
229 // unchanged, so processSample() stays bit-identical for mono.
230 for (int start = 0; start < nS; start += kChunk)
231 {
232 const int n = std::min(kChunk, nS - start);
233
234 // Pass 1: linked peak detection (loudest channel per frame).
235 std::fill(chunkPeak_, chunkPeak_ + n, T(0));
236 for (int ch = 0; ch < nCh; ++ch)
237 {
238 const T* x = chData[ch] + start;
239 for (int i = 0; i < n; ++i)
240 {
241 const T chPeak = isp ? truePeak_.processSample(x[i], ch) : std::abs(x[i]);
242 chunkPeak_[i] = std::max(chunkPeak_[i], chPeak); // NaN is ignored
243 }
244 }
245
246 // Pass 2: ceiling smoother, lookahead glide and gain computer.
247 for (int i = 0; i < n; ++i)
248 {
249 const T ceiling = ceilingSmooth_.getNextValue();
250 chunkCeiling_[i] = ceiling;
251
252 // Live-change smoothing of the lookahead read offset: at most
253 // one sample of change per sample, so a setLookahead() during
254 // playback glides (brief micro pitch-shift) instead of clicking.
255 if (lookaheadCurrent_ < lookTarget) lookaheadCurrent_ += T(1);
256 else if (lookaheadCurrent_ > lookTarget) lookaheadCurrent_ -= T(1);
257 chunkLook_[i] = static_cast<int>(lookaheadCurrent_);
258
259 advanceGain(chunkPeak_[i], ceiling, adaptive, relMs);
261 }
262
263 // Pass 3: delay, apply the gain, optional safety clip.
264 for (int ch = 0; ch < nCh; ++ch)
265 {
266 T* x = chData[ch] + start;
267 auto& line = delayLines_[static_cast<size_t>(ch)];
268 line.pushBlock(x, n); // frame i now sits (n - 1 - i) samples back
269 for (int i = 0; i < n; ++i)
270 {
271 const T ceiling = chunkCeiling_[i];
272 T out = line.read(chunkLook_[i] + (n - 1 - i)) * chunkGain_[i];
273
274 // Rounding backstop: the gain computer already meets the
275 // ceiling; the clamp only trims float rounding and the
276 // brief transition after a live ceiling or lookahead change.
277 out = std::clamp(out, -ceiling, ceiling);
278
279 if (safetyClip)
280 {
281 const T clipCeil = std::min(kSafetyClipCeiling, ceiling);
282 if (std::abs(out) > clipCeil)
283 out = applySafetyClipper(out, clipCeil);
284 }
285
286 x[i] = out;
287 }
288 }
289 }
290
291 // One relaxed store per block, outside the per-sample loop.
292 publishedGain_.store(currentGain_, std::memory_order_relaxed);
293 }
294
308 [[nodiscard]] T processSample(T input, int channel) noexcept
309 {
310 if (!prepared_) return input;
311 // Release-safe channel bound (delayLines_ is sized to numChannels_):
312 // out-of-range channels are an exact pass-through, no state touched.
313 if (channel < 0 || channel >= numChannels_) return input;
314
315 const bool isp = truePeakEnabled_.load(std::memory_order_relaxed);
316 const bool adaptive = adaptiveRelease_.load(std::memory_order_relaxed);
317 const bool safetyClip = safetyClipEnabled_.load(std::memory_order_relaxed);
318
319 if (channel == 0)
320 {
321 if (lookaheadDirty_.exchange(false, std::memory_order_acquire))
324 sampleCeiling_ = ceilingSmooth_.getNextValue();
325
326 const T lookTarget = static_cast<T>(lookaheadSamples_);
327 if (lookaheadCurrent_ < lookTarget) lookaheadCurrent_ += T(1);
328 else if (lookaheadCurrent_ > lookTarget) lookaheadCurrent_ -= T(1);
329 sampleLookNow_ = static_cast<int>(lookaheadCurrent_);
330 }
331 const T ceiling = sampleCeiling_;
332
333 delayLines_[channel].push(input);
334 const T chPeak = isp ? truePeak_.processSample(input, channel) : std::abs(input);
335
336 // The gain computer advances on channel 0. Peaks of the other channels
337 // reach it on the next frame (they are processed after channel 0), so
338 // they are one sample late: prefer processBlock() for linked
339 // multi-channel limiting. Mono is bit-identical to processBlock().
340 if (channel == 0)
341 {
342 const T relMs = std::max(releaseMs_.load(std::memory_order_relaxed), T(1));
343 advanceGain(std::max(chPeak, pendingPeak_), ceiling, adaptive, relMs);
344 pendingPeak_ = T(0);
345 publishedGain_.store(currentGain_, std::memory_order_relaxed);
346 }
347 else if (chPeak > pendingPeak_)
348 {
349 pendingPeak_ = chPeak;
350 }
351
352 T out = delayLines_[channel].read(sampleLookNow_) * currentGain_;
353 out = std::clamp(out, -ceiling, ceiling); // exact brickwall contract
354
355 if (safetyClip)
356 {
357 const T clipCeil = std::min(kSafetyClipCeiling, ceiling);
358 if (std::abs(out) > clipCeil)
359 out = applySafetyClipper(out, clipCeil);
360 }
361 return out;
362 }
363
365 void reset() noexcept
366 {
367 for (auto& dl : delayLines_) dl.reset();
369 currentGain_ = T(1);
370 publishedGain_.store(T(1), std::memory_order_relaxed);
372 pendingPeak_ = T(0);
374 lookaheadCurrent_ = static_cast<T>(lookaheadSamples_);
375 ceilingSmooth_.skip();
376 sampleCeiling_ = ceilingSmooth_.getCurrentValue();
378 }
379
380 // -- Level 1: Simple API ----------------------------------------------------
381
387 void setCeiling(T dB) noexcept
388 {
389 if (!std::isfinite(dB)) return;
390 ceilingDb_.store(dB, std::memory_order_relaxed);
391 }
392
393 // -- Level 2: Intermediate API ----------------------------------------------
394
400 void setRelease(T ms) noexcept
401 {
402 if (!std::isfinite(ms)) return;
403 releaseMs_.store(std::max(T(1), ms), std::memory_order_relaxed);
404 }
405
416 void setLookahead(T ms) noexcept
417 {
418 if (!std::isfinite(ms)) return;
419 lookaheadMs_.store(std::clamp(ms, T(0.5), static_cast<T>(kMaxLookaheadMs)),
420 std::memory_order_relaxed);
421 lookaheadDirty_.store(true, std::memory_order_release);
422 }
423
424 // -- Level 3: Expert API ----------------------------------------------------
425
429 void setTruePeak(bool enabled) noexcept
430 {
431 truePeakEnabled_.store(enabled, std::memory_order_relaxed);
432 lookaheadDirty_.store(true, std::memory_order_release);
433 }
434
436 void setAdaptiveRelease(bool enabled) noexcept { adaptiveRelease_.store(enabled, std::memory_order_relaxed); }
437
455 void setHold(T ms) noexcept
456 {
457 if (!std::isfinite(ms)) return;
458 holdMs_.store(std::clamp(ms, T(0), T(50)), std::memory_order_relaxed);
459 }
460
462 [[nodiscard]] T getHold() const noexcept { return holdMs_.load(std::memory_order_relaxed); }
463
471 void setSafetyClip(bool enabled) noexcept { safetyClipEnabled_.store(enabled, std::memory_order_relaxed); }
472
473 // Getters
474 [[nodiscard]] bool isTruePeakEnabled() const noexcept { return truePeakEnabled_.load(std::memory_order_relaxed); }
475 [[nodiscard]] bool isAdaptiveReleaseEnabled() const noexcept { return adaptiveRelease_.load(std::memory_order_relaxed); }
476 [[nodiscard]] bool isSafetyClipEnabled() const noexcept { return safetyClipEnabled_.load(std::memory_order_relaxed); }
477 [[nodiscard]] T getLookahead() const noexcept { return lookaheadMs_.load(std::memory_order_relaxed); }
478
486 [[nodiscard]] int getLatency() const noexcept
487 {
488 return lookaheadSamplesFor(lookaheadMs_.load(std::memory_order_relaxed));
489 }
490
498 [[nodiscard]] T getGainReductionDb() const noexcept
499 {
500 return gainToDecibels(publishedGain_.load(std::memory_order_relaxed));
501 }
502
503
505 [[nodiscard]] std::vector<uint8_t> getState() const
506 {
507 StateWriter w(stateId("LIMT"), 1);
508 w.write("ceiling", static_cast<float>(ceilingDb_.load(std::memory_order_relaxed)));
509 w.write("release", static_cast<float>(releaseMs_.load(std::memory_order_relaxed)));
510 w.write("lookahead", static_cast<float>(lookaheadMs_.load(std::memory_order_relaxed)));
511 w.write("truePeak", truePeakEnabled_.load(std::memory_order_relaxed));
512 w.write("adaptive", adaptiveRelease_.load(std::memory_order_relaxed));
513 w.write("safetyClip", safetyClipEnabled_.load(std::memory_order_relaxed));
514 w.write("hold", static_cast<float>(holdMs_.load(std::memory_order_relaxed)));
515 return w.blob();
516 }
517
519 bool setState(const uint8_t* data, size_t size)
520 {
521 StateReader r(data, size);
522 if (!r.isValid() || r.processorId() != stateId("LIMT")) return false;
523 setCeiling(static_cast<T>(r.read("ceiling", -0.3f)));
524 setRelease(static_cast<T>(r.read("release", 100.0f)));
525 setLookahead(static_cast<T>(r.read("lookahead", 2.0f)));
526 setTruePeak(r.read("truePeak", false));
527 setAdaptiveRelease(r.read("adaptive", false));
528 setSafetyClip(r.read("safetyClip", false));
529 // A blob written before the hold existed was rendered without one.
530 setHold(static_cast<T>(r.read("hold", 0.0f)));
531 return true;
532 }
533
534protected:
535 static constexpr int kMaxChannels = 16;
536 static constexpr double kMaxLookaheadMs = 10.0;
537 static constexpr T kSafetyClipCeiling = T(0.96605);
538
539 // Shared ITU-R BS.1770-4 true-peak detector (Core/TruePeakDetector.h).
541
545 [[nodiscard]] int lookaheadSamplesFor(T ms) const noexcept
546 {
547 const double clampedMs = std::clamp(static_cast<double>(ms), 0.5, kMaxLookaheadMs);
548 const double exact = sampleRate_ * clampedMs / 1000.0;
549 return std::max(1, static_cast<int>(std::min(exact, 1.0e7)));
550 }
551
553 inline void applyLookaheadTarget() noexcept
554 {
555 lookaheadSamples_ = lookaheadSamplesFor(lookaheadMs_.load(std::memory_order_relaxed));
557 }
558
559 // ---- Gain computer ------------------------------------------------------
560 //
561 // r[n] = min(1, ceiling / peak[n]) is the gain sample n requires. With a
562 // lookahead of L samples the audio sample x[p] leaves the delay line at
563 // p + L, and the applied gain there is the double moving average of the
564 // released envelope e over [p + L - span, p + L]. e never exceeds the
565 // sliding minimum h[n] = min(r[n - L .. n]), so every e in that window is
566 // at or below r[p] whenever span <= L: the ceiling holds by construction.
567 // In ISP mode a true-peak reading at n is formed from x[n - 11 .. n], so
568 // the span shrinks by 11 samples to cover the earliest of them.
569
572 void configureGainComputer() noexcept
573 {
574 if (histSize_ == 0) return;
575 const int L = std::clamp(lookaheadSamples_, 1, histSize_ - 3);
576 const int isp = truePeakEnabled_.load(std::memory_order_relaxed)
578 const int span = std::max(0, L - isp); // (a - 1) + (b - 1)
579 holdLen_ = L + 1;
580 boxA_ = span / 2 + 1;
581 boxB_ = span - (boxA_ - 1) + 1;
582 invBoxA_ = 1.0 / static_cast<double>(boxA_);
583 invBoxB_ = 1.0 / static_cast<double>(boxB_);
584
585 // Rebuild the running sums and the hold from the stored history
586 // (O(L), only on a configuration change), so the new windows are
587 // exact from the next frame on.
588 resumBoxes();
590 }
591
593 DSPARK_NOINLINE void resumBoxes() noexcept
594 {
595 sum1_ = 0.0;
596 for (int k = 0; k < boxA_; ++k)
597 sum1_ += eHist_[static_cast<size_t>((frame_ - 1 - k) & histMask_)];
598 sum2_ = 0.0;
599 for (int k = 0; k < boxB_; ++k)
600 sum2_ += avgHist_[static_cast<size_t>((frame_ - 1 - k) & histMask_)];
601 }
602
610 DSPARK_NOINLINE void restartHoldBlock(int64_t firstFrame) noexcept
611 {
612 double m = 1.0;
613 sufMin_[static_cast<size_t>(holdLen_)] = 1.0; // empty suffix
614 for (int j = holdLen_ - 1; j >= 0; --j)
615 {
616 m = std::min(m, rHist_[static_cast<size_t>((firstFrame + j) & histMask_)]);
617 sufMin_[static_cast<size_t>(j)] = m;
618 }
619 blockPos_ = 0;
620 prefixMin_ = 1.0;
621 }
622
624 void resetGainComputer() noexcept
625 {
626 std::fill(eHist_.begin(), eHist_.end(), 1.0);
627 std::fill(avgHist_.begin(), avgHist_.end(), 1.0);
628 std::fill(rHist_.begin(), rHist_.end(), 1.0);
629 std::fill(sufMin_.begin(), sufMin_.end(), 1.0);
630 blockPos_ = 0;
631 prefixMin_ = 1.0;
632 frame_ = 0;
633 envelope_ = 1.0;
634 holdLeft_ = 0;
635 sum1_ = static_cast<double>(boxA_);
636 sum2_ = static_cast<double>(boxB_);
637 sinceResum_ = 0;
638 currentGain_ = T(1);
639 }
640
642 inline void advanceGain(T peak, T ceiling, bool adaptive, T relMs) noexcept
643 {
644 // min() instead of a branch on peak > ceiling (data-dependent, so it
645 // mispredicts constantly under limiting); peak == 0 gives +inf -> 1.
646 const double required = std::min(1.0, static_cast<double>(ceiling / peak));
647
648 // Exact minimum of r over the last holdLen_ frames (see restartHoldBlock()).
649 rHist_[static_cast<size_t>(frame_ & histMask_)] = required;
650 prefixMin_ = std::min(prefixMin_, required);
651 const double held = std::min(sufMin_[static_cast<size_t>(blockPos_ + 1)], prefixMin_);
652 if (++blockPos_ == holdLen_)
654
655 // Release: the envelope follows reductions at once (the moving
656 // averages below shape the attack) and recovers with the one-pole.
657 // min() selects between the two without a data-dependent branch.
658 const bool attacking = held < envelope_;
659 double coeff = static_cast<double>(releaseCoeff_);
660 if (adaptive)
661 {
662 // Program-dependent release: up to 3x slower after sustained
663 // limiting. 1 / (1 + fs * tau) avoids std::exp per sample.
664 T baseFactor = T(1);
665 if (limitingDuration_ > 0)
666 {
667 const T durationMs = static_cast<T>(limitingDuration_) * T(1000) * invSampleRate_;
668 baseFactor = T(1) + std::min(durationMs / T(100), T(2));
669 }
670 coeff = 1.0 / (1.0 + sampleRate_ * static_cast<double>(relMs * baseFactor) / 1000.0);
671 }
672 // Hold: a frame that pushes the envelope down (or keeps it down)
673 // restarts the count; until it runs out the envelope does not rise.
674 if (held <= envelope_) holdLeft_ = holdSamples_;
675 else if (holdLeft_ > 0) { --holdLeft_; coeff = 0.0; }
676 envelope_ = std::min(held, envelope_ + coeff * (held - envelope_));
677 limitingDuration_ = attacking ? std::min(limitingDuration_ + 1, maxLimitSamples_)
678 : (envelope_ > 0.999 ? 0 : limitingDuration_);
679
680 // Two cascaded moving averages (lengths boxA_, boxB_): an S-shaped,
681 // click-free ramp spanning exactly the window the hold covers.
682 const size_t w = static_cast<size_t>(frame_ & histMask_);
683 eHist_[w] = envelope_;
684 const double avg1 = detail::advanceBoxAverage(envelope_,
685 eHist_[static_cast<size_t>((frame_ - boxA_) & histMask_)], sum1_, invBoxA_);
686 avgHist_[w] = avg1;
687 (void)detail::advanceBoxAverage(avg1,
688 avgHist_[static_cast<size_t>((frame_ - boxB_) & histMask_)], sum2_, invBoxB_);
689 ++frame_;
690
691 // Periodic exact re-summation keeps the running sums drift-free.
692 if (++sinceResum_ >= kResumPeriod)
693 {
694 sinceResum_ = 0;
695 resumBoxes();
696 }
697
698 currentGain_ = static_cast<T>(std::min(sum2_ * invBoxB_, 1.0));
699 }
700
701 // Fast-path synchronization for atomic variables. The linear ceiling is
702 // only recomputed when the dB parameter actually changed (skips the pow).
703 inline void syncParameters() noexcept
704 {
705 const T ceilDb = ceilingDb_.load(std::memory_order_relaxed);
706 if (ceilDb != lastCeilingDb_)
707 {
708 lastCeilingDb_ = ceilDb;
709 ceilingSmooth_.setTargetValue(decibelsToGain(ceilDb));
710 }
711
712 const T relMs = std::max(releaseMs_.load(std::memory_order_relaxed), T(1));
713 if (relMs != lastReleaseMs_)
714 {
715 lastReleaseMs_ = relMs;
717 }
718
719 const T holdMs = holdMs_.load(std::memory_order_relaxed);
720 if (holdMs != lastHoldMs_)
721 {
722 lastHoldMs_ = holdMs;
724 }
725 }
726
727 inline void updateHoldSamples() noexcept
728 {
729 holdSamples_ = static_cast<int>(std::lround(std::min(sampleRate_ * static_cast<double>(lastHoldMs_) / 1000.0, 1.0e8)));
730 }
731
732 inline void updateReleaseCoefficient() noexcept
733 {
734 if (sampleRate_ > 0)
735 releaseCoeff_ = T(1) - std::exp(T(-1) / (static_cast<T>(sampleRate_) * lastReleaseMs_ / T(1000)));
736 }
737
740 [[nodiscard]] inline T applySafetyClipper(T out, T clipCeil) const noexcept
741 {
742 T sign = (out >= T(0)) ? T(1) : T(-1);
743 T excess = std::abs(out) - clipCeil;
744 T blend = T(1) / (T(1) + excess * T(10));
745 out = sign * (clipCeil * blend + std::abs(out) * (T(1) - blend));
746
747 const T hardCeil = clipCeil * T(1.05);
748 if (std::abs(out) > hardCeil)
749 out = std::clamp(out, -hardCeil, hardCeil);
750
751 return out;
752 }
753
754 bool prepared_ = false;
755 double sampleRate_ = 48000.0;
756 T invSampleRate_ = T(1.0 / 48000.0);
759 int maxLimitSamples_ = 96000;
760
761 std::atomic<T> ceilingDb_ { T(-0.3) };
762 std::atomic<T> releaseMs_ { T(100) };
763 std::atomic<T> holdMs_ { T(10) };
764 std::atomic<T> lookaheadMs_ { T(2) };
765 std::atomic<bool> lookaheadDirty_ { false };
766 std::atomic<bool> truePeakEnabled_ { false };
767 std::atomic<bool> adaptiveRelease_ { false };
768 std::atomic<bool> safetyClipEnabled_ { false };
769
771 T lastCeilingDb_ = T(-0.3);
772
774 T lastReleaseMs_ = T(-1);
775 T lastHoldMs_ = T(-1);
776 int holdSamples_ = 0;
777 int holdLeft_ = 0;
778
779 T currentGain_ = T(1);
784 std::atomic<T> publishedGain_ { T(1) };
787
788 // Gain computer (see advanceGain()): sliding minimum, released envelope
789 // and the two moving averages, all in double (recursive state).
790 static constexpr int kResumPeriod = 1 << 16;
791 int histSize_ = 0;
792 int histMask_ = 0;
793 std::vector<double> eHist_;
794 std::vector<double> avgHist_;
795 std::vector<double> rHist_;
796 std::vector<double> sufMin_;
797 int blockPos_ = 0;
798 double prefixMin_ = 1.0;
799 int64_t frame_ = 0;
800 double envelope_ = 1.0;
801 double sum1_ = 1.0;
802 double sum2_ = 1.0;
803 int boxA_ = 1;
804 int boxB_ = 1;
805 double invBoxA_ = 1.0;
806 double invBoxB_ = 1.0;
807 int holdLen_ = 97;
808 int sinceResum_ = 0;
809 T pendingPeak_ = T(0);
810
811 // processBlock() chunk scratch (see its three passes).
812 static constexpr int kChunk = 128;
817
818 // Per-sample path caches (advanced on channel 0, reused by later channels).
819 T sampleCeiling_ = T(0.96605);
821
822 std::vector<RingBuffer<T>> delayLines_;
823};
824
825} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
High-performance brickwall lookahead limiter.
Definition Limiter.h:94
void setLookahead(T ms) noexcept
Sets the lookahead time dynamically.
Definition Limiter.h:416
void prepare(double sampleRate, int numChannels=2, double initialLookaheadMs=-1.0)
Allocates memory and prepares the limiter for processing.
Definition Limiter.h:116
void setRelease(T ms) noexcept
Sets the base release time.
Definition Limiter.h:400
std::vector< double > rHist_
Required-gain history.
Definition Limiter.h:795
bool isAdaptiveReleaseEnabled() const noexcept
Definition Limiter.h:475
double envelope_
Definition Limiter.h:800
std::atomic< T > lookaheadMs_
Definition Limiter.h:764
T processSample(T input, int channel) noexcept
Processes a single sample of one channel.
Definition Limiter.h:308
T getGainReductionDb() const noexcept
Current gain reduction in dB (metering).
Definition Limiter.h:498
T lookaheadCurrent_
Smoothed read offset (glides on live changes).
Definition Limiter.h:786
int64_t frame_
Definition Limiter.h:799
int chunkLook_[kChunk]
Definition Limiter.h:816
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
Definition Limiter.h:519
~Limiter()=default
void prepare(const AudioSpec &spec)
Prepares from AudioSpec (unified API).
Definition Limiter.h:187
int maxLimitSamples_
Adaptive-release duration cap (2 s).
Definition Limiter.h:759
void configureGainComputer() noexcept
Definition Limiter.h:572
void setSafetyClip(bool enabled) noexcept
Enables the post-limiter soft-knee safety clipper. RT-Safe.
Definition Limiter.h:471
void resetGainComputer() noexcept
Definition Limiter.h:624
std::atomic< bool > lookaheadDirty_
Definition Limiter.h:765
void advanceGain(T peak, T ceiling, bool adaptive, T relMs) noexcept
Definition Limiter.h:642
T chunkCeiling_[kChunk]
Definition Limiter.h:814
void syncParameters() noexcept
Definition Limiter.h:703
std::atomic< bool > truePeakEnabled_
Definition Limiter.h:766
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Definition Limiter.h:505
int getLatency() const noexcept
Reports the processing latency (the lookahead) in samples.
Definition Limiter.h:486
int lookaheadSamplesFor(T ms) const noexcept
Definition Limiter.h:545
int blockPos_
Position inside the current hold block.
Definition Limiter.h:797
T pendingPeak_
Other channels' peaks for the next processSample() frame.
Definition Limiter.h:809
std::atomic< bool > safetyClipEnabled_
Definition Limiter.h:768
void reset() noexcept
Resets the internal state (delays, gain reduction). RT-Safe.
Definition Limiter.h:365
void updateReleaseCoefficient() noexcept
Definition Limiter.h:732
SmoothedValue< T > ceilingSmooth_
Definition Limiter.h:770
double invBoxB_
Definition Limiter.h:806
int lookaheadSamples_
Audio-side lookahead (consumed from lookaheadMs_).
Definition Limiter.h:758
T applySafetyClipper(T out, T clipCeil) const noexcept
Definition Limiter.h:740
int holdLeft_
Hold samples left before the release may act.
Definition Limiter.h:777
std::vector< double > eHist_
Released envelope history.
Definition Limiter.h:793
void applyLookaheadTarget() noexcept
Definition Limiter.h:553
void setTruePeak(bool enabled) noexcept
Enables 4x oversampled ISP true-peak detection. RT-Safe. The attack ramp shortens by the estimator's ...
Definition Limiter.h:429
std::vector< double > sufMin_
Suffix minima of the last complete hold block (+1 empty).
Definition Limiter.h:796
bool isSafetyClipEnabled() const noexcept
Definition Limiter.h:476
int holdSamples_
Hold length in samples.
Definition Limiter.h:776
std::vector< RingBuffer< T > > delayLines_
Definition Limiter.h:822
std::atomic< T > releaseMs_
Definition Limiter.h:762
void setAdaptiveRelease(bool enabled) noexcept
Enables program-dependent adaptive release. RT-Safe.
Definition Limiter.h:436
static constexpr int kChunk
Definition Limiter.h:812
static constexpr int kResumPeriod
Definition Limiter.h:790
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an AudioBufferView in-place.
Definition Limiter.h:203
double prefixMin_
Running minimum of the current hold block.
Definition Limiter.h:798
DSPARK_NOINLINE void restartHoldBlock(int64_t firstFrame) noexcept
Definition Limiter.h:610
T getLookahead() const noexcept
Definition Limiter.h:477
void updateHoldSamples() noexcept
Definition Limiter.h:727
std::atomic< bool > adaptiveRelease_
Definition Limiter.h:767
bool isTruePeakEnabled() const noexcept
Definition Limiter.h:474
static constexpr T kSafetyClipCeiling
-0.3 dBFS
Definition Limiter.h:537
std::atomic< T > publishedGain_
Definition Limiter.h:784
std::atomic< T > holdMs_
Definition Limiter.h:763
static constexpr double kMaxLookaheadMs
Definition Limiter.h:536
void setHold(T ms) noexcept
Sets how long the reduction is held after the last peak that asked for it, before the release starts....
Definition Limiter.h:455
T getHold() const noexcept
Definition Limiter.h:462
static constexpr int kMaxChannels
Definition Limiter.h:535
std::atomic< T > ceilingDb_
Definition Limiter.h:761
void setCeiling(T dB) noexcept
Sets the absolute output ceiling.
Definition Limiter.h:387
DSPARK_NOINLINE void resumBoxes() noexcept
Definition Limiter.h:593
T chunkGain_[kChunk]
Definition Limiter.h:815
double sampleRate_
Definition Limiter.h:755
double invBoxA_
Definition Limiter.h:805
T chunkPeak_[kChunk]
Definition Limiter.h:813
std::vector< double > avgHist_
First moving-average output history.
Definition Limiter.h:794
T lastCeilingDb_
Change detector for the ceiling pow skip.
Definition Limiter.h:771
TruePeakDetector< T, kMaxChannels > truePeak_
Definition Limiter.h:540
int limitingDuration_
Definition Limiter.h:785
Zero-allocation parameter smoother for real-time audio.
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
Per-channel 4x-oversampled inter-sample peak estimator.
static constexpr int getTaps() noexcept
Input samples one estimate is computed from (taps per phase).
T processSample(T sample, int channel) noexcept
Feeds one sample and returns the local true-peak estimate.
void reset() noexcept
Clears all channel histories. Safe on the audio thread.
double advanceBoxAverage(double incoming, double outgoing, double &sum, double inverseLength) noexcept
Definition BoxAverage.h:12
Main namespace for the DSPark framework.
T decibelsToGain(T dB, T minusInfinityDb=T(-100)) noexcept
Converts a value in decibels to linear gain.
Definition DspMath.h:74
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
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