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"
122template <
typename Bank,
123 typename Hooks = ReverbPublisherNoopHooks,
124 std::uint32_t GenerationMax = 134217727u>
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");
159 template <
typename Commit>
162 Commit&& commit)
noexcept
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);
170 SlotChoice choice = scanOnce();
171 bool claimedPending =
false;
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)
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)
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)
195 Hooks::controlValidation(reusable);
202 pendingToken_.store(old, std::memory_order_release);
205 choice = { index, generation + 1u,
true,
false };
206 claimedPending =
true;
220 Hooks::control(Hooks::ControlPoint::noCapacity);
224 Hooks::control(Hooks::ControlPoint::afterSlotSelection);
225 auto& slot = slots_[choice.index];
228 const std::uint32_t claimedGeneration = choice.generation - 1u;
229 const bool retired = retireControlOwned(
230 makeToken(claimedGeneration, choice.index));
234 nextGeneration_[choice.index] = choice.generation;
236 std::memory_order_relaxed);
237 if (choice.reclaimsRetired)
238 Hooks::control(Hooks::ControlPoint::beforeOldReclaim);
239 slot.bank = std::move(candidate);
241 std::memory_order_release);
242 Hooks::control(Hooks::ControlPoint::afterPublishedStore);
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))
254 assert(
false &&
"Reverb publication ownership invariant");
257 Hooks::control(Hooks::ControlPoint::beforeCommit);
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);
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)
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)
282 Hooks::audioValidation(nextValid);
286 slots_[nextIndex].stateWord.store(
288 std::memory_order_release);
289 Hooks::audioStateStore();
291 const std::uint32_t old = activeToken_;
293 if (old != emptyToken)
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)
300 Hooks::audioValidation(oldValid);
304 slots_[oldIndex].stateWord.store(
306 std::memory_order_release);
307 Hooks::audioStateStore();
313 return activeToken_ == emptyToken
315 : slots_[tokenIndex(activeToken_)].bank.get();
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);
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)
335 Hooks::controlValidation(valid);
337 if (!valid)
return nullptr;
338 pinnedToken_ = latest;
339 return slots_[index].bank.get();
344 return (publicationMetadata_.load(std::memory_order_acquire)
350 return static_cast<int>(publicationMetadata_.load(
351 std::memory_order_acquire) & latencyMask);
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_)
366 std::memory_order_relaxed);
368 nextGeneration_.fill(0u);
375 return slots_[index].stateWord.load(std::memory_order_acquire);
379 return pendingToken_.load(std::memory_order_acquire);
386 std::size_t count = 0;
387 for (
const auto& slot : slots_) count += slot.bank !=
nullptr ? 1u : 0u;
391 std::size_t index)
const noexcept
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_);
405 return statePhase(state);
408 std::uint32_t state)
noexcept
410 return stateGeneration(state);
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;
422 std::unique_ptr<Bank> bank;
423 std::atomic<std::uint32_t> stateWord { 0u };
428 std::size_t index = 0;
429 std::uint32_t generation = 0;
431 bool reclaimsRetired =
false;
434 [[nodiscard]]
static constexpr std::uint32_t makeToken(
435 std::uint32_t generation, std::size_t index)
noexcept
437 return (generation << 2u) |
static_cast<std::uint32_t
>(index);
440 [[nodiscard]]
static constexpr std::size_t tokenIndex(
441 std::uint32_t token)
noexcept
443 return static_cast<std::size_t
>(token & slotMask);
446 [[nodiscard]]
static constexpr std::uint32_t tokenGeneration(
447 std::uint32_t token)
noexcept
452 [[nodiscard]]
static constexpr std::uint32_t encodeState(
453 std::uint32_t generation,
Phase phase)
noexcept
455 return (generation << 3u) |
static_cast<std::uint32_t
>(phase);
458 [[nodiscard]]
static constexpr std::uint32_t stateGeneration(
459 std::uint32_t state)
noexcept
464 [[nodiscard]]
static constexpr Phase statePhase(std::uint32_t state)
noexcept
466 return static_cast<Phase>(state & phaseMask);
469 [[nodiscard]]
static constexpr std::uint32_t packMetadata(
470 std::uint32_t
latency)
noexcept
472 return loadedMask | std::min(
latency, latencyMask);
475 [[nodiscard]] SlotChoice scanOnce() noexcept
477 for (std::size_t offset = 0; offset < slots_.size(); ++offset)
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);
490 const bool pristine = generation == 0u
491 && nextGeneration_[index] == 0u && slot.bank ==
nullptr;
492 Hooks::controlValidation(pristine);
494 if (pristine)
return { index, 1u,
true,
false };
501 const bool generationMatches =
502 nextGeneration_[index] == generation;
503 Hooks::controlValidation(generationMatches);
504 assert(generationMatches);
505 if (!generationMatches)
continue;
507 if (generation >= GenerationMax)
continue;
508 return { index, generation + 1u,
true,
true };
513 bool retireControlOwned(std::uint32_t token)
noexcept
515 const std::size_t index = tokenIndex(token);
516 const std::uint32_t generation = tokenGeneration(token);
517 auto& slot = slots_[index];
519 slot.stateWord.load(std::memory_order_acquire)
521 Hooks::controlValidation(valid);
523 if (!valid)
return false;
525 Hooks::control(Hooks::ControlPoint::beforeOldRetirement);
527 std::memory_order_release);
528 if (token != pinnedToken_)
530 Hooks::control(Hooks::ControlPoint::beforeOldReclaim);
536 void quarantineExhaustedRetired() noexcept
538 for (std::size_t index = 0; index < slots_.size(); ++index)
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);
545 || generation < GenerationMax
546 || makeToken(generation, index) == pinnedToken_)
548 Hooks::control(Hooks::ControlPoint::beforeOldReclaim);
551 std::memory_order_release);
555 void reclaimIfRetired(std::uint32_t token)
noexcept
557 auto& slot = slots_[tokenIndex(token)];
558 const std::uint32_t state =
559 slot.stateWord.load(std::memory_order_acquire);
562 Hooks::control(Hooks::ControlPoint::beforeOldReclaim);
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;
571 std::uint32_t latestToken_ = emptyToken;
572 std::uint32_t pinnedToken_ = emptyToken;
573 std::array<std::uint32_t, 4> nextGeneration_ {};
574 std::size_t scanStart_ = 0;
588template <FloatType T>
604#if defined(DSPARK_REVERB_TEST_GENERATION_MAX)
606 DSPARK_REVERB_TEST_GENERATION_MAX;
643 const int blockSize = std::clamp(spec.
maxBlockSize, 1, 1 << 20);
645 while (fftBlock < blockSize) fftBlock <<= 1;
654 const int maxDelaySamples =
static_cast<int>(spec.
sampleRate * 0.5) + 1;
655 std::vector<RingBuffer<T>> nextPreDelayBuffers(
657 for (
auto& rb : nextPreDelayBuffers)
658 rb.prepare(maxDelaySamples);
665 mixer_ = std::move(nextMixer);
668 std::memory_order_relaxed);
675 stretch_.load(std::memory_order_relaxed));
676 const std::uint32_t latency =
bankLatency(*candidate);
678 std::move(candidate), latency,
682 mixer_ = std::move(nextMixer);
685 std::memory_order_relaxed);
707 if (!bank || bank->
convolvers.empty())
return;
709 const int nCh = std::min(buffer.getNumChannels(),
711 const int nS = buffer.getNumSamples();
713 if (bank->
extraDelay > 0 && nS <= bank->scratchLength
714 && buffer.getNumChannels() <=
static_cast<int>(bank->
dryDelay.size()))
717 const int dCh = buffer.getNumChannels();
718 std::array<T*, 64> ptrs {};
719 for (
int ch = 0; ch < dCh && ch < 64; ++ch)
721 auto& ring = bank->
dryDelay[
static_cast<size_t>(ch)];
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)
730 ptrs[
static_cast<size_t>(ch)] = dst;
740 T mixVal =
mix_.load(std::memory_order_relaxed);
742 for (
int ch = 0; ch < nCh; ++ch)
744 T* data = buffer.getChannel(ch);
749 for (
int i = 0; i < nS; ++i)
752 data[i] = ring.read(preDelSamp);
756 bank->
convolvers[
static_cast<size_t>(ch)].processInPlace(data, nS);
759 mixer_.mixWet(buffer, mixVal);
775 for (
auto& conv : bank->convolvers)
777 for (
auto& ring : bank->dryDelay)
787#ifndef DSPARK_NO_FILE_IO
806 if (info.numSamples <= 0 || info.numChannels <= 0
807 || info.numSamples > (
static_cast<int64_t
>(1) << 30)
808 || !(info.sampleRate > 0))
815 irBuf.
resize(info.numChannels,
static_cast<int>(info.numSamples));
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)
826 T* dst = nextStorage.data()
827 +
static_cast<size_t>(ch) *
static_cast<size_t>(nextLength);
828 std::copy_n(src, nextLength, dst);
831 nextChannels, info.sampleRate);
842 if (!std::isfinite(dryWet))
return;
843 mix_.store(std::clamp(dryWet, T(0), T(1)), std::memory_order_relaxed);
856 bool loadIR(
const T* data,
int length,
double irSampleRate)
858 if (data ==
nullptr || length <= 0
859 || !std::isfinite(irSampleRate) || !(irSampleRate > 0.0))
862 std::vector<T> nextStorage(data, data + length);
885 if (nextChannels <= 0 || nextLength <= 0
886 || !std::isfinite(irSampleRate) || !(irSampleRate > 0.0))
888 for (
int ch = 0; ch < nextChannels; ++ch)
889 if (ir.
getChannel(ch) ==
nullptr)
return false;
891 std::vector<T> nextStorage(
892 static_cast<size_t>(nextChannels) *
static_cast<size_t>(nextLength));
893 for (
int ch = 0; ch < nextChannels; ++ch)
895 nextStorage.data() +
static_cast<size_t>(ch) *
static_cast<size_t>(nextLength));
897 nextChannels, irSampleRate);
912 if (!std::isfinite(ms))
return;
913 preDelayMs_.store(std::clamp(ms, T(0), T(500)), std::memory_order_relaxed);
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;
947 decayScale_.store(next, std::memory_order_relaxed);
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); });
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;
980 stretch_.store(next, std::memory_order_relaxed);
987 const std::uint32_t latency =
bankLatency(*candidate);
989 std::move(candidate), latency,
990 [&]()
noexcept {
stretch_.store(next, std::memory_order_relaxed); });
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)];
1055 [[nodiscard]] T
getMix() const noexcept {
return mix_.load(std::memory_order_relaxed); }
1084 w.
write(
"mix",
mix_.load(std::memory_order_relaxed));
1087 w.
write(
"stretch",
stretch_.load(std::memory_order_relaxed));
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));
1114 const bool shapeChanged =
1116 || st !=
stretch_.load(std::memory_order_relaxed);
1118 const auto commitParameters = [&]()
noexcept {
1119 mix_.store(mix, std::memory_order_relaxed);
1120 preDelayMs_.store(preDelay, std::memory_order_relaxed);
1123 stretch_.store(st, std::memory_order_relaxed);
1138 const std::uint32_t latency =
bankLatency(*candidate);
1140 std::move(candidate), latency, commitParameters);
1141 if (result != Publisher::PublishResult::published)
return false;
1149 spec,
preDelayMs_.load(std::memory_order_relaxed));
1153 const AudioSpec& spec, T preDelayMs)
noexcept
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);
1170 std::memory_order_relaxed);
1177 double nextSampleRate)
1188 auto candidate =
buildBank(nextStorage, nextLength, nextChannels,
1191 stretch_.load(std::memory_order_relaxed));
1192 const std::uint32_t latency =
bankLatency(*candidate);
1194 std::move(candidate), latency,
1201 return result == Publisher::PublishResult::published;
1205 const std::vector<T>& source,
1208 double sourceSampleRate,
1212 T stretchRatio)
const
1214 auto newBank = std::make_unique<ConvolverBank>();
1215 newBank->convolvers.resize(
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);
1226 std::vector<T> shaped;
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);
1245 for (
int ch = 0; ch < processingSpec.
numChannels; ++ch)
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;
1256 if (shapedCh != irCh)
1258 shaped =
shapeDecay(irData, sourceLength, dScale);
1261 if (!shaped.empty())
1263 irData = shaped.data();
1264 irLen =
static_cast<int>(shaped.size());
1268 auto& out = converted[
static_cast<size_t>(ch)];
1271 out.assign(irData, irData + irLen);
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;
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();
1291 for (
int ch = 0; ch < processingSpec.
numChannels; ++ch)
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;
1298 conv.prepare(fftBlock, out.data() + start, len);
1301 const T silence = T(0);
1302 conv.prepare(fftBlock, &silence, 1);
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));
1321 if (bank.convolvers.empty())
return 0u;
1322 return static_cast<std::uint32_t
>(
1323 std::max(0, bank.convolvers.front().getLatency() + bank.extraDelay));
1337 double factor)
const
1339 if (!ir || len < 64)
return {};
1343 double peakMag = 0.0;
1345 for (
int n = 0; n < len; ++n)
1347 const double v =
static_cast<double>(ir[n]);
1349 const double m = std::abs(v);
1350 if (m > peakMag) { peakMag = m; peak = n; }
1352 if (total <= 1e-30 || peakMag <= 0.0)
return {};
1355 constexpr double r5 = 0.31622776601683794;
1356 constexpr double r25 = 0.0031622776601683794;
1357 int t5 = -1, t25 = -1;
1358 double tail = total;
1359 for (
int n = 0; n < len; ++n)
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]);
1367 if (t5 < 0 || t25 < 0 || t25 - t5 < 32)
return {};
1371 const double beta = 2.302585092994046 /
static_cast<double>(t25 - t5);
1372 const double k = beta * (1.0 / factor - 1.0);
1374 std::vector<T> out(
static_cast<size_t>(len));
1375 const double gStep = std::exp(-k);
1377 for (
int n = 0; n < len; ++n)
1379 out[
static_cast<size_t>(n)] = (n <= peak)
1381 :
static_cast<T
>(
static_cast<double>(ir[n]) * g);
1382 if (n >= peak) g *= gStep;
1390 double sTotal = 0.0;
1391 for (
const T v : out) sTotal +=
static_cast<double>(v) * v;
1394 double sTail = sTotal;
1396 for (
int n = 0; n < len; ++n)
1398 if (sTail / sTotal <= 1e-10) { cut = n;
break; }
1399 const double v =
static_cast<double>(out[
static_cast<size_t>(n)]);
1402 cut = std::max(cut, 64);
1403 if (cut < len) out.resize(
static_cast<size_t>(cut));
1406 else if (factor > 1.0)
1411 const int fade = std::min(len / 8, 960);
1412 const int start = len - fade;
1413 for (
int i = 0; i < fade; ++i)
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);
Non-owning view over audio channel data.
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.
Pre-allocated, SIMD-friendly dry/wet blender for real-time audio.
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.
int64_t getReach() const noexcept
How far, in output samples, the kernel reaches before an input sample and after it: processRange() ov...
double getRatio() const noexcept
Returns the conversion ratio (targetRate / sourceRate).
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.
void prepare(double sourceRate, double targetRate, Quality quality=Quality::Normal)
Prepares the resampler for a given rate conversion.
Convolution reverb with IR loading, dry/wet, and pre-delay.
void processBlock(AudioBufferView< T > buffer) noexcept
Processes audio through the reverb.
static int calculatePreDelaySamples(const AudioSpec &spec, T preDelayMs) noexcept
DryWetMixer< T > & getMixer()
Direct access to the DryWetMixer (processing thread).
bool commitImpulseResponse(std::vector< T > nextStorage, int nextLength, int nextChannels, double nextSampleRate)
bool loadIR(AudioBufferView< const T > ir, double irSampleRate)
Loads a multi-channel IR from memory.
T getMix() const noexcept
Returns the current mix value.
std::atomic< int > preDelaySamples_
static std::uint32_t bankLatency(const ConvolverBank &bank) noexcept
bool isLoaded() const noexcept
Returns true if an IR has been loaded and applied.
std::vector< T > irStorage_
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).
void updatePreDelay() noexcept
std::atomic< T > stretch_
int fftBlockSize_
Convolver partition size = engine latency (set in prepare()).
bool setState(const uint8_t *data, size_t size)
Restores parameters from a blob (tolerant; rejects foreign ids).
int calculatePreDelaySamples(const AudioSpec &spec) const noexcept
T getPreDelay() const noexcept
Returns the current pre-delay in ms.
std::atomic< T > decayScale_
bool loadIR(const char *wavFilePath)
Loads an impulse response from a WAV file.
std::atomic< T > preDelayMs_
int getLatency() const noexcept
Returns the convolution latency in samples.
bool loadIR(const T *data, int length, double irSampleRate)
Loads an IR from raw sample data.
static constexpr std::uint32_t publisherGenerationMax
Convolver< T > fallbackConvolver_
Inert engine for getConvolver() with no bank.
void setStretch(T ratio)
Stretches the loaded IR in time (tape-speed style).
void prepare(const AudioSpec &spec)
Prepares the reverb for processing.
void setMix(T dryWet) noexcept
Sets the dry/wet mix.
std::vector< RingBuffer< T > > preDelayBuffers_
std::vector< uint8_t > getState() const
Serializes the parameter state. The impulse response itself is content (load it with loadIR),...
Reverb & operator=(Reverb &&)=delete
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
Reverb & operator=(const Reverb &)=delete
Convolver< T > & getConvolver(int channel=0)
Direct access to a channel's Convolver (GUI thread only).
void reset() noexcept
Resets the DSP state (convolver tails, pre-delay, mixer). RT-Safe.
T getDecayScale() const noexcept
Returns the current IR decay scale.
void setDecayScale(T scale)
Scales the decay time (T60) of the loaded IR.
T getStretch() const noexcept
Returns the current IR stretch ratio.
void setPreDelay(T ms) noexcept
Sets the pre-delay time in milliseconds.
Tolerant reader: missing keys yield defaults, unknown keys are skipped.
float read(const char *key, float defaultValue) const
Reads a float, or defaultValue when the key is absent.
bool isValid() const noexcept
uint32_t processorId() const noexcept
Serializes key/value parameters into a versioned blob.
std::vector< uint8_t > blob() const
Finalizes and returns the blob.
void write(const char *key, float value)
Writes a float parameter.
Complete WAV file reader and writer in pure C++20.
AudioFileInfo getInfo() const override
Retrieves metadata of the currently opened file.
bool readSamples(AudioBufferView< float > dest) override
Reads samples from the start of the file into the destination view.
bool openRead(const std::filesystem::path &path) override
Opens a WAV file for reading.
void close() override
Finalizes file headers and releases system handles.
const void * atomicWordAddressForTest(std::size_t index) const noexcept
bool isLoaded() const noexcept
Bank * pinLatest() noexcept
std::uint32_t pinnedTokenForTest() const noexcept
ReverbBankPublisher(const ReverbBankPublisher &)=delete
ReverbBankPublisher(ReverbBankPublisher &&)=delete
ReverbBankPublisher & operator=(ReverbBankPublisher &&)=delete
Bank * adoptAtBoundary() noexcept
static constexpr std::uint32_t generationForTest(std::uint32_t state) noexcept
std::uint32_t pendingTokenForTest() const noexcept
static constexpr Phase phaseForTest(std::uint32_t state) noexcept
std::uint32_t stateWordForTest(std::size_t index) const noexcept
ReverbBankPublisher & operator=(const ReverbBankPublisher &)=delete
std::size_t residentBanksForTest() const noexcept
std::uint32_t latestTokenForTest() const noexcept
int latency() const noexcept
PublishResult publish(std::unique_ptr< Bank > candidate, std::uint32_t latency, Commit &&commit) noexcept
std::uint32_t activeTokenForTest() const noexcept
static constexpr std::size_t atomicWordCountForTest() noexcept
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").
Describes the audio environment for a DSP processor.
constexpr bool isValid() const noexcept
Checks if the specification contains valid, processable parameters.
int numChannels
Number of audio channels (e.g., 1 = mono, 2 = stereo).
int maxBlockSize
Maximum number of samples per processing block.
double sampleRate
Sample rate in Hz.
std::vector< RingBuffer< T > > dryDelay
Per channel, extraDelay deep.
std::vector< Convolver< T > > convolvers
std::vector< T > dryScratch
Channel-major, one block per channel.
static constexpr void audioAfterExchange(bool) noexcept
static constexpr void controlScan() noexcept
static constexpr void audioExchange() noexcept
static constexpr void audioStateStore() noexcept
static constexpr void control(ControlPoint) noexcept
static constexpr void controlValidation(bool) noexcept
static constexpr void audioValidation(bool) noexcept