92 [[nodiscard]]
explicit operator bool() const noexcept
104 const std::int64_t sourceLength =
end -
start;
106 if (fade > sourceLength / 2)
108 return sourceLength - fade;
123 std::int64_t minLoopLength,
124 std::int64_t maxLoopLength,
125 const Settings& settings = {})
const
129 if (channels == 0 || sampleCount == 0)
132 if (channels < 1 || channels > kMaxChannels)
134 for (
int ch = 0; ch < channels; ++ch)
142 if (!validSettings(settings)
144 || (startRange.
last >=
static_cast<std::int64_t
>(sampleCount))
146 || (endRange.
last >
static_cast<std::int64_t
>(sampleCount))
147 || (minLoopLength < 1) || (maxLoopLength < minLoopLength)
148 || (maxLoopLength >
static_cast<std::int64_t
>(sampleCount)))
151 const std::int64_t window = settings.comparisonLength;
152 const std::int64_t fade = settings.crossfadeLength;
153 if (window == std::numeric_limits<std::int64_t>::max()
154 || fade > std::numeric_limits<std::int64_t>::max() / 2)
156 const std::int64_t minimumPeriod =
157 std::max(window + 1, fade * 2);
159 if (sampleCount > kMaxInputSamples)
162 long double sourceScale = 0.0L;
163 for (
int ch = 0; ch < channels; ++ch)
165 const T*
const samples = audio.
getChannel(ch);
166 for (
int n = 0; n < sampleCount; ++n)
168 if (!std::isfinite(samples[n]))
170 sourceScale = std::max(sourceScale,
171 std::abs(
static_cast<long double>(samples[n])));
175 if (sampleCount < minimumPeriod)
178 bool sourceHasActivity =
false;
179 long double sourceVarianceSum = 0.0L;
180 int sourceActiveChannels = 0;
181 if (sourceScale > 0.0L)
183 for (
int ch = 0; ch < channels; ++ch)
185 const T*
const samples = audio.
getChannel(ch);
186 long double mean = 0.0L;
187 for (
int n = 0; n < sampleCount; ++n)
188 mean +=
static_cast<long double>(samples[n]) / sourceScale;
189 mean /=
static_cast<long double>(sampleCount);
191 long double variance = 0.0L;
192 for (
int n = 0; n < sampleCount; ++n)
194 const long double centered =
195 static_cast<long double>(samples[n]) / sourceScale - mean;
196 variance += centered * centered;
198 variance /=
static_cast<long double>(sampleCount);
199 variance = std::clamp(variance, 0.0L, 1.0L);
200 const long double rms = sourceScale * std::sqrt(variance);
201 if (!std::isfinite(variance) || !std::isfinite(rms))
203 if (rms >=
static_cast<long double>(settings.minimumActivityRms))
205 sourceHasActivity =
true;
206 sourceVarianceSum += variance;
207 ++sourceActiveChannels;
211 if (!sourceHasActivity)
214 const long double sourceRms = sourceScale * std::sqrt(std::min(
215 1.0L, sourceVarianceSum
216 /
static_cast<long double>(sourceActiveChannels)));
217 if (!std::isfinite(sourceRms))
220 const std::uint64_t startCount = latticeCount(startRange, settings.coarseStride);
221 const std::uint64_t endCount = latticeCount(endRange, settings.coarseStride);
222 std::uint64_t coarseUpper = 0;
223 if (!checkedMultiply(startCount, endCount, coarseUpper))
226 const std::uint64_t seedUpper = std::min<std::uint64_t>(8u, coarseUpper);
227 const std::uint64_t radiusSpan =
228 static_cast<std::uint64_t
>(settings.refinementRadius) * 2u + 1u;
229 const std::uint64_t startSpan = std::min(
230 static_cast<std::uint64_t
>(startRange.
last - startRange.
first) + 1u,
232 const std::uint64_t endSpan = std::min(
233 static_cast<std::uint64_t
>(endRange.
last - endRange.
first) + 1u,
235 std::uint64_t refinementUpper = 0;
236 std::uint64_t upperRectangle = 0;
237 if (!checkedMultiply(startSpan, endSpan, upperRectangle)
238 || !checkedMultiply(seedUpper, upperRectangle, refinementUpper)
239 || coarseUpper > settings.maxCoarsePairs
240 || refinementUpper > settings.maxRefinementPairs)
243 ScoringScratch scratch(settings.comparisonLength);
244 std::array<Candidate, 8> seeds {};
247 bool hasBest =
false;
249 auto geometricallyValid = [&](std::int64_t start,
250 std::int64_t end)
noexcept {
251 if (start < 0 || end <= start
252 || end >
static_cast<std::int64_t
>(sampleCount))
254 const std::int64_t period = end - start;
255 return period >= minimumPeriod
256 && period >= minLoopLength && period <= maxLoopLength;
259 auto consider = [&](std::int64_t start, std::int64_t end,
261 if (!geometricallyValid(start, end))
return ScoreState::Inactive;
262 Candidate candidate { start, end, T(1) };
263 const ScoreState state = scoreCandidate(audio, start, end,
266 if (state != ScoreState::Scored)
return state;
267 if (!hasBest || better(candidate, best))
272 if (keepSeed) insertSeed(seeds, seedCount, candidate);
273 return ScoreState::Scored;
276 for (std::uint64_t si = 0; si < startCount; ++si)
278 const std::int64_t start = startRange.
first
279 +
static_cast<std::int64_t
>(si)
280 *
static_cast<std::int64_t
>(settings.coarseStride);
281 for (std::uint64_t ei = 0; ei < endCount; ++ei)
283 const std::int64_t end = endRange.
first
284 +
static_cast<std::int64_t
>(ei)
285 *
static_cast<std::int64_t
>(settings.coarseStride);
286 if (consider(start, end,
true) == ScoreState::NumericalFailure)
291 std::uint64_t exactRefinementAttempts = 0;
292 for (
int seed = 0; seed < seedCount; ++seed)
294 const std::int64_t lowStart = std::max(startRange.
first,
295 seeds[
static_cast<std::size_t
>(seed)].start
296 - settings.refinementRadius);
297 const std::int64_t highStart = std::min(startRange.
last,
298 seeds[
static_cast<std::size_t
>(seed)].start
299 + settings.refinementRadius);
300 const std::int64_t lowEnd = std::max(endRange.
first,
301 seeds[
static_cast<std::size_t
>(seed)].end
302 - settings.refinementRadius);
303 const std::int64_t highEnd = std::min(endRange.
last,
304 seeds[
static_cast<std::size_t
>(seed)].end
305 + settings.refinementRadius);
306 const std::uint64_t widthStart =
307 static_cast<std::uint64_t
>(highStart - lowStart) + 1u;
308 const std::uint64_t widthEnd =
309 static_cast<std::uint64_t
>(highEnd - lowEnd) + 1u;
310 std::uint64_t rectangle = 0;
311 if (!checkedMultiply(widthStart, widthEnd, rectangle)
312 || exactRefinementAttempts > refinementUpper
313 || rectangle > refinementUpper - exactRefinementAttempts)
315 exactRefinementAttempts += rectangle;
317 if (exactRefinementAttempts > settings.maxRefinementPairs)
320 for (
int seed = 0; seed < seedCount; ++seed)
322 const std::int64_t lowStart = std::max(startRange.
first,
323 seeds[
static_cast<std::size_t
>(seed)].start
324 - settings.refinementRadius);
325 const std::int64_t highStart = std::min(startRange.
last,
326 seeds[
static_cast<std::size_t
>(seed)].start
327 + settings.refinementRadius);
328 const std::int64_t lowEnd = std::max(endRange.
first,
329 seeds[
static_cast<std::size_t
>(seed)].end
330 - settings.refinementRadius);
331 const std::int64_t highEnd = std::min(endRange.
last,
332 seeds[
static_cast<std::size_t
>(seed)].end
333 + settings.refinementRadius);
334 for (std::int64_t start = lowStart; start <= highStart; ++start)
336 for (std::int64_t end = lowEnd; end <= highEnd; ++end)
338 if (consider(start, end,
false)
339 == ScoreState::NumericalFailure)
345 if (!hasBest || best.cost > settings.maximumCost)
348 settings.crossfadeLength, best.cost };
365 || (channels > kMaxChannels) || (sampleCount <= 0)
366 || (loop.start < 0) || (loop.end <= loop.start)
367 || (loop.end >
static_cast<std::int64_t
>(sampleCount))
368 || (loop.crossfadeLength < 2)
369 || !std::isfinite(loop.seamCost) || (loop.seamCost < T(0))
370 || (loop.seamCost > T(1)))
372 for (
int ch = 0; ch < channels; ++ch)
376 const std::int64_t period = loop.end - loop.start;
377 const std::int64_t fade = loop.crossfadeLength;
378 if (fade > period / 2)
380 const std::int64_t rendered = period - fade;
381 if (rendered <= 0 || rendered > std::numeric_limits<int>::max())
384 if (output.getNumChannels() != channels
385 || output.getNumSamples() !=
static_cast<int>(rendered))
387 for (
int ch = 0; ch < channels; ++ch)
388 if (output.getChannel(ch) ==
nullptr)
391 const std::size_t sourceBytes =
static_cast<std::size_t
>(sampleCount)
393 const std::size_t outputBytes =
static_cast<std::size_t
>(rendered)
395 for (
int outChannel = 0; outChannel < channels; ++outChannel)
397 const T*
const destination = output.getChannel(outChannel);
398 for (
int sourceChannel = 0; sourceChannel < channels; ++sourceChannel)
399 if (rangesOverlap(audio.
getChannel(sourceChannel), sourceBytes,
400 destination, outputBytes))
402 for (
int other = 0; other < outChannel; ++other)
403 if (rangesOverlap(output.getChannel(other), outputBytes,
404 destination, outputBytes))
408 for (
int ch = 0; ch < channels; ++ch)
411 for (
int n = 0; n < sampleCount; ++n)
412 if (!std::isfinite(source[n]))
414 if (!dryRenderChannel(source, loop))
418 for (
int ch = 0; ch < channels; ++ch)
421 T*
const destination = output.getChannel(ch);
422 std::copy_n(source +
static_cast<std::ptrdiff_t
>(loop.start),
423 static_cast<std::size_t
>(rendered), destination);
426 for (
int i = 0; i < loop.crossfadeLength; ++i)
427 destination[i] = renderOverlapSample(source, loop, i, crossfade);
433 static constexpr int kMaxChannels = 16;
434 static constexpr int kMaxInputSamples = 1 << 26;
436 enum class ScoreState : std::uint8_t { Inactive, Scored,
NumericalFailure };
439 std::int64_t start = 0;
440 std::int64_t end = 0;
444 struct ScoringScratch {
445 explicit ScoringScratch(
int windowSize)
446 : fft(static_cast<std::size_t>(windowSize)),
447 window(static_cast<std::size_t>(windowSize)),
448 timeA(static_cast<std::size_t>(windowSize)),
449 timeB(static_cast<std::size_t>(windowSize)),
450 frequencyA(static_cast<std::size_t>(windowSize + 2)),
451 frequencyB(static_cast<std::size_t>(windowSize + 2))
453 for (
int i = 0; i < windowSize; ++i)
455 window[
static_cast<std::size_t
>(i)] = T(0.5)
456 - T(0.5) * std::cos(T(2) * std::numbers::pi_v<T>
457 *
static_cast<T
>(i) /
static_cast<T
>(windowSize));
462 std::vector<T> window;
463 std::vector<T> timeA;
464 std::vector<T> timeB;
465 std::vector<T> frequencyA;
466 std::vector<T> frequencyB;
467 std::array<std::pair<long double, long double>, kMaxChannels> channelCosts {};
470 [[nodiscard]]
static Result failure(
Status status)
noexcept
472 return { status, 0, 0, 0, T(1) };
475 [[nodiscard]]
static bool validSettings(
const Settings& settings)
noexcept
477 const int window = settings.comparisonLength;
478 return window >= 64 && window <= 4096
479 && (window & (window - 1)) == 0
480 && settings.crossfadeLength >= 2
481 && settings.crossfadeLength <= window
482 && settings.coarseStride >= 1 && settings.coarseStride <= 4096
483 && settings.refinementRadius >= 0
484 && settings.refinementRadius <= 4096
485 && settings.maxCoarsePairs >= 1
486 && settings.maxCoarsePairs <= 1048576u
487 && settings.maxRefinementPairs >= 1
488 && settings.maxRefinementPairs <= 1048576u
489 && std::isfinite(settings.maximumCost)
490 && settings.maximumCost >= T(0) && settings.maximumCost <= T(1)
491 && std::isfinite(settings.minimumActivityRms)
492 && settings.minimumActivityRms > T(0)
493 && settings.minimumActivityRms <= T(1);
496 [[nodiscard]]
static std::uint64_t latticeCount(Range range,
499 return static_cast<std::uint64_t
>(range.last - range.first)
500 /
static_cast<std::uint64_t
>(stride) + 1u;
503 [[nodiscard]]
static bool checkedMultiply(std::uint64_t a,
505 std::uint64_t& result)
noexcept
507 if (a != 0u && b > std::numeric_limits<std::uint64_t>::max() / a)
513 [[nodiscard]]
static bool better(
const Candidate& lhs,
514 const Candidate& rhs)
noexcept
516 return lhs.cost < rhs.cost
517 || (lhs.cost == rhs.cost
518 && (lhs.start < rhs.start
519 || (lhs.start == rhs.start && lhs.end < rhs.end)));
522 static void insertSeed(std::array<Candidate, 8>& seeds,
524 Candidate candidate)
noexcept
527 while (position < count
528 && !better(candidate, seeds[
static_cast<std::size_t
>(position)]))
530 if (position >= 8)
return;
531 const int newCount = std::min(8, count + 1);
532 for (
int i = newCount - 1; i > position; --i)
533 seeds[
static_cast<std::size_t
>(i)] =
534 seeds[
static_cast<std::size_t
>(i - 1)];
535 seeds[
static_cast<std::size_t
>(position)] = candidate;
539 [[nodiscard]]
static ScoreState scoreCandidate(
540 AudioBufferView<const T> audio,
543 const Settings& settings,
544 ScoringScratch& scratch,
545 T& outputCost)
noexcept
548 const int window = settings.comparisonLength;
549 long double scale = 0.0L;
550 for (
int ch = 0; ch < channels; ++ch)
553 for (
int i = 0; i < window; ++i)
555 scale = std::max(scale, std::abs(
static_cast<long double>(
556 source[end - window + i])));
557 scale = std::max(scale, std::abs(
static_cast<long double>(
558 source[start + i])));
561 if (scale == 0.0L)
return ScoreState::Inactive;
563 std::array<long double, kMaxChannels> meanA {};
564 std::array<long double, kMaxChannels> meanB {};
565 std::array<long double, kMaxChannels> varianceA {};
566 std::array<long double, kMaxChannels> varianceB {};
567 std::array<bool, kMaxChannels> included {};
568 int includedCount = 0;
569 long double aggregateVarianceA = 0.0L;
570 long double aggregateVarianceB = 0.0L;
572 for (
int ch = 0; ch < channels; ++ch)
575 for (
int i = 0; i < window; ++i)
577 meanA[
static_cast<std::size_t
>(ch)] +=
578 static_cast<long double>(source[end - window + i]) / scale;
579 meanB[
static_cast<std::size_t
>(ch)] +=
580 static_cast<long double>(source[start + i]) / scale;
582 meanA[
static_cast<std::size_t
>(ch)] /= window;
583 meanB[
static_cast<std::size_t
>(ch)] /= window;
584 for (
int i = 0; i < window; ++i)
586 const long double a =
587 static_cast<long double>(source[end - window + i]) / scale
588 - meanA[
static_cast<std::size_t
>(ch)];
589 const long double b =
590 static_cast<long double>(source[start + i]) / scale
591 - meanB[
static_cast<std::size_t
>(ch)];
592 varianceA[
static_cast<std::size_t
>(ch)] += a * a;
593 varianceB[
static_cast<std::size_t
>(ch)] += b * b;
595 varianceA[
static_cast<std::size_t
>(ch)] = std::clamp(
596 varianceA[
static_cast<std::size_t
>(ch)] / window, 0.0L, 1.0L);
597 varianceB[
static_cast<std::size_t
>(ch)] = std::clamp(
598 varianceB[
static_cast<std::size_t
>(ch)] / window, 0.0L, 1.0L);
599 const long double rmsA = scale * std::sqrt(
600 varianceA[
static_cast<std::size_t
>(ch)]);
601 const long double rmsB = scale * std::sqrt(
602 varianceB[
static_cast<std::size_t
>(ch)]);
603 if (!std::isfinite(rmsA) || !std::isfinite(rmsB))
604 return ScoreState::NumericalFailure;
605 if (std::max(rmsA, rmsB)
606 >=
static_cast<long double>(settings.minimumActivityRms))
608 included[
static_cast<std::size_t
>(ch)] =
true;
610 aggregateVarianceA += varianceA[
static_cast<std::size_t
>(ch)];
611 aggregateVarianceB += varianceB[
static_cast<std::size_t
>(ch)];
614 if (includedCount == 0)
return ScoreState::Inactive;
615 const long double aggregateRmsA = scale * std::sqrt(std::min(1.0L,
616 aggregateVarianceA /
static_cast<long double>(includedCount)));
617 const long double aggregateRmsB = scale * std::sqrt(std::min(1.0L,
618 aggregateVarianceB /
static_cast<long double>(includedCount)));
619 if (!std::isfinite(aggregateRmsA) || !std::isfinite(aggregateRmsB))
620 return ScoreState::NumericalFailure;
621 if (aggregateRmsA <
static_cast<long double>(settings.minimumActivityRms)
623 <
static_cast<long double>(settings.minimumActivityRms))
624 return ScoreState::Inactive;
627 long double maximumChannelCost = 0.0L;
628 for (
int ch = 0; ch < channels; ++ch)
630 if (!included[
static_cast<std::size_t
>(ch)])
continue;
632 long double d0Numerator = 0.0L;
633 long double d0Denominator = 0.0L;
634 long double d1Numerator = 0.0L;
635 long double d1Denominator = 0.0L;
636 long double previousA = 0.0L;
637 long double previousB = 0.0L;
638 for (
int i = 0; i < window; ++i)
640 const long double a =
641 static_cast<long double>(source[end - window + i]) / scale;
642 const long double b =
643 static_cast<long double>(source[start + i]) / scale;
644 const long double difference = a - b;
645 d0Numerator += difference * difference;
646 d0Denominator += a * a + b * b;
649 const long double derivativeA = a - previousA;
650 const long double derivativeB = b - previousB;
651 const long double derivativeDifference =
652 derivativeA - derivativeB;
653 d1Numerator += derivativeDifference * derivativeDifference;
654 d1Denominator += derivativeA * derivativeA
655 + derivativeB * derivativeB;
659 scratch.timeA[
static_cast<std::size_t
>(i)] =
static_cast<T
>(
660 (a - meanA[
static_cast<std::size_t
>(ch)])
661 * scratch.window[
static_cast<std::size_t
>(i)]);
662 scratch.timeB[
static_cast<std::size_t
>(i)] =
static_cast<T
>(
663 (b - meanB[
static_cast<std::size_t
>(ch)])
664 * scratch.window[
static_cast<std::size_t
>(i)]);
667 const long double d0 = d0Denominator == 0.0L ? 0.0L
668 : std::clamp(0.5L * d0Numerator / d0Denominator, 0.0L, 1.0L);
669 const long double d1 = d1Denominator == 0.0L ? 0.0L
670 : std::clamp(0.5L * d1Numerator / d1Denominator, 0.0L, 1.0L);
672 scratch.fft.forward(scratch.timeA.data(), scratch.frequencyA.data());
673 scratch.fft.forward(scratch.timeB.data(), scratch.frequencyB.data());
674 const int bins = window / 2 + 1;
675 long double totalA = 0.0L;
676 long double totalB = 0.0L;
677 for (
int bin = 0; bin < bins; ++bin)
679 const long double weight =
680 (bin == 0 || bin == bins - 1) ? 1.0L : 2.0L;
681 const long double ar = scratch.frequencyA[2 * bin];
682 const long double ai = scratch.frequencyA[2 * bin + 1];
683 const long double br = scratch.frequencyB[2 * bin];
684 const long double bi = scratch.frequencyB[2 * bin + 1];
685 totalA += weight * (ar * ar + ai * ai);
686 totalB += weight * (br * br + bi * bi);
689 long double djs = 0.0L;
690 if ((totalA == 0.0L) != (totalB == 0.0L))
694 else if (totalA != 0.0L)
696 long double divergenceA = 0.0L;
697 long double divergenceB = 0.0L;
698 for (
int bin = 0; bin < bins; ++bin)
700 const long double weight =
701 (bin == 0 || bin == bins - 1) ? 1.0L : 2.0L;
702 const long double ar = scratch.frequencyA[2 * bin];
703 const long double ai = scratch.frequencyA[2 * bin + 1];
704 const long double br = scratch.frequencyB[2 * bin];
705 const long double bi = scratch.frequencyB[2 * bin + 1];
706 const long double probabilityA =
707 weight * (ar * ar + ai * ai) / totalA;
708 const long double probabilityB =
709 weight * (br * br + bi * bi) / totalB;
710 const long double midpoint =
711 (probabilityA + probabilityB) * 0.5L;
712 if (probabilityA > 0.0L)
713 divergenceA += probabilityA
714 * std::log(probabilityA / midpoint);
715 if (probabilityB > 0.0L)
716 divergenceB += probabilityB
717 * std::log(probabilityB / midpoint);
719 djs = std::clamp((divergenceA + divergenceB)
720 * 0.5L / std::numbers::ln2_v<long double>, 0.0L, 1.0L);
723 const long double channelCost = std::clamp(
724 0.45L * d0 + 0.20L * d1 + 0.35L * djs, 0.0L, 1.0L);
725 const long double channelWeight =
726 varianceA[
static_cast<std::size_t
>(ch)]
727 + varianceB[
static_cast<std::size_t
>(ch)];
728 if (!std::isfinite(d0) || !std::isfinite(d1)
729 || !std::isfinite(djs) || !std::isfinite(channelCost)
730 || !std::isfinite(channelWeight))
731 return ScoreState::NumericalFailure;
732 scratch.channelCosts[
static_cast<std::size_t
>(costCount++)] =
733 { channelWeight, channelCost };
734 maximumChannelCost = std::max(maximumChannelCost, channelCost);
737 std::sort(scratch.channelCosts.begin(),
738 scratch.channelCosts.begin() + costCount,
739 [](
const auto& lhs,
const auto& rhs) {
740 return lhs.first < rhs.first
741 || (lhs.first == rhs.first && lhs.second < rhs.second);
743 long double weightedCost = 0.0L;
744 long double totalWeight = 0.0L;
745 for (
int i = 0; i < costCount; ++i)
747 weightedCost += scratch.channelCosts[
static_cast<std::size_t
>(i)].first
748 * scratch.channelCosts[
static_cast<std::size_t
>(i)].second;
749 totalWeight += scratch.channelCosts[
static_cast<std::size_t
>(i)].first;
751 if (!(totalWeight > 0.0L) || !std::isfinite(weightedCost)
752 || !std::isfinite(totalWeight))
753 return ScoreState::NumericalFailure;
754 const long double cost = std::clamp(
755 0.75L * weightedCost / totalWeight
756 + 0.25L * maximumChannelCost,
758 if (!std::isfinite(cost))
return ScoreState::NumericalFailure;
759 outputCost =
static_cast<T
>(cost);
760 return std::isfinite(outputCost)
761 ? ScoreState::Scored : ScoreState::NumericalFailure;
764 [[nodiscard]]
static bool rangesOverlap(
const void* first,
765 std::size_t firstBytes,
767 std::size_t secondBytes)
noexcept
769 const auto firstBegin =
reinterpret_cast<std::uintptr_t
>(first);
770 const auto secondBegin =
reinterpret_cast<std::uintptr_t
>(second);
771 if (firstBytes > std::numeric_limits<std::uintptr_t>::max() - firstBegin
773 > std::numeric_limits<std::uintptr_t>::max() - secondBegin)
775 const auto firstEnd = firstBegin + firstBytes;
776 const auto secondEnd = secondBegin + secondBytes;
777 return firstBegin < secondEnd && secondBegin < firstEnd;
780 [[nodiscard]]
static T renderOverlapSample(
const T* source,
783 Crossfade<T>& crossfade)
noexcept
785 const T tail = source[loop.end - loop.crossfadeLength + index];
786 const T head = source[loop.start + index];
787 if (index == 0)
return tail;
788 if (index == loop.crossfadeLength - 1)
return head;
789 const T scale = std::max(std::abs(tail), std::abs(head));
790 if (scale == T(0))
return T(0);
791 const T position =
static_cast<T
>(index)
792 /
static_cast<T
>(loop.crossfadeLength - 1);
793 crossfade.setPosition(position);
794 const T mixed = crossfade.process(tail / scale, head / scale);
795 const T denominator = crossfade.getGainA() + crossfade.getGainB();
796 const T normalized = std::clamp(mixed / denominator, T(-1), T(1));
797 return scale * normalized;
800 [[nodiscard]]
static bool dryRenderChannel(
const T* source,
801 const Result& loop)
noexcept
803 Crossfade<T> crossfade;
805 for (
int i = 0; i < loop.crossfadeLength; ++i)
806 if (!std::isfinite(renderOverlapSample(source, loop, i, crossfade)))