DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
Delay.h
Go to the documentation of this file.
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
29#include "../Core/AudioBuffer.h"
30#include "../Core/AudioSpec.h"
31#include "../Core/DspMath.h"
32#include "../Core/Smoothers.h"
33#include "../Core/StateBlob.h"
34
35#include <algorithm>
36#include <array>
37#include <atomic>
38#include <cmath>
39#include <cstdint>
40#include <cstring>
41#include <vector>
42
43namespace dspark {
44
45template <typename SampleType>
46class Delay final
47{
48public:
54
56 enum class FeedbackMode
57 {
58 Clean,
61 Analog
64 };
65
67 void setFeedbackMode(FeedbackMode mode) noexcept
68 {
69 feedbackMode_.store(mode, std::memory_order_relaxed);
70 }
71
73 [[nodiscard]] FeedbackMode getFeedbackMode() const noexcept
74 {
75 return feedbackMode_.load(std::memory_order_relaxed);
76 }
77
78 // -- Lifecycle -----------------------------------------------------------
79
88 void prepare(const AudioSpec& spec, double maxDelaySeconds)
89 {
90 if (!spec.isValid() || !(maxDelaySeconds >= 0.0)) return;
91 sampleRate_ = static_cast<SampleType>(spec.sampleRate);
92 // Clamp to the fixed per-channel state arrays (states_/writeIndices_ are
93 // kMaxChannels): maybeUpdateSmoothers()/updateSmootherTargets() iterate
94 // [0, numChannels_) and would otherwise read/write out of bounds.
95 numChannels_ = std::min(static_cast<int>(spec.numChannels), kMaxChannels);
96 blockSize_ = spec.maxBlockSize;
97
98 // Cap in double BEFORE the int cast: a huge request would otherwise be
99 // undefined behaviour in the cast, and nextPow2 past 2^29 overflows
100 // its shift. 2^29 samples is over 3 hours at 48 kHz.
101 const double capacityD =
102 std::min(std::ceil(maxDelaySeconds * spec.sampleRate) + blockSize_,
103 536870912.0); // 2^29
104 maxDelaySamples_ = nextPow2(static_cast<int>(capacityD));
105 bufferMask_ = maxDelaySamples_ - 1;
106
107 delayBuffer_.resize(numChannels_, maxDelaySamples_);
108 wetBuffer_.resize(numChannels_, blockSize_);
109 lastWetSamples_ = blockSize_; // full capacity until the first push
110
111 float timeMs = smoothingTimeMs_.load(std::memory_order_relaxed);
112 for (int ch = 0; ch < std::min(numChannels_, kMaxChannels); ++ch)
113 {
114 resetChannelSmoothers(states_[ch], timeMs, 0.0f);
115 writeIndices_[ch] = 0;
116 }
117
118 mixSmoother_.reset(spec.sampleRate, timeMs, 1.0f);
119 reset();
120
121 prepared_.store(true, std::memory_order_release);
122 }
123
129 void prepareMs(const AudioSpec& spec, double maxDelayMs)
130 {
131 prepare(spec, maxDelayMs / 1000.0);
132 }
133
137 void reset() noexcept
138 {
139 delayBuffer_.clear();
140 wetBuffer_.clear();
141 for (int ch = 0; ch < kMaxChannels; ++ch)
142 {
143 writeIndices_[ch] = 0;
144 resetChannelState(states_[ch]);
145 }
146 mixSmoother_.skip();
147 }
148
149 // -- Configuration -------------------------------------------------------
150
155 void setSmoother(SmootherType type) noexcept
156 {
157 smootherType_.store(type, std::memory_order_relaxed);
158 smootherDirty_.store(true, std::memory_order_release);
159 }
160
165 void setSmoothingTime(float ms) noexcept
166 {
167 if (!std::isfinite(ms)) return;
168 smoothingTimeMs_.store(std::max(0.0f, ms), std::memory_order_relaxed);
169 smootherDirty_.store(true, std::memory_order_release);
170 }
171
172 // -- Parameters ----------------------------------------------------------
173
180 void setDelaySamples(SampleType samples) noexcept
181 {
182 if (!std::isfinite(samples)) return;
183 samples = std::clamp(samples, SampleType(0),
184 static_cast<SampleType>(std::max(0, maxDelaySamples_ - 1)));
185 globalDelay_.store(samples, std::memory_order_relaxed);
186 // Publish/apply pattern: the smoothers are NOT touched here (they are
187 // non-atomic audio-thread state - mutating them from a control thread
188 // raced against advanceSmoother). The audio thread applies the new
189 // target in maybeUpdateSmoothers() at the top of its next process call.
190 delayTargetDirty_.store(true, std::memory_order_release);
191 }
192
194 void setDelayMs(SampleType ms) noexcept { setDelaySamples(ms * sampleRate_ / SampleType(1000)); }
195
197 void setDelaySeconds(SampleType secs) noexcept { setDelaySamples(secs * sampleRate_); }
198
200 [[nodiscard]] SampleType getCurrentDelaySamples() const noexcept { return globalDelay_.load(std::memory_order_relaxed); }
201
208 void setFeedback(SampleType gain) noexcept
209 {
210 if (!std::isfinite(gain)) return;
211 feedbackGain_.store(gain, std::memory_order_relaxed);
212 }
213
216 void setFeedbackLpHz(SampleType freq) noexcept
217 {
218 if (!std::isfinite(freq)) return;
219 fbLpCoef_.store((freq > 0) ? calcLpCoef(freq) : SampleType(0), std::memory_order_relaxed);
220 fbLpHzShadow_.store(freq, std::memory_order_relaxed); // serialization readback
221 }
222
225 void setFeedbackHpHz(SampleType freq) noexcept
226 {
227 if (!std::isfinite(freq)) return;
228 fbHpCoef_.store((freq > 0) ? calcHpCoef(freq) : SampleType(0), std::memory_order_relaxed);
229 fbHpHzShadow_.store(freq, std::memory_order_relaxed); // serialization readback
230 }
231
232 // -- Sample processing ---------------------------------------------------
233
240 SampleType processSample(int ch, SampleType input) noexcept
241 {
242 if (ch < 0 || ch >= numChannels_ || !prepared_.load(std::memory_order_acquire)) return input;
243 maybeUpdateSmoothers();
244
245 auto& s = states_[ch];
246 auto st = smootherType_.load(std::memory_order_relaxed);
247 SampleType delay = (st == SmootherType::None)
248 ? globalDelay_.load(std::memory_order_relaxed) : advanceSmoother(s, st);
249
250 SampleType out = processSampleInternal(ch, input, delay, s,
251 feedbackGain_.load(std::memory_order_relaxed),
252 fbLpCoef_.load(std::memory_order_relaxed),
253 fbHpCoef_.load(std::memory_order_relaxed));
254 advanceWriteIndexUnchecked(ch);
255 return out;
256 }
257
258 // -- Block processing ----------------------------------------------------
259
273 void processBlock(AudioBufferView<SampleType> buffer, SampleType delayMs,
274 SampleType feedback = 0, SampleType lpHz = 0, SampleType hpHz = 0) noexcept
275 {
276 if (!prepared_.load(std::memory_order_acquire)) return;
277
278 setDelayMs(delayMs);
279 setFeedback(feedback);
280 setFeedbackLpHz(lpHz);
281 setFeedbackHpHz(hpHz);
282 maybeUpdateSmoothers();
283
284 const int nS = buffer.getNumSamples();
285 const int nCh = std::min(buffer.getNumChannels(), numChannels_);
286
287 // Cache atomic parameters to local stack for auto-vectorization & performance
288 const auto smoothType = smootherType_.load(std::memory_order_relaxed);
289 const SampleType targetDelay = globalDelay_.load(std::memory_order_relaxed);
290 const SampleType fb = feedbackGain_.load(std::memory_order_relaxed);
291 const SampleType lpC = fbLpCoef_.load(std::memory_order_relaxed);
292 const SampleType hpC = fbHpCoef_.load(std::memory_order_relaxed);
293
294 for (int ch = 0; ch < nCh; ++ch)
295 {
296 SampleType* data = buffer.getChannel(ch);
297 auto& s = states_[ch];
298
299 for (int i = 0; i < nS; ++i)
300 {
301 SampleType currentDelay = (smoothType == SmootherType::None) ? targetDelay : advanceSmoother(s, smoothType);
302 data[i] = processSampleInternal(ch, data[i], currentDelay, s, fb, lpC, hpC);
303 advanceWriteIndexUnchecked(ch);
304 }
305 }
306 }
307
311 void processChannel(AudioBufferView<SampleType> buffer, int ch, SampleType delayMs,
312 SampleType feedback = 0, SampleType lpHz = 0, SampleType hpHz = 0) noexcept
313 {
314 if (ch < 0 || ch >= numChannels_ || !prepared_.load(std::memory_order_acquire)) return;
315
316 setDelayMs(delayMs);
317 setFeedback(feedback);
318 setFeedbackLpHz(lpHz);
319 setFeedbackHpHz(hpHz);
320 maybeUpdateSmoothers();
321
322 SampleType* data = buffer.getChannel(ch);
323 const int nS = buffer.getNumSamples();
324
325 const auto smoothType = smootherType_.load(std::memory_order_relaxed);
326 const SampleType targetDelay = globalDelay_.load(std::memory_order_relaxed);
327 const SampleType fb = feedbackGain_.load(std::memory_order_relaxed);
328 const SampleType lpC = fbLpCoef_.load(std::memory_order_relaxed);
329 const SampleType hpC = fbHpCoef_.load(std::memory_order_relaxed);
330
331 auto& s = states_[ch];
332
333 for (int i = 0; i < nS; ++i)
334 {
335 SampleType currentDelay = (smoothType == SmootherType::None) ? targetDelay : advanceSmoother(s, smoothType);
336 data[i] = processSampleInternal(ch, data[i], currentDelay, s, fb, lpC, hpC);
337 advanceWriteIndexUnchecked(ch);
338 }
339 }
340
341 // -- Wet buffer processing -----------------------------------------------
342
345 {
346 pushDryToWetImpl([&](int ch) { return dry.getChannel(ch); }, dry.getNumChannels(), dry.getNumSamples());
347 }
348
351 {
352 pushDryToWetImpl([&](int ch) { return dry.getChannel(ch); }, dry.getNumChannels(), dry.getNumSamples());
353 }
354
360 void processWet(SampleType delayMs, SampleType feedback = 0, SampleType lpHz = 0, SampleType hpHz = 0) noexcept
361 {
362 processBlock(wetBuffer_.toView().getSubView(0, lastWetSamples_), delayMs, feedback, lpHz, hpHz);
363 }
364
373 void processPingPong(SampleType delayMs, SampleType feedback = 0, SampleType lpHz = 0, SampleType hpHz = 0) noexcept
374 {
375 if (wetBuffer_.getNumChannels() < 2 || !prepared_.load(std::memory_order_acquire)) return;
376
377 setDelayMs(delayMs);
378 setFeedback(feedback);
379 setFeedbackLpHz(lpHz);
380 setFeedbackHpHz(hpHz);
381 maybeUpdateSmoothers();
382
383 SampleType* L = wetBuffer_.getChannel(0);
384 SampleType* R = wetBuffer_.getChannel(1);
385 const int nS = std::min(lastWetSamples_, wetBuffer_.getNumSamples());
386
387 const auto smoothType = smootherType_.load(std::memory_order_relaxed);
388 const SampleType targetDelay = globalDelay_.load(std::memory_order_relaxed);
389 const SampleType fb = feedbackGain_.load(std::memory_order_relaxed);
390 const SampleType lpC = fbLpCoef_.load(std::memory_order_relaxed);
391 const SampleType hpC = fbHpCoef_.load(std::memory_order_relaxed);
392 const bool analogFb = feedbackMode_.load(std::memory_order_relaxed) == FeedbackMode::Analog;
393
394 for (int i = 0; i < nS; ++i)
395 {
396 auto& sL = states_[0];
397 auto& sR = states_[1];
398
399 SampleType currentDelay = (smoothType == SmootherType::None) ? targetDelay : advanceSmoother(sL, smoothType);
400 advanceSmoother(sR, smoothType); // Keep smoothers in sync
401
402 // s.pingPongFb holds what THIS channel receives next sample, i.e.
403 // the OTHER channel's previous output. (The original wiring
404 // consumed each channel's own stored value while storing f(other)
405 // into it: every echo regenerated into its own channel and the
406 // signal never crossed the stereo field at all.)
407 SampleType inL = L[i] + sL.pingPongFb;
408 SampleType inR = R[i] + sR.pingPongFb;
409
410 SampleType outL = processSampleInternal(0, inL, currentDelay, sL, SampleType(0), lpC, hpC);
411 SampleType outR = processSampleInternal(1, inR, currentDelay, sR, SampleType(0), lpC, hpC);
412
413 sR.pingPongFb = saturateFeedback(processFbFilters(1, outL * fb, lpC, hpC), analogFb); // L crosses to R
414 sL.pingPongFb = saturateFeedback(processFbFilters(0, outR * fb, lpC, hpC), analogFb); // R crosses to L
415
416 L[i] = outL;
417 R[i] = outR;
418 advanceWriteIndexUnchecked(0);
419 advanceWriteIndexUnchecked(1);
420 }
421 }
422
429 void mixWetToDry(AudioBufferView<SampleType> dry, SampleType mix) noexcept
430 {
431 // min/max with this argument order also resolves NaN to the dry bound.
432 mix = std::min(SampleType(1), std::max(SampleType(0), mix));
433 auto st = smootherType_.load(std::memory_order_relaxed);
434 if (st != SmootherType::None)
435 mixSmoother_.setTargetValue(static_cast<float>(mix));
436
437 const int nS = std::min(dry.getNumSamples(), wetBuffer_.getNumSamples());
438 const int nCh = std::min(dry.getNumChannels(), wetBuffer_.getNumChannels());
439
440 // Sample-outer loop: ONE smoother value per sample, shared by every
441 // channel. Advancing the smoother inside a channel-outer loop ran the
442 // ramp numChannels times too fast and, worse, gave each channel a
443 // different segment of it (channel 1 got the ramp a whole block ahead
444 // of channel 0), audibly tilting the stereo image on every mix change.
445 std::array<SampleType*, kMaxChannels> d {};
446 std::array<const SampleType*, kMaxChannels> w {};
447 for (int ch = 0; ch < nCh; ++ch)
448 {
449 d[ch] = dry.getChannel(ch);
450 w[ch] = wetBuffer_.getChannel(ch);
451 }
452 for (int i = 0; i < nS; ++i)
453 {
454 const SampleType m = (st != SmootherType::None)
455 ? static_cast<SampleType>(mixSmoother_.getNextValue()) : mix;
456 for (int ch = 0; ch < nCh; ++ch)
457 d[ch][i] = d[ch][i] * (SampleType(1) - m) + w[ch][i] * m;
458 }
459 }
460
462 AudioBufferView<SampleType> getWetView() noexcept { return wetBuffer_.toView(); }
463
465 [[nodiscard]] int getMaxDelaySamples() const noexcept { return maxDelaySamples_; }
466
467
469 [[nodiscard]] std::vector<uint8_t> getState() const
470 {
471 StateWriter w(stateId("DLAY"), 1);
472 w.write("delaySamples", globalDelay_.load(std::memory_order_relaxed));
473 w.write("feedback", feedbackGain_.load(std::memory_order_relaxed));
474 w.write("fbLpHz", fbLpHzShadow_.load(std::memory_order_relaxed));
475 w.write("fbHpHz", fbHpHzShadow_.load(std::memory_order_relaxed));
476 w.write("smoother", static_cast<int32_t>(smootherType_.load(std::memory_order_relaxed)));
477 w.write("smoothingMs", smoothingTimeMs_.load(std::memory_order_relaxed));
478 w.write("fbMode", static_cast<int32_t>(feedbackMode_.load(std::memory_order_relaxed)));
479 return w.blob();
480 }
481
483 bool setState(const uint8_t* data, size_t size)
484 {
485 StateReader r(data, size);
486 if (!r.isValid() || r.processorId() != stateId("DLAY")) return false;
487 setDelaySamples(static_cast<SampleType>(r.read("delaySamples", 0.0f)));
488 setFeedback(static_cast<SampleType>(r.read("feedback", 0.0f)));
489 setFeedbackLpHz(static_cast<SampleType>(r.read("fbLpHz", 0.0f)));
490 setFeedbackHpHz(static_cast<SampleType>(r.read("fbHpHz", 0.0f)));
491 // Enum fields clamped so a corrupt/foreign blob cannot install ids
492 // outside the ranges the switches expect.
493 setSmoother(static_cast<SmootherType>(std::clamp(r.read("smoother", 0), 0,
494 static_cast<int>(SmootherType::CriticallyDamped))));
495 setSmoothingTime(r.read("smoothingMs", 20.0f));
496 setFeedbackMode(static_cast<FeedbackMode>(std::clamp(r.read("fbMode", 1), 0,
497 static_cast<int>(FeedbackMode::Analog))));
498 return true;
499 }
500
501protected:
502 static constexpr int kMaxChannels = 16;
503
521
522public:
528 void advanceWriteIndex(int ch) noexcept
529 {
530 if (ch < 0 || ch >= kMaxChannels) return;
531 advanceWriteIndexUnchecked(ch);
532 }
533
534private:
536 void advanceWriteIndexUnchecked(int ch) noexcept { writeIndices_[ch] = (writeIndices_[ch] + 1) & bufferMask_; }
537
541 [[nodiscard]] SampleType saturateFeedback(SampleType x, bool analog) const noexcept
542 {
543 return analog ? std::tanh(x) : std::clamp(x, SampleType(-2), SampleType(2));
544 }
545
546 template <typename ChannelFn>
547 void pushDryToWetImpl(ChannelFn&& getCh, int dryCh, int drySamples) noexcept
548 {
549 const int nCh = std::min(dryCh, wetBuffer_.getNumChannels());
550 const int nS = std::min(drySamples, wetBuffer_.getNumSamples());
551 for (int ch = 0; ch < nCh; ++ch)
552 std::memcpy(wetBuffer_.getChannel(ch), getCh(ch), static_cast<std::size_t>(nS) * sizeof(SampleType));
553 lastWetSamples_ = nS; // processWet()/processPingPong() run exactly this many
554 }
555
556 void resetChannelState(ChannelState& s) noexcept
557 {
558 s.fbLpZ1 = s.fbHpZ1 = s.lastFeedback = s.pingPongFb = 0;
559 s.currentDelay = s.targetDelay = 0;
560 }
561
562 void resetChannelSmoothers(ChannelState& s, float timeMs, float init) noexcept
563 {
564 double sr = static_cast<double>(sampleRate_);
565 s.lin.reset(sr, timeMs, init);
566 s.exp.reset(sr, timeMs, std::max(init, 1e-6f));
567 s.onePole.reset(sr, timeMs, init);
568 s.multi2.reset(sr, timeMs, init);
569 s.asym.reset(sr, timeMs / 5.0f, timeMs, init);
570 float timeSec = timeMs / 1000.0f;
571 float maxRate = static_cast<float>(maxDelaySamples_) / std::max(timeSec, 1e-6f);
572 s.slew.reset(sr, maxRate, init);
573 s.svf.reset(sr, timeMs, 0.707f, init);
574 s.butter.reset(sr, timeMs, init);
575 s.crit.reset(sr, timeMs, init);
576 }
577
578 void maybeUpdateSmoothers() noexcept
579 {
580 // Audio-thread application point for control-thread publications.
581 if (delayTargetDirty_.exchange(false, std::memory_order_acquire))
582 updateSmootherTargets(globalDelay_.load(std::memory_order_relaxed));
583
584 // exchange (not load+store: a publication between the two would be
585 // lost) with acquire, pairing with the setters' release stores so the
586 // freshly written type/time values are visible here.
587 if (!smootherDirty_.exchange(false, std::memory_order_acquire)) return;
588 float timeMs = smoothingTimeMs_.load(std::memory_order_relaxed);
589 for (int ch = 0; ch < numChannels_; ++ch)
590 {
591 auto& s = states_[ch];
592 float cur = static_cast<float>(s.currentDelay);
593 float tgt = static_cast<float>(s.targetDelay);
594 resetChannelSmoothers(s, timeMs, cur);
595 setSmootherTarget(s, static_cast<SampleType>(tgt));
596 }
597 mixSmoother_.reset(static_cast<double>(sampleRate_), timeMs, mixSmoother_.getCurrentValue());
598 }
599
600 void updateSmootherTargets(SampleType target) noexcept
601 {
602 if (smootherType_.load(std::memory_order_relaxed) == SmootherType::None) return;
603 for (int ch = 0; ch < numChannels_; ++ch)
604 setSmootherTarget(states_[ch], target);
605 }
606
607 void setSmootherTarget(ChannelState& s, SampleType target) noexcept
608 {
609 s.targetDelay = target;
610 float t = static_cast<float>(target);
611 switch (smootherType_.load(std::memory_order_relaxed))
612 {
613 case SmootherType::Linear: s.lin.setTargetValue(t); break;
614 case SmootherType::Exponential: s.exp.setTargetValue(t); break;
615 case SmootherType::OnePole: s.onePole.setTargetValue(t); break;
616 case SmootherType::MultiPole2: s.multi2.setTargetValue(t); break;
617 case SmootherType::Asymmetric: s.asym.setTargetValue(t); break;
618 case SmootherType::SlewLimiter: s.slew.setTargetValue(t); break;
619 case SmootherType::StateVariable: s.svf.setTargetValue(t); break;
620 case SmootherType::Butterworth: s.butter.setTargetValue(t); break;
621 case SmootherType::CriticallyDamped:s.crit.setTargetValue(t); break;
622 default: break;
623 }
624 }
625
626 inline SampleType advanceSmoother(ChannelState& s, SmootherType st) noexcept
627 {
628 float val;
629 switch (st)
630 {
631 case SmootherType::Linear: val = s.lin.getNextValue(); break;
632 case SmootherType::Exponential: val = s.exp.getNextValue(); break;
633 case SmootherType::OnePole: val = s.onePole.getNextValue(); break;
634 case SmootherType::MultiPole2: val = s.multi2.getNextValue(); break;
635 case SmootherType::Asymmetric: val = s.asym.getNextValue(); break;
636 case SmootherType::SlewLimiter: val = s.slew.getNextValue(); break;
637 case SmootherType::StateVariable: val = s.svf.getNextValue(); break;
638 case SmootherType::Butterworth: val = s.butter.getNextValue(); break;
639 case SmootherType::CriticallyDamped:val = s.crit.getNextValue(); break;
640 default: val = static_cast<float>(s.targetDelay);
641 }
642 s.currentDelay = static_cast<SampleType>(val);
643 return s.currentDelay;
644 }
645
646 inline SampleType processSampleInternal(int ch, SampleType input, SampleType delaySamples,
647 ChannelState& s, SampleType fbMult,
648 SampleType lpC, SampleType hpC) noexcept
649 {
650 // Front-door non-finite guard (M-006 C1): a NaN/Inf input, once written
651 // to the ring and recirculated through the recursive feedback filters
652 // (fbLpZ1/fbHpZ1) and the tanh saturator, poisons the delay line
653 // permanently. Replace it with 0 before it enters any state. No-op on
654 // finite input, so conformance metrics stay byte-identical. Shared by
655 // every entry point (processBlock/Channel/Wet/PingPong/processSample).
656 if (!std::isfinite(input)) input = SampleType(0);
657
658 // 4-point Hermite needs read positions at idx0-1, idx0, idx0+1, idx0+2.
659 // The write index points to the slot we are about to write THIS sample.
660 // Slots writeIdx, writeIdx+1, writeIdx+2 still hold stale data from
661 // the previous ring-buffer wrap. To keep all 4 interpolation taps
662 // inside the already-written history we need delaySamples >= 3.
663 // Below that, samples idx1 / idx2 leak data ~bufferSize samples old
664 // which manifests as audible clicks (notably when a binaural / Haas
665 // panner crosses the centre). 3 samples ~ 62 us at 48 kHz -
666 // imperceptible and the same on both channels, so spatial cues
667 // (centre image, ITD ratios) are preserved.
668 constexpr SampleType kMinHermiteDelay = SampleType(3);
669 delaySamples = std::clamp(delaySamples, kMinHermiteDelay,
670 static_cast<SampleType>(maxDelaySamples_ - 1));
671
672 SampleType* data = delayBuffer_.getChannel(ch);
673 int writeIdx = writeIndices_[ch];
674
675 SampleType readPos = static_cast<SampleType>(writeIdx) - delaySamples;
676 if (readPos < SampleType(0)) readPos += static_cast<SampleType>(maxDelaySamples_);
677
678 // 3rd-order Hermite Interpolation (4-point)
679 int idx0 = static_cast<int>(readPos) & bufferMask_;
680 SampleType frac = readPos - static_cast<SampleType>(idx0);
681
682 int idxM1 = (idx0 - 1 + maxDelaySamples_) & bufferMask_;
683 int idx1 = (idx0 + 1) & bufferMask_;
684 int idx2 = (idx0 + 2) & bufferMask_;
685
686 SampleType ym1 = data[idxM1];
687 SampleType y0 = data[idx0];
688 SampleType y1 = data[idx1];
689 SampleType y2 = data[idx2];
690
691 // Hermite polynomial logic
692 SampleType c = (y1 - ym1) * SampleType(0.5);
693 SampleType v = y0 - y1;
694 SampleType w = c + v;
695 SampleType a = w + v + (y2 - y0) * SampleType(0.5);
696 SampleType b = w + a;
697 SampleType delayed = ((((a * frac) - b) * frac + c) * frac + y0);
698
699 // Feedback processing
700 SampleType fbInput = delayed * fbMult;
701 if (fbInput != SampleType(0))
702 {
703 fbInput = processFbFilters(ch, fbInput, lpC, hpC);
704 fbInput = saturateFeedback(fbInput,
705 feedbackMode_.load(std::memory_order_relaxed) == FeedbackMode::Analog);
706 }
707 s.lastFeedback = fbInput;
708
709 data[writeIdx] = input + s.lastFeedback;
710 return delayed;
711 }
712
713 inline SampleType processFbFilters(int ch, SampleType sample, SampleType lpC, SampleType hpC) noexcept
714 {
715 auto& s = states_[ch];
716 SampleType out = sample;
717
718 // Apply denormal prevention (1e-18f bias)
719 out += SampleType(1e-18);
720
721 if (hpC > SampleType(0))
722 {
723 s.fbHpZ1 = out * (SampleType(1) - hpC) + s.fbHpZ1 * hpC;
724 out -= s.fbHpZ1;
725 }
726 if (lpC > SampleType(0))
727 {
728 s.fbLpZ1 = out * (SampleType(1) - lpC) + s.fbLpZ1 * lpC;
729 out = s.fbLpZ1;
730 }
731
732 out -= SampleType(1e-18);
733 return out;
734 }
735
737 SampleType calcOnePoleCoef(SampleType freq) const noexcept { return std::exp(-twoPi<SampleType> * freq / sampleRate_); }
738 SampleType calcLpCoef(SampleType freq) const noexcept { return calcOnePoleCoef(freq); }
739 SampleType calcHpCoef(SampleType freq) const noexcept { return calcOnePoleCoef(freq); }
740
741 static int nextPow2(int v) noexcept
742 {
743 int r = 1;
744 while (r < v) r <<= 1;
745 return r;
746 }
747
748 // -- Members -------------------------------------------------------------
749 std::atomic<bool> prepared_{ false };
750 AudioBuffer<SampleType> delayBuffer_, wetBuffer_;
751 std::array<ChannelState, kMaxChannels> states_ {};
752 std::array<int, kMaxChannels> writeIndices_ {};
753
754 int maxDelaySamples_ = 0, bufferMask_ = 0;
755 int numChannels_ = 0, blockSize_ = 0;
756 int lastWetSamples_ = 0;
757 SampleType sampleRate_ = SampleType(48000);
758
759 std::atomic<SampleType> globalDelay_ { SampleType(0) };
760 std::atomic<SampleType> feedbackGain_ { SampleType(0) };
761 std::atomic<SampleType> fbLpCoef_ { SampleType(0) };
762 std::atomic<SampleType> fbLpHzShadow_ { SampleType(0) };
763 std::atomic<SampleType> fbHpHzShadow_ { SampleType(0) };
764 std::atomic<SampleType> fbHpCoef_ { SampleType(0) };
765
766 std::atomic<SmootherType> smootherType_ { SmootherType::Exponential };
767 std::atomic<float> smoothingTimeMs_ { 20.0f };
768 std::atomic<bool> smootherDirty_ { false };
769 std::atomic<bool> delayTargetDirty_ { false };
770 std::atomic<FeedbackMode> feedbackMode_ { FeedbackMode::Analog };
771
772 Smoothers::LinearSmoother mixSmoother_;
773};
774
775} // 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:165
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
Definition Delay.h:483
SampleType processSample(int ch, SampleType input) noexcept
Processes a single sample for a specific channel.
Definition Delay.h:240
AudioBufferView< SampleType > getWetView() noexcept
Exposes the internal wet buffer view.
Definition Delay.h:462
void setFeedback(SampleType gain) noexcept
Sets the global feedback amount.
Definition Delay.h:208
std::vector< uint8_t > getState() const
Serializes the parameter state (setup/UI threads; allocates).
Definition Delay.h:469
void mixWetToDry(AudioBufferView< SampleType > dry, SampleType mix) noexcept
Mixes the processed wet buffer back into the provided dry buffer.
Definition Delay.h:429
void pushDryToWet(AudioBufferView< SampleType > dry) noexcept
Copies a dry buffer into the internal wet buffer.
Definition Delay.h:350
int getMaxDelaySamples() const noexcept
Returns maximum capacity in samples.
Definition Delay.h:465
SampleType getCurrentDelaySamples() const noexcept
Returns the current target delay time in samples.
Definition Delay.h:200
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:225
void prepareMs(const AudioSpec &spec, double maxDelayMs)
Prepares the delay with a maximum capacity in milliseconds.
Definition Delay.h:129
void prepare(const AudioSpec &spec, double maxDelaySeconds)
Prepares the delay structures and allocates memory. Must be called before processing.
Definition Delay.h:88
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:311
FeedbackMode getFeedbackMode() const noexcept
Returns the active feedback mode.
Definition Delay.h:73
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:373
void pushDryToWet(AudioBufferView< const SampleType > dry) noexcept
Copies a dry buffer into the internal wet buffer.
Definition Delay.h:344
void setSmoother(SmootherType type) noexcept
Sets the smoothing algorithm used for delay time changes.
Definition Delay.h:155
void setDelaySamples(SampleType samples) noexcept
Sets the delay time in samples.
Definition Delay.h:180
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:216
void setFeedbackMode(FeedbackMode mode) noexcept
Selects clean or analog feedback regeneration. Thread-safe.
Definition Delay.h:67
FeedbackMode
Feedback path colour.
Definition Delay.h:57
void setDelayMs(SampleType ms) noexcept
Sets the delay time in milliseconds.
Definition Delay.h:194
static constexpr int kMaxChannels
Definition Delay.h:502
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:273
void advanceWriteIndex(int ch) noexcept
Advances the write index for a specific channel.
Definition Delay.h:528
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:360
void setDelaySeconds(SampleType secs) noexcept
Sets the delay time in seconds.
Definition Delay.h:197
void reset() noexcept
Clears the delay buffers and resets all filter and feedback states.
Definition Delay.h:137
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
Definition StateBlob.h:160
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
Definition StateBlob.h:203
bool isValid() const noexcept
Definition StateBlob.h:198
uint32_t processorId() const noexcept
Definition StateBlob.h:199
Serializes key/value parameters into a versioned blob.
Definition StateBlob.h:52
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
Definition StateBlob.h:104
void write(const char *key, float value)
Writes a float parameter.
Definition StateBlob.h:70
Main namespace for the DSPark framework.
constexpr uint32_t stateId(const char(&tag)[5]) noexcept
Builds a FOURCC processor id, e.g. dspark::stateId("COMP").
Definition StateBlob.h:639
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
Definition AudioSpec.h:67
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
Definition AudioSpec.h:56
int maxBlockSize
Maximum number of samples per processing block.
Definition AudioSpec.h:51
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43
Smoothers::ExponentialSmoother exp
Definition Delay.h:512
Smoothers::OnePoleSmoother onePole
Definition Delay.h:513
Smoothers::StateVariableSmoother svf
Definition Delay.h:517
Smoothers::ButterworthSmoother butter
Definition Delay.h:518
Smoothers::LinearSmoother lin
Definition Delay.h:511
Smoothers::CriticallyDampedSmoother crit
Definition Delay.h:519
Smoothers::SlewLimiter slew
Definition Delay.h:516
Smoothers::MultiPoleSmoother< 2 > multi2
Definition Delay.h:514
Smoothers::AsymmetricSmoother asym
Definition Delay.h:515
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