DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
Reverb.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
60#include "../Core/Convolver.h"
61#include "../Core/DryWetMixer.h"
62#include "../Core/RingBuffer.h"
63#include "../Core/AudioSpec.h"
64#include "../Core/AudioBuffer.h"
65#include "../Core/DspMath.h"
66#include "../Core/Resampler.h"
67#include "../Core/StateBlob.h"
68#ifndef DSPARK_NO_FILE_IO
69#include "../IO/WavFile.h"
70#endif
71
72#include <algorithm>
73#include <atomic>
74#include <cmath>
75#include <cstddef>
76#include <cstdint>
77#include <memory>
78#include <vector>
79
80namespace dspark {
81
91template <FloatType T>
92class Reverb
93{
94public:
95 ~Reverb() = default; // non-virtual: leaf class (no virtual dispatch)
96
97 // -- Lifecycle --------------------------------------------------------------
98
110 void prepare(const AudioSpec& spec)
111 {
112 if (!spec.isValid()) return; // release-safe: keep previous state
113
114 spec_ = spec;
115
116 // The convolution engine partitions at the next power of two of the
117 // max block size (>= 2, matching Convolver's own normalisation), and
118 // that is exactly its processing latency. Clamp before the round-up
119 // loop so an absurd block size cannot overflow the shift.
120 const int blockSize = std::clamp(spec.maxBlockSize, 1, 1 << 20);
121 int fftBlock = 2;
122 while (fftBlock < blockSize) fftBlock <<= 1;
123 fftBlockSize_ = fftBlock;
124
125 // Delay the dry path by the same amount so dry and wet stay aligned
126 // at any mix (the wet is late by the convolver latency; without this
127 // the mix comb-filtered against the shifted dry).
128 mixer_.prepare(spec);
129 mixer_.setLatencyCompensation(fftBlockSize_);
130
131 // Pre-delay ring buffers (one per channel, max 500ms)
132 int maxDelaySamples = static_cast<int>(spec.sampleRate * 0.5) + 1;
133 preDelayBuffers_.resize(static_cast<size_t>(spec.numChannels));
134 for (auto& rb : preDelayBuffers_)
135 rb.prepare(maxDelaySamples);
136
138
139 // Re-apply IR if one was already loaded. applyIR() rebuilds the bank
140 // on this (GUI) thread and publishes it atomically.
141 if (!irStorage_.empty())
142 applyIR();
143 }
144
161 void processBlock(AudioBufferView<T> buffer) noexcept
162 {
163 // Design note: the bank swap is guarded by a one-flag spinlock (see
164 // loadBank()): the only writer is loadIR(), a rare, user-initiated
165 // event, so contention is effectively zero. A manual RCU scheme
166 // would remove the spinlock at the cost of a real use-after-free
167 // hazard under racing loads; correctness wins here.
168 auto bank = loadBank();
169 if (!bank || bank->convolvers.empty()) return;
170
171 const int nCh = std::min(buffer.getNumChannels(),
172 static_cast<int>(bank->convolvers.size()));
173 const int nS = buffer.getNumSamples();
174
175 mixer_.pushDry(buffer);
176
177 int preDelSamp = preDelaySamples_.load(std::memory_order_relaxed);
178 T mixVal = mix_.load(std::memory_order_relaxed);
179
180 for (int ch = 0; ch < nCh; ++ch)
181 {
182 T* data = buffer.getChannel(ch);
183
184 if (preDelSamp > 0)
185 {
186 auto& ring = preDelayBuffers_[static_cast<size_t>(ch)];
187 for (int i = 0; i < nS; ++i)
188 {
189 ring.push(data[i]);
190 data[i] = ring.read(preDelSamp);
191 }
192 }
193
194 bank->convolvers[static_cast<size_t>(ch)].processInPlace(data, nS);
195 }
196
197 mixer_.mixWet(buffer, mixVal);
198 }
199
207 void reset() noexcept
208 {
209 // Reset the snapshot we can see; if a concurrent load publishes a
210 // replacement bank it arrives freshly zeroed anyway.
211 if (auto bank = loadBank())
212 for (auto& conv : bank->convolvers)
213 conv.reset();
214 for (auto& rb : preDelayBuffers_)
215 rb.reset();
216 mixer_.reset();
217 }
218
219 // -- Level 1: Simple API ----------------------------------------------------
220
221#ifndef DSPARK_NO_FILE_IO
233 bool loadIR(const char* wavFilePath)
234 {
235 WavFile wav;
236 if (!wav.openRead(wavFilePath))
237 return false;
238
239 auto info = wav.getInfo();
240 if (info.numSamples <= 0 || info.numChannels <= 0
241 || info.numSamples > (static_cast<int64_t>(1) << 30)
242 || !(info.sampleRate > 0))
243 {
244 wav.close();
245 return false;
246 }
247
248 AudioBuffer<T> irBuf;
249 irBuf.resize(info.numChannels, static_cast<int>(info.numSamples));
250 wav.readSamples(irBuf.toView());
251 wav.close();
252
253 // Store channel 0 (or all channels)
254 irChannels_ = info.numChannels;
255 int irLen = static_cast<int>(info.numSamples);
256
257 irStorage_.resize(static_cast<size_t>(irChannels_) * static_cast<size_t>(irLen));
258 for (int ch = 0; ch < irChannels_; ++ch)
259 {
260 const T* src = irBuf.getChannel(ch);
261 T* dst = irStorage_.data() + static_cast<size_t>(ch) * static_cast<size_t>(irLen);
262 std::copy_n(src, irLen, dst);
263 }
264 irLength_ = irLen;
265 irSampleRate_ = info.sampleRate;
266
267 if (spec_.sampleRate > 0)
268 applyIR();
269
270 return true;
271 }
272#endif // DSPARK_NO_FILE_IO
273
279 void setMix(T dryWet) noexcept
280 {
281 if (!std::isfinite(dryWet)) return;
282 mix_.store(std::clamp(dryWet, T(0), T(1)), std::memory_order_relaxed);
283 }
284
285 // -- Level 2: Intermediate API ----------------------------------------------
286
295 bool loadIR(const T* data, int length, double irSampleRate)
296 {
297 if (data == nullptr || length <= 0
298 || !std::isfinite(irSampleRate) || !(irSampleRate > 0.0))
299 return false;
300
301 irChannels_ = 1;
302 irLength_ = length;
303 irSampleRate_ = irSampleRate;
304
305 irStorage_.assign(data, data + length);
306
307 if (spec_.sampleRate > 0)
308 applyIR();
309
310 return true;
311 }
312
323 void setPreDelay(T ms) noexcept
324 {
325 if (!std::isfinite(ms)) return;
326 preDelayMs_.store(std::clamp(ms, T(0), T(500)), std::memory_order_relaxed);
328 }
329
353 void setDecayScale(T scale)
354 {
355 if (!std::isfinite(scale)) return;
356 decayScale_.store(std::clamp(scale, T(0.25), T(2)),
357 std::memory_order_relaxed);
358 if (spec_.sampleRate > 0 && !irStorage_.empty())
359 applyIR();
360 }
361
376 void setStretch(T ratio)
377 {
378 if (!std::isfinite(ratio)) return;
379 stretch_.store(std::clamp(ratio, T(0.5), T(2)),
380 std::memory_order_relaxed);
381 if (spec_.sampleRate > 0 && !irStorage_.empty())
382 applyIR();
383 }
384
386 [[nodiscard]] T getDecayScale() const noexcept { return decayScale_.load(std::memory_order_relaxed); }
387
389 [[nodiscard]] T getStretch() const noexcept { return stretch_.load(std::memory_order_relaxed); }
390
391 // -- Level 3: Expert API ----------------------------------------------------
392
404 Convolver<T>& getConvolver(int channel = 0)
405 {
406 auto bank = loadBank();
407 if (!bank || bank->convolvers.empty())
408 return fallbackConvolver_;
409 const int n = static_cast<int>(bank->convolvers.size());
410 channel = std::clamp(channel, 0, n - 1);
411 return bank->convolvers[static_cast<size_t>(channel)];
412 }
413
416
418 [[nodiscard]] bool isLoaded() const noexcept
419 {
420 return static_cast<bool>(loadBank());
421 }
422
424 [[nodiscard]] T getMix() const noexcept { return mix_.load(std::memory_order_relaxed); }
425
427 [[nodiscard]] T getPreDelay() const noexcept { return preDelayMs_.load(std::memory_order_relaxed); }
428
437 [[nodiscard]] int getLatency() const noexcept
438 {
439 auto bank = loadBank();
440 return (bank && !bank->convolvers.empty())
441 ? bank->convolvers.front().getLatency() : 0;
442 }
443
444
447 [[nodiscard]] std::vector<uint8_t> getState() const
448 {
449 StateWriter w(stateId("CRVB"), 1);
450 w.write("mix", mix_.load(std::memory_order_relaxed));
451 w.write("preDelay", preDelayMs_.load(std::memory_order_relaxed));
452 w.write("decayScale", decayScale_.load(std::memory_order_relaxed));
453 w.write("stretch", stretch_.load(std::memory_order_relaxed));
454 return w.blob();
455 }
456
458 bool setState(const uint8_t* data, size_t size)
459 {
460 StateReader r(data, size);
461 if (!r.isValid() || r.processorId() != stateId("CRVB")) return false;
462 setMix(static_cast<T>(r.read("mix", 0.3f)));
463 setPreDelay(static_cast<T>(r.read("preDelay", 0.0f)));
464 // Store both shaping values first, then rebuild once (each setter
465 // would otherwise trigger its own IR rebuild). Non-finite blob values
466 // keep the current settings.
467 T ds = static_cast<T>(r.read("decayScale", 1.0f));
468 T st = static_cast<T>(r.read("stretch", 1.0f));
469 if (!std::isfinite(ds)) ds = decayScale_.load(std::memory_order_relaxed);
470 if (!std::isfinite(st)) st = stretch_.load(std::memory_order_relaxed);
471 ds = std::clamp(ds, T(0.25), T(2));
472 st = std::clamp(st, T(0.5), T(2));
473 const bool shapeChanged =
474 ds != decayScale_.load(std::memory_order_relaxed)
475 || st != stretch_.load(std::memory_order_relaxed);
476 decayScale_.store(ds, std::memory_order_relaxed);
477 stretch_.store(st, std::memory_order_relaxed);
478 if (shapeChanged && spec_.sampleRate > 0 && !irStorage_.empty())
479 applyIR();
480 return true;
481 }
482
483protected:
484 void updatePreDelay() noexcept
485 {
486 if (spec_.sampleRate > 0)
487 {
488 // The pre-delay ring buffers hold 500 ms; clamp so an over-range pre-delay
489 // can't read past the buffer (RingBuffer::read would wrap to a wrong sample).
490 const int maxSamp = static_cast<int>(spec_.sampleRate * 0.5);
491 const int samp = static_cast<int>(static_cast<T>(spec_.sampleRate)
492 * preDelayMs_.load(std::memory_order_relaxed) / T(1000));
493 preDelaySamples_.store(std::clamp(samp, 0, maxSamp), std::memory_order_relaxed);
494 }
495 }
496
497 void applyIR()
498 {
499 if (irStorage_.empty() || irLength_ <= 0 || fftBlockSize_ <= 0) return;
500
501 int numCh = spec_.numChannels;
502
503 // Build the new bank in a local shared_ptr. The old bank (if any)
504 // stays live until the last audio-thread snapshot releases it.
505 auto newBank = std::make_shared<ConvolverBank>();
506 newBank->convolvers.resize(static_cast<size_t>(numCh));
507
508 // IR shaping controls, always applied to the stored original.
509 // Stretch works by declaring a scaled source rate and letting the
510 // resampling stage do the time-scaling (tape-speed semantics).
511 const double dScale =
512 static_cast<double>(decayScale_.load(std::memory_order_relaxed));
513 const double stretch =
514 static_cast<double>(stretch_.load(std::memory_order_relaxed));
515 const bool doShape = std::abs(dScale - 1.0) > 1e-6;
516 const double effIrRate = irSampleRate_ / std::max(stretch, 0.01);
517
518 std::vector<T> shaped; // lazy decay-shaped copy of one IR channel
519 int shapedCh = -1;
520
521 for (int ch = 0; ch < numCh; ++ch)
522 {
523 // Pick IR channel: use corresponding channel if available, else mono (ch 0)
524 int irCh = (ch < irChannels_) ? ch : 0;
525 const T* irData = irStorage_.data()
526 + static_cast<size_t>(irCh) * static_cast<size_t>(irLength_);
527 int irLen = irLength_;
528
529 if (doShape)
530 {
531 if (shapedCh != irCh)
532 {
533 shaped = shapeDecay(irData, irLength_, dScale);
534 shapedCh = irCh;
535 }
536 if (!shaped.empty())
537 {
538 irData = shaped.data();
539 irLen = static_cast<int>(shaped.size());
540 }
541 }
542
543 auto& conv = newBank->convolvers[static_cast<size_t>(ch)];
544
545 // Resample if the (stretch-adjusted) IR rate differs from the engine
546 if (std::abs(effIrRate - spec_.sampleRate) > 1.0)
547 {
548 Resampler<T> resampler;
549 resampler.prepare(effIrRate, spec_.sampleRate);
550 // Size from the resampler's own bound (INT_MAX-safe): the old
551 // floor(n*ratio)+1 arithmetic could overflow the int cast with
552 // an extreme rate ratio.
553 std::vector<T> resampled(
554 static_cast<size_t>(resampler.getMaxOutputSamples(irLen)));
555 int produced = resampler.processBlock(irData, irLen,
556 resampled.data());
557
558 conv.prepare(fftBlockSize_, resampled.data(), produced);
559 }
560 else
561 {
562 conv.prepare(fftBlockSize_, irData, irLen);
563 }
564 }
565
566 // Atomic release-store: any subsequent acquire-load in processBlock
567 // will observe the fully-constructed bank.
568 storeBank(newBank);
569 }
570
581 [[nodiscard]] std::vector<T> shapeDecay(const T* ir, int len,
582 double factor) const
583 {
584 if (!ir || len < 64) return {};
585
586 // Total energy + direct-sound peak (double accumulation).
587 double total = 0.0;
588 double peakMag = 0.0;
589 int peak = 0;
590 for (int n = 0; n < len; ++n)
591 {
592 const double v = static_cast<double>(ir[n]);
593 total += v * v;
594 const double m = std::abs(v);
595 if (m > peakMag) { peakMag = m; peak = n; }
596 }
597 if (total <= 1e-30 || peakMag <= 0.0) return {};
598
599 // Schroeder EDC crossings at -5 dB and -25 dB (energy ratios).
600 constexpr double r5 = 0.31622776601683794; // 10^(-5/10)
601 constexpr double r25 = 0.0031622776601683794; // 10^(-25/10)
602 int t5 = -1, t25 = -1;
603 double tail = total;
604 for (int n = 0; n < len; ++n)
605 {
606 const double ratio = tail / total;
607 if (t5 < 0 && ratio <= r5 && n > peak) t5 = n;
608 if (ratio <= r25 && n > peak) { t25 = n; break; }
609 const double v = static_cast<double>(ir[n]);
610 tail -= v * v;
611 }
612 if (t5 < 0 || t25 < 0 || t25 - t5 < 32) return {}; // no usable slope
613
614 // Amplitude decay rate: the EDC drops 20 dB over (t25 - t5) samples,
615 // so exp(-beta * t) with beta = ln(10) / (t25 - t5).
616 const double beta = 2.302585092994046 / static_cast<double>(t25 - t5);
617 const double k = beta * (1.0 / factor - 1.0);
618
619 std::vector<T> out(static_cast<size_t>(len));
620 const double gStep = std::exp(-k);
621 double g = 1.0;
622 for (int n = 0; n < len; ++n)
623 {
624 out[static_cast<size_t>(n)] = (n <= peak)
625 ? ir[n]
626 : static_cast<T>(static_cast<double>(ir[n]) * g);
627 if (n >= peak) g *= gStep;
628 }
629
630 if (factor < 1.0)
631 {
632 // Trim where the shaped energy falls below -100 dB of its total:
633 // the removed stretch is inaudible, and a shorter IR means fewer
634 // convolution partitions (the CPU saving the shaping is for).
635 double sTotal = 0.0;
636 for (const T v : out) sTotal += static_cast<double>(v) * v;
637 if (sTotal > 1e-30)
638 {
639 double sTail = sTotal;
640 int cut = len;
641 for (int n = 0; n < len; ++n)
642 {
643 if (sTail / sTotal <= 1e-10) { cut = n; break; }
644 const double v = static_cast<double>(out[static_cast<size_t>(n)]);
645 sTail -= v * v;
646 }
647 cut = std::max(cut, 64);
648 if (cut < len) out.resize(static_cast<size_t>(cut));
649 }
650 }
651 else if (factor > 1.0)
652 {
653 // A raised envelope would end in a cliff at the IR boundary:
654 // fade the final stretch (up to 20 ms at 48 kHz) with a raised
655 // cosine so the lengthened tail closes cleanly.
656 const int fade = std::min(len / 8, 960);
657 const int start = len - fade;
658 for (int i = 0; i < fade; ++i)
659 {
660 const double w = 0.5 * (1.0 + std::cos(3.141592653589793
661 * static_cast<double>(i + 1)
662 / static_cast<double>(fade)));
663 const size_t idx = static_cast<size_t>(start + i);
664 out[idx] = static_cast<T>(static_cast<double>(out[idx]) * w);
665 }
666 }
667 return out;
668 }
669
670 // Holds the current convolver set. Published by applyIR() and snapshotted
671 // by the audio thread at the top of processBlock, so an in-flight block
672 // keeps a stable bank alive.
674 {
675 std::vector<Convolver<T>> convolvers;
676 };
677
680 std::atomic<T> mix_ { T(0.3) };
681 std::atomic<T> preDelayMs_ { T(0) };
682 std::atomic<int> preDelaySamples_ { 0 };
683 std::atomic<T> decayScale_ { T(1) };
684 std::atomic<T> stretch_ { T(1) };
685
686 // IR storage (GUI-thread only: rebuild source of truth)
687 std::vector<T> irStorage_;
688 int irLength_ = 0;
689 int irChannels_ = 0;
690 double irSampleRate_ = 0;
691
692 // Processing (audio-thread visible state).
693 //
694 // Portable stand-in for std::atomic<std::shared_ptr>: libc++ (macOS,
695 // Emscripten) does not ship the C++20 specialization. A one-flag
696 // spinlock guards only the pointer copy/swap (nanoseconds on the audio
697 // thread against one UI-initiated store per IR load), preserving the
698 // exact snapshot semantics: an in-flight processBlock keeps its own
699 // shared_ptr alive, so audio never reads a half-freed bank.
700 // HONEST CAVEAT (M-006, known MINOR/backlog): the previous bank is normally
701 // released on the UI thread by storeBank(), BUT in the rare interleaving
702 // where the audio thread had already snapshotted it, storeBank() only drops
703 // to refcount 1 and the FINAL release runs on the audio thread when the
704 // block's local shared_ptr expires -- i.e. an IR hot-swap concurrent with a
705 // block can free the old bank's FFT buffers on the audio thread. This is
706 // load-time only (never in steady state) and does not corrupt audio; a
707 // fully wait-free reclaim (hazard pointer / retire list) is backlogged.
708 [[nodiscard]] std::shared_ptr<ConvolverBank> loadBank() const noexcept
709 {
710 while (bankLock_.test_and_set(std::memory_order_acquire)) {}
711 auto copy = bankPtr_;
712 bankLock_.clear(std::memory_order_release);
713 return copy;
714 }
715 void storeBank(std::shared_ptr<ConvolverBank> next) noexcept
716 {
717 while (bankLock_.test_and_set(std::memory_order_acquire)) {}
718 bankPtr_.swap(next);
719 bankLock_.clear(std::memory_order_release);
720 // `next` (the previous bank) destructs here, outside the lock.
721 }
722
723 mutable std::atomic_flag bankLock_ = ATOMIC_FLAG_INIT;
724 std::shared_ptr<ConvolverBank> bankPtr_;
725 std::vector<RingBuffer<T>> preDelayBuffers_;
728};
729
730} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
Owning audio buffer with contiguous, 32-byte aligned storage.
AudioBufferView< T, MaxChannels > toView() noexcept
Returns a non-owning mutable view of this buffer. The view's channel capacity is propagated from MaxC...
T * getChannel(int ch) noexcept
Returns a pointer to the sample data.
void resize(int numChannels, int numSamples)
Allocates the buffer for the given dimensions.
Real-time partitioned convolution using overlap-save with FFT.
Definition Convolver.h:68
Pre-allocated, SIMD-friendly dry/wet blender for real-time audio.
Definition DryWetMixer.h:72
Windowed-sinc sample rate converter optimized for real-time DSP.
Definition Resampler.h:69
int processBlock(const T *input, int inputLength, T *output) noexcept
Resamples a block of audio in single-channel streaming mode.
Definition Resampler.h:186
void prepare(double sourceRate, double targetRate, Quality quality=Quality::Normal)
Prepares the resampler for a given rate conversion.
Definition Resampler.h:88
int getMaxOutputSamples(int inputLength) const noexcept
Returns the maximum number of output samples for a given input length.
Definition Resampler.h:227
Convolution reverb with IR loading, dry/wet, and pre-delay.
Definition Reverb.h:93
void processBlock(AudioBufferView< T > buffer) noexcept
Processes audio through the reverb.
Definition Reverb.h:161
DryWetMixer< T > & getMixer()
Direct access to the DryWetMixer.
Definition Reverb.h:415
~Reverb()=default
T getMix() const noexcept
Returns the current mix value.
Definition Reverb.h:424
std::atomic< int > preDelaySamples_
Definition Reverb.h:682
bool isLoaded() const noexcept
Returns true if an IR has been loaded and applied.
Definition Reverb.h:418
std::vector< T > irStorage_
Definition Reverb.h:687
std::vector< T > shapeDecay(const T *ir, int len, double factor) const
Returns a decay-scaled copy of one IR channel (see setDecayScale).
Definition Reverb.h:581
void storeBank(std::shared_ptr< ConvolverBank > next) noexcept
Definition Reverb.h:715
void updatePreDelay() noexcept
Definition Reverb.h:484
std::atomic< T > stretch_
Definition Reverb.h:684
int fftBlockSize_
Convolver partition size = engine latency (set in prepare()).
Definition Reverb.h:679
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
Definition Reverb.h:458
T getPreDelay() const noexcept
Returns the current pre-delay in ms.
Definition Reverb.h:427
std::atomic< T > decayScale_
Definition Reverb.h:683
bool loadIR(const char *wavFilePath)
Loads an impulse response from a WAV file.
Definition Reverb.h:233
std::atomic< T > preDelayMs_
Definition Reverb.h:681
int getLatency() const noexcept
Returns the convolution latency in samples.
Definition Reverb.h:437
AudioSpec spec_
Definition Reverb.h:678
bool loadIR(const T *data, int length, double irSampleRate)
Loads an IR from raw sample data.
Definition Reverb.h:295
int irChannels_
Definition Reverb.h:689
std::shared_ptr< ConvolverBank > loadBank() const noexcept
Definition Reverb.h:708
Convolver< T > fallbackConvolver_
Inert engine for getConvolver() with no bank.
Definition Reverb.h:727
void setStretch(T ratio)
Stretches the loaded IR in time (tape-speed style).
Definition Reverb.h:376
void prepare(const AudioSpec &spec)
Prepares the reverb for processing.
Definition Reverb.h:110
void setMix(T dryWet) noexcept
Sets the dry/wet mix.
Definition Reverb.h:279
std::vector< RingBuffer< T > > preDelayBuffers_
Definition Reverb.h:725
std::atomic_flag bankLock_
Definition Reverb.h:723
std::vector< uint8_t > getState() const
Serializes the parameter state. The impulse response itself is content (load it with loadIR),...
Definition Reverb.h:447
DryWetMixer< T > mixer_
Definition Reverb.h:726
std::atomic< T > mix_
Definition Reverb.h:680
Convolver< T > & getConvolver(int channel=0)
Direct access to a channel's Convolver (GUI thread only).
Definition Reverb.h:404
std::shared_ptr< ConvolverBank > bankPtr_
Definition Reverb.h:724
void reset() noexcept
Resets the DSP state (convolver tails, pre-delay, mixer). RT-Safe.
Definition Reverb.h:207
T getDecayScale() const noexcept
Returns the current IR decay scale.
Definition Reverb.h:386
void setDecayScale(T scale)
Scales the decay time (T60) of the loaded IR.
Definition Reverb.h:353
double irSampleRate_
Definition Reverb.h:690
void applyIR()
Definition Reverb.h:497
T getStretch() const noexcept
Returns the current IR stretch ratio.
Definition Reverb.h:389
void setPreDelay(T ms) noexcept
Sets the pre-delay time in milliseconds.
Definition Reverb.h:323
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
Complete WAV file reader and writer in pure C++20.
Definition WavFile.h:67
AudioFileInfo getInfo() const override
Retrieves metadata of the currently opened file.
Definition WavFile.h:145
bool readSamples(AudioBufferView< float > dest) override
Reads samples from the start of the file into the destination view.
Definition WavFile.h:147
bool openRead(const std::filesystem::path &path) override
Opens a WAV file for reading.
Definition WavFile.h:78
void close() override
Finalizes file headers and releases system handles.
Definition WavFile.h:226
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
std::vector< Convolver< T > > convolvers
Definition Reverb.h:675