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