90 windowSize_ = std::clamp(windowSize, 1024, 16384);
91 hopSize_ = windowSize_ / 2;
93 ring_.assign(
static_cast<size_t>(windowSize_), T(0));
97 window_.resize(
static_cast<size_t>(windowSize_));
99 scratch_.resize(
static_cast<size_t>(windowSize_));
101 for (
int n = 0; n < kNumNotes; ++n)
103 const double freq = 440.0 * std::exp2((kFirstMidi + n - 69) / 12.0);
104 notes_[
static_cast<size_t>(n)].
prepare(sampleRate_, freq, windowSize_);
107 prepared_.store(
true, std::memory_order_release);
120 std::fill(ring_.begin(), ring_.end(), T(0));
123 packed_.store(pack(
Result {}), std::memory_order_relaxed);
135 if (!std::isfinite(threshold))
return;
136 threshold_.store(std::clamp(threshold, 0.0f, 1.0f), std::memory_order_relaxed);
142 return threshold_.load(std::memory_order_relaxed);
150 if (!prepared_.load(std::memory_order_acquire))
return;
151 const int nCh = buffer.getNumChannels();
152 const int nS = buffer.getNumSamples();
153 if (nCh <= 0)
return;
155 const T invCh = T(1) /
static_cast<T
>(nCh);
156 for (
int i = 0; i < nS; ++i)
159 for (
int ch = 0; ch < nCh; ++ch)
160 m += buffer.getChannel(ch)[i] * invCh;
168 if (!prepared_.load(std::memory_order_acquire))
return;
169 for (
const T s : samples)
178 return unpack(packed_.load(std::memory_order_relaxed));
190 if (size <= 0)
return 0;
196 static constexpr const char* kRoots[12] = {
197 "C",
"C#",
"D",
"D#",
"E",
"F",
"F#",
"G",
"G#",
"A",
"A#",
"B"
199 static constexpr const char* kSuffix[11] = {
200 "",
"",
"m",
"dim",
"aug",
"sus2",
"sus4",
"7",
"maj7",
"m7",
"m7b5"
203 for (
const char* p = kRoots[result.rootPitchClass]; *p && len < size - 1; ++p)
205 for (
const char* p = kSuffix[
static_cast<int>(result.type)]; *p && len < size - 1; ++p)
212 static constexpr int kFirstMidi = 36;
213 static constexpr int kNumNotes = 48;
214 static constexpr int kNumTemplates = 10;
223 static constexpr std::array<Template, kNumTemplates> kTemplates { {
236 void push(T sample)
noexcept
238 ring_[
static_cast<size_t>(writePos_)] = sample;
239 writePos_ = (writePos_ + 1) % windowSize_;
240 if (++sinceHop_ >= hopSize_)
247 void analyze() noexcept
250 for (
int i = 0; i < windowSize_; ++i)
252 const int idx = (writePos_ + i) % windowSize_;
253 scratch_[
static_cast<size_t>(i)] = ring_[
static_cast<size_t>(idx)]
254 * window_[
static_cast<size_t>(i)];
260 std::array<double, 12> chroma {};
261 std::array<double, static_cast<size_t>(kNumNotes)> noteE {};
262 double total = 0.0, maxNote = 0.0;
263 for (
int n = 0; n < kNumNotes; ++n)
265 auto& g = notes_[
static_cast<size_t>(n)];
267 g.processBlock(scratch_.data(), windowSize_);
268 const double e =
static_cast<double>(g.getMagnitude());
269 noteE[
static_cast<size_t>(n)] = e * e;
270 chroma[
static_cast<size_t>((kFirstMidi + n) % 12)] += e * e;
272 maxNote = std::max(maxNote, e * e);
277 for (
int n = 0; n < kNumNotes; ++n)
279 const double e = noteE[
static_cast<size_t>(n)];
280 const double prev = (n > 0) ? noteE[
static_cast<size_t>(n - 1)] : 0.0;
281 const double next = (n + 1 < kNumNotes) ? noteE[
static_cast<size_t>(n + 1)] : 0.0;
282 if (e > 0.15 * maxNote && e >= prev && e >= next)
284 bassPc = (kFirstMidi + n) % 12;
291 Result held = unpack(packed_.load(std::memory_order_relaxed));
292 held.confidence = 0.0f;
293 packed_.store(pack(held), std::memory_order_relaxed);
298 for (
const double c : chroma) norm += c * c;
299 norm = std::sqrt(norm);
302 double best = 0.0, second = 0.0;
305 for (
int root = 0; root < 12; ++root)
307 for (
const auto& tpl : kTemplates)
310 for (
int iv = 0; iv < 12; ++iv)
311 if (tpl.mask & (1u << iv))
312 inSum += chroma[
static_cast<size_t>((root + iv) % 12)];
313 double score = inSum / (norm * std::sqrt(
static_cast<double>(tpl.count)));
323 else if (score > second)
331 const double margin = (best > 1e-9) ? std::clamp((best - second) / best * 4.0, 0.0, 1.0)
333 const auto confidence =
static_cast<float>(std::clamp(best, 0.0, 1.0) * (0.5 + 0.5 * margin));
336 if (confidence >= threshold_.load(std::memory_order_relaxed))
338 out.rootPitchClass = bestRoot;
340 out.confidence = confidence;
344 out = unpack(packed_.load(std::memory_order_relaxed));
345 out.confidence = confidence;
347 packed_.store(pack(out), std::memory_order_relaxed);
351 [[nodiscard]]
static uint64_t pack(
const Result& r)
noexcept
353 const auto conf =
static_cast<uint32_t
>(std::clamp(r.confidence, 0.0f, 1.0f) * 65535.0f);
354 return (
static_cast<uint64_t
>(
static_cast<uint8_t
>(r.rootPitchClass + 1)) << 24)
355 | (
static_cast<uint64_t
>(
static_cast<uint8_t
>(r.type)) << 16)
359 [[nodiscard]]
static Result unpack(uint64_t v)
noexcept
362 r.rootPitchClass =
static_cast<int>((v >> 24) & 0xFF) - 1;
363 r.type =
static_cast<ChordType>((v >> 16) & 0xFF);
364 r.confidence =
static_cast<float>(v & 0xFFFF) / 65535.0f;
369 double sampleRate_ = 48000.0;
370 int windowSize_ = 4096;
372 std::atomic<bool> prepared_ {
false };
374 std::vector<T> ring_, window_, scratch_;
378 std::array<Goertzel<T>,
static_cast<size_t>(kNumNotes)> notes_;
380 std::atomic<uint64_t> packed_ { 0 };
381 std::atomic<float> threshold_ { 0.55f };