DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
Compressor.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
46#include "../Core/DspMath.h"
47#include "../Core/AudioSpec.h"
48#include "../Core/AudioBuffer.h"
49#include "../Core/SmoothedValue.h"
50#include "../Core/RingBuffer.h"
51#include "../Core/DenormalGuard.h"
52#include "../Core/Hilbert.h"
53#include "../Core/TruePeakDetector.h"
54#include "../Core/StateBlob.h"
55
56#include <algorithm>
57#include <array>
58#include <atomic>
59#include <cmath>
60#include <cstddef>
61#include <cstdint>
62#include <numbers>
63#include <vector>
64
65namespace dspark {
66
86template <FloatType T>
88{
89public:
90 ~Compressor() = default;
91
93 enum class DetectorType
94 {
95 Peak,
96 Rms,
97 TruePeak,
99 Hilbert
106 };
107
109 enum class Topology
110 {
112 FeedBack
118 };
119
126 enum class Character
127 {
128 Clean,
130
131 Opto,
139
140 FET,
150
151 Varimu
155 };
156
158 enum class Mode
159 {
160 Downward,
161 Upward
165 };
166
168 enum class AutoMakeupMode
169 {
170 Off,
171 Static,
174 Adaptive
177 };
178
179 // -- Lifecycle --------------------------------------------------------------
180
190 void prepare(const AudioSpec& spec)
191 {
192 if (!spec.isValid()) return;
193 spec_ = spec;
194 sampleRate_ = spec.sampleRate;
195
196 T fs = static_cast<T>(sampleRate_);
198 timeConstantsDirty_.store(false, std::memory_order_relaxed);
199
200 // Smoothed parameters initialization
201 T thresh = threshold_.load(std::memory_order_relaxed);
202 T rat = ratio_.load(std::memory_order_relaxed);
203 T knee = kneeWidth_.load(std::memory_order_relaxed);
204
205 thresholdSmooth_.prepare(sampleRate_, 30.0);
206 thresholdSmooth_.reset(thresh);
207 ratioSmooth_.prepare(sampleRate_, 30.0);
208 ratioSmooth_.reset(std::max(rat, T(1)));
209 kneeSmooth_.prepare(sampleRate_, 30.0);
210 kneeSmooth_.reset(std::max(knee, T(0)));
211 makeupSmooth_.prepare(sampleRate_, 30.0);
212 makeupSmooth_.reset(makeupGain_.load(std::memory_order_relaxed));
213 mixSmooth_.prepare(sampleRate_, 30.0);
214 mixSmooth_.reset(mix_.load(std::memory_order_relaxed));
215 colorSmooth_.prepare(sampleRate_, 30.0);
216 colorSmooth_.reset(characterColor_.load(std::memory_order_relaxed));
217
218 // Pre-allocate and initialize per-channel instances. Capacity covers
219 // the 10 ms user lookahead plus the Hilbert detector's group delay
220 // (the audio is delayed by it to stay aligned with the envelope).
221 // Every channel slot gets a delay line, not just spec.numChannels:
222 // a view wider than the spec would otherwise read an unprepared ring
223 // (silence) on the extra channels whenever lookahead is active.
224 int maxLaSamples = static_cast<int>(sampleRate_ * 0.01) + 1
226 for (int ch = 0; ch < kMaxChannels; ++ch)
227 {
228 lookaheadBuffers_[ch].prepare(maxLaSamples);
229 hilbertDetectors_[ch].prepare(sampleRate_);
230 }
231
232 lookaheadSamples_ = static_cast<int>(fs * std::clamp(
233 lookaheadMs_.load(std::memory_order_relaxed), T(0), T(10)) / T(1000));
234
236
237 // RMS Buffer pre-allocation (Max 500ms to ensure zero real-time allocation)
238 int maxRmsSamples = static_cast<int>(sampleRate_ * 0.5) + 1;
239 for (auto& buf : rmsBuffers_)
240 {
241 buf.assign(static_cast<size_t>(maxRmsSamples), T(0));
242 }
244
245 reset();
246 }
247
252 void prepare(double sampleRate) noexcept
253 {
254 AudioSpec spec { sampleRate, 512, 2 };
255 prepare(spec);
256 }
257
262 void processBlock(AudioBufferView<T> buffer) noexcept
263 {
264 processBlockImpl(buffer, buffer);
265 }
266
276 void processBlock(AudioBufferView<T> audio, AudioBufferView<T> sidechain) noexcept
277 {
278 processBlockImpl(audio, sidechain);
279 }
280
306 [[nodiscard]] T processSample(T input, int channel) noexcept
307 {
308 // Release-safe guards: a wild channel would index every per-channel
309 // state array out of bounds (a negative one is a giant size_t).
310 if (channel < 0 || channel >= kMaxChannels) return input;
311 if (!(sampleRate_ > 0)) return input;
312
313 // Sync atomic parameters into the smoothers here too: processBlock does this
314 // per block, but a processSample-only workflow would otherwise never see
315 // setThreshold()/setRatio()/setKnee() changes (the smoothers stayed frozen).
316 thresholdSmooth_.setTargetValue(threshold_.load(std::memory_order_relaxed));
317 ratioSmooth_.setTargetValue(std::max(ratio_.load(std::memory_order_relaxed), T(1)));
318 kneeSmooth_.setTargetValue(std::max(kneeWidth_.load(std::memory_order_relaxed), T(0)));
319 makeupSmooth_.setTargetValue(makeupGain_.load(std::memory_order_relaxed));
320 colorSmooth_.setTargetValue(characterColor_.load(std::memory_order_relaxed));
321
322 // Channel 0 advances the shared smoothers; advancing on every call
323 // would scale their time constants with the caller's channel count.
324 const bool advanceShared = (channel == 0);
325 T thresh, ratio, knee, makeupDb, colorAmt;
326 if (advanceShared)
327 {
328 thresh = thresholdSmooth_.getNextValue();
329 ratio = ratioSmooth_.getNextValue();
330 knee = kneeSmooth_.getNextValue();
331 makeupDb = makeupSmooth_.getNextValue();
332 colorAmt = colorSmooth_.getNextValue();
333 }
334 else
335 {
336 thresh = thresholdSmooth_.getCurrentValue();
337 ratio = ratioSmooth_.getCurrentValue();
338 knee = kneeSmooth_.getCurrentValue();
339 makeupDb = makeupSmooth_.getCurrentValue();
340 colorAmt = colorSmooth_.getCurrentValue();
341 }
342
343 auto detType = detectorType_.load(std::memory_order_relaxed);
344 auto topo = topology_.load(std::memory_order_relaxed);
345 auto charType = character_.load(std::memory_order_relaxed);
346 auto modeType = mode_.load(std::memory_order_relaxed);
347 bool scHpf = scHpfEnabled_.load(std::memory_order_relaxed);
348 auto autoMkup = autoMakeupMode_.load(std::memory_order_relaxed);
349
350 // Cheap relaxed pre-check keeps the per-sample cost to one load when
351 // nothing changed; the exchange claims the flag only when it is set.
352 if (timeConstantsDirty_.load(std::memory_order_relaxed)
353 && timeConstantsDirty_.exchange(false, std::memory_order_acquire))
354 {
355 updateTimeConstants(static_cast<T>(sampleRate_));
356 }
357 if (channel == 0
358 && rmsWindowDirty_.load(std::memory_order_relaxed)
359 && rmsWindowDirty_.exchange(false, std::memory_order_acquire))
360 {
361 rmsWindowMs_ = rmsWindowMsAtomic_.load(std::memory_order_relaxed);
363 }
364
365 // The FET character is a feedback design like the 1176 it models: it
366 // always detects on the compressed output with the hardware's peak
367 // rectifier, whatever Topology and Detector say.
368 const Topology topoEff = (charType == Character::FET) ? Topology::FeedBack : topo;
369 const DetectorType detTypeEff = (charType == Character::FET) ? DetectorType::Peak : detType;
370 const bool fbImplicit = topoEff == Topology::FeedBack
371 && modeType == Mode::Downward
372 && detTypeEff == DetectorType::Peak;
373
374 // The sidechain filter must process whatever the detector consumes:
375 // in FeedBack that is the compressed output (previous sample on the
376 // explicit path; on the semi-implicit path the loop equation supplies
377 // the gain, so the current input feeds the filter directly).
378 T detectorIn = (topoEff == Topology::FeedBack && !fbImplicit)
379 ? fbLastOutput_[channel] : input;
380 if (scHpf) detectorIn = applySidechainHPF(detectorIn, channel);
381
382 T levelDb = detectLevel(detectorIn, channel, detTypeEff);
383
384 // Sustained-level guard envelope for Upward mode: instant rise,
385 // constant 60 dB/s fall (peak hold with linear decay).
386 T guardDb = levelDb;
387 if (modeType == Mode::Upward)
388 {
389 T& g = upwardGuardDb_[channel];
390 g = std::max(levelDb, g - upwardGuardDecay_);
391 guardDb = g;
392 }
393
394 const bool splitAdaptive = detTypeEff == DetectorType::SplitPolarity;
395 T smoothedGR_Db;
396 if (fbImplicit)
397 {
398 smoothedGR_Db = applyBallisticsFeedbackImplicit(levelDb, channel, charType,
399 thresh, ratio, knee);
400 }
401 else if (topoEff == Topology::FeedBack && modeType == Mode::Downward)
402 {
403 const auto law = computeGainFeedback(levelDb, thresh, ratio, knee, charType);
404 const T targetGR_Db = applyHoldAndRange(law.target, channel);
405 smoothedGR_Db = applyBallistics(targetGR_Db, channel, splitAdaptive, law.slope);
406 }
407 else
408 {
409 T targetGR_Db = computeGain(levelDb, thresh, ratio, knee, charType, modeType, guardDb);
410 targetGR_Db = applyHoldAndRange(targetGR_Db, channel);
411 smoothedGR_Db = applyBallistics(targetGR_Db, channel, splitAdaptive);
412 }
413 T smoothedGainLinear = decibelsToGain(smoothedGR_Db);
414
415 if (advanceShared)
416 autoMakeupEnv_ = smoothedGR_Db + autoMakeupCoeff_ * (autoMakeupEnv_ - smoothedGR_Db);
417
418 T makeup = makeupDb;
419 if (modeType == Mode::Downward)
420 {
421 if (autoMkup == AutoMakeupMode::Adaptive)
422 makeup += -autoMakeupEnv_;
423 else if (autoMkup == AutoMakeupMode::Static)
424 makeup += computeGain(T(0), thresh, ratio, knee, charType,
425 Mode::Downward, T(0)) * T(-0.5);
426 }
427
428 T output = input * smoothedGainLinear;
429 if (colorAmt > T(0) && charType == Character::FET)
430 {
431 // Same 2nd-order FET channel modulation as the block path.
432 const T sq = output * output;
433 T& dc = fetDcState_[channel];
434 dc += fetDcCoeff_ * (sq - dc);
435 const T grDepth = std::clamp(-smoothedGR_Db * T(0.1), T(0), T(1));
436 output += colorAmt * kFetColorH2 * grDepth * (sq - dc);
437 }
438 output *= decibelsToGain(makeup);
439
440 fbLastOutput_[channel] = output;
441 gainReductionDb_.store(smoothedGR_Db, std::memory_order_relaxed);
442 return output;
443 }
444
448 void reset() noexcept
449 {
450 for (int ch = 0; ch < kMaxChannels; ++ch)
451 {
452 envFastDb_[ch] = T(0); // 0 dB gain change = neutral
453 envSlowDb_[ch] = T(0);
454 upwardGuardDb_[ch] = T(-200);
455 fbLastOutput_[ch] = T(0);
456 fetDcState_[ch] = T(0);
457 scHpfState_[ch] = T(0);
458 scHpfPrev_[ch] = T(0);
459 channelLevelDb_[ch] = T(-200);
460 lookaheadBuffers_[ch].reset();
461 hilbertDetectors_[ch].reset();
462 }
463 for (auto& buf : rmsBuffers_)
464 std::fill(buf.begin(), buf.end(), T(0));
465 for (auto& sum : rmsSums_) sum = T(0);
466 for (auto& idx : rmsIndices_) idx = 0;
467 for (auto& cnt : rmsRecomputeCounters_) cnt = 0;
468
470 holdCounters_.fill(0);
471 heldGrDb_.fill(T(0));
472 gainReductionDb_.store(T(0), std::memory_order_relaxed);
473 autoMakeupEnv_ = T(0);
474 splitPosEnv_.fill(T(0));
475 splitNegEnv_.fill(T(0));
476
477 thresholdSmooth_.skip();
478 ratioSmooth_.skip();
479 kneeSmooth_.skip();
480 makeupSmooth_.skip();
481 mixSmooth_.skip();
482 colorSmooth_.skip();
483 }
484
485 // =========================================================================
486 // Parameter Setters
487 // =========================================================================
488
489 // All numeric setters ignore non-finite values (NaN/Inf) and keep the
490 // previous parameter: a poisoned time constant, level or coefficient
491 // would otherwise contaminate the gain path permanently.
492
494 void setThreshold(T dB) noexcept
495 {
496 if (!std::isfinite(dB)) return;
497 threshold_.store(dB, std::memory_order_relaxed);
498 }
499
501 void setRatio(T ratio) noexcept
502 {
503 if (!std::isfinite(ratio)) return;
504 ratio_.store(std::max(ratio, T(1)), std::memory_order_relaxed);
505 // The feedback attack compensation depends on the loop gain (ratio).
506 timeConstantsDirty_.store(true, std::memory_order_release);
507 }
508
520 void setAttack(T ms) noexcept
521 {
522 if (!std::isfinite(ms)) return;
523 attackMs_.store(std::max(ms, T(0.01)), std::memory_order_relaxed);
524 timeConstantsDirty_.store(true, std::memory_order_release);
525 }
526
536 void setRelease(T ms) noexcept
537 {
538 if (!std::isfinite(ms)) return;
539 releaseMs_.store(std::max(ms, T(1)), std::memory_order_relaxed);
540 timeConstantsDirty_.store(true, std::memory_order_release);
541 }
542
544 void setKnee(T dB) noexcept
545 {
546 if (!std::isfinite(dB)) return;
547 kneeWidth_.store(std::max(dB, T(0)), std::memory_order_relaxed);
548 }
549
551 void setMakeupGain(T dB) noexcept
552 {
553 if (!std::isfinite(dB)) return;
554 makeupGain_.store(dB, std::memory_order_relaxed);
555 }
556
567 void setAutoMakeup(AutoMakeupMode mode) noexcept
568 {
570 std::memory_order_relaxed);
571 }
572
574 void setAutoMakeup(bool on) noexcept
575 {
577 }
578
580 void setMode(Mode mode) noexcept
581 {
582 mode_.store(clampEnum(mode, Mode::Upward), std::memory_order_relaxed);
583 // Feedback attack compensation only applies to Downward loops.
584 timeConstantsDirty_.store(true, std::memory_order_release);
585 }
586
588 void setStereoLink(T amount) noexcept
589 {
590 if (!std::isfinite(amount)) return;
591 stereoLink_.store(std::clamp(amount, T(0), T(1)), std::memory_order_relaxed);
592 }
593
595 void setMix(T dryWet) noexcept
596 {
597 if (!std::isfinite(dryWet)) return;
598 mix_.store(std::clamp(dryWet, T(0), T(1)), std::memory_order_relaxed);
599 }
600
605 void setLookahead(T ms) noexcept
606 {
607 if (!std::isfinite(ms)) return;
608 lookaheadMs_.store(std::clamp(ms, T(0), T(10)), std::memory_order_relaxed);
609 timeConstantsDirty_.store(true, std::memory_order_release);
610 }
611
613 void setDetector(DetectorType type) noexcept
614 {
615 // The shared TruePeakDetector builds its coefficients lazily on first
616 // use (thread-safe static), so this is a pure atomic publication.
617 detectorType_.store(clampEnum(type, DetectorType::Hilbert), std::memory_order_relaxed);
618 }
619
628 void setHoldTime(T ms) noexcept
629 {
630 if (!std::isfinite(ms)) return;
631 holdMs_.store(std::clamp(ms, T(0), T(500)), std::memory_order_relaxed);
632 timeConstantsDirty_.store(true, std::memory_order_release);
633 }
634
643 void setRange(T dB) noexcept
644 {
645 if (!std::isfinite(dB)) return;
646 rangeDb_.store(std::max(dB, T(0)), std::memory_order_relaxed);
647 }
648
650 void setTopology(Topology topo) noexcept
651 {
652 topology_.store(clampEnum(topo, Topology::FeedBack), std::memory_order_relaxed);
653 // Feedback loops re-derive the attack coefficient (loop speed-up).
654 timeConstantsDirty_.store(true, std::memory_order_release);
655 }
656
658 void setCharacter(Character type) noexcept
659 {
660 character_.store(clampEnum(type, Character::Varimu), std::memory_order_relaxed);
661 timeConstantsDirty_.store(true, std::memory_order_release);
662 }
663
678 void setCharacterColor(T amount) noexcept
679 {
680 if (!std::isfinite(amount)) return;
681 characterColor_.store(std::clamp(amount, T(0), T(1)), std::memory_order_relaxed);
682 }
683
685 [[nodiscard]] T getCharacterColor() const noexcept
686 {
687 return characterColor_.load(std::memory_order_relaxed);
688 }
689
700 void setSidechainHPF(bool enabled, T cutoffHz = T(80)) noexcept
701 {
702 scHpfEnabled_.store(enabled, std::memory_order_relaxed);
703 if (std::isfinite(cutoffHz) && cutoffHz > T(0))
704 scHpfFreq_.store(cutoffHz, std::memory_order_relaxed);
705 }
706
716 void setRmsWindow(T ms) noexcept
717 {
718 if (!std::isfinite(ms)) return;
719 rmsWindowMsAtomic_.store(std::clamp(ms, T(1), T(500)), std::memory_order_relaxed);
720 rmsWindowDirty_.store(true, std::memory_order_release);
721 }
722
723 // =========================================================================
724 // Metering & Getters
725 // =========================================================================
726
728 [[nodiscard]] T getGainReductionDb() const noexcept { return gainReductionDb_.load(std::memory_order_relaxed); }
729
731 [[nodiscard]] DetectorType getDetector() const noexcept { return detectorType_.load(std::memory_order_relaxed); }
732
734 [[nodiscard]] Topology getTopology() const noexcept { return topology_.load(std::memory_order_relaxed); }
735
737 [[nodiscard]] Character getCharacter() const noexcept { return character_.load(std::memory_order_relaxed); }
738
748 [[nodiscard]] int getLatency() const noexcept
749 {
750 const bool feedback =
751 topology_.load(std::memory_order_relaxed) == Topology::FeedBack
752 || character_.load(std::memory_order_relaxed) == Character::FET;
753 if (feedback) return 0;
754 const int hilbertComp =
755 (detectorType_.load(std::memory_order_relaxed) == DetectorType::Hilbert)
757 // Derive the lookahead from the published parameter rather than the
758 // audio-thread cache: hosts re-read the latency right after a setter,
759 // before the next block has consumed the coefficient-update flag.
760 const int lookNow = static_cast<int>(static_cast<T>(sampleRate_) * std::clamp(
761 lookaheadMs_.load(std::memory_order_relaxed), T(0), T(10)) / T(1000));
762 return lookNow + hilbertComp;
763 }
764
765
767 [[nodiscard]] std::vector<uint8_t> getState() const
768 {
769 StateWriter w(stateId("COMP"), 1);
770 // Explicit float casts: the blob stores float, and with T = double the
771 // unqualified write(key, double) would be ambiguous (float/int32/bool).
772 w.write("threshold", static_cast<float>(threshold_.load(std::memory_order_relaxed)));
773 w.write("ratio", static_cast<float>(ratio_.load(std::memory_order_relaxed)));
774 w.write("attack", static_cast<float>(attackMs_.load(std::memory_order_relaxed)));
775 w.write("release", static_cast<float>(releaseMs_.load(std::memory_order_relaxed)));
776 w.write("knee", static_cast<float>(kneeWidth_.load(std::memory_order_relaxed)));
777 w.write("makeup", static_cast<float>(makeupGain_.load(std::memory_order_relaxed)));
778 const auto amMode = autoMakeupMode_.load(std::memory_order_relaxed);
779 w.write("autoMakeup", amMode != AutoMakeupMode::Off); // legacy bool key
780 w.write("autoMakeupMode", static_cast<int32_t>(amMode));
781 w.write("stereoLink", static_cast<float>(stereoLink_.load(std::memory_order_relaxed)));
782 w.write("mix", static_cast<float>(mix_.load(std::memory_order_relaxed)));
783 w.write("lookahead", static_cast<float>(lookaheadMs_.load(std::memory_order_relaxed)));
784 w.write("hold", static_cast<float>(holdMs_.load(std::memory_order_relaxed)));
785 w.write("range", static_cast<float>(rangeDb_.load(std::memory_order_relaxed)));
786 w.write("detector", static_cast<int32_t>(detectorType_.load(std::memory_order_relaxed)));
787 w.write("topology", static_cast<int32_t>(topology_.load(std::memory_order_relaxed)));
788 w.write("character", static_cast<int32_t>(character_.load(std::memory_order_relaxed)));
789 w.write("characterColor", static_cast<float>(characterColor_.load(std::memory_order_relaxed)));
790 w.write("mode", static_cast<int32_t>(mode_.load(std::memory_order_relaxed)));
791 w.write("scHpf", scHpfEnabled_.load(std::memory_order_relaxed));
792 w.write("scHpfFreq", static_cast<float>(scHpfFreq_.load(std::memory_order_relaxed)));
793 w.write("rmsWindow", static_cast<float>(rmsWindowMsAtomic_.load(std::memory_order_relaxed)));
794 return w.blob();
795 }
796
798 bool setState(const uint8_t* data, size_t size)
799 {
800 StateReader r(data, size);
801 if (!r.isValid() || r.processorId() != stateId("COMP")) return false;
802 setThreshold(static_cast<T>(r.read("threshold", -20.0f)));
803 setRatio(static_cast<T>(r.read("ratio", 4.0f)));
804 setAttack(static_cast<T>(r.read("attack", 5.0f)));
805 setRelease(static_cast<T>(r.read("release", 100.0f)));
806 setKnee(static_cast<T>(r.read("knee", 0.0f)));
807 setMakeupGain(static_cast<T>(r.read("makeup", 0.0f)));
808 // The mode key wins; legacy blobs only carry the bool (true = Adaptive).
809 const int amLegacy = r.read("autoMakeup", false) ? 2 : 0;
810 setAutoMakeup(static_cast<AutoMakeupMode>(
811 std::clamp(r.read("autoMakeupMode", amLegacy), 0, 2)));
812 setStereoLink(static_cast<T>(r.read("stereoLink", 1.0f)));
813 setMix(static_cast<T>(r.read("mix", 1.0f)));
814 setLookahead(static_cast<T>(r.read("lookahead", 0.0f)));
815 setHoldTime(static_cast<T>(r.read("hold", 0.0f)));
816 setRange(static_cast<T>(r.read("range", 100.0f)));
817 setDetector(static_cast<DetectorType>(r.read("detector", 0)));
818 setTopology(static_cast<Topology>(r.read("topology", 0)));
819 setCharacter(static_cast<Character>(r.read("character", 0)));
820 setCharacterColor(static_cast<T>(r.read("characterColor", 0.0f)));
821 setMode(static_cast<Mode>(r.read("mode", 0)));
822 setSidechainHPF(r.read("scHpf", false), static_cast<T>(r.read("scHpfFreq", 80.0f)));
823 setRmsWindow(static_cast<T>(r.read("rmsWindow", 10.0f)));
824 return true;
825 }
826
827protected:
828 static constexpr int kMaxChannels = 16;
829
838 template <typename E>
839 [[nodiscard]] static E clampEnum(E value, E last) noexcept
840 {
841 const int v = std::clamp(static_cast<int>(value), 0, static_cast<int>(last));
842 return static_cast<E>(v);
843 }
844
856 {
857 if (!(sampleRate_ > 0)) return; // not prepared: leave the audio untouched
858 DenormalGuard guard;
859 const int nCh = std::min(audio.getNumChannels(), kMaxChannels);
860 const int nS = audio.getNumSamples();
861 // A short external sidechain would be read past its end; fall back
862 // to the internal key instead (release-safe, documented above).
863 const int scCh = (sidechain.getNumSamples() >= nS)
864 ? sidechain.getNumChannels() : 0;
865
866 // Sync atomic parameters to block-local smoothed state
867 thresholdSmooth_.setTargetValue(threshold_.load(std::memory_order_relaxed));
868 ratioSmooth_.setTargetValue(std::max(ratio_.load(std::memory_order_relaxed), T(1)));
869 kneeSmooth_.setTargetValue(std::max(kneeWidth_.load(std::memory_order_relaxed), T(0)));
870 makeupSmooth_.setTargetValue(makeupGain_.load(std::memory_order_relaxed));
871 mixSmooth_.setTargetValue(mix_.load(std::memory_order_relaxed));
872 colorSmooth_.setTargetValue(characterColor_.load(std::memory_order_relaxed));
873
874 if (timeConstantsDirty_.exchange(false, std::memory_order_acquire))
875 updateTimeConstants(static_cast<T>(sampleRate_));
877
878 // Apply a pending RMS window change here, on the audio thread.
879 if (rmsWindowDirty_.exchange(false, std::memory_order_acquire))
880 {
881 rmsWindowMs_ = rmsWindowMsAtomic_.load(std::memory_order_relaxed);
883 }
884
885 // Cache enum/bool params locally to prevent atomic stalls inside the tight DSP loop
886 auto detType = detectorType_.load(std::memory_order_relaxed);
887 auto topo = topology_.load(std::memory_order_relaxed);
888 auto charType = character_.load(std::memory_order_relaxed);
889 auto modeType = mode_.load(std::memory_order_relaxed);
890 bool scHpf = scHpfEnabled_.load(std::memory_order_relaxed);
891 auto autoMkup = autoMakeupMode_.load(std::memory_order_relaxed);
892 T sLink = stereoLink_.load(std::memory_order_relaxed);
893
894 // The FET character is a feedback design like the 1176 it models: it
895 // always detects on the compressed output with the hardware's peak
896 // rectifier, whatever Topology and Detector say.
897 const Topology topoEff = (charType == Character::FET) ? Topology::FeedBack : topo;
898 const DetectorType detTypeEff = (charType == Character::FET) ? DetectorType::Peak : detType;
899
900 // Downward feedback with the peak detector resolves the loop
901 // semi-implicitly (the level the loop will read is a known function
902 // of the gain): stable at any attack, and the observed static curve
903 // lands exactly on the requested ratio/knee. Detectors with memory
904 // (RMS/TruePeak/Hilbert/Split) keep the explicit one-sample loop.
905 const bool fbImplicit = topoEff == Topology::FeedBack
906 && modeType == Mode::Downward
907 && detTypeEff == DetectorType::Peak;
908
909 // The Hilbert detector reports the envelope kCenter samples late;
910 // delaying the audio by the same amount re-aligns gain and signal.
911 const int hilbertComp = (detTypeEff == DetectorType::Hilbert)
913 const bool splitAdaptive = detTypeEff == DetectorType::SplitPolarity;
914
915 // Lookahead (and the Hilbert alignment delay) break causal logic in
916 // Feedback mode, so both are strictly disabled there.
917 int activeLookahead = (topoEff == Topology::FeedBack) ? 0
918 : lookaheadSamples_ + hilbertComp;
919
920 for (int i = 0; i < nS; ++i)
921 {
922 T thresh = thresholdSmooth_.getNextValue();
923 T ratio = ratioSmooth_.getNextValue();
924 T knee = kneeSmooth_.getNextValue();
925 T mkupGain = makeupSmooth_.getNextValue();
926 T mixVal = mixSmooth_.getNextValue();
927 T colorAmt = colorSmooth_.getNextValue();
928 T linkedLevel = T(-200);
929
930 // Static auto makeup follows the smoothed curve parameters, so it
931 // stays click-free through threshold/ratio/knee automation.
932 if (autoMkup == AutoMakeupMode::Static && modeType == Mode::Downward)
933 mkupGain += computeGain(T(0), thresh, ratio, knee, charType,
934 Mode::Downward, T(0)) * T(-0.5);
935
936 // 1. Detection Path (Per-Channel)
937 for (int ch = 0; ch < nCh; ++ch)
938 {
939 // Fallback to internal channel if sidechain buffer lacks channels
940 T sample = (scCh > 0) ? sidechain.getChannel(std::min(ch, scCh - 1))[i]
941 : audio.getChannel(ch)[i];
942
943 // The sidechain filter must process whatever the detector
944 // consumes. In FeedBack that is the compressed output (the
945 // external key is ignored): the previous sample on the
946 // explicit path, or the channel's own current input on the
947 // semi-implicit path (the loop equation supplies the gain).
948 T detectorIn;
949 if (topoEff == Topology::FeedBack)
950 detectorIn = fbImplicit ? audio.getChannel(ch)[i] : fbLastOutput_[ch];
951 else
952 detectorIn = sample;
953 if (scHpf) detectorIn = applySidechainHPF(detectorIn, ch);
954
955 T levelDb = detectLevel(detectorIn, ch, detTypeEff);
956
957 channelLevelDb_[ch] = levelDb;
958 if (levelDb > linkedLevel) linkedLevel = levelDb;
959 }
960
961 // 2. Stereo Linking & Gain Application
962 T blockGR = T(0);
963 for (int ch = 0; ch < nCh; ++ch)
964 {
965 T chLevel = channelLevelDb_[ch];
966 T inputDb = chLevel + sLink * (linkedLevel - chLevel);
967
968 // Sustained-level guard envelope for Upward mode: instant
969 // rise, constant 60 dB/s fall (peak hold with linear decay).
970 T guardDb = inputDb;
971 if (modeType == Mode::Upward)
972 {
973 T& g = upwardGuardDb_[ch];
974 g = std::max(inputDb, g - upwardGuardDecay_);
975 guardDb = g;
976 }
977
978 // 3. Static curve + character ballistics (log domain)
979 T smoothedGR_Db;
980 if (fbImplicit)
981 {
982 smoothedGR_Db = applyBallisticsFeedbackImplicit(inputDb, ch, charType,
983 thresh, ratio, knee);
984 }
985 else if (topoEff == Topology::FeedBack && modeType == Mode::Downward)
986 {
987 // Explicit loop (detector with memory): calibrated element
988 // law plus a per-sample stability floor on the ballistics.
989 const auto law = computeGainFeedback(inputDb, thresh, ratio, knee, charType);
990 const T targetGR_Db = applyHoldAndRange(law.target, ch);
991 smoothedGR_Db = applyBallistics(targetGR_Db, ch, splitAdaptive, law.slope);
992 }
993 else
994 {
995 T targetGR_Db = computeGain(inputDb, thresh, ratio, knee, charType, modeType, guardDb);
996 targetGR_Db = applyHoldAndRange(targetGR_Db, ch);
997 smoothedGR_Db = applyBallistics(targetGR_Db, ch, splitAdaptive);
998 }
999 T smoothedGainLinear = decibelsToGain(smoothedGR_Db);
1000
1001 // 4. Makeup & Mix
1002 T makeup = mkupGain;
1003 if (autoMkup == AutoMakeupMode::Adaptive && modeType == Mode::Downward)
1004 makeup += -autoMakeupEnv_;
1005
1006 T input;
1007 if (activeLookahead > 0)
1008 {
1009 lookaheadBuffers_[ch].push(audio.getChannel(ch)[i]);
1010 input = lookaheadBuffers_[ch].read(activeLookahead);
1011 }
1012 else
1013 {
1014 input = audio.getChannel(ch)[i];
1015 }
1016
1017 T wet = input * smoothedGainLinear;
1018 if (colorAmt > T(0) && charType == Character::FET)
1019 {
1020 // 2nd-order FET channel modulation: the drain-source
1021 // resistance bends with the signal across it, so colour
1022 // only appears while the FET conducts (gain reduction
1023 // active); the squared term is AC-coupled like the
1024 // hardware's output transformer. Applied pre-makeup: the
1025 // line amp after the element is clean.
1026 const T sq = wet * wet;
1027 T& dc = fetDcState_[ch];
1028 dc += fetDcCoeff_ * (sq - dc);
1029 const T grDepth = std::clamp(-smoothedGR_Db * T(0.1), T(0), T(1));
1030 wet += colorAmt * kFetColorH2 * grDepth * (sq - dc);
1031 }
1032 wet *= decibelsToGain(makeup);
1033 fbLastOutput_[ch] = wet; // feedback detector reads the compressed signal
1034
1035 // Parallel (New York) mix done inline: the dry reference is `input`,
1036 // which carries the SAME lookahead delay as the wet, so dry and wet
1037 // stay phase-aligned (the previous DryWetMixer captured the UNDELAYED
1038 // input and comb-filtered when lookahead + mix<1 were combined).
1039 audio.getChannel(ch)[i] = (mixVal < T(1))
1040 ? (input * (T(1) - mixVal) + wet * mixVal)
1041 : wet;
1042
1043 if (ch == 0 || smoothedGR_Db < blockGR)
1044 blockGR = smoothedGR_Db; // Track worst-case GR for metering
1045 }
1046
1047 gainReductionDb_.store(blockGR, std::memory_order_relaxed);
1048
1049 // Auto-makeup envelope tracks slowly (~300ms)
1050 autoMakeupEnv_ = blockGR + autoMakeupCoeff_ * (autoMakeupEnv_ - blockGR);
1051 }
1052 }
1053
1058 void updateTimeConstants(T fs) noexcept
1059 {
1060 T attMs = std::max(attackMs_.load(std::memory_order_relaxed), T(0.01));
1061 T relMs = std::max(releaseMs_.load(std::memory_order_relaxed), T(1));
1062 autoMakeupCoeff_ = std::exp(T(-1) / (fs * T(0.3)));
1063
1064 // One-pole coefficient for a time constant given in milliseconds.
1065 auto tc = [fs](T ms) { return std::exp(T(-1) / (fs * ms / T(1000))); };
1066
1067 // Character ballistics: time constants of the dB-domain envelopes.
1068 // The default arm backs up the setter's enum clamp: falling through
1069 // with no case would leave the coefficients zero-initialized
1070 // (instant ballistics, i.e. a waveshaper).
1071 switch (character_.load(std::memory_order_relaxed))
1072 {
1073 default:
1074 case Character::Clean:
1075 case Character::Varimu:
1076 charAttCoeff_ = tc(attMs);
1077 charRelCoeff_ = tc(relMs);
1080 charFastWeight_ = T(1); // single envelope
1081 break;
1082
1083 case Character::Opto:
1084 // T4 cell: the release knob is the ~50% recovery time. With a
1085 // 0.6/0.4 fast/slow split, tau_fast = 0.61 x release puts the
1086 // half-recovery point on the knob value while the memory tail
1087 // decays ~35x slower, capped at the physical 5 s of the cell
1088 // (LA-2A spec: 50% in ~0.06 s, complete in 0.5-5 s). The slow
1089 // stage charges over ~4 release times (capped at 2 s), so only
1090 // sustained compression builds the long tail. The 10 ms attack
1091 // floor is the cell's published attack time.
1092 charAttCoeff_ = tc(std::max(attMs, T(10)));
1093 charRelCoeff_ = tc(relMs * T(0.61));
1094 charSlowRelCoeff_ = tc(std::min(relMs * T(25), T(5000)));
1095 charChargeCoeff_ = tc(std::clamp(relMs * T(4), T(100), T(2000)));
1096 charFastWeight_ = T(0.6);
1097 break;
1098
1099 case Character::FET:
1100 {
1101 // 1176: knob ranges are 20-800 us attack and 50-1100 ms
1102 // release. With a 0.75/0.25 split the compound release passes
1103 // ~t63 at the knob value, and a 150 ms history charge gives
1104 // the program-dependent tail.
1105 const T rel = std::clamp(relMs, T(50), T(1100));
1106 charAttCoeff_ = tc(std::clamp(attMs, T(0.02), T(0.8)));
1107 charRelCoeff_ = tc(rel * T(0.7));
1108 charSlowRelCoeff_ = tc(rel * T(2.5));
1109 charChargeCoeff_ = tc(T(150));
1110 charFastWeight_ = T(0.75);
1111 break;
1112 }
1113 }
1114
1115 // Downward feedback closes its loop around the attack stage, which
1116 // multiplies the raw ballistics speed by (1 + loop gain). Re-derive
1117 // the coefficient from rho = coeff/(1 + w beta A) so the OBSERVED
1118 // attack t63 stays on the knob, exactly like the hardware panels
1119 // (the 1176's 20-800 us figures are measured results, not RC values).
1120 // A is the linear-region loop gain (R - 1); Varimu uses its base
1121 // ratio (the progressive part is level-dependent).
1122 {
1123 const auto charNow = character_.load(std::memory_order_relaxed);
1124 const bool fbLoop = (charNow == Character::FET
1125 || topology_.load(std::memory_order_relaxed) == Topology::FeedBack)
1126 && mode_.load(std::memory_order_relaxed) == Mode::Downward;
1127 if (fbLoop)
1128 {
1129 const T loopGain = charFastWeight_
1130 * (std::max(ratio_.load(std::memory_order_relaxed), T(1)) - T(1));
1131 const T rho = charAttCoeff_;
1132 charAttCoeff_ = T(1) - (T(1) - rho) / (T(1) + rho * loopGain);
1133 }
1134 }
1135
1136 // SplitPolarity detector ballistics. The time constants reproduce the
1137 // original per-sample coefficients (0.6 attack / 0.99 release) at
1138 // 44.1 kHz, but are now sample-rate invariant.
1139 splitDetAttCoeff_ = std::exp(T(-1) / (fs * T(44.39e-6)));
1140 splitDetRelCoeff_ = std::exp(T(-1) / (fs * T(2.2562e-3)));
1141
1142 // Upward silence-guard envelope: 60 dB/s linear decay.
1143 upwardGuardDecay_ = T(60) / fs;
1144
1145 // AC coupling (~10 Hz) of the FET color's 2nd-order term.
1146 fetDcCoeff_ = T(1) - std::exp(T(-2) * std::numbers::pi_v<T> * T(10) / fs);
1147
1148 lookaheadSamples_ = static_cast<int>(fs * std::clamp(
1149 lookaheadMs_.load(std::memory_order_relaxed), T(0), T(10)) / T(1000));
1150
1151 holdSamples_ = static_cast<int>(fs * holdMs_.load(std::memory_order_relaxed) / T(1000));
1152 }
1153
1160 [[nodiscard]] T applyHoldAndRange(T targetGR_Db, int ch) noexcept
1161 {
1162 const T range = rangeDb_.load(std::memory_order_relaxed);
1163 targetGR_Db = std::clamp(targetGR_Db, -range, range);
1164
1165 if (holdSamples_ > 0)
1166 {
1167 T& held = heldGrDb_[ch];
1168 int& counter = holdCounters_[ch];
1169 if (std::abs(targetGR_Db) >= std::abs(held))
1170 {
1171 held = targetGR_Db; // deeper action: re-arm the hold
1172 counter = holdSamples_;
1173 }
1174 else if (counter > 0)
1175 {
1176 --counter; // shallower: freeze at held depth
1177 targetGR_Db = held;
1178 }
1179 else
1180 {
1181 held = targetGR_Db; // hold elapsed: track normally
1182 }
1183 }
1184 return targetGR_Db;
1185 }
1186
1187 // ---- Detectors ----
1188
1196 [[nodiscard]] T detectLevel(T sample, int ch, DetectorType detType) noexcept
1197 {
1198 T level = std::abs(sample);
1199 switch (detType)
1200 {
1201 case DetectorType::Peak:
1202 break; // level already holds abs(sample)
1203
1204 case DetectorType::Rms:
1205 {
1206 T sq = sample * sample;
1207 auto& buf = rmsBuffers_[ch];
1208 auto& sum = rmsSums_[ch];
1209 auto& idx = rmsIndices_[ch];
1210 auto& recomputeCount = rmsRecomputeCounters_[ch];
1211 int len = rmsWindowSamples_;
1212
1213 if (len > 0 && len <= static_cast<int>(buf.size()))
1214 {
1215 sum -= buf[idx];
1216 buf[idx] = sq;
1217 sum += sq;
1218 if (++idx >= len) idx = 0; // branch beats an integer division per sample
1219
1220 // Periodic full re-summation to prevent floating-point drift
1221 if (++recomputeCount >= kRmsRecomputePeriod)
1222 {
1223 sum = T(0);
1224 for (int j = 0; j < len; ++j) sum += buf[j];
1225 recomputeCount = 0;
1226 }
1227
1228 level = std::sqrt(std::max(sum / static_cast<T>(len), T(0)));
1229 }
1230 break;
1231 }
1232
1234 level = truePeak_.processSample(sample, ch);
1235 break;
1236
1238 {
1239 T pos = std::max(sample, T(0));
1240 T neg = std::max(-sample, T(0));
1241
1242 T posCoeff = (pos > splitPosEnv_[ch]) ? splitDetAttCoeff_ : splitDetRelCoeff_;
1243 T negCoeff = (neg > splitNegEnv_[ch]) ? splitDetAttCoeff_ : splitDetRelCoeff_;
1244
1245 splitPosEnv_[ch] = pos + posCoeff * (splitPosEnv_[ch] - pos);
1246 splitNegEnv_[ch] = neg + negCoeff * (splitNegEnv_[ch] - neg);
1247
1248 level = std::max(splitPosEnv_[ch], splitNegEnv_[ch]);
1249 break;
1250 }
1251
1253 {
1254 auto res = hilbertDetectors_[ch].process(sample);
1255 // Direct euclidean distance for magnitude calculation.
1256 // Assuming bounded audio signal [-1.0, 1.0], std::sqrt is safe and faster than std::hypot.
1257 level = std::sqrt(res.real * res.real + res.imag * res.imag);
1258 break;
1259 }
1260 }
1261 return gainToDecibels(level);
1262 }
1263
1264 // ---- Gain curves ----
1265
1270 [[nodiscard]] static T effectiveRatioFor(Character charType, T ratio, T excessDb) noexcept
1271 {
1272 // Fairchild-style progressive compression: a remote-cutoff tube's
1273 // effective ratio grows with level above the threshold.
1274 if (charType == Character::Varimu && excessDb > T(0))
1275 return ratio * (T(1) + excessDb / T(40));
1276 return ratio;
1277 }
1278
1280 [[nodiscard]] static T effectiveKneeFor(Character charType, T knee) noexcept
1281 {
1282 // Neither a remote-cutoff tube (Varimu) nor a photocell (Opto) can
1283 // form a hard corner: both transfers bend gradually over their whole
1284 // operating region, so the knee has a 10 dB physical floor.
1285 return (charType == Character::Varimu || charType == Character::Opto)
1286 ? std::max(knee, T(10)) : knee;
1287 }
1288
1299 [[nodiscard]] T computeGain(T inputDb, T thresh, T ratio, T knee, Character charType, Mode modeType,
1300 T guardDb) const noexcept
1301 {
1302 if (modeType == Mode::Upward)
1303 return computeGainUpward(inputDb, thresh, ratio, knee, guardDb);
1304
1305 const T effectiveRatio = effectiveRatioFor(charType, ratio, inputDb - thresh);
1306 const T effectiveKnee = effectiveKneeFor(charType, knee);
1307
1308 if (effectiveKnee <= T(0))
1309 {
1310 // Hard knee
1311 if (inputDb <= thresh) return T(0);
1312 return (thresh - inputDb) * (T(1) - T(1) / effectiveRatio);
1313 }
1314 else
1315 {
1316 // Soft knee interpolation
1317 T halfKnee = effectiveKnee / T(2);
1318 T lower = thresh - halfKnee;
1319 T upper = thresh + halfKnee;
1320
1321 if (inputDb <= lower) return T(0);
1322 if (inputDb >= upper) return (thresh - inputDb) * (T(1) - T(1) / effectiveRatio);
1323
1324 T x = inputDb - lower;
1325 return (T(1) - T(1) / effectiveRatio) * x * x / (T(2) * effectiveKnee) * T(-1);
1326 }
1327 }
1328
1339 [[nodiscard]] T computeGainUpward(T inputDb, T thresh, T ratio, T knee, T guardDb) const noexcept
1340 {
1341 T effectiveRatio = ratio;
1342 T boost;
1343 if (knee <= T(0))
1344 {
1345 if (inputDb >= thresh) return T(0);
1346 boost = (thresh - inputDb) * (T(1) - T(1) / effectiveRatio);
1347 }
1348 else
1349 {
1350 T halfKnee = knee / T(2);
1351 T lower = thresh - halfKnee;
1352 T upper = thresh + halfKnee;
1353
1354 if (inputDb >= upper) return T(0);
1355 if (inputDb <= lower)
1356 {
1357 boost = (thresh - inputDb) * (T(1) - T(1) / effectiveRatio);
1358 }
1359 else
1360 {
1361 T x = upper - inputDb;
1362 boost = (T(1) - T(1) / effectiveRatio) * x * x / (T(2) * knee);
1363 }
1364 }
1365
1366 const T silenceGuard = std::clamp((guardDb - (thresh - T(60))) / T(20), T(0), T(1));
1367 return boost * silenceGuard;
1368 }
1369
1370 // ---- Feedback law (element transfer solved from the observed curve) ----
1371
1374 {
1377 };
1378
1395 [[nodiscard]] static FeedbackLaw elementLaw(T eOut, T r, T w) noexcept
1396 {
1397 const T a = r - T(1);
1398 if (w <= T(0))
1399 {
1400 if (eOut <= T(0)) return { T(0), T(0) };
1401 return { -a * eOut, a };
1402 }
1403 const T halfW = w / T(2);
1404 if (eOut <= -halfW) return { T(0), T(0) };
1405 if (eOut >= halfW / r) return { -a * eOut, a };
1406 // Knee: invert eOut = u - W/2 - s u^2 / (2W) for the input-side
1407 // excess u in (0, W). s = 1 - 1/R is the feed-forward slope factor;
1408 // the slope denominator is analytically >= 1/R.
1409 const T s = a / r;
1410 const T b = eOut + halfW;
1411 const T z = std::min(T(2) * s * b / w, T(1));
1412 const T u = std::min(T(2) * b / (T(1) + std::sqrt(T(1) - z)), w);
1413 const T su = s * u / w;
1414 return { -s * u * u / (T(2) * w), su / std::max(T(1) - su, T(1) / r) };
1415 }
1416
1427 [[nodiscard]] FeedbackLaw computeGainFeedback(T outDb, T thresh, T ratio, T knee,
1428 Character charType) const noexcept
1429 {
1430 const T eOut = outDb - thresh;
1431 const T w = effectiveKneeFor(charType, knee);
1432 T r = std::max(effectiveRatioFor(charType, ratio, eOut), T(1));
1433 FeedbackLaw law = elementLaw(eOut, r, w);
1434 if (charType == Character::Varimu)
1435 {
1436 for (int pass = 0; pass < 2; ++pass)
1437 {
1438 r = std::max(effectiveRatioFor(charType, ratio, eOut - law.target), T(1));
1439 law = elementLaw(eOut, r, w);
1440 }
1441 }
1442 return law;
1443 }
1444
1447 {
1450 };
1451
1469 [[nodiscard]] FeedbackSolve solveFeedbackGain(T inDb, T k, T effB, T thresh,
1470 T ratio, T knee, Character charType) const noexcept
1471 {
1472 const T r = std::max(effectiveRatioFor(charType, ratio, inDb - thresh), T(1));
1473 const T w = effectiveKneeFor(charType, knee);
1474 const T halfW = w / T(2);
1475 const T a = r - T(1);
1476
1477 // Piece 1: at or below the knee start the law is zero and G = k.
1478 T e = inDb + k - thresh;
1479 if (e <= ((w > T(0)) ? -halfW : T(0)))
1480 return { k, T(0) };
1481
1482 // Piece 3: linear region, target = -a * e at the solved level. With a
1483 // hard knee this piece always resolves (e scales by 1/(1 + effB a)).
1484 const T g3 = (k - effB * a * (inDb - thresh)) / (T(1) + effB * a);
1485 e = inDb + g3 - thresh;
1486 if (e >= ((w > T(0)) ? halfW / r : T(0)))
1487 return { g3, -a * e };
1488
1489 // Piece 2: quadratic knee. Substituting G = k + effB * gel into the
1490 // knee relation gives q u^2 - u + (d + W/2) = 0 with
1491 // q = (1 - effB) s / (2W); the cancellation-free smaller root is used.
1492 const T s = a / r;
1493 const T q = (T(1) - effB) * s / (T(2) * w);
1494 const T b = (inDb - thresh + k) + halfW;
1495 const T disc = std::max(T(1) - T(4) * q * b, T(0));
1496 T u = T(2) * b / (T(1) + std::sqrt(disc));
1497 u = std::clamp(u, T(0), w);
1498 const T target = -s * u * u / (T(2) * w);
1499 return { k + effB * target, target };
1500 }
1501
1512 [[nodiscard]] T applyBallisticsFeedbackImplicit(T inDb, int ch, Character charType,
1513 T thresh, T ratio, T knee) noexcept
1514 {
1515 T& fast = envFastDb_[ch];
1516 T& slow = envSlowDb_[ch];
1517 const T wFast = charFastWeight_;
1518 const bool dual = wFast < T(1);
1519
1520 if (dual)
1521 {
1522 // The memory stage tracks the REAL sustained gain reduction (the
1523 // static curve at the current input), never the element's
1524 // internal target: the loop gain scales that one by up to
1525 // (R - 1), and a memory charging toward the amplified signal
1526 // overshoots, drags the settled blend off the curve and smears
1527 // the observed attack. Advancing it first keeps the solve below
1528 // consistent with the state it blends against.
1529 const T range = rangeDb_.load(std::memory_order_relaxed);
1530 const T slowTarget = std::clamp(
1531 computeGain(inDb, thresh, ratio, knee, charType, Mode::Downward, T(0)),
1532 -range, range);
1533 const bool charging = slowTarget < slow;
1534 slow = slowTarget + (charging ? charChargeCoeff_ : charSlowRelCoeff_) * (slow - slowTarget);
1535 if (std::abs(slow) < T(1e-4)) slow = T(0);
1536 }
1537
1538 // Branch on the direction the loop is about to move: attack while
1539 // the reduction deepens at the previous gain, release otherwise.
1540 const T shownPrev = dual ? wFast * fast + (T(1) - wFast) * slow : fast;
1541 const T probe = computeGainFeedback(inDb + shownPrev, thresh, ratio, knee, charType).target;
1542 const bool engaging = probe < fast;
1543 const T coeff = engaging ? charAttCoeff_ : charRelCoeff_;
1544 const T beta = T(1) - coeff;
1545
1546 const T k = dual ? wFast * coeff * fast + (T(1) - wFast) * slow : coeff * fast;
1547 const T effB = dual ? wFast * beta : beta;
1548 const auto sol = solveFeedbackGain(inDb, k, effB, thresh, ratio, knee, charType);
1549
1550 fast = sol.target + coeff * (fast - sol.target);
1551 if (std::abs(fast) < T(1e-4)) fast = T(0);
1552 T shown = dual ? wFast * fast + (T(1) - wFast) * slow : fast;
1553
1554 // Hold and range act on the OBSERVED gain: the element's internal
1555 // target is loop-amplified by up to (R - 1) during transients, so
1556 // clamping THAT to the user's range would throttle the attack, and
1557 // holding it would compare mismatched domains. When they override,
1558 // the fast stage is rewritten so the blend lands on the bound.
1559 const T bounded = applyHoldAndRange(shown, ch);
1560 if (bounded != shown)
1561 {
1562 shown = bounded;
1563 fast = dual ? (shown - (T(1) - wFast) * slow) / wFast : shown;
1564 }
1565 return shown;
1566 }
1567
1568 // ---- Ballistics (log domain) ----
1569
1592 [[nodiscard]] T applyBallistics(T targetGrDb, int ch, bool splitAdaptive,
1593 T loopSlope = T(0)) noexcept
1594 {
1595 // Level-adaptive release for the SplitPolarity detector: full-scale
1596 // output halves the release time constant. Halving tau squares the
1597 // one-pole coefficient, so blending between the two valid endpoints
1598 // keeps the modulation on the exponential curve.
1599 T attCoeff = charAttCoeff_;
1600 T relCoeff = charRelCoeff_;
1601 if (splitAdaptive)
1602 {
1603 const T outputLevel = std::min(std::abs(fbLastOutput_[ch]), T(1));
1604 relCoeff += outputLevel * (relCoeff * relCoeff - relCoeff);
1605 }
1606 if (loopSlope > T(0))
1607 {
1608 // Cap the per-sample loop advance at 0.5 (half-deadbeat): the
1609 // one-sample delay then converges monotonically instead of
1610 // sustaining a marginal dance on the detector's residual ripple.
1611 const T floorCoeff = T(1) - T(0.5) / (T(1) + loopSlope);
1612 attCoeff = std::max(attCoeff, floorCoeff);
1613 relCoeff = std::max(relCoeff, floorCoeff);
1614 }
1615
1616 // Attack acts while the gain FALLS (louder signal: deeper reduction,
1617 // or less upward boost); release acts while it recovers upward. A
1618 // magnitude comparison would invert the two in Upward mode and make
1619 // the envelope chase the huge boosts of the waveform's zero crossings.
1620 T& fast = envFastDb_[ch];
1621 const bool engaging = targetGrDb < fast;
1622 fast = targetGrDb + (engaging ? attCoeff : relCoeff) * (fast - targetGrDb);
1623 if (std::abs(fast) < T(1e-4)) fast = T(0); // kill the asymptotic dB tail
1624
1625 if (charFastWeight_ >= T(1)) // Clean / Varimu: single envelope
1626 return fast;
1627
1628 // Opto / FET memory stage: charges toward the target over the history
1629 // time constant and discharges with the slow release, so its level
1630 // encodes how long (and how deep) the compressor has been working.
1631 T& slow = envSlowDb_[ch];
1632 const bool charging = targetGrDb < slow;
1633 slow = targetGrDb + (charging ? charChargeCoeff_ : charSlowRelCoeff_) * (slow - targetGrDb);
1634 if (std::abs(slow) < T(1e-4)) slow = T(0);
1635
1636 return charFastWeight_ * fast + (T(1) - charFastWeight_) * slow;
1637 }
1638
1639 // ---- Sidechain HPF ----
1640
1643 {
1644 // The setter rejects invalid cutoffs; the clamp keeps the coefficient
1645 // in the stable range even against a hostile in-memory value.
1646 const double scFreq = std::clamp(
1647 static_cast<double>(scHpfFreq_.load(std::memory_order_relaxed)),
1648 1.0, sampleRate_ * 0.45);
1649 scHpfB1_ = static_cast<T>(std::exp(-std::numbers::pi * 2.0 * scFreq / sampleRate_));
1650 scHpfA0_ = (T(1) + scHpfB1_) / T(2); // Normalization to prevent high-frequency boost
1651 }
1652
1659 [[nodiscard]] T applySidechainHPF(T input, int ch) noexcept
1660 {
1661 T& xp = scHpfPrev_[ch];
1662 T& yp = scHpfState_[ch];
1663 T output = scHpfA0_ * (input - xp) + scHpfB1_ * yp;
1664 xp = input;
1665 yp = output;
1666 return output;
1667 }
1668
1669 // ---- RMS Configuration ----
1670
1672 void updateRmsWindow() noexcept
1673 {
1674 if (sampleRate_ > 0)
1675 {
1676 int requestedSamples = static_cast<int>(sampleRate_ * static_cast<double>(rmsWindowMs_) / 1000.0);
1677
1678 // Limit the logical window to the LIVE element count. capacity()
1679 // can exceed size() after a re-prepare to a lower rate, and a
1680 // window reaching into the dead tail reads stale squares from the
1681 // previous stream (measured: audible gain reduction on silence).
1682 if (!rmsBuffers_[0].empty())
1683 {
1684 int maxLen = static_cast<int>(rmsBuffers_[0].size());
1685 rmsWindowSamples_ = std::clamp(requestedSamples, 1, maxLen);
1686 }
1687 else
1688 {
1689 rmsWindowSamples_ = std::max(1, requestedSamples);
1690 }
1691
1692 for (int ch = 0; ch < kMaxChannels; ++ch)
1693 {
1694 // Clear the window contents too: stale squares from a previous
1695 // window length would otherwise be subtracted from the fresh
1696 // running sum (transient negative-sum glitch).
1697 std::fill(rmsBuffers_[ch].begin(), rmsBuffers_[ch].end(), T(0));
1698 rmsSums_[ch] = T(0);
1699 rmsIndices_[ch] = 0;
1700 rmsRecomputeCounters_[ch] = 0;
1701 }
1702 }
1703 }
1704
1705 // =========================================================================
1706 // Members & State
1707 // =========================================================================
1708
1710 double sampleRate_ = 0;
1711
1712 // User Parameters (Atomic for thread safety)
1713 std::atomic<T> threshold_ { T(-20) };
1714 std::atomic<T> ratio_ { T(4) };
1715 std::atomic<T> attackMs_ { T(5) };
1716 std::atomic<T> releaseMs_ { T(100) };
1717 std::atomic<T> kneeWidth_ { T(0) };
1718 std::atomic<T> makeupGain_ { T(0) };
1719 std::atomic<T> stereoLink_ { T(1) };
1720 std::atomic<T> mix_ { T(1) };
1721 std::atomic<T> lookaheadMs_ { T(0) };
1722 std::atomic<T> characterColor_ { T(0) };
1723 std::atomic<AutoMakeupMode> autoMakeupMode_ { AutoMakeupMode::Off };
1724
1725 std::atomic<DetectorType> detectorType_ { DetectorType::Peak };
1726 std::atomic<Topology> topology_ { Topology::FeedForward };
1727 std::atomic<Character> character_ { Character::Clean };
1728 std::atomic<Mode> mode_ { Mode::Downward };
1729
1730 // Internal DSP Coefficients & State
1731 T autoMakeupCoeff_ = T(0.9995);
1733
1734 // Character ballistics coefficients (dB-domain one-poles, see
1735 // updateTimeConstants). charFastWeight_ == 1 selects the single-envelope
1736 // path (Clean/Varimu); Opto/FET blend fast and slow memory envelopes.
1737 std::atomic<bool> timeConstantsDirty_ { true };
1738 T charAttCoeff_ = T(0);
1739 T charRelCoeff_ = T(0);
1743
1750
1751 // FET colour: 2nd-order term gain at full colour, calibrated so limiting
1752 // at -6 dBFS program level measures within the 1176's published THD spec
1753 // (< 0.5%); the one-pole DC estimate AC-couples the squared term.
1754 static constexpr T kFetColorH2 = T(0.028);
1755 T fetDcCoeff_ = T(0);
1756 std::array<T, kMaxChannels> fetDcState_ {};
1757
1758 std::array<T, kMaxChannels> envFastDb_ {};
1759 std::array<T, kMaxChannels> envSlowDb_ {};
1760 std::array<T, kMaxChannels> upwardGuardDb_ {};
1761 T upwardGuardDecay_ = T(0.00125);
1762 std::array<T, kMaxChannels> fbLastOutput_ {};
1763 std::array<T, kMaxChannels> channelLevelDb_ {};
1764
1765 std::array<RingBuffer<T>, kMaxChannels> lookaheadBuffers_ {};
1767
1768 std::array<Hilbert<T>, kMaxChannels> hilbertDetectors_ {};
1769
1770 // Sidechain Filtering
1771 std::atomic<bool> scHpfEnabled_ { false };
1772 std::atomic<T> scHpfFreq_ { T(80) };
1773 T scHpfB1_ = T(0);
1774 T scHpfA0_ = T(0);
1775 std::array<T, kMaxChannels> scHpfState_ {};
1776 std::array<T, kMaxChannels> scHpfPrev_ {};
1777
1778 // Split-Polarity Detector State
1779 std::array<T, kMaxChannels> splitPosEnv_ {};
1780 std::array<T, kMaxChannels> splitNegEnv_ {};
1782 T splitDetRelCoeff_ = T(0.99);
1783
1784 // RMS Detector
1785 T rmsWindowMs_ = T(10);
1786 std::atomic<T> rmsWindowMsAtomic_ { T(10) };
1787 std::atomic<bool> rmsWindowDirty_ { false };
1789 std::array<std::vector<T>, kMaxChannels> rmsBuffers_;
1790 std::array<T, kMaxChannels> rmsSums_ {};
1791 std::array<int, kMaxChannels> rmsIndices_ {};
1792 static constexpr int kRmsRecomputePeriod = 4096;
1793 std::array<int, kMaxChannels> rmsRecomputeCounters_ {};
1794
1795 // Shared ITU-R BS.1770-4 true-peak detector (Core/TruePeakDetector.h).
1797
1798 // Hold & Range
1799 std::atomic<T> holdMs_ { T(0) };
1800 std::atomic<T> rangeDb_ { T(100) };
1802 std::array<int, kMaxChannels> holdCounters_ {};
1803 std::array<T, kMaxChannels> heldGrDb_ {};
1804
1805 std::atomic<T> gainReductionDb_ { T(0) };
1806};
1807
1808} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
High-fidelity modular compressor designed for real-time applications.
Definition Compressor.h:88
DetectorType
Level detection methodologies.
Definition Compressor.h:94
@ SplitPolarity
Asymmetric positive/negative half-wave tracking (ButterComp2 style).
@ TruePeak
4x oversampled peak detection (ITU-R BS.1770-4 compliant).
@ Rms
Sliding-window Root-Mean-Square. Smoother, responds to average energy.
@ Peak
Instantaneous absolute value tracking. Fast and standard.
static constexpr int kMaxChannels
Definition Compressor.h:828
T charChargeCoeff_
History charge coefficient of the memory stage.
void setThreshold(T dB) noexcept
Sets the compression threshold in dB.
Definition Compressor.h:494
void setHoldTime(T ms) noexcept
Sets the gain-reduction hold time.
Definition Compressor.h:628
std::array< RingBuffer< T >, kMaxChannels > lookaheadBuffers_
Lookahead delay lines.
void prepare(const AudioSpec &spec)
Allocates buffers and initializes internal DSP state.
Definition Compressor.h:190
std::array< T, kMaxChannels > heldGrDb_
std::atomic< bool > scHpfEnabled_
HPF toggle.
std::atomic< AutoMakeupMode > autoMakeupMode_
Auto-makeup behavior.
SmoothedValue< T > makeupSmooth_
De-zippered makeup gain (dB).
std::atomic< T > releaseMs_
Release time in milliseconds.
FeedbackSolve solveFeedbackGain(T inDb, T k, T effB, T thresh, T ratio, T knee, Character charType) const noexcept
Solves one feedback ballistics step against the current input.
std::array< T, kMaxChannels > fetDcState_
Per-channel DC estimate of wet^2.
std::atomic< T > stereoLink_
Stereo linking amount (0 to 1).
T charRelCoeff_
Release coefficient (fast stage).
std::atomic< T > gainReductionDb_
Publicly readable Gain Reduction meter.
T getGainReductionDb() const noexcept
Returns current active gain reduction in dB (negative value).
Definition Compressor.h:728
static E clampEnum(E value, E last) noexcept
Clamps an enum to its valid [first, last] range.
Definition Compressor.h:839
std::array< int, kMaxChannels > holdCounters_
Character getCharacter() const noexcept
Returns the currently active character.
Definition Compressor.h:737
T computeGainUpward(T inputDb, T thresh, T ratio, T knee, T guardDb) const noexcept
Upward compression curve calculation.
SmoothedValue< T > kneeSmooth_
De-zippered knee.
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
Definition Compressor.h:798
std::array< T, kMaxChannels > splitPosEnv_
Positive half-wave tracking.
std::atomic< bool > rmsWindowDirty_
Applied at the next block.
Character
Time-constant behavior and release curve shape.
Definition Compressor.h:127
void setRelease(T ms) noexcept
Sets the release time in milliseconds (clamped to >= 1 ms: below that the envelope stops smoothing at...
Definition Compressor.h:536
T detectLevel(T sample, int ch, DetectorType detType) noexcept
Computes level detection in Decibels.
T scHpfB1_
HPF internal feedback coefficient.
T charAttCoeff_
Attack coefficient of the fast envelope.
std::array< int, kMaxChannels > rmsIndices_
Write heads for RMS buffers.
T applySidechainHPF(T input, int ch) noexcept
Applies sidechain filtering for a specific channel.
void updateHpfCoefficients() noexcept
Updates the normalized DC-blocker / High-pass filter coefficients.
void setTopology(Topology topo) noexcept
Changes signal routing topology (FeedForward or FeedBack).
Definition Compressor.h:650
void setMakeupGain(T dB) noexcept
Sets manual static makeup gain in dB.
Definition Compressor.h:551
std::atomic< Character > character_
Selected ballistics.
T getCharacterColor() const noexcept
Returns the character colour amount (see setCharacterColor).
Definition Compressor.h:685
void setAutoMakeup(AutoMakeupMode mode) noexcept
Selects the automatic makeup behavior (default: Off).
Definition Compressor.h:567
std::array< std::vector< T >, kMaxChannels > rmsBuffers_
Pre-allocated RMS sliding windows.
SmoothedValue< T > thresholdSmooth_
De-zippered threshold.
T applyHoldAndRange(T targetGR_Db, int ch) noexcept
Applies the hold and range stages to the static gain target (dB).
SmoothedValue< T > mixSmooth_
De-zippered parallel mix.
void setLookahead(T ms) noexcept
Sets lookahead time in ms (0 = off, max = 10ms).
Definition Compressor.h:605
std::atomic< T > scHpfFreq_
HPF Cutoff frequency.
T computeGain(T inputDb, T thresh, T ratio, T knee, Character charType, Mode modeType, T guardDb) const noexcept
Calculates static target gain reduction based on knee and ratio.
T autoMakeupEnv_
Smoothed internal auto-makeup envelope.
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Definition Compressor.h:767
T autoMakeupCoeff_
Auto-makeup tracking factor.
std::array< T, kMaxChannels > envSlowDb_
Slow memory envelope per channel (dB).
std::array< T, kMaxChannels > scHpfState_
HPF y[n-1] state.
void setDetector(DetectorType type) noexcept
Changes the level detection algorithm (Peak, RMS, TruePeak, Hilbert).
Definition Compressor.h:613
std::atomic< bool > timeConstantsDirty_
Coefficients need a recompute.
std::atomic< T > mix_
Wet/Dry mix (1 = full wet).
SmoothedValue< T > ratioSmooth_
De-zippered ratio.
void setAutoMakeup(bool on) noexcept
Convenience overload: true selects Adaptive, false turns auto makeup off.
Definition Compressor.h:574
static T effectiveRatioFor(Character charType, T ratio, T excessDb) noexcept
Level-dependent effective ratio of the character (Varimu grows).
T processSample(T input, int channel) noexcept
Processes a single sample on one channel.
Definition Compressor.h:306
Topology
Signal routing topology for the detector sidechain.
Definition Compressor.h:110
@ FeedForward
Detector reads uncompressed input (modern, precise, transparent).
void setRmsWindow(T ms) noexcept
Sets the RMS analysis window size in milliseconds.
Definition Compressor.h:716
std::array< T, kMaxChannels > splitNegEnv_
Negative half-wave tracking.
static constexpr int kRmsRecomputePeriod
Re-summation interval to halt drift.
std::array< T, kMaxChannels > upwardGuardDb_
Peak-held sustained level (Upward guard).
T charFastWeight_
Fast/slow blend (1 = single envelope).
T charSlowRelCoeff_
Release coefficient of the memory stage.
void setMix(T dryWet) noexcept
Sets dry/wet balance for parallel (New York) compression (1.0 = fully wet).
Definition Compressor.h:595
void reset() noexcept
Resets all internal DSP history, states, and buffers to neutral.
Definition Compressor.h:448
void setRatio(T ratio) noexcept
Sets the compression ratio (1.0 = off, >20.0 = limiting).
Definition Compressor.h:501
T applyBallistics(T targetGrDb, int ch, bool splitAdaptive, T loopSlope=T(0)) noexcept
Smooths the static gain target with the active character's ballistics.
std::array< T, kMaxChannels > envFastDb_
Fast gain envelope per channel (dB).
void setSidechainHPF(bool enabled, T cutoffHz=T(80)) noexcept
Toggles the internal sidechain high-pass filter.
Definition Compressor.h:700
std::array< Hilbert< T >, kMaxChannels > hilbertDetectors_
Analytic signal generators for detection.
T splitDetAttCoeff_
Detector attack coefficient (sample-rate derived).
std::atomic< DetectorType > detectorType_
Selected detection method.
void setKnee(T dB) noexcept
Sets knee width in dB (0 = hard knee, >0 = soft knee).
Definition Compressor.h:544
double sampleRate_
Cached operating sample rate.
FeedbackLaw computeGainFeedback(T outDb, T thresh, T ratio, T knee, Character charType) const noexcept
Feedback target for detectors with memory (explicit path).
void setCharacterColor(T amount) noexcept
Sets the amount of the character's harmonic signature (0 to 1).
Definition Compressor.h:678
std::array< T, kMaxChannels > rmsSums_
Running sums for RMS.
std::atomic< T > lookaheadMs_
Lookahead latency target in ms.
void setCharacter(Character type) noexcept
Changes ballistics and envelope behavior character.
Definition Compressor.h:658
void prepare(double sampleRate) noexcept
Prepares the compressor using sample rate only (backward compatibility).
Definition Compressor.h:252
void processBlockImpl(AudioBufferView< T > audio, AudioBufferView< T > sidechain) noexcept
Core DSP loop executing sidechain detection, linking, ballistics, and gain application.
Definition Compressor.h:855
static constexpr T kFetColorH2
void setRange(T dB) noexcept
Limits the maximum gain change the compressor may apply.
Definition Compressor.h:643
std::atomic< T > rangeDb_
Max |gain change| in dB.
std::array< T, kMaxChannels > fbLastOutput_
Feedback topology history buffer.
Topology getTopology() const noexcept
Returns the currently active topology.
Definition Compressor.h:734
std::atomic< T > attackMs_
Attack time in milliseconds.
static T effectiveKneeFor(Character charType, T knee) noexcept
Physical knee floor of the character (dB).
T upwardGuardDecay_
Guard decay per sample (60 dB/s).
T fetDcCoeff_
~10 Hz DC-tracking coefficient.
AutoMakeupMode
Automatic makeup-gain behavior (applies in Downward mode).
Definition Compressor.h:169
@ Off
Manual makeup only (setMakeupGain).
int getLatency() const noexcept
Returns total processing latency in samples.
Definition Compressor.h:748
DetectorType getDetector() const noexcept
Returns the currently active detector type.
Definition Compressor.h:731
std::atomic< T > rmsWindowMsAtomic_
Control-thread published target.
T scHpfA0_
HPF normalized feedforward coefficient.
void updateRmsWindow() noexcept
Safely recalculates the RMS window size ensuring zero heap allocations.
AudioSpec spec_
Active audio environment specification.
std::atomic< Topology > topology_
Selected topology.
T applyBallisticsFeedbackImplicit(T inDb, int ch, Character charType, T thresh, T ratio, T knee) noexcept
Feedback ballistics with the loop resolved semi-implicitly.
std::array< T, kMaxChannels > channelLevelDb_
Raw detected level buffer.
static FeedbackLaw elementLaw(T eOut, T r, T w) noexcept
Downward element law for feedback detection, per output level.
std::atomic< Mode > mode_
Selected compression mode.
std::array< int, kMaxChannels > rmsRecomputeCounters_
Re-summation counters.
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an audio buffer in-place using its own signal as the sidechain.
Definition Compressor.h:262
std::atomic< T > kneeWidth_
Knee width in dB.
void setMode(Mode mode) noexcept
Sets processing mode (Downward or Upward compression).
Definition Compressor.h:580
int lookaheadSamples_
Active lookahead latency in samples.
TruePeakDetector< T, kMaxChannels > truePeak_
std::atomic< T > threshold_
Threshold in dB.
void processBlock(AudioBufferView< T > audio, AudioBufferView< T > sidechain) noexcept
Processes audio with an independent external sidechain.
Definition Compressor.h:276
void updateTimeConstants(T fs) noexcept
Pre-calculates recursive exponential decay coefficients.
T rmsWindowMs_
Active RMS length in ms (audio-thread copy).
void setStereoLink(T amount) noexcept
Sets stereo linking amount (0.0 = unlinked dual mono, 1.0 = fully linked).
Definition Compressor.h:588
std::atomic< T > makeupGain_
Static makeup gain in dB.
T splitDetRelCoeff_
Detector release coefficient (sample-rate derived).
std::atomic< T > holdMs_
Gain-hold time in ms (0 = off).
void setAttack(T ms) noexcept
Sets the attack time in milliseconds.
Definition Compressor.h:520
int rmsWindowSamples_
Active RMS length in samples.
SmoothedValue< T > colorSmooth_
De-zippered character colour amount.
Mode
Processing direction.
Definition Compressor.h:159
@ Downward
Standard: Reduces dynamic range by attenuating signals above the threshold.
std::atomic< T > characterColor_
Character harmonic amount (0 to 1).
~Compressor()=default
std::atomic< T > ratio_
Ratio (e.g., 4 = 4:1).
std::array< T, kMaxChannels > scHpfPrev_
HPF x[n-1] state.
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
90-degree phase-differencing network (analytic-signal generator).
Definition Hilbert.h:63
void prepare(double sampleRate) noexcept
Prepares the transformer. The FIR kernel is sample-rate independent; sampleRate is accepted for API s...
Definition Hilbert.h:77
Zero-allocation parameter smoother for real-time audio.
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
Per-channel 4x-oversampled inter-sample peak estimator.
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.
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:65
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
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43
Element law result: static target (dB, <= 0) and its |slope|.
T slope
d|target| / d(level in dB): the incremental loop gain.
T target
Gain change the element commands at this output level.
Semi-implicit solve result: stepped gain and the law's target.
T target
The element law's static target at the solved level.
T gain
Blended gain (dB) after this sample's ballistics step.