DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
Reverb.h
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 <array>
74#include <atomic>
75#include <cassert>
76#include <cmath>
77#include <cstddef>
78#include <cstdint>
79#include <memory>
80#include <type_traits>
81#include <utility>
82#include <vector>
83
84namespace dspark {
85
86namespace detail {
87
90{
105
106 static constexpr void control(ControlPoint) noexcept {}
107 static constexpr void audioExchange() noexcept {}
108 static constexpr void audioAfterExchange(bool) noexcept {}
109 static constexpr void audioStateStore() noexcept {}
110 static constexpr void audioValidation(bool) noexcept {}
111 static constexpr void controlValidation(bool) noexcept {}
112 static constexpr void controlScan() noexcept {}
113};
114
122template <typename Bank,
123 typename Hooks = ReverbPublisherNoopHooks,
124 std::uint32_t GenerationMax = 134217727u>
126{
127public:
128 static_assert(std::atomic<std::uint32_t>::is_always_lock_free,
129 "Reverb publication requires lock-free 32-bit atomics");
130 static_assert(GenerationMax >= 1u && GenerationMax <= 134217727u,
131 "Generation must fit the 27-bit token encoding");
132
133 enum class Phase : std::uint32_t
134 {
135 free = 0u,
136 building = 1u,
137 published = 2u,
138 active = 3u,
139 retired = 4u,
140 exhausted = 5u
141 };
142
143 enum class PublishResult
144 {
145 published,
148 };
149
150 ReverbBankPublisher() noexcept = default;
151 ~ReverbBankPublisher() noexcept { shutdown(); }
152
157
159 template <typename Commit>
160 PublishResult publish(std::unique_ptr<Bank> candidate,
161 std::uint32_t latency,
162 Commit&& commit) noexcept
163 {
164 static_assert(std::is_nothrow_invocable_v<Commit&>,
165 "Reverb publication commit must be noexcept");
166 assert(candidate != nullptr);
167 Hooks::control(Hooks::ControlPoint::candidateReady);
168 Hooks::control(Hooks::ControlPoint::beforeFirstScan);
169
170 SlotChoice choice = scanOnce();
171 bool claimedPending = false;
172 if (!choice.valid)
173 {
174 const std::uint32_t observed =
175 pendingToken_.load(std::memory_order_acquire);
176 const bool exactPinnedPending = observed != emptyToken
177 && observed == pinnedToken_;
178 const bool generationLimitPending = observed != emptyToken
179 && tokenGeneration(observed) >= GenerationMax;
180 if (!exactPinnedPending && !generationLimitPending)
181 {
182 Hooks::control(Hooks::ControlPoint::beforePendingExchange);
183 const std::uint32_t old =
184 pendingToken_.exchange(emptyToken, std::memory_order_acq_rel);
185 Hooks::control(Hooks::ControlPoint::afterPendingExchange);
186 if (old != emptyToken)
187 {
188 const std::size_t index = tokenIndex(old);
189 const std::uint32_t generation = tokenGeneration(old);
190 const bool exactObserved = old == observed;
191 const bool reusable = exactObserved
192 && old != pinnedToken_ && generation < GenerationMax
193 && slots_[index].stateWord.load(std::memory_order_acquire)
194 == encodeState(generation, Phase::published);
195 Hooks::controlValidation(reusable);
196 assert(reusable);
197 if (!reusable)
198 {
199 // No slot or metadata has changed yet. Restore the exact
200 // token so even an injected invariant failure has the
201 // strong failure guarantee.
202 pendingToken_.store(old, std::memory_order_release);
204 }
205 choice = { index, generation + 1u, true, false };
206 claimedPending = true;
207 }
208 else
209 {
210 // Audio won the pending token. One read-only bounded rescan
211 // may observe the RETIRED slot it released; an empty-token
212 // exchange changed no externally visible state.
213 choice = scanOnce();
214 }
215 }
216 }
217
218 if (!choice.valid)
219 {
220 Hooks::control(Hooks::ControlPoint::noCapacity);
222 }
223
224 Hooks::control(Hooks::ControlPoint::afterSlotSelection);
225 auto& slot = slots_[choice.index];
226 if (claimedPending)
227 {
228 const std::uint32_t claimedGeneration = choice.generation - 1u;
229 const bool retired = retireControlOwned(
230 makeToken(claimedGeneration, choice.index));
231 assert(retired);
232 (void)retired;
233 }
234 nextGeneration_[choice.index] = choice.generation;
235 slot.stateWord.store(encodeState(choice.generation, Phase::building),
236 std::memory_order_relaxed);
237 if (choice.reclaimsRetired)
238 Hooks::control(Hooks::ControlPoint::beforeOldReclaim);
239 slot.bank = std::move(candidate);
240 slot.stateWord.store(encodeState(choice.generation, Phase::published),
241 std::memory_order_release);
242 Hooks::control(Hooks::ControlPoint::afterPublishedStore);
243
244 const std::uint32_t token = makeToken(choice.generation, choice.index);
245 Hooks::control(Hooks::ControlPoint::beforePendingExchange);
246 const std::uint32_t old =
247 pendingToken_.exchange(token, std::memory_order_acq_rel);
248 Hooks::control(Hooks::ControlPoint::afterPendingExchange);
249 if (old != emptyToken && !retireControlOwned(old))
250 {
251 // Once slot publication begins, the protocol is a no-fail commit.
252 // A token returned by this exchange is control-owned and must name
253 // PUBLISHED storage under the single-writer contract.
254 assert(false && "Reverb publication ownership invariant");
255 }
256
257 Hooks::control(Hooks::ControlPoint::beforeCommit);
258 commit();
259 latestToken_ = token;
260 publicationMetadata_.store(packMetadata(latency),
261 std::memory_order_release);
262 scanStart_ = (choice.index + 1u) & slotMask;
263 quarantineExhaustedRetired();
264 Hooks::control(Hooks::ControlPoint::afterCommit);
266 }
267
269 [[nodiscard]] Bank* adoptAtBoundary() noexcept
270 {
271 Hooks::audioExchange();
272 const std::uint32_t next =
273 pendingToken_.exchange(emptyToken, std::memory_order_acq_rel);
274 Hooks::audioAfterExchange(next != emptyToken);
275 if (next != emptyToken)
276 {
277 const std::size_t nextIndex = tokenIndex(next);
278 const std::uint32_t nextGeneration = tokenGeneration(next);
279 const bool nextValid =
280 slots_[nextIndex].stateWord.load(std::memory_order_acquire)
281 == encodeState(nextGeneration, Phase::published);
282 Hooks::audioValidation(nextValid);
283 assert(nextValid);
284 if (nextValid)
285 {
286 slots_[nextIndex].stateWord.store(
287 encodeState(nextGeneration, Phase::active),
288 std::memory_order_release);
289 Hooks::audioStateStore();
290
291 const std::uint32_t old = activeToken_;
292 activeToken_ = next;
293 if (old != emptyToken)
294 {
295 const std::size_t oldIndex = tokenIndex(old);
296 const std::uint32_t oldGeneration = tokenGeneration(old);
297 const bool oldValid =
298 slots_[oldIndex].stateWord.load(std::memory_order_acquire)
299 == encodeState(oldGeneration, Phase::active);
300 Hooks::audioValidation(oldValid);
301 assert(oldValid);
302 if (oldValid)
303 {
304 slots_[oldIndex].stateWord.store(
305 encodeState(oldGeneration, Phase::retired),
306 std::memory_order_release);
307 Hooks::audioStateStore();
308 }
309 }
310 }
311 }
312
313 return activeToken_ == emptyToken
314 ? nullptr
315 : slots_[tokenIndex(activeToken_)].bank.get();
316 }
317
319 [[nodiscard]] Bank* pinLatest() noexcept
320 {
321 const std::uint32_t previous = pinnedToken_;
322 pinnedToken_ = emptyToken;
323 const std::uint32_t latest = latestToken_;
324 if (previous != emptyToken && previous != latest)
325 reclaimIfRetired(previous);
326
327 if (latest == emptyToken) return nullptr;
328 const std::size_t index = tokenIndex(latest);
329 const std::uint32_t state =
330 slots_[index].stateWord.load(std::memory_order_acquire);
331 const Phase phase = statePhase(state);
332 const bool valid = stateGeneration(state) == tokenGeneration(latest)
333 && phase != Phase::free && phase != Phase::building
334 && phase != Phase::exhausted && slots_[index].bank != nullptr;
335 Hooks::controlValidation(valid);
336 assert(valid);
337 if (!valid) return nullptr;
338 pinnedToken_ = latest;
339 return slots_[index].bank.get();
340 }
341
342 [[nodiscard]] bool isLoaded() const noexcept
343 {
344 return (publicationMetadata_.load(std::memory_order_acquire)
345 & loadedMask) != 0u;
346 }
347
348 [[nodiscard]] int latency() const noexcept
349 {
350 return static_cast<int>(publicationMetadata_.load(
351 std::memory_order_acquire) & latencyMask);
352 }
353
355 void shutdown() noexcept
356 {
357 pendingToken_.exchange(emptyToken, std::memory_order_acq_rel);
358 activeToken_ = emptyToken;
359 latestToken_ = emptyToken;
360 pinnedToken_ = emptyToken;
361 publicationMetadata_.store(0u, std::memory_order_release);
362 for (auto& slot : slots_)
363 {
364 slot.bank.reset();
365 slot.stateWord.store(encodeState(0u, Phase::free),
366 std::memory_order_relaxed);
367 }
368 nextGeneration_.fill(0u);
369 scanStart_ = 0u;
370 }
371
372 // Deterministic detail-level observability used by the dedicated tests.
373 [[nodiscard]] std::uint32_t stateWordForTest(std::size_t index) const noexcept
374 {
375 return slots_[index].stateWord.load(std::memory_order_acquire);
376 }
377 [[nodiscard]] std::uint32_t pendingTokenForTest() const noexcept
378 {
379 return pendingToken_.load(std::memory_order_acquire);
380 }
381 [[nodiscard]] std::uint32_t activeTokenForTest() const noexcept { return activeToken_; }
382 [[nodiscard]] std::uint32_t latestTokenForTest() const noexcept { return latestToken_; }
383 [[nodiscard]] std::uint32_t pinnedTokenForTest() const noexcept { return pinnedToken_; }
384 [[nodiscard]] std::size_t residentBanksForTest() const noexcept
385 {
386 std::size_t count = 0;
387 for (const auto& slot : slots_) count += slot.bank != nullptr ? 1u : 0u;
388 return count;
389 }
390 [[nodiscard]] const void* atomicWordAddressForTest(
391 std::size_t index) const noexcept
392 {
393 if (index < slots_.size())
394 return static_cast<const void*>(&slots_[index].stateWord);
395 if (index == slots_.size())
396 return static_cast<const void*>(&pendingToken_);
397 return static_cast<const void*>(&publicationMetadata_);
398 }
399 [[nodiscard]] static constexpr std::size_t atomicWordCountForTest() noexcept
400 {
401 return 6u;
402 }
403 [[nodiscard]] static constexpr Phase phaseForTest(std::uint32_t state) noexcept
404 {
405 return statePhase(state);
406 }
407 [[nodiscard]] static constexpr std::uint32_t generationForTest(
408 std::uint32_t state) noexcept
409 {
410 return stateGeneration(state);
411 }
412
413private:
414 static constexpr std::uint32_t emptyToken = 0u;
415 static constexpr std::uint32_t slotMask = 3u;
416 static constexpr std::uint32_t phaseMask = 7u;
417 static constexpr std::uint32_t loadedMask = 0x80000000u;
418 static constexpr std::uint32_t latencyMask = 0x7fffffffu;
419
420 struct BankSlot
421 {
422 std::unique_ptr<Bank> bank;
423 std::atomic<std::uint32_t> stateWord { 0u };
424 };
425
426 struct SlotChoice
427 {
428 std::size_t index = 0;
429 std::uint32_t generation = 0;
430 bool valid = false;
431 bool reclaimsRetired = false;
432 };
433
434 [[nodiscard]] static constexpr std::uint32_t makeToken(
435 std::uint32_t generation, std::size_t index) noexcept
436 {
437 return (generation << 2u) | static_cast<std::uint32_t>(index);
438 }
439
440 [[nodiscard]] static constexpr std::size_t tokenIndex(
441 std::uint32_t token) noexcept
442 {
443 return static_cast<std::size_t>(token & slotMask);
444 }
445
446 [[nodiscard]] static constexpr std::uint32_t tokenGeneration(
447 std::uint32_t token) noexcept
448 {
449 return token >> 2u;
450 }
451
452 [[nodiscard]] static constexpr std::uint32_t encodeState(
453 std::uint32_t generation, Phase phase) noexcept
454 {
455 return (generation << 3u) | static_cast<std::uint32_t>(phase);
456 }
457
458 [[nodiscard]] static constexpr std::uint32_t stateGeneration(
459 std::uint32_t state) noexcept
460 {
461 return state >> 3u;
462 }
463
464 [[nodiscard]] static constexpr Phase statePhase(std::uint32_t state) noexcept
465 {
466 return static_cast<Phase>(state & phaseMask);
467 }
468
469 [[nodiscard]] static constexpr std::uint32_t packMetadata(
470 std::uint32_t latency) noexcept
471 {
472 return loadedMask | std::min(latency, latencyMask);
473 }
474
475 [[nodiscard]] SlotChoice scanOnce() noexcept
476 {
477 for (std::size_t offset = 0; offset < slots_.size(); ++offset)
478 {
479 Hooks::controlScan();
480 const std::size_t index = (scanStart_ + offset) & slotMask;
481 auto& slot = slots_[index];
482 const std::uint32_t state =
483 slot.stateWord.load(std::memory_order_acquire);
484 const Phase phase = statePhase(state);
485 const std::uint32_t generation = stateGeneration(state);
486 const std::uint32_t exactToken = makeToken(generation, index);
487
488 if (phase == Phase::free)
489 {
490 const bool pristine = generation == 0u
491 && nextGeneration_[index] == 0u && slot.bank == nullptr;
492 Hooks::controlValidation(pristine);
493 assert(pristine);
494 if (pristine) return { index, 1u, true, false };
495 continue;
496 }
497
498 if (phase != Phase::retired || exactToken == pinnedToken_)
499 continue;
500
501 const bool generationMatches =
502 nextGeneration_[index] == generation;
503 Hooks::controlValidation(generationMatches);
504 assert(generationMatches);
505 if (!generationMatches) continue;
506
507 if (generation >= GenerationMax) continue;
508 return { index, generation + 1u, true, true };
509 }
510 return {};
511 }
512
513 bool retireControlOwned(std::uint32_t token) noexcept
514 {
515 const std::size_t index = tokenIndex(token);
516 const std::uint32_t generation = tokenGeneration(token);
517 auto& slot = slots_[index];
518 const bool valid =
519 slot.stateWord.load(std::memory_order_acquire)
520 == encodeState(generation, Phase::published);
521 Hooks::controlValidation(valid);
522 assert(valid);
523 if (!valid) return false;
524
525 Hooks::control(Hooks::ControlPoint::beforeOldRetirement);
526 slot.stateWord.store(encodeState(generation, Phase::retired),
527 std::memory_order_release);
528 if (token != pinnedToken_)
529 {
530 Hooks::control(Hooks::ControlPoint::beforeOldReclaim);
531 slot.bank.reset();
532 }
533 return true;
534 }
535
536 void quarantineExhaustedRetired() noexcept
537 {
538 for (std::size_t index = 0; index < slots_.size(); ++index)
539 {
540 auto& slot = slots_[index];
541 const std::uint32_t state =
542 slot.stateWord.load(std::memory_order_acquire);
543 const std::uint32_t generation = stateGeneration(state);
544 if (statePhase(state) != Phase::retired
545 || generation < GenerationMax
546 || makeToken(generation, index) == pinnedToken_)
547 continue;
548 Hooks::control(Hooks::ControlPoint::beforeOldReclaim);
549 slot.bank.reset();
550 slot.stateWord.store(encodeState(generation, Phase::exhausted),
551 std::memory_order_release);
552 }
553 }
554
555 void reclaimIfRetired(std::uint32_t token) noexcept
556 {
557 auto& slot = slots_[tokenIndex(token)];
558 const std::uint32_t state =
559 slot.stateWord.load(std::memory_order_acquire);
560 if (state == encodeState(tokenGeneration(token), Phase::retired))
561 {
562 Hooks::control(Hooks::ControlPoint::beforeOldReclaim);
563 slot.bank.reset();
564 }
565 }
566
567 std::array<BankSlot, 4> slots_ {};
568 std::atomic<std::uint32_t> pendingToken_ { emptyToken };
569 std::atomic<std::uint32_t> publicationMetadata_ { 0u };
570 std::uint32_t activeToken_ = emptyToken; // Audio-owner only.
571 std::uint32_t latestToken_ = emptyToken; // Control-owner only.
572 std::uint32_t pinnedToken_ = emptyToken; // Control-owner only.
573 std::array<std::uint32_t, 4> nextGeneration_ {};
574 std::size_t scanStart_ = 0; // Control-owner only.
575};
576
577} // namespace detail
578
588template <FloatType T>
590{
591protected:
593 {
594 std::vector<Convolver<T>> convolvers;
598 int extraDelay = 0;
599 std::vector<RingBuffer<T>> dryDelay;
600 std::vector<T> dryScratch;
602 };
603
604#if defined(DSPARK_REVERB_TEST_GENERATION_MAX)
605 static constexpr std::uint32_t publisherGenerationMax =
606 DSPARK_REVERB_TEST_GENERATION_MAX;
607#else
608 static constexpr std::uint32_t publisherGenerationMax = 134217727u;
609#endif
612
613public:
614 Reverb() = default;
616
617 Reverb(const Reverb&) = delete;
618 Reverb& operator=(const Reverb&) = delete;
619 Reverb(Reverb&&) = delete;
620 Reverb& operator=(Reverb&&) = delete;
621
622 // -- Lifecycle --------------------------------------------------------------
623
635 void prepare(const AudioSpec& spec)
636 {
637 if (!spec.isValid()) return; // release-safe: keep previous state
638
639 // The convolution engine partitions at the next power of two of the
640 // max block size (>= 2, matching Convolver's own normalisation), and
641 // that is exactly its processing latency. Clamp before the round-up
642 // loop so an absurd block size cannot overflow the shift.
643 const int blockSize = std::clamp(spec.maxBlockSize, 1, 1 << 20);
644 int fftBlock = 2;
645 while (fftBlock < blockSize) fftBlock <<= 1;
646 // Build every potentially-throwing setup object locally. The stopped
647 // audio/setup ownership contract makes the final moves atomic as one
648 // logical transaction even though the members themselves are plain.
649 DryWetMixer<T> nextMixer;
650 nextMixer.prepare(spec);
651 nextMixer.setLatencyCompensation(fftBlock);
652
653 // Pre-delay ring buffers (one per channel, max 500ms)
654 const int maxDelaySamples = static_cast<int>(spec.sampleRate * 0.5) + 1;
655 std::vector<RingBuffer<T>> nextPreDelayBuffers(
656 static_cast<size_t>(spec.numChannels));
657 for (auto& rb : nextPreDelayBuffers)
658 rb.prepare(maxDelaySamples);
659 const int nextPreDelaySamples = calculatePreDelaySamples(spec);
660
661 if (irStorage_.empty())
662 {
663 spec_ = spec;
664 fftBlockSize_ = fftBlock;
665 mixer_ = std::move(nextMixer);
666 preDelayBuffers_ = std::move(nextPreDelayBuffers);
667 preDelaySamples_.store(nextPreDelaySamples,
668 std::memory_order_relaxed);
669 return;
670 }
671
672 auto candidate = buildBank(irStorage_, irLength_, irChannels_,
673 irSampleRate_, spec, fftBlock,
674 decayScale_.load(std::memory_order_relaxed),
675 stretch_.load(std::memory_order_relaxed));
676 const std::uint32_t latency = bankLatency(*candidate);
678 std::move(candidate), latency,
679 [&]() noexcept {
680 spec_ = spec;
681 fftBlockSize_ = fftBlock;
682 mixer_ = std::move(nextMixer);
683 preDelayBuffers_ = std::move(nextPreDelayBuffers);
684 preDelaySamples_.store(nextPreDelaySamples,
685 std::memory_order_relaxed);
686 });
687 }
688
704 void processBlock(AudioBufferView<T> buffer) noexcept
705 {
707 if (!bank || bank->convolvers.empty()) return;
708
709 const int nCh = std::min(buffer.getNumChannels(),
710 static_cast<int>(bank->convolvers.size()));
711 const int nS = buffer.getNumSamples();
712
713 if (bank->extraDelay > 0 && nS <= bank->scratchLength
714 && buffer.getNumChannels() <= static_cast<int>(bank->dryDelay.size()))
715 {
716 // The dry path waits for the wet path's extra samples too.
717 const int dCh = buffer.getNumChannels();
718 std::array<T*, 64> ptrs {};
719 for (int ch = 0; ch < dCh && ch < 64; ++ch)
720 {
721 auto& ring = bank->dryDelay[static_cast<size_t>(ch)];
722 T* dst = bank->dryScratch.data()
723 + static_cast<size_t>(ch) * static_cast<size_t>(bank->scratchLength);
724 const T* src = buffer.getChannel(ch);
725 for (int i = 0; i < nS; ++i)
726 {
727 ring.push(src[i]);
728 dst[i] = ring.read(bank->extraDelay);
729 }
730 ptrs[static_cast<size_t>(ch)] = dst;
731 }
732 mixer_.pushDry(AudioBufferView<T>(ptrs.data(), std::min(dCh, 64), nS));
733 }
734 else
735 {
736 mixer_.pushDry(buffer);
737 }
738
739 int preDelSamp = preDelaySamples_.load(std::memory_order_relaxed);
740 T mixVal = mix_.load(std::memory_order_relaxed);
741
742 for (int ch = 0; ch < nCh; ++ch)
743 {
744 T* data = buffer.getChannel(ch);
745
746 if (preDelSamp > 0)
747 {
748 auto& ring = preDelayBuffers_[static_cast<size_t>(ch)];
749 for (int i = 0; i < nS; ++i)
750 {
751 ring.push(data[i]);
752 data[i] = ring.read(preDelSamp);
753 }
754 }
755
756 bank->convolvers[static_cast<size_t>(ch)].processInPlace(data, nS);
757 }
758
759 mixer_.mixWet(buffer, mixVal);
760 }
761
769 void reset() noexcept
770 {
771 // Reset the snapshot we can see; if a concurrent load publishes a
772 // replacement bank it arrives freshly zeroed anyway.
773 if (ConvolverBank* const bank = bankPublisher_.adoptAtBoundary())
774 {
775 for (auto& conv : bank->convolvers)
776 conv.reset();
777 for (auto& ring : bank->dryDelay)
778 ring.reset();
779 }
780 for (auto& rb : preDelayBuffers_)
781 rb.reset();
782 mixer_.reset();
783 }
784
785 // -- Level 1: Simple API ----------------------------------------------------
786
787#ifndef DSPARK_NO_FILE_IO
799 bool loadIR(const char* wavFilePath)
800 {
801 WavFile wav;
802 if (!wav.openRead(wavFilePath))
803 return false;
804
805 auto info = wav.getInfo();
806 if (info.numSamples <= 0 || info.numChannels <= 0
807 || info.numSamples > (static_cast<int64_t>(1) << 30)
808 || !(info.sampleRate > 0))
809 {
810 wav.close();
811 return false;
812 }
813
814 AudioBuffer<T> irBuf;
815 irBuf.resize(info.numChannels, static_cast<int>(info.numSamples));
816 wav.readSamples(irBuf.toView());
817 wav.close();
818
819 const int nextChannels = info.numChannels;
820 const int nextLength = static_cast<int>(info.numSamples);
821 std::vector<T> nextStorage(
822 static_cast<size_t>(nextChannels) * static_cast<size_t>(nextLength));
823 for (int ch = 0; ch < nextChannels; ++ch)
824 {
825 const T* src = irBuf.getChannel(ch);
826 T* dst = nextStorage.data()
827 + static_cast<size_t>(ch) * static_cast<size_t>(nextLength);
828 std::copy_n(src, nextLength, dst);
829 }
830 return commitImpulseResponse(std::move(nextStorage), nextLength,
831 nextChannels, info.sampleRate);
832 }
833#endif // DSPARK_NO_FILE_IO
834
840 void setMix(T dryWet) noexcept
841 {
842 if (!std::isfinite(dryWet)) return;
843 mix_.store(std::clamp(dryWet, T(0), T(1)), std::memory_order_relaxed);
844 }
845
846 // -- Level 2: Intermediate API ----------------------------------------------
847
856 bool loadIR(const T* data, int length, double irSampleRate)
857 {
858 if (data == nullptr || length <= 0
859 || !std::isfinite(irSampleRate) || !(irSampleRate > 0.0))
860 return false;
861
862 std::vector<T> nextStorage(data, data + length);
863 return commitImpulseResponse(std::move(nextStorage), length, 1,
864 irSampleRate);
865 }
866
881 bool loadIR(AudioBufferView<const T> ir, double irSampleRate)
882 {
883 const int nextChannels = ir.getNumChannels();
884 const int nextLength = ir.getNumSamples();
885 if (nextChannels <= 0 || nextLength <= 0
886 || !std::isfinite(irSampleRate) || !(irSampleRate > 0.0))
887 return false;
888 for (int ch = 0; ch < nextChannels; ++ch)
889 if (ir.getChannel(ch) == nullptr) return false;
890
891 std::vector<T> nextStorage(
892 static_cast<size_t>(nextChannels) * static_cast<size_t>(nextLength));
893 for (int ch = 0; ch < nextChannels; ++ch)
894 std::copy_n(ir.getChannel(ch), nextLength,
895 nextStorage.data() + static_cast<size_t>(ch) * static_cast<size_t>(nextLength));
896 return commitImpulseResponse(std::move(nextStorage), nextLength,
897 nextChannels, irSampleRate);
898 }
899
910 void setPreDelay(T ms) noexcept
911 {
912 if (!std::isfinite(ms)) return;
913 preDelayMs_.store(std::clamp(ms, T(0), T(500)), std::memory_order_relaxed);
915 }
916
940 void setDecayScale(T scale)
941 {
942 if (!std::isfinite(scale)) return;
943 const T next = std::clamp(scale, T(0.25), T(2));
944 if (next == decayScale_.load(std::memory_order_relaxed)) return;
945 if (spec_.sampleRate <= 0 || irStorage_.empty())
946 {
947 decayScale_.store(next, std::memory_order_relaxed);
948 return;
949 }
950 auto candidate = buildBank(irStorage_, irLength_, irChannels_,
952 stretch_.load(std::memory_order_relaxed));
953 const std::uint32_t latency = bankLatency(*candidate);
955 std::move(candidate), latency,
956 [&]() noexcept { decayScale_.store(next, std::memory_order_relaxed); });
957 }
958
973 void setStretch(T ratio)
974 {
975 if (!std::isfinite(ratio)) return;
976 const T next = std::clamp(ratio, T(0.5), T(2));
977 if (next == stretch_.load(std::memory_order_relaxed)) return;
978 if (spec_.sampleRate <= 0 || irStorage_.empty())
979 {
980 stretch_.store(next, std::memory_order_relaxed);
981 return;
982 }
983 auto candidate = buildBank(irStorage_, irLength_, irChannels_,
985 decayScale_.load(std::memory_order_relaxed),
986 next);
987 const std::uint32_t latency = bankLatency(*candidate);
989 std::move(candidate), latency,
990 [&]() noexcept { stretch_.store(next, std::memory_order_relaxed); });
991 }
992
994 [[nodiscard]] T getDecayScale() const noexcept { return decayScale_.load(std::memory_order_relaxed); }
995
997 [[nodiscard]] T getStretch() const noexcept { return stretch_.load(std::memory_order_relaxed); }
998
999 // -- Level 3: Expert API ----------------------------------------------------
1000
1027 Convolver<T>& getConvolver(int channel = 0)
1028 {
1029 ConvolverBank* const bank = bankPublisher_.pinLatest();
1030 if (!bank || bank->convolvers.empty())
1031 return fallbackConvolver_;
1032 const int n = static_cast<int>(bank->convolvers.size());
1033 channel = std::clamp(channel, 0, n - 1);
1034 return bank->convolvers[static_cast<size_t>(channel)];
1035 }
1036
1047
1049 [[nodiscard]] bool isLoaded() const noexcept
1050 {
1051 return bankPublisher_.isLoaded();
1052 }
1053
1055 [[nodiscard]] T getMix() const noexcept { return mix_.load(std::memory_order_relaxed); }
1056
1058 [[nodiscard]] T getPreDelay() const noexcept { return preDelayMs_.load(std::memory_order_relaxed); }
1059
1073 [[nodiscard]] int getLatency() const noexcept
1074 {
1075 return bankPublisher_.latency();
1076 }
1077
1078
1081 [[nodiscard]] std::vector<uint8_t> getState() const
1082 {
1083 StateWriter w(stateId("CRVB"), 1);
1084 w.write("mix", mix_.load(std::memory_order_relaxed));
1085 w.write("preDelay", preDelayMs_.load(std::memory_order_relaxed));
1086 w.write("decayScale", decayScale_.load(std::memory_order_relaxed));
1087 w.write("stretch", stretch_.load(std::memory_order_relaxed));
1088 return w.blob();
1089 }
1090
1092 bool setState(const uint8_t* data, size_t size)
1093 {
1094 StateReader r(data, size);
1095 if (!r.isValid() || r.processorId() != stateId("CRVB")) return false;
1096
1097 // Parse, validate and stage the complete requested state before the
1098 // first externally observable write. Non-finite blob values retain the
1099 // corresponding current value, matching the individual setter policy.
1100 T mix = static_cast<T>(r.read("mix", 0.3f));
1101 T preDelay = static_cast<T>(r.read("preDelay", 0.0f));
1102 T ds = static_cast<T>(r.read("decayScale", 1.0f));
1103 T st = static_cast<T>(r.read("stretch", 1.0f));
1104 if (!std::isfinite(mix)) mix = mix_.load(std::memory_order_relaxed);
1105 if (!std::isfinite(preDelay))
1106 preDelay = preDelayMs_.load(std::memory_order_relaxed);
1107 if (!std::isfinite(ds)) ds = decayScale_.load(std::memory_order_relaxed);
1108 if (!std::isfinite(st)) st = stretch_.load(std::memory_order_relaxed);
1109 mix = std::clamp(mix, T(0), T(1));
1110 preDelay = std::clamp(preDelay, T(0), T(500));
1111 ds = std::clamp(ds, T(0.25), T(2));
1112 st = std::clamp(st, T(0.5), T(2));
1113 const int preDelaySamples = calculatePreDelaySamples(spec_, preDelay);
1114 const bool shapeChanged =
1115 ds != decayScale_.load(std::memory_order_relaxed)
1116 || st != stretch_.load(std::memory_order_relaxed);
1117
1118 const auto commitParameters = [&]() noexcept {
1119 mix_.store(mix, std::memory_order_relaxed);
1120 preDelayMs_.store(preDelay, std::memory_order_relaxed);
1121 preDelaySamples_.store(preDelaySamples, std::memory_order_relaxed);
1122 decayScale_.store(ds, std::memory_order_relaxed);
1123 stretch_.store(st, std::memory_order_relaxed);
1124 };
1125
1126 if (!shapeChanged || spec_.sampleRate <= 0 || irStorage_.empty())
1127 {
1128 commitParameters();
1129 return true;
1130 }
1131
1132 // Candidate construction owns every throwing operation. Publisher
1133 // capacity is resolved before its first slot/scalar mutation; once the
1134 // commit begins, only unique_ptr moves and atomic/plain no-throw stores
1135 // remain.
1136 auto candidate = buildBank(irStorage_, irLength_, irChannels_,
1138 const std::uint32_t latency = bankLatency(*candidate);
1139 const auto result = bankPublisher_.publish(
1140 std::move(candidate), latency, commitParameters);
1141 if (result != Publisher::PublishResult::published) return false;
1142 return true;
1143 }
1144
1145protected:
1146 [[nodiscard]] int calculatePreDelaySamples(const AudioSpec& spec) const noexcept
1147 {
1149 spec, preDelayMs_.load(std::memory_order_relaxed));
1150 }
1151
1152 [[nodiscard]] static int calculatePreDelaySamples(
1153 const AudioSpec& spec, T preDelayMs) noexcept
1154 {
1155 if (!(spec.sampleRate > 0)) return 0;
1156 const int maxSamp = static_cast<int>(spec.sampleRate * 0.5);
1157 const int samp = static_cast<int>(
1158 static_cast<T>(spec.sampleRate)
1159 * preDelayMs / T(1000));
1160 return std::clamp(samp, 0, maxSamp);
1161 }
1162
1163 void updatePreDelay() noexcept
1164 {
1165 if (spec_.sampleRate > 0)
1166 {
1167 // The pre-delay ring buffers hold 500 ms; clamp so an over-range pre-delay
1168 // can't read past the buffer (RingBuffer::read would wrap to a wrong sample).
1170 std::memory_order_relaxed);
1171 }
1172 }
1173
1174 bool commitImpulseResponse(std::vector<T> nextStorage,
1175 int nextLength,
1176 int nextChannels,
1177 double nextSampleRate)
1178 {
1179 if (spec_.sampleRate <= 0 || fftBlockSize_ <= 0)
1180 {
1181 irStorage_.swap(nextStorage);
1182 irLength_ = nextLength;
1183 irChannels_ = nextChannels;
1184 irSampleRate_ = nextSampleRate;
1185 return true;
1186 }
1187
1188 auto candidate = buildBank(nextStorage, nextLength, nextChannels,
1189 nextSampleRate, spec_, fftBlockSize_,
1190 decayScale_.load(std::memory_order_relaxed),
1191 stretch_.load(std::memory_order_relaxed));
1192 const std::uint32_t latency = bankLatency(*candidate);
1193 const auto result = bankPublisher_.publish(
1194 std::move(candidate), latency,
1195 [&]() noexcept {
1196 irStorage_.swap(nextStorage);
1197 irLength_ = nextLength;
1198 irChannels_ = nextChannels;
1199 irSampleRate_ = nextSampleRate;
1200 });
1201 return result == Publisher::PublishResult::published;
1202 }
1203
1204 [[nodiscard]] std::unique_ptr<ConvolverBank> buildBank(
1205 const std::vector<T>& source,
1206 int sourceLength,
1207 int sourceChannels,
1208 double sourceSampleRate,
1209 const AudioSpec& processingSpec,
1210 int fftBlock,
1211 T decayScale,
1212 T stretchRatio) const
1213 {
1214 auto newBank = std::make_unique<ConvolverBank>();
1215 newBank->convolvers.resize(
1216 static_cast<size_t>(processingSpec.numChannels));
1217
1218 // IR shaping controls, always applied to the stored original.
1219 // Stretch works by declaring a scaled source rate and letting the
1220 // resampling stage do the time-scaling (tape-speed semantics).
1221 const double dScale = static_cast<double>(decayScale);
1222 const double stretch = static_cast<double>(stretchRatio);
1223 const bool doShape = std::abs(dScale - 1.0) > 1e-6;
1224 const double effIrRate = sourceSampleRate / std::max(stretch, 0.01);
1225
1226 std::vector<T> shaped; // lazy decay-shaped copy of one IR channel
1227 int shapedCh = -1;
1228
1229 // An IR at another rate (or stretched) is converted offline, time
1230 // aligned, over the whole span the kernel reaches, and scaled by the
1231 // rate ratio: a sampled impulse response scales with the sampling
1232 // period, so a response held at twice the rate carries half the
1233 // weight per sample. The span includes the kernel's ringing BEFORE
1234 // the IR's first sample, which an IR that starts at full level (a
1235 // trimmed recording, a unit impulse) needs: cut off, it takes a
1236 // share of the direct sound's gain with it. Those leading samples
1237 // are kept only where the IR's own leading silence cannot hold
1238 // them, and the dry path is delayed by as many, so dry and wet stay
1239 // aligned and the direct sound falls exactly at getLatency().
1240 const bool convert = std::abs(effIrRate - processingSpec.sampleRate) > 1e-9 * effIrRate;
1241 std::vector<std::vector<T>> converted(static_cast<size_t>(processingSpec.numChannels));
1242 std::vector<int> zeroIndex(static_cast<size_t>(processingSpec.numChannels), 0);
1243 int extra = 0;
1244
1245 for (int ch = 0; ch < processingSpec.numChannels; ++ch)
1246 {
1247 // Pick IR channel: use corresponding channel if available, else mono (ch 0)
1248 const int irCh = (ch < sourceChannels) ? ch : 0;
1249 const T* irData = source.data()
1250 + static_cast<size_t>(irCh)
1251 * static_cast<size_t>(sourceLength);
1252 int irLen = sourceLength;
1253
1254 if (doShape)
1255 {
1256 if (shapedCh != irCh)
1257 {
1258 shaped = shapeDecay(irData, sourceLength, dScale);
1259 shapedCh = irCh;
1260 }
1261 if (!shaped.empty())
1262 {
1263 irData = shaped.data();
1264 irLen = static_cast<int>(shaped.size());
1265 }
1266 }
1267
1268 auto& out = converted[static_cast<size_t>(ch)];
1269 if (!convert)
1270 {
1271 out.assign(irData, irData + irLen);
1272 continue;
1273 }
1274 Resampler<T> resampler;
1275 resampler.prepare(effIrRate, processingSpec.sampleRate, Resampler<T>::Quality::High);
1276 const int64_t reach = resampler.getReach();
1277 const int64_t body = static_cast<int64_t>(
1278 std::ceil(static_cast<double>(irLen) * resampler.getRatio()));
1279 out = resampler.processRange(irData, irLen, -reach, body + 2 * reach);
1280 const T scale = static_cast<T>(effIrRate / processingSpec.sampleRate);
1281 for (auto& v : out) v *= scale;
1282 // Leading samples that are exactly zero carry nothing; the ones
1283 // before the IR's first instant that are not become extra delay.
1284 int first = 0;
1285 while (first < static_cast<int>(out.size()) && out[static_cast<size_t>(first)] == T(0)) ++first;
1286 zeroIndex[static_cast<size_t>(ch)] = static_cast<int>(reach);
1287 extra = std::max(extra, static_cast<int>(reach) - first);
1288 while (!out.empty() && out.back() == T(0)) out.pop_back();
1289 }
1290
1291 for (int ch = 0; ch < processingSpec.numChannels; ++ch)
1292 {
1293 auto& conv = newBank->convolvers[static_cast<size_t>(ch)];
1294 const auto& out = converted[static_cast<size_t>(ch)];
1295 const int start = convert ? zeroIndex[static_cast<size_t>(ch)] - extra : 0;
1296 const int len = static_cast<int>(out.size()) - start;
1297 if (len > 0)
1298 conv.prepare(fftBlock, out.data() + start, len);
1299 else
1300 {
1301 const T silence = T(0);
1302 conv.prepare(fftBlock, &silence, 1);
1303 }
1304 }
1305
1306 if (extra > 0)
1307 {
1308 newBank->extraDelay = extra;
1309 newBank->scratchLength = std::max(1, processingSpec.maxBlockSize);
1310 newBank->dryDelay.resize(static_cast<size_t>(processingSpec.numChannels));
1311 for (auto& ring : newBank->dryDelay) ring.prepare(extra + 1);
1312 newBank->dryScratch.assign(static_cast<size_t>(processingSpec.numChannels)
1313 * static_cast<size_t>(newBank->scratchLength), T(0));
1314 }
1315 return newBank;
1316 }
1317
1318 [[nodiscard]] static std::uint32_t bankLatency(
1319 const ConvolverBank& bank) noexcept
1320 {
1321 if (bank.convolvers.empty()) return 0u;
1322 return static_cast<std::uint32_t>(
1323 std::max(0, bank.convolvers.front().getLatency() + bank.extraDelay));
1324 }
1325
1336 [[nodiscard]] std::vector<T> shapeDecay(const T* ir, int len,
1337 double factor) const
1338 {
1339 if (!ir || len < 64) return {};
1340
1341 // Total energy + direct-sound peak (double accumulation).
1342 double total = 0.0;
1343 double peakMag = 0.0;
1344 int peak = 0;
1345 for (int n = 0; n < len; ++n)
1346 {
1347 const double v = static_cast<double>(ir[n]);
1348 total += v * v;
1349 const double m = std::abs(v);
1350 if (m > peakMag) { peakMag = m; peak = n; }
1351 }
1352 if (total <= 1e-30 || peakMag <= 0.0) return {};
1353
1354 // Schroeder EDC crossings at -5 dB and -25 dB (energy ratios).
1355 constexpr double r5 = 0.31622776601683794; // 10^(-5/10)
1356 constexpr double r25 = 0.0031622776601683794; // 10^(-25/10)
1357 int t5 = -1, t25 = -1;
1358 double tail = total;
1359 for (int n = 0; n < len; ++n)
1360 {
1361 const double ratio = tail / total;
1362 if (t5 < 0 && ratio <= r5 && n > peak) t5 = n;
1363 if (ratio <= r25 && n > peak) { t25 = n; break; }
1364 const double v = static_cast<double>(ir[n]);
1365 tail -= v * v;
1366 }
1367 if (t5 < 0 || t25 < 0 || t25 - t5 < 32) return {}; // no usable slope
1368
1369 // Amplitude decay rate: the EDC drops 20 dB over (t25 - t5) samples,
1370 // so exp(-beta * t) with beta = ln(10) / (t25 - t5).
1371 const double beta = 2.302585092994046 / static_cast<double>(t25 - t5);
1372 const double k = beta * (1.0 / factor - 1.0);
1373
1374 std::vector<T> out(static_cast<size_t>(len));
1375 const double gStep = std::exp(-k);
1376 double g = 1.0;
1377 for (int n = 0; n < len; ++n)
1378 {
1379 out[static_cast<size_t>(n)] = (n <= peak)
1380 ? ir[n]
1381 : static_cast<T>(static_cast<double>(ir[n]) * g);
1382 if (n >= peak) g *= gStep;
1383 }
1384
1385 if (factor < 1.0)
1386 {
1387 // Trim where the shaped energy falls below -100 dB of its total:
1388 // the removed stretch is inaudible, and a shorter IR means fewer
1389 // convolution partitions (the CPU saving the shaping is for).
1390 double sTotal = 0.0;
1391 for (const T v : out) sTotal += static_cast<double>(v) * v;
1392 if (sTotal > 1e-30)
1393 {
1394 double sTail = sTotal;
1395 int cut = len;
1396 for (int n = 0; n < len; ++n)
1397 {
1398 if (sTail / sTotal <= 1e-10) { cut = n; break; }
1399 const double v = static_cast<double>(out[static_cast<size_t>(n)]);
1400 sTail -= v * v;
1401 }
1402 cut = std::max(cut, 64);
1403 if (cut < len) out.resize(static_cast<size_t>(cut));
1404 }
1405 }
1406 else if (factor > 1.0)
1407 {
1408 // A raised envelope would end in a cliff at the IR boundary:
1409 // fade the final stretch (up to 20 ms at 48 kHz) with a raised
1410 // cosine so the lengthened tail closes cleanly.
1411 const int fade = std::min(len / 8, 960);
1412 const int start = len - fade;
1413 for (int i = 0; i < fade; ++i)
1414 {
1415 const double w = 0.5 * (1.0 + std::cos(3.141592653589793
1416 * static_cast<double>(i + 1)
1417 / static_cast<double>(fade)));
1418 const size_t idx = static_cast<size_t>(start + i);
1419 out[idx] = static_cast<T>(static_cast<double>(out[idx]) * w);
1420 }
1421 }
1422 return out;
1423 }
1424
1427 std::atomic<T> mix_ { T(0.3) };
1428 std::atomic<T> preDelayMs_ { T(0) };
1429 std::atomic<int> preDelaySamples_ { 0 };
1430 std::atomic<T> decayScale_ { T(1) };
1431 std::atomic<T> stretch_ { T(1) };
1432
1433 // IR storage (GUI-thread only: rebuild source of truth)
1434 std::vector<T> irStorage_;
1435 int irLength_ = 0;
1437 double irSampleRate_ = 0;
1438
1440 std::vector<RingBuffer<T>> preDelayBuffers_;
1443};
1444
1445} // 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.
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:78
void setLatencyCompensation(int samples)
Delays the captured dry signal to compensate for an effect's internal latency (e.g....
void prepare(const AudioSpec &spec)
Allocates the internal dry buffer for the given audio spec.
Windowed-sinc sample rate converter optimized for real-time DSP.
Definition Resampler.h:86
int64_t getReach() const noexcept
How far, in output samples, the kernel reaches before an input sample and after it: processRange() ov...
Definition Resampler.h:249
double getRatio() const noexcept
Returns the conversion ratio (targetRate / sourceRate).
Definition Resampler.h:312
std::vector< T > processRange(const T *input, int inputLength, int64_t firstOutput, int64_t count)
Offline, time-aligned conversion of any span of output samples.
Definition Resampler.h:208
void prepare(double sourceRate, double targetRate, Quality quality=Quality::Normal)
Prepares the resampler for a given rate conversion.
Definition Resampler.h:108
Convolution reverb with IR loading, dry/wet, and pre-delay.
Definition Reverb.h:590
void processBlock(AudioBufferView< T > buffer) noexcept
Processes audio through the reverb.
Definition Reverb.h:704
static int calculatePreDelaySamples(const AudioSpec &spec, T preDelayMs) noexcept
Definition Reverb.h:1152
DryWetMixer< T > & getMixer()
Direct access to the DryWetMixer (processing thread).
Definition Reverb.h:1046
bool commitImpulseResponse(std::vector< T > nextStorage, int nextLength, int nextChannels, double nextSampleRate)
Definition Reverb.h:1174
~Reverb() noexcept
Definition Reverb.h:615
bool loadIR(AudioBufferView< const T > ir, double irSampleRate)
Loads a multi-channel IR from memory.
Definition Reverb.h:881
T getMix() const noexcept
Returns the current mix value.
Definition Reverb.h:1055
std::atomic< int > preDelaySamples_
Definition Reverb.h:1429
static std::uint32_t bankLatency(const ConvolverBank &bank) noexcept
Definition Reverb.h:1318
bool isLoaded() const noexcept
Returns true if an IR has been loaded and applied.
Definition Reverb.h:1049
Reverb()=default
std::vector< T > irStorage_
Definition Reverb.h:1434
Reverb(const Reverb &)=delete
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:1336
void updatePreDelay() noexcept
Definition Reverb.h:1163
std::atomic< T > stretch_
Definition Reverb.h:1431
Reverb(Reverb &&)=delete
int fftBlockSize_
Convolver partition size = engine latency (set in prepare()).
Definition Reverb.h:1426
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
Definition Reverb.h:1092
int calculatePreDelaySamples(const AudioSpec &spec) const noexcept
Definition Reverb.h:1146
T getPreDelay() const noexcept
Returns the current pre-delay in ms.
Definition Reverb.h:1058
std::atomic< T > decayScale_
Definition Reverb.h:1430
bool loadIR(const char *wavFilePath)
Loads an impulse response from a WAV file.
Definition Reverb.h:799
std::atomic< T > preDelayMs_
Definition Reverb.h:1428
int getLatency() const noexcept
Returns the convolution latency in samples.
Definition Reverb.h:1073
AudioSpec spec_
Definition Reverb.h:1425
bool loadIR(const T *data, int length, double irSampleRate)
Loads an IR from raw sample data.
Definition Reverb.h:856
static constexpr std::uint32_t publisherGenerationMax
Definition Reverb.h:608
Convolver< T > fallbackConvolver_
Inert engine for getConvolver() with no bank.
Definition Reverb.h:1442
void setStretch(T ratio)
Stretches the loaded IR in time (tape-speed style).
Definition Reverb.h:973
void prepare(const AudioSpec &spec)
Prepares the reverb for processing.
Definition Reverb.h:635
Publisher bankPublisher_
Definition Reverb.h:1439
void setMix(T dryWet) noexcept
Sets the dry/wet mix.
Definition Reverb.h:840
std::vector< RingBuffer< T > > preDelayBuffers_
Definition Reverb.h:1440
std::vector< uint8_t > getState() const
Serializes the parameter state. The impulse response itself is content (load it with loadIR),...
Definition Reverb.h:1081
Reverb & operator=(Reverb &&)=delete
DryWetMixer< T > mixer_
Definition Reverb.h:1441
std::atomic< T > mix_
Definition Reverb.h:1427
std::unique_ptr< ConvolverBank > buildBank(const std::vector< T > &source, int sourceLength, int sourceChannels, double sourceSampleRate, const AudioSpec &processingSpec, int fftBlock, T decayScale, T stretchRatio) const
Definition Reverb.h:1204
Reverb & operator=(const Reverb &)=delete
Convolver< T > & getConvolver(int channel=0)
Direct access to a channel's Convolver (GUI thread only).
Definition Reverb.h:1027
void reset() noexcept
Resets the DSP state (convolver tails, pre-delay, mixer). RT-Safe.
Definition Reverb.h:769
T getDecayScale() const noexcept
Returns the current IR decay scale.
Definition Reverb.h:994
void setDecayScale(T scale)
Scales the decay time (T60) of the loaded IR.
Definition Reverb.h:940
double irSampleRate_
Definition Reverb.h:1437
T getStretch() const noexcept
Returns the current IR stretch ratio.
Definition Reverb.h:997
void setPreDelay(T ms) noexcept
Sets the pre-delay time in milliseconds.
Definition Reverb.h:910
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
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
const void * atomicWordAddressForTest(std::size_t index) const noexcept
Definition Reverb.h:390
bool isLoaded() const noexcept
Definition Reverb.h:342
std::uint32_t pinnedTokenForTest() const noexcept
Definition Reverb.h:383
ReverbBankPublisher(const ReverbBankPublisher &)=delete
ReverbBankPublisher(ReverbBankPublisher &&)=delete
ReverbBankPublisher & operator=(ReverbBankPublisher &&)=delete
Bank * adoptAtBoundary() noexcept
Definition Reverb.h:269
static constexpr std::uint32_t generationForTest(std::uint32_t state) noexcept
Definition Reverb.h:407
std::uint32_t pendingTokenForTest() const noexcept
Definition Reverb.h:377
static constexpr Phase phaseForTest(std::uint32_t state) noexcept
Definition Reverb.h:403
std::uint32_t stateWordForTest(std::size_t index) const noexcept
Definition Reverb.h:373
ReverbBankPublisher & operator=(const ReverbBankPublisher &)=delete
std::size_t residentBanksForTest() const noexcept
Definition Reverb.h:384
std::uint32_t latestTokenForTest() const noexcept
Definition Reverb.h:382
int latency() const noexcept
Definition Reverb.h:348
PublishResult publish(std::unique_ptr< Bank > candidate, std::uint32_t latency, Commit &&commit) noexcept
Definition Reverb.h:160
std::uint32_t activeTokenForTest() const noexcept
Definition Reverb.h:381
static constexpr std::size_t atomicWordCountForTest() noexcept
Definition Reverb.h:399
ReverbBankPublisher() noexcept=default
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: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
std::vector< RingBuffer< T > > dryDelay
Per channel, extraDelay deep.
Definition Reverb.h:599
std::vector< Convolver< T > > convolvers
Definition Reverb.h:594
std::vector< T > dryScratch
Channel-major, one block per channel.
Definition Reverb.h:600
static constexpr void audioAfterExchange(bool) noexcept
Definition Reverb.h:108
static constexpr void controlScan() noexcept
Definition Reverb.h:112
static constexpr void audioExchange() noexcept
Definition Reverb.h:107
static constexpr void audioStateStore() noexcept
Definition Reverb.h:109
static constexpr void control(ControlPoint) noexcept
Definition Reverb.h:106
static constexpr void controlValidation(bool) noexcept
Definition Reverb.h:111
static constexpr void audioValidation(bool) noexcept
Definition Reverb.h:110