166 const double highestNote =
167 440.0 * std::exp2((kFirstMidi + kNumNotes - 1 - 69) / 12.0);
168 if (!(spec.
sampleRate > highestNote * 2.0))
return false;
178 const double target = sampleRate_ * (4096.0 / 48000.0);
180 while (n < 16384 &&
static_cast<double>(n) < target) n *= 2;
185 windowSize_ = std::clamp(windowSize, 1024, 16384);
187 hopSize_ = windowSize_ / 2;
189 ring_.assign(
static_cast<std::size_t
>(windowSize_), T(0));
193 window_.resize(
static_cast<std::size_t
>(windowSize_));
195 scratch_.resize(
static_cast<std::size_t
>(windowSize_));
197 for (
int n = 0; n < kNumNotes; ++n)
199 const double freq = 440.0 * std::exp2((kFirstMidi + n - 69) / 12.0);
200 notes_[
static_cast<std::size_t
>(n)].
prepare(sampleRate_, freq, windowSize_);
203 prepared_.store(
true, std::memory_order_release);
217 std::fill(ring_.begin(), ring_.end(), T(0));
220 chromaFrame_.fill(T(0));
222 packed_.store(pack(
Result {}), std::memory_order_relaxed);
234 if (!std::isfinite(threshold))
return;
235 threshold_.store(std::clamp(threshold, 0.0f, 1.0f), std::memory_order_relaxed);
241 return threshold_.load(std::memory_order_relaxed);
261 [[nodiscard]]
int getHopSize() const noexcept {
return hopSize_; }
275 [[nodiscard]] std::uint64_t
getFrameCount() const noexcept {
return frameCount_; }
297 [[nodiscard]]
const std::array<T, 12>&
getChroma() const noexcept {
return chromaFrame_; }
304 if (!prepared_.load(std::memory_order_acquire))
return;
305 const int nCh = buffer.getNumChannels();
306 const int nS = buffer.getNumSamples();
307 if (nCh <= 0)
return;
309 const T invCh = T(1) /
static_cast<T
>(nCh);
310 for (
int i = 0; i < nS; ++i)
313 for (
int ch = 0; ch < nCh; ++ch)
314 m += buffer.getChannel(ch)[i] * invCh;
322 if (!prepared_.load(std::memory_order_acquire))
return;
323 for (
const T s : samples)
332 return unpack(packed_.load(std::memory_order_relaxed));
344 if (size <= 0)
return 0;
350 static constexpr const char* kRoots[12] = {
351 "C",
"C#",
"D",
"D#",
"E",
"F",
"F#",
"G",
"G#",
"A",
"A#",
"B"
353 static constexpr const char* kSuffix[11] = {
354 "",
"",
"m",
"dim",
"aug",
"sus2",
"sus4",
"7",
"maj7",
"m7",
"m7b5"
357 for (
const char* p = kRoots[result.rootPitchClass]; *p && len < size - 1; ++p)
359 for (
const char* p = kSuffix[
static_cast<int>(result.type)]; *p && len < size - 1; ++p)
366 static constexpr int kFirstMidi = 36;
367 static constexpr int kNumNotes = 48;
368 static constexpr int kNumTemplates = 10;
377 static constexpr std::array<Template, kNumTemplates> kTemplates { {
390 void push(T sample)
noexcept
392 ring_[
static_cast<std::size_t
>(writePos_)] = sample;
393 writePos_ = (writePos_ + 1) % windowSize_;
394 if (++sinceHop_ >= hopSize_)
401 void analyze() noexcept
404 for (
int i = 0; i < windowSize_; ++i)
406 const int idx = (writePos_ + i) % windowSize_;
407 scratch_[
static_cast<std::size_t
>(i)] = ring_[
static_cast<std::size_t
>(idx)]
408 * window_[
static_cast<std::size_t
>(i)];
414 std::array<double, 12> chroma {};
415 std::array<double, static_cast<std::size_t>(kNumNotes)> noteE {};
416 double total = 0.0, maxNote = 0.0;
417 for (
int n = 0; n < kNumNotes; ++n)
419 auto& g = notes_[
static_cast<std::size_t
>(n)];
421 g.processBlock(scratch_.data(), windowSize_);
422 const double e =
static_cast<double>(g.getMagnitude());
423 noteE[
static_cast<std::size_t
>(n)] = e * e;
424 chroma[
static_cast<std::size_t
>((kFirstMidi + n) % 12)] += e * e;
426 maxNote = std::max(maxNote, e * e);
432 for (
int pc = 0; pc < 12; ++pc)
433 chromaFrame_[
static_cast<std::size_t
>(pc)] =
434 static_cast<T
>(chroma[
static_cast<std::size_t
>(pc)]);
440 for (
int n = 0; n < kNumNotes; ++n)
442 const double e = noteE[
static_cast<std::size_t
>(n)];
443 const double prev = (n > 0) ? noteE[
static_cast<std::size_t
>(n - 1)] : 0.0;
444 const double next = (n + 1 < kNumNotes) ? noteE[
static_cast<std::size_t
>(n + 1)] : 0.0;
445 if (e > 0.15 * maxNote && e >= prev && e >= next)
447 bassPc = (kFirstMidi + n) % 12;
454 Result held = unpack(packed_.load(std::memory_order_relaxed));
455 held.confidence = 0.0f;
456 packed_.store(pack(held), std::memory_order_relaxed);
461 for (
const double c : chroma) norm += c * c;
462 norm = std::sqrt(norm);
465 double best = 0.0, second = 0.0;
468 for (
int root = 0; root < 12; ++root)
470 for (
const auto& tpl : kTemplates)
473 for (
int iv = 0; iv < 12; ++iv)
474 if (tpl.mask & (1u << iv))
475 inSum += chroma[
static_cast<std::size_t
>((root + iv) % 12)];
476 double score = inSum / (norm * std::sqrt(
static_cast<double>(tpl.count)));
486 else if (score > second)
494 const double margin = (best > 1e-9) ? std::clamp((best - second) / best * 4.0, 0.0, 1.0)
496 const auto confidence =
static_cast<float>(std::clamp(best, 0.0, 1.0) * (0.5 + 0.5 * margin));
499 if (confidence >= threshold_.load(std::memory_order_relaxed))
501 out.rootPitchClass = bestRoot;
503 out.confidence = confidence;
507 out = unpack(packed_.load(std::memory_order_relaxed));
508 out.confidence = confidence;
510 packed_.store(pack(out), std::memory_order_relaxed);
514 [[nodiscard]]
static std::uint64_t pack(
const Result& r)
noexcept
516 const auto conf =
static_cast<std::uint32_t
>(std::clamp(r.confidence, 0.0f, 1.0f) * 65535.0f);
517 return (
static_cast<std::uint64_t
>(
static_cast<std::uint8_t
>(r.rootPitchClass + 1)) << 24)
518 | (
static_cast<std::uint64_t
>(
static_cast<std::uint8_t
>(r.type)) << 16)
522 [[nodiscard]]
static Result unpack(std::uint64_t v)
noexcept
525 r.rootPitchClass =
static_cast<int>((v >> 24) & 0xFF) - 1;
526 r.type =
static_cast<ChordType>((v >> 16) & 0xFF);
527 r.confidence =
static_cast<float>(v & 0xFFFF) / 65535.0f;
532 double sampleRate_ = 48000.0;
533 int windowSize_ = 4096;
535 std::atomic<bool> prepared_ {
false };
537 std::vector<T> ring_, window_, scratch_;
541 std::array<Goertzel<T>,
static_cast<std::size_t
>(kNumNotes)> notes_;
544 std::array<T, 12> chromaFrame_ {};
545 std::uint64_t frameCount_ = 0;
547 std::atomic<std::uint64_t> packed_ { 0 };
548 std::atomic<float> threshold_ { 0.55f };