DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
LoopFinder.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
23#include "../Core/AudioBuffer.h"
24#include "../Core/DspMath.h"
25#include "../Core/FFT.h"
26#include "../Effects/Crossfade.h"
27
28#include <algorithm>
29#include <array>
30#include <cmath>
31#include <cstddef>
32#include <cstdint>
33#include <limits>
34#include <numbers>
35#include <utility>
36#include <vector>
37
38namespace dspark {
39
51template <FloatType T>
52class LoopFinder final
53{
54public:
68
69 struct Range {
70 std::int64_t first = 0;
71 std::int64_t last = -1;
72 };
73
74 struct Settings {
75 int comparisonLength = 1024;
76 int crossfadeLength = 256;
77 int coarseStride = 64;
79 std::uint32_t maxCoarsePairs = 131072;
80 std::uint32_t maxRefinementPairs = 262144;
81 T maximumCost = T(0.20);
82 T minimumActivityRms = T(1.0e-5);
83 };
84
85 struct Result {
87 std::int64_t start = 0;
88 std::int64_t end = 0;
90 T seamCost = T(1);
91
92 [[nodiscard]] explicit operator bool() const noexcept
93 {
94 return status == Status::Success;
95 }
96
97 [[nodiscard]] std::int64_t renderedLength() const noexcept
98 {
99 if (status != Status::Success || start < 0 || end < start
100 || crossfadeLength < 2)
101 return 0;
102
103 // Safe because 0 <= start <= end <= INT64_MAX.
104 const std::int64_t sourceLength = end - start;
105 const std::int64_t fade = static_cast<std::int64_t>(crossfadeLength);
106 if (fade > sourceLength / 2)
107 return 0;
108 return sourceLength - fade;
109 }
110 };
111
121 Range startRange,
122 Range endRange,
123 std::int64_t minLoopLength,
124 std::int64_t maxLoopLength,
125 const Settings& settings = {}) const
126 {
127 const int channels = audio.getNumChannels();
128 const int sampleCount = audio.getNumSamples();
129 if (channels == 0 || sampleCount == 0)
130 return failure(Status::EmptyInput);
131
132 if (channels < 1 || channels > kMaxChannels)
133 return failure(Status::InvalidArgument);
134 for (int ch = 0; ch < channels; ++ch)
135 if (audio.getChannel(ch) == nullptr)
136 return failure(Status::InvalidArgument);
137
138 // Every comparison below is parenthesised on purpose. Left bare in a
139 // template, MSVC reads `endRange.last < ... || endRange.last >` as a
140 // template argument list on a member name it has not resolved yet and
141 // rejects the whole predicate.
142 if (!validSettings(settings)
143 || (startRange.first < 0) || (startRange.last < startRange.first)
144 || (startRange.last >= static_cast<std::int64_t>(sampleCount))
145 || (endRange.first < 0) || (endRange.last < endRange.first)
146 || (endRange.last > static_cast<std::int64_t>(sampleCount))
147 || (minLoopLength < 1) || (maxLoopLength < minLoopLength)
148 || (maxLoopLength > static_cast<std::int64_t>(sampleCount)))
149 return failure(Status::InvalidArgument);
150
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)
155 return failure(Status::InvalidArgument);
156 const std::int64_t minimumPeriod =
157 std::max(window + 1, fade * 2);
158
159 if (sampleCount > kMaxInputSamples)
160 return failure(Status::InputTooLong);
161
162 long double sourceScale = 0.0L;
163 for (int ch = 0; ch < channels; ++ch)
164 {
165 const T* const samples = audio.getChannel(ch);
166 for (int n = 0; n < sampleCount; ++n)
167 {
168 if (!std::isfinite(samples[n]))
169 return failure(Status::NonFiniteInput);
170 sourceScale = std::max(sourceScale,
171 std::abs(static_cast<long double>(samples[n])));
172 }
173 }
174
175 if (sampleCount < minimumPeriod)
176 return failure(Status::InputTooShort);
177
178 bool sourceHasActivity = false;
179 long double sourceVarianceSum = 0.0L;
180 int sourceActiveChannels = 0;
181 if (sourceScale > 0.0L)
182 {
183 for (int ch = 0; ch < channels; ++ch)
184 {
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);
190
191 long double variance = 0.0L;
192 for (int n = 0; n < sampleCount; ++n)
193 {
194 const long double centered =
195 static_cast<long double>(samples[n]) / sourceScale - mean;
196 variance += centered * centered;
197 }
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))
202 return failure(Status::NumericalFailure);
203 if (rms >= static_cast<long double>(settings.minimumActivityRms))
204 {
205 sourceHasActivity = true;
206 sourceVarianceSum += variance;
207 ++sourceActiveChannels;
208 }
209 }
210 }
211 if (!sourceHasActivity)
212 return failure(Status::InsufficientActivity);
213
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))
218 return failure(Status::NumericalFailure);
219
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))
224 return failure(Status::CandidateLimitExceeded);
225
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,
231 radiusSpan);
232 const std::uint64_t endSpan = std::min(
233 static_cast<std::uint64_t>(endRange.last - endRange.first) + 1u,
234 radiusSpan);
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)
241 return failure(Status::CandidateLimitExceeded);
242
243 ScoringScratch scratch(settings.comparisonLength);
244 std::array<Candidate, 8> seeds {};
245 int seedCount = 0;
246 Candidate best {};
247 bool hasBest = false;
248
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))
253 return false;
254 const std::int64_t period = end - start;
255 return period >= minimumPeriod
256 && period >= minLoopLength && period <= maxLoopLength;
257 };
258
259 auto consider = [&](std::int64_t start, std::int64_t end,
260 bool keepSeed) {
261 if (!geometricallyValid(start, end)) return ScoreState::Inactive;
262 Candidate candidate { start, end, T(1) };
263 const ScoreState state = scoreCandidate(audio, start, end,
264 settings, scratch,
265 candidate.cost);
266 if (state != ScoreState::Scored) return state;
267 if (!hasBest || better(candidate, best))
268 {
269 best = candidate;
270 hasBest = true;
271 }
272 if (keepSeed) insertSeed(seeds, seedCount, candidate);
273 return ScoreState::Scored;
274 };
275
276 for (std::uint64_t si = 0; si < startCount; ++si)
277 {
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)
282 {
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)
287 return failure(Status::NumericalFailure);
288 }
289 }
290
291 std::uint64_t exactRefinementAttempts = 0;
292 for (int seed = 0; seed < seedCount; ++seed)
293 {
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)
314 return failure(Status::NumericalFailure);
315 exactRefinementAttempts += rectangle;
316 }
317 if (exactRefinementAttempts > settings.maxRefinementPairs)
318 return failure(Status::CandidateLimitExceeded);
319
320 for (int seed = 0; seed < seedCount; ++seed)
321 {
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)
335 {
336 for (std::int64_t end = lowEnd; end <= highEnd; ++end)
337 {
338 if (consider(start, end, false)
339 == ScoreState::NumericalFailure)
340 return failure(Status::NumericalFailure);
341 }
342 }
343 }
344
345 if (!hasBest || best.cost > settings.maximumCost)
346 return failure(Status::NoAcceptableLoop);
347 return { Status::Success, best.start, best.end,
348 settings.crossfadeLength, best.cost };
349 }
350
355 [[nodiscard]] static Status renderLoop(AudioBufferView<const T> audio,
356 const Result& loop,
357 AudioBufferView<T> output) noexcept
358 {
359 const int channels = audio.getNumChannels();
360 const int sampleCount = audio.getNumSamples();
361 // Parenthesised for the same reason as the range check in find():
362 // bare, `loop.end < ... || loop.end >` reads as a template argument
363 // list to MSVC.
364 if (loop.status != Status::Success || (channels < 1)
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)
373 if (audio.getChannel(ch) == nullptr)
375
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())
383
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)
390
391 const std::size_t sourceBytes = static_cast<std::size_t>(sampleCount)
392 * sizeof(T);
393 const std::size_t outputBytes = static_cast<std::size_t>(rendered)
394 * sizeof(T);
395 for (int outChannel = 0; outChannel < channels; ++outChannel)
396 {
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))
406 }
407
408 for (int ch = 0; ch < channels; ++ch)
409 {
410 const T* const source = audio.getChannel(ch);
411 for (int n = 0; n < sampleCount; ++n)
412 if (!std::isfinite(source[n]))
414 if (!dryRenderChannel(source, loop))
416 }
417
418 for (int ch = 0; ch < channels; ++ch)
419 {
420 const T* const source = audio.getChannel(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);
424 Crossfade<T> crossfade;
426 for (int i = 0; i < loop.crossfadeLength; ++i)
427 destination[i] = renderOverlapSample(source, loop, i, crossfade);
428 }
429 return Status::Success;
430 }
431
432private:
433 static constexpr int kMaxChannels = 16;
434 static constexpr int kMaxInputSamples = 1 << 26;
435
436 enum class ScoreState : std::uint8_t { Inactive, Scored, NumericalFailure };
437
438 struct Candidate {
439 std::int64_t start = 0;
440 std::int64_t end = 0;
441 T cost = T(1);
442 };
443
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))
452 {
453 for (int i = 0; i < windowSize; ++i)
454 {
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));
458 }
459 }
460
461 FFTReal<T> fft;
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 {};
468 };
469
470 [[nodiscard]] static Result failure(Status status) noexcept
471 {
472 return { status, 0, 0, 0, T(1) };
473 }
474
475 [[nodiscard]] static bool validSettings(const Settings& settings) noexcept
476 {
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);
494 }
495
496 [[nodiscard]] static std::uint64_t latticeCount(Range range,
497 int stride) noexcept
498 {
499 return static_cast<std::uint64_t>(range.last - range.first)
500 / static_cast<std::uint64_t>(stride) + 1u;
501 }
502
503 [[nodiscard]] static bool checkedMultiply(std::uint64_t a,
504 std::uint64_t b,
505 std::uint64_t& result) noexcept
506 {
507 if (a != 0u && b > std::numeric_limits<std::uint64_t>::max() / a)
508 return false;
509 result = a * b;
510 return true;
511 }
512
513 [[nodiscard]] static bool better(const Candidate& lhs,
514 const Candidate& rhs) noexcept
515 {
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)));
520 }
521
522 static void insertSeed(std::array<Candidate, 8>& seeds,
523 int& count,
524 Candidate candidate) noexcept
525 {
526 int position = 0;
527 while (position < count
528 && !better(candidate, seeds[static_cast<std::size_t>(position)]))
529 ++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;
536 count = newCount;
537 }
538
539 [[nodiscard]] static ScoreState scoreCandidate(
540 AudioBufferView<const T> audio,
541 std::int64_t start,
542 std::int64_t end,
543 const Settings& settings,
544 ScoringScratch& scratch,
545 T& outputCost) noexcept
546 {
547 const int channels = audio.getNumChannels();
548 const int window = settings.comparisonLength;
549 long double scale = 0.0L;
550 for (int ch = 0; ch < channels; ++ch)
551 {
552 const T* const source = audio.getChannel(ch);
553 for (int i = 0; i < window; ++i)
554 {
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])));
559 }
560 }
561 if (scale == 0.0L) return ScoreState::Inactive;
562
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;
571
572 for (int ch = 0; ch < channels; ++ch)
573 {
574 const T* const source = audio.getChannel(ch);
575 for (int i = 0; i < window; ++i)
576 {
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;
581 }
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)
585 {
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;
594 }
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))
607 {
608 included[static_cast<std::size_t>(ch)] = true;
609 ++includedCount;
610 aggregateVarianceA += varianceA[static_cast<std::size_t>(ch)];
611 aggregateVarianceB += varianceB[static_cast<std::size_t>(ch)];
612 }
613 }
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)
622 || aggregateRmsB
623 < static_cast<long double>(settings.minimumActivityRms))
624 return ScoreState::Inactive;
625
626 int costCount = 0;
627 long double maximumChannelCost = 0.0L;
628 for (int ch = 0; ch < channels; ++ch)
629 {
630 if (!included[static_cast<std::size_t>(ch)]) continue;
631 const T* const source = audio.getChannel(ch);
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)
639 {
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;
647 if (i > 0)
648 {
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;
656 }
657 previousA = a;
658 previousB = b;
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)]);
665 }
666
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);
671
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)
678 {
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);
687 }
688
689 long double djs = 0.0L;
690 if ((totalA == 0.0L) != (totalB == 0.0L))
691 {
692 djs = 1.0L;
693 }
694 else if (totalA != 0.0L)
695 {
696 long double divergenceA = 0.0L;
697 long double divergenceB = 0.0L;
698 for (int bin = 0; bin < bins; ++bin)
699 {
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);
718 }
719 djs = std::clamp((divergenceA + divergenceB)
720 * 0.5L / std::numbers::ln2_v<long double>, 0.0L, 1.0L);
721 }
722
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);
735 }
736
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);
742 });
743 long double weightedCost = 0.0L;
744 long double totalWeight = 0.0L;
745 for (int i = 0; i < costCount; ++i)
746 {
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;
750 }
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,
757 0.0L, 1.0L);
758 if (!std::isfinite(cost)) return ScoreState::NumericalFailure;
759 outputCost = static_cast<T>(cost);
760 return std::isfinite(outputCost)
761 ? ScoreState::Scored : ScoreState::NumericalFailure;
762 }
763
764 [[nodiscard]] static bool rangesOverlap(const void* first,
765 std::size_t firstBytes,
766 const void* second,
767 std::size_t secondBytes) noexcept
768 {
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
772 || secondBytes
773 > std::numeric_limits<std::uintptr_t>::max() - secondBegin)
774 return true;
775 const auto firstEnd = firstBegin + firstBytes;
776 const auto secondEnd = secondBegin + secondBytes;
777 return firstBegin < secondEnd && secondBegin < firstEnd;
778 }
779
780 [[nodiscard]] static T renderOverlapSample(const T* source,
781 const Result& loop,
782 int index,
783 Crossfade<T>& crossfade) noexcept
784 {
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;
798 }
799
800 [[nodiscard]] static bool dryRenderChannel(const T* source,
801 const Result& loop) noexcept
802 {
803 Crossfade<T> crossfade;
804 crossfade.setCurve(Crossfade<T>::Curve::EqualPower);
805 for (int i = 0; i < loop.crossfadeLength; ++i)
806 if (!std::isfinite(renderOverlapSample(source, loop, i, crossfade)))
807 return false;
808 return true;
809 }
810};
811
812} // 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.
Artifact-free, SIMD-friendly crossfader for two audio signals.
Definition Crossfade.h:63
@ EqualPower
Sine/cosine law. Constant power, no volume drop, smooth at both ends.
void setCurve(Curve curve) noexcept
Sets the crossfade curve type. Thread-safe. Can be called from the GUI thread. A curve change is blen...
Definition Crossfade.h:118
Finds and renders bounded, deterministic loop seams.
Definition LoopFinder.h:53
Result find(AudioBufferView< const T > audio, Range startRange, Range endRange, std::int64_t minLoopLength, std::int64_t maxLoopLength, const Settings &settings={}) const
Searches explicit inclusive endpoint ranges for the lowest-cost supported loop seam.
Definition LoopFinder.h:120
static Status renderLoop(AudioBufferView< const T > audio, const Result &loop, AudioBufferView< T > output) noexcept
Renders one normalized equal-power loop cycle without allocation.
Definition LoopFinder.h:355
Main namespace for the DSPark framework.
std::int64_t renderedLength() const noexcept
Definition LoopFinder.h:97
std::uint32_t maxRefinementPairs
Definition LoopFinder.h:80