DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
Delay.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
37#include "../Core/AudioBuffer.h"
38#include "../Core/AudioSpec.h"
39#include "../Core/DspMath.h"
40#include "../Core/Smoothers.h"
41#include "../Core/StateBlob.h"
42
43#include <algorithm>
44#include <array>
45#include <atomic>
46#include <cmath>
47#include <cstdint>
48#include <cstring>
49#include <vector>
50
51namespace dspark {
52
53template <typename SampleType>
54class Delay final
55{
56public:
62
64 enum class FeedbackMode
65 {
66 Clean,
69 Analog
72 };
73
75 void setFeedbackMode(FeedbackMode mode) noexcept
76 {
77 feedbackMode_.store(mode, std::memory_order_relaxed);
78 }
79
81 [[nodiscard]] FeedbackMode getFeedbackMode() const noexcept
82 {
83 return feedbackMode_.load(std::memory_order_relaxed);
84 }
85
86 // -- Lifecycle -----------------------------------------------------------
87
92 void prepare(const AudioSpec& spec)
93 {
95 }
96
105 void prepare(const AudioSpec& spec, double maxDelaySeconds)
106 {
107 if (!spec.isValid() || !(maxDelaySeconds >= 0.0)) return;
108 sampleRate_ = static_cast<SampleType>(spec.sampleRate);
109 // Clamp to the fixed per-channel state arrays (states_/writeIndices_ are
110 // kMaxChannels): maybeUpdateSmoothers()/updateSmootherTargets() iterate
111 // [0, numChannels_) and would otherwise read/write out of bounds.
112 numChannels_ = std::min(static_cast<int>(spec.numChannels), kMaxChannels);
113 blockSize_ = spec.maxBlockSize;
114
115 // Cap in double BEFORE the int cast: a huge request would otherwise be
116 // undefined behaviour in the cast, and nextPow2 past 2^29 overflows
117 // its shift. 2^29 samples is over 3 hours at 48 kHz.
118 const double capacityD =
119 std::min(std::ceil(maxDelaySeconds * spec.sampleRate) + blockSize_,
120 536870912.0); // 2^29
121 maxDelaySamples_ = nextPow2(static_cast<int>(capacityD));
122 bufferMask_ = maxDelaySamples_ - 1;
123
124 delayBuffer_.resize(numChannels_, maxDelaySamples_);
125 wetBuffer_.resize(numChannels_, blockSize_);
126 lastWetSamples_ = blockSize_; // full capacity until the first push
127
128 float timeMs = smoothingTimeMs_.load(std::memory_order_relaxed);
129 for (int ch = 0; ch < std::min(numChannels_, kMaxChannels); ++ch)
130 {
131 resetChannelSmoothers(states_[ch], timeMs, 0.0f);
132 writeIndices_[ch] = 0;
133 }
134
135 mixSmoother_.reset(spec.sampleRate, timeMs, 1.0f);
136 insertMixMaxStep_ = static_cast<SampleType>(1.0 / std::max(1.0, spec.sampleRate * 0.02));
137 reset();
138
139 prepared_.store(true, std::memory_order_release);
140 }
141
147 void prepareMs(const AudioSpec& spec, double maxDelayMs)
148 {
149 prepare(spec, maxDelayMs / 1000.0);
150 }
151
155 void reset() noexcept
156 {
157 delayBuffer_.clear();
158 wetBuffer_.clear();
159 for (int ch = 0; ch < kMaxChannels; ++ch)
160 {
161 writeIndices_[ch] = 0;
162 resetChannelState(states_[ch]);
163 }
164 mixSmoother_.skip();
165 insertMix_ = mix_.load(std::memory_order_relaxed); // no fade-in on start
166 }
167
168 // -- Configuration -------------------------------------------------------
169
174 void setSmoother(SmootherType type) noexcept
175 {
176 smootherType_.store(type, std::memory_order_relaxed);
177 smootherDirty_.store(true, std::memory_order_release);
178 }
179
184 void setSmoothingTime(float ms) noexcept
185 {
186 if (!std::isfinite(ms)) return;
187 smoothingTimeMs_.store(std::max(0.0f, ms), std::memory_order_relaxed);
188 smootherDirty_.store(true, std::memory_order_release);
189 }
190
191 // -- Parameters ----------------------------------------------------------
192
199 void setDelaySamples(SampleType samples) noexcept
200 {
201 if (!std::isfinite(samples)) return;
202 samples = std::clamp(samples, SampleType(0),
203 static_cast<SampleType>(std::max(0, maxDelaySamples_ - 1)));
204 globalDelay_.store(samples, std::memory_order_relaxed);
205 // Publish/apply pattern: the smoothers are NOT touched here (they are
206 // non-atomic audio-thread state - mutating them from a control thread
207 // raced against advanceSmoother). The audio thread applies the new
208 // target in maybeUpdateSmoothers() at the top of its next process call.
209 delayTargetDirty_.store(true, std::memory_order_release);
210 }
211
213 void setDelayMs(SampleType ms) noexcept { setDelaySamples(ms * sampleRate_ / SampleType(1000)); }
214
216 void setDelaySeconds(SampleType secs) noexcept { setDelaySamples(secs * sampleRate_); }
217
219 [[nodiscard]] SampleType getCurrentDelaySamples() const noexcept { return globalDelay_.load(std::memory_order_relaxed); }
220
227 void setFeedback(SampleType gain) noexcept
228 {
229 if (!std::isfinite(gain)) return;
230 feedbackGain_.store(gain, std::memory_order_relaxed);
231 }
232
235 void setFeedbackLpHz(SampleType freq) noexcept
236 {
237 if (!std::isfinite(freq)) return;
238 fbLpCoef_.store((freq > 0) ? calcLpCoef(freq) : SampleType(0), std::memory_order_relaxed);
239 fbLpHzShadow_.store(freq, std::memory_order_relaxed); // serialization readback
240 }
241
244 void setFeedbackHpHz(SampleType freq) noexcept
245 {
246 if (!std::isfinite(freq)) return;
247 fbHpCoef_.store((freq > 0) ? calcHpCoef(freq) : SampleType(0), std::memory_order_relaxed);
248 fbHpHzShadow_.store(freq, std::memory_order_relaxed); // serialization readback
249 }
250
251 // -- Sample processing ---------------------------------------------------
252
259 SampleType processSample(int ch, SampleType input) noexcept
260 {
261 if (ch < 0 || ch >= numChannels_ || !prepared_.load(std::memory_order_acquire)) return input;
262 maybeUpdateSmoothers();
263
264 auto& s = states_[ch];
265 auto st = smootherType_.load(std::memory_order_relaxed);
266 SampleType delay = (st == SmootherType::None)
267 ? globalDelay_.load(std::memory_order_relaxed) : advanceSmoother(s, st);
268
269 SampleType out = processSampleInternal(ch, input, delay, s,
270 feedbackGain_.load(std::memory_order_relaxed),
271 fbLpCoef_.load(std::memory_order_relaxed),
272 fbHpCoef_.load(std::memory_order_relaxed));
273 advanceWriteIndexUnchecked(ch);
274 return out;
275 }
276
277 // -- Block processing ----------------------------------------------------
278
289 {
290 if (!prepared_.load(std::memory_order_acquire)) return;
291 maybeUpdateSmoothers();
292
293 const int nS = buffer.getNumSamples();
294 const int nCh = std::min(buffer.getNumChannels(), numChannels_);
295 const auto smoothType = smootherType_.load(std::memory_order_relaxed);
296 const SampleType targetDelay = globalDelay_.load(std::memory_order_relaxed);
297 const SampleType fb = feedbackGain_.load(std::memory_order_relaxed);
298 const SampleType lpC = fbLpCoef_.load(std::memory_order_relaxed);
299 const SampleType hpC = fbHpCoef_.load(std::memory_order_relaxed);
300 const SampleType mixTarget = mix_.load(std::memory_order_relaxed);
301 const SampleType mixStart = insertMix_;
302 const bool pingPong = pingPong_.load(std::memory_order_relaxed) && nCh >= 2;
303
304 if (pingPong)
305 {
306 // The first two channels cross their feedback (the same wiring as
307 // processPingPong()); the blend is the insert's.
308 const bool analogFb = feedbackMode_.load(std::memory_order_relaxed) == FeedbackMode::Analog;
309 SampleType* L = buffer.getChannel(0);
310 SampleType* R = buffer.getChannel(1);
311 auto& sL = states_[0];
312 auto& sR = states_[1];
313 for (int i = 0; i < nS; ++i)
314 {
315 const SampleType currentDelay = (smoothType == SmootherType::None)
316 ? targetDelay : advanceSmoother(sL, smoothType);
317 advanceSmoother(sR, smoothType); // kept in step, as in processPingPong()
318 const SampleType dryL = L[i], dryR = R[i];
319 const SampleType outL = processSampleInternal(0, dryL + sL.pingPongFb, currentDelay, sL, SampleType(0), lpC, hpC);
320 const SampleType outR = processSampleInternal(1, dryR + sR.pingPongFb, currentDelay, sR, SampleType(0), lpC, hpC);
321 sR.pingPongFb = saturateFeedback(processFbFilters(1, outL * fb, lpC, hpC), analogFb);
322 sL.pingPongFb = saturateFeedback(processFbFilters(0, outR * fb, lpC, hpC), analogFb);
323 advanceWriteIndexUnchecked(0);
324 advanceWriteIndexUnchecked(1);
325 const SampleType m = moveTowards(mixStart, mixTarget,
326 insertMixMaxStep_ * static_cast<SampleType>(i + 1));
327 L[i] = dryL + (outL - dryL) * m;
328 R[i] = dryR + (outR - dryR) * m;
329 }
330 }
331
332 for (int ch = pingPong ? 2 : 0; ch < nCh; ++ch)
333 {
334 SampleType* data = buffer.getChannel(ch);
335 auto& s = states_[ch];
336 for (int i = 0; i < nS; ++i)
337 {
338 const SampleType currentDelay = (smoothType == SmootherType::None)
339 ? targetDelay : advanceSmoother(s, smoothType);
340 const SampleType dry = data[i];
341 const SampleType wet = processSampleInternal(ch, dry, currentDelay, s, fb, lpC, hpC);
342 advanceWriteIndexUnchecked(ch);
343 const SampleType m = moveTowards(mixStart, mixTarget,
344 insertMixMaxStep_ * static_cast<SampleType>(i + 1));
345 data[i] = dry + (wet - dry) * m;
346 }
347 }
348 insertMix_ = moveTowards(mixStart, mixTarget, insertMixMaxStep_ * static_cast<SampleType>(nS));
349 }
350
356 void setMix(SampleType mix) noexcept
357 {
358 if (!std::isfinite(mix)) return;
359 mix_.store(std::clamp(mix, SampleType(0), SampleType(1)), std::memory_order_relaxed);
360 }
361
363 [[nodiscard]] SampleType getMix() const noexcept { return mix_.load(std::memory_order_relaxed); }
364
373 void setPingPong(bool on) noexcept { pingPong_.store(on, std::memory_order_relaxed); }
374
376 [[nodiscard]] bool getPingPong() const noexcept { return pingPong_.load(std::memory_order_relaxed); }
377
379 [[nodiscard]] int getLatency() const noexcept { return 0; }
380
394 void processBlock(AudioBufferView<SampleType> buffer, SampleType delayMs,
395 SampleType feedback = 0, SampleType lpHz = 0, SampleType hpHz = 0) noexcept
396 {
397 if (!prepared_.load(std::memory_order_acquire)) return;
398
399 setDelayMs(delayMs);
400 setFeedback(feedback);
401 setFeedbackLpHz(lpHz);
402 setFeedbackHpHz(hpHz);
403 maybeUpdateSmoothers();
404
405 const int nS = buffer.getNumSamples();
406 const int nCh = std::min(buffer.getNumChannels(), numChannels_);
407
408 // Cache atomic parameters to local stack for auto-vectorization & performance
409 const auto smoothType = smootherType_.load(std::memory_order_relaxed);
410 const SampleType targetDelay = globalDelay_.load(std::memory_order_relaxed);
411 const SampleType fb = feedbackGain_.load(std::memory_order_relaxed);
412 const SampleType lpC = fbLpCoef_.load(std::memory_order_relaxed);
413 const SampleType hpC = fbHpCoef_.load(std::memory_order_relaxed);
414
415 for (int ch = 0; ch < nCh; ++ch)
416 {
417 SampleType* data = buffer.getChannel(ch);
418 auto& s = states_[ch];
419
420 for (int i = 0; i < nS; ++i)
421 {
422 SampleType currentDelay = (smoothType == SmootherType::None) ? targetDelay : advanceSmoother(s, smoothType);
423 data[i] = processSampleInternal(ch, data[i], currentDelay, s, fb, lpC, hpC);
424 advanceWriteIndexUnchecked(ch);
425 }
426 }
427 }
428
432 void processChannel(AudioBufferView<SampleType> buffer, int ch, SampleType delayMs,
433 SampleType feedback = 0, SampleType lpHz = 0, SampleType hpHz = 0) noexcept
434 {
435 if (ch < 0 || ch >= numChannels_ || !prepared_.load(std::memory_order_acquire)) return;
436
437 setDelayMs(delayMs);
438 setFeedback(feedback);
439 setFeedbackLpHz(lpHz);
440 setFeedbackHpHz(hpHz);
441 maybeUpdateSmoothers();
442
443 SampleType* data = buffer.getChannel(ch);
444 const int nS = buffer.getNumSamples();
445
446 const auto smoothType = smootherType_.load(std::memory_order_relaxed);
447 const SampleType targetDelay = globalDelay_.load(std::memory_order_relaxed);
448 const SampleType fb = feedbackGain_.load(std::memory_order_relaxed);
449 const SampleType lpC = fbLpCoef_.load(std::memory_order_relaxed);
450 const SampleType hpC = fbHpCoef_.load(std::memory_order_relaxed);
451
452 auto& s = states_[ch];
453
454 for (int i = 0; i < nS; ++i)
455 {
456 SampleType currentDelay = (smoothType == SmootherType::None) ? targetDelay : advanceSmoother(s, smoothType);
457 data[i] = processSampleInternal(ch, data[i], currentDelay, s, fb, lpC, hpC);
458 advanceWriteIndexUnchecked(ch);
459 }
460 }
461
462 // -- Wet buffer processing -----------------------------------------------
463
466 {
467 pushDryToWetImpl([&](int ch) { return dry.getChannel(ch); }, dry.getNumChannels(), dry.getNumSamples());
468 }
469
472 {
473 pushDryToWetImpl([&](int ch) { return dry.getChannel(ch); }, dry.getNumChannels(), dry.getNumSamples());
474 }
475
481 void processWet(SampleType delayMs, SampleType feedback = 0, SampleType lpHz = 0, SampleType hpHz = 0) noexcept
482 {
483 processBlock(wetBuffer_.toView().getSubView(0, lastWetSamples_), delayMs, feedback, lpHz, hpHz);
484 }
485
494 void processPingPong(SampleType delayMs, SampleType feedback = 0, SampleType lpHz = 0, SampleType hpHz = 0) noexcept
495 {
496 if (wetBuffer_.getNumChannels() < 2 || !prepared_.load(std::memory_order_acquire)) return;
497
498 setDelayMs(delayMs);
499 setFeedback(feedback);
500 setFeedbackLpHz(lpHz);
501 setFeedbackHpHz(hpHz);
502 maybeUpdateSmoothers();
503
504 SampleType* L = wetBuffer_.getChannel(0);
505 SampleType* R = wetBuffer_.getChannel(1);
506 const int nS = std::min(lastWetSamples_, wetBuffer_.getNumSamples());
507
508 const auto smoothType = smootherType_.load(std::memory_order_relaxed);
509 const SampleType targetDelay = globalDelay_.load(std::memory_order_relaxed);
510 const SampleType fb = feedbackGain_.load(std::memory_order_relaxed);
511 const SampleType lpC = fbLpCoef_.load(std::memory_order_relaxed);
512 const SampleType hpC = fbHpCoef_.load(std::memory_order_relaxed);
513 const bool analogFb = feedbackMode_.load(std::memory_order_relaxed) == FeedbackMode::Analog;
514
515 for (int i = 0; i < nS; ++i)
516 {
517 auto& sL = states_[0];
518 auto& sR = states_[1];
519
520 SampleType currentDelay = (smoothType == SmootherType::None) ? targetDelay : advanceSmoother(sL, smoothType);
521 advanceSmoother(sR, smoothType); // Keep smoothers in sync
522
523 // s.pingPongFb holds what THIS channel receives next sample, i.e.
524 // the OTHER channel's previous output. (The original wiring
525 // consumed each channel's own stored value while storing f(other)
526 // into it: every echo regenerated into its own channel and the
527 // signal never crossed the stereo field at all.)
528 SampleType inL = L[i] + sL.pingPongFb;
529 SampleType inR = R[i] + sR.pingPongFb;
530
531 SampleType outL = processSampleInternal(0, inL, currentDelay, sL, SampleType(0), lpC, hpC);
532 SampleType outR = processSampleInternal(1, inR, currentDelay, sR, SampleType(0), lpC, hpC);
533
534 sR.pingPongFb = saturateFeedback(processFbFilters(1, outL * fb, lpC, hpC), analogFb); // L crosses to R
535 sL.pingPongFb = saturateFeedback(processFbFilters(0, outR * fb, lpC, hpC), analogFb); // R crosses to L
536
537 L[i] = outL;
538 R[i] = outR;
539 advanceWriteIndexUnchecked(0);
540 advanceWriteIndexUnchecked(1);
541 }
542 }
543
550 void mixWetToDry(AudioBufferView<SampleType> dry, SampleType mix) noexcept
551 {
552 // min/max with this argument order also resolves NaN to the dry bound.
553 mix = std::min(SampleType(1), std::max(SampleType(0), mix));
554 auto st = smootherType_.load(std::memory_order_relaxed);
555 if (st != SmootherType::None)
556 mixSmoother_.setTargetValue(static_cast<float>(mix));
557
558 const int nS = std::min(dry.getNumSamples(), wetBuffer_.getNumSamples());
559 const int nCh = std::min(dry.getNumChannels(), wetBuffer_.getNumChannels());
560
561 // Sample-outer loop: ONE smoother value per sample, shared by every
562 // channel. Advancing the smoother inside a channel-outer loop ran the
563 // ramp numChannels times too fast and, worse, gave each channel a
564 // different segment of it (channel 1 got the ramp a whole block ahead
565 // of channel 0), audibly tilting the stereo image on every mix change.
566 std::array<SampleType*, kMaxChannels> d {};
567 std::array<const SampleType*, kMaxChannels> w {};
568 for (int ch = 0; ch < nCh; ++ch)
569 {
570 d[ch] = dry.getChannel(ch);
571 w[ch] = wetBuffer_.getChannel(ch);
572 }
573 for (int i = 0; i < nS; ++i)
574 {
575 const SampleType m = (st != SmootherType::None)
576 ? static_cast<SampleType>(mixSmoother_.getNextValue()) : mix;
577 for (int ch = 0; ch < nCh; ++ch)
578 d[ch][i] = d[ch][i] * (SampleType(1) - m) + w[ch][i] * m;
579 }
580 }
581
583 AudioBufferView<SampleType> getWetView() noexcept { return wetBuffer_.toView(); }
584
586 [[nodiscard]] int getMaxDelaySamples() const noexcept { return maxDelaySamples_; }
587
588
590 [[nodiscard]] std::vector<uint8_t> getState() const
591 {
592 StateWriter w(stateId("DLAY"), 1);
593 w.write("delaySamples", globalDelay_.load(std::memory_order_relaxed));
594 w.write("feedback", feedbackGain_.load(std::memory_order_relaxed));
595 w.write("fbLpHz", fbLpHzShadow_.load(std::memory_order_relaxed));
596 w.write("fbHpHz", fbHpHzShadow_.load(std::memory_order_relaxed));
597 w.write("smoother", static_cast<int32_t>(smootherType_.load(std::memory_order_relaxed)));
598 w.write("smoothingMs", smoothingTimeMs_.load(std::memory_order_relaxed));
599 w.write("fbMode", static_cast<int32_t>(feedbackMode_.load(std::memory_order_relaxed)));
600 w.write("mix", static_cast<float>(mix_.load(std::memory_order_relaxed)));
601 w.write("pingPong", static_cast<int32_t>(pingPong_.load(std::memory_order_relaxed) ? 1 : 0));
602 return w.blob();
603 }
604
606 bool setState(const uint8_t* data, size_t size)
607 {
608 StateReader r(data, size);
609 if (!r.isValid() || r.processorId() != stateId("DLAY")) return false;
610 setDelaySamples(static_cast<SampleType>(r.read("delaySamples", 0.0f)));
611 setFeedback(static_cast<SampleType>(r.read("feedback", 0.0f)));
612 setFeedbackLpHz(static_cast<SampleType>(r.read("fbLpHz", 0.0f)));
613 setFeedbackHpHz(static_cast<SampleType>(r.read("fbHpHz", 0.0f)));
614 // Enum fields clamped so a corrupt/foreign blob cannot install ids
615 // outside the ranges the switches expect.
616 setSmoother(static_cast<SmootherType>(std::clamp(r.read("smoother", 0), 0,
617 static_cast<int>(SmootherType::CriticallyDamped))));
618 setSmoothingTime(r.read("smoothingMs", 20.0f));
619 setFeedbackMode(static_cast<FeedbackMode>(std::clamp(r.read("fbMode", 1), 0,
620 static_cast<int>(FeedbackMode::Analog))));
621 setMix(static_cast<SampleType>(r.read("mix", 0.3f)));
622 setPingPong(r.read("pingPong", 0) != 0);
623 return true;
624 }
625
626protected:
627 static constexpr int kMaxChannels = 16;
628 static constexpr double kDefaultMaxDelaySeconds = 4.0;
629
647
648public:
654 void advanceWriteIndex(int ch) noexcept
655 {
656 if (ch < 0 || ch >= kMaxChannels) return;
657 advanceWriteIndexUnchecked(ch);
658 }
659
660private:
662 void advanceWriteIndexUnchecked(int ch) noexcept { writeIndices_[ch] = (writeIndices_[ch] + 1) & bufferMask_; }
663
667 [[nodiscard]] SampleType saturateFeedback(SampleType x, bool analog) const noexcept
668 {
669 return analog ? std::tanh(x) : std::clamp(x, SampleType(-2), SampleType(2));
670 }
671
672 template <typename ChannelFn>
673 void pushDryToWetImpl(ChannelFn&& getCh, int dryCh, int drySamples) noexcept
674 {
675 const int nCh = std::min(dryCh, wetBuffer_.getNumChannels());
676 const int nS = std::min(drySamples, wetBuffer_.getNumSamples());
677 for (int ch = 0; ch < nCh; ++ch)
678 std::memcpy(wetBuffer_.getChannel(ch), getCh(ch), static_cast<std::size_t>(nS) * sizeof(SampleType));
679 lastWetSamples_ = nS; // processWet()/processPingPong() run exactly this many
680 }
681
682 void resetChannelState(ChannelState& s) noexcept
683 {
684 s.fbLpZ1 = s.fbHpZ1 = s.lastFeedback = s.pingPongFb = 0;
685 s.currentDelay = s.targetDelay = 0;
686 }
687
688 void resetChannelSmoothers(ChannelState& s, float timeMs, float init) noexcept
689 {
690 double sr = static_cast<double>(sampleRate_);
691 s.lin.reset(sr, timeMs, init);
692 s.exp.reset(sr, timeMs, std::max(init, 1e-6f));
693 s.onePole.reset(sr, timeMs, init);
694 s.multi2.reset(sr, timeMs, init);
695 s.asym.reset(sr, timeMs / 5.0f, timeMs, init);
696 float timeSec = timeMs / 1000.0f;
697 float maxRate = static_cast<float>(maxDelaySamples_) / std::max(timeSec, 1e-6f);
698 s.slew.reset(sr, maxRate, init);
699 s.svf.reset(sr, timeMs, 0.707f, init);
700 s.butter.reset(sr, timeMs, init);
701 s.crit.reset(sr, timeMs, init);
702 }
703
704 void maybeUpdateSmoothers() noexcept
705 {
706 // Audio-thread application point for control-thread publications.
707 if (delayTargetDirty_.exchange(false, std::memory_order_acquire))
708 updateSmootherTargets(globalDelay_.load(std::memory_order_relaxed));
709
710 // exchange (not load+store: a publication between the two would be
711 // lost) with acquire, pairing with the setters' release stores so the
712 // freshly written type/time values are visible here.
713 if (!smootherDirty_.exchange(false, std::memory_order_acquire)) return;
714 float timeMs = smoothingTimeMs_.load(std::memory_order_relaxed);
715 for (int ch = 0; ch < numChannels_; ++ch)
716 {
717 auto& s = states_[ch];
718 float cur = static_cast<float>(s.currentDelay);
719 float tgt = static_cast<float>(s.targetDelay);
720 resetChannelSmoothers(s, timeMs, cur);
721 setSmootherTarget(s, static_cast<SampleType>(tgt));
722 }
723 mixSmoother_.reset(static_cast<double>(sampleRate_), timeMs, mixSmoother_.getCurrentValue());
724 }
725
726 void updateSmootherTargets(SampleType target) noexcept
727 {
728 if (smootherType_.load(std::memory_order_relaxed) == SmootherType::None) return;
729 for (int ch = 0; ch < numChannels_; ++ch)
730 setSmootherTarget(states_[ch], target);
731 }
732
733 void setSmootherTarget(ChannelState& s, SampleType target) noexcept
734 {
735 s.targetDelay = target;
736 float t = static_cast<float>(target);
737 switch (smootherType_.load(std::memory_order_relaxed))
738 {
739 case SmootherType::Linear: s.lin.setTargetValue(t); break;
740 case SmootherType::Exponential: s.exp.setTargetValue(t); break;
741 case SmootherType::OnePole: s.onePole.setTargetValue(t); break;
742 case SmootherType::MultiPole2: s.multi2.setTargetValue(t); break;
743 case SmootherType::Asymmetric: s.asym.setTargetValue(t); break;
744 case SmootherType::SlewLimiter: s.slew.setTargetValue(t); break;
745 case SmootherType::StateVariable: s.svf.setTargetValue(t); break;
746 case SmootherType::Butterworth: s.butter.setTargetValue(t); break;
747 case SmootherType::CriticallyDamped:s.crit.setTargetValue(t); break;
748 default: break;
749 }
750 }
751
752 inline SampleType advanceSmoother(ChannelState& s, SmootherType st) noexcept
753 {
754 float val;
755 switch (st)
756 {
757 case SmootherType::Linear: val = s.lin.getNextValue(); break;
758 case SmootherType::Exponential: val = s.exp.getNextValue(); break;
759 case SmootherType::OnePole: val = s.onePole.getNextValue(); break;
760 case SmootherType::MultiPole2: val = s.multi2.getNextValue(); break;
761 case SmootherType::Asymmetric: val = s.asym.getNextValue(); break;
762 case SmootherType::SlewLimiter: val = s.slew.getNextValue(); break;
763 case SmootherType::StateVariable: val = s.svf.getNextValue(); break;
764 case SmootherType::Butterworth: val = s.butter.getNextValue(); break;
765 case SmootherType::CriticallyDamped:val = s.crit.getNextValue(); break;
766 default: val = static_cast<float>(s.targetDelay);
767 }
768 s.currentDelay = static_cast<SampleType>(val);
769 return s.currentDelay;
770 }
771
772 inline SampleType processSampleInternal(int ch, SampleType input, SampleType delaySamples,
773 ChannelState& s, SampleType fbMult,
774 SampleType lpC, SampleType hpC) noexcept
775 {
776 // Front-door non-finite guard: a NaN/Inf input, once written to the
777 // ring and recirculated through the recursive feedback filters
778 // (fbLpZ1/fbHpZ1) and the tanh saturator, poisons the delay line
779 // permanently. Replace it with 0 before it enters any state. No-op on
780 // finite input, so conformance metrics stay byte-identical. Shared by
781 // every entry point (processBlock/Channel/Wet/PingPong/processSample).
782 if (!std::isfinite(input)) input = SampleType(0);
783
784 // 4-point Hermite needs read positions at idx0-1, idx0, idx0+1, idx0+2.
785 // The write index points to the slot we are about to write THIS sample.
786 // Slots writeIdx, writeIdx+1, writeIdx+2 still hold stale data from
787 // the previous ring-buffer wrap. To keep all 4 interpolation taps
788 // inside the already-written history we need delaySamples >= 3.
789 // Below that, samples idx1 / idx2 leak data ~bufferSize samples old
790 // which manifests as audible clicks (notably when a binaural / Haas
791 // panner crosses the centre). 3 samples ~ 62 us at 48 kHz -
792 // imperceptible and the same on both channels, so spatial cues
793 // (centre image, ITD ratios) are preserved.
794 constexpr SampleType kMinHermiteDelay = SampleType(3);
795 delaySamples = std::clamp(delaySamples, kMinHermiteDelay,
796 static_cast<SampleType>(maxDelaySamples_ - 1));
797
798 SampleType* data = delayBuffer_.getChannel(ch);
799 int writeIdx = writeIndices_[ch];
800
801 // Read position writeIdx - delaySamples, split into an integer index
802 // and a fraction in [0, 1] without ever forming the wrapped float
803 // position: writeIdx - delay + size rounded up to exactly `size` when
804 // the position fell a hair below 0, which masked the index to 0 while
805 // the "fraction" became `size` (a Hermite blow-up, heard as random
806 // zips on moving Haas/binaural delays).
807 const int dInt = static_cast<int>(delaySamples); // >= 3
808 const SampleType dFrac = delaySamples - static_cast<SampleType>(dInt);
809 int idx0;
810 SampleType frac;
811 if (dFrac > SampleType(0))
812 {
813 idx0 = (writeIdx - dInt - 1) & bufferMask_;
814 frac = SampleType(1) - dFrac;
815 }
816 else
817 {
818 idx0 = (writeIdx - dInt) & bufferMask_;
819 frac = SampleType(0);
820 }
821
822 // 3rd-order Hermite Interpolation (4-point)
823
824 int idxM1 = (idx0 - 1 + maxDelaySamples_) & bufferMask_;
825 int idx1 = (idx0 + 1) & bufferMask_;
826 int idx2 = (idx0 + 2) & bufferMask_;
827
828 SampleType ym1 = data[idxM1];
829 SampleType y0 = data[idx0];
830 SampleType y1 = data[idx1];
831 SampleType y2 = data[idx2];
832
833 // Hermite polynomial logic
834 SampleType c = (y1 - ym1) * SampleType(0.5);
835 SampleType v = y0 - y1;
836 SampleType w = c + v;
837 SampleType a = w + v + (y2 - y0) * SampleType(0.5);
838 SampleType b = w + a;
839 SampleType delayed = ((((a * frac) - b) * frac + c) * frac + y0);
840
841 // Feedback processing
842 SampleType fbInput = delayed * fbMult;
843 if (fbInput != SampleType(0))
844 {
845 fbInput = processFbFilters(ch, fbInput, lpC, hpC);
846 fbInput = saturateFeedback(fbInput,
847 feedbackMode_.load(std::memory_order_relaxed) == FeedbackMode::Analog);
848 }
849 s.lastFeedback = fbInput;
850
851 data[writeIdx] = input + s.lastFeedback;
852 return delayed;
853 }
854
855 inline SampleType processFbFilters(int ch, SampleType sample, SampleType lpC, SampleType hpC) noexcept
856 {
857 auto& s = states_[ch];
858 SampleType out = sample;
859
860 // Apply denormal prevention (1e-18f bias)
861 out += SampleType(1e-18);
862
863 if (hpC > SampleType(0))
864 {
865 s.fbHpZ1 = out * (SampleType(1) - hpC) + s.fbHpZ1 * hpC;
866 out -= s.fbHpZ1;
867 }
868 if (lpC > SampleType(0))
869 {
870 s.fbLpZ1 = out * (SampleType(1) - lpC) + s.fbLpZ1 * lpC;
871 out = s.fbLpZ1;
872 }
873
874 out -= SampleType(1e-18);
875 return out;
876 }
877
879 SampleType calcOnePoleCoef(SampleType freq) const noexcept { return std::exp(-twoPi<SampleType> * freq / sampleRate_); }
880 SampleType calcLpCoef(SampleType freq) const noexcept { return calcOnePoleCoef(freq); }
881 SampleType calcHpCoef(SampleType freq) const noexcept { return calcOnePoleCoef(freq); }
882
883 static int nextPow2(int v) noexcept
884 {
885 int r = 1;
886 while (r < v) r <<= 1;
887 return r;
888 }
889
890 // -- Members -------------------------------------------------------------
891 std::atomic<bool> prepared_{ false };
892 AudioBuffer<SampleType> delayBuffer_, wetBuffer_;
893 std::array<ChannelState, kMaxChannels> states_ {};
894 std::array<int, kMaxChannels> writeIndices_ {};
895
896 int maxDelaySamples_ = 0, bufferMask_ = 0;
897 int numChannels_ = 0, blockSize_ = 0;
898 int lastWetSamples_ = 0;
899 SampleType sampleRate_ = SampleType(48000);
900
901 std::atomic<SampleType> globalDelay_ { SampleType(0) };
902 std::atomic<SampleType> feedbackGain_ { SampleType(0) };
903 std::atomic<SampleType> fbLpCoef_ { SampleType(0) };
904 std::atomic<SampleType> fbLpHzShadow_ { SampleType(0) };
905 std::atomic<SampleType> fbHpHzShadow_ { SampleType(0) };
906 std::atomic<SampleType> fbHpCoef_ { SampleType(0) };
907
908 std::atomic<SmootherType> smootherType_ { SmootherType::Exponential };
909 std::atomic<float> smoothingTimeMs_ { 20.0f };
910 std::atomic<bool> smootherDirty_ { false };
911 std::atomic<bool> delayTargetDirty_ { false };
912 std::atomic<FeedbackMode> feedbackMode_ { FeedbackMode::Analog };
913
914 Smoothers::LinearSmoother mixSmoother_;
915 std::atomic<SampleType> mix_ { SampleType(0.3) };
916 std::atomic<bool> pingPong_ { false };
917 SampleType insertMix_ = SampleType(0.3);
918 SampleType insertMixMaxStep_ = SampleType(1.0 / 960.0);
919};
920
921} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
int getNumSamples() const noexcept
Returns the number of samples per channel.
int getNumChannels() const noexcept
Returns the number of channels in this view.
T * getChannel(int ch) const noexcept
Returns a pointer to the sample data for the given channel.
AudioBufferView< T, MaxChannels > toView() noexcept
Returns a non-owning mutable view of this buffer. The view's channel capacity is propagated from MaxC...
int getNumChannels() const noexcept
Returns the number of active channels.
T * getChannel(int ch) noexcept
Returns a pointer to the sample data.
int getNumSamples() const noexcept
Returns the number of samples per channel.
void resize(int numChannels, int numSamples)
Allocates the buffer for the given dimensions.
void clear() noexcept
Clears all channels, including SIMD alignment padding. Ensures any SIMD over-read will consume pure z...
void setSmoothingTime(float ms) noexcept
Sets the smoothing transition time.
Definition Delay.h:184
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
Definition Delay.h:606
SampleType processSample(int ch, SampleType input) noexcept
Processes a single sample for a specific channel.
Definition Delay.h:259
AudioBufferView< SampleType > getWetView() noexcept
Exposes the internal wet buffer view.
Definition Delay.h:583
void setFeedback(SampleType gain) noexcept
Sets the global feedback amount.
Definition Delay.h:227
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Definition Delay.h:590
void mixWetToDry(AudioBufferView< SampleType > dry, SampleType mix) noexcept
Mixes the processed wet buffer back into the provided dry buffer.
Definition Delay.h:550
void pushDryToWet(AudioBufferView< SampleType > dry) noexcept
Copies a dry buffer into the internal wet buffer.
Definition Delay.h:471
int getMaxDelaySamples() const noexcept
Returns maximum capacity in samples.
Definition Delay.h:586
static constexpr double kDefaultMaxDelaySeconds
Definition Delay.h:628
SampleType getCurrentDelaySamples() const noexcept
Returns the current target delay time in samples.
Definition Delay.h:219
void setFeedbackHpHz(SampleType freq) noexcept
Sets the cutoff frequency for the feedback high-pass filter (0 to disable). Non-finite values are ign...
Definition Delay.h:244
void setMix(SampleType mix) noexcept
Sets the dry/wet blend of the insert-style processBlock(buffer).
Definition Delay.h:356
void prepareMs(const AudioSpec &spec, double maxDelayMs)
Prepares the delay with a maximum capacity in milliseconds.
Definition Delay.h:147
void prepare(const AudioSpec &spec, double maxDelaySeconds)
Prepares the delay structures and allocates memory. Must be called before processing.
Definition Delay.h:105
void processChannel(AudioBufferView< SampleType > buffer, int ch, SampleType delayMs, SampleType feedback=0, SampleType lpHz=0, SampleType hpHz=0) noexcept
Processes a single channel block in-place. Safe to call sequentially for different channels.
Definition Delay.h:432
FeedbackMode getFeedbackMode() const noexcept
Returns the active feedback mode.
Definition Delay.h:81
void processPingPong(SampleType delayMs, SampleType feedback=0, SampleType lpHz=0, SampleType hpHz=0) noexcept
Processes a true ping-pong delay (L feeds R, R feeds L) on the samples of the most recent pushDryToWe...
Definition Delay.h:494
void pushDryToWet(AudioBufferView< const SampleType > dry) noexcept
Copies a dry buffer into the internal wet buffer.
Definition Delay.h:465
void prepare(const AudioSpec &spec)
Prepares for insert use with the default capacity (4 seconds).
Definition Delay.h:92
void setSmoother(SmootherType type) noexcept
Sets the smoothing algorithm used for delay time changes.
Definition Delay.h:174
void setDelaySamples(SampleType samples) noexcept
Sets the delay time in samples.
Definition Delay.h:199
void setFeedbackLpHz(SampleType freq) noexcept
Sets the cutoff frequency for the feedback low-pass filter (0 to disable). Non-finite values are igno...
Definition Delay.h:235
void setFeedbackMode(FeedbackMode mode) noexcept
Selects clean or analog feedback regeneration. Thread-safe.
Definition Delay.h:75
FeedbackMode
Feedback path colour.
Definition Delay.h:65
void setDelayMs(SampleType ms) noexcept
Sets the delay time in milliseconds.
Definition Delay.h:213
void setPingPong(bool on) noexcept
Ping-pong for the insert-style processBlock(buffer): the first two channels feed their echoes to each...
Definition Delay.h:373
SampleType getMix() const noexcept
Returns the insert dry/wet blend.
Definition Delay.h:363
void processBlock(AudioBufferView< SampleType > buffer) noexcept
Insert-style processing: the dry/wet blend, in place, with the stored parameters (setDelayMs(),...
Definition Delay.h:288
static constexpr int kMaxChannels
Definition Delay.h:627
void processBlock(AudioBufferView< SampleType > buffer, SampleType delayMs, SampleType feedback=0, SampleType lpHz=0, SampleType hpHz=0) noexcept
Processes a multi-channel block in-place.
Definition Delay.h:394
void advanceWriteIndex(int ch) noexcept
Advances the write index for a specific channel.
Definition Delay.h:654
bool getPingPong() const noexcept
True when the insert path runs as a ping-pong delay.
Definition Delay.h:376
void processWet(SampleType delayMs, SampleType feedback=0, SampleType lpHz=0, SampleType hpHz=0) noexcept
Processes the wet buffer in place with current settings. Processes exactly the samples of the most re...
Definition Delay.h:481
void setDelaySeconds(SampleType secs) noexcept
Sets the delay time in seconds.
Definition Delay.h:216
int getLatency() const noexcept
Latency in samples: none (the delay is the effect, not a latency).
Definition Delay.h:379
void reset() noexcept
Clears the delay buffers and resets all filter and feedback states.
Definition Delay.h:155
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
Main namespace for the DSPark framework.
T moveTowards(T from, T to, T maxDelta) noexcept
Moves a value toward a target by at most a given distance.
Definition DspMath.h:134
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:651
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:37
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:71
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:58
int maxBlockSize
Maximum number of samples per processing block.
Definition AudioSpec.h:53
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45
Smoothers::ExponentialSmoother exp
Definition Delay.h:638
Smoothers::OnePoleSmoother onePole
Definition Delay.h:639
Smoothers::StateVariableSmoother svf
Definition Delay.h:643
Smoothers::ButterworthSmoother butter
Definition Delay.h:644
Smoothers::LinearSmoother lin
Definition Delay.h:637
Smoothers::CriticallyDampedSmoother crit
Definition Delay.h:645
Smoothers::SlewLimiter slew
Definition Delay.h:642
Smoothers::MultiPoleSmoother< 2 > multi2
Definition Delay.h:640
Smoothers::AsymmetricSmoother asym
Definition Delay.h:641
Smoother with asymmetric attack/release times.
Definition Smoothers.h:189
Butterworth low-pass smoother for maximally flat response.
Definition Smoothers.h:286
Critically damped smoother (no overshoot, exact Q=0.5).
Definition Smoothers.h:314
Exponential (multiplicative) smoother for natural, perceptual responses.
Definition Smoothers.h:101
Linear ramp smoother for predictable, uniform interpolation.
Definition Smoothers.h:56
void reset(double sampleRate, float rampTimeMilliseconds, float initialValue=0.0f) noexcept
Definition Smoothers.h:327
void setTargetValue(float newTarget) noexcept
Definition Smoothers.h:336
float getCurrentValue() const noexcept
Definition Smoothers.h:64
Templated cascaded multi-pole smoother for steeper roll-off.
Definition Smoothers.h:162
Authentic one-pole exponential IIR low-pass smoother.
Definition Smoothers.h:137
Rate limiter to cap maximum change per sample.
Definition Smoothers.h:216
Second-order state variable filter (SVF) smoother (TPT implementation).
Definition Smoothers.h:245