DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
AlgorithmicReverb.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
198#include "../Core/DryWetMixer.h"
199#include "../Core/DspMath.h"
200#include "../Core/AudioSpec.h"
201#include "../Core/AudioBuffer.h"
202#include "../Core/DenormalGuard.h"
203#include "../Core/Biquad.h"
204#include "../Core/StateBlob.h"
205#include "../Core/SimdOps.h"
206
207#include <algorithm>
208#include <array>
209#include <atomic>
210#include <cmath>
211#include <cstddef>
212#include <cstdint>
213#include <utility>
214#include <vector>
215
216namespace dspark {
217
224template <FloatType T>
226{
227public:
229 enum class Type
230 {
231 Room,
232 Hall,
233 Chamber,
234 Plate,
235 Spring,
236 Cathedral
237 };
238
242 enum class Quality
243 {
244 Full,
245 Eco
246 };
247
248 ~AlgorithmicReverb() = default; // non-virtual: leaf class (no virtual dispatch)
249
250 // -- Lifecycle --------------------------------------------------------------
251
260 void prepare(const AudioSpec& spec)
261 {
262 if (!spec.isValid()) return; // release-safe: keep previous state
263
264 spec_ = spec;
265 mixer_.prepare(spec);
266 const double sr = spec.sampleRate;
267
268 // Re-derive the sample counts stored at set-time: after a re-prepare
269 // at a different rate they would keep the OLD rate's sample count.
270 preDelaySamples_.store(msToSamples(preDelayMs_.load(std::memory_order_relaxed)),
271 std::memory_order_relaxed);
272 erToLateSamples_.store(msToSamples(erToLateMs_.load(std::memory_order_relaxed)),
273 std::memory_order_relaxed);
274
275 const auto samples = [sr](double ms) { return static_cast<int>(ms * sr / 1000.0) + 8; };
276 for (int c = 0; c < 2; ++c)
277 {
278 // The early taps read the raw input straight from here.
279 preDelay_[c].prepare(samples(kMaxPreDelayMs + kMaxErMs));
280 // The diffused ring feeds the early taps (up to 170 ms, their
281 // contralateral copies up to 1.3x that) and the late injection
282 // (ER-to-late gap plus the injection spread).
283 ring_[c].prepare(samples(kMaxErToLateMs + kMaxInjectMs));
284 for (auto& ap : inAP_[c]) ap.prepare(samples(kMaxInDiffMs));
285 for (int k = 0; k < kOutStages; ++k)
286 {
287 outAP_[c][k].prepare(samples(kOutDiffMs_[c][k]));
288 outAPLen_[c][k] = nearestPrime(std::max(1, static_cast<int>(kOutDiffMs_[c][k] * sr / 1000.0)));
289 }
290 }
291 const int maxLine = samples(kBaseDelaysMs_[kMaxLines - 1] + kModMaxMs) + 4;
292 lines_.prepare(kMaxLines, maxLine);
294
295 // Spring dispersion: stretching factor K puts the chirp band edge at
296 // fs / (2K) ~ kSpringCutHz; a 4th-order Butterworth just below it
297 // removes the stretched allpasses' mirrored bands.
298 springK_ = std::max(1, static_cast<int>(std::lround(sr / (2.0 * kSpringCutHz))));
299 springP_ = 1;
300 while (springP_ < springK_ + 1) springP_ <<= 1;
301 springHist_.assign(static_cast<std::size_t>(kSprings * (kSpringStages + 1) * springP_), T(0));
302 {
303 const double fc = 0.9 * sr / (2.0 * springK_);
304 const double qs[2] = { 0.5411961001461970, 1.3065629648763764 };
305 for (int k = 0; k < 2; ++k)
306 {
307 const auto c = BiquadCoeffs::makeLowPass(sr, fc, qs[k]);
308 springLP_[k] = { static_cast<T>(c.b0), static_cast<T>(c.b1), static_cast<T>(c.b2),
309 static_cast<T>(c.a1), static_cast<T>(c.a2) };
310 }
311 }
312
313 {
314 const auto c = BiquadCoeffs::makeHighPass(sr, kSubsonicHz);
315 subC_ = { static_cast<T>(c.b0), static_cast<T>(c.b1), static_cast<T>(c.b2),
316 static_cast<T>(c.a1), static_cast<T>(c.a2) };
317 }
318 {
319 const auto c = BiquadCoeffs::makeHighPass(sr, std::min(kCoherenceHz, 0.45 * sr), kCoherenceQ);
320 cohC_ = { static_cast<T>(c.b0), static_cast<T>(c.b1), static_cast<T>(c.b2),
321 static_cast<T>(c.a1), static_cast<T>(c.a2) };
322 }
323 shadowCoeff_ = static_cast<T>(1.0 - std::exp(-6.283185307179586 * kShadowHz / sr));
324 erGlidePerSample_ = static_cast<T>(1.0 / (0.02 * sr));
325 maxReadPos_ = static_cast<T>(maxLine - 2);
326
327 eco_ = quality_.load(std::memory_order_relaxed) == Quality::Eco;
328 spring_ = type_.load(std::memory_order_relaxed) == Type::Spring;
330 prepared_ = true;
331
332 applyPreset(type_.load(std::memory_order_relaxed));
333 // prepare() applied whatever the caller configured before it, so no
334 // drain is needed on the first processBlock.
335 presetDirty_.store(false, std::memory_order_relaxed);
336 paramsDirty_.store(false, std::memory_order_relaxed);
337 qualityDirty_.store(false, std::memory_order_relaxed);
338 toneDirty_.store(true, std::memory_order_relaxed);
339 drainTone();
340 reset();
341 }
342
352 void processBlock(AudioBufferView<T> buffer) noexcept
353 {
354 DenormalGuard guard;
355 const int nCh = std::min(buffer.getNumChannels(), 2);
356 const int nS = buffer.getNumSamples();
357 if (nCh == 0 || nS == 0 || !prepared_) return;
358
359 // Front-door non-finite guard: the FDN is fully recursive, so a single
360 // NaN/Inf input would poison the tail forever. No-op on finite input.
361 for (int ch = 0; ch < nCh; ++ch)
362 {
363 T* d = buffer.getChannel(ch);
364 for (int i = 0; i < nS; ++i)
365 if (!std::isfinite(d[i])) d[i] = T(0);
366 }
367
369
370 mixer_.pushDry(buffer);
372
373 T* chL = buffer.getChannel(0);
374 T* chR = nCh >= 2 ? buffer.getChannel(1) : nullptr;
375 alignas(64) T outL[kChunk];
376 alignas(64) T outR[kChunk];
377 for (int start = 0; start < nS; start += kChunk)
378 {
379 const int n = std::min(kChunk, nS - start);
380 processChunk(chL + start, (chR ? chR : chL) + start, outL, outR, n);
381 if (chR)
382 {
383 std::copy(outL, outL + n, chL + start);
384 std::copy(outR, outR + n, chR + start);
385 }
386 else
387 {
388 for (int i = 0; i < n; ++i)
389 chL[start + i] = (outL[i] + outR[i]) * T(0.5);
390 }
391 }
392
393 mixer_.mixWet(buffer, mix_.load(std::memory_order_relaxed));
394 }
395
406 [[nodiscard]] std::pair<T, T> processSample(T input) noexcept
407 {
408 if (!prepared_) return { T(0), T(0) };
409 DenormalGuard guard;
412 // Front-door non-finite guard (see processBlock).
413 if (!std::isfinite(input)) input = T(0);
414 T outL = T(0), outR = T(0);
415 processChunk(&input, &input, &outL, &outR, 1);
416 return { outL, outR };
417 }
418
423 void reset() noexcept
424 {
425 for (int c = 0; c < 2; ++c)
426 {
427 preDelay_[c].clear();
428 ring_[c].clear();
429 for (auto& ap : inAP_[c]) ap.clear();
430 for (auto& ap : outAP_[c]) ap.clear();
431 erLP_[c].fill(T(0));
432 erShadowLP_[c].fill(T(0));
433 erGain_[c] = erGainTarget_[c];
435 subZ_[c].fill(T(0));
436 }
437 lines_.clear();
438 loopAP_.clear();
439 cohZ_.fill(T(0));
440 std::fill(springHist_.begin(), springHist_.end(), T(0));
441 springW_ = 0;
442 for (auto& st : springLPState_) st.fill(T(0));
443 apY1_.fill(T(0));
444 jotX1_.fill(T(0));
445 jotY1_.fill(T(0));
446 bassX1_.fill(T(0));
447 bassY1_.fill(T(0));
448
449 const double sr = spec_.sampleRate > 0 ? spec_.sampleRate : 48000.0;
450 const T rate = modRate_.load(std::memory_order_relaxed);
451 for (int i = 0; i < kMaxLines; ++i)
452 {
453 lfo_[i].prepare(sr, rate * lfoRateFactor(i), lfoSeed(i));
454 if (i < kMaxLines / 2)
455 {
456 rotLfo_[i].prepare(sr, rotRate() * rotRateFactor(i), rotSeed(i));
457 rotC_[i] = T(1);
458 rotS_[i] = T(0);
459 }
460 lenCur_[i] = static_cast<T>(lenTarget_[i]);
461 loopAPCur_[i] = static_cast<T>(loopAPLen_[i]);
462 pos_[i] = std::max(lenCur_[i], T(kMinReadPos));
463 posInc_[i] = T(0);
464 }
465 ctrlPhase_ = 0;
466 loopAPGliding_ = false;
467
470 mixer_.reset();
471 }
472
473 // =========================================================================
474 // Level 1: Simple API
475 // =========================================================================
476
484 void setType(Type type) noexcept
485 {
486 // Wild enum values (a corrupted blob, a stray cast) clamp into range.
487 const int t = std::clamp(static_cast<int>(type), 0,
488 static_cast<int>(Type::Cathedral));
489 type_.store(static_cast<Type>(t), std::memory_order_relaxed);
490 // A preset load re-establishes the baseline: forget prior overrides so
491 // the preset's own values apply, unless a setter is called AFTER this.
492 userParamMask_.store(0u, std::memory_order_relaxed);
493 presetDirty_.store(true, std::memory_order_release);
494 }
495
521 void setQuality(Quality q) noexcept
522 {
524 std::memory_order_relaxed);
525 qualityDirty_.store(true, std::memory_order_release);
526 }
527
537 void setDecay(T seconds) noexcept
538 {
539 if (!std::isfinite(seconds)) return;
540 decayTime_.store(std::clamp(seconds, T(0.1), T(30)),
541 std::memory_order_relaxed);
543 paramsDirty_.store(true, std::memory_order_release);
544 }
545
547 void setMix(T dryWet) noexcept
548 {
549 if (!std::isfinite(dryWet)) return;
550 mix_.store(std::clamp(dryWet, T(0), T(1)), std::memory_order_relaxed);
551 }
552
553 // =========================================================================
554 // Level 2: Intermediate API
555 // =========================================================================
556
562 void setSize(T size) noexcept
563 {
564 if (!std::isfinite(size)) return;
565 size_.store(std::clamp(size, T(0.01), T(1)), std::memory_order_relaxed);
567 paramsDirty_.store(true, std::memory_order_release);
568 }
569
576 void setDamping(T amount) noexcept
577 {
578 if (!std::isfinite(amount)) return;
579 T clamped = std::clamp(amount, T(0), T(1));
580 damping_.store(clamped, std::memory_order_relaxed);
581 highDecayMult_.store(T(1) - clamped * T(0.9), std::memory_order_relaxed);
583 paramsDirty_.store(true, std::memory_order_release);
584 }
585
587 void setPreDelay(T ms) noexcept
588 {
589 if (!std::isfinite(ms)) return;
590 T clamped = std::clamp(ms, T(0), T(kMaxPreDelayMs));
591 preDelayMs_.store(clamped, std::memory_order_relaxed);
592 if (spec_.sampleRate > 0)
593 preDelaySamples_.store(msToSamples(clamped), std::memory_order_relaxed);
594 }
595
603 void setDiffusion(T amount) noexcept
604 {
605 if (!std::isfinite(amount)) return;
606 diffusion_.store(std::clamp(amount, T(0), T(1)), std::memory_order_relaxed);
608 paramsDirty_.store(true, std::memory_order_release);
609 }
610
616 void setModulation(T amount) noexcept
617 {
618 if (!std::isfinite(amount)) return;
619 modDepth_.store(std::clamp(amount, T(0), T(1)), std::memory_order_relaxed);
621 paramsDirty_.store(true, std::memory_order_release);
622 }
623
633 void setWidth(T width) noexcept
634 {
635 if (!std::isfinite(width)) return;
636 width_.store(std::clamp(width, T(0), T(2)), std::memory_order_relaxed);
637 }
638
640 void setErToLateDelay(T ms) noexcept
641 {
642 if (!std::isfinite(ms)) return;
643 T clamped = std::clamp(ms, T(0), T(kMaxErToLateMs));
644 erToLateMs_.store(clamped, std::memory_order_relaxed);
646 if (spec_.sampleRate > 0)
647 erToLateSamples_.store(msToSamples(clamped), std::memory_order_relaxed);
648 }
649
650 // =========================================================================
651 // Level 3: Expert API - Frequency-Dependent Decay
652 // =========================================================================
653
663 void setHighDecayMultiplier(T mult) noexcept
664 {
665 if (!std::isfinite(mult)) return;
666 T clamped = std::clamp(mult, T(0.05), T(1));
667 highDecayMult_.store(clamped, std::memory_order_relaxed);
668 damping_.store(std::clamp((T(1) - clamped) / T(0.9), T(0), T(1)),
669 std::memory_order_relaxed);
671 paramsDirty_.store(true, std::memory_order_release);
672 }
673
684 void setBassDecayMultiplier(T mult) noexcept
685 {
686 if (!std::isfinite(mult)) return;
687 bassDecayMult_.store(std::clamp(mult, T(0.3), T(3)),
688 std::memory_order_relaxed);
690 paramsDirty_.store(true, std::memory_order_release);
691 }
692
706 void setHighCrossover(T hz) noexcept
707 {
708 if (!std::isfinite(hz)) return;
709 highCrossover_.store(std::clamp(hz, T(1000), T(16000)),
710 std::memory_order_relaxed);
712 paramsDirty_.store(true, std::memory_order_release);
713 }
714
723 void setBassCrossover(T hz) noexcept
724 {
725 if (!std::isfinite(hz)) return;
726 bassCrossover_.store(std::clamp(hz, T(50), T(500)),
727 std::memory_order_relaxed);
729 paramsDirty_.store(true, std::memory_order_release);
730 }
731
732 // =========================================================================
733 // Level 3: Expert API - Tone & Levels
734 // =========================================================================
735
742 void setEarlyLevel(T dB) noexcept
743 {
744 if (!std::isfinite(dB)) return;
745 earlyLevel_.store(decibelsToGain(std::clamp(dB, T(-60), T(6))), std::memory_order_relaxed);
747 }
748
750 void setLateLevel(T dB) noexcept
751 {
752 if (!std::isfinite(dB)) return;
753 lateLevel_.store(decibelsToGain(std::clamp(dB, T(-60), T(6))), std::memory_order_relaxed);
755 }
756
758 void setModRate(T hz) noexcept
759 {
760 if (!std::isfinite(hz)) return;
761 modRate_.store(std::clamp(hz, T(0.1), T(5)), std::memory_order_relaxed);
763 paramsDirty_.store(true, std::memory_order_release);
764 }
765
770 void setToneLowCut(T hz) noexcept
771 {
772 if (!std::isfinite(hz)) return;
773 toneLowCutHz_.store(hz, std::memory_order_relaxed);
774 toneDirty_.store(true, std::memory_order_release);
775 }
776
781 void setToneHighCut(T hz) noexcept
782 {
783 if (!std::isfinite(hz)) return;
784 toneHighCutHz_.store(hz, std::memory_order_relaxed);
785 toneDirty_.store(true, std::memory_order_release);
786 }
787
788 // =========================================================================
789 // Getters
790 // =========================================================================
791
792 [[nodiscard]] Type getType() const noexcept { return type_.load(std::memory_order_relaxed); }
793 [[nodiscard]] Quality getQuality() const noexcept { return quality_.load(std::memory_order_relaxed); }
794 [[nodiscard]] T getDecay() const noexcept { return decayTime_.load(std::memory_order_relaxed); }
795 [[nodiscard]] T getMix() const noexcept { return mix_.load(std::memory_order_relaxed); }
796 [[nodiscard]] T getSize() const noexcept { return size_.load(std::memory_order_relaxed); }
797 [[nodiscard]] T getDamping() const noexcept { return damping_.load(std::memory_order_relaxed); }
798 [[nodiscard]] T getDiffusion() const noexcept { return diffusion_.load(std::memory_order_relaxed); }
799 [[nodiscard]] T getModulation() const noexcept { return modDepth_.load(std::memory_order_relaxed); }
800 [[nodiscard]] T getModRate() const noexcept { return modRate_.load(std::memory_order_relaxed); }
801 [[nodiscard]] T getPreDelay() const noexcept { return preDelayMs_.load(std::memory_order_relaxed); }
802 [[nodiscard]] T getErToLateDelay() const noexcept { return erToLateMs_.load(std::memory_order_relaxed); }
803 [[nodiscard]] T getHighDecayMultiplier() const noexcept { return highDecayMult_.load(std::memory_order_relaxed); }
804 [[nodiscard]] T getBassDecayMultiplier() const noexcept { return bassDecayMult_.load(std::memory_order_relaxed); }
805 [[nodiscard]] T getWidth() const noexcept { return width_.load(std::memory_order_relaxed); }
806 [[nodiscard]] T getHighCrossover() const noexcept { return highCrossover_.load(std::memory_order_relaxed); }
808 [[nodiscard]] T getEarlyLevel() const noexcept { return levelDb(earlyLevel_.load(std::memory_order_relaxed)); }
810 [[nodiscard]] T getLateLevel() const noexcept { return levelDb(lateLevel_.load(std::memory_order_relaxed)); }
811 [[nodiscard]] T getBassCrossover() const noexcept { return bassCrossover_.load(std::memory_order_relaxed); }
812
814 [[nodiscard]] std::vector<uint8_t> getState() const
815 {
816 StateWriter w(stateId("ARVB"), 1);
817 w.write("type", static_cast<int32_t>(type_.load(std::memory_order_relaxed)));
818 w.write("quality", static_cast<int32_t>(quality_.load(std::memory_order_relaxed)));
819 w.write("decay", static_cast<float>(decayTime_.load(std::memory_order_relaxed)));
820 w.write("size", static_cast<float>(size_.load(std::memory_order_relaxed)));
821 w.write("damping", static_cast<float>(damping_.load(std::memory_order_relaxed)));
822 w.write("diffusion", static_cast<float>(diffusion_.load(std::memory_order_relaxed)));
823 w.write("modDepth", static_cast<float>(modDepth_.load(std::memory_order_relaxed)));
824 w.write("modRate", static_cast<float>(modRate_.load(std::memory_order_relaxed)));
825 w.write("preDelay", static_cast<float>(preDelayMs_.load(std::memory_order_relaxed)));
826 w.write("erToLate", static_cast<float>(erToLateMs_.load(std::memory_order_relaxed)));
827 w.write("mix", static_cast<float>(mix_.load(std::memory_order_relaxed)));
828 w.write("width", static_cast<float>(width_.load(std::memory_order_relaxed)));
829 w.write("highDecay", static_cast<float>(highDecayMult_.load(std::memory_order_relaxed)));
830 w.write("bassDecay", static_cast<float>(bassDecayMult_.load(std::memory_order_relaxed)));
831 w.write("highXover", static_cast<float>(highCrossover_.load(std::memory_order_relaxed)));
832 w.write("bassXover", static_cast<float>(bassCrossover_.load(std::memory_order_relaxed)));
833 w.write("earlyDb", static_cast<float>(getEarlyLevel()));
834 w.write("lateDb", static_cast<float>(getLateLevel()));
835 w.write("toneLowCut", static_cast<float>(toneLowCutHz_.load(std::memory_order_relaxed)));
836 w.write("toneHighCut", static_cast<float>(toneHighCutHz_.load(std::memory_order_relaxed)));
837 return w.blob();
838 }
839
841 bool setState(const uint8_t* data, size_t size)
842 {
843 StateReader r(data, size);
844 if (!r.isValid() || r.processorId() != stateId("ARVB")) return false;
845 setType(static_cast<Type>(r.read("type", 0)));
846 // Older blobs have no "quality" key: default 0 = Full.
847 setQuality(r.read("quality", 0) == 1 ? Quality::Eco : Quality::Full);
848 setDecay(static_cast<T>(r.read("decay", 1.0f)));
849 setSize(static_cast<T>(r.read("size", 0.5f)));
850 setDamping(static_cast<T>(r.read("damping", 0.5f)));
851 setDiffusion(static_cast<T>(r.read("diffusion", 0.7f)));
852 setModulation(static_cast<T>(r.read("modDepth", 0.1f)));
853 setModRate(static_cast<T>(r.read("modRate", 1.0f)));
854 setPreDelay(static_cast<T>(r.read("preDelay", 0.0f)));
855 setErToLateDelay(static_cast<T>(r.read("erToLate", 0.0f)));
856 setMix(static_cast<T>(r.read("mix", 0.3f)));
857 setWidth(static_cast<T>(r.read("width", 1.0f)));
858 setHighDecayMultiplier(static_cast<T>(r.read("highDecay", 0.5f)));
859 setBassDecayMultiplier(static_cast<T>(r.read("bassDecay", 1.2f)));
860 setHighCrossover(static_cast<T>(r.read("highXover", 5000.0f)));
861 setBassCrossover(static_cast<T>(r.read("bassXover", 200.0f)));
862 setEarlyLevel(static_cast<T>(r.read("earlyDb", 0.0f)));
863 setLateLevel(static_cast<T>(r.read("lateDb", 0.0f)));
864 const float lo = r.read("toneLowCut", -1.0f);
865 const float hi = r.read("toneHighCut", -1.0f);
866 if (lo > 0.0f) setToneLowCut(static_cast<T>(lo));
867 if (hi > 0.0f) setToneHighCut(static_cast<T>(hi));
868 return true;
869 }
870
871protected:
872 // --- Constants -----------------------------------------------------------
873
874 static constexpr int kMaxLines = 32;
875 static constexpr int kEcoLines = 16;
876 static constexpr int kInStages = 4;
877 static constexpr int kMaxERTaps = 128;
878 static constexpr int kEcoERTaps = 48;
879 static constexpr int kShadowBins = 2;
880 static constexpr int kMaxDiscreteER = 7;
881 static constexpr int kERGroups = 4;
882 static constexpr int kCtrl = 16;
883 static constexpr int kChunk = 64;
884
885 static constexpr double kMaxPreDelayMs = 200.0;
886 static constexpr double kMaxErToLateMs = 200.0;
887 static constexpr double kMaxErMs = 170.0;
888 static constexpr double kMaxInjectMs = 8.0;
889 static constexpr double kMaxInDiffMs = 2.0;
890 static constexpr double kMaxLoopApMs = 5.5;
891 static constexpr double kCoherenceHz = 355.0;
892 static constexpr double kCoherenceQ = 0.74;
893 static constexpr double kShadowHz = 1000.0;
894 static constexpr double kShadowHF = 0.5;
895 static constexpr double kModMaxMs = 2.0;
900 static constexpr double kRotMaxRad = 10.0;
901 static constexpr double kRotRateHz = 0.35;
905 static constexpr int kRotPairStride = 4;
906 static constexpr double kGlideMs = 60.0;
907 static constexpr double kMaxGlideSpeed = 0.04;
908 static constexpr double kSubsonicHz = 20.0;
909 static constexpr T kMinReadPos = T(2);
910 static constexpr T kSoftLimit = T(2);
911
914 static constexpr T kOutGain = T(0.25);
915
916 // FDN base delay times in ms (at size = 1); lengths are rounded to primes.
917 // About 3.7 s of delay in all: the modal density (modes per Hz equals the
918 // total delay in seconds) keeps the modes overlapping, so they do not
919 // ring out on their own.
920 static constexpr double kBaseDelaysMs_[kMaxLines] = {
921 44.5, 46.5, 49.7, 52.3, 56.1, 58.9, 61.9, 65.2,
922 68.6, 73.7, 76.2, 79.8, 86.4, 89.8, 95.7, 101.2,
923 105.4, 112.5, 116.7, 126.4, 132.1, 140.6, 146.9, 154.1,
924 162.9, 175.8, 184.1, 192.8, 204.0, 218.5, 224.2, 240.1
925 };
926
927 // In-loop allpass delays (ms): short and fixed in time, shuffled against
928 // the line lengths so no line's total is a near multiple of another's.
929 static constexpr double kLoopApMs_[kMaxLines] = {
930 3.55, 1.47, 3.80, 1.96, 1.84, 2.33, 4.41, 4.90,
931 1.10, 1.35, 4.29, 3.43, 4.16, 4.53, 2.08, 3.06,
932 1.22, 2.94, 3.92, 3.18, 2.57, 4.04, 4.65, 2.45,
933 2.69, 4.78, 3.67, 1.71, 2.20, 1.59, 2.82, 3.31
934 };
935
936 // Input diffusion allpass delays (ms) of the late-field feed,
937 // decorrelated between channels. Short, so the frequencies an allpass
938 // holds back longest (its group-delay peaks) are not heard ringing on
939 // through a short decay.
940 static constexpr double kInDiffMs_[2][kInStages] = {
941 { 0.36, 0.69, 1.10, 1.65 },
942 { 0.43, 0.80, 1.20, 1.79 }
943 };
944 static constexpr double kInDiffCoeffs_[kInStages] = { 0.75, 0.72, 0.68, 0.64 };
945
946 // Output diffusion allpass delays (ms) on the late sum, per channel: each
947 // echo of the tank leaves as a short dense burst, so the late field is
948 // smooth from its first milliseconds.
949 static constexpr int kOutStages = 2;
950 static constexpr double kOutDiffMs_[2][kOutStages] = {
951 { 1.13, 2.41 },
952 { 1.31, 2.67 }
953 };
954
955 static constexpr double kMeanInjectMs = 4.0;
956 // Late injection taps (ms after the ER-to-late gap), one per line.
957 static constexpr double kInjectMs_[kMaxLines] = {
958 7.14, 3.31, 0.76, 0.25, 5.86, 2.04, 4.84, 5.35,
959 5.61, 6.88, 0.00, 7.65, 2.80, 1.02, 7.90, 1.53,
960 1.27, 4.33, 6.63, 2.55, 3.06, 6.12, 5.10, 6.37,
961 3.82, 1.78, 3.57, 7.39, 2.29, 0.51, 4.08, 4.59
962 };
963 static constexpr int kInjectSign_[kMaxLines] = {
964 1, 1, -1, 1, -1, 1, 1, -1,
965 -1, 1, 1, -1, 1, -1, 1, 1,
966 1, 1, 1, -1, 1, 1, 1, 1,
967 1, -1, 1, 1, -1, 1, -1, -1
968 };
969
970 // Orthogonal stereo output sign vectors (inner product 0; the first 8
971 // entries are orthogonal too, so Eco keeps the decorrelation).
972 static constexpr int kOutSignL_[kMaxLines] = {
973 -1, -1, 1, -1, -1, 1, 1, 1,
974 -1, -1, 1, 1, -1, -1, -1, 1,
975 -1, -1, 1, -1, -1, 1, 1, -1,
976 1, 1, -1, -1, -1, -1, -1, 1
977 };
978 static constexpr int kOutSignR_[kMaxLines] = {
979 -1, 1, 1, 1, -1, -1, 1, -1,
980 -1, 1, 1, -1, -1, 1, -1, -1,
981 -1, 1, 1, 1, -1, -1, 1, 1,
982 1, -1, -1, 1, -1, 1, -1, -1
983 };
984
985 // Spring tanks (Type::Spring): six springs per side (even = left), all
986 // of different lengths; Eco keeps one per side.
987 static constexpr int kSprings = 12;
988 static constexpr int kEcoSprings = 2;
989 static constexpr int kSpringStages = 72;
990 static constexpr int kEcoSpringStages = 40;
991 static constexpr double kSpringJitterMs = 3.0;
994 static double springBaseMs(int s) noexcept
995 {
996 return 33.0 + 27.0 * s / (kSprings - 1) + 1.3 * (hash01(s, 90) - 0.5);
997 }
998 static constexpr double kSpringCutHz = 4400.0;
999 static constexpr T kSpringOutGain = T(2);
1000
1001 // =========================================================================
1002 // Building blocks
1003 // =========================================================================
1004
1006 struct Line
1007 {
1008 std::vector<T> buf;
1009 int mask = 0;
1010 int w = 0;
1011
1012 void prepare(int maxDelay)
1013 {
1014 int size = 1;
1015 while (size < maxDelay + 2) size <<= 1;
1016 buf.assign(static_cast<std::size_t>(size), T(0));
1017 mask = size - 1;
1018 w = 0;
1019 }
1020 void clear() noexcept { std::fill(buf.begin(), buf.end(), T(0)); w = 0; }
1021 [[nodiscard]] T at(int k) const noexcept { return buf[static_cast<std::size_t>((w - k) & mask)]; }
1022 void push(T x) noexcept { buf[static_cast<std::size_t>(w)] = x; w = (w + 1) & mask; }
1023 };
1024
1031 {
1032 std::vector<T> buf;
1033 int size = 0;
1034 int stride = 0;
1035 int mask = 0;
1036 int w = 0;
1037
1038 void prepare(int numLines, int maxDelay)
1039 {
1040 size = 1;
1041 while (size < maxDelay + 2) size <<= 1;
1042 mask = size - 1;
1043 // One cache line of padding per line: with a power-of-two stride
1044 // every line would start in the same L1 set, and the 32 streams
1045 // read each sample would keep evicting each other.
1046 stride = size + 64 / static_cast<int>(sizeof(T));
1047 buf.assign(static_cast<std::size_t>(stride) * static_cast<std::size_t>(numLines), T(0));
1048 w = 0;
1049 }
1050 void clear() noexcept { std::fill(buf.begin(), buf.end(), T(0)); w = 0; }
1051 [[nodiscard]] T at(int i, int k) const noexcept
1052 { return buf[static_cast<std::size_t>(i * stride + ((w - k) & mask))]; }
1053 void write(int i, T x) noexcept { buf[static_cast<std::size_t>(i * stride + w)] = x; }
1054 void advance() noexcept { w = (w + 1) & mask; }
1055 };
1056
1065 {
1066 T h0_ = T(0), h1_ = T(0), h2_ = T(0), h3_ = T(0);
1067 T phase_ = T(0);
1068 T phaseInc_ = T(0);
1069 uint32_t state_ = 1;
1070
1071 void prepare(double sr, T rate, uint32_t seed) noexcept
1072 {
1073 phaseInc_ = rate / static_cast<T>(sr);
1074 state_ = seed ? seed : 1;
1075 h0_ = nextRandom(); h1_ = nextRandom();
1076 h2_ = nextRandom(); h3_ = nextRandom();
1077 phase_ = T(0);
1078 }
1079
1080 void setRate(T rate, double sr) noexcept { phaseInc_ = rate / static_cast<T>(sr); }
1081
1084 T nextStride(int stride) noexcept
1085 {
1086 phase_ += phaseInc_ * static_cast<T>(stride);
1087 if (phase_ >= T(1))
1088 {
1089 phase_ -= T(1);
1090 h0_ = h1_; h1_ = h2_; h2_ = h3_;
1091 h3_ = nextRandom();
1092 }
1093 const T d = phase_;
1094 const T c1 = T(0.5) * (h2_ - h0_);
1095 const T c2 = h0_ - T(2.5) * h1_ + T(2) * h2_ - T(0.5) * h3_;
1096 const T c3 = T(0.5) * (h3_ - h0_) + T(1.5) * (h1_ - h2_);
1097 return ((c3 * d + c2) * d + c1) * d + h1_;
1098 }
1099
1100 private:
1101 T nextRandom() noexcept
1102 {
1103 state_ ^= state_ << 13;
1104 state_ ^= state_ >> 17;
1105 state_ ^= state_ << 5;
1106 return static_cast<T>(state_) / static_cast<T>(0xFFFFFFFFu) * T(2) - T(1);
1107 }
1108 };
1109
1110 static constexpr T lfoRateFactor(int i) noexcept
1111 {
1112 return T(0.71) + T(0.063) * static_cast<T>(i);
1113 }
1114 static constexpr uint32_t lfoSeed(int i) noexcept
1115 {
1116 return static_cast<uint32_t>(i) * 7919u + 1u;
1117 }
1118 static constexpr T rotRate() noexcept { return static_cast<T>(kRotRateHz); }
1119 static constexpr T rotRateFactor(int i) noexcept
1120 {
1121 return T(0.83) + T(0.051) * static_cast<T>(i);
1122 }
1123 static constexpr uint32_t rotSeed(int i) noexcept
1124 {
1125 return static_cast<uint32_t>(i) * 104729u + 7u;
1126 }
1127
1129 static double hash01(int k, int s) noexcept
1130 {
1131 const double v = std::sin(static_cast<double>(k) * 12.9898
1132 + static_cast<double>(s) * 78.233) * 43758.5453;
1133 return v - std::floor(v);
1134 }
1135
1137 static T levelDb(T gain) noexcept
1138 {
1139 return static_cast<T>(20.0 * std::log10(std::max(static_cast<double>(gain), 1e-6)));
1140 }
1141
1142 int msToSamples(T ms) const noexcept
1143 {
1144 return static_cast<int>(static_cast<T>(spec_.sampleRate) * ms / T(1000));
1145 }
1146
1147 // --- State ---------------------------------------------------------------
1148
1150 bool prepared_ = false;
1151 bool eco_ = false;
1153 int ctrlPhase_ = 0;
1154
1155 // Input stage (per channel)
1156 std::array<Line, 2> preDelay_;
1157 std::array<std::array<Line, kInStages>, 2> inAP_;
1158 std::array<std::array<int, kInStages>, 2> inAPLen_ {};
1159 std::array<T, kInStages> inAPCoeff_ {};
1160 std::array<Line, 2> ring_;
1161 std::array<std::array<Line, kOutStages>, 2> outAP_;
1162 std::array<std::array<int, kOutStages>, 2> outAPLen_ {};
1163 T outAPCoeff_ = T(0.5);
1164
1165 // Early reflections (per side): velvet taps, each reading its own side
1166 // (erChan_ 0) or the other (1)
1167 int numERTaps_ = 0;
1169 std::array<std::array<int, kMaxERTaps>, 2> erTap_ {}, erChan_ {}, erBin_ {};
1170 std::array<std::array<T, kMaxERTaps>, 2> erGain_ {};
1171 std::array<std::array<T, kMaxERTaps>, 2> erGainTarget_ {};
1172 std::array<T, kERGroups> erShelfGainTarget_ {};
1173 T erGlidePerSample_ = T(0.001);
1174 std::array<int, kERGroups + 1> erGroupStart_ {};
1175 std::array<T, kERGroups> erShelfGain_ {};
1176 T erShelfCoeff_ = T(0.5);
1177 std::array<std::array<T, kERGroups>, 2> erLP_ {};
1178 std::array<std::array<T, kERGroups>, 2> erShadowLP_ {};
1179 double meanLoopSec_ = 0.1;
1181
1182 // FDN
1184 std::array<int, kMaxLines> lenTarget_ {};
1185 std::array<T, kMaxLines> lenCur_ {};
1186 std::array<T, kMaxLines> pos_ {};
1187 std::array<T, kMaxLines> posInc_ {};
1188 std::array<T, kMaxLines> apY1_ {};
1189 std::array<int, kMaxLines> injTap_ {};
1190 std::array<T, kMaxLines> injGain_ {};
1191 std::array<T, kMaxLines> outSignL_ {}, outSignR_ {};
1192 std::array<SmoothRandomLFO, kMaxLines> lfo_;
1193 // Time-varying mixing: one slow Givens rotation per line pair after the
1194 // Hadamard mix (orthogonal, so the loop stays lossless).
1196 alignas(64) std::array<T, kMaxLines / 2> rotC_ {};
1197 alignas(64) std::array<T, kMaxLines / 2> rotS_ {};
1198 T rotDepth_ = T(0);
1199 unsigned rotTick_ = 0;
1201 T glideCoeff_ = T(0);
1203 T maxReadPos_ = T(8);
1204
1205 // Spring tanks: stretched-allpass histories [spring][stage + 1][P]
1206 // (stage m reads history m and writes history m + 1), shared write
1207 // index; 4th-order Butterworth band limit per spring.
1208 bool spring_ = false;
1209 std::vector<T> springHist_;
1210 int springP_ = 8;
1211 int springW_ = 0;
1212 int springK_ = 5;
1214 T springA_ = T(0.5);
1215 std::array<std::array<T, 5>, 2> springLP_ {};
1216 std::array<std::array<T, kSprings>, 4> springLPState_ {};
1217
1218 // In-loop allpasses
1220 std::array<int, kMaxLines> loopAPLen_ {};
1221 std::array<T, kMaxLines> loopAPCur_ {};
1222 bool loopAPGliding_ = false;
1223 T loopAPCoeff_ = T(0.4);
1224
1225 // Absorption: Jot shelf (mid/high) and bass shelf, per line
1226 std::array<T, kMaxLines> jotB0_ {}, jotB1_ {}, jotA1_ {}, jotX1_ {}, jotY1_ {};
1227 std::array<T, kMaxLines> bassB0_ {}, bassB1_ {}, bassA1_ {}, bassX1_ {}, bassY1_ {};
1228
1229 // Output stage
1230 std::array<T, 2> cohZ_ {};
1231 std::array<T, 5> cohC_ {};
1232 std::array<std::array<T, 2>, 2> subZ_ {};
1233 std::array<T, 5> subC_ {};
1234
1235 // Tone correction EQ (Biquad 12 dB/oct)
1238 bool toneLPActive_ = false;
1239 bool toneHPActive_ = false;
1240
1242
1243 // --- Parameters ----------------------------------------------------------
1244 //
1245 // Thread-safety model: every setter mutates only the atomic shadows below
1246 // and raises a dirty flag; the audio thread drains the flags at the top of
1247 // processBlock()/processSample() and rebuilds the non-atomic coefficient
1248 // arrays there, so nothing on the audio path is written from other threads.
1249
1250 std::atomic<Type> type_ { Type::Room };
1251 std::atomic<T> decayTime_ { T(1) };
1252 std::atomic<T> size_ { T(0.5) };
1253 std::atomic<T> damping_ { T(0.5) };
1254 std::atomic<T> diffusion_ { T(0.7) };
1255 std::atomic<T> modDepth_ { T(0.1) };
1256 std::atomic<T> modRate_ { T(1) };
1257 std::atomic<T> preDelayMs_ { T(0) };
1258 std::atomic<T> erToLateMs_ { T(0) };
1259 std::atomic<T> mix_ { T(0.3) };
1260 std::atomic<T> earlyLevel_ { T(1) };
1261 std::atomic<T> lateLevel_ { T(1) };
1262 std::atomic<T> width_ { T(1) }; // 0 = mono, 1 = natural, 2 = wide
1263
1264 std::atomic<T> highDecayMult_ { T(0.5) }; // HF T60 multiplier (0.05-1.0)
1265 std::atomic<T> bassDecayMult_ { T(1.2) }; // bass T60 multiplier (0.3-3.0)
1266 std::atomic<T> highCrossover_ { T(5000) }; // Hz
1267 std::atomic<T> bassCrossover_ { T(200) }; // Hz
1268
1269 std::atomic<Quality> quality_ { Quality::Full };
1270
1271 std::atomic<int> preDelaySamples_ { 0 };
1272 std::atomic<int> erToLateSamples_ { 0 };
1273
1274 std::atomic<bool> presetDirty_ { false }; // setType() -> rebuild + reset
1275 std::atomic<bool> paramsDirty_ { false }; // other setters -> refresh coefficients
1276 std::atomic<bool> qualityDirty_ { false }; // setQuality() -> resize engine + reset
1277 std::atomic<bool> toneDirty_ { false }; // tone EQ cutoff changes
1278 std::atomic<T> toneLowCutHz_ { T(-1) }; // <= 0 = off
1279 std::atomic<T> toneHighCutHz_ { T(-1) };
1280
1281 // Per-param user-override mask. setType() clears it so the preset
1282 // re-establishes the baseline; each param setter marks its bit;
1283 // commitPreset() skips any atomic the user overrode, so a setter issued
1284 // after setType() always wins regardless of drain timing (the documented
1285 // quick-start is `setType(Hall); setDecay(2.0f);`).
1286 enum : uint32_t {
1289 kUserModDepth = 128u, kUserModRate = 256u, kUserEarly = 512u,
1290 kUserLate = 1024u, kUserErToLate = 2048u
1292 std::atomic<uint32_t> userParamMask_ { 0u };
1293 void markUserParam(uint32_t bit) noexcept
1294 { userParamMask_.fetch_or(bit, std::memory_order_release); }
1295
1298 {
1301 T earlyLevel = T(1);
1302 T lateLevel = T(1);
1303 T width = T(1);
1304 };
1306
1307 // =========================================================================
1308 // Audio-thread control
1309 // =========================================================================
1310
1318 void drainPendingChanges() noexcept
1319 {
1320 if (qualityDirty_.load(std::memory_order_acquire)
1321 && qualityDirty_.exchange(false, std::memory_order_acquire))
1322 {
1323 const bool eco = quality_.load(std::memory_order_relaxed) == Quality::Eco;
1324 if (eco != eco_)
1325 {
1326 eco_ = eco;
1328 updateAll();
1329 reset(); // engine topology changed: old state is stale
1330 }
1331 }
1332
1333 if (presetDirty_.load(std::memory_order_acquire)
1334 && presetDirty_.exchange(false, std::memory_order_acquire))
1335 {
1336 applyPreset(type_.load(std::memory_order_relaxed));
1337 reset(); // new room: drop the old tail, snap the lengths
1338 paramsDirty_.store(false, std::memory_order_relaxed);
1339 }
1340 else if (paramsDirty_.load(std::memory_order_acquire)
1341 && paramsDirty_.exchange(false, std::memory_order_acquire))
1342 {
1343 // Knob change: refresh coefficients without touching the audio
1344 // state; line lengths glide to their new targets.
1345 updateAll();
1346 }
1347
1348 drainTone();
1349 }
1350
1351 void drainTone() noexcept
1352 {
1353 if (!(toneDirty_.load(std::memory_order_acquire)
1354 && toneDirty_.exchange(false, std::memory_order_acquire)))
1355 return;
1356 const T hpHz = toneLowCutHz_.load(std::memory_order_relaxed);
1357 const T lpHz = toneHighCutHz_.load(std::memory_order_relaxed);
1358 toneHPActive_ = hpHz > T(0) && spec_.sampleRate > 0;
1359 if (toneHPActive_)
1361 spec_.sampleRate, static_cast<double>(std::clamp(hpHz, T(20), T(500)))));
1362 toneLPActive_ = lpHz > T(0) && spec_.sampleRate > 0;
1363 if (toneLPActive_)
1365 spec_.sampleRate, static_cast<double>(std::clamp(lpHz, T(2000), T(16000)))));
1366 }
1367
1369 void refreshCachedParams() noexcept
1370 {
1371 cachedParams_.preDelaySamples = preDelaySamples_.load(std::memory_order_relaxed);
1372 cachedParams_.erToLateSamples = erToLateSamples_.load(std::memory_order_relaxed);
1373 cachedParams_.earlyLevel = earlyLevel_.load(std::memory_order_relaxed);
1374 cachedParams_.lateLevel = lateLevel_.load(std::memory_order_relaxed);
1375 cachedParams_.width = width_.load(std::memory_order_relaxed);
1376 }
1377
1383 void controlTick() noexcept
1384 {
1385 const int n = nLines_;
1386 // The pair angles move by under 0.005 rad per 64 samples: refresh
1387 // their sine and cosine every fourth control tick.
1388 if ((++rotTick_ & 3) == 0)
1389 for (int k = 0; k < n / 2; ++k)
1390 {
1391 const T theta = rotLfo_[k].nextStride(4 * kCtrl) * rotDepth_;
1392 // Form the rotation in double before rounding coefficients to T.
1393 // Single-precision libm differences are otherwise recirculated
1394 // through the feedback network on every sample.
1395 rotC_[k] = static_cast<T>(std::cos(static_cast<double>(theta)));
1396 rotS_[k] = static_cast<T>(std::sin(static_cast<double>(theta)));
1397 }
1398 for (int i = 0; i < n; ++i)
1399 {
1400 const T diff = static_cast<T>(lenTarget_[i]) - lenCur_[i];
1401 lenCur_[i] += std::clamp(diff * glideCoeff_, -maxGlideStep_, maxGlideStep_);
1402 const T m = lfo_[i].nextStride(kCtrl) * modDepthSamples_;
1403 const T target = std::clamp(lenCur_[i] + m, kMinReadPos, maxReadPos_);
1404 posInc_[i] = (target - pos_[i]) * (T(1) / T(kCtrl));
1405 }
1406 if (loopAPGliding_)
1407 {
1408 // The in-loop allpasses follow a size change with the lines.
1409 bool moving = false;
1410 for (int i = 0; i < n; ++i)
1411 {
1412 const T diff = static_cast<T>(loopAPLen_[i]) - loopAPCur_[i];
1413 if (std::abs(diff) < T(0.01))
1414 loopAPCur_[i] = static_cast<T>(loopAPLen_[i]);
1415 else
1416 {
1417 loopAPCur_[i] += std::clamp(diff * glideCoeff_, -maxGlideStep_, maxGlideStep_);
1418 moving = true;
1419 }
1420 }
1421 loopAPGliding_ = moving;
1422 }
1423 }
1424
1427 template <int M>
1428 static void hadamardSmall(T* x) noexcept
1429 {
1430 if constexpr (M > 1)
1431 {
1432 hadamardSmall<M / 2>(x);
1433 hadamardSmall<M / 2>(x + M / 2);
1434 [x]<std::size_t... I>(std::index_sequence<I...>) {
1435 ((void) [x] {
1436 const T a = x[I], b = x[I + M / 2];
1437 x[I] = a + b;
1438 x[I + M / 2] = a - b;
1439 }(), ...);
1440 }(std::make_index_sequence<M / 2> {});
1441 }
1442 }
1443
1446 template <int N, int W, typename O>
1447 static void fwht(T* x) noexcept
1448 {
1449 using V = typename O::V;
1450 for (int b = 0; b < N; b += W)
1451 hadamardSmall<W>(x + b);
1452 for (int h = W; h < N; h <<= 1)
1453 for (int i = 0; i < N; i += h << 1)
1454 for (int j = i; j < i + h; j += W)
1455 {
1456 const V a = O::load(x + j), b = O::load(x + j + h);
1457 O::store(x + j, O::add(a, b));
1458 O::store(x + j + h, O::sub(a, b));
1459 }
1460 }
1461
1468 template <int N>
1469 void runFdn(T& lateL, T& lateR, int injLag) noexcept
1470 {
1471 constexpr int W = std::min(simd::kVecWidth<T>, N);
1472 using O = simd::Vec<T, W>;
1473 using V = typename O::V;
1474 static_assert(N % W == 0, "line count must be a multiple of the SIMD width");
1475
1476 alignas(64) std::array<T, N + 1> r; // +1: wrap slot for the rotation
1477 alignas(64) std::array<T, N> v;
1478 alignas(64) std::array<T, N> tmp;
1479 alignas(64) std::array<int, N> idx;
1480
1481 // Allpass-interpolated modulated reads. eta = (1 - d) / (1 + d) for
1482 // the fractional part d in [0.5, 1.5), as the series -h / (1 + h) in
1483 // h = (d - 1) / 2, |h| <= 1/4: the error (< 1e-3) only nudges the
1484 // fractional delay, the interpolator stays exactly allpass. The
1485 // arithmetic runs in separate (vectorizable) passes around the
1486 // scalar gather.
1487 for (int i = 0; i < N; ++i)
1488 {
1489 const T p = pos_[i] += posInc_[i];
1490 const int M = static_cast<int>(p - T(0.5));
1491 const T h = (p - static_cast<T>(M) - T(1)) * T(0.5);
1492 v[i] = -h * (T(1) - h * (T(1) - h * (T(1) - h)));
1493 idx[i] = M;
1494 }
1495 for (int i = 0; i < N; ++i)
1496 {
1497 r[i] = lines_.at(i, idx[i]);
1498 tmp[i] = lines_.at(i, idx[i] + 1);
1499 }
1500 for (int i = 0; i < N; i += W)
1501 {
1502 const V y = O::madd(O::load(v.data() + i),
1503 O::sub(O::load(r.data() + i), O::load(apY1_.data() + i)),
1504 O::load(tmp.data() + i));
1505 O::store(apY1_.data() + i, y);
1506 O::store(r.data() + i, y);
1507 }
1508
1509 // Absorption of what each line delivers (its own round trip: the
1510 // in-loop allpass that fed it plus the line): Jot mid/high shelf,
1511 // then bass shelf, y = b0 x + b1 x1 - a1 y1 each.
1512 for (int i = 0; i < N; i += W)
1513 {
1514 const V x = O::load(r.data() + i);
1515 const V j = O::sub(O::madd(O::load(jotB0_.data() + i), x,
1516 O::mul(O::load(jotB1_.data() + i), O::load(jotX1_.data() + i))),
1517 O::mul(O::load(jotA1_.data() + i), O::load(jotY1_.data() + i)));
1518 O::store(jotX1_.data() + i, x);
1519 O::store(jotY1_.data() + i, j);
1520 const V b = O::sub(O::madd(O::load(bassB0_.data() + i), j,
1521 O::mul(O::load(bassB1_.data() + i), O::load(bassX1_.data() + i))),
1522 O::mul(O::load(bassA1_.data() + i), O::load(bassY1_.data() + i)));
1523 O::store(bassX1_.data() + i, j);
1524 O::store(bassY1_.data() + i, b);
1525 O::store(r.data() + i, b);
1526 }
1527
1528 // Orthogonal L/R output sums, taken after the absorption: even the
1529 // first pass through a line leaves attenuated by that line's own
1530 // decay, so the tail decays exponentially from its first echoes
1531 // instead of holding a plateau for one round trip.
1532 {
1533 V aL = O::set1(T(0)), aR = aL;
1534 for (int i = 0; i < N; i += W)
1535 {
1536 const V x = O::load(r.data() + i);
1537 aL = O::madd(O::load(outSignL_.data() + i), x, aL);
1538 aR = O::madd(O::load(outSignR_.data() + i), x, aR);
1539 }
1540 alignas(64) std::array<T, W> sumL, sumR;
1541 O::store(sumL.data(), aL);
1542 O::store(sumR.data(), aR);
1543 for (int k = 0; k < W; ++k) { lateL += sumL[k]; lateR += sumR[k]; }
1544 }
1545
1546 // Hadamard mix, rotation of the lines by one, normalisation.
1547 const V norm = O::set1(T(1) / std::sqrt(static_cast<T>(N)));
1548 fwht<N, W, O>(r.data());
1549 r[N] = r[0];
1550 for (int i = 0; i < N; i += W)
1551 O::store(v.data() + i, O::mul(norm, O::load(r.data() + i + 1)));
1552 // Slow Givens rotation of each line pair: the mixing matrix drifts
1553 // while staying orthogonal (lossless), so modes exchange energy
1554 // without the pitch deviation of delay modulation, whose spread
1555 // grows with frequency.
1556 // Pairs (i, i + 4) inside each block of 8: contiguous, branch-free,
1557 // so the four rotations of a block run as one vector operation.
1558 static_assert(kRotPairStride == 4 && N % 8 == 0, "rotation pairs are (i, i + 4) in blocks of 8");
1559 for (int blk = 0; blk < N; blk += 8)
1560 {
1561 T* lo = v.data() + blk;
1562 T* hi = lo + 4;
1563 const T* c = rotC_.data() + blk / 2;
1564 const T* sn = rotS_.data() + blk / 2;
1565 for (int q = 0; q < 4; ++q)
1566 {
1567 const T a = lo[q], b = hi[q];
1568 lo[q] = c[q] * a - sn[q] * b;
1569 hi[q] = sn[q] * a + c[q] * b;
1570 }
1571 }
1572
1573 {
1574 if (loopAPGliding_) [[unlikely]]
1575 {
1576 for (int i = 0; i < N; ++i)
1577 {
1578 const int k = static_cast<int>(loopAPCur_[i]);
1579 const T f = loopAPCur_[i] - static_cast<T>(k);
1580 const T a = loopAP_.at(i, k);
1581 tmp[i] = a + f * (loopAP_.at(i, k + 1) - a);
1582 }
1583 }
1584 else
1585 {
1586 for (int i = 0; i < N; ++i)
1587 tmp[i] = loopAP_.at(i, loopAPLen_[i]);
1588 }
1589 const V g = O::set1(loopAPCoeff_);
1590 for (int i = 0; i < N; i += W)
1591 {
1592 const V d = O::load(tmp.data() + i);
1593 const V w = O::madd(g, d, O::load(v.data() + i));
1594 O::store(v.data() + i, O::sub(d, O::mul(g, w)));
1595 O::store(tmp.data() + i, w);
1596 }
1597 for (int i = 0; i < N; ++i)
1598 loopAP_.write(i, tmp[i]);
1599 loopAP_.advance();
1600 }
1601
1602 for (int i = 0; i < N; ++i)
1603 {
1604 T x = v[i];
1605 // Safety limiter (never reached at sane levels): identity below
1606 // kSoftLimit, a rational soft knee above.
1607 if (std::abs(x) > kSoftLimit) [[unlikely]]
1608 {
1609 const T over = std::abs(x) - kSoftLimit;
1610 x = std::copysign(kSoftLimit + over / (T(1) + over), x);
1611 }
1612 lines_.write(i, x + injGain_[i] * ring_[(i >> 1) & 1].at(injLag + injTap_[i]));
1613 }
1614 lines_.advance();
1615 }
1616
1634 template <int Springs, int Stages>
1635 void runSprings(T& lateL, T& lateR, const T (&raw)[2]) noexcept
1636 {
1637 // The springs are independent, so the stage cascade runs across
1638 // them: one SIMD lane per spring, the history laid out as
1639 // [stage][slot][spring].
1640 constexpr int NW = simd::kVecNarrowWidth<T>;
1641 constexpr int W = (Springs % NW == 0) ? NW : 1;
1642 using O = simd::Vec<T, W>;
1643 using V = typename O::V;
1644
1645 const int P = springP_;
1646 const int mask = P - 1;
1647 const int iw = springW_;
1648 const int ik = (springW_ - springK_) & mask;
1649 // Each spring hears mostly its own side (a mono input drives both);
1650 // the raw transducer signal, since any diffusion would blur the chirps.
1651 const T drive[2] = { T(0.75) * raw[0] + T(0.25) * raw[1],
1652 T(0.25) * raw[0] + T(0.75) * raw[1] };
1653
1654 alignas(64) std::array<T, Springs> x;
1655 for (int s = 0; s < Springs; ++s)
1656 {
1657 const T p = pos_[s] += posInc_[s];
1658 const int M = static_cast<int>(p - T(0.5));
1659 const T h = (p - static_cast<T>(M) - T(1)) * T(0.5);
1660 const T eta = -h * (T(1) - h * (T(1) - h * (T(1) - h)));
1661 const T y = eta * (lines_.at(s, M) - apY1_[s]) + lines_.at(s, M + 1);
1662 apY1_[s] = y;
1663 x[s] = y;
1664 }
1665
1666 // Dispersion: y = a (x - y[n-K]) + x[n-K], stage by stage.
1667 const V a = O::set1(springA_);
1668 T* hist = springHist_.data();
1669 const std::size_t stageStride = static_cast<std::size_t>(P) * kSprings;
1670 const std::size_t wOff = static_cast<std::size_t>(iw) * kSprings;
1671 const std::size_t kOff = static_cast<std::size_t>(ik) * kSprings;
1672 for (int g = 0; g < Springs; g += W)
1673 {
1674 V xv = O::load(x.data() + g);
1675 T* hin = hist + g;
1676 for (int m = 0; m < Stages; ++m, hin += stageStride)
1677 {
1678 O::store(hin + wOff, xv);
1679 xv = O::madd(a, O::sub(xv, O::load(hin + stageStride + kOff)), O::load(hin + kOff));
1680 }
1681 O::store(hin + wOff, xv);
1682 O::store(x.data() + g, xv);
1683 }
1684
1685 // Band limit: two TDF-II Butterworth sections.
1686 for (int k = 0; k < 2; ++k)
1687 {
1688 const auto& c = springLP_[static_cast<std::size_t>(k)];
1689 const V b0 = O::set1(c[0]), b1 = O::set1(c[1]), b2 = O::set1(c[2]);
1690 const V a1 = O::set1(c[3]), a2 = O::set1(c[4]);
1691 T* s1 = springLPState_[static_cast<std::size_t>(2 * k)].data();
1692 T* s2 = springLPState_[static_cast<std::size_t>(2 * k + 1)].data();
1693 for (int g = 0; g < Springs; g += W)
1694 {
1695 const V xv = O::load(x.data() + g);
1696 const V y = O::madd(b0, xv, O::load(s1 + g));
1697 O::store(s1 + g, O::sub(O::madd(b1, xv, O::load(s2 + g)), O::mul(a1, y)));
1698 O::store(s2 + g, O::sub(O::mul(b2, xv), O::mul(a2, y)));
1699 O::store(x.data() + g, y);
1700 }
1701 }
1702
1703 T out[2] = { T(0), T(0) };
1704 for (int s = 0; s < Springs; ++s) out[s & 1] += x[s];
1705
1706 // Absorption shelves (exact per-band T60), limiter, injection.
1707 for (int g = 0; g < Springs; g += W)
1708 {
1709 const V xv = O::load(x.data() + g);
1710 const V j = O::sub(O::madd(O::load(jotB0_.data() + g), xv,
1711 O::mul(O::load(jotB1_.data() + g), O::load(jotX1_.data() + g))),
1712 O::mul(O::load(jotA1_.data() + g), O::load(jotY1_.data() + g)));
1713 O::store(jotX1_.data() + g, xv);
1714 O::store(jotY1_.data() + g, j);
1715 const V b = O::sub(O::madd(O::load(bassB0_.data() + g), j,
1716 O::mul(O::load(bassB1_.data() + g), O::load(bassX1_.data() + g))),
1717 O::mul(O::load(bassA1_.data() + g), O::load(bassY1_.data() + g)));
1718 O::store(bassX1_.data() + g, j);
1719 O::store(bassY1_.data() + g, b);
1720 O::store(x.data() + g, b);
1721 }
1722 for (int s = 0; s < Springs; ++s)
1723 {
1724 T v = x[s];
1725 if (std::abs(v) > kSoftLimit) [[unlikely]]
1726 {
1727 const T over = std::abs(v) - kSoftLimit;
1728 v = std::copysign(kSoftLimit + over / (T(1) + over), v);
1729 }
1730 lines_.write(s, v + drive[s & 1]);
1731 }
1732 lines_.advance();
1733 springW_ = (springW_ + 1) & mask;
1734 const T og = kSpringOutGain / std::sqrt(static_cast<T>(Springs / 2));
1735 lateL += out[0] * og;
1736 lateR += out[1] * og;
1737 }
1738
1740 void refreshTopology() noexcept
1741 {
1743 }
1744
1747 static void addTap(T* acc, const Line& l, int lag0, T gain, int count) noexcept
1748 {
1749 // The span is contiguous in the ring except across its wrap: split it
1750 // there so both parts run as SIMD multiply-adds.
1751 const T* b = l.buf.data();
1752 const int start = (l.w - lag0) & l.mask;
1753 const int first = std::min(count, l.mask + 1 - start);
1754 simd::addWithGain(acc, b + start, gain, first);
1755 if (first < count)
1756 simd::addWithGain(acc + first, b, gain, count - first);
1757 }
1758
1766 void processChunk(const T* inL, const T* inR, T* outL, T* outR, int count) noexcept
1767 {
1768 const int pd = cachedParams_.preDelaySamples;
1769 const int gap = cachedParams_.erToLateSamples;
1770 const int last = count - 1;
1771
1772 // --- Pre-delay, then the allpass diffusers of the late-field feed ---
1773 alignas(64) T raw[2][kChunk];
1774 for (int c = 0; c < 2; ++c)
1775 {
1776 Line& pre = preDelay_[static_cast<std::size_t>(c)];
1777 const T* in = c == 0 ? inL : inR;
1778 for (int n = 0; n < count; ++n) pre.push(in[n]);
1779 std::fill(raw[c], raw[c] + count, T(0));
1780 addTap(raw[c], pre, pd + 1 + last, T(1), count);
1781 Line& ring = ring_[static_cast<std::size_t>(c)];
1782 auto& aps = inAP_[static_cast<std::size_t>(c)];
1783 for (int n = 0; n < count; ++n)
1784 {
1785 T x = raw[c][n];
1786 for (int st = 0; st < kInStages; ++st)
1787 {
1788 Line& ap = aps[static_cast<std::size_t>(st)];
1789 const T g = inAPCoeff_[static_cast<std::size_t>(st)];
1790 const T d = ap.at(inAPLen_[static_cast<std::size_t>(c)][static_cast<std::size_t>(st)]);
1791 const T w = x + g * d;
1792 ap.push(w);
1793 x = d - g * w;
1794 }
1795 ring.push(x);
1796 }
1797 }
1798
1799 // The early gains follow size and decay changes in a ~20 ms glide
1800 // (per chunk), so automating them does not click.
1801 {
1802 const T gc = erGlidePerSample_ * static_cast<T>(count);
1803 for (int s = 0; s < 2; ++s)
1804 for (int k = 0; k < numERTaps_; ++k)
1805 erGain_[s][k] += gc * (erGainTarget_[s][k] - erGain_[s][k]);
1806 for (int g = 0; g < kERGroups; ++g)
1807 erShelfGain_[g] += gc * (erShelfGainTarget_[g] - erShelfGain_[g]);
1808 }
1809
1810 // --- Early reflections: velvet taps with rising density, each on its
1811 // own side or the other; the first few read the raw input (crisp
1812 // discrete reflections), the rest the diffused feed (each a short
1813 // dense burst); grouped absorption, later groups darker ---
1814 alignas(64) T early[2][kChunk];
1815 for (int s = 0; s < 2; ++s)
1816 {
1817 const Line* src[4] = { &ring_[static_cast<std::size_t>(s)], &ring_[static_cast<std::size_t>(1 - s)],
1818 &preDelay_[static_cast<std::size_t>(s)], &preDelay_[static_cast<std::size_t>(1 - s)] };
1819 const int lagOff[4] = { last, last, pd + last, pd + last };
1820 std::fill(early[s], early[s] + count, T(0));
1821 auto& lp = erLP_[static_cast<std::size_t>(s)];
1822 for (int g = 0; g < kERGroups; ++g)
1823 {
1824 // Each tap lands in its head-shadow class: open (the ear
1825 // facing the reflection) or shadowed (the far ear).
1826 alignas(64) T acc[kShadowBins][kChunk];
1827 for (auto& a : acc) std::fill(a, a + count, T(0));
1828 for (int k = erGroupStart_[g]; k < erGroupStart_[g + 1]; ++k)
1829 addTap(acc[erBin_[s][k]], *src[erChan_[s][k]], erTap_[s][k] + lagOff[erChan_[s][k]],
1830 erGain_[s][k], count);
1831 {
1832 // The shadowed class keeps its lows, loses half its highs.
1833 constexpr T hf = static_cast<T>(kShadowHF) - T(1);
1834 T z = erShadowLP_[static_cast<std::size_t>(s)][static_cast<std::size_t>(g)];
1835 for (int n = 0; n < count; ++n)
1836 {
1837 const T x = acc[1][n];
1838 z += shadowCoeff_ * (x - z);
1839 acc[0][n] += x + hf * (x - z);
1840 }
1841 erShadowLP_[static_cast<std::size_t>(s)][static_cast<std::size_t>(g)] = z;
1842 }
1843 // Frequency-dependent absorption: a first-order high shelf
1844 // at the high crossover, the group's highs attenuated as the
1845 // late field's are by that time (its HF T60).
1846 const T c = erShelfCoeff_;
1847 const T hg = erShelfGain_[static_cast<std::size_t>(g)];
1848 T y = lp[g];
1849 for (int n = 0; n < count; ++n)
1850 {
1851 const T x = acc[0][n];
1852 y += c * (x - y);
1853 early[s][n] += y + hg * (x - y);
1854 }
1855 lp[g] = y;
1856 }
1857 }
1858
1859 for (int n = 0; n < count; ++n)
1860 {
1861 if (ctrlPhase_ == 0) controlTick();
1862 ctrlPhase_ = (ctrlPhase_ + 1) & (kCtrl - 1);
1863
1864 // --- Late field ---
1865 T lateL = T(0), lateR = T(0);
1866 if (spring_)
1867 {
1868 const T rawN[2] = { raw[0][n], raw[1][n] };
1869 if (eco_) runSprings<kEcoSprings, kEcoSpringStages>(lateL, lateR, rawN);
1870 else runSprings<kSprings, kSpringStages>(lateL, lateR, rawN);
1871 }
1872 else if (eco_) runFdn<kEcoLines>(lateL, lateR, gap + last - n);
1873 else runFdn<kMaxLines>(lateL, lateR, gap + last - n);
1874
1875 const auto [yl, yr] = outputStage(lateL, lateR, early[0][n], early[1][n]);
1876 outL[n] = yl;
1877 outR[n] = yr;
1878 }
1879 }
1880
1883 std::pair<T, T> outputStage(T lateL, T lateR, T earlyL, T earlyR) noexcept
1884 {
1885 // --- Output: coherent low band, width, early + late, subsonic high-pass, tone ---
1886 const T lateLvl = cachedParams_.lateLevel * kOutGain;
1887 lateL *= lateLvl;
1888 lateR *= lateLvl;
1889 if (!spring_)
1890 {
1891 // Output diffusion (the springs' chirps stay crisp).
1892 T* lr[2] = { &lateL, &lateR };
1893 const T g = outAPCoeff_;
1894 for (int c = 0; c < 2; ++c)
1895 {
1896 T x = *lr[c];
1897 for (int k = 0; k < kOutStages; ++k)
1898 {
1899 Line& ap = outAP_[static_cast<std::size_t>(c)][static_cast<std::size_t>(k)];
1900 const T d = ap.at(outAPLen_[static_cast<std::size_t>(c)][static_cast<std::size_t>(k)]);
1901 const T w = x + g * d;
1902 ap.push(w);
1903 x = d - g * w;
1904 }
1905 *lr[c] = x;
1906 }
1907 }
1908 {
1909 // A diffuse field is coherent between the ears at low frequencies
1910 // (the wavelength dwarfs the head) and incoherent above about
1911 // 500 Hz. The side signal is high-passed (Butterworth-like at
1912 // kCoherenceHz), which reproduces the interaural coherence of a
1913 // measured concert hall seat: about 0.97 at 125 Hz, 0.65 at
1914 // 250 Hz, under 0.1 from 500 Hz up.
1915 const T mid = (lateL + lateR) * T(0.5);
1916 T side = (lateL - lateR) * T(0.5);
1917 const T hp = cohC_[0] * side + cohZ_[0]; // TDF-II biquad
1918 cohZ_[0] = cohC_[1] * side - cohC_[3] * hp + cohZ_[1];
1919 cohZ_[1] = cohC_[2] * side - cohC_[4] * hp;
1920 side = hp * cachedParams_.width;
1921 lateL = mid + side;
1922 lateR = mid - side;
1923 }
1924
1925 T out[2] = { earlyL * cachedParams_.earlyLevel + lateL,
1926 earlyR * cachedParams_.earlyLevel + lateR };
1927 for (int c = 0; c < 2; ++c)
1928 {
1929 auto& z = subZ_[static_cast<std::size_t>(c)];
1930 const T y = subC_[0] * out[c] + z[0];
1931 z[0] = subC_[1] * out[c] - subC_[3] * y + z[1];
1932 z[1] = subC_[2] * out[c] - subC_[4] * y;
1933 out[c] = y;
1934 }
1935 if (toneHPActive_)
1936 {
1937 out[0] = toneHPBiquad_.processSample(out[0], 0);
1938 out[1] = toneHPBiquad_.processSample(out[1], 1);
1939 }
1940 if (toneLPActive_)
1941 {
1942 out[0] = toneLPBiquad_.processSample(out[0], 0);
1943 out[1] = toneLPBiquad_.processSample(out[1], 1);
1944 }
1945 return { out[0], out[1] };
1946 }
1947
1948 // =========================================================================
1949 // Coefficient update helpers (audio thread or prepare)
1950 // =========================================================================
1951
1952 void updateAll() noexcept
1953 {
1954 if (spec_.sampleRate <= 0) return;
1959 const T hd = highDecayMult_.load(std::memory_order_relaxed);
1960 damping_.store(std::clamp((T(1) - hd) / T(0.9), T(0), T(1)), std::memory_order_relaxed);
1961 erToLateSamples_.store(msToSamples(erToLateMs_.load(std::memory_order_relaxed)),
1962 std::memory_order_relaxed);
1963 generateERTapsForType(erType_); // the discrete/diffuse split follows the diffusion
1964 }
1965
1967 T sizeFactor() const noexcept
1968 {
1969 return T(0.35) + T(0.65) * size_.load(std::memory_order_relaxed);
1970 }
1971
1972 void updateDelayLengths() noexcept
1973 {
1974 const double sr = spec_.sampleRate;
1975 const double sz = static_cast<double>(sizeFactor());
1976 const auto ms = [sr](double v) { return std::max(1, static_cast<int>(v * sr / 1000.0)); };
1977
1978 for (int i = 0; i < kMaxLines; ++i)
1979 {
1980 // Eco picks every other base delay, so its 16 lines still span
1981 // the full range (even mode density).
1982 const int src = eco_ ? std::min(i * (kMaxLines / kEcoLines) + 1, kMaxLines - 1) : i;
1983 lenTarget_[i] = nearestPrime(ms(kBaseDelaysMs_[src] * sz));
1984 // The in-loop allpasses scale with the room like the lines, so
1985 // their group-delay ripple stays a small, size-independent share
1986 // of each loop: no frequency lingers longer in a small room.
1987 loopAPLen_[i] = nearestPrime(ms(kLoopApMs_[i] * sz));
1988 loopAPGliding_ = true;
1989 injTap_[i] = std::max(1, ms(kInjectMs_[i]));
1990 injGain_[i] = static_cast<T>(kInjectSign_[i]) / std::sqrt(static_cast<T>(nLines_));
1991 outSignL_[i] = static_cast<T>(kOutSignL_[i]);
1992 outSignR_[i] = static_cast<T>(kOutSignR_[i]);
1993 }
1994 if (spring_)
1995 {
1996 // Round trips of 27-49 ms at the preset size (0.11), up to
1997 // 156 ms at size 1 (within the line capacity).
1998 const double springScale = 0.6 + 2.0 * static_cast<double>(size_.load(std::memory_order_relaxed));
1999 for (int s = 0; s < nLines_; ++s)
2000 lenTarget_[s] = nearestPrime(ms(springBaseMs(s) * springScale));
2001 }
2002 for (int c = 0; c < 2; ++c)
2003 for (int st = 0; st < kInStages; ++st)
2004 inAPLen_[static_cast<std::size_t>(c)][static_cast<std::size_t>(st)]
2005 = nearestPrime(ms(kInDiffMs_[c][st]));
2006 }
2007
2008 void updateDiffCoeffs() noexcept
2009 {
2010 const double diff = static_cast<double>(diffusion_.load(std::memory_order_relaxed));
2011 for (int st = 0; st < kInStages; ++st)
2012 inAPCoeff_[static_cast<std::size_t>(st)] = static_cast<T>(kInDiffCoeffs_[st] * diff);
2013 loopAPCoeff_ = static_cast<T>(0.075 + 0.175 * diff);
2014 outAPCoeff_ = static_cast<T>(0.6 * diff);
2015
2016 // Spring chirp strength: the group-delay rise per stage is
2017 // (1 + a) / (1 - a) - (1 - a) / (1 + a). The total dispersion is
2018 // that of a 72-stage cascade at a = 0.3 + 0.5 x diffusion; shorter
2019 // cascades get a larger coefficient to match it.
2020 {
2021 const double aRef = 0.3 + 0.5 * diff;
2023 const double f = 72.0 / springStages_ * (1.0 + aRef) / (1.0 - aRef);
2024 springA_ = static_cast<T>((f - 1.0) / (f + 1.0));
2025 }
2026 }
2027
2028 void updateModulation() noexcept
2029 {
2030 const double sr = spec_.sampleRate;
2031 const T rate = modRate_.load(std::memory_order_relaxed);
2032 for (int i = 0; i < kMaxLines; ++i)
2033 lfo_[i].setRate(rate * lfoRateFactor(i), sr);
2034 // Springs jitter by a fixed time (the random round-trip variation of
2035 // spring models, which smears their sparse modes); the FDN wanders
2036 // in proportion to the room size.
2037 modDepthSamples_ = modDepth_.load(std::memory_order_relaxed)
2038 * (spring_ ? static_cast<T>(kSpringJitterMs * sr / 1000.0)
2039 : static_cast<T>(kModMaxMs * sr / 1000.0) * sizeFactor());
2040 rotDepth_ = spring_ ? T(0)
2041 : modDepth_.load(std::memory_order_relaxed) * static_cast<T>(kRotMaxRad);
2042 for (int k = 0; k < kMaxLines / 2; ++k)
2043 rotLfo_[k].setRate(rotRate() * rotRateFactor(k), sr);
2044 glideCoeff_ = static_cast<T>(1.0 - std::exp(-kCtrl / (kGlideMs * sr / 1000.0)));
2045 maxGlideStep_ = static_cast<T>(kMaxGlideSpeed * kCtrl);
2046 }
2047
2061 void updateDecayParams() noexcept
2062 {
2063 const double sr = spec_.sampleRate;
2064 const double decay = calibratedMidDecay();
2065 const double t60H = std::max(0.05, decay * static_cast<double>(
2066 highDecayMult_.load(std::memory_order_relaxed)));
2067 const double t60B = std::max(0.05, decay * static_cast<double>(
2068 bassDecayMult_.load(std::memory_order_relaxed)));
2069 const double kPi = 3.14159265358979323846;
2070 const double fh = std::clamp(static_cast<double>(
2071 highCrossover_.load(std::memory_order_relaxed)), 100.0, 0.45 * sr);
2072 const double fb = std::clamp(static_cast<double>(
2073 bassCrossover_.load(std::memory_order_relaxed)), 10.0, 0.45 * sr);
2074 const double Kh = std::tan(kPi * fh / sr);
2075 const double Kb = std::tan(kPi * fb / sr);
2076 double loopSum = 0.0;
2077 for (int i = 0; i < kMaxLines; ++i)
2078 {
2079 // Loop length: line + group delay of the in-loop allpasses. The
2080 // FDN's short allpasses count their mean (their length). A
2081 // spring's dispersion makes the loop time frequency dependent,
2082 // from K (1 - a) / (1 + a) per stage at DC up to the chirp at the
2083 // band edge; the low and mid band, where the energy is, set it.
2084 const double M = spring_
2085 ? static_cast<double>(lenTarget_[i]) + springStages_ * springK_
2086 * (1.0 - static_cast<double>(springA_)) / (1.0 + static_cast<double>(springA_))
2087 : static_cast<double>(lenTarget_[i] + loopAPLen_[i]);
2088 const double gM = std::pow(0.001, M / (decay * sr));
2089 if (i < nLines_) loopSum += M;
2090 const double gH = std::min(std::pow(0.001, M / (t60H * sr)), gM);
2091 // Keep the bass loop gain below 1 so extreme settings ring out
2092 // instead of self-sustaining.
2093 const double gB = std::min(std::pow(0.001, M / (t60B * sr)), 0.9995);
2094
2095 {
2096 const double ratio = std::sqrt(gM / gH);
2097 const double a = Kh * ratio, b = Kh / ratio, inv = 1.0 / (1.0 + b);
2098 jotB0_[i] = static_cast<T>(gH * (1.0 + a) * inv);
2099 jotB1_[i] = static_cast<T>(gH * (a - 1.0) * inv);
2100 jotA1_[i] = static_cast<T>((b - 1.0) * inv);
2101 }
2102 {
2103 const double sq = std::sqrt(gB / gM);
2104 const double a = Kb * sq, b = Kb / sq, inv = 1.0 / (1.0 + b);
2105 bassB0_[i] = static_cast<T>((1.0 + a) * inv);
2106 bassB1_[i] = static_cast<T>((a - 1.0) * inv);
2107 bassA1_[i] = static_cast<T>((b - 1.0) * inv);
2108 }
2109 }
2110 meanLoopSec_ = loopSum / std::max(1, nLines_) / sr;
2111 }
2112
2129 [[nodiscard]] double calibratedMidDecay() const noexcept
2130 {
2131 const double sr = spec_.sampleRate;
2132 const double target = static_cast<double>(decayTime_.load(std::memory_order_relaxed));
2133 if (!(sr > 0.0) || nLines_ < 1) return target;
2134 const double hd = static_cast<double>(highDecayMult_.load(std::memory_order_relaxed));
2135 const double bd = static_cast<double>(bassDecayMult_.load(std::memory_order_relaxed));
2136 const double kPi = 3.14159265358979323846;
2137 const double Kh = std::tan(kPi * std::clamp(static_cast<double>(
2138 highCrossover_.load(std::memory_order_relaxed)), 100.0, 0.45 * sr) / sr);
2139 const double Kb = std::tan(kPi * std::clamp(static_cast<double>(
2140 bassCrossover_.load(std::memory_order_relaxed)), 10.0, 0.45 * sr) / sr);
2141 auto loopLen = [&](int i) {
2142 return spring_
2143 ? static_cast<double>(lenTarget_[i]) + springStages_ * springK_
2144 * (1.0 - static_cast<double>(springA_)) / (1.0 + static_cast<double>(springA_))
2145 : static_cast<double>(lenTarget_[i] + loopAPLen_[i]);
2146 };
2147
2148 // |H(e^jw)| of ((1 + a) + (a - 1) z^-1) / ((1 + b) + (b - 1) z^-1).
2149 auto shelf = [](double a, double b, double w) {
2150 const double c = std::cos(w), sn = std::sin(w);
2151 const double nr = (1.0 + a) + (a - 1.0) * c, ni = -(a - 1.0) * sn;
2152 const double dr = (1.0 + b) + (b - 1.0) * c, di = -(b - 1.0) * sn;
2153 return std::sqrt((nr * nr + ni * ni) / (dr * dr + di * di));
2154 };
2155 // Decay rate (1/T60) of the line mix at hz: each line's shelves are
2156 // designed from its own length, so the rate between the anchors
2157 // differs from line to line; the tail decays at their mean rate.
2158 auto t60At = [&](double d, double hz) {
2159 const double w = 2.0 * kPi * hz / sr;
2160 double rate = 0.0;
2161 int used = 0;
2162 for (int i = 0; i < nLines_; ++i)
2163 {
2164 const double M = loopLen(i);
2165 if (!(M > 0.0)) continue;
2166 const double gM = std::pow(0.001, M / (d * sr));
2167 const double gH = std::min(std::pow(0.001, M / (std::max(0.05, d * hd) * sr)), gM);
2168 const double gB = std::min(std::pow(0.001, M / (std::max(0.05, d * bd) * sr)), 0.9995);
2169 const double rh = std::sqrt(gM / gH), rb = std::sqrt(gB / gM);
2170 const double g = gH * shelf(Kh * rh, Kh / rh, w) * shelf(Kb * rb, Kb / rb, w);
2171 if (!(g > 0.0 && g < 1.0)) continue;
2172 rate += -sr * std::log10(g) / (3.0 * M);
2173 ++used;
2174 }
2175 return (used > 0 && rate > 0.0) ? used / rate : d;
2176 };
2177 double d = target;
2178 for (int it = 0; it < 4; ++it)
2179 {
2180 const double tm = 0.5 * (t60At(d, 500.0) + t60At(d, 1000.0));
2181 if (!(tm > 0.0)) break;
2182 d *= target / tm;
2183 }
2184 return std::clamp(d, 0.05, 120.0);
2185 }
2186
2187 // --- Early reflection generation -----------------------------------------
2188
2190 void generateERTapsForType(Type type) noexcept
2191 {
2192 const int cap = eco_ ? kEcoERTaps : kMaxERTaps;
2193 switch (type)
2194 {
2195 case Type::Room: generateERTaps(1.5, 35.0, cap); break;
2196 case Type::Hall: generateERTaps(5.0, 110.0, cap); break;
2197 case Type::Chamber: generateERTaps(3.0, 60.0, cap); break;
2198 case Type::Plate: generateERTaps(0.0, 0.0, 0); break;
2199 case Type::Spring: generateERTaps(0.0, 0.0, 0); break;
2200 case Type::Cathedral: generateERTaps(10.0, 160.0, cap); break;
2201 }
2202 }
2203
2217 void generateERTaps(double minMs, double maxMs, int numTaps) noexcept
2218 {
2219 numERTaps_ = std::clamp(numTaps, 0, kMaxERTaps) / 2 * 2;
2220 for (int g = 0; g <= kERGroups; ++g)
2222 if (numERTaps_ == 0) return;
2223
2224 const double sr = spec_.sampleRate;
2225 const int R = numERTaps_ / 2; // reflections per input channel
2226 constexpr double p = 1.6;
2227 // Diffusion sets how many of the first reflections stay discrete
2228 // (crisp, room-like) instead of reading the diffused feed (smooth).
2229 const int discrete = 1 + static_cast<int>(std::lround(
2230 (kMaxDiscreteER - 1) * (1.0 - static_cast<double>(diffusion_.load(std::memory_order_relaxed)))));
2231
2232 const double decay = static_cast<double>(decayTime_.load(std::memory_order_relaxed));
2233 constexpr double kLn1000 = 6.907755278982137;
2234 struct Tap { double ms; int chan; int bin; double gain; };
2235 std::array<std::array<Tap, kMaxERTaps>, 2> taps {};
2236 std::array<int, 2> used { 0, 0 };
2237 for (int c = 0; c < 2; ++c) // input channel: a source on that side
2238 {
2239 std::array<double, kMaxERTaps / 2> gv {}, msv {}, lat {};
2240 double dcSum = 0.0;
2241 for (int k = 0; k < R; ++k)
2242 {
2243 const double u = (k + 0.15 + 0.7 * hash01(k, c + 20)) / R;
2244 // Warped time in [0, 1): a floor of uniform density plus a
2245 // share rising like t^(p - 1).
2246 const double w = 0.3 * u + 0.7 * std::pow(u, 1.0 / p);
2247 msv[k] = minMs + (maxMs - minMs) * w;
2248 const double density = 1.0 / (0.3 + 0.7 / p * std::pow(std::max(u, 1e-3), 1.0 / p - 1.0)); // dk/dw
2249 const double sign = k < 4 || hash01(k, c + 40) < 0.5 ? 1.0 : -1.0;
2250 // Energy per unit time follows the room's own decay (the
2251 // late field's rate, so the two join without a swell or a
2252 // dip at any decay time): sparse taps are individually
2253 // louder.
2254 gv[k] = sign * std::exp(-kLn1000 * msv[k] * 1e-3 / decay) / std::sqrt(density);
2255 if (k >= discrete) dcSum += gv[k];
2256 // Arrival direction as the lateral coordinate (sine of the
2257 // lateral angle, uniform over a diffuse field), positive
2258 // toward the source's side: the first two (floor and
2259 // ceiling) share the source's own direction (30 degrees, a
2260 // hard-panned input), the next come mostly from the source's
2261 // side of the room, the later ones from everywhere.
2262 const double side = hash01(k, c + 50) < 0.92 - 0.42 * w ? 1.0 : -1.0;
2263 lat[k] = k < 2 ? 0.5 : side * (0.25 + 0.75 * hash01(k, c + 60));
2264 }
2265 // No DC: the early field's low end is left to the (coherent) late
2266 // field, and the output high-pass never has to drain a step.
2267 if (R > discrete)
2268 for (int k = discrete; k < R; ++k) gv[k] -= dcSum / (R - discrete);
2269 double energy = 0.0;
2270 for (int k = 0; k < R; ++k) energy += gv[k] * gv[k];
2271 // The early field joins the late one on a single exponential:
2272 // the late field's energy flow at time t after its injection is
2273 // exp(-t / tau) / (mean round trip) (the output taps follow the
2274 // absorption, so even its first echoes sit on that curve), and
2275 // the early reflections carry that same density over their
2276 // window. Half per input, so a mono source keeps it.
2277 const double tau = decay / (2.0 * kLn1000);
2278 const double tInj = static_cast<double>(erToLateSamples_.load(std::memory_order_relaxed)) / sr
2279 + kMeanInjectMs * 1e-3;
2280 const double target = static_cast<double>(kOutGain) * static_cast<double>(kOutGain)
2281 * tau / meanLoopSec_
2282 * (std::exp(-(minMs * 1e-3 - tInj) / tau) - std::exp(-(maxMs * 1e-3 - tInj) / tau));
2283 const double norm = std::sqrt(0.5 * target / energy);
2284
2285 // Every reflection reaches both outputs like both ears: the far
2286 // one later (Woodworth ITD, up to 0.66 ms), quieter (up to
2287 // -6 dB) and, beyond a lateral angle of 17 degrees, duller (half
2288 // its highs); the near one up to 1.6 dB louder.
2289 for (int e = 0; e < 2; ++e)
2290 {
2291 for (int k = 0; k < R; ++k)
2292 {
2293 const double x = e == c ? lat[k] : -lat[k]; // toward this ear
2294 const double phi = std::asin(std::min(1.0, std::abs(x)));
2295 const double itdMs = x < 0.0 ? 1000.0 * 0.0875 / 343.0 * (phi + std::sin(phi)) : 0.0;
2296 const int bin = x < -0.3 ? 1 : 0;
2297 const double ild = x > 0.0 ? 1.0 + 0.2 * x : 1.0 + 0.5 * x;
2298 taps[e][used[e]++] = { msv[k] + itdMs, (e == c ? 0 : 1) + (k < discrete ? 2 : 0),
2299 bin, gv[k] * norm * ild };
2300 }
2301 }
2302 }
2303 for (int e = 0; e < 2; ++e)
2304 {
2305 // Time order, so the absorption groups run from early to late.
2306 std::sort(taps[e].begin(), taps[e].begin() + used[e],
2307 [](const Tap& a, const Tap& b) { return a.ms < b.ms; });
2308 for (int k = 0; k < used[e]; ++k)
2309 {
2310 erTap_[e][k] = std::max(1, static_cast<int>(taps[e][k].ms * sr / 1000.0));
2311 erChan_[e][k] = taps[e][k].chan;
2312 erBin_[e][k] = taps[e][k].bin;
2313 erGainTarget_[e][k] = static_cast<T>(taps[e][k].gain);
2314 }
2315 }
2316
2317 // Each absorption group loses its highs as the late field does by
2318 // the group's mean arrival time: 60 dB per HF T60 beyond the mid band.
2319 const double fh = std::clamp(static_cast<double>(highCrossover_.load(std::memory_order_relaxed)),
2320 100.0, 0.45 * sr);
2321 erShelfCoeff_ = static_cast<T>(1.0 - std::exp(-6.283185307179586 * fh / sr));
2322 const double t60H = std::max(0.05, calibratedMidDecay()
2323 * static_cast<double>(highDecayMult_.load(std::memory_order_relaxed)));
2324 for (int g = 0; g < kERGroups; ++g)
2325 {
2326 double tSum = 0.0;
2327 int cnt = 0;
2328 for (int e = 0; e < 2; ++e)
2329 for (int k = erGroupStart_[g]; k < erGroupStart_[g + 1]; ++k, ++cnt)
2330 tSum += erTap_[e][k] / sr;
2331 const double t = cnt > 0 ? tSum / cnt : 0.0;
2332 erShelfGainTarget_[g] = static_cast<T>(std::pow(10.0, -3.0 * t * std::max(0.0, 1.0 / t60H - 1.0 / decay)));
2333 }
2334 }
2335
2336 // Sieve of Eratosthenes up to kPrimeTableMax, computed once on first use.
2337 static constexpr int kPrimeTableMax = 131072;
2338
2339 static const std::vector<uint8_t>& getPrimeSieve() noexcept
2340 {
2341 static const std::vector<uint8_t> sieve = []
2342 {
2343 std::vector<uint8_t> s(static_cast<size_t>(kPrimeTableMax), 1);
2344 s[0] = 0;
2345 s[1] = 0;
2346 for (int i = 2; i * i < kPrimeTableMax; ++i)
2347 if (s[static_cast<size_t>(i)])
2348 for (int j = i * i; j < kPrimeTableMax; j += i)
2349 s[static_cast<size_t>(j)] = 0;
2350 return s;
2351 }();
2352 return sieve;
2353 }
2354
2356 static int nearestPrime(int n) noexcept
2357 {
2358 if (n <= 2) return 2;
2359 if (n < kPrimeTableMax)
2360 {
2361 const auto& sieve = getPrimeSieve();
2362 while (n < kPrimeTableMax && !sieve[static_cast<size_t>(n)])
2363 ++n;
2364 if (n < kPrimeTableMax) return n;
2365 }
2366 if (n % 2 == 0) ++n;
2367 while (true)
2368 {
2369 bool isPrime = true;
2370 for (int d = 3; d * d <= n; d += 2)
2371 if (n % d == 0) { isPrime = false; break; }
2372 if (isPrime) return n;
2373 n += 2;
2374 }
2375 }
2376
2377 // --- Preset application --------------------------------------------------
2378
2383
2384 void commitPreset(const PresetValues& p) noexcept
2385 {
2386 // Only write the atomics the user did NOT override since the last
2387 // setType(). The acquire-read pairs with the release-mark in each setter.
2388 const uint32_t m = userParamMask_.load(std::memory_order_acquire);
2389 if (!(m & kUserSize)) size_.store(p.size, std::memory_order_relaxed);
2390 if (!(m & kUserDecay)) decayTime_.store(p.decay, std::memory_order_relaxed);
2391 if (!(m & kUserDamping)) highDecayMult_.store(p.hdMult, std::memory_order_relaxed);
2392 if (!(m & kUserBassDecay)) bassDecayMult_.store(p.bdMult, std::memory_order_relaxed);
2393 if (!(m & kUserHighXover)) highCrossover_.store(p.hxover, std::memory_order_relaxed);
2394 if (!(m & kUserBassXover)) bassCrossover_.store(p.bxover, std::memory_order_relaxed);
2395 if (!(m & kUserDiffusion)) diffusion_.store(p.diff, std::memory_order_relaxed);
2396 if (!(m & kUserModDepth)) modDepth_.store(p.modDepth, std::memory_order_relaxed);
2397 if (!(m & kUserModRate)) modRate_.store(p.modRate, std::memory_order_relaxed);
2398 if (!(m & kUserEarly)) earlyLevel_.store(p.earlyLvl, std::memory_order_relaxed);
2399 if (!(m & kUserLate)) lateLevel_.store(p.lateLvl, std::memory_order_relaxed);
2400 if (!(m & kUserErToLate)) erToLateMs_.store(p.erToLate, std::memory_order_relaxed);
2401 }
2402
2403 void applyPreset(Type type) noexcept
2404 {
2405 // size decay hdMult bdMult hxover bxover
2406 // diff modDep modRate early late erToLate
2407 switch (type)
2408 {
2409 case Type::Room:
2410 commitPreset({T(0.22), T(0.5), T(0.40), T(1.1), T(5000), T(250),
2411 T(0.72), T(0.07), T(0.5), T(1), T(0.8), T(0)});
2412 break;
2413 case Type::Hall:
2414 commitPreset({T(0.68), T(2.2), T(0.32), T(1.3), T(4500), T(200),
2415 T(0.84), T(0.10), T(0.55), T(1), T(1), T(15)});
2416 break;
2417 case Type::Chamber:
2418 commitPreset({T(0.38), T(1.2), T(0.38), T(1.1), T(5000), T(250),
2419 T(0.78), T(0.08), T(0.6), T(1), T(0.9), T(8)});
2420 break;
2421 case Type::Plate:
2422 commitPreset({T(0.14), T(1.5), T(0.55), T(0.8), T(7000), T(150),
2423 T(0.94), T(0.16), T(1.4), T(0), T(1), T(0)});
2424 break;
2425 case Type::Spring:
2426 commitPreset({T(0.11), T(0.9), T(0.28), T(1.0), T(4000), T(200),
2427 T(0.6), T(0.08), T(0.35), T(0.5), T(1), T(0)});
2428 break;
2429 case Type::Cathedral:
2430 commitPreset({T(0.98), T(5.0), T(0.24), T(1.5), T(3500), T(150),
2431 T(0.91), T(0.12), T(0.35), T(1), T(1), T(25)});
2432 break;
2433 }
2434
2435 spring_ = type == Type::Spring;
2436 erType_ = type;
2438 if (prepared_)
2439 updateAll();
2440 }
2441};
2442
2443} // namespace dspark
True-stereo 32-line FDN reverb with exact per-band decay and 6 presets.
static constexpr T rotRateFactor(int i) noexcept
static constexpr double kOutDiffMs_[2][kOutStages]
Quality
Engine quality / CPU cost trade-off (see setQuality()).
@ Eco
Reduced 8-line engine, about 3x cheaper. For constrained targets.
@ Full
Complete 32-line engine. Default.
void runSprings(T &lateL, T &lateR, const T(&raw)[2]) noexcept
One sample of the spring tanks (Type::Spring): six springs per side (one in Eco), all of different le...
std::array< std::array< T, kERGroups >, 2 > erShadowLP_
T getHighDecayMultiplier() const noexcept
static constexpr double kLoopApMs_[kMaxLines]
static void hadamardSmall(T *x) noexcept
T sizeFactor() const noexcept
Size factor: size 0 -> 0.35, size 1 -> 1.
std::array< int, kMaxLines > loopAPLen_
in-loop allpass target lengths
double meanLoopSec_
mean FDN round trip (s), for the early/late energy match
std::atomic< int > erToLateSamples_
void processChunk(const T *inL, const T *inR, T *outL, T *outR, int count) noexcept
Core processing of up to kChunk samples: stereo in, wet stereo out.
T erGlidePerSample_
glide rate of the two (1 / samples)
std::array< T, kERGroups > erShelfGainTarget_
std::array< T, kMaxLines > posInc_
per-sample ramp
static constexpr double kInjectMs_[kMaxLines]
static constexpr double kMaxInjectMs
static constexpr int kShadowBins
head-shadow classes of the early taps (open, shadowed)
std::pair< T, T > outputStage(T lateL, T lateR, T earlyL, T earlyR) noexcept
static constexpr int kInStages
input diffusers per channel feeding the late field (Full)
static constexpr double kSpringCutHz
dispersion band edge (transition frequency)
static constexpr double kMaxErToLateMs
std::array< T, kMaxLines/2 > rotC_
std::array< std::array< T, kERGroups >, 2 > erLP_
std::array< T, kMaxLines > bassY1_
std::array< T, 5 > cohC_
its coefficients b0 b1 b2 a1 a2
std::array< std::array< int, kMaxERTaps >, 2 > erBin_
static constexpr double kInDiffCoeffs_[kInStages]
static constexpr double kInDiffMs_[2][kInStages]
static double hash01(int k, int s) noexcept
Deterministic hash in [0, 1) for the early-reflection layout.
void setQuality(Quality q) noexcept
Selects the engine quality / CPU cost trade-off.
Type getType() const noexcept
static constexpr uint32_t rotSeed(int i) noexcept
std::array< int, kMaxLines > lenTarget_
prime line lengths (samples)
static void fwht(T *x) noexcept
void setToneHighCut(T hz) noexcept
Sets a post-reverb high-cut filter on the wet signal (12 dB/oct).
static constexpr double kMaxInDiffMs
static constexpr double kMaxGlideSpeed
max length change per sample (4% Doppler)
double calibratedMidDecay() const noexcept
The loop's DC decay that puts the ISO 3382 mid-frequency reverberation time on the decay setting.
void applyPreset(Type type) noexcept
static const std::vector< uint8_t > & getPrimeSieve() noexcept
std::array< T, kMaxLines > lenCur_
gliding length
std::pair< T, T > processSample(T input) noexcept
Processes a single mono sample and returns the wet stereo pair.
void generateERTapsForType(Type type) noexcept
Regenerates the early-reflection taps for a reverb type (Eco caps the count).
void refreshCachedParams() noexcept
Pulls the atomic parameters into the block-local cache (audio thread).
static constexpr int kPrimeTableMax
void setDiffusion(T amount) noexcept
Diffusion (0 - 1): the strength of the input, in-loop and output allpass diffusers,...
void setType(Type type) noexcept
Loads the selected preset baseline.
static constexpr int kEcoSprings
static constexpr double kMaxPreDelayMs
void setEarlyLevel(T dB) noexcept
Early reflections level in dB (-60 to +6). 0 dB is the physical balance: the early field then carries...
static constexpr int kMaxERTaps
early taps per output side: 64 reflections per input (Full)
Quality getQuality() const noexcept
std::array< T, kMaxLines > outSignR_
static constexpr double kMeanInjectMs
mean of kInjectMs_
static constexpr int kOutSignR_[kMaxLines]
static constexpr int kSpringStages
dispersion allpasses per spring (Full)
T getErToLateDelay() const noexcept
std::array< T, kMaxLines > jotB1_
std::array< std::array< T, 2 >, 2 > subZ_
subsonic high-pass states per channel
static constexpr double kShadowHz
head-shadow corner of the early taps
static constexpr int kOutStages
static constexpr int kChunk
sparse-FIR processing chunk (samples)
std::array< T, kMaxLines > jotA1_
static constexpr int kMaxLines
std::atomic< bool > paramsDirty_
std::array< T, kInStages > inAPCoeff_
std::array< T, kMaxLines/2 > rotS_
std::array< std::array< T, 5 >, 2 > springLP_
b0 b1 b2 a1 a2, two sections
std::array< T, kMaxLines > apY1_
allpass interpolator state
void controlTick() noexcept
Control-rate update: glides the line lengths toward their targets, draws the next modulation values a...
static constexpr double kCoherenceQ
static constexpr T rotRate() noexcept
std::array< T, kMaxLines > pos_
current read position
std::array< T, kMaxLines > outSignL_
T erShelfCoeff_
one-pole coefficient at the high crossover
static int nearestPrime(int n) noexcept
Smallest prime >= n (lengths only grow by a few samples).
T getBassDecayMultiplier() const noexcept
void setModulation(T amount) noexcept
Modulation depth (0 - 1) of the delay-line wander. It smears the tail's resonances; 0 is fully static...
T getHighCrossover() const noexcept
T getModulation() const noexcept
static constexpr double kBaseDelaysMs_[kMaxLines]
static constexpr uint32_t lfoSeed(int i) noexcept
std::array< std::array< Line, kOutStages >, 2 > outAP_
late-field output diffusers
static T levelDb(T gain) noexcept
Linear gain to dB (a zero gain, a preset's muted early field, reads -120 dB).
void setBassCrossover(T hz) noexcept
Frequency where the bass decay transition is centered.
static constexpr int kInjectSign_[kMaxLines]
T getEarlyLevel() const noexcept
Early reflections level in dB (as set, or the preset's).
void setPreDelay(T ms) noexcept
Pre-delay before the early reflections, in ms (0 - 200).
T getPreDelay() const noexcept
static constexpr T kSpringOutGain
std::array< T, kERGroups > erShelfGain_
HF gain of each absorption group.
std::array< std::array< int, kInStages >, 2 > inAPLen_
void setMix(T dryWet) noexcept
Dry/wet mix (0 = dry, 1 = wet).
T getModRate() const noexcept
void drainPendingChanges() noexcept
Drains deferred parameter changes on the audio thread.
int msToSamples(T ms) const noexcept
static constexpr int kEcoSpringStages
void reset() noexcept
Clears all delay lines and filter states and restarts the modulation, snapping any length glide to it...
static constexpr int kCtrl
modulation control period (samples)
std::array< T, kMaxLines > bassX1_
static constexpr T lfoRateFactor(int i) noexcept
static constexpr double kSpringJitterMs
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
T getBassCrossover() const noexcept
std::array< std::array< T, kSprings >, 4 > springLPState_
[section * 2 + state][spring]
static constexpr int kRotPairStride
std::array< T, kMaxLines > loopAPCur_
gliding lengths after a size change
std::array< T, kMaxLines > injGain_
sign / sqrt(lines)
std::array< std::array< Line, kInStages >, 2 > inAP_
void setLateLevel(T dB) noexcept
Late tail level in dB (-60 to +6).
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).
std::array< T, kMaxLines > bassA1_
static double springBaseMs(int s) noexcept
std::array< T, kMaxLines > bassB1_
T getLateLevel() const noexcept
Late tail level in dB (as set, or the preset's).
std::array< std::array< T, kMaxERTaps >, 2 > erGainTarget_
gains for the current size/decay
std::atomic< bool > toneDirty_
std::array< T, kMaxLines > bassB0_
static constexpr double kMaxErMs
static constexpr int kERGroups
absorption groups (early -> late)
void setDamping(T amount) noexcept
Sets high-frequency damping (0 = bright, 1 = dark).
void setErToLateDelay(T ms) noexcept
Extra gap between the early reflections and the late tail, in ms (0 - 200).
void processBlock(AudioBufferView< T > buffer) noexcept
Processes an audio block in place with zero allocations.
void markUserParam(uint32_t bit) noexcept
std::array< std::array< int, kOutStages >, 2 > outAPLen_
static constexpr T kMinReadPos
static constexpr double kRotRateHz
std::atomic< uint32_t > userParamMask_
void setSize(T size) noexcept
Room size (0.01 - 1). Scales the delay lines from 36% to 100% of their base lengths....
std::atomic< bool > presetDirty_
void commitPreset(const PresetValues &p) noexcept
std::array< SmoothRandomLFO, kMaxLines/2 > rotLfo_
static constexpr double kSubsonicHz
wet-output high-pass (2nd order): no room rings below it
void setModRate(T hz) noexcept
Modulation rate in Hz (0.1 - 5); each line wanders at its own multiple.
static constexpr int kSprings
std::array< SmoothRandomLFO, kMaxLines > lfo_
void updateDecayParams() noexcept
Per-line absorption: Jot mid/high shelf and bass shelf.
void setWidth(T width) noexcept
Sets stereo width of the late reverb tail.
@ 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< std::array< int, kMaxERTaps >, 2 > erTap_
T getDiffusion() const noexcept
std::array< int, kERGroups+1 > erGroupStart_
void setHighDecayMultiplier(T mult) noexcept
Sets HF decay as a multiplier of mid decay time.
std::array< Line, 2 > preDelay_
void generateERTaps(double minMs, double maxMs, int numTaps) noexcept
Velvet-noise early field (Valimaki et al.): numTaps sparse +-1 impulses per side between minMs and ma...
std::array< Line, 2 > ring_
allpass-diffused input, feeds the late field
static constexpr double kCoherenceHz
side high-pass of the late field (2nd order, Q 0.74)
std::atomic< Quality > quality_
static void addTap(T *acc, const Line &l, int lag0, T gain, int count) noexcept
std::array< std::array< T, kMaxERTaps >, 2 > erGain_
current (gliding) tap gains
void refreshTopology() noexcept
Line count of the active engine: the springs, or the FDN size.
static constexpr int kMaxDiscreteER
raw (discrete) early taps at diffusion 0
std::array< std::array< int, kMaxERTaps >, 2 > erChan_
std::array< T, kMaxLines > jotX1_
void setHighCrossover(T hz) noexcept
Frequency where the HF decay transition is centered.
static constexpr double kModMaxMs
void prepare(const AudioSpec &spec)
Prepares the reverberation engine and allocates required memory.
std::array< T, kMaxLines > jotY1_
std::array< T, kMaxLines > jotB0_
void runFdn(T &lateL, T &lateR, int injLag) noexcept
One sample of the N-line FDN (compile-time N so the per-line stages vectorize): allpass-interpolated ...
void setDecay(T seconds) noexcept
Mid-frequency reverberation time in seconds (0.1 - 30).
static constexpr double kShadowHF
high-frequency gain of a shadowed (far-ear) tap
static constexpr int kEcoLines
T getDamping() const noexcept
static constexpr int kOutSignL_[kMaxLines]
void setBassDecayMultiplier(T mult) noexcept
Sets bass decay as a multiplier of mid decay time.
std::atomic< bool > qualityDirty_
std::array< int, kMaxLines > injTap_
static constexpr T kSoftLimit
in-loop safety limiter threshold
std::atomic< int > preDelaySamples_
std::array< T, 2 > cohZ_
side high-pass state (coherent low band)
static constexpr double kGlideMs
size-change glide time constant
static constexpr double kRotMaxRad
peak rotation angle at modulation 1 (rad)
static constexpr double kMaxLoopApMs
static constexpr int kEcoERTaps
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:661
void setCoeffsNow(const BiquadCoeffs &c) noexcept
Stream-owner direct set: makes c the active set immediately.
Definition Biquad.h:771
void reset() noexcept
Resets all per-channel filter states to zero to avoid ringing/clicks.
Definition Biquad.h:811
T processSample(T input, int channel) noexcept
Processes a single sample for a specific channel.
Definition Biquad.h:837
RAII scope guard to disable denormalised (subnormal) floating-point numbers.
Pre-allocated, SIMD-friendly dry/wet blender for real-time audio.
Definition DryWetMixer.h:78
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 addWithGain(float *DSPARK_RESTRICT dst, const float *DSPARK_RESTRICT src, float gain, int count) noexcept
Adds source samples scaled by a gain factor into a destination buffer.
Definition SimdOps.h:77
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
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:651
Block-cached copies of the atomics read in the sample loop.
Equal-size delay lines in one contiguous buffer sharing a write index (every line is written once per...
void prepare(int numLines, int maxDelay)
T at(int i, int k) const noexcept
Power-of-two circular delay line: at(k) is the sample pushed k samples ago.
Hermite-interpolated random noise generator for organic modulation.
void setRate(T rate, double sr) noexcept
void prepare(double sr, T rate, uint32_t seed) noexcept
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:71
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45
static BiquadCoeffs makeHighPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
High-pass filter.
Definition Biquad.h:142
static BiquadCoeffs makeLowPass(double sampleRate, double freq, double Q=0.7071067811865476) noexcept
Low-pass filter.
Definition Biquad.h:118