DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
AlgorithmicReverb.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
121#include "../Core/RingBuffer.h"
122#include "../Core/DryWetMixer.h"
123#include "../Core/DspMath.h"
124#include "../Core/AudioSpec.h"
125#include "../Core/AudioBuffer.h"
126#include "../Core/DenormalGuard.h"
127#include "../Core/Biquad.h"
128#include "../Core/StateBlob.h"
129
130#include <algorithm>
131#include <array>
132#include <atomic>
133#include <cmath>
134#include <cstddef>
135#include <cstdint>
136#include <utility>
137#include <vector>
138
139namespace dspark {
140
147template <FloatType T>
149{
150public:
152 enum class Type
153 {
154 Room,
155 Hall,
156 Chamber,
157 Plate,
158 Spring,
159 Cathedral
160 };
161
165 enum class Quality
166 {
167 Full,
168 Eco
169 };
170
171 ~AlgorithmicReverb() = default; // non-virtual: leaf class (no virtual dispatch)
172
173 // -- Lifecycle --------------------------------------------------------------
174
187 void prepare(const AudioSpec& spec)
188 {
189 if (!spec.isValid()) return; // release-safe: keep previous state
190
191 spec_ = spec;
192 mixer_.prepare(spec);
193 double sr = spec.sampleRate;
194
195 // Re-derive the sample counts stored at set-time: after a re-prepare
196 // at a different rate they would keep the OLD rate's sample count
197 // (e.g. a 100 ms pre-delay set at 48 kHz played back as 50 ms at 96 kHz).
198 preDelaySamples_.store(static_cast<int>(
199 static_cast<T>(sr) * preDelayMs_.load(std::memory_order_relaxed) / T(1000)),
200 std::memory_order_relaxed);
201 erToLateSamples_.store(static_cast<int>(
202 static_cast<T>(sr) * erToLateMs_.load(std::memory_order_relaxed) / T(1000)),
203 std::memory_order_relaxed);
204
205 preDelayBuf_.prepare(static_cast<int>(sr * 0.2) + 1);
206 erBuf_.prepare(static_cast<int>(sr * 0.2) + 1);
207 erToLateBuf_.prepare(static_cast<int>(sr * 0.2) + 1);
208
209 int maxDiff = static_cast<int>(sr * 0.012) + 1;
210 for (auto& buf : diffBufs_)
211 buf.prepare(maxDiff);
212
213 int maxFDN = static_cast<int>(sr * 0.5) + 1;
214 for (auto& dl : fdnDelays_)
215 dl.prepare(maxFDN);
216
217 // Parallel allpass diffuser - step 1 (~20ms max)
218 int maxParAP = static_cast<int>(sr * 0.021) + 1;
219 for (auto& buf : parAPBufs_) buf.prepare(maxParAP);
220
221 // Multi-channel diffuser - step 2 (~47ms max)
222 int maxDiffS2 = static_cast<int>(sr * 0.047) + 1;
223 for (auto& buf : diffuserStep2_) buf.prepare(maxDiffS2);
224
225 // Feedback allpass buffers (~60ms max, proportional to FDN delays)
226 int maxFbAP = static_cast<int>(sr * 0.06) + 1;
227 for (auto& buf : fbAPBufsA_) buf.prepare(maxFbAP);
228 for (auto& buf : fbAPBufsB_) buf.prepare(maxFbAP);
229
230 // Internal serial allpass buffers (~35ms max)
231 int maxIntAP = static_cast<int>(sr * 0.035) + 1;
232 for (auto& buf : intAPBufsA_) buf.prepare(maxIntAP);
233 for (auto& buf : intAPBufsB_) buf.prepare(maxIntAP);
234
235 // Output diffusion buffers (~3ms max)
236 int maxOutDiff = static_cast<int>(sr * 0.003) + 1;
237 for (auto& buf : outDiffBufsL_)
238 buf.prepare(maxOutDiff);
239 for (auto& buf : outDiffBufsR_)
240 buf.prepare(maxOutDiff);
241
242 // Initialize smooth random LFOs
243 T rate = modRate_.load(std::memory_order_relaxed);
244 for (int i = 0; i < kFDNSize; ++i)
245 {
246 modLFOA_[i].prepare(sr, rate * (T(0.7) + T(0.05) * static_cast<T>(i)),
247 static_cast<uint32_t>(i * 7919 + 1));
248 modLFOB_[i].prepare(sr, rate * (T(1.8) + T(0.11) * static_cast<T>(i)),
249 static_cast<uint32_t>(i * 6271 + 31337));
250 }
251
252 // DC block coefficient (~23 Hz, sample-rate independent)
253 dcCoeff_ = T(1) - std::exp(T(-6.283185307179586) * T(23)
254 / static_cast<T>(sr));
255
256 // Noise modulation: LP cutoff ~3 Hz, depth scales with modDepth_
257 noiseCoeff_ = T(1) - std::exp(T(-6.283185307179586) * T(3)
258 / static_cast<T>(sr));
259 // Eco quality refreshes the noise LP only once per control interval,
260 // so the coefficient is scaled to keep the same ~3 Hz cutoff.
261 noiseCoeffEco_ = T(1) - std::exp(T(-6.283185307179586) * T(3)
263 / static_cast<T>(sr));
264 noiseState_ = 1;
265 noiseLP_ = T(0);
266
267 eco_ = quality_.load(std::memory_order_relaxed) == Quality::Eco;
269
270 applyPreset(type_.load(std::memory_order_relaxed));
271 // Clear pending flags - prepare() has already applied whatever the
272 // caller configured before prepare, so no drain is needed on the first
273 // processBlock.
274 presetDirty_.store(false, std::memory_order_relaxed);
275 paramsDirty_.store(false, std::memory_order_relaxed);
276 toneDirty_.store(false, std::memory_order_relaxed);
277 qualityDirty_.store(false, std::memory_order_relaxed);
278 reset();
279 }
280
289 void processBlock(AudioBufferView<T> buffer) noexcept
290 {
291 DenormalGuard guard;
292 const int nCh = std::min(buffer.getNumChannels(), 2);
293 const int nS = buffer.getNumSamples();
294 if (nCh == 0 || nS == 0) return;
295
296 // Front-door non-finite guard (M-006 C1): the FDN is fully recursive
297 // (fdnDelays_ feed back through Householder/absorption/allpasses) and the
298 // soft-clip does not remove NaN, so a single NaN/Inf input poisons the
299 // tail forever. Scrub non-finite input to 0 before the dry snapshot and
300 // any state. No-op on finite input (metrics byte-identical).
301 for (int ch = 0; ch < nCh; ++ch)
302 {
303 T* d = buffer.getChannel(ch);
304 for (int i = 0; i < nS; ++i)
305 if (!std::isfinite(d[i])) d[i] = T(0);
306 }
307
309
310 mixer_.pushDry(buffer);
312
313 for (int i = 0; i < nS; ++i)
314 {
315 T monoIn;
316 if (nCh >= 2)
317 {
318 T L = buffer.getChannel(0)[i];
319 T R = buffer.getChannel(1)[i];
320 T sum = L + R;
321 T env = std::abs(L) + std::abs(R);
322
323 if (std::abs(sum) < T(1e-5) * env)
324 {
325 // Pure side condition: decode the side channel instead of hard-switching
326 // Preserves phase coherency and prevents toggle distortion.
327 monoIn = (L - R) * T(0.5);
328 }
329 else
330 {
331 monoIn = sum * T(0.5);
332 }
333 }
334 else
335 {
336 monoIn = buffer.getChannel(0)[i];
337 }
338
339 auto [outL, outR] = processSampleInternal(monoIn);
340
341 if (nCh >= 2)
342 {
343 buffer.getChannel(0)[i] = outL;
344 buffer.getChannel(1)[i] = outR;
345 }
346 else
347 {
348 buffer.getChannel(0)[i] = (outL + outR) * T(0.5);
349 }
350 }
351
352 mixer_.mixWet(buffer, mix_.load(std::memory_order_relaxed));
353 }
354
365 [[nodiscard]] std::pair<T, T> processSample(T input) noexcept
366 {
369 // Front-door non-finite guard (M-006 C1): mirror processBlock so a
370 // per-sample caller cannot poison the recursive FDN tail forever.
371 if (!std::isfinite(input)) input = T(0);
372 return processSampleInternal(input);
373 }
374
380 void reset() noexcept
381 {
382 preDelayBuf_.reset();
383 erBuf_.reset();
384 erToLateBuf_.reset();
385 for (auto& buf : diffBufs_) buf.reset();
386 for (auto& dl : fdnDelays_) dl.reset();
387 for (auto& buf : parAPBufs_) buf.reset();
388 for (auto& buf : diffuserStep2_) buf.reset();
389 for (auto& buf : fbAPBufsA_) buf.reset();
390 for (auto& buf : fbAPBufsB_) buf.reset();
391 for (auto& buf : intAPBufsA_) buf.reset();
392 for (auto& buf : intAPBufsB_) buf.reset();
393 for (auto& buf : outDiffBufsL_) buf.reset();
394 for (auto& buf : outDiffBufsR_) buf.reset();
395
396 absState_.fill(T(0));
397 absX1_.fill(T(0));
398 bassState_.fill(T(0));
399 dcZ_.fill(T(0));
400 prevFeedback_.fill(T(0));
401 apInterpState_.fill(T(0));
402 erLPStateL_.fill(T(0));
403 erLPStateR_.fill(T(0));
404 modACache_.fill(T(0));
405 modBCache_.fill(T(0));
406 noiseND_ = T(0);
407 noiseLP_ = T(0);
408 ctrlPhase_ = 0;
409
410 for (auto& lfo : modLFOA_) lfo.reset();
411 for (auto& lfo : modLFOB_) lfo.reset();
412
415 mixer_.reset();
416 }
417
418 // =========================================================================
419 // Level 1: Simple API
420 // =========================================================================
421
422 void setType(Type type) noexcept
423 {
424 // C2 fix: don't touch non-atomic state from GUI thread. Publish the
425 // new type and raise the preset-dirty flag; the audio thread will
426 // run applyPreset() + reset() at the top of its next processBlock.
427 // Wild enum values (a corrupted blob, a stray cast) clamp into range.
428 const int t = std::clamp(static_cast<int>(type), 0,
429 static_cast<int>(Type::Cathedral));
430 type_.store(static_cast<Type>(t), std::memory_order_relaxed);
431 // A preset load re-establishes the baseline: forget prior overrides so
432 // the preset's own values apply, unless a setter is called AFTER this.
433 userParamMask_.store(0u, std::memory_order_relaxed);
434 presetDirty_.store(true, std::memory_order_release);
435 }
436
466 void setQuality(Quality q) noexcept
467 {
469 std::memory_order_relaxed);
470 qualityDirty_.store(true, std::memory_order_release);
471 }
472
473 void setDecay(T seconds) noexcept
474 {
475 if (!std::isfinite(seconds)) return;
476 decayTime_.store(std::clamp(seconds, T(0.1), T(30)),
477 std::memory_order_relaxed);
479 paramsDirty_.store(true, std::memory_order_release);
480 }
481
482 void setMix(T dryWet) noexcept
483 {
484 if (!std::isfinite(dryWet)) return;
485 mix_.store(std::clamp(dryWet, T(0), T(1)), std::memory_order_relaxed);
486 }
487
488 // =========================================================================
489 // Level 2: Intermediate API
490 // =========================================================================
491
492 void setSize(T size) noexcept
493 {
494 if (!std::isfinite(size)) return;
495 size_.store(std::clamp(size, T(0.01), T(1)), std::memory_order_relaxed);
497 paramsDirty_.store(true, std::memory_order_release);
498 }
499
506 void setDamping(T amount) noexcept
507 {
508 if (!std::isfinite(amount)) return;
509 T clamped = std::clamp(amount, T(0), T(1));
510 damping_.store(clamped, std::memory_order_relaxed);
511 highDecayMult_.store(T(1) - clamped * T(0.9), std::memory_order_relaxed);
513 paramsDirty_.store(true, std::memory_order_release);
514 }
515
516 void setPreDelay(T ms) noexcept
517 {
518 if (!std::isfinite(ms)) return;
519 T clamped = std::clamp(ms, T(0), T(200));
520 preDelayMs_.store(clamped, std::memory_order_relaxed);
521 // preDelaySamples_ is already atomic and only consumed in the audio
522 // loop; storing it from any thread is safe and cheap.
523 if (spec_.sampleRate > 0)
524 preDelaySamples_.store(static_cast<int>(
525 static_cast<T>(spec_.sampleRate) * clamped / T(1000)),
526 std::memory_order_relaxed);
527 }
528
529 void setDiffusion(T amount) noexcept
530 {
531 if (!std::isfinite(amount)) return;
532 diffusion_.store(std::clamp(amount, T(0), T(1)), std::memory_order_relaxed);
534 paramsDirty_.store(true, std::memory_order_release);
535 }
536
537 void setModulation(T amount) noexcept
538 {
539 if (!std::isfinite(amount)) return;
540 T clamped = std::clamp(amount, T(0), T(1));
541 modDepth_.store(clamped, std::memory_order_relaxed);
542 modDepthA_.store(clamped * T(30), std::memory_order_relaxed);
543 modDepthB_.store(clamped * T(15), std::memory_order_relaxed);
545 }
546
555 void setWidth(T width) noexcept
556 {
557 if (!std::isfinite(width)) return;
558 width_.store(std::clamp(width, T(0), T(2)), std::memory_order_relaxed);
559 }
560
561 void setErToLateDelay(T ms) noexcept
562 {
563 if (!std::isfinite(ms)) return;
564 T clamped = std::clamp(ms, T(0), T(200));
565 erToLateMs_.store(clamped, std::memory_order_relaxed);
567 if (spec_.sampleRate > 0)
568 erToLateSamples_.store(static_cast<int>(
569 static_cast<T>(spec_.sampleRate) * clamped / T(1000)),
570 std::memory_order_relaxed);
571 }
572
573 // =========================================================================
574 // Level 3: Expert API - Frequency-Dependent Decay
575 // =========================================================================
576
586 void setHighDecayMultiplier(T mult) noexcept
587 {
588 if (!std::isfinite(mult)) return;
589 T clamped = std::clamp(mult, T(0.05), T(1));
590 highDecayMult_.store(clamped, std::memory_order_relaxed);
591 damping_.store(std::clamp((T(1) - clamped) / T(0.9), T(0), T(1)),
592 std::memory_order_relaxed);
594 paramsDirty_.store(true, std::memory_order_release);
595 }
596
607 void setBassDecayMultiplier(T mult) noexcept
608 {
609 if (!std::isfinite(mult)) return;
610 bassDecayMult_.store(std::clamp(mult, T(0.3), T(3)),
611 std::memory_order_relaxed);
613 paramsDirty_.store(true, std::memory_order_release);
614 }
615
629 void setHighCrossover(T hz) noexcept
630 {
631 if (!std::isfinite(hz)) return;
632 highCrossover_.store(std::clamp(hz, T(1000), T(16000)),
633 std::memory_order_relaxed);
635 paramsDirty_.store(true, std::memory_order_release);
636 }
637
642 void setBassCrossover(T hz) noexcept
643 {
644 if (!std::isfinite(hz)) return;
645 bassCrossover_.store(std::clamp(hz, T(50), T(500)),
646 std::memory_order_relaxed);
648 paramsDirty_.store(true, std::memory_order_release);
649 }
650
651 // =========================================================================
652 // Level 3: Expert API - Tone & Levels
653 // =========================================================================
654
655 void setEarlyLevel(T dB) noexcept
656 {
657 if (!std::isfinite(dB)) return;
658 earlyLevel_.store(decibelsToGain(std::clamp(dB, T(-60), T(6))), std::memory_order_relaxed);
660 }
661
662 void setLateLevel(T dB) noexcept
663 {
664 if (!std::isfinite(dB)) return;
665 lateLevel_.store(decibelsToGain(std::clamp(dB, T(-60), T(6))), std::memory_order_relaxed);
667 }
668
669 void setModRate(T hz) noexcept
670 {
671 if (!std::isfinite(hz)) return;
672 modRate_.store(std::clamp(hz, T(0.1), T(5)), std::memory_order_relaxed);
674 paramsDirty_.store(true, std::memory_order_release);
675 }
676
681 void setToneLowCut(T hz) noexcept
682 {
683 if (!std::isfinite(hz)) return;
684 // Queue target; audio thread rebuilds the biquad inside processBlock.
685 toneLowCutHz_.store(hz, std::memory_order_relaxed);
686 toneDirty_.store(true, std::memory_order_release);
687 }
688
693 void setToneHighCut(T hz) noexcept
694 {
695 if (!std::isfinite(hz)) return;
696 toneHighCutHz_.store(hz, std::memory_order_relaxed);
697 toneDirty_.store(true, std::memory_order_release);
698 }
699
700 // =========================================================================
701 // Getters
702 // =========================================================================
703
704 [[nodiscard]] Type getType() const noexcept { return type_.load(std::memory_order_relaxed); }
705 [[nodiscard]] Quality getQuality() const noexcept { return quality_.load(std::memory_order_relaxed); }
706 [[nodiscard]] T getDecay() const noexcept { return decayTime_.load(std::memory_order_relaxed); }
707 [[nodiscard]] T getMix() const noexcept { return mix_.load(std::memory_order_relaxed); }
708 [[nodiscard]] T getHighDecayMultiplier() const noexcept { return highDecayMult_.load(std::memory_order_relaxed); }
709 [[nodiscard]] T getBassDecayMultiplier() const noexcept { return bassDecayMult_.load(std::memory_order_relaxed); }
710 [[nodiscard]] T getWidth() const noexcept { return width_.load(std::memory_order_relaxed); }
711 [[nodiscard]] T getHighCrossover() const noexcept { return highCrossover_.load(std::memory_order_relaxed); }
712 [[nodiscard]] T getBassCrossover() const noexcept { return bassCrossover_.load(std::memory_order_relaxed); }
713
714
716 [[nodiscard]] std::vector<uint8_t> getState() const
717 {
718 StateWriter w(stateId("ARVB"), 1);
719 w.write("type", static_cast<int32_t>(type_.load(std::memory_order_relaxed)));
720 w.write("quality", static_cast<int32_t>(quality_.load(std::memory_order_relaxed)));
721 w.write("decay", decayTime_.load(std::memory_order_relaxed));
722 w.write("size", size_.load(std::memory_order_relaxed));
723 w.write("damping", damping_.load(std::memory_order_relaxed));
724 w.write("diffusion", diffusion_.load(std::memory_order_relaxed));
725 w.write("modDepth", modDepth_.load(std::memory_order_relaxed));
726 w.write("modRate", modRate_.load(std::memory_order_relaxed));
727 w.write("preDelay", preDelayMs_.load(std::memory_order_relaxed));
728 w.write("erToLate", erToLateMs_.load(std::memory_order_relaxed));
729 w.write("mix", mix_.load(std::memory_order_relaxed));
730 w.write("width", width_.load(std::memory_order_relaxed));
731 w.write("highDecay", highDecayMult_.load(std::memory_order_relaxed));
732 w.write("bassDecay", bassDecayMult_.load(std::memory_order_relaxed));
733 w.write("highXover", highCrossover_.load(std::memory_order_relaxed));
734 w.write("bassXover", bassCrossover_.load(std::memory_order_relaxed));
735 w.write("earlyDb", static_cast<float>(20.0 * std::log10(std::max(
736 static_cast<double>(earlyLevel_.load(std::memory_order_relaxed)), 1e-6))));
737 w.write("lateDb", static_cast<float>(20.0 * std::log10(std::max(
738 static_cast<double>(lateLevel_.load(std::memory_order_relaxed)), 1e-6))));
739 w.write("toneLowCut", toneLowCutHz_.load(std::memory_order_relaxed));
740 w.write("toneHighCut", toneHighCutHz_.load(std::memory_order_relaxed));
741 return w.blob();
742 }
743
745 bool setState(const uint8_t* data, size_t size)
746 {
747 StateReader r(data, size);
748 if (!r.isValid() || r.processorId() != stateId("ARVB")) return false;
749 setType(static_cast<Type>(r.read("type", 0)));
750 // Older blobs have no "quality" key: default 0 = Full (unchanged sound).
751 setQuality(r.read("quality", 0) == 1 ? Quality::Eco : Quality::Full);
752 setDecay(static_cast<T>(r.read("decay", 1.0f)));
753 setSize(static_cast<T>(r.read("size", 0.5f)));
754 setDamping(static_cast<T>(r.read("damping", 0.5f)));
755 setDiffusion(static_cast<T>(r.read("diffusion", 0.7f)));
756 setModulation(static_cast<T>(r.read("modDepth", 0.1f)));
757 setModRate(static_cast<T>(r.read("modRate", 1.0f)));
758 setPreDelay(static_cast<T>(r.read("preDelay", 0.0f)));
759 setErToLateDelay(static_cast<T>(r.read("erToLate", 0.0f)));
760 setMix(static_cast<T>(r.read("mix", 0.3f)));
761 setWidth(static_cast<T>(r.read("width", 1.0f)));
762 setHighDecayMultiplier(static_cast<T>(r.read("highDecay", 0.5f)));
763 setBassDecayMultiplier(static_cast<T>(r.read("bassDecay", 1.2f)));
764 setHighCrossover(static_cast<T>(r.read("highXover", 5000.0f)));
765 setBassCrossover(static_cast<T>(r.read("bassXover", 200.0f)));
766 setEarlyLevel(static_cast<T>(r.read("earlyDb", 0.0f)));
767 setLateLevel(static_cast<T>(r.read("lateDb", 0.0f)));
768 const float lo = r.read("toneLowCut", -1.0f);
769 const float hi = r.read("toneHighCut", -1.0f);
770 if (lo > 0.0f) setToneLowCut(static_cast<T>(lo));
771 if (hi > 0.0f) setToneHighCut(static_cast<T>(hi));
772 return true;
773 }
774
775protected:
776 // --- Constants -----------------------------------------------------------
777
778 static constexpr int kFDNSize = 16;
779 static constexpr int kDiffStages = 8;
780 static constexpr int kMaxERTaps = 40;
781 static constexpr int kNumMultiTaps = 7;
782 static constexpr int kOutDiffStages = 2;
783
784 // Eco quality engine reductions (see setQuality)
785 static constexpr int kEcoLines = 8;
786 static constexpr int kEcoCtrlInterval = 16;
787 static constexpr int kEcoERTaps = 12;
788
789 static constexpr T kInputGain = T(1) / T(8);
790
791 // Output normalization: 16 main + 7 multi-tap + fbAP taps + intAP taps
792 static constexpr T kOutputNorm = T(1) / T(5.0);
793
794 // Eco output normalization: 8 sign-weighted lines only (no extra taps).
795 // Calibrated so the Eco wet tail matches Full loudness (measured RMS).
796 static constexpr T kOutputNormEco = T(0.32);
797
798 // Orthogonal stereo output sign vectors (inner product = 0)
799 static constexpr int kOutSignL_[kFDNSize] = {
800 1, -1, 1, 1, -1, 1, -1, -1,
801 1, -1, -1, 1, -1, 1, 1, -1
802 };
803 static constexpr int kOutSignR_[kFDNSize] = {
804 1, 1, -1, 1, 1, -1, -1, -1,
805 -1, 1, -1, 1, -1, -1, 1, 1
806 };
807
808 // Multi-tap output: Dattorro-style decorrelation from different delay positions
809 static constexpr int kMultiTapLineL_[kNumMultiTaps] = {0, 2, 5, 7, 9, 12, 14};
810 static constexpr int kMultiTapLineR_[kNumMultiTaps] = {1, 3, 4, 8, 10, 13, 15};
811 static constexpr double kMultiTapFracL_[kNumMultiTaps] = {0.37, 0.67, 0.23, 0.81, 0.44, 0.59, 0.31};
812 static constexpr double kMultiTapFracR_[kNumMultiTaps] = {0.43, 0.71, 0.29, 0.63, 0.47, 0.53, 0.37};
813 static constexpr int kMultiTapSignL_[kNumMultiTaps] = {+1, -1, +1, -1, +1, -1, +1};
814 static constexpr int kMultiTapSignR_[kNumMultiTaps] = {+1, +1, -1, +1, -1, +1, -1};
815
816 // FDN base delay times in ms (at size=1.0)
817 static constexpr double kBaseDelaysMs_[kFDNSize] = {
818 29.7, 34.1, 39.3, 45.2, 52.0, 58.1, 64.9, 72.3,
819 80.4, 89.0, 98.3, 108.7, 119.9, 132.3, 145.7, 160.1
820 };
821
822 // Input diffusion allpass delays (ms) - 8 stages (Dattorro-style, ~34ms total)
823 static constexpr double kDiffDelaysMs_[kDiffStages] = {
824 1.03, 1.47, 2.19, 3.13, 4.23, 5.59, 7.19, 9.47
825 };
826
827 static constexpr double kDiffBaseCoeffs_[kDiffStages] = {
828 0.75, 0.75, 0.72, 0.72, 0.70, 0.70, 0.68, 0.68
829 };
830
831 // Feedback allpass stages per FDN line (echo density multiplier)
832 static constexpr int kFbAPStages = 2;
833
834 // Feedback allpass delays as fraction of FDN delay (Dattorro-style proportional)
835 static constexpr double kFbAPRatioA_ = 0.25; // 25% of FDN delay
836 static constexpr double kFbAPRatioB_ = 0.35; // 35% of FDN delay
837
838 // Parallel allpass diffuser - step 1 delays (ms, per channel, different IRs)
839 static constexpr double kParAPDelaysMs_[kFDNSize] = {
840 5.3, 6.1, 7.1, 7.9, 8.9, 9.7, 10.7, 11.7,
841 12.3, 13.3, 14.3, 15.1, 16.1, 17.1, 18.1, 19.1
842 };
843
844 // Multi-channel diffuser - step 2 per-channel delays (ms)
845 static constexpr double kDiffuserStep2Ms_[kFDNSize] = {
846 15.7, 17.9, 20.1, 22.3, 24.7, 26.9, 29.3, 31.1,
847 33.7, 35.3, 37.1, 39.3, 41.1, 42.9, 44.3, 45.7
848 };
849
850 // Output diffusion allpass delays (ms, decorrelated L/R)
851 static constexpr double kOutDiffDelaysMsL_[kOutDiffStages] = {1.47, 2.31};
852 static constexpr double kOutDiffDelaysMsR_[kOutDiffStages] = {1.63, 2.47};
853
854 // =========================================================================
855 // Smooth Random LFO (Lexicon-style modulation)
856 // =========================================================================
857
866 {
867 T h0_ = T(0), h1_ = T(0), h2_ = T(0), h3_ = T(0);
868 T phase_ = T(0);
869 T phaseInc_ = T(0);
870 uint32_t state_ = 1;
871
872 void prepare(double sr, T rate, uint32_t seed) noexcept
873 {
874 phaseInc_ = rate / static_cast<T>(sr);
875 state_ = seed ? seed : 1;
876 h0_ = nextRandom(); h1_ = nextRandom();
877 h2_ = nextRandom(); h3_ = nextRandom();
878 phase_ = T(0);
879 }
880
881 void setRate(T rate, double sr) noexcept
882 {
883 phaseInc_ = rate / static_cast<T>(sr);
884 }
885
886 T next() noexcept { return nextStride(1); }
887
896 T nextStride(int stride) noexcept
897 {
898 phase_ += phaseInc_ * static_cast<T>(stride);
899 if (phase_ >= T(1))
900 {
901 phase_ -= T(1);
902 h0_ = h1_; h1_ = h2_; h2_ = h3_;
903 h3_ = nextRandom();
904 }
905 // Cubic Hermite (Catmull-Rom) interpolation
906 T d = phase_;
907 T c0 = h1_;
908 T c1 = T(0.5) * (h2_ - h0_);
909 T c2 = h0_ - T(2.5) * h1_ + T(2) * h2_ - T(0.5) * h3_;
910 T c3 = T(0.5) * (h3_ - h0_) + T(1.5) * (h1_ - h2_);
911 return ((c3 * d + c2) * d + c1) * d + c0;
912 }
913
914 void reset() noexcept { phase_ = T(0); h0_ = h1_ = h2_ = h3_ = T(0); }
915
916 private:
917 T nextRandom() noexcept
918 {
919 state_ ^= state_ << 13;
920 state_ ^= state_ >> 17;
921 state_ ^= state_ << 5;
922 return static_cast<T>(state_)
923 / static_cast<T>(0xFFFFFFFFu) * T(2) - T(1);
924 }
925 };
926
927 // --- State ---------------------------------------------------------------
928
930
931 // FDN delay lines
932 std::array<RingBuffer<T>, kFDNSize> fdnDelays_;
933 std::array<int, kFDNSize> fdnDelayLens_ {};
934
935 // Jot absorption filter state (per line): first-order pole-zero shelf
936 // H(z) = (b0 + b1 z^-1) / (1 + a1 z^-1), designed so |H(1)| = gMid and
937 // |H(-1)| = gHigh EXACTLY (the T60 calibration anchors) with the
938 // transition midpoint sqrt(gMid * gHigh) placed at highCrossover_.
939 std::array<T, kFDNSize> absB0_ {}; // feedforward x[n]
940 std::array<T, kFDNSize> absB1_ {}; // feedforward x[n-1]
941 std::array<T, kFDNSize> absA1_ {}; // feedback y[n-1] (denominator sign)
942 std::array<T, kFDNSize> absX1_ {}; // x[n-1] state
943 std::array<T, kFDNSize> absState_ {}; // y[n-1] state
944
945 // Bass shelf state (per line)
946 std::array<T, kFDNSize> bassRatio_ {}; // g_bass / g_mid
947 std::array<T, kFDNSize> bassState_ {}; // 1-pole LP state
948
949 // DC blocker state
950 std::array<T, kFDNSize> dcZ_ {};
951
952 // Feedback allpass (2 stages per FDN line, density multiplier)
953 std::array<RingBuffer<T>, kFDNSize> fbAPBufsA_;
954 std::array<RingBuffer<T>, kFDNSize> fbAPBufsB_;
955 std::array<int, kFDNSize> fbAPDelaysA_ {};
956 std::array<int, kFDNSize> fbAPDelaysB_ {};
957 T fbAPCoeff_ = T(0.6);
958
959 // Internal serial allpasses (per FDN line, pre-write + post-read)
960 std::array<RingBuffer<T>, kFDNSize> intAPBufsA_; // pre-write
961 std::array<RingBuffer<T>, kFDNSize> intAPBufsB_; // post-read
962 std::array<int, kFDNSize> intAPDelaysA_ {};
963 std::array<int, kFDNSize> intAPDelaysB_ {};
964 static constexpr T intAPCoeff_ = T(0.5); // Infinity2 uses 0.5
965 static constexpr double kIntAPRatioA_ = 0.15; // 15% of FDN delay
966 static constexpr double kIntAPRatioB_ = 0.20; // 20% of FDN delay
967
968 // Feedback IIR smoothing (Verbity technique: eliminates discrete-echo quality)
969 std::array<T, kFDNSize> prevFeedback_ {};
970 T fbSmooth_ = T(0.3);
971
972 // Smooth random modulation (2 per line = 32 total)
973 std::array<SmoothRandomLFO, kFDNSize> modLFOA_;
974 std::array<SmoothRandomLFO, kFDNSize> modLFOB_;
975
976 // Allpass interpolation state for modulated FDN reads (preserves HF)
977 std::array<T, kFDNSize> apInterpState_ {};
978
979 // Filtered noise for modulation randomization (Progenitor-style)
980 uint32_t noiseState_ = 1;
981 T noiseLP_ = T(0);
982 T noiseCoeff_ = T(0);
983 T noiseDepth_ = T(0);
984
985 // Quality engine state (audio-thread only, derived from quality_ at the
986 // dirty-flag drain). Full: 16 lines, per-sample modulation. Eco: 8 lines,
987 // control-rate modulation refreshed every kEcoCtrlInterval samples.
988 bool eco_ = false;
990 std::array<T, kFDNSize> modACache_ {}; // per-line LFO A value (samples)
991 std::array<T, kFDNSize> modBCache_ {}; // per-line LFO B value (samples)
992 T noiseND_ = T(0); // filtered noise * noiseDepth_
993 int ctrlPhase_ = 0; // Eco control-rate phase counter
994 T noiseCoeffEco_ = T(0); // noise LP coeff at control rate
995
996 // Input diffusion
997 std::array<RingBuffer<T>, kDiffStages> diffBufs_;
998 std::array<int, kDiffStages> diffDelays_ {};
999 std::array<T, kDiffStages> diffCoeffs_ {};
1000
1001 // Parallel allpass diffuser - step 1 (16 parallel allpass, different delays)
1002 std::array<RingBuffer<T>, kFDNSize> parAPBufs_;
1003 std::array<int, kFDNSize> parAPDelays_ {};
1004 T parAPCoeff_ = T(0.65);
1005
1006 // Multi-channel diffuser - step 2 (16 per-channel delay buffers)
1007 std::array<RingBuffer<T>, kFDNSize> diffuserStep2_;
1008 std::array<int, kFDNSize> diffuserStep2Delays_ {};
1009
1010 // Output diffusion (L/R decorrelated)
1011 std::array<RingBuffer<T>, kOutDiffStages> outDiffBufsL_;
1012 std::array<RingBuffer<T>, kOutDiffStages> outDiffBufsR_;
1013 std::array<int, kOutDiffStages> outDiffDelaysL_ {};
1014 std::array<int, kOutDiffStages> outDiffDelaysR_ {};
1015 T outDiffCoeff_ = T(0.45);
1016
1017 // Early reflections
1019 std::array<int, kMaxERTaps> erTapsL_ {}, erTapsR_ {};
1020 std::array<T, kMaxERTaps> erGainsL_ {}, erGainsR_ {};
1021 std::array<T, kMaxERTaps> erAbsCoeffs_ {};
1022 std::array<T, kMaxERTaps> erLPStateL_ {};
1023 std::array<T, kMaxERTaps> erLPStateR_ {};
1024 int numERTaps_ = 0;
1025
1026 // Pre-delay
1028 std::atomic<int> preDelaySamples_ { 0 };
1029
1030 // ER-to-late gap
1032 std::atomic<int> erToLateSamples_ { 0 };
1033
1034 // Tone correction EQ (Biquad 12 dB/oct)
1037 bool toneLPActive_ = false;
1038 bool toneHPActive_ = false;
1039
1040 // Mixer
1042
1043 // --- Parameters ----------------------------------------------------------
1044 //
1045 // Thread-safety model (C2/C3 fix):
1046 // - All GUI-thread setters mutate ONLY atomic shadow fields below and set
1047 // `paramsDirty_` (or `presetDirty_` for setType).
1048 // - The audio thread drains the dirty flags at the top of `processBlock()`
1049 // and calls the update helpers there - so every non-atomic array that
1050 // participates in the audio path (fdnDelayLens_, diffCoeffs_, ...) is
1051 // mutated exclusively from the audio thread. No races, no torn reads.
1052
1053 std::atomic<Type> type_ { Type::Room };
1054 std::atomic<T> decayTime_ { T(1) };
1055 std::atomic<T> size_ { T(0.5) };
1056 std::atomic<T> damping_ { T(0.5) };
1057 std::atomic<T> diffusion_ { T(0.7) };
1058 std::atomic<T> modDepth_ { T(0.1) };
1059 std::atomic<T> modRate_ { T(1) };
1060 std::atomic<T> preDelayMs_ { T(0) };
1061 std::atomic<T> erToLateMs_ { T(0) };
1062 std::atomic<T> mix_ { T(0.3) };
1063 std::atomic<T> earlyLevel_ { T(1) };
1064 std::atomic<T> lateLevel_ { T(1) };
1065 std::atomic<T> width_ { T(1) }; // Stereo width: 0=mono, 1=natural, 2=wide
1066
1067 // Frequency-dependent decay parameters
1068 std::atomic<T> highDecayMult_ { T(0.5) }; // HF T60 multiplier (0.05-1.0)
1069 std::atomic<T> bassDecayMult_ { T(1.2) }; // bass T60 multiplier (0.3-3.0)
1070 std::atomic<T> highCrossover_ { T(5000) }; // Hz
1071 std::atomic<T> bassCrossover_ { T(200) }; // Hz
1072
1073 std::atomic<Quality> quality_ { Quality::Full };
1074
1075 // Deferred-apply flags (audio thread drains these at top of processBlock)
1076 std::atomic<bool> presetDirty_ { false }; // setType() -> rebuild topology + reset
1077 std::atomic<bool> paramsDirty_ { false }; // any other setter -> refresh coeffs
1078
1079 // Per-param user-override mask (M-006 D-M006-C1). A preset load (setType)
1080 // must not silently discard param setters batched with it before the first
1081 // processBlock -- the header's own documented quick-start is
1082 // `setType(Hall); setDecay(2.0f);`. setType() clears the mask so the preset
1083 // re-establishes the baseline; each param setter marks its bit; commitPreset()
1084 // then SKIPS any atomic the user overrode, so a setter issued after setType
1085 // always wins regardless of drain timing. RT-safe: release-mark on the UI
1086 // thread, acquire-read on the audio thread, one atomic, no locks.
1087 enum : uint32_t {
1090 kUserModDepth = 128u, kUserModRate = 256u, kUserEarly = 512u,
1091 kUserLate = 1024u, kUserErToLate = 2048u
1093 std::atomic<uint32_t> userParamMask_ { 0u };
1094 void markUserParam(uint32_t bit) noexcept
1095 { userParamMask_.fetch_or(bit, std::memory_order_release); }
1096 std::atomic<bool> toneDirty_ { false }; // tone EQ cutoff changes
1097 std::atomic<bool> qualityDirty_ { false }; // setQuality() -> resize engine + reset
1098 std::atomic<T> toneLowCutHz_ { T(-1) }; // <0 = off, queued value for audio thread
1099 std::atomic<T> toneHighCutHz_ { T(-1) };
1100
1101 // --- Computed coefficients ------------------------------------------------
1102
1103 T bassLPCoeff_ = T(0.026); // bass crossover filter coeff
1104 T dcCoeff_ = T(0.003); // DC blocker coeff (~23Hz)
1105 std::atomic<T> modDepthA_ { T(1) }; // slow LFO depth in samples
1106 std::atomic<T> modDepthB_ { T(0.5) }; // fast LFO depth in samples
1107
1108 // =========================================================================
1109 // Processing helpers
1110 // =========================================================================
1111
1114 {
1115 noiseState_ = noiseState_ * 196314165u + 907633515u;
1116 T white = static_cast<T>(static_cast<int32_t>(noiseState_))
1117 / T(2147483648.0);
1118 noiseLP_ += noiseCoeff_ * (white - noiseLP_);
1119 return noiseLP_;
1120 }
1121
1125 {
1126 noiseState_ = noiseState_ * 196314165u + 907633515u;
1127 T white = static_cast<T>(static_cast<int32_t>(noiseState_))
1128 / T(2147483648.0);
1129 noiseLP_ += noiseCoeffEco_ * (white - noiseLP_);
1130 return noiseLP_;
1131 }
1132
1133 T processAllpass(RingBuffer<T>& buf, int delay, T coeff, T input) noexcept
1134 {
1135 T delayed = buf.read(delay);
1136 T temp = input + coeff * delayed;
1137 T output = delayed - coeff * temp;
1138 buf.push(temp);
1139 return output;
1140 }
1141
1142 T processAllpassModulated(RingBuffer<T>& buf, int baseDelay, T modAmount,
1143 T coeff, T input, bool linearInterp) noexcept
1144 {
1145 T readPos = static_cast<T>(baseDelay) + modAmount;
1146 readPos = std::max(readPos, T(1));
1147 // Eco quality reads with 2-point linear interpolation (the industry
1148 // standard for diffusion allpasses with small excursions); Full keeps
1149 // the default 4-point Hermite read.
1150 T delayed = linearInterp
1151 ? buf.template readInterpolated<InterpMethod::Linear>(readPos)
1152 : buf.readInterpolated(readPos);
1153 T temp = input + coeff * delayed;
1154 T output = delayed - coeff * temp;
1155 buf.push(temp);
1156 return output;
1157 }
1158
1165 static void hadamardInPlace(std::array<T, kFDNSize>& x) noexcept
1166 {
1167 for (int i = 0; i < kFDNSize; i += 2)
1168 { T a = x[i], b = x[i+1]; x[i] = a + b; x[i+1] = a - b; }
1169
1170 for (int i = 0; i < kFDNSize; i += 4)
1171 for (int j = 0; j < 2; ++j)
1172 { T a = x[i+j], b = x[i+j+2]; x[i+j] = a + b; x[i+j+2] = a - b; }
1173
1174 for (int i = 0; i < kFDNSize; i += 8)
1175 for (int j = 0; j < 4; ++j)
1176 { T a = x[i+j], b = x[i+j+4]; x[i+j] = a + b; x[i+j+4] = a - b; }
1177
1178 for (int j = 0; j < 8; ++j)
1179 { T a = x[j], b = x[j+8]; x[j] = a + b; x[j+8] = a - b; }
1180
1181 for (auto& v : x) v *= T(0.25);
1182 }
1183
1189 static void hadamard8InPlace(std::array<T, kFDNSize>& x) noexcept
1190 {
1191 for (int i = 0; i < 8; i += 2)
1192 { T a = x[i], b = x[i+1]; x[i] = a + b; x[i+1] = a - b; }
1193
1194 for (int i = 0; i < 8; i += 4)
1195 for (int j = 0; j < 2; ++j)
1196 { T a = x[i+j], b = x[i+j+2]; x[i+j] = a + b; x[i+j+2] = a - b; }
1197
1198 for (int j = 0; j < 4; ++j)
1199 { T a = x[j], b = x[j+4]; x[j] = a + b; x[j+4] = a - b; }
1200
1201 constexpr T norm = T(0.35355339059327373); // 1/sqrt(8)
1202 for (int i = 0; i < 8; ++i) x[i] *= norm;
1203 }
1204
1212 static void householderInPlace(std::array<T, kFDNSize>& x, int n) noexcept
1213 {
1214 T sum = T(0);
1215 for (int i = 0; i < n; ++i) sum += x[i];
1216 T factor = sum * T(2) / static_cast<T>(n);
1217 for (int i = 0; i < n; ++i) x[i] -= factor;
1218 }
1219
1225 {
1228 T earlyLevel = T(1);
1229 T lateLevel = T(1);
1230 T width = T(1);
1231 T modDepthA = T(1);
1232 T modDepthB = T(0.5);
1233 };
1235
1246 void drainPendingChanges() noexcept
1247 {
1248 if (qualityDirty_.load(std::memory_order_acquire)
1249 && qualityDirty_.exchange(false, std::memory_order_acquire))
1250 {
1251 const bool eco =
1252 quality_.load(std::memory_order_relaxed) == Quality::Eco;
1253 if (eco != eco_)
1254 {
1255 eco_ = eco;
1256 nLines_ = eco ? kEcoLines : kFDNSize;
1257 if (spec_.sampleRate > 0)
1258 {
1259 updateDelayLengths(); // re-derives per-line delays + decay
1260 generateERTapsForType(type_.load(std::memory_order_relaxed));
1261 }
1262 // Engine topology changed: old delay/filter state is stale.
1263 reset();
1264 }
1265 }
1266
1267 if (presetDirty_.load(std::memory_order_acquire)
1268 && presetDirty_.exchange(false, std::memory_order_acquire))
1269 {
1270 Type t = type_.load(std::memory_order_relaxed);
1271 applyPreset(t);
1272 // C3 fix: wipe all delay buffers and filter state; topology just
1273 // changed, so old state is stale and can spike the output.
1274 reset();
1275 // The preset-rebuild already ran updateDelayLengths /
1276 // updateDiffCoeffs / updateModulation, so consume paramsDirty_
1277 // without doing the work twice.
1278 paramsDirty_.store(false, std::memory_order_relaxed);
1279 }
1280 else if (paramsDirty_.load(std::memory_order_acquire)
1281 && paramsDirty_.exchange(false, std::memory_order_acquire))
1282 {
1283 // Non-topology parameter change: refresh coefficient arrays but
1284 // DO NOT wipe delay buffers (would click on every knob tweak).
1285 if (spec_.sampleRate > 0)
1286 {
1287 updateDelayLengths(); // also runs updateDecayParams
1290
1291 T md = modDepth_.load(std::memory_order_relaxed);
1292 modDepthA_.store(md * T(30), std::memory_order_relaxed);
1293 modDepthB_.store(md * T(15), std::memory_order_relaxed);
1294
1295 T hd = highDecayMult_.load(std::memory_order_relaxed);
1296 T damp = std::clamp((T(1) - hd) / T(0.9), T(0), T(1));
1297 damping_.store(damp, std::memory_order_relaxed);
1298 }
1299 }
1300
1301 if (toneDirty_.load(std::memory_order_acquire)
1302 && toneDirty_.exchange(false, std::memory_order_acquire))
1303 {
1304 T hpHz = toneLowCutHz_.load(std::memory_order_relaxed);
1305 T lpHz = toneHighCutHz_.load(std::memory_order_relaxed);
1306 if (hpHz <= T(0) || spec_.sampleRate <= 0)
1307 {
1308 toneHPActive_ = false;
1309 }
1310 else
1311 {
1312 toneHPActive_ = true;
1315 static_cast<double>(std::clamp(hpHz, T(20), T(500)))));
1316 }
1317 if (lpHz <= T(0) || spec_.sampleRate <= 0)
1318 {
1319 toneLPActive_ = false;
1320 }
1321 else
1322 {
1323 toneLPActive_ = true;
1326 static_cast<double>(std::clamp(lpHz, T(2000), T(16000)))));
1327 }
1328 }
1329 }
1330
1332 void refreshCachedParams() noexcept
1333 {
1334 cachedParams_.preDelaySamples = preDelaySamples_.load(std::memory_order_relaxed);
1335 cachedParams_.erToLateSamples = erToLateSamples_.load(std::memory_order_relaxed);
1336 cachedParams_.earlyLevel = earlyLevel_.load(std::memory_order_relaxed);
1337 cachedParams_.lateLevel = lateLevel_.load(std::memory_order_relaxed);
1338 cachedParams_.width = width_.load(std::memory_order_relaxed);
1339 cachedParams_.modDepthA = modDepthA_.load(std::memory_order_relaxed);
1340 cachedParams_.modDepthB = modDepthB_.load(std::memory_order_relaxed);
1341 }
1342
1344 std::pair<T, T> processSampleInternal(T input) noexcept
1345 {
1346 // Block-cached params (see refreshCachedParams)
1347 const int preDelSamp = cachedParams_.preDelaySamples;
1348 const int erToLateSamp = cachedParams_.erToLateSamples;
1349 const T earlyLvl = cachedParams_.earlyLevel;
1350 const T lateLvl = cachedParams_.lateLevel;
1351 const T widthVal = cachedParams_.width;
1352 const T modDA = cachedParams_.modDepthA;
1353 const T modDB = cachedParams_.modDepthB;
1354
1355 // --- Pre-delay ---
1356 preDelayBuf_.push(input);
1357 T delayed = (preDelSamp > 0)
1358 ? preDelayBuf_.read(preDelSamp) : input;
1359
1360 // --- Input diffusion: 8 cascaded modulated allpass ---
1361 T diffused = delayed;
1362 T diffNoise = nextFilteredNoise();
1363 for (int d = 0; d < kDiffStages; ++d)
1364 {
1365 T diffPol = (d & 1) ? T(-1) : T(1);
1366 T diffMod = diffNoise * T(2) * diffPol; // +/-2 samples excursion
1368 diffMod, diffCoeffs_[d], diffused,
1369 eco_);
1370 }
1371
1372 // --- Early reflections with progressive absorption ---
1373 erBuf_.push(diffused);
1374 T earlyL = T(0), earlyR = T(0);
1375 for (int t = 0; t < numERTaps_; ++t)
1376 {
1377 T rawL = erBuf_.read(erTapsL_[t]) * erGainsL_[t];
1378 T rawR = erBuf_.read(erTapsR_[t]) * erGainsR_[t];
1379 // Progressive 1-pole LP: later taps are darker (wall absorption)
1380 erLPStateL_[t] += erAbsCoeffs_[t] * (rawL - erLPStateL_[t]);
1381 erLPStateR_[t] += erAbsCoeffs_[t] * (rawR - erLPStateR_[t]);
1382 earlyL += erLPStateL_[t];
1383 earlyR += erLPStateR_[t];
1384 }
1385 earlyL *= earlyLvl;
1386 earlyR *= earlyLvl;
1387
1388 // --- ER-to-late gap ---
1389 erToLateBuf_.push(diffused);
1390 T fdnInputRaw = (erToLateSamp > 0)
1391 ? erToLateBuf_.read(erToLateSamp) : diffused;
1392
1393 // --- Parallel allpass diffuser (2-step, 16^2 = 256 echo paths) ---
1394 // Step 1: parallel allpass with different delays create unique IRs.
1395 // Eco: single step, 8 channels (16 echo paths via one Hadamard).
1396 const int n = nLines_;
1397 std::array<T, kFDNSize> diffCh {};
1398 for (int d = 0; d < n; ++d)
1399 diffCh[d] = processAllpass(parAPBufs_[d], parAPDelays_[d],
1400 parAPCoeff_, fdnInputRaw);
1401 if (!eco_)
1402 {
1403 hadamardInPlace(diffCh);
1404 // Step 2: per-channel delay + Hadamard -> 256 unique paths
1405 for (int d = 0; d < kFDNSize; ++d)
1406 diffuserStep2_[d].push(diffCh[d]);
1407 for (int d = 0; d < kFDNSize; ++d)
1408 diffCh[d] = diffuserStep2_[d].read(diffuserStep2Delays_[d]);
1409 hadamardInPlace(diffCh);
1410 }
1411 else
1412 {
1413 hadamard8InPlace(diffCh);
1414 }
1415
1416 // =================================================================
1417 // FDN Core
1418 // =================================================================
1419
1420 // Refresh modulation values. Full: per sample (bit-identical to the
1421 // previous inline computation: same LFO call order and arithmetic).
1422 // Eco: every kEcoCtrlInterval samples (control rate); at reverb mod
1423 // rates (0.1-5 Hz) the position steps are microscopic (< 1e-2 samples)
1424 // and far below audibility.
1425 if (!eco_)
1426 {
1427 T noise = nextFilteredNoise();
1428 noiseND_ = noise * noiseDepth_;
1429 for (int d = 0; d < n; ++d)
1430 {
1431 modACache_[d] = modLFOA_[d].next() * modDA;
1432 modBCache_[d] = modLFOB_[d].next() * modDB;
1433 }
1434 }
1435 else
1436 {
1437 if (ctrlPhase_ == 0)
1438 {
1440 for (int d = 0; d < n; ++d)
1441 {
1442 modACache_[d] = modLFOA_[d].nextStride(kEcoCtrlInterval) * modDA;
1443 modBCache_[d] = modLFOB_[d].nextStride(kEcoCtrlInterval) * modDB;
1444 }
1445 }
1447 }
1448
1449 // Read from the delay lines with LFO + noise modulated delay
1450 std::array<T, kFDNSize> reads {};
1451 for (int d = 0; d < n; ++d)
1452 {
1453 // Noise with alternating polarity (Progenitor-style)
1454 T polarity = (d & 1) ? T(-1) : T(1);
1455 T noiseMod = noiseND_ * polarity;
1456 T readPos = static_cast<T>(fdnDelayLens_[d]) + modACache_[d]
1457 + modBCache_[d] + noiseMod;
1458 readPos = std::max(readPos, T(1));
1459 // Allpass interpolation: preserves HF over hundreds of feedback
1460 // iterations (linear/cubic causes cumulative dulling).
1461 // Formula: z1 = older + frac*(newer - z1) [Progenitor2-style]
1462 {
1463 int readInt = static_cast<int>(readPos);
1464 T frac = readPos - static_cast<T>(readInt);
1465 T newer = fdnDelays_[d].read(readInt);
1466 T older = fdnDelays_[d].read(readInt + 1);
1467 apInterpState_[d] = older + frac * (newer - apInterpState_[d]);
1468 reads[d] = apInterpState_[d];
1469 }
1470 // Post-read serial allpass (density multiplier, Infinity2-style)
1471 reads[d] = processAllpass(intAPBufsB_[d], intAPDelaysB_[d],
1472 intAPCoeff_, reads[d]);
1473 }
1474
1475 // Householder mixing (moderate coupling, keeps lines independent)
1476 std::array<T, kFDNSize> mixed = reads;
1477 householderInPlace(mixed, n);
1478
1479 // Per-line: Jot absorption -> bass shelf -> feedback allpass -> write
1480 for (int d = 0; d < n; ++d)
1481 {
1482 T val = mixed[d];
1483
1484 // --- Jot absorption filter (1st-order pole-zero shelf) ---
1485 // y[n] = b0*x[n] + b1*x[n-1] - a1*y[n-1]
1486 // DC gain = g_mid, Nyquist gain = g_high (exact: T60 anchors),
1487 // transition midpoint at the highCrossover_ frequency.
1488 {
1489 const T y = absB0_[d] * val + absB1_[d] * absX1_[d]
1490 - absA1_[d] * absState_[d];
1491 absX1_[d] = val;
1492 absState_[d] = y;
1493 val = y;
1494 }
1495
1496 // --- Bass shelf (independent LF control) ---
1497 bassState_[d] += bassLPCoeff_ * (val - bassState_[d]);
1498 val += bassState_[d] * (bassRatio_[d] - T(1));
1499
1500 // --- Feedback allpass (2 stages, modulated, Dattorro-style) ---
1501 {
1502 T fbMod = modBCache_[d] * T(0.3);
1504 fbMod, fbAPCoeff_, val, eco_);
1506 fbMod * T(-0.7), fbAPCoeff_, val,
1507 eco_);
1508 }
1509
1510 // --- DC blocker (~23 Hz) ---
1511 dcZ_[d] += dcCoeff_ * (val - dcZ_[d]);
1512 val -= dcZ_[d];
1513
1514 // --- Soft saturation (Branchless fast approximation) ---
1515 // Extract the overshoot beyond [-1, 1] without branching
1516 T exceed = std::max(T(0), val - T(1)) - std::max(T(0), T(-1) - val);
1517
1518 // Limit base value strictly to [-1, 1]
1519 val -= exceed;
1520
1521 // Apply fast rational soft-clip: x / (1 + |x|) ONLY to the exceeding portion
1522 // Approximates tanh curve infinitely closer to limits without unpredictable CPU branch hits
1523 val += exceed / (T(1) + std::abs(exceed));
1524
1525 // --- Pre-write serial allpass (density, Infinity2-style) ---
1527 intAPCoeff_, val);
1528
1529 // --- Feedback IIR smoothing (Verbity technique) ---
1530 val = val * (T(1) - fbSmooth_) + prevFeedback_[d] * fbSmooth_;
1531 prevFeedback_[d] = val;
1532
1533 // --- Write back with per-line diffuser output ---
1534 fdnDelays_[d].push(val + diffCh[d] * kInputGain);
1535 }
1536
1537 // --- Stereo output ---
1538
1539 // Main: orthogonal sign-weighted from all line reads (the first 8
1540 // sign entries are also mutually orthogonal, so Eco keeps the L/R
1541 // decorrelation property)
1542 T lateL = T(0), lateR = T(0);
1543 for (int d = 0; d < n; ++d)
1544 {
1545 lateL += reads[d] * static_cast<T>(kOutSignL_[d]);
1546 lateR += reads[d] * static_cast<T>(kOutSignR_[d]);
1547 }
1548
1549 if (!eco_)
1550 {
1551 // Multi-tap: Dattorro-style reads from different delay positions
1552 for (int t = 0; t < kNumMultiTaps; ++t)
1553 {
1554 int posL = std::max(1, static_cast<int>(
1556 int posR = std::max(1, static_cast<int>(
1558 lateL += fdnDelays_[kMultiTapLineL_[t]].read(posL)
1559 * static_cast<T>(kMultiTapSignL_[t]) * T(0.7);
1560 lateR += fdnDelays_[kMultiTapLineR_[t]].read(posR)
1561 * static_cast<T>(kMultiTapSignR_[t]) * T(0.7);
1562 }
1563
1564 // Feedback allpass taps (Dattorro-style: read from within AP buffers)
1565 for (int d = 0; d < kFDNSize; d += 2)
1566 {
1567 int tapA = std::max(1, fbAPDelaysA_[d] * 2 / 3);
1568 int tapB = std::max(1, fbAPDelaysB_[d] * 3 / 5);
1569 lateL += fbAPBufsA_[d].read(tapA) * static_cast<T>(kOutSignL_[d]) * T(0.35);
1570 lateR += fbAPBufsB_[d].read(tapB) * static_cast<T>(kOutSignR_[d]) * T(0.35);
1571 }
1572 for (int d = 1; d < kFDNSize; d += 2)
1573 {
1574 int tapA = std::max(1, fbAPDelaysA_[d] * 3 / 5);
1575 int tapB = std::max(1, fbAPDelaysB_[d] * 2 / 3);
1576 lateL += fbAPBufsB_[d].read(tapB) * static_cast<T>(kOutSignL_[d]) * T(0.35);
1577 lateR += fbAPBufsA_[d].read(tapA) * static_cast<T>(kOutSignR_[d]) * T(0.35);
1578 }
1579
1580 // Internal serial allpass taps (additional temporal smearing)
1581 for (int d = 0; d < kFDNSize; d += 2)
1582 {
1583 int tapA = std::max(1, intAPDelaysA_[d] * 2 / 3);
1584 int tapB = std::max(1, intAPDelaysB_[d] * 3 / 5);
1585 lateL += intAPBufsA_[d].read(tapA) * static_cast<T>(kOutSignL_[d]) * T(0.2);
1586 lateR += intAPBufsB_[d].read(tapB) * static_cast<T>(kOutSignR_[d]) * T(0.2);
1587 }
1588 for (int d = 1; d < kFDNSize; d += 2)
1589 {
1590 int tapA = std::max(1, intAPDelaysA_[d] * 3 / 5);
1591 int tapB = std::max(1, intAPDelaysB_[d] * 2 / 3);
1592 lateL += intAPBufsB_[d].read(tapB) * static_cast<T>(kOutSignL_[d]) * T(0.2);
1593 lateR += intAPBufsA_[d].read(tapA) * static_cast<T>(kOutSignR_[d]) * T(0.2);
1594 }
1595 } // !eco_ (extra output taps)
1596
1597 const T outNorm = eco_ ? kOutputNormEco : kOutputNorm;
1598 lateL *= lateLvl * outNorm;
1599 lateR *= lateLvl * outNorm;
1600
1601 // --- Output diffusion (L/R decorrelated allpass) ---
1602 for (int s = 0; s < kOutDiffStages; ++s)
1603 {
1605 outDiffCoeff_, lateL);
1607 outDiffCoeff_, lateR);
1608 }
1609
1610 // --- Stereo width (M/S on late tail only) ---
1611 {
1612 T mid = (lateL + lateR) * T(0.5);
1613 T side = (lateL - lateR) * T(0.5);
1614 side *= widthVal;
1615 lateL = mid + side;
1616 lateR = mid - side;
1617 }
1618
1619 // --- Combine early + late ---
1620 T outL = earlyL + lateL;
1621 T outR = earlyR + lateR;
1622
1623 // --- Tone correction EQ (Biquad 12 dB/oct) ---
1624 if (toneHPActive_)
1625 {
1626 outL = toneHPBiquad_.processSample(outL, 0);
1627 outR = toneHPBiquad_.processSample(outR, 1);
1628 }
1629 if (toneLPActive_)
1630 {
1631 outL = toneLPBiquad_.processSample(outL, 0);
1632 outR = toneLPBiquad_.processSample(outR, 1);
1633 }
1634
1635 return { outL, outR };
1636 }
1637
1638 // =========================================================================
1639 // Coefficient update helpers
1640 // =========================================================================
1641
1655 void updateDecayParams() noexcept
1656 {
1657 T decay = decayTime_.load(std::memory_order_relaxed);
1658 if (spec_.sampleRate <= 0 || decay <= T(0)) return;
1659
1660 T sr = static_cast<T>(spec_.sampleRate);
1661 T hdMult = highDecayMult_.load(std::memory_order_relaxed);
1662 T bdMult = bassDecayMult_.load(std::memory_order_relaxed);
1663 T t60Mid = decay;
1664 T t60High = decay * hdMult;
1665 T t60Bass = decay * bdMult;
1666
1667 t60High = std::max(t60High, T(0.05));
1668 t60Bass = std::max(t60Bass, T(0.05));
1669
1670 // Common shelf transition for every line (Jot: a shared shape keeps
1671 // the frequency-dependent T60 coherent across lines). Designed via
1672 // the bilinear transform of the analog prototype
1673 // Ha(s) = gHigh * (s + w1) / (s + w2),
1674 // w1 = K * sqrt(gMid/gHigh), w2 = K * sqrt(gHigh/gMid),
1675 // which pins |H(1)| = gMid and |H(-1)| = gHigh EXACTLY (the T60
1676 // anchors, independent of the crossover) and places the geometric
1677 // midpoint sqrt(gMid*gHigh) at the crossover frequency.
1678 // The clamp keeps tan() below the pole at fs/2 for low sample rates.
1679 const double fsD = static_cast<double>(spec_.sampleRate);
1680 const double fc = std::clamp(
1681 static_cast<double>(highCrossover_.load(std::memory_order_relaxed)),
1682 100.0, 0.45 * fsD);
1683 const double K = std::tan(3.14159265358979323846 * fc / fsD);
1684
1685 for (int d = 0; d < kFDNSize; ++d)
1686 {
1687 // Total loop delay = FDN delay + feedback APs + internal APs
1688 T M = static_cast<T>(fdnDelayLens_[d] + fbAPDelaysA_[d] + fbAPDelaysB_[d]
1689 + intAPDelaysA_[d] + intAPDelaysB_[d]);
1690 if (M < T(1)) M = T(1);
1691
1692 // Per-loop gains: g = 0.001^(M / (T60 * sr))
1693 T gMid = std::pow(T(0.001), M / (t60Mid * sr));
1694 T gHigh = std::pow(T(0.001), M / (t60High * sr));
1695 T gBass = std::pow(T(0.001), M / (t60Bass * sr));
1696
1697 // Pole-zero shelf coefficients (designed in double):
1698 // H(z) = gHigh * ((1+w1) + (w1-1) z^-1) / ((1+w2) + (w2-1) z^-1)
1699 // Unconditionally stable: pole = (1-w2)/(1+w2), |pole| < 1 for w2 > 0.
1700 {
1701 const double gM = std::max(static_cast<double>(gMid), 1e-12);
1702 const double gH = std::clamp(static_cast<double>(gHigh), 1e-12, gM);
1703 const double ratio = std::sqrt(gM / gH);
1704 const double w1 = K * ratio;
1705 const double w2 = K / ratio;
1706 const double inv = 1.0 / (1.0 + w2);
1707 absB0_[d] = static_cast<T>(gH * (1.0 + w1) * inv);
1708 absB1_[d] = static_cast<T>(gH * (w1 - 1.0) * inv);
1709 absA1_[d] = static_cast<T>((w2 - 1.0) * inv);
1710 }
1711
1712 // Bass ratio for independent LF control. The bass shelf multiplies the
1713 // per-line DC loop gain by bassRatio, so cap it so gMid*bassRatio stays
1714 // below 1 - otherwise extreme decay + high bass multiplier make the low
1715 // end self-sustain (a non-decaying drone) instead of ringing out.
1716 bassRatio_[d] = gBass / (gMid + T(1e-10));
1717 const T maxBassRatio = T(0.98) / std::max(gMid, T(1e-6));
1718 bassRatio_[d] = std::clamp(bassRatio_[d], T(0.1), std::min(T(3), maxBassRatio));
1719 }
1720
1721 // Bass crossover filter coefficient
1722 T bassCut = bassCrossover_.load(std::memory_order_relaxed);
1723 bassLPCoeff_ = T(1) - std::exp(T(-6.283185307179586) * bassCut / sr);
1724 }
1725
1726 void updateDelayLengths() noexcept
1727 {
1728 double sr = spec_.sampleRate;
1729 // Nonlinear size mapping: size=0 -> 0.35, size=1 -> 1.0
1730 double sz = 0.35 + 0.65 * static_cast<double>(
1731 size_.load(std::memory_order_relaxed));
1732
1733 for (int d = 0; d < kFDNSize; ++d)
1734 {
1735 // Eco (8 lines) picks every other base delay so the active lines
1736 // still span the full 29.7-160 ms range (mode density stays even).
1737 const int srcIdx = eco_ ? std::min(d * 2, kFDNSize - 1) : d;
1738 int raw = std::max(1, static_cast<int>(
1739 kBaseDelaysMs_[srcIdx] * sz * sr / 1000.0));
1740 fdnDelayLens_[d] = nearestPrime(raw);
1741 }
1742
1743 for (int d = 0; d < kDiffStages; ++d)
1744 diffDelays_[d] = nearestPrime(std::max(1,
1745 static_cast<int>(kDiffDelaysMs_[d] * sr / 1000.0)));
1746
1747 for (int d = 0; d < kFDNSize; ++d)
1748 {
1749 fbAPDelaysA_[d] = nearestPrime(std::max(1,
1750 static_cast<int>(fdnDelayLens_[d] * kFbAPRatioA_)));
1751 fbAPDelaysB_[d] = nearestPrime(std::max(1,
1752 static_cast<int>(fdnDelayLens_[d] * kFbAPRatioB_)));
1753 intAPDelaysA_[d] = nearestPrime(std::max(1,
1754 static_cast<int>(fdnDelayLens_[d] * kIntAPRatioA_)));
1755 intAPDelaysB_[d] = nearestPrime(std::max(1,
1756 static_cast<int>(fdnDelayLens_[d] * kIntAPRatioB_)));
1757 }
1758
1759 for (int s = 0; s < kOutDiffStages; ++s)
1760 {
1761 outDiffDelaysL_[s] = nearestPrime(std::max(1,
1762 static_cast<int>(kOutDiffDelaysMsL_[s] * sr / 1000.0)));
1763 outDiffDelaysR_[s] = nearestPrime(std::max(1,
1764 static_cast<int>(kOutDiffDelaysMsR_[s] * sr / 1000.0)));
1765 }
1766
1767 // Parallel allpass diffuser - step 1 delays
1768 for (int d = 0; d < kFDNSize; ++d)
1769 parAPDelays_[d] = nearestPrime(std::max(1,
1770 static_cast<int>(kParAPDelaysMs_[d] * sr / 1000.0)));
1771
1772 // Multi-channel diffuser - step 2 delays
1773 for (int d = 0; d < kFDNSize; ++d)
1774 diffuserStep2Delays_[d] = nearestPrime(std::max(1,
1775 static_cast<int>(kDiffuserStep2Ms_[d] * sr / 1000.0)));
1776
1778 }
1779
1780 void updateDiffCoeffs() noexcept
1781 {
1782 T diff = diffusion_.load(std::memory_order_relaxed);
1783 for (int d = 0; d < kDiffStages; ++d)
1784 diffCoeffs_[d] = static_cast<T>(kDiffBaseCoeffs_[d]) * diff;
1785 outDiffCoeff_ = T(0.45) * diff;
1786 fbAPCoeff_ = T(0.3) + T(0.4) * diff; // Dattorro range 0.3-0.7
1787 // Feedback IIR smoothing: higher diffusion = more smoothing
1788 fbSmooth_ = T(0.1) + T(0.25) * diff; // range 0.1-0.35
1789 }
1790
1791 void updateModulation() noexcept
1792 {
1793 if (spec_.sampleRate <= 0) return;
1794 T rate = modRate_.load(std::memory_order_relaxed);
1795 for (int i = 0; i < kFDNSize; ++i)
1796 {
1797 modLFOA_[i].setRate(rate * (T(0.7) + T(0.05) * static_cast<T>(i)),
1799 modLFOB_[i].setRate(rate * (T(1.8) + T(0.11) * static_cast<T>(i)),
1801 }
1802 // Noise depth: 40% of LFO depth (breaks periodic patterns)
1803 noiseDepth_ = modDepthA_.load(std::memory_order_relaxed) * T(0.4);
1804 }
1805
1806 // --- Early reflection generation -----------------------------------------
1807
1812 void generateERTapsForType(Type type) noexcept
1813 {
1814 const int cap = eco_ ? kEcoERTaps : kMaxERTaps;
1815 switch (type)
1816 {
1817 case Type::Room: generateERTaps(1.5, 35.0, std::min(30, cap)); break;
1818 case Type::Hall: generateERTaps(5.0, 110.0, std::min(40, cap)); break;
1819 case Type::Chamber: generateERTaps(3.0, 60.0, std::min(35, cap)); break;
1820 case Type::Plate: numERTaps_ = 0; break;
1821 case Type::Spring: generateERTaps(1.0, 25.0, std::min(15, cap)); break;
1822 case Type::Cathedral: generateERTaps(10.0, 160.0, std::min(40, cap)); break;
1823 }
1824 }
1825
1826 void generateERTaps(double minMs, double maxMs, int numTaps) noexcept
1827 {
1828 numERTaps_ = std::min(numTaps, kMaxERTaps);
1829 if (numERTaps_ <= 0) return;
1830
1831 double sr = spec_.sampleRate;
1832 double ratio = (maxMs > minMs) ? maxMs / minMs : 1.0;
1833 double sqrtN = std::sqrt(static_cast<double>(numERTaps_));
1834
1835 for (int t = 0; t < numERTaps_; ++t)
1836 {
1837 double frac = (numERTaps_ > 1)
1838 ? static_cast<double>(t) / static_cast<double>(numERTaps_ - 1) : 0.0;
1839
1840 double msL = minMs * std::pow(ratio, frac);
1841 erTapsL_[t] = std::max(1, static_cast<int>(msL * sr / 1000.0));
1842
1843 double jitter = 1.0 + 0.13 * std::sin(static_cast<double>(t) * 2.39996323);
1844 double msR = msL * jitter;
1845 msR = std::clamp(msR, minMs * 0.8, maxMs * 1.15);
1846 erTapsR_[t] = std::max(1, static_cast<int>(msR * sr / 1000.0));
1847
1848 if (erTapsR_[t] == erTapsL_[t])
1849 erTapsR_[t] = std::max(1, erTapsR_[t] + ((t & 1) ? 1 : -1));
1850
1851 double rawGain = std::pow(0.92, static_cast<double>(t));
1852 erGainsL_[t] = static_cast<T>(rawGain / sqrtN);
1853 erGainsR_[t] = static_cast<T>(rawGain / sqrtN * 0.95);
1854 }
1855
1856 // Progressive frequency absorption: early taps bright, late taps dark
1857 // Cutoff sweeps exponentially from 15kHz (tap 0) to 3kHz (last tap)
1858 for (int t = 0; t < numERTaps_; ++t)
1859 {
1860 T f = static_cast<T>(t) / static_cast<T>(std::max(1, numERTaps_ - 1));
1861 T cutoff = T(15000) * std::pow(T(3000) / T(15000), f);
1862 erAbsCoeffs_[t] = T(1) - std::exp(T(-6.283185307179586) * cutoff
1863 / static_cast<T>(sr));
1864 }
1865 }
1866
1867 // Sieve of Eratosthenes up to kPrimeTableMax, computed once the first
1868 // time `nearestPrime` is called. Keeps parameter-update calls cheap
1869 // (previously O(sqrt(n)) per lookup x ~20 calls per update). For
1870 // `n >= kPrimeTableMax` we fall back to the original trial division -
1871 // that branch is only reached at extreme sample rates / reverb sizes (M4).
1872 static constexpr int kPrimeTableMax = 131072; // 2^17 - covers up to ~2.7 s @48kHz
1873
1874 static const std::vector<uint8_t>& getPrimeSieve() noexcept
1875 {
1876 static const std::vector<uint8_t> sieve = []
1877 {
1878 std::vector<uint8_t> s(static_cast<size_t>(kPrimeTableMax), 1);
1879 s[0] = 0;
1880 s[1] = 0;
1881 for (int i = 2; i * i < kPrimeTableMax; ++i)
1882 if (s[static_cast<size_t>(i)])
1883 for (int j = i * i; j < kPrimeTableMax; j += i)
1884 s[static_cast<size_t>(j)] = 0;
1885 return s;
1886 }();
1887 return sieve;
1888 }
1889
1890 static int nearestPrime(int n) noexcept
1891 {
1892 if (n <= 2) return 2;
1893 if (n < kPrimeTableMax)
1894 {
1895 const auto& sieve = getPrimeSieve();
1896 while (n < kPrimeTableMax && !sieve[static_cast<size_t>(n)])
1897 ++n;
1898 if (n < kPrimeTableMax) return n;
1899 // fall through to trial division for values at/above the table
1900 }
1901 if (n % 2 == 0) ++n;
1902 while (true)
1903 {
1904 bool isPrime = true;
1905 for (int d = 3; d * d <= n; d += 2)
1906 if (n % d == 0) { isPrime = false; break; }
1907 if (isPrime) return n;
1908 n += 2;
1909 }
1910 }
1911
1912 // --- Preset application --------------------------------------------------
1913
1914 // Small helper: atomic-store all preset params in one go. Keeps the big
1915 // preset switch readable and avoids repeating `.store(..., mo_relaxed)`.
1921
1922 void commitPreset(const PresetValues& p) noexcept
1923 {
1924 // D-M006-C1: only write the atomics the user did NOT override since the
1925 // last setType(). acquire-read pairs with the release-mark in each
1926 // setter so a param batched after setType (its bit set) is preserved.
1927 const uint32_t m = userParamMask_.load(std::memory_order_acquire);
1928 if (!(m & kUserSize)) size_.store(p.size, std::memory_order_relaxed);
1929 if (!(m & kUserDecay)) decayTime_.store(p.decay, std::memory_order_relaxed);
1930 if (!(m & kUserDamping)) highDecayMult_.store(p.hdMult, std::memory_order_relaxed);
1931 if (!(m & kUserBassDecay)) bassDecayMult_.store(p.bdMult, std::memory_order_relaxed);
1932 if (!(m & kUserHighXover)) highCrossover_.store(p.hxover, std::memory_order_relaxed);
1933 if (!(m & kUserBassXover)) bassCrossover_.store(p.bxover, std::memory_order_relaxed);
1934 if (!(m & kUserDiffusion)) diffusion_.store(p.diff, std::memory_order_relaxed);
1935 if (!(m & kUserModDepth)) modDepth_.store(p.modDepth, std::memory_order_relaxed);
1936 if (!(m & kUserModRate)) modRate_.store(p.modRate, std::memory_order_relaxed);
1937 if (!(m & kUserEarly)) earlyLevel_.store(p.earlyLvl, std::memory_order_relaxed);
1938 if (!(m & kUserLate)) lateLevel_.store(p.lateLvl, std::memory_order_relaxed);
1939 if (!(m & kUserErToLate)) erToLateMs_.store(p.erToLate, std::memory_order_relaxed);
1940 toneLPActive_ = p.toneLP;
1941 toneHPActive_ = p.toneHP;
1942 }
1943
1945 {
1946 switch (type)
1947 {
1948 case Type::Room:
1949 commitPreset({T(0.22), T(0.5), T(0.40), T(1.1),
1950 T(5000), T(250), T(0.72), T(0.14), T(1.2),
1951 T(1), T(0.8), T(0), false, false});
1952 break;
1953
1954 case Type::Hall:
1955 commitPreset({T(0.68), T(2.2), T(0.32), T(1.3),
1956 T(4500), T(200), T(0.84), T(0.22), T(0.55),
1957 T(0.7), T(1), T(15), false, false});
1958 break;
1959
1960 case Type::Chamber:
1961 commitPreset({T(0.38), T(1.2), T(0.38), T(1.1),
1962 T(5000), T(250), T(0.78), T(0.18), T(0.8),
1963 T(0.9), T(0.9), T(8), false, false});
1964 break;
1965
1966 case Type::Plate:
1967 commitPreset({T(0.14), T(1.5), T(0.55), T(0.8),
1968 T(7000), T(150), T(0.94), T(0.32), T(1.4),
1969 T(0), T(1), T(0), false, false});
1970 numERTaps_ = 0;
1971 break;
1972
1973 case Type::Spring:
1974 commitPreset({T(0.11), T(0.9), T(0.28), T(1.0),
1975 T(4000), T(200), T(0.38), T(0.08), T(0.35),
1976 T(0.5), T(1), T(0), false, false});
1977 break;
1978
1979 case Type::Cathedral:
1980 commitPreset({T(0.98), T(5.0), T(0.24), T(1.5),
1981 T(3500), T(150), T(0.91), T(0.18), T(0.35),
1982 T(0.5), T(1), T(25), false, false});
1983 break;
1984 }
1985
1986 // Sync damping_ from highDecayMult_
1987 T hd = highDecayMult_.load(std::memory_order_relaxed);
1988 T damp = std::clamp((T(1) - hd) / T(0.9), T(0), T(1));
1989 damping_.store(damp, std::memory_order_relaxed);
1990
1992 T md = modDepth_.load(std::memory_order_relaxed);
1993 modDepthA_.store(md * T(30), std::memory_order_relaxed);
1994 modDepthB_.store(md * T(15), std::memory_order_relaxed);
1995
1996 if (spec_.sampleRate > 0)
1997 {
1998 updateDelayLengths(); // also calls updateDecayParams()
2000
2001 T er = erToLateMs_.load(std::memory_order_relaxed);
2002 erToLateSamples_.store(static_cast<int>(
2003 static_cast<T>(spec_.sampleRate) * er / T(1000)),
2004 std::memory_order_relaxed);
2005
2006 T rate = modRate_.load(std::memory_order_relaxed);
2007 for (int i = 0; i < kFDNSize; ++i)
2008 {
2009 modLFOA_[i].prepare(spec_.sampleRate,
2010 rate * (T(0.7) + T(0.05) * static_cast<T>(i)),
2011 static_cast<uint32_t>(i * 7919 + 1));
2012 modLFOB_[i].prepare(spec_.sampleRate,
2013 rate * (T(1.8) + T(0.11) * static_cast<T>(i)),
2014 static_cast<uint32_t>(i * 6271 + 31337));
2015 }
2016
2018 }
2019 }
2020};
2021
2022} // namespace dspark
16-line FDN reverb with Jot absorption and 6 presets.
std::array< int, kFDNSize > fbAPDelaysB_
std::array< T, kFDNSize > modACache_
std::array< T, kFDNSize > absState_
static constexpr double kMultiTapFracR_[kNumMultiTaps]
std::array< RingBuffer< T >, kOutDiffStages > outDiffBufsL_
std::array< RingBuffer< T >, kOutDiffStages > outDiffBufsR_
std::array< int, kMaxERTaps > erTapsL_
Quality
Engine quality / CPU cost trade-off (see setQuality()).
@ Eco
Reduced engine, roughly 3-4x cheaper. For constrained targets.
@ Full
Complete 16-line engine. Default; bit-identical to previous releases.
T getHighDecayMultiplier() const noexcept
static void householderInPlace(std::array< T, kFDNSize > &x, int n) noexcept
In-place Householder reflection for 16 channels.
static constexpr double kIntAPRatioA_
std::atomic< int > erToLateSamples_
std::array< T, kMaxERTaps > erGainsL_
std::array< RingBuffer< T >, kFDNSize > intAPBufsA_
std::array< T, kFDNSize > bassRatio_
std::array< int, kFDNSize > intAPDelaysA_
static constexpr int kMultiTapLineR_[kNumMultiTaps]
std::pair< T, T > processSampleInternal(T input) noexcept
Core per-sample processing - returns wet {L, R}.
static constexpr double kOutDiffDelaysMsL_[kOutDiffStages]
std::array< RingBuffer< T >, kFDNSize > fdnDelays_
void setQuality(Quality q) noexcept
Selects the engine quality / CPU cost trade-off.
Type getType() const noexcept
std::array< RingBuffer< T >, kFDNSize > parAPBufs_
static constexpr int kMultiTapSignR_[kNumMultiTaps]
static constexpr T kOutputNormEco
std::array< T, kFDNSize > modBCache_
std::array< int, kFDNSize > fdnDelayLens_
std::array< RingBuffer< T >, kFDNSize > fbAPBufsA_
T nextFilteredNoise() noexcept
Lowpass-filtered white noise for modulation randomization.
void setToneHighCut(T hz) noexcept
Sets a post-reverb high-cut filter on the wet signal (12 dB/oct).
static const std::vector< uint8_t > & getPrimeSieve() noexcept
std::pair< T, T > processSample(T input) noexcept
Processes a single sample and returns a stereo pair.
void generateERTapsForType(Type type) noexcept
static constexpr int kMultiTapLineL_[kNumMultiTaps]
void refreshCachedParams() noexcept
Pulls the atomic parameters into the block-local cache (audio thread).
static constexpr int kPrimeTableMax
void setDiffusion(T amount) noexcept
void setType(Type type) noexcept
std::array< T, kMaxERTaps > erGainsR_
void setEarlyLevel(T dB) noexcept
static constexpr int kMaxERTaps
Quality getQuality() const noexcept
static constexpr int kEcoCtrlInterval
modulation refresh period (samples)
std::array< T, kMaxERTaps > erLPStateL_
static constexpr double kDiffDelaysMs_[kDiffStages]
static constexpr double kBaseDelaysMs_[kFDNSize]
std::array< RingBuffer< T >, kFDNSize > fbAPBufsB_
static constexpr int kOutSignL_[kFDNSize]
std::atomic< bool > paramsDirty_
std::array< int, kFDNSize > intAPDelaysB_
static int nearestPrime(int n) noexcept
std::array< T, kFDNSize > absX1_
T getBassDecayMultiplier() const noexcept
static constexpr int kOutSignR_[kFDNSize]
void setModulation(T amount) noexcept
T getHighCrossover() const noexcept
std::array< T, kFDNSize > absB1_
std::array< int, kOutDiffStages > outDiffDelaysL_
void setBassCrossover(T hz) noexcept
Frequency below which bass decay multiplier applies.
void setPreDelay(T ms) noexcept
static constexpr int kDiffStages
std::array< T, kFDNSize > absB0_
void setMix(T dryWet) noexcept
void drainPendingChanges() noexcept
Drains deferred parameter changes on the audio thread.
std::array< T, kFDNSize > apInterpState_
void reset() noexcept
Resets all internal delay buffers, filters, and LFO phases.
static void hadamard8InPlace(std::array< T, kFDNSize > &x) noexcept
In-place Hadamard 8x8 on the first 8 elements (Eco quality).
std::array< int, kDiffStages > diffDelays_
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
T getBassCrossover() const noexcept
void setLateLevel(T dB) noexcept
std::array< RingBuffer< T >, kFDNSize > intAPBufsB_
std::array< SmoothRandomLFO, kFDNSize > modLFOA_
void setToneLowCut(T hz) noexcept
Sets a post-reverb low-cut filter on the wet signal (12 dB/oct).
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
static constexpr double kIntAPRatioB_
std::atomic< bool > toneDirty_
void setDamping(T amount) noexcept
Sets high-frequency damping (0 = bright, 1 = dark).
static constexpr int kMultiTapSignL_[kNumMultiTaps]
static constexpr T kInputGain
std::array< T, kMaxERTaps > erAbsCoeffs_
static constexpr T kOutputNorm
T processAllpassModulated(RingBuffer< T > &buf, int baseDelay, T modAmount, T coeff, T input, bool linearInterp) noexcept
void setErToLateDelay(T ms) noexcept
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an audio block in-place with zero allocations.
void markUserParam(uint32_t bit) noexcept
std::atomic< uint32_t > userParamMask_
void setSize(T size) noexcept
std::atomic< bool > presetDirty_
std::array< T, kDiffStages > diffCoeffs_
void commitPreset(const PresetValues &p) noexcept
std::array< T, kFDNSize > dcZ_
void setModRate(T hz) noexcept
std::array< int, kFDNSize > parAPDelays_
std::array< int, kFDNSize > fbAPDelaysA_
static constexpr int kNumMultiTaps
static constexpr int kOutDiffStages
void updateDecayParams() noexcept
Computes per-line Jot absorption filter coefficients.
void setWidth(T width) noexcept
Sets stereo width of the late reverb tail.
std::array< T, kFDNSize > absA1_
@ Chamber
Recording studio chamber, warm, balanced.
@ Spring
Spring reverb, bouncy vintage character.
@ Cathedral
Large cathedral, immense decay, vast space.
@ Hall
Concert hall, spacious, long smooth tail.
@ Plate
Metal plate, dense shimmer, no early reflections.
@ Room
Small room, short decay, dense close reflections.
std::array< RingBuffer< T >, kDiffStages > diffBufs_
std::array< int, kOutDiffStages > outDiffDelaysR_
static constexpr double kParAPDelaysMs_[kFDNSize]
std::array< SmoothRandomLFO, kFDNSize > modLFOB_
void setHighDecayMultiplier(T mult) noexcept
Sets HF decay as a multiplier of mid decay time.
void generateERTaps(double minMs, double maxMs, int numTaps) noexcept
std::array< T, kFDNSize > bassState_
static constexpr double kMultiTapFracL_[kNumMultiTaps]
std::atomic< Quality > quality_
static void hadamardInPlace(std::array< T, kFDNSize > &x) noexcept
In-place Hadamard 16x16 via Fast Walsh-Hadamard butterfly.
static constexpr double kDiffBaseCoeffs_[kDiffStages]
void setHighCrossover(T hz) noexcept
Frequency where the HF decay transition is centered.
void prepare(const AudioSpec &spec)
Prepares the reverberation engine and allocates required memory.
void setDecay(T seconds) noexcept
static constexpr T intAPCoeff_
static constexpr int kEcoLines
FDN lines in Eco quality.
static constexpr double kDiffuserStep2Ms_[kFDNSize]
std::array< T, kMaxERTaps > erLPStateR_
std::array< RingBuffer< T >, kFDNSize > diffuserStep2_
T processAllpass(RingBuffer< T > &buf, int delay, T coeff, T input) noexcept
std::array< int, kMaxERTaps > erTapsR_
std::array< T, kFDNSize > prevFeedback_
static constexpr double kOutDiffDelaysMsR_[kOutDiffStages]
void setBassDecayMultiplier(T mult) noexcept
Sets bass decay as a multiplier of mid decay time.
std::atomic< bool > qualityDirty_
static constexpr double kFbAPRatioB_
std::atomic< int > preDelaySamples_
static constexpr int kFbAPStages
std::array< int, kFDNSize > diffuserStep2Delays_
static constexpr double kFbAPRatioA_
static constexpr int kFDNSize
static constexpr int kEcoERTaps
early-reflection tap cap in Eco
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
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
T processSample(T input, int channel) noexcept
Processes a single sample for a specific channel.
Definition Biquad.h:694
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
Pre-allocated, SIMD-friendly dry/wet blender for real-time audio.
Definition DryWetMixer.h:72
Power-of-two circular buffer with compile-time interpolated read access.
Definition RingBuffer.h:58
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
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
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:639
Hermite-interpolated random noise generator for organic modulation.
void setRate(T rate, double sr) noexcept
T nextStride(int stride) noexcept
Advances the LFO by stride samples in one call.
void prepare(double sr, T rate, uint32_t seed) noexcept
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:67
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43
static BiquadCoeffs makeHighPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
High-pass filter.
Definition Biquad.h:127
static BiquadCoeffs makeLowPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Low-pass filter.
Definition Biquad.h:103