164 if (!front_.prepare(spec, windowSize))
return;
165 prepared_.store(
true, std::memory_order_release);
180 chromaOut_.fill(T(0));
181 maxFrameTotal_ = 0.0;
184 packed_.store(pack(
Key {}), std::memory_order_relaxed);
196 profile_.store(
static_cast<std::uint8_t
>(p), std::memory_order_relaxed);
202 return static_cast<Profile>(profile_.load(std::memory_order_relaxed));
212 [[nodiscard]]
int getWindowSize() const noexcept {
return front_.getWindowSize(); }
225 if (!prepared_.load(std::memory_order_acquire))
return;
226 const int hop = front_.getHopSize();
227 const int n = buffer.getNumSamples();
228 if (hop <= 0 || n <= 0)
return;
229 for (
int off = 0; off < n; off += hop)
231 front_.processBlock(buffer.getSubView(off, std::min(hop, n - off)));
239 if (!prepared_.load(std::memory_order_acquire))
return;
240 const int hop = front_.getHopSize();
241 if (hop <= 0)
return;
242 const auto step =
static_cast<std::size_t
>(hop);
243 for (std::size_t off = 0; off < samples.size(); off += step)
245 front_.pushSamples(samples.subspan(off, std::min(step, samples.size() - off)));
278 return unpack(packed_.load(std::memory_order_relaxed));
297 [[nodiscard]]
const std::array<T, 12>&
chroma() const noexcept {
return chromaOut_; }
300 [[nodiscard]] std::uint64_t
getFrameCount() const noexcept {
return framesUsed_; }
314 if (dest ==
nullptr || size <= 0)
return 0;
317 if ((key.tonicPitchClass < 0) || (key.tonicPitchClass > 11))
322 const std::string_view root =
327 if (len >= size - 1)
break;
330 if (key.isMinor && len < size - 1) dest[len++] =
'm';
346 6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88
358 6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17
370 5.0, 2.0, 3.5, 2.0, 4.5, 4.0, 2.0, 4.5, 2.0, 3.5, 1.5, 4.0
382 5.0, 2.0, 3.5, 4.5, 2.0, 4.0, 2.0, 4.5, 3.5, 2.0, 1.5, 4.0
396 static constexpr double kFrameGateFraction = 1.0e-3;
399 static constexpr double kFrameEnergyFloor = 1.0e-12;
401 void consumeFrames() noexcept
403 const std::uint64_t fc = front_.getFrameCount();
404 if (fc == lastFrame_)
return;
407 const std::array<T, 12>& frame = front_.getChroma();
409 for (
const T v : frame) total += static_cast<double>(v);
410 if (!(total > kFrameEnergyFloor))
return;
412 if (total > maxFrameTotal_) maxFrameTotal_ = total;
413 if (total < kFrameGateFraction * maxFrameTotal_)
return;
417 const double inv = 1.0 / total;
418 for (
int pc = 0; pc < 12; ++pc)
419 accum_[
static_cast<std::size_t
>(pc)] +=
420 static_cast<double>(frame[
static_cast<std::size_t
>(pc)]) * inv;
426 void estimate() noexcept
429 for (
const double v : accum_) sum += v;
430 if (!(sum > 0.0))
return;
440 const double inv = 1.0 / sum;
441 for (
int pc = 0; pc < 12; ++pc)
442 chromaOut_[
static_cast<std::size_t
>(pc)] =
443 static_cast<T
>(accum_[
static_cast<std::size_t
>(pc)] * inv);
449 std::array<double, 24> scores {};
451 for (
int mode = 0; mode < 2; ++mode)
453 const auto& p = (mode == 0) ? major : minor;
454 for (
int tonic = 0; tonic < 12; ++tonic)
456 const double r = pearsonRotated(accum_, p, tonic);
457 scores[
static_cast<std::size_t
>(mode * 12 + tonic)] = r;
465 std::array<int, 24> order {};
466 for (
int ordinal = 0; ordinal < 24; ++ordinal)
470 && scores[
static_cast<std::size_t
>(ordinal)]
471 > scores[
static_cast<std::size_t
>(order[
static_cast<std::size_t
>(pos - 1)])])
473 order[
static_cast<std::size_t
>(pos)] =
474 order[
static_cast<std::size_t
>(pos - 1)];
477 order[
static_cast<std::size_t
>(pos)] = ordinal;
480 const int bestOrdinal = order[0];
481 const int secondOrdinal = order[1];
482 const double best = scores[
static_cast<std::size_t
>(bestOrdinal)];
483 const double second = scores[
static_cast<std::size_t
>(secondOrdinal)];
484 const int bestPc = bestOrdinal % 12;
485 const bool bestMinor = bestOrdinal >= 12;
490 const int relativePc = (bestPc + (bestMinor ? 3 : 9)) % 12;
491 const bool relativeMinor = !bestMinor;
492 const int relativeOrdinal = (relativeMinor ? 12 : 0) + relativePc;
493 int relativeRank = 0;
494 for (
int rank = 0; rank < 24; ++rank)
496 if (order[
static_cast<std::size_t
>(rank)] == relativeOrdinal)
498 relativeRank = rank + 1;
504 key.tonicPitchClass = bestPc;
505 key.isMinor = bestMinor;
506 key.runnerUpPitchClass = secondOrdinal % 12;
507 key.runnerUpMinor = secondOrdinal >= 12;
508 key.relativeAlternativePitchClass = relativePc;
509 key.relativeAlternativeMinor = relativeMinor;
510 key.relativeAlternativeRank = relativeRank;
514 key.confidence = (best > 0.0)
515 ?
static_cast<T
>(std::clamp(
516 (best - second) / best, 0.0, 1.0))
518 packed_.store(pack(key), std::memory_order_relaxed);
533 [[nodiscard]]
static double pearsonRotated(
const std::array<double, 12>& x,
534 const std::array<double, 12>& profile,
537 double mx = 0.0, mp = 0.0;
538 for (
int i = 0; i < 12; ++i)
540 mx += x[
static_cast<std::size_t
>(i)];
541 mp += profile[
static_cast<std::size_t
>(i)];
546 double num = 0.0, dx = 0.0, dp = 0.0;
547 for (
int pc = 0; pc < 12; ++pc)
549 const double a = x[
static_cast<std::size_t
>(pc)] - mx;
550 const double b = profile[
static_cast<std::size_t
>((pc - tonic + 12) % 12)] - mp;
555 const double den = std::sqrt(dx * dp);
556 return (den > 0.0) ? (num / den) : 0.0;
562 [[nodiscard]]
static std::uint64_t pack(
const Key& k)
noexcept
564 const auto q =
static_cast<std::uint64_t
>(
565 std::clamp(
static_cast<double>(k.confidence), 0.0, 1.0) * 65535.0 + 0.5);
566 const auto t =
static_cast<std::uint64_t
>(
567 std::clamp(k.tonicPitchClass, -1, 11) + 1);
568 const auto ru =
static_cast<std::uint64_t
>(
569 std::clamp(k.runnerUpPitchClass, -1, 11) + 1);
570 const auto rel =
static_cast<std::uint64_t
>(
571 std::clamp(k.relativeAlternativePitchClass, -1, 11) + 1);
572 const auto rank =
static_cast<std::uint64_t
>(
573 std::clamp(k.relativeAlternativeRank, 0, 24));
575 | (
static_cast<std::uint64_t
>(k.isMinor ? 1 : 0) << 16)
577 | (
static_cast<std::uint64_t
>(k.runnerUpMinor ? 1 : 0) << 21)
579 | (static_cast<std::uint64_t>(k.relativeAlternativeMinor ? 1 : 0) << 26)
584 [[nodiscard]]
static Key unpack(std::uint64_t v)
noexcept
587 k.confidence =
static_cast<T
>(
static_cast<double>(v & 0xFFFFu) / 65535.0);
588 k.isMinor = ((v >> 16) & 1u) != 0u;
589 k.tonicPitchClass =
static_cast<int>((v >> 17) & 0xFu) - 1;
590 k.runnerUpMinor = ((v >> 21) & 1u) != 0u;
591 k.runnerUpPitchClass =
static_cast<int>((v >> 22) & 0xFu) - 1;
592 k.relativeAlternativeMinor = ((v >> 26) & 1u) != 0u;
593 k.relativeAlternativePitchClass =
static_cast<int>((v >> 27) & 0xFu) - 1;
594 k.relativeAlternativeRank =
static_cast<int>((v >> 31) & 0x1Fu);
598 static_assert(std::atomic<std::uint64_t>::is_always_lock_free,
599 "audio-thread stores must not lock");
601 ChordDetector<T> front_;
602 std::atomic<bool> prepared_ {
false };
604 std::array<double, 12> accum_ {};
605 std::array<T, 12> chromaOut_ {};
606 double maxFrameTotal_ = 0.0;
607 std::uint64_t lastFrame_ = 0;
608 std::uint64_t framesUsed_ = 0;
610 std::atomic<std::uint64_t> packed_ { 0 };