DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
Saturation.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
38#include "../Core/AudioBuffer.h"
39#include "../Core/AudioSpec.h"
40#include "../Core/Biquad.h"
41#include "../Core/DryWetMixer.h"
42#include "../Core/DspMath.h"
43#include "../Core/Oversampling.h"
44#include "../Core/Smoothers.h"
45#include "../Core/AnalogRandom.h"
46#include "../Core/SpscQueue.h"
47#include "../Core/SpinLock.h"
48#include "../Core/StateBlob.h"
49#include "DCBlocker.h"
50#include "MidSide.h"
51
52#include <algorithm>
53#include <array>
54#include <atomic>
55#include <cmath>
56#include <cstdint>
57#include <cstring>
58#include <limits>
59#include <memory>
60#include <type_traits>
61#include <vector>
62
63namespace dspark {
64
65template <typename SampleType> class Saturation;
66
67namespace detail {
68
70template <typename T>
71inline T logCosh(T x) noexcept
72{
73 T a = std::abs(x);
74 return a + std::log1p(std::exp(T(-2) * a)) - T(0.6931471805599453); // ln 2
75}
76
77template <typename T>
79{
80public:
81 static constexpr int kAaCh = 16;
82
83 virtual ~SaturationAlgorithm() = default;
84
86 virtual void prepare(const AudioSpec& spec) noexcept = 0;
87
89 virtual void reset() noexcept = 0;
90
92 virtual void update(T /*driveGain*/, T /*character*/, const AudioSpec& /*spec*/) noexcept {}
93
95 virtual typename Saturation<T>::Algorithm getType() const noexcept = 0;
96
103 void setAntialias(bool on) noexcept { antialias_.store(on, std::memory_order_relaxed); }
104
105protected:
106 std::atomic<bool> antialias_ { false };
107};
108
109// -- SoftClip (tanh) ---------------------------------------------------------
110template <typename T>
111class TanhAlgorithm final : public SaturationAlgorithm<T>
112{
113 std::array<T, SaturationAlgorithm<T>::kAaCh> prevX_ {};
114public:
115 void prepare(const AudioSpec&) noexcept override { reset(); }
116 void reset() noexcept override { prevX_.fill(T(0)); }
117 typename Saturation<T>::Algorithm getType() const noexcept override { return Saturation<T>::Algorithm::SoftClip; }
118
119 inline T processSample(T sample, T drive, T character, int ch) noexcept
120 {
121 T x = sample * drive;
122 T bias = character * T(0.3);
123 if (!this->antialias_.load(std::memory_order_relaxed))
124 return fastTanh(x + bias) - fastTanh(bias);
125
126 // ADAA of f(x) = tanh(x+bias) - tanh(bias); antiderivative log(cosh(x+bias)) - tanh(bias)*x.
127 int c = ch & (SaturationAlgorithm<T>::kAaCh - 1);
128 T x0 = prevX_[c];
129 prevX_[c] = x;
130 T tb = std::tanh(bias);
131 T dx = x - x0;
132 if (std::abs(dx) > T(1e-5))
133 return (logCosh(x + bias) - logCosh(x0 + bias)) / dx - tb;
134 return std::tanh(T(0.5) * (x + x0) + bias) - tb;
135 }
136};
137
138// -- Tube (12AX7-style asymmetric triode model) -----------------------------
139template <typename T>
140class TubeAlgorithm final : public SaturationAlgorithm<T>
141{
142 std::array<T, SaturationAlgorithm<T>::kAaCh> prevX_ {};
143
144 static inline T f1(T x, T asym) noexcept
145 {
146 return (x >= T(0)) ? logCosh(x) : logCosh(x * asym) / asym;
147 }
148public:
149 void prepare(const AudioSpec&) noexcept override { reset(); }
150 void reset() noexcept override { prevX_.fill(T(0)); }
151 typename Saturation<T>::Algorithm getType() const noexcept override { return Saturation<T>::Algorithm::Tube; }
152
153 inline T processSample(T sample, T drive, T character, int ch) noexcept
154 {
155 T x = sample * drive;
156 T asym = T(1.15) + character * T(0.5);
157 if (!this->antialias_.load(std::memory_order_relaxed))
158 return (x >= T(0)) ? fastTanh(x) : fastTanh(x * asym);
159
160 // ADAA of the asymmetric triode curve; antiderivative is piecewise log(cosh).
161 int c = ch & (SaturationAlgorithm<T>::kAaCh - 1);
162 T x0 = prevX_[c];
163 prevX_[c] = x;
164 T dx = x - x0;
165 if (std::abs(dx) > T(1e-5))
166 return (f1(x, asym) - f1(x0, asym)) / dx;
167 T m = T(0.5) * (x + x0);
168 return (m >= T(0)) ? std::tanh(m) : std::tanh(m * asym);
169 }
170};
171
172// -- HardClip ----------------------------------------------------------------
173template <typename T>
175{
176 std::array<T, SaturationAlgorithm<T>::kAaCh> prevX_ {};
177
179 static inline T g(T u) noexcept
180 {
181 T a = std::abs(u);
182 return (a <= T(1)) ? T(0.5) * u * u : a - T(0.5);
183 }
184public:
185 void prepare(const AudioSpec&) noexcept override { reset(); }
186 void reset() noexcept override { prevX_.fill(T(0)); }
187 typename Saturation<T>::Algorithm getType() const noexcept override { return Saturation<T>::Algorithm::HardClip; }
188
189 inline T processSample(T sample, T drive, T character, int ch) noexcept
190 {
191 T bias = character * T(0.3);
192 T cb = std::clamp(bias, T(-1), T(1));
193 T d = sample * drive;
194 if (!this->antialias_.load(std::memory_order_relaxed))
195 return std::clamp(d + bias, T(-1), T(1)) - cb;
196
197 // ADAA of the hard clip; antiderivative is the piecewise quadratic g().
198 int c = ch & (SaturationAlgorithm<T>::kAaCh - 1);
199 T d0 = prevX_[c];
200 prevX_[c] = d;
201 T u = d + bias, u0 = d0 + bias;
202 if (u >= T(-1) && u <= T(1) && u0 >= T(-1) && u0 <= T(1))
203 return u - cb; // linear body (below the clip point): pass through, no averaging (no HF roll-off)
204 T dd = d - d0;
205 if (std::abs(dd) > T(1e-5))
206 return (g(u) - g(u0)) / dd - cb;
207 return std::clamp(T(0.5) * (d + d0) + bias, T(-1), T(1)) - cb;
208 }
209};
210
211// -- Exciter (polynomial waveshaper) -----------------------------------------
212template <typename T>
214{
215public:
216 void prepare(const AudioSpec&) noexcept override {}
217 void reset() noexcept override {}
218 typename Saturation<T>::Algorithm getType() const noexcept override { return Saturation<T>::Algorithm::Exciter; }
219
220 inline T processSample(T sample, T drive, T character, int) noexcept
221 {
222 T x = std::clamp(sample * drive, T(-10), T(10));
223 T x2 = x * x;
224 T x3 = x2 * x;
225 T result = x + character * T(0.25) * x2 - T(0.15) * x3;
226 return std::clamp(result, T(-1), T(1));
227 }
228};
229
230// -- Wavefolder (sin + first-order ADAA) -------------------------------------
231template <typename T>
233{
234 static constexpr int kMaxCh = 16;
235 std::array<T, kMaxCh> lastX_ {};
236 std::array<T, kMaxCh> lastF_ {};
237
238public:
239 void prepare(const AudioSpec&) noexcept override { reset(); }
240 void reset() noexcept override
241 {
242 lastX_.fill(T(0));
243 lastF_.fill(T(-1));
244 }
245 typename Saturation<T>::Algorithm getType() const noexcept override { return Saturation<T>::Algorithm::Wavefolder; }
246
258 inline T processSample(T sample, T drive, T character, int ch) noexcept
259 {
260 const T bias = character * (pi<T> / T(4));
261 const T x = sample * drive + bias;
262 const T sb = std::sin(bias);
263 T F_x = -std::cos(x);
264 T diff = x - lastX_[ch];
265
266 T result;
267 if (std::abs(diff) > T(1e-5))
268 result = (F_x - lastF_[ch]) / diff - sb; // exact: mean of sin minus the constant
269 else
270 result = std::sin(x) - sb;
271
272 lastX_[ch] = x;
273 lastF_[ch] = F_x;
274 return result;
275 }
276};
277
278// -- Bitcrusher (TPDF dither) ------------------------------------------------
279template <typename T>
281{
282 // Per-sample TPDF dither needs a true white PRNG. (The previous
283 // AnalogRandom-based source was a 1 Hz sample-and-hold: two consecutive
284 // reads were almost always identical, so the dither was effectively zero
285 // and the crusher truncated with audible quantisation distortion.)
286 uint32_t rngState_ = 0x9E3779B9u;
287 T steps_ = T(1);
288 T invSteps_ = T(1);
289
290 [[nodiscard]] inline T nextRandom() noexcept
291 {
292 rngState_ ^= rngState_ << 13;
293 rngState_ ^= rngState_ >> 17;
294 rngState_ ^= rngState_ << 5;
295 constexpr T scale = T(1) / static_cast<T>(0xFFFFFFFFu);
296 return static_cast<T>(rngState_) * scale - T(0.5);
297 }
298
299public:
300 void prepare(const AudioSpec&) noexcept override
301 {
302 if (rngState_ == 0) rngState_ = 1; // xorshift fixed point guard
303 }
304 void reset() noexcept override {}
305 typename Saturation<T>::Algorithm getType() const noexcept override { return Saturation<T>::Algorithm::Bitcrusher; }
306
307 void update(T drive, T /*character*/, const AudioSpec&) noexcept override
308 {
309 T clamped = std::clamp(drive, T(1), T(100));
310 T bitDepth = mapRange(clamped, T(1), T(100), T(16), T(2));
311 steps_ = std::pow(T(2), bitDepth);
312 invSteps_ = T(1) / steps_;
313 }
314
315 inline T processSample(T sample, T, T, int) noexcept
316 {
317 // True TPDF: difference of two independent uniforms, +-1 LSB peak.
318 T dither = (nextRandom() - nextRandom()) * invSteps_;
319 return invSteps_ * std::round((sample + dither) * steps_);
320 }
321};
322
323// -- Tape (anhysteretic Langevin magnetisation + head bump + HF rolloff) -----
324template <typename T>
325class TapeAlgorithm final : public SaturationAlgorithm<T>
326{
327 static constexpr int kMaxCh = 16;
328 std::array<Biquad<T, 1>, kMaxCh> preFilters_;
329 std::array<Biquad<T, 1>, kMaxCh> postFilters_;
330 std::array<T, kMaxCh> M_ {};
331 int numChannels_ = 0;
332 T lastDrive_ = T(-1);
333 double lastSampleRate_ = 0.0;
334
338 static inline T langevin(T x) noexcept
339 {
340 const T ax = std::abs(x);
341 if (ax < T(0.5))
342 {
343 const T x2 = x * x;
344 return x * (T(1) / T(3) - x2 * (T(1) / T(45) - x2 * (T(2) / T(945))));
345 }
346 if (ax > T(20)) return std::copysign(T(1), x) - T(1) / x;
347 return T(1) / std::tanh(x) - T(1) / x;
348 }
349
351 static inline T langevinDeriv(T x) noexcept
352 {
353 const T ax = std::abs(x);
354 if (ax < T(0.5))
355 {
356 const T x2 = x * x;
357 return T(1) / T(3) - x2 * (T(1) / T(15) - x2 * (T(2) / T(189)));
358 }
359 if (ax > T(20)) return T(1) / (x * x);
360 const T s = std::sinh(x);
361 return T(1) / (x * x) - T(1) / (s * s);
362 }
363
364public:
365 void prepare(const AudioSpec& spec) noexcept override
366 {
367 numChannels_ = std::min(spec.numChannels, kMaxCh); // clamp per-channel state
368 // Invalidate the filter-design cache: a re-prepare can widen the
369 // channel count at an unchanged rate/drive, and the new channels'
370 // filters must still receive coefficients on the next update().
371 lastDrive_ = T(-1);
372 lastSampleRate_ = 0.0;
373 reset();
374 }
375 void reset() noexcept override
376 {
377 for (auto& f : preFilters_) f.reset();
378 for (auto& f : postFilters_) f.reset();
379 M_.fill(T(0));
380 }
381 typename Saturation<T>::Algorithm getType() const noexcept override { return Saturation<T>::Algorithm::Tape; }
382
383 void update(T drive, T /*character*/, const AudioSpec& spec) noexcept override
384 {
385 // Both filters depend only on drive and sample rate; skip the
386 // trig-heavy redesign when neither changed since the last block.
387 if (drive == lastDrive_ && spec.sampleRate == lastSampleRate_) return;
388 lastDrive_ = drive;
389 lastSampleRate_ = spec.sampleRate;
390
391 auto driveDb = gainToDecibels(drive, T(-100));
392 T bumpGain = T(1.5) + std::min(driveDb * T(0.05), T(3.0));
393 auto peakCoeffs = BiquadCoeffs::makePeak(spec.sampleRate, 80.0, 0.6, static_cast<double>(bumpGain));
394 auto lpFreq = std::max(6000.0, 19000.0 - static_cast<double>(driveDb) * 200.0);
395 auto lpCoeffs = BiquadCoeffs::makeLowPass(spec.sampleRate, lpFreq, 0.55);
396
397 for (int ch = 0; ch < numChannels_; ++ch)
398 {
399 preFilters_[ch].setCoeffs(peakCoeffs);
400 postFilters_[ch].setCoeffs(lpCoeffs);
401 }
402 }
403
419 inline T processSample(T sample, T drive, T character, int ch) noexcept
420 {
421 const T filtered = preFilters_[ch].processSample(sample, 0);
422 const T H = filtered * drive;
423
424 // character (clamped [-1,1] upstream) sets the mean-field coupling:
425 // harder knee and more mid-level bloom as alpha rises.
426 const T alpha = T(0.35) + T(0.15) * character;
427 const T a = T(1) / (T(3) * (T(1) - alpha));
428 const T Ms = T(3) * a;
429
430 T M = M_[ch];
431 for (int it = 0; it < 3; ++it)
432 {
433 const T x = (H + alpha * M) / a;
434 const T f = M - Ms * langevin(x);
435 const T fp = T(1) - T(3) * alpha * langevinDeriv(x);
436 M -= f / fp;
437 }
438 M = std::clamp(M, -Ms, Ms);
439 M_[ch] = M;
440
441 return postFilters_[ch].processSample((T(1) - alpha) * M, 0);
442 }
443};
444
445// -- Transformer (frequency-dependent: heavy LF, light HF) ------------------
446template <typename T>
448{
449 static constexpr int kMaxCh = 16;
450 std::array<Biquad<T, 1>, kMaxCh> lpFilters_;
451 int numChannels_ = 0;
452 double lastSampleRate_ = 0.0;
453
454public:
455 void prepare(const AudioSpec& spec) noexcept override
456 {
457 numChannels_ = std::min(spec.numChannels, kMaxCh); // clamp per-channel state
458 lastSampleRate_ = 0.0; // force a redesign so channels added by a re-prepare get coefficients
459 reset();
460 }
461 void reset() noexcept override
462 {
463 for (auto& f : lpFilters_) f.reset();
464 }
465 typename Saturation<T>::Algorithm getType() const noexcept override { return Saturation<T>::Algorithm::Transformer; }
466
467 void update(T /*drive*/, T /*character*/, const AudioSpec& spec) noexcept override
468 {
469 // The crossover is fixed at 250 Hz: only recompute when the effective
470 // sample rate changes (update() runs every block from the pipeline).
471 if (spec.sampleRate == lastSampleRate_) return;
472 lastSampleRate_ = spec.sampleRate;
473
474 auto c = BiquadCoeffs::makeLowPass(spec.sampleRate, 250.0, 0.707);
475 for (int ch = 0; ch < numChannels_; ++ch)
476 lpFilters_[ch].setCoeffs(c);
477 }
478
489 inline T processSample(T sample, T drive, T character, int ch) noexcept
490 {
491 const T low = lpFilters_[ch].processSample(sample, 0);
492 const T high = sample - low;
493 const T bias = character * T(0.2);
494 const T kLo = drive * T(1.4);
495 const T kHi = drive * T(0.85);
496 const T satLow = (fastTanh((low + bias) * kLo) - fastTanh(bias * kLo)) / T(1.4);
497 const T satHigh = (fastTanh((high + bias) * kHi) - fastTanh(bias * kHi)) / T(0.85);
498 return satLow + satHigh;
499 }
500};
501
502// -- Downsample (sample rate reduction) --------------------------------------
503template <typename T>
505{
506 static constexpr int kMaxCh = 16;
507 std::array<Biquad<T, 1>, kMaxCh> aaFilters_;
508 std::array<T, kMaxCh> lastSample_ {};
509 std::array<int, kMaxCh> counter_ {};
510 int numChannels_ = 0;
511 int reduction_ = 1;
512
513public:
514 void prepare(const AudioSpec& spec) noexcept override
515 {
516 numChannels_ = std::min(spec.numChannels, kMaxCh); // clamp per-channel state
517 reset();
518 }
519 void reset() noexcept override
520 {
521 for (auto& f : aaFilters_) f.reset();
522 lastSample_.fill(T(0));
523 counter_.fill(0);
524 }
525 typename Saturation<T>::Algorithm getType() const noexcept override { return Saturation<T>::Algorithm::Downsample; }
526
527 void update(T drive, T, const AudioSpec& spec) noexcept override
528 {
529 T clamped = std::clamp(drive, T(1), T(100));
530 reduction_ = std::max(1, static_cast<int>(mapRange(clamped, T(1), T(100), T(1), T(50))));
531
532 auto c = BiquadCoeffs::makeLowPass(spec.sampleRate, spec.sampleRate / (2.5 * reduction_), 0.707);
533 for (int ch = 0; ch < numChannels_; ++ch)
534 aaFilters_[ch].setCoeffs(c);
535 }
536
537 inline T processSample(T sample, T, T, int ch) noexcept
538 {
539 T filtered = aaFilters_[ch].processSample(sample, 0);
540 if (++counter_[ch] >= reduction_)
541 {
542 counter_[ch] = 0;
543 lastSample_[ch] = filtered;
544 }
545 return lastSample_[ch];
546 }
547};
548
549// -- MultiStage (Tube -> Tape -> Transformer cascade) -------------------------
550template <typename T>
552{
553 TubeAlgorithm<T> tube_;
554 TapeAlgorithm<T> tape_;
556
557public:
558 void prepare(const AudioSpec& spec) noexcept override
559 {
560 tube_.prepare(spec);
561 tape_.prepare(spec);
562 xfmr_.prepare(spec);
563 }
564 void reset() noexcept override { tube_.reset(); tape_.reset(); xfmr_.reset(); }
565 typename Saturation<T>::Algorithm getType() const noexcept override { return Saturation<T>::Algorithm::MultiStage; }
566
567 void update(T drive, T character, const AudioSpec& spec) noexcept override
568 {
569 // The stages are private instances (not the pool's), so the owner's
570 // setAntialiasing() never reaches them directly: forward the flag to
571 // the one memoryless stage that implements ADAA.
572 tube_.setAntialias(this->antialias_.load(std::memory_order_relaxed));
573 tape_.update(drive * T(0.6), character, spec);
574 xfmr_.update(drive * T(0.8), character, spec);
575 }
576
577 inline T processSample(T sample, T drive, T character, int ch) noexcept
578 {
579 // Inter-stage makeup undoes each stage's fixed small-signal factor
580 // (1/0.5, 1/0.6) so the cascade is gain-staged: roughly transparent
581 // at neutral drive instead of a flat -15 dB drop, and every stage
582 // keeps receiving a healthy level.
583 T tubeOut = tube_.processSample(sample, drive * T(0.5), character, ch) * T(2);
584 T tapeOut = tape_.processSample(tubeOut, drive * T(0.6), character, ch) * (T(1) / T(0.6));
585 return xfmr_.processSample(tapeOut, drive * T(0.8), character, ch);
586 }
587};
588
589} // namespace detail
590
591// ============================================================================
592// Saturation - Public API
593// ============================================================================
594
610template <typename SampleType>
612{
613 static_assert(std::is_floating_point_v<SampleType>,
614 "Saturation: SampleType must be float or double.");
615
616public:
617 // -- Enums ---------------------------------------------------------------
618
622 enum class Algorithm
623 {
624 Tube,
625 Tape,
627 SoftClip,
628 HardClip,
629 Exciter,
630 Wavefolder,
631 Bitcrusher,
632 Downsample,
634 };
635
638
640 enum class OutputMode { Normal, WetOnly, Delta };
641
642 // -- Lifecycle -----------------------------------------------------------
643
645 {
646 pool_[0] = std::make_unique<detail::TubeAlgorithm<SampleType>>();
647 pool_[1] = std::make_unique<detail::TapeAlgorithm<SampleType>>();
648 pool_[2] = std::make_unique<detail::TransformerAlgorithm<SampleType>>();
649 pool_[3] = std::make_unique<detail::TanhAlgorithm<SampleType>>();
650 pool_[4] = std::make_unique<detail::HardClipAlgorithm<SampleType>>();
651 pool_[5] = std::make_unique<detail::ExciterAlgorithm<SampleType>>();
652 pool_[6] = std::make_unique<detail::WavefolderAlgorithm<SampleType>>();
653 pool_[7] = std::make_unique<detail::BitcrusherAlgorithm<SampleType>>();
654 pool_[8] = std::make_unique<detail::DownsampleAlgorithm<SampleType>>();
655 pool_[9] = std::make_unique<detail::MultiStageAlgorithm<SampleType>>();
656
657 active_.store(pool_[static_cast<int>(Algorithm::SoftClip)].get());
658 next_.store(nullptr);
659 }
660
661 ~Saturation() = default;
662 Saturation(const Saturation&) = delete;
663 Saturation& operator=(const Saturation&) = delete;
664
676 void prepare(const AudioSpec& spec)
677 {
678 if (!spec.isValid()) return;
679 spec_ = spec;
680 for (auto& algo : pool_)
681 if (algo) algo->prepare(spec);
682
685 // Order 2 at 5 Hz: the same Butterworth design this stage always used,
686 // now run in the double core (see DCBlocker.h). Prepared for the full
687 // channel range so it covers every channel the pipeline touches, not
688 // just the first eight.
691
692 lastPreHpFreq_ = -1.0f;
693 lastPostTiltFreq_ = -1.0f;
694 lastPostTiltGain_ = std::numeric_limits<float>::quiet_NaN();
695 dryWetMixer_.prepare(spec);
696
697 if (oversampler_) oversampler_->prepare(spec);
698
702
703 // Keep the dry path aligned with the (latent) oversampled wet path so the
704 // dry/wet, Delta and adaptive-blend mixes do not comb-filter.
706 (oversampler_ && oversamplingFactor_ > 1) ? oversampler_->getLatency() : 0);
707
708 auto sr = spec.sampleRate;
709 driveSmoother_.reset(sr, 20.0f, 0.707f, 0.0f);
710 mixSmoother_.reset(sr, 20.0f, 1.0f);
711 characterSmoother_.reset(sr, 20.0f, 0.0f);
712 driftSmoother_.reset(sr, 500.0f, 0.0f);
713 preHpSmoother_.reset(sr, 30.0f, 0.707f, 20.0f);
714 postTiltFreqSmoother_.reset(sr, 30.0f, 0.707f, 1000.0f);
715 postTiltGainSmoother_.reset(sr, 30.0f, 0.0f);
716 outputGainSmoother_.reset(sr, 20.0f, 0.0f);
717 crossfader_.reset(sr, 10.0f, 1.0f);
718
721 leftDrift_.reseed(0x9E3779B97F4A7C15ULL);
722 rightDrift_.reseed(0xBF58476D1CE4E5B9ULL);
723 // Smooth the random drift targets (~100 ms): without smoothing the
724 // generators step once per second, modulating the drive in audible
725 // jumps instead of an analog-style slow wander.
726 leftDrift_.setSmoothing(true, SampleType(100));
727 rightDrift_.setSmoothing(true, SampleType(100));
728
729 reset();
730 prepared_ = true;
731 }
732
739 void reset() noexcept
740 {
741 // Complete any pending algorithm switch instantly: every algorithm's
742 // state is cleared below anyway, and after a reset the most recently
743 // requested algorithm must be the one playing. (Leaving next_ armed
744 // made a later request of that same algorithm a silent no-op.)
745 if (auto* pending = next_.load())
746 {
747 active_.store(pending);
748 next_.store(nullptr);
749 }
750
751 for (auto& algo : pool_)
752 if (algo) algo->reset();
753
758 if (oversampler_) oversampler_->reset();
759
769
770 prevBlendSample_.fill(SampleType(0));
771 prevSlewSample_.fill(SampleType(0));
772 }
773
774 // -- Audio Processing ----------------------------------------------------
775
780 void processBlock(AudioBufferView<SampleType> buffer) noexcept { if (!prepared_) return; process(buffer); }
781
795 {
796 if (!prepared_) return;
798
799 dryWetMixer_.pushDry(buffer);
800
801 // Pre-filter
802 {
803 const int numSamples = buffer.getNumSamples();
804 constexpr int kCoefRefresh = 16;
805 for (int i = 0; i < numSamples; i += kCoefRefresh)
806 {
807 const int chunk = std::min(kCoefRefresh, numSamples - i);
808 float curFreq = lastPreHpFreq_;
809 for (int k = 0; k < chunk; ++k)
810 curFreq = preHpSmoother_.getNextValue();
811
812 if (curFreq != lastPreHpFreq_)
813 {
814 auto c = BiquadCoeffs::makeHighPass(spec_.sampleRate, static_cast<double>(curFreq));
816 lastPreHpFreq_ = curFreq;
817 }
818 auto subView = buffer.getSubView(i, chunk);
819 preFilter_.processBlock(subView);
820 }
821 }
822
823 const bool isMidSide = (procMode_ == ProcessingMode::MidOnly || procMode_ == ProcessingMode::SideOnly || procMode_ == ProcessingMode::MidSide) && buffer.getNumChannels() == 2;
824 if (isMidSide) MidSide<SampleType>::encode(buffer);
825
826 // MidOnly / SideOnly: snapshot the channel that must stay UNPROCESSED
827 // and restore it after the saturation pipeline. The snapshot lives in
828 // the SAME domain the pipeline runs in (oversampled when OS is active),
829 // so the restored channel shares the wet path's half-band round-trip
830 // and group delay - perfectly time-aligned with the processed channel.
831 // (The old in-pipeline routing read the DryWetMixer's base-rate L/R
832 // capture from inside the oversampled loop: wrong domain AND an
833 // out-of-bounds read, caught by AddressSanitizer.)
834 const int keepChannel =
835 (isMidSide && procMode_ == ProcessingMode::MidOnly) ? 1 :
836 (isMidSide && procMode_ == ProcessingMode::SideOnly) ? 0 : -1;
837
839 {
840 auto upView = oversampler_->upsample(buffer);
841
842 const int upSamples = std::min(upView.getNumSamples(), msKeepBuffer_.getNumSamples());
843 if (keepChannel >= 0 && keepChannel < upView.getNumChannels() && upSamples > 0)
844 std::memcpy(msKeepBuffer_.getChannel(0), upView.getChannel(keepChannel),
845 static_cast<std::size_t>(upSamples) * sizeof(SampleType));
846
848
849 if (keepChannel >= 0 && keepChannel < upView.getNumChannels() && upSamples > 0)
850 std::memcpy(upView.getChannel(keepChannel), msKeepBuffer_.getChannel(0),
851 static_cast<std::size_t>(upSamples) * sizeof(SampleType));
852
853 oversampler_->downsample(buffer);
854 }
855 else
856 {
857 const int baseSamples = std::min(buffer.getNumSamples(), msKeepBuffer_.getNumSamples());
858 if (keepChannel >= 0 && keepChannel < buffer.getNumChannels() && baseSamples > 0)
859 std::memcpy(msKeepBuffer_.getChannel(0), buffer.getChannel(keepChannel),
860 static_cast<std::size_t>(baseSamples) * sizeof(SampleType));
861
863
864 if (keepChannel >= 0 && keepChannel < buffer.getNumChannels() && baseSamples > 0)
865 std::memcpy(buffer.getChannel(keepChannel), msKeepBuffer_.getChannel(0),
866 static_cast<std::size_t>(baseSamples) * sizeof(SampleType));
867 }
868
869 if (isMidSide) MidSide<SampleType>::decode(buffer);
870
871 // Program-dependent adaptive blend, applied at BASE rate in the L/R
872 // domain where the latency-compensated dry capture is valid.
873 if (adaptiveBlend_.load(std::memory_order_relaxed))
874 applyAdaptiveBlend(buffer);
875
876 // Post-filter
877 {
878 const int numSamples = buffer.getNumSamples();
879 constexpr int kCoefRefresh = 16;
880 for (int i = 0; i < numSamples; i += kCoefRefresh)
881 {
882 const int chunk = std::min(kCoefRefresh, numSamples - i);
883 float curFreq = lastPostTiltFreq_;
884 float curGain = lastPostTiltGain_;
885 for (int k = 0; k < chunk; ++k)
886 {
889 }
890
891 if (curFreq != lastPostTiltFreq_ || curGain != lastPostTiltGain_)
892 {
893 // A genuine tilt shelf, as the setter documents. (This used
894 // to be makePeak: a bell AT the pivot, which mid-boosted
895 // instead of brightening/darkening.)
896 auto c = BiquadCoeffs::makeTilt(spec_.sampleRate, static_cast<double>(curFreq), static_cast<double>(curGain));
898 lastPostTiltFreq_ = curFreq;
899 lastPostTiltGain_ = curGain;
900 }
901 auto subView = buffer.getSubView(i, chunk);
902 postFilter_.processBlock(subView);
903 }
904 }
905
907 applyOutputGain(buffer);
908
909 // Mix Output
911 else if (outputMode_ == OutputMode::Delta)
912 {
913 const int nCh = std::min(buffer.getNumChannels(), dryWetMixer_.getDryNumChannels());
914 const int nS = std::min(buffer.getNumSamples(), dryWetMixer_.getDryCapturedSamples());
915 for (int ch = 0; ch < nCh; ++ch)
916 {
917 SampleType* wet = buffer.getChannel(ch);
918 const SampleType* dry = dryWetMixer_.getDryChannel(ch);
919 for (int i = 0; i < nS; ++i) wet[i] -= dry[i];
920 }
921 }
922 else
923 {
924 dryWetMixer_.mixWet(buffer, static_cast<SampleType>(mixSmoother_.getTargetValue()));
925 }
926 }
927
928 // -- Thread-Safe Setters (GUI / Automation Thread) -----------------------
929
935 void setAlgorithm(Algorithm algo) { pushParam([&](auto& p){ p.algorithm = algo; }); }
936
942 void setDrive(SampleType dB) { pushParam([&](auto& p){ p.driveDb = dB; }); }
943
949 void setMix(SampleType mix01) { pushParam([&](auto& p){ p.mix = mix01; }); }
950
963 void setCharacter(SampleType c) { pushParam([&](auto& p){ p.character = c; }); }
964
970 void setProcessingMode(ProcessingMode m){ pushParam([&](auto& p){ p.processingMode = m; }); }
971
977 void setOutputMode(OutputMode m) { pushParam([&](auto& p){ p.outputMode = m; }); }
978
984 void setAnalogDrift(SampleType i) { pushParam([&](auto& p){ p.analogDrift = i; }); }
985
991 void setPreFilterHpFrequency(SampleType hz) { pushParam([&](auto& p){ p.preFilterHpFreq = hz; }); }
992
998 void setOutputGain(SampleType dB) { pushParam([&](auto& p){ p.outputGain = dB; }); }
999
1005 void setDcBlocking(bool on) { pushParam([&](auto& p){ p.dcBlocking = on; }); }
1006
1012 void setAdaptiveBlend(bool on) noexcept { adaptiveBlend_.store(on, std::memory_order_relaxed); }
1013
1025 void setAntialiasing(bool on) noexcept
1026 {
1027 antialiasShadow_.store(on, std::memory_order_relaxed); // serialization readback
1028 for (auto& a : pool_) if (a) a->setAntialias(on);
1029 }
1030
1037 void setSlewSensitivity(SampleType amount) noexcept
1038 {
1039 if (!std::isfinite(amount)) return;
1040 slewSensitivity_.store(std::clamp(amount, SampleType(0), SampleType(1)), std::memory_order_relaxed);
1041 }
1042
1054 void setPostFilterTilt(SampleType centerHz, SampleType amountDb)
1055 {
1056 pushParam([&](auto& p){ p.postFilterTiltFreq = centerHz; p.postFilterTiltGain = amountDb; });
1057 }
1058
1067 void setOversampling(int factor)
1068 {
1069 if (factor < 1 || (factor & (factor - 1)) != 0) return;
1070 oversamplingFactor_ = factor;
1071 if (factor > 1)
1072 {
1073 oversampler_ = std::make_unique<Oversampling<SampleType>>(factor);
1074 if (spec_.sampleRate > 0) oversampler_->prepare(spec_);
1075 }
1076 else
1077 oversampler_.reset();
1078
1079 // If prepare() already ran, grow the scratch buffers to hold the
1080 // upsampled block and realign the dry path; otherwise prepare() does it.
1081 if (prepared_)
1082 {
1083 const int upBlock = spec_.maxBlockSize * std::max(1, oversamplingFactor_);
1084 if (tempBuffer_.getNumSamples() < upBlock)
1086 if (driftBuffer_.getNumSamples() < upBlock)
1088 if (msKeepBuffer_.getNumSamples() < upBlock)
1089 msKeepBuffer_.resize(1, upBlock);
1090
1092 (oversampler_ && oversamplingFactor_ > 1) ? oversampler_->getLatency() : 0);
1093 }
1094 }
1095
1096 // -- Thread-Safe Getters (GUI / Metering) --------------------------------
1097
1102 [[nodiscard]] int getOversamplingFactor() const noexcept { return oversamplingFactor_; }
1103
1109 [[nodiscard]] int getLatencySamples() const noexcept
1110 {
1111 return (oversampler_ && oversamplingFactor_ > 1) ? oversampler_->getLatency() : 0;
1112 }
1113
1116 [[nodiscard]] int getLatency() const noexcept { return getLatencySamples(); }
1117
1123 [[nodiscard]] Algorithm getCurrentAlgorithm() const noexcept { return currentAlgoType_.load(std::memory_order_relaxed); }
1124
1130 [[nodiscard]] SampleType getGainReductionDb() const noexcept { return gainReductionDb_.load(std::memory_order_relaxed); }
1131
1132
1134 [[nodiscard]] std::vector<uint8_t> getState() const
1135 {
1136 Params p;
1137 {
1139 p = lastParams_;
1140 }
1141 StateWriter w(stateId("SATU"), 1);
1142 w.write("algorithm", static_cast<int32_t>(p.algorithm));
1143 w.write("procMode", static_cast<int32_t>(p.processingMode));
1144 w.write("outMode", static_cast<int32_t>(p.outputMode));
1145 w.write("drive", static_cast<float>(p.driveDb));
1146 w.write("mix", static_cast<float>(p.mix));
1147 w.write("character", static_cast<float>(p.character));
1148 w.write("drift", static_cast<float>(p.analogDrift));
1149 w.write("preHp", static_cast<float>(p.preFilterHpFreq));
1150 w.write("tiltFreq", static_cast<float>(p.postFilterTiltFreq));
1151 w.write("tiltGain", static_cast<float>(p.postFilterTiltGain));
1152 w.write("outputGain", static_cast<float>(p.outputGain));
1153 w.write("dcBlocking", p.dcBlocking);
1154 w.write("antialias", antialiasShadow_.load(std::memory_order_relaxed));
1155 w.write("adaptiveBlend", adaptiveBlend_.load(std::memory_order_relaxed));
1156 w.write("slewSens", slewSensitivity_.load(std::memory_order_relaxed));
1157 w.write("oversampling", oversamplingFactor_);
1158 return w.blob();
1159 }
1160
1164 bool setState(const uint8_t* data, size_t size)
1165 {
1166 StateReader r(data, size);
1167 if (!r.isValid() || r.processorId() != stateId("SATU")) return false;
1168 setAlgorithm(static_cast<Algorithm>(r.read("algorithm", 0)));
1169 setProcessingMode(static_cast<ProcessingMode>(r.read("procMode", 0)));
1170 setOutputMode(static_cast<OutputMode>(r.read("outMode", 0)));
1171 setDrive(static_cast<SampleType>(r.read("drive", 0.0f)));
1172 setMix(static_cast<SampleType>(r.read("mix", 1.0f)));
1173 setCharacter(static_cast<SampleType>(r.read("character", 0.0f)));
1174 setAnalogDrift(static_cast<SampleType>(r.read("drift", 0.0f)));
1175 setPreFilterHpFrequency(static_cast<SampleType>(r.read("preHp", 20.0f)));
1176 setPostFilterTilt(static_cast<SampleType>(r.read("tiltFreq", 1000.0f)),
1177 static_cast<SampleType>(r.read("tiltGain", 0.0f)));
1178 setOutputGain(static_cast<SampleType>(r.read("outputGain", 0.0f)));
1179 setDcBlocking(r.read("dcBlocking", true));
1180 setAntialiasing(r.read("antialias", false));
1181 setAdaptiveBlend(r.read("adaptiveBlend", false));
1182 setSlewSensitivity(static_cast<SampleType>(r.read("slewSens", 0.0f)));
1183 setOversampling(r.read("oversampling", 1));
1184 return true;
1185 }
1186
1187protected:
1188 struct Params
1189 {
1193 SampleType driveDb = SampleType(0);
1194 SampleType mix = SampleType(1);
1195 SampleType character = SampleType(0);
1196 SampleType analogDrift = SampleType(0);
1197 SampleType preFilterHpFreq = SampleType(20);
1198 SampleType postFilterTiltFreq = SampleType(1000);
1199 SampleType postFilterTiltGain = SampleType(0);
1200 SampleType outputGain = SampleType(0);
1201 bool dcBlocking = true;
1202 };
1203
1210 static void sanitizeParams(Params& p, const Params& prev) noexcept
1211 {
1212 const auto keepFinite = [](SampleType& v, SampleType old) noexcept
1213 {
1214 if (!std::isfinite(v)) v = old;
1215 };
1216 keepFinite(p.driveDb, prev.driveDb);
1217 keepFinite(p.mix, prev.mix);
1218 keepFinite(p.character, prev.character);
1219 keepFinite(p.analogDrift, prev.analogDrift);
1220 keepFinite(p.preFilterHpFreq, prev.preFilterHpFreq);
1221 keepFinite(p.postFilterTiltFreq, prev.postFilterTiltFreq);
1222 keepFinite(p.postFilterTiltGain, prev.postFilterTiltGain);
1223 keepFinite(p.outputGain, prev.outputGain);
1224
1225 const auto clampEnum = [](auto& e, int hi) noexcept
1226 {
1227 using E = std::remove_reference_t<decltype(e)>;
1228 e = static_cast<E>(std::clamp(static_cast<int>(e), 0, hi));
1229 };
1230 clampEnum(p.algorithm, kNumAlgorithms - 1);
1231 clampEnum(p.processingMode, static_cast<int>(ProcessingMode::MidSide));
1232 clampEnum(p.outputMode, static_cast<int>(OutputMode::Delta));
1233 }
1234
1235 template <typename Fn>
1236 void pushParam(Fn&& mutate)
1237 {
1239 const Params prev = lastParams_;
1240 mutate(lastParams_);
1242 if (!paramQueue_.push(lastParams_))
1243 {
1244 // Queue full (a burst of GUI edits within one audio block).
1245 // lastParams_ already holds the complete state, so flag the audio
1246 // thread to fetch it directly; without this the newest edit would
1247 // silently wait for the NEXT parameter change to become audible.
1248 paramsPending_.store(true, std::memory_order_release);
1249 }
1250 }
1251
1253 {
1254 Params p;
1255 while (paramQueue_.pop(p))
1257
1258 // Recover an edit that could not be queued. lastParams_ is always the
1259 // newest complete state, so applying it can never move backwards.
1260 if (paramsPending_.exchange(false, std::memory_order_acquire))
1261 {
1263 if (guard.isLocked())
1265 else
1266 paramsPending_.store(true, std::memory_order_release); // retry next block
1267 }
1268 }
1269
1271 {
1272 driveSmoother_.setTargetValue(std::clamp(static_cast<float>(p.driveDb), -24.0f, 48.0f));
1273 mixSmoother_.setTargetValue(std::clamp(static_cast<float>(p.mix), 0.0f, 1.0f));
1274 characterSmoother_.setTargetValue(std::clamp(static_cast<float>(p.character), -1.0f, 1.0f));
1275 driftSmoother_.setTargetValue(std::clamp(static_cast<float>(p.analogDrift), 0.0f, 1.0f));
1276 outputGainSmoother_.setTargetValue(static_cast<float>(p.outputGain));
1277
1278 float nyquist = static_cast<float>(spec_.sampleRate) / 2.0f;
1279 preHpSmoother_.setTargetValue(std::clamp(static_cast<float>(p.preFilterHpFreq), 10.0f, nyquist));
1280 postTiltFreqSmoother_.setTargetValue(std::clamp(static_cast<float>(p.postFilterTiltFreq), 100.0f, nyquist));
1281 postTiltGainSmoother_.setTargetValue(std::clamp(static_cast<float>(p.postFilterTiltGain), -12.0f, 12.0f));
1282
1286 currentAlgoType_.store(p.algorithm, std::memory_order_relaxed);
1287
1288 // Algorithm switching state machine. The crossfader fades active_ out
1289 // towards next_ (1 -> 0); processSaturationPipeline resolves the end
1290 // of the fade by its final value (0 = promote next_, 1 = discard it).
1291 auto* requested = pool_[static_cast<int>(p.algorithm)].get();
1292 auto* act = active_.load();
1293 auto* nxt = next_.load();
1294 if (requested == act)
1295 {
1296 // Return to the algorithm still playing: steer any running fade
1297 // back to 1 so the pending algorithm is discarded when it lands.
1298 // (Ignoring this case made a quick B-then-back-to-A edit complete
1299 // the fade to B: the wrong algorithm played forever while
1300 // getCurrentAlgorithm() reported A.)
1301 if (nxt != nullptr)
1302 {
1304 if (!crossfader_.isSmoothing())
1305 {
1306 // The fade had not moved yet (still at 1): discard now, or
1307 // the un-smoothing crossfader would leave nxt armed and a
1308 // later reset() would wrongly promote it.
1309 nxt->reset();
1310 next_.store(nullptr);
1311 }
1312 }
1313 }
1314 else if (requested == nxt)
1315 {
1316 // Re-request of the incoming algorithm (e.g. after a revert
1317 // started): make sure the fade heads towards it again.
1319 }
1320 else
1321 {
1322 // New target. It has not been audible for a while, so clear its
1323 // state: stale filter/ADAA history would leak an old-signal
1324 // transient into the fade-in. If a fade was already running, its
1325 // pending algorithm is simply replaced (one fade-weighted step,
1326 // bounded like a hard switch; the abandoned one is re-cleared
1327 // here whenever it is next requested).
1328 requested->reset();
1329 next_.store(requested);
1331 }
1332 }
1333
1334 // Data-Oriented Pipeline to eliminate per-sample virtual dispatch
1336 {
1337 auto* primary = active_.load();
1338 auto* secondary = next_.load();
1339 bool xfading = secondary != nullptr && crossfader_.isSmoothing();
1340
1341 // Clamp to per-channel state capacity (prevSlewSample_/prevBlendSample_
1342 // are kMaxCh) AND to the prepared channel count: driftBuffer_ and
1343 // tempBuffer_ only hold spec_.numChannels channels, so a caller view
1344 // with more channels used to read/write past them (out of bounds in
1345 // release) as soon as drift or an algorithm crossfade was active.
1346 // Channels beyond the prepared spec now bypass the saturation stage.
1347 const int nCh = std::min({ buffer.getNumChannels(), kMaxCh, spec_.numChannels });
1348 const int nS = buffer.getNumSamples();
1349
1350 auto driveDbTarget = static_cast<SampleType>(driveSmoother_.getTargetValue());
1351 auto driveGainTarget = decibelsToGain(driveDbTarget);
1352 auto characterTarget = static_cast<SampleType>(characterSmoother_.getTargetValue());
1353
1354 AudioSpec updateSpec = spec_;
1355 if (oversamplingFactor_ > 1) updateSpec.sampleRate *= oversamplingFactor_;
1356
1357 if (primary) primary->update(driveGainTarget, characterTarget, updateSpec);
1358 if (secondary) secondary->update(driveGainTarget, characterTarget, updateSpec);
1359
1360 SampleType peakInOriginal = SampleType(0);
1361 for (int ch = 0; ch < nCh; ++ch) {
1362 const SampleType* d = buffer.getChannel(ch);
1363 for (int i = 0; i < nS; ++i) peakInOriginal = std::max(peakInOriginal, std::abs(d[i]));
1364 }
1365
1366 // 1. Slew (In-place)
1367 auto slewAmt = slewSensitivity_.load(std::memory_order_relaxed);
1368 if (slewAmt > SampleType(0))
1369 {
1370 for (int ch = 0; ch < nCh; ++ch) {
1371 SampleType* data = buffer.getChannel(ch);
1372 for (int i = 0; i < nS; ++i) {
1373 SampleType dry = data[i];
1374 SampleType delta = dry - prevSlewSample_[ch];
1375 data[i] += std::tanh(std::abs(delta)) * slewAmt * dry;
1376 prevSlewSample_[ch] = dry;
1377 }
1378 }
1379 }
1380
1381 // 2. Pre-generate Drift
1382 auto driftIntensity = driftSmoother_.getTargetValue();
1383 bool useDrift = driftIntensity > 0.01f;
1384 if (useDrift)
1385 {
1386 auto driftView = driftBuffer_.toView();
1387 for (int i = 0; i < nS; ++i) {
1388 auto driftS = driftSmoother_.getNextValue();
1389 // Exactly one draw per generator per sample (stereo-identical
1390 // to drawing inside the channel loop). Channels beyond the
1391 // second share the right generator's value; drawing per
1392 // channel instead advanced its clock nCh-1 times per sample,
1393 // speeding the wander up with the channel count.
1394 const SampleType noiseL = leftDrift_.getNextSample();
1395 const SampleType noiseR = (nCh > 1) ? rightDrift_.getNextSample() : SampleType(0);
1396 for (int ch = 0; ch < nCh; ++ch) {
1397 driftView.getChannel(ch)[i] =
1398 SampleType(1) + static_cast<SampleType>(driftS) * (ch == 0 ? noiseL : noiseR);
1399 }
1400 }
1401 }
1402
1403 // 3. Primary Saturation (and secondary if xfade)
1404 if (xfading)
1405 {
1406 auto tempView = tempBuffer_.toView().getSubView(0, nS);
1407 for (int ch = 0; ch < nCh; ++ch)
1408 std::memcpy(tempView.getChannel(ch), buffer.getChannel(ch), static_cast<std::size_t>(nS) * sizeof(SampleType));
1409
1410 dispatchSaturator(primary, buffer, useDrift);
1411 dispatchSaturator(secondary, tempView, useDrift);
1412
1413 for (int i = 0; i < nS; ++i) {
1414 auto fade = static_cast<SampleType>(crossfader_.getNextValue());
1415 for (int ch = 0; ch < nCh; ++ch) {
1416 SampleType* out = buffer.getChannel(ch);
1417 const SampleType* alt = tempView.getChannel(ch);
1418 out[i] = out[i] * fade + alt[i] * (SampleType(1) - fade);
1419 }
1420 }
1421
1422 if (!crossfader_.isSmoothing()) {
1423 if (crossfader_.getCurrentValue() < 0.5f) {
1424 // Fade ran to 0: the pending algorithm takes over.
1425 active_.store(secondary);
1426 next_.store(nullptr);
1427 if (primary) primary->reset();
1428 } else {
1429 // Reverted fade landed back on the active algorithm:
1430 // discard the pending one.
1431 secondary->reset();
1432 next_.store(nullptr);
1433 }
1435 }
1436 }
1437 else
1438 {
1439 dispatchSaturator(primary, buffer, useDrift);
1440 }
1441
1442 // (Adaptive blend and the MidOnly/SideOnly channel passthrough run at
1443 // BASE rate in process() - see the snapshot/restore logic there.)
1444
1445 // Gain Reduction Tracking
1446 SampleType peakOut = SampleType(0);
1447 for (int ch = 0; ch < nCh; ++ch) {
1448 const SampleType* d = buffer.getChannel(ch);
1449 for (int i = 0; i < nS; ++i) peakOut = std::max(peakOut, std::abs(d[i]));
1450 }
1451
1452 SampleType peakInDriven = peakInOriginal * driveGainTarget;
1453 if (peakInDriven > SampleType(1e-6)) {
1454 SampleType ratio = std::min(peakOut / peakInDriven, SampleType(1));
1455 gainReductionDb_.store(gainToDecibels(ratio, SampleType(-100)), std::memory_order_relaxed);
1456 } else {
1457 gainReductionDb_.store(SampleType(0), std::memory_order_relaxed);
1458 }
1459 }
1460
1465 {
1466 const int nCh = std::min({ buffer.getNumChannels(),
1468 const int nS = std::min(buffer.getNumSamples(),
1470
1471 for (int ch = 0; ch < nCh; ++ch)
1472 {
1473 SampleType* wetData = buffer.getChannel(ch);
1474 const SampleType* dryData = dryWetMixer_.getDryChannel(ch);
1475 for (int i = 0; i < nS; ++i)
1476 {
1477 SampleType dry = dryData[i];
1478 SampleType wet = wetData[i];
1479 SampleType avg = (std::abs(prevBlendSample_[ch]) + std::abs(dry)) * SampleType(0.5);
1480 SampleType apply = std::clamp(avg, SampleType(0), SampleType(1));
1481 wetData[i] = dry * (SampleType(1) - apply) + wet * apply;
1482 prevBlendSample_[ch] = dry;
1483 }
1484 }
1485 }
1486
1487 // Compile-time resolver (Zero virtual dispatch in hot path)
1489 {
1490 if (!baseAlgo) return;
1491 switch (baseAlgo->getType())
1492 {
1493 case Algorithm::Tube: processCore(static_cast<detail::TubeAlgorithm<SampleType>*>(baseAlgo), buffer, useDrift); break;
1494 case Algorithm::Tape: processCore(static_cast<detail::TapeAlgorithm<SampleType>*>(baseAlgo), buffer, useDrift); break;
1495 case Algorithm::Transformer: processCore(static_cast<detail::TransformerAlgorithm<SampleType>*>(baseAlgo), buffer, useDrift); break;
1496 case Algorithm::SoftClip: processCore(static_cast<detail::TanhAlgorithm<SampleType>*>(baseAlgo), buffer, useDrift); break;
1497 case Algorithm::HardClip: processCore(static_cast<detail::HardClipAlgorithm<SampleType>*>(baseAlgo), buffer, useDrift); break;
1498 case Algorithm::Exciter: processCore(static_cast<detail::ExciterAlgorithm<SampleType>*>(baseAlgo), buffer, useDrift); break;
1499 case Algorithm::Wavefolder: processCore(static_cast<detail::WavefolderAlgorithm<SampleType>*>(baseAlgo), buffer, useDrift); break;
1500 case Algorithm::Bitcrusher: processCore(static_cast<detail::BitcrusherAlgorithm<SampleType>*>(baseAlgo), buffer, useDrift); break;
1501 case Algorithm::Downsample: processCore(static_cast<detail::DownsampleAlgorithm<SampleType>*>(baseAlgo), buffer, useDrift); break;
1502 case Algorithm::MultiStage: processCore(static_cast<detail::MultiStageAlgorithm<SampleType>*>(baseAlgo), buffer, useDrift); break;
1503 }
1504 }
1505
1506 template <typename ExactAlgo>
1507 void processCore(ExactAlgo* algo, AudioBufferView<SampleType> buffer, bool useDrift) noexcept
1508 {
1509 // Same channel clamp as processSaturationPipeline: per-channel algorithm
1510 // state is bounded to kMaxCh and driftBuffer_ to spec_.numChannels.
1511 const int nCh = std::min({ buffer.getNumChannels(), kMaxCh, spec_.numChannels });
1512 const int nS = buffer.getNumSamples();
1513 auto driftView = driftBuffer_.toView();
1514
1515 for (int i = 0; i < nS; ++i)
1516 {
1517 auto driveGainS = decibelsToGain(static_cast<SampleType>(driveSmoother_.getNextValue()));
1518 auto charS = static_cast<SampleType>(characterSmoother_.getNextValue());
1519
1520 for (int ch = 0; ch < nCh; ++ch)
1521 {
1522 SampleType drift = useDrift ? driftView.getChannel(ch)[i] : SampleType(1);
1523 SampleType* data = buffer.getChannel(ch);
1524 data[i] = algo->processSample(data[i], driveGainS * drift, charS * drift, ch);
1525 }
1526 }
1527 }
1528
1530 {
1531 auto targetGainDb = outputGainSmoother_.getTargetValue();
1532 if (std::abs(targetGainDb - outputGainSmoother_.getCurrentValue()) < 0.001f)
1533 {
1534 buffer.applyGain(decibelsToGain(static_cast<SampleType>(targetGainDb)));
1535 }
1536 else
1537 {
1538 const int nCh = buffer.getNumChannels();
1539 const int nS = buffer.getNumSamples();
1540 for (int i = 0; i < nS; ++i)
1541 {
1542 auto gain = decibelsToGain(static_cast<SampleType>(outputGainSmoother_.getNextValue()));
1543 for (int ch = 0; ch < nCh; ++ch) buffer.getChannel(ch)[i] *= gain;
1544 }
1545 }
1546 }
1547
1549 bool prepared_ = false;
1550
1551 static constexpr int kNumAlgorithms = 10;
1552 std::array<std::unique_ptr<detail::SaturationAlgorithm<SampleType>>, kNumAlgorithms> pool_;
1553 std::atomic<detail::SaturationAlgorithm<SampleType>*> active_ { nullptr };
1554 std::atomic<detail::SaturationAlgorithm<SampleType>*> next_ { nullptr };
1555
1559 std::atomic<bool> paramsPending_ { false }; // set when a full queue dropped a snapshot
1560 std::atomic<bool> antialiasShadow_ { false };
1561
1565 std::atomic<Algorithm> currentAlgoType_ { Algorithm::SoftClip };
1566
1571
1573 // A dedicated DCBlocker, not a Biquad: a 5 Hz high-pass keeps its poles
1574 // 1e-4 from the unit circle, where a float biquad recursion cannot hold
1575 // the zero at DC (it degenerates into an integrator of its own rounding
1576 // error above ~96 kHz and sources the very offset it is here to remove).
1579
1581
1585
1586 std::unique_ptr<Oversampling<SampleType>> oversampler_;
1588
1589 std::atomic<SampleType> gainReductionDb_ { SampleType(0) };
1590
1591 std::atomic<bool> adaptiveBlend_ { false };
1592 static constexpr int kMaxCh = 16;
1593 std::array<SampleType, kMaxCh> prevBlendSample_ {};
1594
1595 std::atomic<SampleType> slewSensitivity_ { SampleType(0) };
1596 std::array<SampleType, kMaxCh> prevSlewSample_ {};
1597
1598 float lastPreHpFreq_ = -1.0f;
1599 float lastPostTiltFreq_ = -1.0f;
1600 float lastPostTiltGain_ = std::numeric_limits<float>::quiet_NaN();
1601};
1602
1603} // namespace dspark
Removes DC offset from audio signals with configurable filter order.
Mid/Side stereo encoding and decoding for real-time audio.
Main generator class for analog-style random modulation.
void setSmoothing(bool shouldBeEnabled, Real timeInMs=static_cast< Real >(50.0)) noexcept
void reseed(std::uint64_t newSeed) noexcept
Request a lock-free reseed of the internal PRNG.
void prepare(double sampleRate) noexcept
Prepare the generator with the audio sample rate.
Real getNextSample() noexcept
Generate and return the next modulation sample.
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Owning audio buffer with contiguous, 32-byte aligned storage.
AudioBufferView< T, MaxChannels > toView() noexcept
Returns a non-owning mutable view of this buffer. The view's channel capacity is propagated from MaxC...
T * getChannel(int ch) noexcept
Returns a pointer to the sample data.
int getNumSamples() const noexcept
Returns the number of samples per channel.
void resize(int numChannels, int numSamples)
Allocates the buffer for the given dimensions.
Biquad filter using Transposed Direct Form II (TDF-II) with thread-safe updates.
Definition Biquad.h:556
void setCoeffs(const BiquadCoeffs &c) noexcept
Sets the filter coefficients asynchronously.
Definition Biquad.h:599
void reset() noexcept
Resets all per-channel filter states to zero to avoid ringing/clicks.
Definition Biquad.h:668
void processBlock(AudioBufferView< T > buffer) noexcept
Processes a full audio buffer in-place.
Definition Biquad.h:750
DC blocking filter with configurable Butterworth order (1-10).
Definition DCBlocker.h:87
void prepare(double sampleRate, int numChannels=2, double cutoffHz=-1.0)
Prepares the DC blocker, resetting internal states and precalculating coefficients.
Definition DCBlocker.h:109
void setOrder(int order) noexcept
Sets the filter order (1-10). Thread-safe.
Definition DCBlocker.h:144
void reset() noexcept
Clears the internal history states to zero.
Definition DCBlocker.h:275
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an AudioBufferView in-place.
Definition DCBlocker.h:186
Pre-allocated, SIMD-friendly dry/wet blender for real-time audio.
Definition DryWetMixer.h:72
void setLatencyCompensation(int samples)
Delays the captured dry signal to compensate for an effect's internal latency (e.g....
int getDryCapturedSamples() const noexcept
Returns the number of samples valid from the last pushDry() call.
void mixWet(AudioBufferView< T > wetBuffer, T targetMix) noexcept
Blends the stored dry signal with the current (wet) buffer in-place.
void reset() noexcept
Resets the internal buffer and smoothing states to zero.
int getDryNumChannels() const noexcept
Returns the internal capacity of channels in the dry buffer.
void pushDry(const AudioBufferView< const T > &input) noexcept
Captures a snapshot of the dry (unprocessed) signal.
void prepare(const AudioSpec &spec)
Allocates the internal dry buffer for the given audio spec.
const T * getDryChannel(int ch) const noexcept
Retrieves a read-only pointer to the captured dry channel data.
Professional multi-algorithm saturation processor with analog simulation.
Definition Saturation.h:612
ProcessingMode procMode_
void pushParam(Fn &&mutate)
void dispatchSaturator(detail::SaturationAlgorithm< SampleType > *baseAlgo, AudioBufferView< SampleType > buffer, bool useDrift) noexcept
Smoothers::StateVariableSmoother driveSmoother_
std::atomic< SampleType > gainReductionDb_
void setProcessingMode(ProcessingMode m)
Sets the routing configuration for multi-channel processing.
Definition Saturation.h:970
void setMix(SampleType mix01)
Sets the global Dry/Wet blend.
Definition Saturation.h:949
std::atomic< bool > adaptiveBlend_
std::atomic< SampleType > slewSensitivity_
std::atomic< bool > paramsPending_
AnalogRandom::Generator< SampleType > rightDrift_
void setAlgorithm(Algorithm algo)
Sets the saturation algorithm topology.
Definition Saturation.h:935
void setOversampling(int factor)
Configures internal polyphase oversampling to reduce aliasing.
void applyOutputGain(AudioBufferView< SampleType > buffer) noexcept
AudioBuffer< SampleType > msKeepBuffer_
MidOnly/SideOnly channel snapshot.
void setCharacter(SampleType c)
Adjusts the specific character/bias of the selected algorithm.
Definition Saturation.h:963
std::unique_ptr< Oversampling< SampleType > > oversampler_
std::atomic< detail::SaturationAlgorithm< SampleType > * > active_
std::atomic< detail::SaturationAlgorithm< SampleType > * > next_
void setOutputGain(SampleType dB)
Sets the post-saturation make-up or trim gain.
Definition Saturation.h:998
std::atomic< Algorithm > currentAlgoType_
std::array< SampleType, kMaxCh > prevSlewSample_
static constexpr int kNumAlgorithms
SpscQueue< Params > paramQueue_
AudioBuffer< SampleType > tempBuffer_
Smoothers::LinearSmoother crossfader_
DryWetMixer< SampleType > dryWetMixer_
Smoothers::LinearSmoother mixSmoother_
AudioBuffer< SampleType > driftBuffer_
Biquad< SampleType > preFilter_
Smoothers::StateVariableSmoother postTiltFreqSmoother_
SampleType getGainReductionDb() const noexcept
Calculates the peak gain reduction (clipping amount) for metering.
OutputMode outputMode_
void processSaturationPipeline(AudioBufferView< SampleType > buffer) noexcept
void setAntialiasing(bool on) noexcept
Enables antiderivative anti-aliasing (ADAA) on the memoryless curves (SoftClip / Tube / HardClip),...
Algorithm
Defines the harmonic generation topology.
Definition Saturation.h:623
Smoothers::StateVariableSmoother preHpSmoother_
std::atomic< bool > antialiasShadow_
Mirror for getState.
void setPreFilterHpFrequency(SampleType hz)
Configures a pre-saturation high-pass filter.
Definition Saturation.h:991
void reset() noexcept
Clears all internal states, phase memory, and history buffers.
Definition Saturation.h:739
void setAdaptiveBlend(bool on) noexcept
Enables program-dependent saturation density.
int getOversamplingFactor() const noexcept
Retrieves the current oversampling factor.
~Saturation()=default
OutputMode
Determines the final output signal routing.
Definition Saturation.h:640
void applyAdaptiveBlend(AudioBufferView< SampleType > buffer) noexcept
Program-dependent dry/wet density blend (base rate, L/R domain). The dry reference is the DryWetMixer...
void applyParamSnapshot(const Params &p)
void processCore(ExactAlgo *algo, AudioBufferView< SampleType > buffer, bool useDrift) noexcept
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (setup/UI threads: it forwards the stored oversampling factor to setO...
void process(AudioBufferView< SampleType > buffer) noexcept
The core audio processing pipeline.
Definition Saturation.h:794
void setOutputMode(OutputMode m)
Sets the output signal path.
Definition Saturation.h:977
Smoothers::LinearSmoother postTiltGainSmoother_
ProcessingMode
Determines how the stereo field is processed.
Definition Saturation.h:637
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
static constexpr int kMaxCh
void processBlock(AudioBufferView< SampleType > buffer) noexcept
Processes an audio block in-place (AudioProcessor standard contract).
Definition Saturation.h:780
int getLatency() const noexcept
Alias of getLatencySamples() following the framework-wide latency reporting name, so ProcessorChain::...
DCBlocker< SampleType > dcBlocker_
AnalogRandom::Generator< SampleType > leftDrift_
Smoothers::LinearSmoother driftSmoother_
Smoothers::LinearSmoother characterSmoother_
void setPostFilterTilt(SampleType centerHz, SampleType amountDb)
Configures a post-saturation first-order tilt EQ.
Saturation & operator=(const Saturation &)=delete
void handleParameterChanges()
void setDcBlocking(bool on)
Enables or disables the fixed 10Hz DC Blocker.
int getLatencySamples() const noexcept
Reports the processor's algorithmic latency in samples.
Smoothers::LinearSmoother outputGainSmoother_
void prepare(const AudioSpec &spec)
Prepares all internal resources, filters, and buffers.
Definition Saturation.h:676
std::array< std::unique_ptr< detail::SaturationAlgorithm< SampleType > >, kNumAlgorithms > pool_
void setAnalogDrift(SampleType i)
Injects true-stereo pseudo-random low-frequency modulation (drift) into the saturation drive.
Definition Saturation.h:984
void setDrive(SampleType dB)
Sets the input drive gain.
Definition Saturation.h:942
void setSlewSensitivity(SampleType amount) noexcept
Sets a derivative-based (slew rate) saturation multiplier.
std::array< SampleType, kMaxCh > prevBlendSample_
Biquad< SampleType > postFilter_
static void sanitizeParams(Params &p, const Params &prev) noexcept
Saturation(const Saturation &)=delete
Algorithm getCurrentAlgorithm() const noexcept
Retrieves the currently active underlying algorithm.
RAII wrapper that acquires the lock on construction and releases on destruction.
Definition SpinLock.h:136
RAII wrapper that tries to acquire the lock without blocking.
Definition SpinLock.h:162
bool isLocked() const noexcept
Queries whether the lock acquisition was successful.
Definition SpinLock.h:178
A minimal, real-time safe spin lock with a TTAS wait loop.
Definition SpinLock.h:70
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
void reset() noexcept override
Resets internal states (filters, phase, memory).
Definition Saturation.h:304
void update(T drive, T, const AudioSpec &) noexcept override
Updates internal coefficients dependent on block-rate parameters.
Definition Saturation.h:307
Saturation< T >::Algorithm getType() const noexcept override
Identifies the exact algorithm type for CRTP static dispatch.
Definition Saturation.h:305
T processSample(T sample, T, T, int) noexcept
Definition Saturation.h:315
void prepare(const AudioSpec &) noexcept override
Prepares the algorithm with the current audio specification.
Definition Saturation.h:300
T processSample(T sample, T, T, int ch) noexcept
Definition Saturation.h:537
void reset() noexcept override
Resets internal states (filters, phase, memory).
Definition Saturation.h:519
void update(T drive, T, const AudioSpec &spec) noexcept override
Updates internal coefficients dependent on block-rate parameters.
Definition Saturation.h:527
void prepare(const AudioSpec &spec) noexcept override
Prepares the algorithm with the current audio specification.
Definition Saturation.h:514
Saturation< T >::Algorithm getType() const noexcept override
Identifies the exact algorithm type for CRTP static dispatch.
Definition Saturation.h:525
T processSample(T sample, T drive, T character, int) noexcept
Definition Saturation.h:220
Saturation< T >::Algorithm getType() const noexcept override
Identifies the exact algorithm type for CRTP static dispatch.
Definition Saturation.h:218
void reset() noexcept override
Resets internal states (filters, phase, memory).
Definition Saturation.h:217
void prepare(const AudioSpec &) noexcept override
Prepares the algorithm with the current audio specification.
Definition Saturation.h:216
T processSample(T sample, T drive, T character, int ch) noexcept
Definition Saturation.h:189
void reset() noexcept override
Resets internal states (filters, phase, memory).
Definition Saturation.h:186
Saturation< T >::Algorithm getType() const noexcept override
Identifies the exact algorithm type for CRTP static dispatch.
Definition Saturation.h:187
void prepare(const AudioSpec &) noexcept override
Prepares the algorithm with the current audio specification.
Definition Saturation.h:185
void reset() noexcept override
Resets internal states (filters, phase, memory).
Definition Saturation.h:564
void prepare(const AudioSpec &spec) noexcept override
Prepares the algorithm with the current audio specification.
Definition Saturation.h:558
Saturation< T >::Algorithm getType() const noexcept override
Identifies the exact algorithm type for CRTP static dispatch.
Definition Saturation.h:565
T processSample(T sample, T drive, T character, int ch) noexcept
Definition Saturation.h:577
void update(T drive, T character, const AudioSpec &spec) noexcept override
Updates internal coefficients dependent on block-rate parameters.
Definition Saturation.h:567
virtual void reset() noexcept=0
Resets internal states (filters, phase, memory).
virtual void update(T, T, const AudioSpec &) noexcept
Updates internal coefficients dependent on block-rate parameters.
Definition Saturation.h:92
virtual Saturation< T >::Algorithm getType() const noexcept=0
Identifies the exact algorithm type for CRTP static dispatch.
virtual void prepare(const AudioSpec &spec) noexcept=0
Prepares the algorithm with the current audio specification.
virtual ~SaturationAlgorithm()=default
void setAntialias(bool on) noexcept
Enables 1st-order antiderivative anti-aliasing (ADAA) on the memoryless curves (Tanh/Tube/HardClip)....
Definition Saturation.h:103
Saturation< T >::Algorithm getType() const noexcept override
Identifies the exact algorithm type for CRTP static dispatch.
Definition Saturation.h:117
void reset() noexcept override
Resets internal states (filters, phase, memory).
Definition Saturation.h:116
T processSample(T sample, T drive, T character, int ch) noexcept
Definition Saturation.h:119
void prepare(const AudioSpec &) noexcept override
Prepares the algorithm with the current audio specification.
Definition Saturation.h:115
void reset() noexcept override
Resets internal states (filters, phase, memory).
Definition Saturation.h:375
void prepare(const AudioSpec &spec) noexcept override
Prepares the algorithm with the current audio specification.
Definition Saturation.h:365
T processSample(T sample, T drive, T character, int ch) noexcept
Definition Saturation.h:419
void update(T drive, T, const AudioSpec &spec) noexcept override
Updates internal coefficients dependent on block-rate parameters.
Definition Saturation.h:383
Saturation< T >::Algorithm getType() const noexcept override
Identifies the exact algorithm type for CRTP static dispatch.
Definition Saturation.h:381
void update(T, T, const AudioSpec &spec) noexcept override
Updates internal coefficients dependent on block-rate parameters.
Definition Saturation.h:467
T processSample(T sample, T drive, T character, int ch) noexcept
Definition Saturation.h:489
Saturation< T >::Algorithm getType() const noexcept override
Identifies the exact algorithm type for CRTP static dispatch.
Definition Saturation.h:465
void reset() noexcept override
Resets internal states (filters, phase, memory).
Definition Saturation.h:461
void prepare(const AudioSpec &spec) noexcept override
Prepares the algorithm with the current audio specification.
Definition Saturation.h:455
void reset() noexcept override
Resets internal states (filters, phase, memory).
Definition Saturation.h:150
void prepare(const AudioSpec &) noexcept override
Prepares the algorithm with the current audio specification.
Definition Saturation.h:149
T processSample(T sample, T drive, T character, int ch) noexcept
Definition Saturation.h:153
Saturation< T >::Algorithm getType() const noexcept override
Identifies the exact algorithm type for CRTP static dispatch.
Definition Saturation.h:151
Saturation< T >::Algorithm getType() const noexcept override
Identifies the exact algorithm type for CRTP static dispatch.
Definition Saturation.h:245
void prepare(const AudioSpec &) noexcept override
Prepares the algorithm with the current audio specification.
Definition Saturation.h:239
void reset() noexcept override
Resets internal states (filters, phase, memory).
Definition Saturation.h:240
T processSample(T sample, T drive, T character, int ch) noexcept
Definition Saturation.h:258
T logCosh(T x) noexcept
Definition Saturation.h:71
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 mapRange(T value, T inMin, T inMax, T outMin, T outMax) noexcept
Maps a value from one range to another (linear interpolation).
Definition DspMath.h:101
T gainToDecibels(T gain, T minusInfinityDb=T(-100)) noexcept
Converts a linear gain value to decibels.
Definition DspMath.h:78
T fastTanh(T x) noexcept
Fast tanh approximation using Pade rational function.
Definition DspMath.h:126
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:639
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:67
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:56
int maxBlockSize
Maximum number of samples per processing block.
Definition AudioSpec.h:51
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43
static BiquadCoeffs makeHighPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
High-pass filter.
Definition Biquad.h:127
static BiquadCoeffs makePeak(double sampleRate, double freq, double Q, double gainDb) noexcept
Peak (parametric EQ) filter.
Definition Biquad.h:185
static BiquadCoeffs makeTilt(double sampleRate, double pivotFreq, double gainDb) noexcept
Creates a first-order tilt filter.
Definition Biquad.h:444
static BiquadCoeffs makeLowPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Low-pass filter.
Definition Biquad.h:103
Static utility for Mid/Side stereo encoding and decoding.
Definition MidSide.h:57
static void encode(AudioBufferView< T > buffer) noexcept
Encodes an entire stereo buffer from Left/Right to Mid/Side.
Definition MidSide.h:106
static void decode(AudioBufferView< T > buffer) noexcept
Decodes an entire stereo buffer from Mid/Side back to Left/Right.
Definition MidSide.h:136
ProcessingMode processingMode
Linear ramp smoother for predictable, uniform interpolation.
Definition Smoothers.h:56
void setCurrentAndTargetValue(float value) noexcept
Definition Smoothers.h:356
void reset(double sampleRate, float rampTimeMilliseconds, float initialValue=0.0f) noexcept
Definition Smoothers.h:327
float getTargetValue() const noexcept
Definition Smoothers.h:65
void setTargetValue(float newTarget) noexcept
Definition Smoothers.h:336
bool isSmoothing() const noexcept
Definition Smoothers.h:364
float getCurrentValue() const noexcept
Definition Smoothers.h:64
Second-order state variable filter (SVF) smoother (TPT implementation).
Definition Smoothers.h:245
void reset(double sampleRate, float timeConstantMilliseconds, float q=0.707f, float initialValue=0.0f) noexcept
Definition Smoothers.h:618
void setTargetValue(float newTarget) noexcept
Definition Smoothers.h:645
float getTargetValue() const noexcept
Definition Smoothers.h:254