DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
Compressor.h
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
107 };
108
110 enum class Topology
111 {
113 FeedBack
119 };
120
127 enum class Character
128 {
129 Clean,
131
132 Opto,
140
141 FET,
151
152 Varimu
156 };
157
159 enum class Mode
160 {
161 Downward,
162 Upward
166 };
167
169 enum class AutoMakeupMode
170 {
171 Off,
172 Static,
181 Adaptive
184 };
185
186 // -- Lifecycle --------------------------------------------------------------
187
197 void prepare(const AudioSpec& spec)
198 {
199 if (!spec.isValid()) return;
200 spec_ = spec;
201 sampleRate_ = spec.sampleRate;
202
203 T fs = static_cast<T>(sampleRate_);
205 timeConstantsDirty_.store(false, std::memory_order_relaxed);
206
207 // Smoothed parameters initialization
208 T thresh = threshold_.load(std::memory_order_relaxed);
209 T rat = ratio_.load(std::memory_order_relaxed);
210 T knee = kneeWidth_.load(std::memory_order_relaxed);
211
212 thresholdSmooth_.prepare(sampleRate_, 30.0);
213 thresholdSmooth_.reset(thresh);
214 ratioSmooth_.prepare(sampleRate_, 30.0);
215 ratioSmooth_.reset(std::max(rat, T(1)));
216 kneeSmooth_.prepare(sampleRate_, 30.0);
217 kneeSmooth_.reset(std::max(knee, T(0)));
218 makeupSmooth_.prepare(sampleRate_, 30.0);
219 makeupSmooth_.reset(makeupGain_.load(std::memory_order_relaxed));
220 mixSmooth_.prepare(sampleRate_, 30.0);
221 mixSmooth_.reset(mix_.load(std::memory_order_relaxed));
222 colorSmooth_.prepare(sampleRate_, 30.0);
223 colorSmooth_.reset(characterColor_.load(std::memory_order_relaxed));
224
225 // Pre-allocate and initialize per-channel instances. Capacity covers
226 // the 10 ms user lookahead. Every channel slot gets a delay line, not just spec.numChannels:
227 // a view wider than the spec would otherwise read an unprepared ring
228 // (silence) on the extra channels whenever lookahead is active.
229 int maxLaSamples = static_cast<int>(sampleRate_ * 0.01) + 1;
230 for (int ch = 0; ch < kMaxChannels; ++ch)
231 {
232 lookaheadBuffers_[ch].prepare(maxLaSamples);
233 hilbertDetectors_[ch].reset();
234 }
235
236 lookaheadSamples_ = static_cast<int>(fs * std::clamp(
237 lookaheadMs_.load(std::memory_order_relaxed), T(0), T(10)) / T(1000));
238
240
241 // RMS Buffer pre-allocation (Max 500ms to ensure zero real-time allocation)
242 int maxRmsSamples = static_cast<int>(sampleRate_ * 0.5) + 1;
243 for (auto& buf : rmsBuffers_)
244 {
245 buf.assign(static_cast<size_t>(maxRmsSamples), T(0));
246 }
248
249 reset();
250 }
251
256 void prepare(double sampleRate) noexcept
257 {
258 AudioSpec spec { sampleRate, 512, 2 };
259 prepare(spec);
260 }
261
266 void processBlock(AudioBufferView<T> buffer) noexcept
267 {
268 processBlockImpl(buffer, buffer);
269 }
270
280 void processBlock(AudioBufferView<T> audio, AudioBufferView<T> sidechain) noexcept
281 {
282 processBlockImpl(audio, sidechain);
283 }
284
310 [[nodiscard]] T processSample(T input, int channel) noexcept
311 {
312 // Release-safe guards: a wild channel would index every per-channel
313 // state array out of bounds (a negative one is a giant size_t).
314 if (channel < 0 || channel >= kMaxChannels) return input;
315 if (!(sampleRate_ > 0)) return input;
316
317 // Sync atomic parameters into the smoothers here too: processBlock does this
318 // per block, but a processSample-only workflow would otherwise never see
319 // setThreshold()/setRatio()/setKnee() changes (the smoothers stayed frozen).
320 thresholdSmooth_.setTargetValue(threshold_.load(std::memory_order_relaxed));
321 ratioSmooth_.setTargetValue(std::max(ratio_.load(std::memory_order_relaxed), T(1)));
322 kneeSmooth_.setTargetValue(std::max(kneeWidth_.load(std::memory_order_relaxed), T(0)));
323 makeupSmooth_.setTargetValue(makeupGain_.load(std::memory_order_relaxed));
324 colorSmooth_.setTargetValue(characterColor_.load(std::memory_order_relaxed));
325
326 // Channel 0 advances the shared smoothers; advancing on every call
327 // would scale their time constants with the caller's channel count.
328 const bool advanceShared = (channel == 0);
329 T thresh, ratio, knee, makeupDb, colorAmt;
330 if (advanceShared)
331 {
332 thresh = thresholdSmooth_.getNextValue();
333 ratio = ratioSmooth_.getNextValue();
334 knee = kneeSmooth_.getNextValue();
335 makeupDb = makeupSmooth_.getNextValue();
336 colorAmt = colorSmooth_.getNextValue();
337 }
338 else
339 {
340 thresh = thresholdSmooth_.getCurrentValue();
341 ratio = ratioSmooth_.getCurrentValue();
342 knee = kneeSmooth_.getCurrentValue();
343 makeupDb = makeupSmooth_.getCurrentValue();
344 colorAmt = colorSmooth_.getCurrentValue();
345 }
346
347 auto detType = detectorType_.load(std::memory_order_relaxed);
348 auto topo = topology_.load(std::memory_order_relaxed);
349 auto charType = character_.load(std::memory_order_relaxed);
350 auto modeType = mode_.load(std::memory_order_relaxed);
351 bool scHpf = scHpfEnabled_.load(std::memory_order_relaxed);
352 auto autoMkup = autoMakeupMode_.load(std::memory_order_relaxed);
353
354 // Cheap relaxed pre-check keeps the per-sample cost to one load when
355 // nothing changed; the exchange claims the flag only when it is set.
356 if (timeConstantsDirty_.load(std::memory_order_relaxed)
357 && timeConstantsDirty_.exchange(false, std::memory_order_acquire))
358 {
359 updateTimeConstants(static_cast<T>(sampleRate_));
360 }
361 if (channel == 0
362 && rmsWindowDirty_.load(std::memory_order_relaxed)
363 && rmsWindowDirty_.exchange(false, std::memory_order_acquire))
364 {
365 rmsWindowMs_ = rmsWindowMsAtomic_.load(std::memory_order_relaxed);
367 }
368
369 // The FET character is a feedback design like the 1176 it models: it
370 // always detects on the compressed output with the hardware's peak
371 // rectifier, whatever Topology and Detector say.
372 const Topology topoEff = (charType == Character::FET) ? Topology::FeedBack : topo;
373 const DetectorType detTypeEff = (charType == Character::FET) ? DetectorType::Peak : detType;
374 const bool fbImplicit = topoEff == Topology::FeedBack
375 && modeType == Mode::Downward
376 && detTypeEff == DetectorType::Peak;
377
378 // The sidechain filter must process whatever the detector consumes:
379 // in FeedBack that is the compressed output (previous sample on the
380 // explicit path; on the semi-implicit path the loop equation supplies
381 // the gain, so the current input feeds the filter directly).
382 T detectorIn = (topoEff == Topology::FeedBack && !fbImplicit)
383 ? fbLastOutput_[channel] : input;
384 if (scHpf) detectorIn = applySidechainHPF(detectorIn, channel);
385
386 T levelDb = detectLevel(detectorIn, channel, detTypeEff);
387
388 // Sustained-level guard envelope for Upward mode: instant rise,
389 // constant 60 dB/s fall (peak hold with linear decay).
390 T guardDb = levelDb;
391 if (modeType == Mode::Upward)
392 {
393 T& g = upwardGuardDb_[channel];
394 g = std::max(levelDb, g - upwardGuardDecay_);
395 guardDb = g;
396 }
397
398 const bool splitAdaptive = detTypeEff == DetectorType::SplitPolarity;
399 T smoothedGR_Db;
400 if (fbImplicit)
401 {
402 smoothedGR_Db = applyBallisticsFeedbackImplicit(levelDb, channel, charType,
403 thresh, ratio, knee);
404 }
405 else if (topoEff == Topology::FeedBack && modeType == Mode::Downward)
406 {
407 const auto law = computeGainFeedback(levelDb, thresh, ratio, knee, charType);
408 const T targetGR_Db = applyHoldAndRange(law.target, channel);
409 smoothedGR_Db = applyBallistics(targetGR_Db, channel, splitAdaptive, law.slope);
410 }
411 else
412 {
413 T targetGR_Db = computeGain(levelDb, thresh, ratio, knee, charType, modeType, guardDb);
414 targetGR_Db = applyHoldAndRange(targetGR_Db, channel);
415 smoothedGR_Db = applyBallistics(targetGR_Db, channel, splitAdaptive);
416 }
417 T smoothedGainLinear = decibelsToGain(smoothedGR_Db);
418
419 if (advanceShared)
420 autoMakeupEnv_ = smoothedGR_Db + autoMakeupCoeff_ * (autoMakeupEnv_ - smoothedGR_Db);
421
422 T makeup = makeupDb;
423 if (modeType == Mode::Downward)
424 {
425 if (autoMkup == AutoMakeupMode::Adaptive)
426 makeup += -autoMakeupEnv_;
427 else if (autoMkup == AutoMakeupMode::Static)
428 makeup += computeGain(T(0), thresh, ratio, knee, charType,
429 Mode::Downward, T(0)) * T(-0.5);
430 }
431
432 T output = input * smoothedGainLinear;
433 if (colorAmt > T(0) && charType == Character::FET)
434 {
435 // Same 2nd-order FET channel modulation as the block path.
436 const T sq = output * output;
437 T& dc = fetDcState_[channel];
438 dc += fetDcCoeff_ * (sq - dc);
439 const T grDepth = std::clamp(-smoothedGR_Db * T(0.1), T(0), T(1));
440 output += colorAmt * kFetColorH2 * grDepth * (sq - dc);
441 }
442 output *= makeupToGain(makeup);
443
444 fbLastOutput_[channel] = output;
445 gainReductionDb_.store(smoothedGR_Db, std::memory_order_relaxed);
446 return output;
447 }
448
452 void reset() noexcept
453 {
454 for (int ch = 0; ch < kMaxChannels; ++ch)
455 {
456 envFastDb_[ch] = T(0); // 0 dB gain change = neutral
457 envSlowDb_[ch] = T(0);
458 upwardGuardDb_[ch] = T(-200);
459 fbLastOutput_[ch] = T(0);
460 fetDcState_[ch] = T(0);
461 scHpfState_[ch] = T(0);
462 scHpfPrev_[ch] = T(0);
463 channelLevelDb_[ch] = T(-200);
464 lookaheadBuffers_[ch].reset();
465 hilbertDetectors_[ch].reset();
466 }
467 for (auto& buf : rmsBuffers_)
468 std::fill(buf.begin(), buf.end(), T(0));
469 for (auto& sum : rmsSums_) sum = T(0);
470 for (auto& idx : rmsIndices_) idx = 0;
471 for (auto& cnt : rmsRecomputeCounters_) cnt = 0;
472
474 holdCounters_.fill(0);
475 heldGrDb_.fill(T(0));
476 gainReductionDb_.store(T(0), std::memory_order_relaxed);
477 autoMakeupEnv_ = T(0);
478 splitPosEnv_.fill(T(0));
479 splitNegEnv_.fill(T(0));
480
481 thresholdSmooth_.skip();
482 ratioSmooth_.skip();
483 kneeSmooth_.skip();
484 makeupSmooth_.skip();
485 mixSmooth_.skip();
486 colorSmooth_.skip();
487 }
488
489 // =========================================================================
490 // Parameter Setters
491 // =========================================================================
492
493 // All numeric setters ignore non-finite values (NaN/Inf) and keep the
494 // previous parameter: a poisoned time constant, level or coefficient
495 // would otherwise contaminate the gain path permanently.
496
498 void setThreshold(T dB) noexcept
499 {
500 if (!std::isfinite(dB)) return;
501 threshold_.store(dB, std::memory_order_relaxed);
502 }
503
505 void setRatio(T ratio) noexcept
506 {
507 if (!std::isfinite(ratio)) return;
508 ratio_.store(std::max(ratio, T(1)), std::memory_order_relaxed);
509 // The feedback attack compensation depends on the loop gain (ratio).
510 timeConstantsDirty_.store(true, std::memory_order_release);
511 }
512
524 void setAttack(T ms) noexcept
525 {
526 if (!std::isfinite(ms)) return;
527 attackMs_.store(std::max(ms, T(0.01)), std::memory_order_relaxed);
528 timeConstantsDirty_.store(true, std::memory_order_release);
529 }
530
540 void setRelease(T ms) noexcept
541 {
542 if (!std::isfinite(ms)) return;
543 releaseMs_.store(std::max(ms, T(1)), std::memory_order_relaxed);
544 timeConstantsDirty_.store(true, std::memory_order_release);
545 }
546
548 void setKnee(T dB) noexcept
549 {
550 if (!std::isfinite(dB)) return;
551 kneeWidth_.store(std::max(dB, T(0)), std::memory_order_relaxed);
552 }
553
555 void setMakeupGain(T dB) noexcept
556 {
557 if (!std::isfinite(dB)) return;
558 makeupGain_.store(dB, std::memory_order_relaxed);
559 }
560
571 void setAutoMakeup(AutoMakeupMode mode) noexcept
572 {
574 std::memory_order_relaxed);
575 }
576
578 void setAutoMakeup(bool on) noexcept
579 {
581 }
582
584 void setMode(Mode mode) noexcept
585 {
586 mode_.store(clampEnum(mode, Mode::Upward), std::memory_order_relaxed);
587 // Feedback attack compensation only applies to Downward loops.
588 timeConstantsDirty_.store(true, std::memory_order_release);
589 }
590
592 void setStereoLink(T amount) noexcept
593 {
594 if (!std::isfinite(amount)) return;
595 stereoLink_.store(std::clamp(amount, T(0), T(1)), std::memory_order_relaxed);
596 }
597
599 void setMix(T dryWet) noexcept
600 {
601 if (!std::isfinite(dryWet)) return;
602 mix_.store(std::clamp(dryWet, T(0), T(1)), std::memory_order_relaxed);
603 }
604
609 void setLookahead(T ms) noexcept
610 {
611 if (!std::isfinite(ms)) return;
612 lookaheadMs_.store(std::clamp(ms, T(0), T(10)), std::memory_order_relaxed);
613 timeConstantsDirty_.store(true, std::memory_order_release);
614 }
615
617 void setDetector(DetectorType type) noexcept
618 {
619 // The shared TruePeakDetector builds its coefficients lazily on first
620 // use (thread-safe static), so this is a pure atomic publication.
621 detectorType_.store(clampEnum(type, DetectorType::Hilbert), std::memory_order_relaxed);
622 }
623
632 void setHoldTime(T ms) noexcept
633 {
634 if (!std::isfinite(ms)) return;
635 holdMs_.store(std::clamp(ms, T(0), T(500)), std::memory_order_relaxed);
636 timeConstantsDirty_.store(true, std::memory_order_release);
637 }
638
647 void setRange(T dB) noexcept
648 {
649 if (!std::isfinite(dB)) return;
650 rangeDb_.store(std::max(dB, T(0)), std::memory_order_relaxed);
651 }
652
654 void setTopology(Topology topo) noexcept
655 {
656 topology_.store(clampEnum(topo, Topology::FeedBack), std::memory_order_relaxed);
657 // Feedback loops re-derive the attack coefficient (loop speed-up).
658 timeConstantsDirty_.store(true, std::memory_order_release);
659 }
660
662 void setCharacter(Character type) noexcept
663 {
664 character_.store(clampEnum(type, Character::Varimu), std::memory_order_relaxed);
665 timeConstantsDirty_.store(true, std::memory_order_release);
666 }
667
682 void setCharacterColor(T amount) noexcept
683 {
684 if (!std::isfinite(amount)) return;
685 characterColor_.store(std::clamp(amount, T(0), T(1)), std::memory_order_relaxed);
686 }
687
689 [[nodiscard]] T getCharacterColor() const noexcept
690 {
691 return characterColor_.load(std::memory_order_relaxed);
692 }
693
704 void setSidechainHPF(bool enabled, T cutoffHz = T(80)) noexcept
705 {
706 scHpfEnabled_.store(enabled, std::memory_order_relaxed);
707 if (std::isfinite(cutoffHz) && cutoffHz > T(0))
708 scHpfFreq_.store(cutoffHz, std::memory_order_relaxed);
709 }
710
720 void setRmsWindow(T ms) noexcept
721 {
722 if (!std::isfinite(ms)) return;
723 rmsWindowMsAtomic_.store(std::clamp(ms, T(1), T(500)), std::memory_order_relaxed);
724 rmsWindowDirty_.store(true, std::memory_order_release);
725 }
726
727 // =========================================================================
728 // Metering & Getters
729 // =========================================================================
730
732 [[nodiscard]] T getGainReductionDb() const noexcept { return gainReductionDb_.load(std::memory_order_relaxed); }
733
735 [[nodiscard]] DetectorType getDetector() const noexcept { return detectorType_.load(std::memory_order_relaxed); }
736
738 [[nodiscard]] Topology getTopology() const noexcept { return topology_.load(std::memory_order_relaxed); }
739
741 [[nodiscard]] Character getCharacter() const noexcept { return character_.load(std::memory_order_relaxed); }
742
751 [[nodiscard]] int getLatency() const noexcept
752 {
753 const bool feedback =
754 topology_.load(std::memory_order_relaxed) == Topology::FeedBack
755 || character_.load(std::memory_order_relaxed) == Character::FET;
756 if (feedback) return 0;
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;
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 const bool splitAdaptive = detTypeEff == DetectorType::SplitPolarity;
910
911 // Lookahead breaks causal logic in Feedback mode, so it is strictly
912 // disabled there.
913 int activeLookahead = (topoEff == Topology::FeedBack) ? 0 : lookaheadSamples_;
914
915 for (int i = 0; i < nS; ++i)
916 {
917 T thresh = thresholdSmooth_.getNextValue();
918 T ratio = ratioSmooth_.getNextValue();
919 T knee = kneeSmooth_.getNextValue();
920 T mkupGain = makeupSmooth_.getNextValue();
921 T mixVal = mixSmooth_.getNextValue();
922 T colorAmt = colorSmooth_.getNextValue();
923 T linkedLevel = T(-200);
924
925 // Static auto makeup follows the smoothed curve parameters, so it
926 // stays click-free through threshold/ratio/knee automation.
927 if (autoMkup == AutoMakeupMode::Static && modeType == Mode::Downward)
928 mkupGain += computeGain(T(0), thresh, ratio, knee, charType,
929 Mode::Downward, T(0)) * T(-0.5);
930
931 // 1. Detection Path (Per-Channel)
932 for (int ch = 0; ch < nCh; ++ch)
933 {
934 // Fallback to internal channel if sidechain buffer lacks channels
935 T sample = (scCh > 0) ? sidechain.getChannel(std::min(ch, scCh - 1))[i]
936 : audio.getChannel(ch)[i];
937
938 // The sidechain filter must process whatever the detector
939 // consumes. In FeedBack that is the compressed output (the
940 // external key is ignored): the previous sample on the
941 // explicit path, or the channel's own current input on the
942 // semi-implicit path (the loop equation supplies the gain).
943 T detectorIn;
944 if (topoEff == Topology::FeedBack)
945 detectorIn = fbImplicit ? audio.getChannel(ch)[i] : fbLastOutput_[ch];
946 else
947 detectorIn = sample;
948 if (scHpf) detectorIn = applySidechainHPF(detectorIn, ch);
949
950 T levelDb = detectLevel(detectorIn, ch, detTypeEff);
951
952 channelLevelDb_[ch] = levelDb;
953 if (levelDb > linkedLevel) linkedLevel = levelDb;
954 }
955
956 // Makeup: shared by every channel of the frame, and usually
957 // unchanged from the previous frame (the conversion is cached).
958 T makeupDb = mkupGain;
959 if (autoMkup == AutoMakeupMode::Adaptive && modeType == Mode::Downward)
960 makeupDb += -autoMakeupEnv_;
961 const T makeupLin = makeupToGain(makeupDb);
962
963 // 2. Stereo Linking & Gain Application
964 T blockGR = T(0);
965 for (int ch = 0; ch < nCh; ++ch)
966 {
967 T chLevel = channelLevelDb_[ch];
968 T inputDb = chLevel + sLink * (linkedLevel - chLevel);
969
970 // Sustained-level guard envelope for Upward mode: instant
971 // rise, constant 60 dB/s fall (peak hold with linear decay).
972 T guardDb = inputDb;
973 if (modeType == Mode::Upward)
974 {
975 T& g = upwardGuardDb_[ch];
976 g = std::max(inputDb, g - upwardGuardDecay_);
977 guardDb = g;
978 }
979
980 // 3. Static curve + character ballistics (log domain)
981 T smoothedGR_Db;
982 if (fbImplicit)
983 {
984 smoothedGR_Db = applyBallisticsFeedbackImplicit(inputDb, ch, charType,
985 thresh, ratio, knee);
986 }
987 else if (topoEff == Topology::FeedBack && modeType == Mode::Downward)
988 {
989 // Explicit loop (detector with memory): calibrated element
990 // law plus a per-sample stability floor on the ballistics.
991 const auto law = computeGainFeedback(inputDb, thresh, ratio, knee, charType);
992 const T targetGR_Db = applyHoldAndRange(law.target, ch);
993 smoothedGR_Db = applyBallistics(targetGR_Db, ch, splitAdaptive, law.slope);
994 }
995 else
996 {
997 T targetGR_Db = computeGain(inputDb, thresh, ratio, knee, charType, modeType, guardDb);
998 targetGR_Db = applyHoldAndRange(targetGR_Db, ch);
999 smoothedGR_Db = applyBallistics(targetGR_Db, ch, splitAdaptive);
1000 }
1001 // (0 dB is the common case below threshold: skip the exp.)
1002 T smoothedGainLinear = (smoothedGR_Db == T(0)) ? T(1) : decibelsToGain(smoothedGR_Db);
1003
1004 T input;
1005 if (activeLookahead > 0)
1006 {
1007 lookaheadBuffers_[ch].push(audio.getChannel(ch)[i]);
1008 input = lookaheadBuffers_[ch].read(activeLookahead);
1009 }
1010 else
1011 {
1012 input = audio.getChannel(ch)[i];
1013 }
1014
1015 T wet = input * smoothedGainLinear;
1016 if (colorAmt > T(0) && charType == Character::FET)
1017 {
1018 // 2nd-order FET channel modulation: the drain-source
1019 // resistance bends with the signal across it, so colour
1020 // only appears while the FET conducts (gain reduction
1021 // active); the squared term is AC-coupled like the
1022 // hardware's output transformer. Applied pre-makeup: the
1023 // line amp after the element is clean.
1024 const T sq = wet * wet;
1025 T& dc = fetDcState_[ch];
1026 dc += fetDcCoeff_ * (sq - dc);
1027 const T grDepth = std::clamp(-smoothedGR_Db * T(0.1), T(0), T(1));
1028 wet += colorAmt * kFetColorH2 * grDepth * (sq - dc);
1029 }
1030 wet *= makeupLin;
1031 fbLastOutput_[ch] = wet; // feedback detector reads the compressed signal
1032
1033 // Parallel (New York) mix done inline: the dry reference is `input`,
1034 // which carries the SAME lookahead delay as the wet, so dry and wet
1035 // stay phase-aligned (the previous DryWetMixer captured the UNDELAYED
1036 // input and comb-filtered when lookahead + mix<1 were combined).
1037 audio.getChannel(ch)[i] = (mixVal < T(1))
1038 ? (input * (T(1) - mixVal) + wet * mixVal)
1039 : wet;
1040
1041 if (ch == 0 || smoothedGR_Db < blockGR)
1042 blockGR = smoothedGR_Db; // Track worst-case GR for metering
1043 }
1044
1045 gainReductionDb_.store(blockGR, std::memory_order_relaxed);
1046
1047 // Auto-makeup envelope tracks slowly (~300ms)
1048 autoMakeupEnv_ = blockGR + autoMakeupCoeff_ * (autoMakeupEnv_ - blockGR);
1049 }
1050 }
1051
1056 void updateTimeConstants(T fs) noexcept
1057 {
1058 T attMs = std::max(attackMs_.load(std::memory_order_relaxed), T(0.01));
1059 T relMs = std::max(releaseMs_.load(std::memory_order_relaxed), T(1));
1060 autoMakeupCoeff_ = std::exp(T(-1) / (fs * T(0.3)));
1061
1062 // One-pole coefficient for a time constant given in milliseconds.
1063 auto tc = [fs](T ms) { return std::exp(T(-1) / (fs * ms / T(1000))); };
1064
1065 // Character ballistics: time constants of the dB-domain envelopes.
1066 // The default arm backs up the setter's enum clamp: falling through
1067 // with no case would leave the coefficients zero-initialized
1068 // (instant ballistics, i.e. a waveshaper).
1069 switch (character_.load(std::memory_order_relaxed))
1070 {
1071 default:
1072 case Character::Clean:
1073 case Character::Varimu:
1074 charAttCoeff_ = tc(attMs);
1075 charRelCoeff_ = tc(relMs);
1078 charFastWeight_ = T(1); // single envelope
1079 break;
1080
1081 case Character::Opto:
1082 // T4 cell: the release knob is the ~50% recovery time. With a
1083 // 0.6/0.4 fast/slow split, tau_fast = 0.61 x release puts the
1084 // half-recovery point on the knob value while the memory tail
1085 // decays ~35x slower, capped at the physical 5 s of the cell
1086 // (LA-2A spec: 50% in ~0.06 s, complete in 0.5-5 s). The slow
1087 // stage charges over ~4 release times (capped at 2 s), so only
1088 // sustained compression builds the long tail. The 10 ms attack
1089 // floor is the cell's published attack time.
1090 charAttCoeff_ = tc(std::max(attMs, T(10)));
1091 charRelCoeff_ = tc(relMs * T(0.61));
1092 charSlowRelCoeff_ = tc(std::min(relMs * T(25), T(5000)));
1093 charChargeCoeff_ = tc(std::clamp(relMs * T(4), T(100), T(2000)));
1094 charFastWeight_ = T(0.6);
1095 break;
1096
1097 case Character::FET:
1098 {
1099 // 1176: knob ranges are 20-800 us attack and 50-1100 ms
1100 // release. With a 0.75/0.25 split the compound release passes
1101 // ~t63 at the knob value, and a 150 ms history charge gives
1102 // the program-dependent tail.
1103 const T rel = std::clamp(relMs, T(50), T(1100));
1104 charAttCoeff_ = tc(std::clamp(attMs, T(0.02), T(0.8)));
1105 charRelCoeff_ = tc(rel * T(0.7));
1106 charSlowRelCoeff_ = tc(rel * T(2.5));
1107 charChargeCoeff_ = tc(T(150));
1108 charFastWeight_ = T(0.75);
1109 break;
1110 }
1111 }
1112
1113 // Downward feedback closes its loop around the attack stage, which
1114 // multiplies the raw ballistics speed by (1 + loop gain). Re-derive
1115 // the coefficient from rho = coeff/(1 + w beta A) so the OBSERVED
1116 // attack t63 stays on the knob, exactly like the hardware panels
1117 // (the 1176's 20-800 us figures are measured results, not RC values).
1118 // A is the linear-region loop gain (R - 1); Varimu uses its base
1119 // ratio (the progressive part is level-dependent).
1120 {
1121 const auto charNow = character_.load(std::memory_order_relaxed);
1122 const bool fbLoop = (charNow == Character::FET
1123 || topology_.load(std::memory_order_relaxed) == Topology::FeedBack)
1124 && mode_.load(std::memory_order_relaxed) == Mode::Downward;
1125 if (fbLoop)
1126 {
1127 const T loopGain = charFastWeight_
1128 * (std::max(ratio_.load(std::memory_order_relaxed), T(1)) - T(1));
1129 const T rho = charAttCoeff_;
1130 charAttCoeff_ = T(1) - (T(1) - rho) / (T(1) + rho * loopGain);
1131 }
1132 }
1133
1134 // SplitPolarity detector ballistics. The time constants reproduce the
1135 // original per-sample coefficients (0.6 attack / 0.99 release) at
1136 // 44.1 kHz, but are now sample-rate invariant.
1137 splitDetAttCoeff_ = std::exp(T(-1) / (fs * T(44.39e-6)));
1138 splitDetRelCoeff_ = std::exp(T(-1) / (fs * T(2.2562e-3)));
1139
1140 // Upward silence-guard envelope: 60 dB/s linear decay.
1141 upwardGuardDecay_ = T(60) / fs;
1142
1143 // AC coupling (~10 Hz) of the FET color's 2nd-order term.
1144 fetDcCoeff_ = T(1) - std::exp(T(-2) * std::numbers::pi_v<T> * T(10) / fs);
1145
1146 lookaheadSamples_ = static_cast<int>(fs * std::clamp(
1147 lookaheadMs_.load(std::memory_order_relaxed), T(0), T(10)) / T(1000));
1148
1149 holdSamples_ = static_cast<int>(fs * holdMs_.load(std::memory_order_relaxed) / T(1000));
1150 }
1151
1158 [[nodiscard]] T applyHoldAndRange(T targetGR_Db, int ch) noexcept
1159 {
1160 const T range = rangeDb_.load(std::memory_order_relaxed);
1161 targetGR_Db = std::clamp(targetGR_Db, -range, range);
1162
1163 if (holdSamples_ > 0)
1164 {
1165 T& held = heldGrDb_[ch];
1166 int& counter = holdCounters_[ch];
1167 if (std::abs(targetGR_Db) >= std::abs(held))
1168 {
1169 held = targetGR_Db; // deeper action: re-arm the hold
1170 counter = holdSamples_;
1171 }
1172 else if (counter > 0)
1173 {
1174 --counter; // shallower: freeze at held depth
1175 targetGR_Db = held;
1176 }
1177 else
1178 {
1179 held = targetGR_Db; // hold elapsed: track normally
1180 }
1181 }
1182 return targetGR_Db;
1183 }
1184
1192 [[nodiscard]] T makeupToGain(T makeupDb) noexcept
1193 {
1194 if (makeupDb != cachedMakeupDb_)
1195 {
1196 cachedMakeupDb_ = makeupDb;
1197 cachedMakeupLin_ = decibelsToGain(makeupDb);
1198 }
1199 return cachedMakeupLin_;
1200 }
1201
1202 // ---- Detectors ----
1203
1211 [[nodiscard]] T detectLevel(T sample, int ch, DetectorType detType) noexcept
1212 {
1213 T level = std::abs(sample);
1214 switch (detType)
1215 {
1216 case DetectorType::Peak:
1217 break; // level already holds abs(sample)
1218
1219 case DetectorType::Rms:
1220 {
1221 T sq = sample * sample;
1222 auto& buf = rmsBuffers_[ch];
1223 auto& sum = rmsSums_[ch];
1224 auto& idx = rmsIndices_[ch];
1225 auto& recomputeCount = rmsRecomputeCounters_[ch];
1226 int len = rmsWindowSamples_;
1227
1228 if (len > 0 && len <= static_cast<int>(buf.size()))
1229 {
1230 sum -= buf[idx];
1231 buf[idx] = sq;
1232 sum += sq;
1233 if (++idx >= len) idx = 0; // branch beats an integer division per sample
1234
1235 // Periodic full re-summation to prevent floating-point drift
1236 if (++recomputeCount >= kRmsRecomputePeriod)
1237 {
1238 sum = T(0);
1239 for (int j = 0; j < len; ++j) sum += buf[j];
1240 recomputeCount = 0;
1241 }
1242
1243 level = std::sqrt(std::max(sum / static_cast<T>(len), T(0)));
1244 }
1245 break;
1246 }
1247
1249 level = truePeak_.processSample(sample, ch);
1250 break;
1251
1253 {
1254 T pos = std::max(sample, T(0));
1255 T neg = std::max(-sample, T(0));
1256
1257 T posCoeff = (pos > splitPosEnv_[ch]) ? splitDetAttCoeff_ : splitDetRelCoeff_;
1258 T negCoeff = (neg > splitNegEnv_[ch]) ? splitDetAttCoeff_ : splitDetRelCoeff_;
1259
1260 splitPosEnv_[ch] = pos + posCoeff * (splitPosEnv_[ch] - pos);
1261 splitNegEnv_[ch] = neg + negCoeff * (splitNegEnv_[ch] - neg);
1262
1263 level = std::max(splitPosEnv_[ch], splitNegEnv_[ch]);
1264 break;
1265 }
1266
1268 {
1269 // The analytic magnitude is the envelope of a sustained tone,
1270 // but the allpass pair takes part of a cycle to form it after
1271 // an onset; the rectified input is never above the envelope of
1272 // a steady tone, so the larger of the two keeps the ripple-free
1273 // level and catches a step as fast as the peak detector.
1274 const double mag = hilbertDetectors_[ch].magnitude(static_cast<double>(sample));
1275 level = static_cast<T>(std::max(mag, std::abs(static_cast<double>(sample))));
1276 break;
1277 }
1278 }
1279 return gainToDecibels(level);
1280 }
1281
1282 // ---- Gain curves ----
1283
1290 [[nodiscard]] static T effectiveRatioFor(Character charType, T ratio, T excessDb) noexcept
1291 {
1292 // Fairchild-style progressive compression: a remote-cutoff tube's
1293 // effective ratio grows with level above the threshold.
1294 if (charType == Character::Varimu && excessDb > T(0))
1295 return ratio * (T(1) + excessDb / T(40));
1296 return ratio;
1297 }
1298
1300 [[nodiscard]] static T effectiveKneeFor(Character charType, T knee) noexcept
1301 {
1302 // Neither a remote-cutoff tube (Varimu) nor a photocell (Opto) can
1303 // form a hard corner: both transfers bend gradually over their whole
1304 // operating region, so the knee has a 10 dB physical floor.
1305 return (charType == Character::Varimu || charType == Character::Opto)
1306 ? std::max(knee, T(10)) : knee;
1307 }
1308
1320 [[nodiscard]] T computeGain(T inputDb, T thresh, T ratio, T knee, Character charType, Mode modeType,
1321 T guardDb) const noexcept
1322 {
1323 if (modeType == Mode::Upward)
1324 return computeGainUpward(inputDb, thresh, ratio, knee, guardDb);
1325
1326 const T effectiveRatio = effectiveRatioFor(charType, ratio, inputDb - thresh);
1327 const T effectiveKnee = effectiveKneeFor(charType, knee);
1328
1329 if (effectiveKnee <= T(0))
1330 {
1331 // Hard knee
1332 if (inputDb <= thresh) return T(0);
1333 return (thresh - inputDb) * (T(1) - T(1) / effectiveRatio);
1334 }
1335 else
1336 {
1337 // Soft knee interpolation
1338 T halfKnee = effectiveKnee / T(2);
1339 T lower = thresh - halfKnee;
1340 T upper = thresh + halfKnee;
1341
1342 if (inputDb <= lower) return T(0);
1343 if (inputDb >= upper) return (thresh - inputDb) * (T(1) - T(1) / effectiveRatio);
1344
1345 T x = inputDb - lower;
1346 return (T(1) - T(1) / effectiveRatio) * x * x / (T(2) * effectiveKnee) * T(-1);
1347 }
1348 }
1349
1360 [[nodiscard]] T computeGainUpward(T inputDb, T thresh, T ratio, T knee, T guardDb) const noexcept
1361 {
1362 T effectiveRatio = ratio;
1363 T boost;
1364 if (knee <= T(0))
1365 {
1366 if (inputDb >= thresh) return T(0);
1367 boost = (thresh - inputDb) * (T(1) - T(1) / effectiveRatio);
1368 }
1369 else
1370 {
1371 T halfKnee = knee / T(2);
1372 T lower = thresh - halfKnee;
1373 T upper = thresh + halfKnee;
1374
1375 if (inputDb >= upper) return T(0);
1376 if (inputDb <= lower)
1377 {
1378 boost = (thresh - inputDb) * (T(1) - T(1) / effectiveRatio);
1379 }
1380 else
1381 {
1382 T x = upper - inputDb;
1383 boost = (T(1) - T(1) / effectiveRatio) * x * x / (T(2) * knee);
1384 }
1385 }
1386
1387 const T silenceGuard = std::clamp((guardDb - (thresh - T(60))) / T(20), T(0), T(1));
1388 return boost * silenceGuard;
1389 }
1390
1391 // ---- Feedback law (element transfer solved from the observed curve) ----
1392
1395 {
1398 };
1399
1416 [[nodiscard]] static FeedbackLaw elementLaw(T eOut, T r, T w) noexcept
1417 {
1418 const T a = r - T(1);
1419 if (w <= T(0))
1420 {
1421 if (eOut <= T(0)) return { T(0), T(0) };
1422 return { -a * eOut, a };
1423 }
1424 const T halfW = w / T(2);
1425 if (eOut <= -halfW) return { T(0), T(0) };
1426 if (eOut >= halfW / r) return { -a * eOut, a };
1427 // Knee: invert eOut = u - W/2 - s u^2 / (2W) for the input-side
1428 // excess u in (0, W). s = 1 - 1/R is the feed-forward slope factor;
1429 // the slope denominator is analytically >= 1/R.
1430 const T s = a / r;
1431 const T b = eOut + halfW;
1432 const T z = std::min(T(2) * s * b / w, T(1));
1433 const T u = std::min(T(2) * b / (T(1) + std::sqrt(T(1) - z)), w);
1434 const T su = s * u / w;
1435 return { -s * u * u / (T(2) * w), su / std::max(T(1) - su, T(1) / r) };
1436 }
1437
1452 [[nodiscard]] FeedbackLaw computeGainFeedback(T outDb, T thresh, T ratio, T knee,
1453 Character charType) const noexcept
1454 {
1455 const T eOut = outDb - thresh;
1456 const T w = effectiveKneeFor(charType, knee);
1457 T r = std::max(effectiveRatioFor(charType, ratio, eOut), T(1));
1458 FeedbackLaw law = elementLaw(eOut, r, w);
1459 if (charType == Character::Varimu)
1460 {
1461 for (int pass = 0; pass < 2; ++pass)
1462 {
1463 r = std::max(effectiveRatioFor(charType, ratio, eOut - law.target), T(1));
1464 law = elementLaw(eOut, r, w);
1465 }
1466 }
1467 return law;
1468 }
1469
1472 {
1475 };
1476
1498 [[nodiscard]] FeedbackSolve solveFeedbackGain(T inDb, T k, T effB, T thresh,
1499 T ratio, T knee, Character charType) const noexcept
1500 {
1501 const T r = std::max(effectiveRatioFor(charType, ratio, inDb - thresh), T(1));
1502 const T w = effectiveKneeFor(charType, knee);
1503 const T halfW = w / T(2);
1504 const T a = r - T(1);
1505
1506 // Piece 1: at or below the knee start the law is zero and G = k.
1507 T e = inDb + k - thresh;
1508 if (e <= ((w > T(0)) ? -halfW : T(0)))
1509 return { k, T(0) };
1510
1511 // Piece 3: linear region, target = -a * e at the solved level. With a
1512 // hard knee this piece always resolves (e scales by 1/(1 + effB a)).
1513 const T g3 = (k - effB * a * (inDb - thresh)) / (T(1) + effB * a);
1514 e = inDb + g3 - thresh;
1515 if (e >= ((w > T(0)) ? halfW / r : T(0)))
1516 return { g3, -a * e };
1517
1518 // Piece 2: quadratic knee. Substituting G = k + effB * gel into the
1519 // knee relation gives q u^2 - u + (d + W/2) = 0 with
1520 // q = (1 - effB) s / (2W); the cancellation-free smaller root is used.
1521 const T s = a / r;
1522 const T q = (T(1) - effB) * s / (T(2) * w);
1523 const T b = (inDb - thresh + k) + halfW;
1524 const T disc = std::max(T(1) - T(4) * q * b, T(0));
1525 T u = T(2) * b / (T(1) + std::sqrt(disc));
1526 u = std::clamp(u, T(0), w);
1527 const T target = -s * u * u / (T(2) * w);
1528 return { k + effB * target, target };
1529 }
1530
1546 [[nodiscard]] T applyBallisticsFeedbackImplicit(T inDb, int ch, Character charType,
1547 T thresh, T ratio, T knee) noexcept
1548 {
1549 T& fast = envFastDb_[ch];
1550 T& slow = envSlowDb_[ch];
1551 const T wFast = charFastWeight_;
1552 const bool dual = wFast < T(1);
1553
1554 if (dual)
1555 {
1556 // The memory stage tracks the REAL sustained gain reduction (the
1557 // static curve at the current input), never the element's
1558 // internal target: the loop gain scales that one by up to
1559 // (R - 1), and a memory charging toward the amplified signal
1560 // overshoots, drags the settled blend off the curve and smears
1561 // the observed attack. Advancing it first keeps the solve below
1562 // consistent with the state it blends against.
1563 const T range = rangeDb_.load(std::memory_order_relaxed);
1564 const T slowTarget = std::clamp(
1565 computeGain(inDb, thresh, ratio, knee, charType, Mode::Downward, T(0)),
1566 -range, range);
1567 const bool charging = slowTarget < slow;
1568 slow = slowTarget + (charging ? charChargeCoeff_ : charSlowRelCoeff_) * (slow - slowTarget);
1569 if (std::abs(slow) < T(1e-4)) slow = T(0);
1570 }
1571
1572 // Branch on the direction the loop is about to move: attack while
1573 // the reduction deepens at the previous gain, release otherwise.
1574 const T shownPrev = dual ? wFast * fast + (T(1) - wFast) * slow : fast;
1575 const T probe = computeGainFeedback(inDb + shownPrev, thresh, ratio, knee, charType).target;
1576 const bool engaging = probe < fast;
1577 const T coeff = engaging ? charAttCoeff_ : charRelCoeff_;
1578 const T beta = T(1) - coeff;
1579
1580 const T k = dual ? wFast * coeff * fast + (T(1) - wFast) * slow : coeff * fast;
1581 const T effB = dual ? wFast * beta : beta;
1582 const auto sol = solveFeedbackGain(inDb, k, effB, thresh, ratio, knee, charType);
1583
1584 fast = sol.target + coeff * (fast - sol.target);
1585 if (std::abs(fast) < T(1e-4)) fast = T(0);
1586 T shown = dual ? wFast * fast + (T(1) - wFast) * slow : fast;
1587
1588 // Hold and range act on the OBSERVED gain: the element's internal
1589 // target is loop-amplified by up to (R - 1) during transients, so
1590 // clamping THAT to the user's range would throttle the attack, and
1591 // holding it would compare mismatched domains. When they override,
1592 // the fast stage is rewritten so the blend lands on the bound.
1593 const T bounded = applyHoldAndRange(shown, ch);
1594 if (bounded != shown)
1595 {
1596 shown = bounded;
1597 fast = dual ? (shown - (T(1) - wFast) * slow) / wFast : shown;
1598 }
1599 return shown;
1600 }
1601
1602 // ---- Ballistics (log domain) ----
1603
1626 [[nodiscard]] T applyBallistics(T targetGrDb, int ch, bool splitAdaptive,
1627 T loopSlope = T(0)) noexcept
1628 {
1629 // Level-adaptive release for the SplitPolarity detector: full-scale
1630 // output halves the release time constant. Halving tau squares the
1631 // one-pole coefficient, so blending between the two valid endpoints
1632 // keeps the modulation on the exponential curve.
1633 T attCoeff = charAttCoeff_;
1634 T relCoeff = charRelCoeff_;
1635 if (splitAdaptive)
1636 {
1637 const T outputLevel = std::min(std::abs(fbLastOutput_[ch]), T(1));
1638 relCoeff += outputLevel * (relCoeff * relCoeff - relCoeff);
1639 }
1640 if (loopSlope > T(0))
1641 {
1642 // Cap the per-sample loop advance at 0.5 (half-deadbeat): the
1643 // one-sample delay then converges monotonically instead of
1644 // sustaining a marginal dance on the detector's residual ripple.
1645 const T floorCoeff = T(1) - T(0.5) / (T(1) + loopSlope);
1646 attCoeff = std::max(attCoeff, floorCoeff);
1647 relCoeff = std::max(relCoeff, floorCoeff);
1648 }
1649
1650 // Attack acts while the gain FALLS (louder signal: deeper reduction,
1651 // or less upward boost); release acts while it recovers upward. A
1652 // magnitude comparison would invert the two in Upward mode and make
1653 // the envelope chase the huge boosts of the waveform's zero crossings.
1654 T& fast = envFastDb_[ch];
1655 const bool engaging = targetGrDb < fast;
1656 fast = targetGrDb + (engaging ? attCoeff : relCoeff) * (fast - targetGrDb);
1657 if (std::abs(fast) < T(1e-4)) fast = T(0); // kill the asymptotic dB tail
1658
1659 if (charFastWeight_ >= T(1)) // Clean / Varimu: single envelope
1660 return fast;
1661
1662 // Opto / FET memory stage: charges toward the target over the history
1663 // time constant and discharges with the slow release, so its level
1664 // encodes how long (and how deep) the compressor has been working.
1665 T& slow = envSlowDb_[ch];
1666 const bool charging = targetGrDb < slow;
1667 slow = targetGrDb + (charging ? charChargeCoeff_ : charSlowRelCoeff_) * (slow - targetGrDb);
1668 if (std::abs(slow) < T(1e-4)) slow = T(0);
1669
1670 return charFastWeight_ * fast + (T(1) - charFastWeight_) * slow;
1671 }
1672
1673 // ---- Sidechain HPF ----
1674
1677 {
1678 // The setter rejects invalid cutoffs; the clamp keeps the coefficient
1679 // in the stable range even against a hostile in-memory value.
1680 const double scFreq = std::clamp(
1681 static_cast<double>(scHpfFreq_.load(std::memory_order_relaxed)),
1682 1.0, sampleRate_ * 0.45);
1683 scHpfB1_ = static_cast<T>(std::exp(-std::numbers::pi * 2.0 * scFreq / sampleRate_));
1684 scHpfA0_ = (T(1) + scHpfB1_) / T(2); // Normalization to prevent high-frequency boost
1685 }
1686
1693 [[nodiscard]] T applySidechainHPF(T input, int ch) noexcept
1694 {
1695 T& xp = scHpfPrev_[ch];
1696 T& yp = scHpfState_[ch];
1697 T output = scHpfA0_ * (input - xp) + scHpfB1_ * yp;
1698 xp = input;
1699 yp = output;
1700 return output;
1701 }
1702
1703 // ---- RMS Configuration ----
1704
1706 void updateRmsWindow() noexcept
1707 {
1708 if (sampleRate_ > 0)
1709 {
1710 int requestedSamples = static_cast<int>(sampleRate_ * static_cast<double>(rmsWindowMs_) / 1000.0);
1711
1712 // Limit the logical window to the LIVE element count. capacity()
1713 // can exceed size() after a re-prepare to a lower rate, and a
1714 // window reaching into the dead tail reads stale squares from the
1715 // previous stream (measured: audible gain reduction on silence).
1716 if (!rmsBuffers_[0].empty())
1717 {
1718 int maxLen = static_cast<int>(rmsBuffers_[0].size());
1719 rmsWindowSamples_ = std::clamp(requestedSamples, 1, maxLen);
1720 }
1721 else
1722 {
1723 rmsWindowSamples_ = std::max(1, requestedSamples);
1724 }
1725
1726 for (int ch = 0; ch < kMaxChannels; ++ch)
1727 {
1728 // Clear the window contents too: stale squares from a previous
1729 // window length would otherwise be subtracted from the fresh
1730 // running sum (transient negative-sum glitch).
1731 std::fill(rmsBuffers_[ch].begin(), rmsBuffers_[ch].end(), T(0));
1732 rmsSums_[ch] = T(0);
1733 rmsIndices_[ch] = 0;
1734 rmsRecomputeCounters_[ch] = 0;
1735 }
1736 }
1737 }
1738
1739 // =========================================================================
1740 // Members & State
1741 // =========================================================================
1742
1744 double sampleRate_ = 0;
1745
1746 // User Parameters (Atomic for thread safety)
1747 std::atomic<T> threshold_ { T(-20) };
1748 std::atomic<T> ratio_ { T(4) };
1749 std::atomic<T> attackMs_ { T(5) };
1750 std::atomic<T> releaseMs_ { T(100) };
1751 std::atomic<T> kneeWidth_ { T(0) };
1752 std::atomic<T> makeupGain_ { T(0) };
1753 std::atomic<T> stereoLink_ { T(1) };
1754 std::atomic<T> mix_ { T(1) };
1755 std::atomic<T> lookaheadMs_ { T(0) };
1756 std::atomic<T> characterColor_ { T(0) };
1757 std::atomic<AutoMakeupMode> autoMakeupMode_ { AutoMakeupMode::Off };
1758
1759 std::atomic<DetectorType> detectorType_ { DetectorType::Peak };
1760 std::atomic<Topology> topology_ { Topology::FeedForward };
1761 std::atomic<Character> character_ { Character::Clean };
1762 std::atomic<Mode> mode_ { Mode::Downward };
1763
1764 // Internal DSP Coefficients & State
1765 T autoMakeupCoeff_ = T(0.9995);
1769
1770 // Character ballistics coefficients (dB-domain one-poles, see
1771 // updateTimeConstants). charFastWeight_ == 1 selects the single-envelope
1772 // path (Clean/Varimu); Opto/FET blend fast and slow memory envelopes.
1773 std::atomic<bool> timeConstantsDirty_ { true };
1774 T charAttCoeff_ = T(0);
1775 T charRelCoeff_ = T(0);
1779
1786
1787 // FET colour: 2nd-order term gain at full colour, calibrated so limiting
1788 // at -6 dBFS program level measures within the 1176's published THD spec
1789 // (< 0.5%); the one-pole DC estimate AC-couples the squared term.
1790 static constexpr T kFetColorH2 = T(0.028);
1791 T fetDcCoeff_ = T(0);
1792 std::array<T, kMaxChannels> fetDcState_ {};
1793
1794 std::array<T, kMaxChannels> envFastDb_ {};
1795 std::array<T, kMaxChannels> envSlowDb_ {};
1796 std::array<T, kMaxChannels> upwardGuardDb_ {};
1797 T upwardGuardDecay_ = T(0.00125);
1798 std::array<T, kMaxChannels> fbLastOutput_ {};
1799 std::array<T, kMaxChannels> channelLevelDb_ {};
1800
1801 std::array<RingBuffer<T>, kMaxChannels> lookaheadBuffers_ {};
1803
1804 std::array<HilbertIIR<double>, kMaxChannels> hilbertDetectors_ {};
1805
1806 // Sidechain Filtering
1807 std::atomic<bool> scHpfEnabled_ { false };
1808 std::atomic<T> scHpfFreq_ { T(80) };
1809 T scHpfB1_ = T(0);
1810 T scHpfA0_ = T(0);
1811 std::array<T, kMaxChannels> scHpfState_ {};
1812 std::array<T, kMaxChannels> scHpfPrev_ {};
1813
1814 // Split-Polarity Detector State
1815 std::array<T, kMaxChannels> splitPosEnv_ {};
1816 std::array<T, kMaxChannels> splitNegEnv_ {};
1818 T splitDetRelCoeff_ = T(0.99);
1819
1820 // RMS Detector
1821 T rmsWindowMs_ = T(10);
1822 std::atomic<T> rmsWindowMsAtomic_ { T(10) };
1823 std::atomic<bool> rmsWindowDirty_ { false };
1825 std::array<std::vector<T>, kMaxChannels> rmsBuffers_;
1826 std::array<T, kMaxChannels> rmsSums_ {};
1827 std::array<int, kMaxChannels> rmsIndices_ {};
1828 static constexpr int kRmsRecomputePeriod = 4096;
1829 std::array<int, kMaxChannels> rmsRecomputeCounters_ {};
1830
1831 // Shared ITU-R BS.1770-4 true-peak detector (Core/TruePeakDetector.h).
1833
1834 // Hold & Range
1835 std::atomic<T> holdMs_ { T(0) };
1836 std::atomic<T> rangeDb_ { T(100) };
1838 std::array<int, kMaxChannels> holdCounters_ {};
1839 std::array<T, kMaxChannels> heldGrDb_ {};
1840
1841 std::atomic<T> gainReductionDb_ { T(0) };
1842};
1843
1844} // 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
Separate envelopes for the positive and negative half-waves.
@ 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:498
void setHoldTime(T ms) noexcept
Sets the gain-reduction hold time.
Definition Compressor.h:632
std::array< RingBuffer< T >, kMaxChannels > lookaheadBuffers_
Lookahead delay lines.
void prepare(const AudioSpec &spec)
Allocates buffers and initializes internal DSP state.
Definition Compressor.h:197
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:732
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:741
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:128
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:540
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:654
void setMakeupGain(T dB) noexcept
Sets manual static makeup gain in dB.
Definition Compressor.h:555
std::atomic< Character > character_
Selected ballistics.
T getCharacterColor() const noexcept
Returns the character colour amount (see setCharacterColor).
Definition Compressor.h:689
void setAutoMakeup(AutoMakeupMode mode) noexcept
Selects the automatic makeup behavior (default: Off).
Definition Compressor.h:571
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).
T cachedMakeupLin_
Its linear gain.
SmoothedValue< T > mixSmooth_
De-zippered parallel mix.
void setLookahead(T ms) noexcept
Sets lookahead time in ms (0 = off, max = 10ms).
Definition Compressor.h:609
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:617
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:578
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:310
Topology
Signal routing topology for the detector sidechain.
Definition Compressor.h:111
@ FeedForward
Detector reads uncompressed input (modern, precise, transparent).
void setRmsWindow(T ms) noexcept
Sets the RMS analysis window size in milliseconds.
Definition Compressor.h:720
std::array< T, kMaxChannels > splitNegEnv_
Negative half-wave tracking.
T makeupToGain(T makeupDb) noexcept
Makeup dB to linear, cached: the makeup is shared by every channel of a frame and constant unless aut...
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:599
void reset() noexcept
Resets all internal DSP history, states, and buffers to neutral.
Definition Compressor.h:452
void setRatio(T ratio) noexcept
Sets the compression ratio (1.0 = off, >20.0 = limiting).
Definition Compressor.h:505
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:704
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:548
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:682
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:662
void prepare(double sampleRate) noexcept
Prepares the compressor using sample rate only (backward compatibility).
Definition Compressor.h:256
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:647
std::array< HilbertIIR< double >, kMaxChannels > hilbertDetectors_
Zero-latency analytic pairs for detection.
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:738
T cachedMakeupDb_
Last makeup converted by makeupToGain().
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:170
@ Off
Manual makeup only (setMakeupGain).
int getLatency() const noexcept
Returns total processing latency in samples.
Definition Compressor.h:751
DetectorType getDetector() const noexcept
Returns the currently active detector type.
Definition Compressor.h:735
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:266
std::atomic< T > kneeWidth_
Knee width in dB.
void setMode(Mode mode) noexcept
Sets processing mode (Downward or Upward compression).
Definition Compressor.h:584
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:280
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:592
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:524
int rmsWindowSamples_
Active RMS length in samples.
SmoothedValue< T > colorSmooth_
De-zippered character colour amount.
Mode
Processing direction.
Definition Compressor.h:160
@ 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:74
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.
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: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
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:71
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45
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.