285#include "../Core/DspMath.h"
286#include "../Core/AudioBuffer.h"
287#include "../Core/AudioSpec.h"
288#include "../Core/detail/AccountedAllocator.h"
289#include "OnsetDetector.h"
303namespace detail {
template <FloatType T>
struct OfflineBeatEngine; }
316template <FloatType T>
319 template <
typename U>
320 using WorkVector = std::vector<U, detail::AccountedAllocator<U>>;
327 for (
auto* values : {&bankPeriod_, &bankCosCoef_, &bankSinCoef_, &bankDecay_,
328 &bankPrior_, &zRe_, &zIm_, &zMass_, &bankCoherence_,
329 &bankLevelRatio_, &env_, &envBal_, &local_, &cumScore_,
330 &acf_, &scratchA_, &kernel_, &localPeriod_, &sweepA_,
331 &sweepB_, &candScore_, &candExplains_})
332 *values = WorkVector<double>(doubles);
333 for (
auto& values : envReg_) values = WorkVector<double>(doubles);
370 prepared_.store(
false, std::memory_order_relaxed);
373 onset_.prepare(spec);
374 hop_ = onset_.getHopSize();
375 if (hop_ < 1) hop_ = 1;
376 frameRate_ = sampleRate_ /
static_cast<double>(hop_);
377 baselineCoef_ = 1.0 - std::exp(-1.0 / std::max(1.0, kBaselineSeconds * frameRate_));
379 latencySamples_.store(
static_cast<int64_t
>(onset_.getEnvelopeLatencySamples())
380 +
static_cast<int64_t
>(hop_),
381 std::memory_order_relaxed);
385 tightness_.store(kDefaultTightness, std::memory_order_relaxed);
388 candidates_.reserve(64);
391 prepared_.store(
true, std::memory_order_relaxed);
404 if (!std::isfinite(minBpm) || !std::isfinite(maxBpm))
return;
405 if (!(minBpm > T(0)) || !(maxBpm > T(0)) || !(minBpm <= maxBpm))
return;
406 if (bankSize_ < 2)
return;
408 const double lo = std::clamp(
static_cast<double>(minBpm),
409 kBankMinBpm, kBankMaxBpm);
410 const double hi = std::clamp(
static_cast<double>(maxBpm),
411 kBankMinBpm, kBankMaxBpm);
415 int iLo = bpmToIndex(hi);
416 int iHi = bpmToIndex(lo);
417 iLo = std::clamp(iLo, 0, bankSize_ - 1);
418 iHi = std::clamp(iHi, 0, bankSize_ - 1);
419 if (iLo > iHi) std::swap(iLo, iHi);
424 activeRange_.store(packRange(iLo, iHi), std::memory_order_relaxed);
510 if (!std::isfinite(alpha))
return;
511 tightness_.store(std::max(kMinTightness,
static_cast<double>(alpha)),
512 std::memory_order_relaxed);
518 return static_cast<T
>(tightness_.load(std::memory_order_relaxed));
536 if (!prepared_.load(std::memory_order_relaxed))
return {};
564 for (
auto& r : envReg_) r.clear();
565 offlineOpen_ =
false;
566 if (!prepared_.load(std::memory_order_relaxed))
return;
569 if (expectedSamples > 0)
571 const size_t frames =
static_cast<size_t>(expectedSamples / std::max(1, hop_) + 2);
572 env_.reserve(frames);
573 envRef_.reserve(frames);
574 for (
auto& r : envReg_) r.reserve(frames);
584 if (!offlineOpen_)
return;
586 while (off < samples.size())
590 const int64_t since = offlinePushed_ %
static_cast<int64_t
>(hop_);
591 const size_t take = std::min(samples.size() - off,
592 static_cast<size_t>(
static_cast<int64_t
>(hop_) - since));
593 onset_.pushSamples(samples.subspan(off, take));
594 offlinePushed_ +=
static_cast<int64_t
>(take);
596 if (offlinePushed_ %
static_cast<int64_t
>(hop_) == 0) appendEnvelopeFrame();
606 if (!offlineOpen_)
return out;
607 offlineOpen_ =
false;
610 return finishEnvelope();
619 if (!prepared_.load(std::memory_order_relaxed) || in.getNumChannels() < 1)
621 const T* ch0 = in.getChannel(0);
622 pushSamples(std::span<const T>(ch0,
static_cast<size_t>(in.getNumSamples())));
635 if (!prepared_.load(std::memory_order_relaxed))
return;
639 while (off < samples.size())
641 const int64_t sinceFrame = samplesPushed_ %
static_cast<int64_t
>(hop_);
642 const size_t toBoundary =
static_cast<size_t>(
static_cast<int64_t
>(hop_)
644 const size_t take = std::min(samples.size() - off, toBoundary);
646 onset_.pushSamples(samples.subspan(off, take));
647 samplesPushed_ +=
static_cast<int64_t
>(take);
650 if (
static_cast<size_t>(take) == toBoundary)
652 if (advanceFrame()) fired =
true;
665 beatLatched_.store(fired, std::memory_order_release);
673 float bpm = 0.0f, conf = 0.0f;
674 unpackTempo(tempoAndConfidence_.load(std::memory_order_relaxed), bpm, conf);
675 return static_cast<T
>(bpm);
706 float bpm = 0.0f, conf = 0.0f;
707 unpackTempo(tempoAndConfidence_.load(std::memory_order_relaxed), bpm, conf);
708 return static_cast<T
>(conf);
722 float bpm = 0.0f, conf = 0.0f;
723 unpackTempo(tempoAndConfidence_.load(std::memory_order_relaxed), bpm, conf);
724 bpmOut =
static_cast<T
>(bpm);
725 confidenceOut =
static_cast<T
>(conf);
746 return beatLatched_.load(std::memory_order_acquire);
764 return lastBeatSample_.load(std::memory_order_relaxed);
776 return static_cast<int>(latencySamples_.load(std::memory_order_relaxed));
780 [[nodiscard]]
double getFrameRate() const noexcept {
return frameRate_; }
793 offlineOpen_ =
false;
800 Result finishEnvelope(
void* beatContext =
nullptr,
801 void (*receiveBeats)(
void*, std::span<const int64_t>) =
nullptr)
804 int iLo = 0, iHi = 0;
805 unpackRange(activeRange_.load(std::memory_order_relaxed), iLo, iHi);
807 const int n =
static_cast<int>(env_.size());
808 const double maxPeriod = bankPeriod_[
static_cast<size_t>(iHi)];
809 if (n <
static_cast<int>(kMinAnalysisBeats * maxPeriod))
820 double tau1 = 0.0, tau2 = 0.0;
821 const bool balanced = (envBal_.size() == env_.size());
822 if (balanced) env_.swap(envBal_);
823 pickCandidates(iLo, iHi, tau1, tau2);
824 if (balanced) env_.swap(envBal_);
825 if (!(tau1 > 0.0)) { resetState();
return out; }
831 if (fundamentalFound_) chooseMetricalLevel(iLo, iHi, tau1, tau2);
856 WorkVector<int64_t> win(detail::AccountedAllocator<int64_t>{&allocationAccount_});
857 localPeriod_.clear();
858 WorkVector<int64_t> moving(detail::AccountedAllocator<int64_t>{&allocationAccount_});
859 bool haveMoving =
false;
860 if (coherence(tau1) >= kMinFundamentalCoherence)
862 computeLocalPeriods(tau1);
863 haveMoving = buildGrid(tau1, moving) && moving.size() >= 2;
865 localPeriod_.clear();
866 bool haveSteady = buildGrid(tau1, win) && win.size() >= 2;
874 WorkVector<int64_t> strict(detail::AccountedAllocator<int64_t>{&allocationAccount_});
875 alphaScale_ = kStrictTightnessFactor;
876 const bool haveStrict = buildGrid(tau1, strict) && strict.size() >= 2;
878 if (haveStrict && (!haveSteady || gridCoherence(strict) > gridCoherence(win)))
884 if (haveMoving && (!haveSteady
885 || gridCoherence(moving) > kMovingGridMargin * gridCoherence(win)))
887 else if (!haveSteady)
897 const double slope = fitBeatSlope(win);
899 receiveBeats(beatContext, {win.data(), win.size()});
901 out.beatSamples.assign(win.begin(), win.end());
902 out.tempoBpm =
static_cast<T
>((slope > 0.0) ? 60.0 * sampleRate_ / slope
903 : periodToBpm(tau1));
904 out.secondaryTempoBpm =
static_cast<T
>((tau2 > 0.0) ? periodToBpm(tau2) : 0.0);
914 const double base = coherence((slope > 0.0) ? slope /
static_cast<double>(hop_)
916 out.confidence =
static_cast<T
>(std::clamp(base * ambiguityDiscount(secondaryShare_),
925 static constexpr double kBankMinBpm = 20.0;
926 static constexpr double kBankMaxBpm = 480.0;
935 static constexpr double kHarmonicReach = 5.0;
936 static constexpr int kBinsPerOctave = 128;
937 static constexpr double kDefaultMinBpm = 40.0;
938 static constexpr double kDefaultMaxBpm = 240.0;
952 static constexpr int kNumHarmonics = 3;
953 static constexpr double kHarmonicDivisors[kNumHarmonics] = { 2.0, 3.0, 5.0 };
959 static constexpr double kIntegrationBeats = 8.0;
963 static constexpr double kPriorCentreSeconds = 0.5;
964 static constexpr double kPriorWidthOctaves = 1.4;
986 static constexpr double kTactusFastOctaves = 0.45;
987 static constexpr double kTactusSlowOctaves = kPriorWidthOctaves;
1013 static constexpr int kLevelCandidates = 3;
1014 static constexpr int kLevelMiddle = 1;
1015 static constexpr size_t kLevelRawFeatures = 18;
1016 static constexpr size_t kLevelShareAtPeriod = 1;
1017 static constexpr size_t kLevelBalancedShareAtPeriod = 2;
1018 static constexpr size_t kLevelShareAtDouble = 7;
1019 static constexpr size_t kLevelShareAtTriple = 14;
1020 static constexpr std::array<double, kLevelCandidates> kLevelMultiples = { 0.5, 1.0, 2.0 };
1021 static constexpr std::array<double, kLevelCandidates> kLevelBias = {
1022 -0.975336646, +0.092188351, +0.883148294 };
1023 static constexpr std::array<double, 2 * kLevelRawFeatures + 1> kLevelWeights = {
1025 -0.542395096, -0.584902910, +0.498236961,
1026 +2.173179030, +0.290865194, +0.064019696,
1027 -0.056958253, -1.551139582, +2.419398334,
1028 +0.999335380, +1.771192530, +1.715518634,
1029 -0.269329648, -0.242521186, +0.512883972,
1030 +2.417326254, -4.177049390, -0.658688841,
1032 -0.740402303, -0.614477620, +0.536878776,
1033 +2.283943101, +0.321349606, +0.070567309,
1034 -0.060601959, -1.661774996, +2.331723324,
1035 +1.031193638, +1.895745003, +1.908437878,
1036 -0.571387423, -0.456177537, +0.860604058,
1037 +4.361899112, -4.360371230, -0.993521862,
1048 static constexpr std::array<double, 4> kTernaryMultiples = {
1049 1.0 / 3.0, 2.0 / 3.0, 1.5, 3.0 };
1050 static constexpr std::array<double, 4> kTernaryBias = {
1051 0.53602663631392855, -0.44218443943636193, -0.98844100123510192,
1052 -4.3780218173667675 };
1053 static constexpr std::array<double, 2 * kLevelRawFeatures + 1> kTernaryWeights = {
1054 0.135530296867787, 1.516476106187614, 0.50512741958750595,
1055 -3.2226909770906635, 0.76647827698580318, -0.5179245237669392,
1056 1.354793380662362, -2.8513739655680381, -2.7957191619709669,
1057 -0.93155463697868401, -0.59124401355810241, 0.57664680014957181,
1058 -0.83331592757939144, -0.92868430654211009, 0.72147135283011565,
1059 1.342529710852844, -0.78142066296951229, 2.2830394447846869,
1060 -0.71810354059356818, 0.89986068541218478, 0.41744186276874551,
1061 1.3685809837441467, 1.8839328472219159, -0.48244306625392142,
1062 0.89516613728223393, -0.60386251174497119, 1.8096407974850166,
1063 2.9213215771564656, 4.4113655210180074, 3.2066836727020585,
1064 2.0230309593047608, 2.7743701263252531, -2.8072203231223085,
1065 -0.60235479209693188, 0.10118126378289685, -2.5817564678948961,
1066 -0.67204847147376856 };
1080 static constexpr double kLevelContrastFloor = 0.1;
1082 static constexpr double kLevelWindowBeats = 8.0;
1086 static constexpr double kDefaultTightness = 25.0;
1087 static constexpr double kMinTightness = 1e-3;
1097 static constexpr double kMinFundamentalCoherence = 0.1;
1112 static constexpr double kAmbiguityFloor = 0.5;
1117 static constexpr double kCandidateSeparationOctaves = 0.25;
1121 static constexpr double kCandidatePeakFloor = 0.25;
1127 static constexpr double kStrictTightnessFactor = 4.0;
1131 static constexpr double kMovingGridMargin = 1.5;
1133 static constexpr int kLocalPeriodSpan = kBinsPerOctave / 6;
1137 static constexpr double kMinAnalysisBeats = 4.0;
1142 static constexpr double kCoherenceWindowBeats = 8.0;
1145 static constexpr double kBaselineSeconds = 1.0;
1149 static constexpr double kLocalScoreSigmaPeriods = 1.0 / 32.0;
1152 static constexpr double kSearchLowFactor = 0.5;
1153 static constexpr double kSearchHighFactor = 2.0;
1160 static constexpr double kSnapFraction = 0.125;
1165 static constexpr double kTrimFraction = 0.5;
1167 static_assert(std::atomic<std::uint64_t>::is_always_lock_free,
1168 "audio-thread stores must not lock");
1169 static_assert(std::atomic<std::uint32_t>::is_always_lock_free,
1170 "audio-thread stores must not lock");
1171 static_assert(std::atomic<std::int64_t>::is_always_lock_free,
1172 "audio-thread stores must not lock");
1173 static_assert(std::atomic<bool>::is_always_lock_free,
1174 "audio-thread stores must not lock");
1175 static_assert(std::atomic<double>::is_always_lock_free,
1176 "audio-thread stores must not lock");
1184 [[nodiscard]]
static std::uint64_t packTempo(
float bpm,
float conf)
noexcept
1186 std::uint32_t a = 0, b = 0;
1187 std::memcpy(&a, &bpm,
sizeof(a));
1188 std::memcpy(&b, &conf,
sizeof(b));
1189 return (
static_cast<std::uint64_t
>(a) << 32) |
static_cast<std::uint64_t
>(b);
1192 static void unpackTempo(std::uint64_t v,
float& bpm,
float& conf)
noexcept
1194 const std::uint32_t a =
static_cast<std::uint32_t
>(v >> 32);
1195 const std::uint32_t b =
static_cast<std::uint32_t
>(v & 0xFFFFFFFFu);
1196 std::memcpy(&bpm, &a,
sizeof(bpm));
1197 std::memcpy(&conf, &b,
sizeof(conf));
1200 [[nodiscard]]
static std::uint32_t packRange(
int lo,
int hi)
noexcept
1202 return (
static_cast<std::uint32_t
>(lo & 0xFFFF) << 16)
1203 |
static_cast<std::uint32_t
>(hi & 0xFFFF);
1206 static void unpackRange(std::uint32_t v,
int& lo,
int& hi)
noexcept
1208 lo =
static_cast<int>((v >> 16) & 0xFFFFu);
1209 hi =
static_cast<int>(v & 0xFFFFu);
1214 [[nodiscard]]
double periodToBpm(
double periodFrames)
const noexcept
1216 return (periodFrames > 0.0) ? 60.0 * frameRate_ / periodFrames : 0.0;
1219 [[nodiscard]]
double bpmToPeriod(
double bpm)
const noexcept
1221 return (bpm > 0.0) ? 60.0 * frameRate_ / bpm : 0.0;
1225 [[nodiscard]]
int bpmToIndex(
double bpm)
const noexcept
1227 const double p = bpmToPeriod(bpm);
1228 if (!(p > 0.0) || bankSize_ < 1)
return 0;
1229 const double x = std::log2(p / bankPeriod_[0])
1230 *
static_cast<double>(kBinsPerOctave);
1231 return std::clamp(
static_cast<int>(std::lround(x)), 0, bankSize_ - 1);
1236 const double pMin = bpmToPeriod(kBankMaxBpm);
1237 const double pMax = bpmToPeriod(kBankMinBpm) * kHarmonicReach;
1238 const double octaves = std::log2(pMax / pMin);
1239 bankSize_ =
static_cast<int>(std::lround(octaves
1240 *
static_cast<double>(kBinsPerOctave))) + 1;
1241 bankSize_ = std::max(bankSize_, 2 * kBinsPerOctave + 2);
1243 bankPeriod_.assign(
static_cast<size_t>(bankSize_), 0.0);
1244 bankCosCoef_.assign(
static_cast<size_t>(bankSize_), 0.0);
1245 bankSinCoef_.assign(
static_cast<size_t>(bankSize_), 0.0);
1246 bankDecay_.assign(
static_cast<size_t>(bankSize_), 0.0);
1247 bankPrior_.assign(
static_cast<size_t>(bankSize_), 0.0);
1248 zRe_.assign(
static_cast<size_t>(bankSize_), 0.0);
1249 zIm_.assign(
static_cast<size_t>(bankSize_), 0.0);
1250 zMass_.assign(
static_cast<size_t>(bankSize_), 0.0);
1251 bankCoherence_.assign(
static_cast<size_t>(bankSize_), 0.0);
1253 const double centre = kPriorCentreSeconds * frameRate_;
1254 for (
int i = 0; i < bankSize_; ++i)
1256 const double p = pMin * std::pow(2.0,
static_cast<double>(i)
1257 /
static_cast<double>(kBinsPerOctave));
1258 bankPeriod_[
static_cast<size_t>(i)] = p;
1260 const double w = 2.0 *
static_cast<double>(pi<double>) / p;
1261 const double lambda = std::exp(-1.0 / (kIntegrationBeats * p));
1262 bankDecay_[
static_cast<size_t>(i)] = lambda;
1263 bankCosCoef_[
static_cast<size_t>(i)] = lambda * std::cos(w);
1264 bankSinCoef_[
static_cast<size_t>(i)] = lambda * std::sin(w);
1266 const double lg = std::log2(p / centre) / kPriorWidthOctaves;
1267 bankPrior_[
static_cast<size_t>(i)] = std::exp(-0.5 * lg * lg);
1274 for (
int k = 0; k < kNumHarmonics; ++k)
1275 harmonicOffset_[k] =
static_cast<int>(std::lround(
1276 std::log2(kHarmonicDivisors[k]) *
static_cast<double>(kBinsPerOctave)));
1280 bankLevelRatio_.assign(
static_cast<size_t>(kNumHarmonics)
1281 *
static_cast<size_t>(bankSize_), 1.0);
1282 for (
int k = 0; k < kNumHarmonics; ++k)
1283 for (
int i = 0; i < bankSize_; ++i)
1284 bankLevelRatio_[
static_cast<size_t>(k) *
static_cast<size_t>(bankSize_)
1285 +
static_cast<size_t>(i)]
1286 = levelRatio(bankPeriod_[
static_cast<size_t>(i)], kHarmonicDivisors[k]);
1299 bool advanceFrame() noexcept
1301 const typename OnsetDetector<T>::OdfFrame f = onset_.getLastOdfFrame();
1304 const double raw = (std::isfinite(f.value) && f.value > T(0))
1305 ?
static_cast<double>(f.value) : 0.0;
1306 lastFrameRef_ = f.referenceSample;
1317 baseline_ += (raw - baseline_) * baselineCoef_;
1318 const double o = std::max(0.0, raw - baseline_);
1320 for (
int i = 0; i < bankSize_; ++i)
1322 const size_t u =
static_cast<size_t>(i);
1323 const double cr = bankCosCoef_[u];
1324 const double ci = bankSinCoef_[u];
1325 const double re = zRe_[u];
1326 const double im = zIm_[u];
1327 zRe_[u] = cr * re - ci * im + o;
1328 zIm_[u] = cr * im + ci * re;
1329 zMass_[u] = bankDecay_[u] * zMass_[u] + o;
1331 const double mass = zMass_[u];
1332 bankCoherence_[u] = (mass > kMassFloor)
1333 ? std::min(1.0, std::sqrt(zRe_[u] * zRe_[u]
1334 + zIm_[u] * zIm_[u]) / mass)
1338 int iLo = 0, iHi = 0;
1339 unpackRange(activeRange_.load(std::memory_order_relaxed), iLo, iHi);
1340 iLo = std::clamp(iLo, 0, bankSize_ - 1);
1341 iHi = std::clamp(iHi, iLo, bankSize_ - 1);
1344 double bestScore = 0.0;
1345 for (
int i = iLo; i <= iHi; ++i)
1347 const double s = scoreAt(i);
1348 if (s > bestScore) { bestScore = s; best = i; }
1366 double runnerUp = 0.0;
1367 const int apart = std::max(1,
static_cast<int>(kCandidateSeparationOctaves
1369 for (
int i = iLo; i <= iHi; ++i)
1371 if (std::abs(i - best) < apart)
continue;
1372 runnerUp = std::max(runnerUp, scoreAt(i,
false));
1374 const double bestExplains = scoreAt(best,
false);
1375 const double share = (bestExplains > 0.0)
1376 ? std::clamp(runnerUp / bestExplains, 0.0, 1.0) : 0.0;
1385 for (
int guard = 0; guard < 2; ++guard)
1387 if (best > 0 && bankCoherence_[
static_cast<size_t>(best - 1)]
1388 > bankCoherence_[
static_cast<size_t>(best)]) --best;
1389 else if (best + 1 < bankSize_
1390 && bankCoherence_[
static_cast<size_t>(best + 1)]
1391 > bankCoherence_[
static_cast<size_t>(best)]) ++best;
1395 const size_t ub =
static_cast<size_t>(best);
1396 const double period = refinePeriod(best);
1397 const double conf = std::clamp(bankCoherence_[ub] * ambiguityDiscount(share),
1400 tempoAndConfidence_.store(packTempo(
static_cast<float>(periodToBpm(period)),
1401 static_cast<float>(conf)),
1402 std::memory_order_relaxed);
1407 const double w = 2.0 *
static_cast<double>(pi<double>) / bankPeriod_[ub];
1408 double sinceBeat = std::atan2(zIm_[ub], zRe_[ub]) / w;
1409 const double p = bankPeriod_[ub];
1410 while (sinceBeat < 0.0) sinceBeat += p;
1411 while (sinceBeat >= p) sinceBeat -= p;
1420 if (havePhase_ && (prevSinceBeat_ - sinceBeat) > 0.5 * p
1421 &&
static_cast<double>(framesSinceBeat_) >= kBeatRefractory * p)
1423 const int64_t backFrames =
static_cast<int64_t
>(std::lround(sinceBeat));
1424 const int64_t ref = lastFrameRef_ - backFrames *
static_cast<int64_t
>(hop_);
1425 lastBeatSample_.store(ref, std::memory_order_relaxed);
1426 framesSinceBeat_ = 0;
1429 prevSinceBeat_ = sinceBeat;
1430 lastBestIndex_ = best;
1444 [[nodiscard]]
double refinePeriod(
int i)
const noexcept
1446 if (i <= 0 || i >= bankSize_ - 1)
return bankPeriod_[
static_cast<size_t>(i)];
1447 const double a = scoreAt(i - 1,
false);
1448 const double b = scoreAt(i,
false);
1449 const double c = scoreAt(i + 1,
false);
1450 const double den = a - 2.0 * b + c;
1451 if (!(std::abs(den) > 0.0))
return bankPeriod_[
static_cast<size_t>(i)];
1452 double d = 0.5 * (a - c) / den;
1453 d = std::clamp(d, -0.5, 0.5);
1454 return bankPeriod_[
static_cast<size_t>(i)]
1455 * std::pow(2.0, d /
static_cast<double>(kBinsPerOctave));
1472 [[nodiscard]]
double scoreAt(
int i,
bool withPreference =
true) const noexcept
1474 const size_t u =
static_cast<size_t>(i);
1475 double s = bankPrior_[u] * bankCoherence_[u];
1476 for (
int k = 0; k < kNumHarmonics; ++k)
1478 const int sub = i + harmonicOffset_[k];
1479 if (sub >= bankSize_)
continue;
1480 const double r = withPreference
1481 ? bankLevelRatio_[
static_cast<size_t>(k) *
static_cast<size_t>(bankSize_) + u]
1483 s *= (1.0 - std::min(1.0, r * bankCoherence_[
static_cast<size_t>(sub)]));
1508 [[nodiscard]]
static double levelPreference(
double periodSeconds)
noexcept
1510 const double lg = std::log2(periodSeconds / kPriorCentreSeconds);
1511 const double w = (lg >= 0.0) ? kTactusSlowOctaves : kTactusFastOctaves;
1512 const double x = lg / w;
1513 return std::exp(-0.5 * x * x);
1516 [[nodiscard]]
double levelRatio(
double periodFrames,
double divisor)
const noexcept
1518 if (!(frameRate_ > 0.0) || !(periodFrames > 0.0))
return 1.0;
1519 const double p = periodFrames / frameRate_;
1520 const double here = levelPreference(p);
1521 if (!(here > 0.0))
return 1.0;
1522 return std::max(1.0, levelPreference(p * divisor) / here);
1531 void appendEnvelopeFrame()
1533 const typename OnsetDetector<T>::OdfFrame f = onset_.getLastOdfFrame();
1534 const double v = std::isfinite(f.value) ?
static_cast<double>(f.value) : 0.0;
1535 env_.push_back(std::max(0.0, v));
1536 envRef_.push_back(f.referenceSample);
1537 for (
int g = 0; g < kRegisters; ++g)
1539 const double r =
static_cast<double>(f.registers[
static_cast<size_t>(g)]);
1540 envReg_[
static_cast<size_t>(g)].push_back(std::isfinite(r) ? std::max(0.0, r) : 0.0);
1555 void conditionEnvelope()
1557 const int n =
static_cast<int>(env_.size());
1575 bool haveRegisters =
false;
1576 for (
const auto& r : envReg_)
1577 if (static_cast<int>(r.size()) == n)
1578 for (const double v : r)
1579 if (v > 0.0) { haveRegisters =
true;
break; }
1587 int iLo = 0, iHi = 0;
1588 unpackRange(activeRange_.load(std::memory_order_relaxed), iLo, iHi);
1589 const int lagLo = std::max(1,
static_cast<int>(std::floor(bankPeriod_[
static_cast<size_t>(iLo)])));
1590 const int lagHi = std::min(n / 2,
static_cast<int>(std::ceil(bankPeriod_[
static_cast<size_t>(iHi)])));
1591 envBal_.assign(
static_cast<size_t>(n), 0.0);
1592 for (
auto& r : envReg_)
1594 conditionSeries(r,
true);
1596 for (
const double v : r) zero += v * v;
1597 zero /=
static_cast<double>(n);
1600 for (
int lag = lagLo; lag <= lagHi; ++lag)
1604 for (
int i = 0; i + lag < n; ++i)
1605 acc += r[
static_cast<size_t>(i)] * r[
static_cast<size_t>(i + lag)];
1606 peak = std::max(peak, acc /
static_cast<double>(n - lag) / zero);
1608 const double w = std::clamp(peak, 0.0, 1.0);
1609 for (
int i = 0; i < n; ++i) envBal_[static_cast<size_t>(i)] += w * r[
static_cast<size_t>(i)];
1611 conditionSeries(envBal_,
false);
1613 conditionSeries(env_,
true);
1617 void conditionSeries(WorkVector<double>& e,
bool removeFloor)
1619 const int n =
static_cast<int>(e.size());
1622 const int half = std::max(1,
static_cast<int>(std::lround(0.5 * kBaselineSeconds
1624 scratchA_.assign(
static_cast<size_t>(n) + 1, 0.0);
1625 for (
int i = 0; i < n; ++i)
1626 scratchA_[
static_cast<size_t>(i) + 1] = scratchA_[
static_cast<size_t>(i)]
1627 + e[
static_cast<size_t>(i)];
1630 for (
int i = 0; i < n; ++i)
1632 if ((i & 255) == 0) checkOfflineWork();
1633 const int lo = std::max(0, i - half);
1634 const int hi = std::min(n, i + half + 1);
1635 const double mean = (scratchA_[
static_cast<size_t>(hi)]
1636 - scratchA_[
static_cast<size_t>(lo)])
1637 /
static_cast<double>(hi - lo);
1638 const double v = removeFloor ? std::max(0.0, e[
static_cast<size_t>(i)] - mean)
1639 : e[static_cast<size_t>(i)];
1640 e[
static_cast<size_t>(i)] = v;
1644 const double rms = std::sqrt(sumSq /
static_cast<double>(n));
1647 const double g = 1.0 / rms;
1648 for (
int i = 0; i < n; ++i) e[static_cast<size_t>(i)] *= g;
1662 [[nodiscard]]
double coherence(
double periodFrames)
const
1664 return coherence(env_, periodFrames);
1667 [[nodiscard]]
double coherence(
const WorkVector<double>& envelope,
1668 double periodFrames)
const
1670 const int n =
static_cast<int>(envelope.size());
1671 if (n < 2 || !(periodFrames > 1.0))
return 0.0;
1673 const int win = std::max(4,
static_cast<int>(std::lround(kCoherenceWindowBeats
1675 if (win > n)
return coherenceWindow(envelope, 0, n, periodFrames);
1677 const int step = std::max(1, win / 2);
1680 for (
int start = 0; start + win <= n; start += step)
1682 acc += coherenceWindow(envelope, start, start + win, periodFrames);
1685 if (count == 0)
return coherenceWindow(envelope, 0, n, periodFrames);
1686 return acc /
static_cast<double>(count);
1689 [[nodiscard]]
double coherenceWindow(
const WorkVector<double>& envelope,
1690 int from,
int to,
double periodFrames)
const
1692 const double w = 2.0 *
static_cast<double>(pi<double>) / periodFrames;
1693 double re = 0.0, im = 0.0, mass = 0.0;
1694 for (
int i = from; i < to; ++i)
1696 if ((i & 4095) == 0) checkOfflineWork();
1697 const double v = envelope[
static_cast<size_t>(i)];
1698 const double a = w *
static_cast<double>(i);
1699 re += v * std::cos(a);
1700 im -= v * std::sin(a);
1703 if (!(mass > kMassFloor))
return 0.0;
1704 return std::min(1.0, std::sqrt(re * re + im * im) / mass);
1708 [[nodiscard]] std::array<double, 2> offlineCandidateScores(
1709 const WorkVector<double>& envelope,
double period)
const
1711 const double centre = kPriorCentreSeconds * frameRate_;
1712 const double lg = std::log2(period / centre) / kPriorWidthOctaves;
1713 const double base = std::exp(-0.5 * lg * lg) * coherence(envelope, period);
1714 double adjudicated = base, explains = base;
1715 for (
const double multiple : kHarmonicDivisors)
1717 const double share = coherence(envelope, multiple * period);
1718 adjudicated *= (1.0 - std::min(1.0, levelRatio(period, multiple) * share));
1719 explains *= (1.0 - std::min(1.0, share));
1721 return { adjudicated, explains };
1747 void pickCandidates(
int iLo,
int iHi,
double& tau1,
double& tau2)
1751 secondaryShare_ = 0.0;
1752 fundamentalFound_ =
false;
1754 const int n =
static_cast<int>(env_.size());
1755 const int lagLo = std::max(1,
static_cast<int>(std::floor(
1756 bankPeriod_[
static_cast<size_t>(iLo)])));
1757 const int lagHi = std::min(n / 2,
static_cast<int>(std::ceil(
1758 bankPeriod_[
static_cast<size_t>(iHi)])));
1759 if (lagHi <= lagLo)
return;
1764 acf_.assign(
static_cast<size_t>(lagHi + 2), 0.0);
1765 const double centre = kPriorCentreSeconds * frameRate_;
1766 for (
int lag = std::max(1, lagLo - 1); lag <= lagHi + 1; ++lag)
1770 const int m = n - lag;
1771 for (
int i = 0; i < m; ++i)
1772 s += env_[
static_cast<size_t>(i)] * env_[
static_cast<size_t>(i + lag)];
1773 s /=
static_cast<double>(m);
1774 const double lg = std::log2(
static_cast<double>(lag) / centre)
1775 / kPriorWidthOctaves;
1776 acf_[
static_cast<size_t>(lag)] = s * std::exp(-0.5 * lg * lg);
1781 candidates_.clear();
1782 for (
int lag = lagLo; lag <= lagHi; ++lag)
1784 const double v = acf_[
static_cast<size_t>(lag)];
1785 if (v > acf_[
static_cast<size_t>(lag - 1)]
1786 && v >= acf_[
static_cast<size_t>(lag + 1)] && v > 0.0)
1790 const double refined = refineLag(lag);
1791 if (refined >= bankPeriod_[
static_cast<size_t>(iLo)]
1792 && refined <= bankPeriod_[
static_cast<size_t>(iHi)])
1793 candidates_.push_back(lag);
1796 if (candidates_.empty())
return;
1808 for (
const int lag : candidates_) top = std::max(top, acf_[static_cast<size_t>(lag)]);
1810 for (
const int lag : candidates_)
1811 if (acf_[static_cast<size_t>(lag)] >= kCandidatePeakFloor * top)
1812 candidates_[w++] = lag;
1813 candidates_.resize(w);
1820 candScore_.assign(candidates_.size(), 0.0);
1821 candExplains_.assign(candidates_.size(), 0.0);
1822 for (
size_t k = 0; k < candidates_.size(); ++k)
1824 const double tau = refineLag(candidates_[k]);
1825 const auto scores = offlineCandidateScores(env_, tau);
1826 candScore_[k] = scores[0];
1827 candExplains_[k] = scores[1];
1831 for (
size_t k = 1; k < candidates_.size(); ++k)
1832 if (candScore_[k] > candScore_[k1]) k1 = k;
1843 bool anyFundamental =
false;
1844 for (
size_t k = 0; k < candidates_.size(); ++k)
1845 if (coherence(refineLag(candidates_[k])) >= kMinFundamentalCoherence)
1846 { anyFundamental =
true;
break; }
1847 fundamentalFound_ = anyFundamental;
1848 if (!anyFundamental)
1851 for (
size_t k = 1; k < candidates_.size(); ++k)
1852 if (acf_[
static_cast<size_t>(candidates_[k])]
1853 > acf_[
static_cast<size_t>(candidates_[k1])]) k1 = k;
1854 for (
size_t k = 0; k < candidates_.size(); ++k)
1855 candScore_[k] = acf_[
static_cast<size_t>(candidates_[k])];
1857 tau1 = refineLag(candidates_[k1]);
1869 double bestOther = 0.0;
1870 for (
size_t k = 0; k < candidates_.size(); ++k)
1872 const double sep = std::abs(std::log2(
static_cast<double>(candidates_[k])
1873 /
static_cast<double>(candidates_[k1])));
1874 if (sep < kCandidateSeparationOctaves)
continue;
1875 if (k2 < 0 || candScore_[k] > candScore_[
static_cast<size_t>(k2)])
1876 k2 =
static_cast<int>(k);
1877 bestOther = std::max(bestOther, candExplains_[k]);
1881 tau2 = refineLag(candidates_[
static_cast<size_t>(k2)]);
1882 const double top = candExplains_[k1];
1883 secondaryShare_ = (top > 0.0) ? std::clamp(bestOther / top, 0.0, 1.0) : 0.0;
1888 [[nodiscard]]
double refineLag(
int lag)
const noexcept
1890 if (lag <= 0 || lag + 1 >=
static_cast<int>(acf_.size()))
1891 return static_cast<double>(lag);
1892 const double a = acf_[
static_cast<size_t>(lag - 1)];
1893 const double b = acf_[
static_cast<size_t>(lag)];
1894 const double c = acf_[
static_cast<size_t>(lag + 1)];
1895 const double den = a - 2.0 * b + c;
1896 if (!(std::abs(den) > 0.0))
return static_cast<double>(lag);
1897 const double d = std::clamp(0.5 * (a - c) / den, -0.5, 0.5);
1898 return static_cast<double>(lag) + d;
1902 [[nodiscard]] std::array<double, kLevelRawFeatures> levelFeatures(
1903 double period,
const WorkVector<double>& balanced)
const noexcept
1905 std::array<double, kLevelRawFeatures> features {};
1907 features[k++] = std::log2(periodToBpm(period) / 120.0);
1908 features[k++] = pulseShare(env_, period);
1909 features[k++] = pulseShare(balanced, period);
1910 for (
const auto& r : envReg_) features[k++] = pulseShare(r, period);
1911 features[k++] = pulseShare(env_, 2.0 * period);
1912 for (
const auto& r : envReg_) features[k++] = pulseShare(r, 2.0 * period);
1913 features[k++] = repetition(env_, period);
1914 features[k++] = repetition(balanced, period);
1915 features[k++] = pulseShare(env_, 3.0 * period);
1916 features[k++] = pulseShare(envReg_[0], 3.0 * period);
1917 features[k++] = pulseShare(balanced, 2.0 * period);
1918 features[k++] = pulseShare(balanced, 3.0 * period);
1936 void chooseMetricalLevel(
int iLo,
int iHi,
double& tau1,
double& tau2)
1938 const double pMin = bankPeriod_[
static_cast<size_t>(iLo)];
1939 const double pMax = bankPeriod_[
static_cast<size_t>(iHi)];
1940 const WorkVector<double>& balanced = (envBal_.size() == env_.size()) ? envBal_ : env_;
1941 std::array<std::array<double, kLevelRawFeatures>, kLevelCandidates> raw {};
1942 for (
int c = 0; c < kLevelCandidates; ++c)
1943 raw[
static_cast<size_t>(c)] = levelFeatures(
1944 tau1 * kLevelMultiples[
static_cast<size_t>(c)], balanced);
1946 const auto& proposed = raw[
static_cast<size_t>(kLevelMiddle)];
1947 if (!(proposed[kLevelShareAtPeriod] > 0.0))
return;
1948 const double contrastFloor = kLevelContrastFloor * proposed[kLevelShareAtPeriod];
1949 int best = kLevelMiddle;
1950 if (proposed[kLevelShareAtDouble] >= contrastFloor)
1953 double bestScore = 0.0;
1954 for (
int c = 0; c < kLevelCandidates; ++c)
1956 const double period = tau1 * kLevelMultiples[
static_cast<size_t>(c)];
1957 if (c != kLevelMiddle && (period < pMin || period > pMax))
continue;
1958 const auto& f = raw[
static_cast<size_t>(c)];
1959 double score = kLevelBias[
static_cast<size_t>(c)];
1960 for (
size_t j = 0; j < kLevelRawFeatures; ++j)
1961 score += kLevelWeights[j] * f[j]
1962 + kLevelWeights[kLevelRawFeatures + j] * (f[j] - proposed[j]);
1963 score += kLevelWeights[2 * kLevelRawFeatures] * f[0] * f[0];
1964 if (best < 0 || score > bestScore) { best = c; bestScore = score; }
1968 const double binaryPeriod = tau1 * kLevelMultiples[
static_cast<size_t>(best)];
1969 double chosenPeriod = binaryPeriod;
1970 bool ternaryChosen =
false;
1971 std::array<double, kTernaryMultiples.size()> supportedPeriods {};
1972 size_t supportedCount = 0;
1973 if (std::max(proposed[kLevelShareAtDouble], proposed[kLevelShareAtTriple]) >= contrastFloor)
1975 const auto& reference = raw[
static_cast<size_t>(best)];
1976 double bestGain = 0.0;
1977 for (
size_t c = 0; c < kTernaryMultiples.size(); ++c)
1979 const double period = tau1 * kTernaryMultiples[c];
1980 if (period < pMin || period > pMax)
continue;
1981 const auto f = levelFeatures(period, balanced);
1982 if (f[kLevelBalancedShareAtPeriod] < kMinFundamentalCoherence)
continue;
1983 supportedPeriods[supportedCount++] = period;
1984 double gain = kTernaryBias[c];
1985 for (
size_t j = 0; j < kLevelRawFeatures; ++j)
1986 gain += kTernaryWeights[j] * f[j]
1987 + kTernaryWeights[kLevelRawFeatures + j] * (f[j] - reference[j]);
1988 gain += kTernaryWeights[2 * kLevelRawFeatures] * f[0] * f[0];
1989 if (gain > bestGain)
1992 chosenPeriod = period;
1993 ternaryChosen =
true;
1999 tau2 = binaryPeriod;
2000 tau1 = chosenPeriod;
2002 else if (best != kLevelMiddle)
2005 tau1 = binaryPeriod;
2007 if (ternaryChosen || best != kLevelMiddle)
2008 refreshMetricalAmbiguity(balanced, tau1, tau2,
2009 { supportedPeriods.data(), supportedCount });
2013 void refreshMetricalAmbiguity(
const WorkVector<double>& envelope,
2014 double chosen,
double displaced,
2015 std::span<const double> addedPeriods)
2017 const double top = offlineCandidateScores(envelope, chosen)[1];
2018 double bestOther = 0.0;
2019 for (
size_t k = 0; k < candidates_.size(); ++k)
2020 if (std::abs(std::log2(refineLag(candidates_[k]) / chosen))
2021 >= kCandidateSeparationOctaves)
2022 bestOther = std::max(bestOther, candExplains_[k]);
2023 const auto consider = [&](
double period) {
2024 if (period > 0.0 && std::abs(std::log2(period / chosen))
2025 >= kCandidateSeparationOctaves)
2026 bestOther = std::max(bestOther, offlineCandidateScores(envelope, period)[1]);
2028 consider(displaced);
2029 for (
const double period : addedPeriods) consider(period);
2030 secondaryShare_ = top > 0.0 ? std::clamp(bestOther / top, 0.0, 1.0)
2031 : (bestOther > 0.0 ? 1.0 : 0.0);
2044 [[nodiscard]]
static double pulseShare(
const WorkVector<double>& e,
2045 double periodFrames)
noexcept
2047 const int n =
static_cast<int>(e.size());
2048 const int win =
static_cast<int>(kLevelWindowBeats * periodFrames);
2049 if (win < 4)
return 0.0;
2050 const double w = 2.0 *
static_cast<double>(pi<double>) / periodFrames;
2051 const double dc = std::cos(w), ds = std::sin(w);
2052 double acc = 0.0, mass = 0.0;
2053 for (
int start = 0; start + win <= n; start += win / 2)
2058 double c = std::cos(w *
static_cast<double>(start));
2059 double s = std::sin(w *
static_cast<double>(start));
2060 double re = 0.0, im = 0.0, m = 0.0;
2061 for (
int i = start; i < start + win; ++i)
2063 const double v = std::max(0.0, e[
static_cast<size_t>(i)]);
2067 const double cn = c * dc - s * ds;
2068 s = s * dc + c * ds;
2071 if (m > 0.0) { acc += std::sqrt(re * re + im * im); mass += m; }
2073 return (mass > 0.0) ? acc / mass : 0.0;
2078 [[nodiscard]]
static double repetition(
const WorkVector<double>& e,
2079 double periodFrames)
noexcept
2081 const int n =
static_cast<int>(e.size());
2082 const int lag =
static_cast<int>(std::lround(periodFrames));
2083 if (lag <= 0 || lag >= n)
return 0.0;
2084 double s0 = 0.0, s = 0.0;
2085 for (
int i = 0; i < n; ++i) s0 += e[static_cast<size_t>(i)] * e[
static_cast<size_t>(i)];
2086 for (
int i = 0; i + lag < n; ++i)
2087 s += e[
static_cast<size_t>(i)] * e[
static_cast<size_t>(i + lag)];
2088 return (s0 > 0.0) ? s / s0 *
static_cast<double>(n) /
static_cast<double>(n - lag)
2111 void computeLocalPeriods(
double globalTau)
2113 const int n =
static_cast<int>(env_.size());
2114 localPeriod_.assign(
static_cast<size_t>(n), globalTau);
2115 if (n < 8 || bankSize_ < 2)
return;
2124 int aLo = 0, aHi = 0;
2125 unpackRange(activeRange_.load(std::memory_order_relaxed), aLo, aHi);
2126 aLo = std::clamp(aLo, 0, bankSize_ - 1);
2127 aHi = std::clamp(aHi, aLo, bankSize_ - 1);
2129 const int gi = periodToIndex(globalTau);
2130 int lo = std::clamp(gi - kLocalPeriodSpan, aLo, aHi);
2131 int hi = std::clamp(gi + kLocalPeriodSpan, aLo, aHi);
2132 if (hi < lo) std::swap(lo, hi);
2133 if (hi <= lo)
return;
2135 sweepA_.assign(
static_cast<size_t>(n), globalTau);
2136 sweepB_.assign(
static_cast<size_t>(n), globalTau);
2137 sweepPeriods(lo, hi,
false, sweepA_);
2138 sweepPeriods(lo, hi,
true, sweepB_);
2140 for (
int i = 0; i < n; ++i)
2142 if ((i & 255) == 0) checkOfflineWork();
2143 const double a = sweepA_[
static_cast<size_t>(i)];
2144 const double b = sweepB_[
static_cast<size_t>(i)];
2145 localPeriod_[
static_cast<size_t>(i)] =
2146 (a > 0.0 && b > 0.0) ? std::sqrt(a * b) : globalTau;
2152 const int half = std::max(1,
static_cast<int>(std::lround(globalTau)));
2153 scratchA_.assign(
static_cast<size_t>(n) + 1, 0.0);
2154 for (
int i = 0; i < n; ++i)
2155 scratchA_[
static_cast<size_t>(i) + 1] = scratchA_[
static_cast<size_t>(i)]
2156 + std::log(localPeriod_[
static_cast<size_t>(i)]);
2157 for (
int i = 0; i < n; ++i)
2159 if ((i & 255) == 0) checkOfflineWork();
2160 const int a = std::max(0, i - half);
2161 const int b = std::min(n, i + half + 1);
2162 const double m = (scratchA_[
static_cast<size_t>(b)]
2163 - scratchA_[
static_cast<size_t>(a)])
2164 /
static_cast<double>(b - a);
2165 localPeriod_[
static_cast<size_t>(i)] = std::exp(m);
2172 void sweepPeriods(
int lo,
int hi,
bool reverse, WorkVector<double>& out)
2174 const int n =
static_cast<int>(env_.size());
2175 std::fill(zRe_.begin(), zRe_.end(), 0.0);
2176 std::fill(zIm_.begin(), zIm_.end(), 0.0);
2177 std::fill(zMass_.begin(), zMass_.end(), 0.0);
2179 for (
int k = 0; k < n; ++k)
2181 if ((k & 255) == 0) checkOfflineWork();
2182 const int i = reverse ? (n - 1 - k) : k;
2183 const double o = env_[
static_cast<size_t>(i)];
2186 double bestValue = 0.0;
2187 for (
int b = lo; b <= hi; ++b)
2189 const size_t u =
static_cast<size_t>(b);
2190 const double re = zRe_[u], im = zIm_[u];
2191 zRe_[u] = bankCosCoef_[u] * re - bankSinCoef_[u] * im + o;
2192 zIm_[u] = bankCosCoef_[u] * im + bankSinCoef_[u] * re;
2193 zMass_[u] = bankDecay_[u] * zMass_[u] + o;
2194 const double mass = zMass_[u];
2195 const double c = (mass > kMassFloor)
2196 ? std::sqrt(zRe_[u] * zRe_[u] + zIm_[u] * zIm_[u]) / mass : 0.0;
2197 bankCoherence_[u] = c;
2198 const double s = bankPrior_[u] * c;
2199 if (s > bestValue) { bestValue = s; best = b; }
2201 out[
static_cast<size_t>(i)] = (best >= 0)
2202 ? bankPeriod_[
static_cast<size_t>(best)] : 0.0;
2209 [[nodiscard]]
static double ambiguityDiscount(
double share)
noexcept
2211 if (!(share > kAmbiguityFloor))
return 1.0;
2212 return std::clamp(1.0 - (share - kAmbiguityFloor) / (1.0 - kAmbiguityFloor),
2217 [[nodiscard]]
int periodToIndex(
double periodFrames)
const noexcept
2219 if (!(periodFrames > 0.0) || bankSize_ < 1)
return 0;
2220 const double x = std::log2(periodFrames / bankPeriod_[0])
2221 *
static_cast<double>(kBinsPerOctave);
2222 return std::clamp(
static_cast<int>(std::lround(x)), 0, bankSize_ - 1);
2229 bool buildGrid(
double periodFrames, WorkVector<int64_t>& gridOut)
2232 WorkVector<int> beatFrames(detail::AccountedAllocator<int>{&allocationAccount_});
2233 if (!runDp(periodFrames, beatFrames) || beatFrames.size() < 2)
return false;
2234 gridOut.reserve(beatFrames.size());
2235 for (
const int f : beatFrames)
2236 gridOut.push_back(beatSample(f));
2253 bool runDp(
double periodFrames, WorkVector<int>& beatFrames)
2256 const int n =
static_cast<int>(env_.size());
2257 const bool varying = (localPeriod_.size() == env_.size());
2258 const int lo = std::max(1,
static_cast<int>(std::lround(kSearchLowFactor
2260 const int hi = std::max(lo + 1,
static_cast<int>(std::lround(kSearchHighFactor
2262 if (n <= hi + 2)
return false;
2264 buildLocalScore(periodFrames);
2266 const double alpha = tightness_.load(std::memory_order_relaxed) * alphaScale_;
2267 cumScore_.assign(
static_cast<size_t>(n), 0.0);
2268 backlink_.assign(
static_cast<size_t>(n), -1);
2270 for (
int i = 0; i < n; ++i)
2272 if ((i & 255) == 0) checkOfflineWork();
2273 const double target = varying ? localPeriod_[
static_cast<size_t>(i)]
2275 const int dLo = varying
2276 ? std::max(1,
static_cast<int>(std::lround(kSearchLowFactor * target)))
2278 const int dHiBound = varying
2279 ? std::max(dLo + 1,
static_cast<int>(std::lround(kSearchHighFactor
2285 const int dHi = std::min(dHiBound, i);
2286 for (
int d = dLo; d <= dHi; ++d)
2288 const int j = i - d;
2289 const double lr = std::log(
static_cast<double>(d) / target);
2290 const double s = cumScore_[
static_cast<size_t>(j)] - alpha * lr * lr;
2291 if (bestJ < 0 || s > best) { best = s; bestJ = j; }
2293 cumScore_[
static_cast<size_t>(i)] = local_[
static_cast<size_t>(i)]
2294 + ((bestJ >= 0) ? best : 0.0);
2295 backlink_[
static_cast<size_t>(i)] = bestJ;
2299 for (
int i = 1; i < n; ++i)
2300 if (cumScore_[
static_cast<size_t>(i)] > cumScore_[
static_cast<size_t>(end)])
2303 for (
int i = end; i >= 0; i = backlink_[
static_cast<size_t>(i)])
2305 beatFrames.push_back(i);
2306 if (backlink_[
static_cast<size_t>(i)] < 0)
break;
2308 std::reverse(beatFrames.begin(), beatFrames.end());
2310 trimBeats(beatFrames);
2311 if (beatFrames.size() < 2)
return false;
2313 snapBeats(beatFrames, periodFrames);
2314 extendGrid(beatFrames);
2315 snapBeats(beatFrames, periodFrames);
2316 return beatFrames.size() >= 2;
2333 void snapBeats(WorkVector<int>& beatFrames,
double periodFrames)
const
2335 const int n =
static_cast<int>(env_.size());
2336 const int win = std::max(1,
static_cast<int>(std::lround(periodFrames
2339 for (
size_t k = 0; k < beatFrames.size(); ++k)
2341 const int f = beatFrames[k];
2343 double bestValue = local_[
static_cast<size_t>(f)];
2344 const int lo = std::max(previous + 1, f - win);
2345 const int hi = std::min(n - 1, f + win);
2346 for (
int i = lo; i <= hi; ++i)
2348 if (local_[
static_cast<size_t>(i)] > bestValue)
2350 bestValue = local_[
static_cast<size_t>(i)];
2354 beatFrames[k] = best;
2374 void extendGrid(WorkVector<int>& beatFrames)
const
2376 if (beatFrames.size() < 3)
return;
2377 const int n =
static_cast<int>(env_.size());
2379 WorkVector<int> ibi(detail::AccountedAllocator<int>{&allocationAccount_});
2380 ibi.reserve(beatFrames.size());
2381 for (
size_t k = 1; k < beatFrames.size(); ++k)
2382 ibi.push_back(beatFrames[k] - beatFrames[k - 1]);
2383 std::sort(ibi.begin(), ibi.end());
2384 const int step = ibi[ibi.size() / 2];
2385 if (step < 2)
return;
2388 for (
const int f : beatFrames) mean += local_[static_cast<size_t>(f)];
2389 mean /=
static_cast<double>(beatFrames.size());
2390 const double floorValue = kTrimFraction * mean;
2391 const int win = std::max(1,
static_cast<int>(std::lround(step * kSnapFraction)));
2393 WorkVector<int> front(detail::AccountedAllocator<int>{&allocationAccount_});
2394 for (
int guess = beatFrames.front() - step; guess - win >= 0; guess -= step)
2397 double bestValue = floorValue;
2398 for (
int i = std::max(0, guess - win); i <= std::min(n - 1, guess + win); ++i)
2399 if (local_[
static_cast<size_t>(i)] > bestValue)
2400 { bestValue = local_[
static_cast<size_t>(i)]; best = i; }
2401 if (best < 0)
break;
2402 front.push_back(best);
2405 std::reverse(front.begin(), front.end());
2407 WorkVector<int> back(detail::AccountedAllocator<int>{&allocationAccount_});
2408 for (
int guess = beatFrames.back() + step; guess + win <= n - 1; guess += step)
2411 double bestValue = floorValue;
2412 for (
int i = std::max(0, guess - win); i <= std::min(n - 1, guess + win); ++i)
2413 if (local_[
static_cast<size_t>(i)] > bestValue)
2414 { bestValue = local_[
static_cast<size_t>(i)]; best = i; }
2415 if (best < 0)
break;
2416 back.push_back(best);
2420 if (front.empty() && back.empty())
return;
2421 WorkVector<int> merged(detail::AccountedAllocator<int>{&allocationAccount_});
2422 merged.reserve(front.size() + beatFrames.size() + back.size());
2423 merged.insert(merged.end(), front.begin(), front.end());
2424 merged.insert(merged.end(), beatFrames.begin(), beatFrames.end());
2425 merged.insert(merged.end(), back.begin(), back.end());
2426 beatFrames.swap(merged);
2432 void buildLocalScore(
double periodFrames)
2434 const int n =
static_cast<int>(env_.size());
2435 local_.assign(
static_cast<size_t>(n), 0.0);
2437 const double sigma = std::max(0.5, kLocalScoreSigmaPeriods * periodFrames);
2438 const int half = std::max(1,
static_cast<int>(std::lround(3.0 * sigma)));
2439 kernel_.assign(
static_cast<size_t>(2 * half + 1), 0.0);
2441 for (
int k = -half; k <= half; ++k)
2443 const double x =
static_cast<double>(k) / sigma;
2444 const double v = std::exp(-0.5 * x * x);
2445 kernel_[
static_cast<size_t>(k + half)] = v;
2449 for (
double& v : kernel_) v /= ksum;
2451 for (
int i = 0; i < n; ++i)
2453 if ((i & 255) == 0) checkOfflineWork();
2455 const int kLo = std::max(-half, -i);
2456 const int kHi = std::min(half, n - 1 - i);
2457 for (
int k = kLo; k <= kHi; ++k)
2458 acc += env_[
static_cast<size_t>(i + k)]
2459 * kernel_[
static_cast<size_t>(k + half)];
2460 local_[
static_cast<size_t>(i)] = acc;
2467 void trimBeats(WorkVector<int>& beatFrames)
const
2469 if (beatFrames.size() < 3)
return;
2472 for (
const int f : beatFrames) mean += local_[static_cast<size_t>(f)];
2473 mean /=
static_cast<double>(beatFrames.size());
2474 const double floorValue = kTrimFraction * mean;
2477 while (first + 2 < beatFrames.size()
2478 && local_[
static_cast<size_t>(beatFrames[first])] < floorValue)
2481 size_t last = beatFrames.size() - 1;
2482 while (last > first + 1
2483 && local_[
static_cast<size_t>(beatFrames[last])] < floorValue)
2486 if (first > 0 || last + 1 < beatFrames.size())
2487 beatFrames = WorkVector<int>(beatFrames.begin()
2488 +
static_cast<std::ptrdiff_t
>(first),
2490 +
static_cast<std::ptrdiff_t
>(last) + 1,
2491 beatFrames.get_allocator());
2519 [[nodiscard]] int64_t beatSample(
int f)
const noexcept
2521 const int n =
static_cast<int>(env_.size());
2522 const int64_t base = envRef_[
static_cast<size_t>(f)];
2523 if (f <= 0 || f >= n - 1)
return base;
2524 const double a = env_[
static_cast<size_t>(f - 1)];
2525 const double b = env_[
static_cast<size_t>(f)];
2526 const double c = env_[
static_cast<size_t>(f + 1)];
2527 const double den = a - 2.0 * b + c;
2528 if (!(den < 0.0))
return base;
2529 const double d = std::clamp(0.5 * (a - c) / den, -0.5, 0.5);
2530 return base +
static_cast<int64_t
>(std::llround(d *
static_cast<double>(hop_)));
2541 [[nodiscard]]
double gridCoherence(
const WorkVector<int64_t>& beats)
const noexcept
2543 if (beats.size() < 2)
return 0.0;
2548 const WorkVector<double>& e = (envBal_.size() == env_.size()) ? envBal_ : env_;
2549 double re = 0.0, mass = 0.0;
2551 for (
size_t i = 0; i < e.size(); ++i)
2553 const int64_t r = envRef_[i];
2554 if (r < beats.front())
continue;
2555 while (k + 1 < beats.size() && beats[k + 1] <= r) ++k;
2556 if (k + 1 >= beats.size())
break;
2557 const double span =
static_cast<double>(beats[k + 1] - beats[k]);
2558 if (!(span > 0.0))
continue;
2559 const double phase =
static_cast<double>(r - beats[k]) / span;
2560 const double o = e[i];
2561 re += o * std::cos(2.0 * pi<double> * phase);
2564 return (mass > kMassFloor) ? re / mass : 0.0;
2588 [[nodiscard]]
static double fitBeatSlope(
const WorkVector<int64_t>& beats)
2590 const size_t n = beats.size();
2591 if (n < 2)
return 0.0;
2592 WorkVector<double> ibi(detail::AccountedAllocator<double>{beats.get_allocator().account()});
2594 for (
size_t k = 1; k < n; ++k) ibi.push_back(
static_cast<double>(beats[k] - beats[k - 1]));
2595 WorkVector<double> sorted = ibi;
2596 std::nth_element(sorted.begin(), sorted.begin() +
static_cast<std::ptrdiff_t
>(sorted.size() / 2), sorted.end());
2597 const double med = sorted[sorted.size() / 2];
2598 if (!(med > 0.0))
return 0.0;
2603 for (
size_t j = 0; j < ibi.size(); ++j)
2604 sorted[j] = std::max(1.0, std::round(ibi[j] / med));
2605 const auto stable = [&](
size_t j) {
2606 const double r = ibi[j] / med;
2607 return r >= 0.75 && std::abs(r - std::round(r)) < 0.125;
2609 for (
size_t j = 3; j + 3 < ibi.size(); ++j)
2611 if (stable(j) || !stable(j - 1) || !stable(j - 2) || !stable(j - 3))
2613 for (
size_t width = 1; width <= 3; ++width)
2615 const size_t end = j + width;
2616 if (end + 3 > ibi.size()
2617 || !stable(end) || !stable(end + 1) || !stable(end + 2))
2620 for (
size_t q = j; q < end; ++q) total += ibi[q] / med;
2621 const double half = std::floor(total) + 0.5;
2622 if (std::abs(total - half) >= 0.125)
continue;
2623 for (
size_t q = j; q < end; ++q)
2624 sorted[q] = (ibi[q] / med) * half / total;
2630 double sx = 0.0, sy = 0.0, sxx = 0.0, sxy = 0.0, index = 0.0;
2631 for (
size_t k = 0; k < n; ++k)
2633 if (k > 0) index += sorted[k - 1];
2634 const double y =
static_cast<double>(beats[k]);
2635 sx += index; sy += y; sxx += index * index; sxy += index * y;
2637 const double dn =
static_cast<double>(n);
2638 const double den = dn * sxx - sx * sx;
2639 if (!(std::abs(den) > 0.0))
return 0.0;
2640 return (dn * sxy - sx * sy) / den;
2645 void resetState() noexcept
2648 std::fill(zRe_.begin(), zRe_.end(), 0.0);
2649 std::fill(zIm_.begin(), zIm_.end(), 0.0);
2650 std::fill(zMass_.begin(), zMass_.end(), 0.0);
2651 std::fill(bankCoherence_.begin(), bankCoherence_.end(), 0.0);
2657 prevSinceBeat_ = 0.0;
2658 framesSinceBeat_ = 0;
2659 lastBestIndex_ = -1;
2662 tempoAndConfidence_.store(packTempo(0.0f, 0.0f), std::memory_order_relaxed);
2663 beatLatched_.store(
false, std::memory_order_relaxed);
2664 lastBeatSample_.store(-1, std::memory_order_relaxed);
2669 static constexpr double kBeatRefractory = 0.5;
2673 static constexpr double kMassFloor = 1e-12;
2677 detail::AllocationAccount allocationAccount_;
2678 void* offlineCheckpointContext_ =
nullptr;
2679 void (*offlineCheckpoint_)(
void*) =
nullptr;
2680 void checkOfflineWork()
const
2682 if (offlineCheckpoint_) offlineCheckpoint_(offlineCheckpointContext_);
2684 OnsetDetector<T> onset_;
2686 double sampleRate_ = 44100.0;
2687 double frameRate_ = 200.0;
2692 WorkVector<double> bankPeriod_;
2693 WorkVector<double> bankCosCoef_, bankSinCoef_, bankDecay_, bankPrior_;
2694 int harmonicOffset_[kNumHarmonics] = { 0, 0, 0 };
2695 WorkVector<double> zRe_, zIm_, zMass_, bankCoherence_;
2696 WorkVector<double> bankLevelRatio_;
2699 int64_t samplesPushed_ = 0;
2700 int64_t frameIndex_ = 0;
2701 int64_t lastFrameRef_ = 0;
2702 double baseline_ = 0.0;
2703 double baselineCoef_ = 0.005;
2704 double prevSinceBeat_ = 0.0;
2705 int64_t framesSinceBeat_ = 0;
2706 int lastBestIndex_ = -1;
2707 bool havePhase_ =
false;
2710 int64_t offlinePushed_ = 0;
2711 bool offlineOpen_ =
false;
2712 WorkVector<double> env_;
2713 WorkVector<int64_t> envRef_;
2715 static_assert(kRegisters == 4,
"the level model (kLevelWeights) reads four register envelopes");
2716 std::array<WorkVector<double>, kRegisters> envReg_;
2717 WorkVector<double> envBal_;
2718 double alphaScale_ = 1.0;
2719 WorkVector<double> local_, cumScore_, acf_, scratchA_, kernel_;
2720 WorkVector<double> localPeriod_, sweepA_, sweepB_;
2721 WorkVector<int> backlink_;
2722 WorkVector<int> candidates_;
2723 WorkVector<double> candScore_, candExplains_;
2724 double secondaryShare_ = 0.0;
2725 bool fundamentalFound_ =
false;
2728 std::atomic<bool> prepared_ {
false };
2729 std::atomic<std::uint64_t> tempoAndConfidence_ { 0 };
2730 std::atomic<std::uint32_t> activeRange_ { 0 };
2731 std::atomic<double> tightness_ { kDefaultTightness };
2732 std::atomic<bool> beatLatched_ {
false };
2733 std::atomic<std::int64_t> lastBeatSample_ { -1 };
2734 std::atomic<std::int64_t> latencySamples_ { 549 };
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.
Tempo and beat tracking with an offline grid and a causal readout.
bool beatNow() const noexcept
True if a beat was reported during the most recent call.
void setTempoRange(T minBpm, T maxBpm) noexcept
Restricts the searched tempo range, in BPM. Lock free, no alloc.
void prepare(const AudioSpec &spec)
Allocates all causal state and prepares the shared front end.
void pushOffline(std::span< const T > samples)
Feeds the next piece of an offline session (channel-0 samples). No-op outside one.
T getTightness() const noexcept
Tightness currently in force.
const OnsetDetector< T > & getOnsetDetector() const noexcept
Read-only access to the front end, for callers that also want onsets and would otherwise run a second...
T getConfidence() const noexcept
Coherence of the envelope with the running tempo, in [0,1].
Result analyze(AudioBufferView< const T > whole)
Tracks tempo and beats over a whole mono buffer (channel 0).
void setTightness(T alpha) noexcept
Sets the dynamic-programming tightness (Ellis alpha).
int64_t getLastBeatSample() const noexcept
Sample index the most recent causal beat is attributed to; -1 before the first one.
void reset() noexcept
Clears all streaming state and abandons an open offline session. Not concurrent with pushSamples().
void beginOffline(int64_t expectedSamples=0)
Opens an incremental offline analysis.
Result finishOffline()
Closes the session and tracks tempo and beats over everything pushed, as analyze() does....
void processBlock(AudioBufferView< const T > in) noexcept
Feeds a mono block; reads channel 0 only. Const, never mutated. Lock free and allocation free....
void pushSamples(std::span< const T > samples) noexcept
Feeds a mono stream of samples. Lock free, allocation free.
void getTempoAndConfidence(T &bpmOut, T &confidenceOut) const noexcept
Running tempo and its confidence, from ONE load.
T getRunningTempoBpm() const noexcept
Running tempo estimate in BPM, 0 before the first frame.
int getLatencySamples() const noexcept
Delay between a beat and the call in which it is announced.
double getFrameRate() const noexcept
Analysis frames per second of the shared envelope.
Causal SuperFlux onset detector with lock-free readout.
static constexpr int kNumRegisters
One frame of the onset-strength envelope (the ODF before the peak picker).
Main namespace for the DSPark framework.
Describes the audio environment for a DSP processor.
double sampleRate
Sample rate in Hz.
What analyze() returns: one tempo, one grid, one number saying how much of the signal that grid expla...
std::vector< int64_t > beatSamples
Beat positions, ascending, in samples.
T secondaryTempoBpm
The competing hypothesis that lost.
T tempoBpm
Delivered tempo, fitted to the grid.
T confidence
Coherence of the envelope with the grid, [0,1].