DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
OnsetDetector.h
Go to the documentation of this file.
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
65#include "../Core/DspMath.h"
66#include "../Core/FFT.h"
67#include "../Core/WindowFunctions.h"
68#include "../Core/AudioBuffer.h"
69#include "../Core/AudioSpec.h"
70
71#include <algorithm>
72#include <atomic>
73#include <cmath>
74#include <cstddef>
75#include <cstdint>
76#include <memory>
77#include <span>
78#include <vector>
79
80namespace dspark {
81
93template <FloatType T>
94class OnsetDetector final
95{
96public:
99
100 // -- Lifecycle -----------------------------------------------------------
101
116 void prepare(const AudioSpec& spec, int fftSize = 2048, int hop = 0)
117 {
118 if (!(spec.sampleRate > 0.0) || !std::isfinite(spec.sampleRate))
119 return;
120
121 fft_.reset(); // gate OFF: the audio path is a no-op while rebuilding
122
123 sampleRate_ = spec.sampleRate;
124
125 int fs = std::clamp(fftSize, kMinFft, kMaxFft);
126 int pow2 = kMinFft;
127 while (pow2 < fs) pow2 <<= 1;
128 fftSize_ = pow2;
129 numBins_ = fftSize_ / 2 + 1;
130
131 if (hop <= 0)
132 hop_ = std::max(1, static_cast<int>(std::lround(sampleRate_ / 200.0)));
133 else
134 hop_ = hop;
135 hop_ = std::clamp(hop_, 1, fftSize_);
136
137 latencySamples_.store(static_cast<int64_t>(fftSize_) + hop_,
138 std::memory_order_relaxed);
139
140 // Analysis-window group-delay compensation: half-wave-rectified spectral
141 // flux peaks on the rising flank of a transient, ~kLocalizationLead*N
142 // before the windowed-energy peak. Adding it back centres the reported
143 // onset on the transient (calibrated on band-limited clicks; softer
144 // onsets localise slightly late but stay within the acceptance window).
145 localizationOffset_ = static_cast<int>(std::lround(kLocalizationLead
146 * static_cast<double>(fftSize_)));
147 // Warm-up: suppress onsets until the analysis ring is fully primed so
148 // the silence->first-input ramp cannot fire a spurious onset.
149 primeFrames_ = fftSize_ / hop_ + 2;
150
151 // Analysis window (periodic Hann) and its ring (mirrored for a
152 // contiguous most-recent-fftSize window without a modulo).
153 window_.assign(static_cast<size_t>(fftSize_), T(0));
154 WindowFunctions<T>::hann(window_.data(), fftSize_, true);
155 ring_.assign(static_cast<size_t>(fftSize_) * 2, T(0));
156
157 fft_time_.assign(static_cast<size_t>(fftSize_), T(0));
158 fft_spec_.assign(static_cast<size_t>(fftSize_) + 2, T(0));
159 mag_.assign(static_cast<size_t>(numBins_), T(0));
160 phase_.assign(static_cast<size_t>(numBins_), T(0));
161 prevPhase_.assign(static_cast<size_t>(numBins_), T(0));
162 prevPhase2_.assign(static_cast<size_t>(numBins_), T(0));
163 prevMag_.assign(static_cast<size_t>(numBins_), T(0));
164 whitenPeak_.assign(static_cast<size_t>(numBins_), T(0));
165
166 buildFilterBank();
167
168 // Band history for the SuperFlux flux-to-mu-th-previous-frame. mu is
169 // one hop by construction (adjacent frames at 200 fps), so two frames
170 // of history are enough; keep a small ring keyed on frame index.
171 muFrames_ = 1;
172 bandCur_.assign(static_cast<size_t>(numBands_), T(0));
173 bandPrev_.assign(static_cast<size_t>(numBands_), T(0));
174 bandMaxPrev_.assign(static_cast<size_t>(numBands_), T(0));
175
176 // Peak-picker windows in frames (derived from the ms defaults, D-2).
177 preMaxFrames_ = msToFrames(30.0);
178 postMaxFrames_ = msToFrames(30.0); // offline; causal uses 1
179 preAvgFrames_ = msToFrames(100.0);
180 postAvgFrames_ = msToFrames(70.0); // offline; causal uses 0
181 waitFrames_ = msToFrames(30.0);
182
183 // Causal ODF history: enough for the pre_avg look-back plus the
184 // single-frame causal confirmation.
185 odfHistLen_ = std::max(preAvgFrames_, preMaxFrames_) + 4;
186 odfHist_.assign(static_cast<size_t>(odfHistLen_), T(0));
187
188 // Pending-onset ring: onsets are held from detection until their
189 // release sample (reference + L). At most ceil(L/hop)+2 can be
190 // in flight; size generously to a power-of-two-ish bound.
191 const int maxPending = (fftSize_ + hop_) / hop_ + 4;
192 pending_.assign(static_cast<size_t>(std::max(8, maxPending)),
193 PendingOnset{});
194 pendingHead_ = 0;
195 pendingCount_ = 0;
196
197 resetState();
198
199 method_.store(Method::SuperFlux, std::memory_order_relaxed);
200 // A conservative default delta tuned against the D-6 synthetic corpus
201 // (OA-1..OA-5). Callers can override per material.
202 threshold_.store(kDefaultDelta, std::memory_order_relaxed);
203 whitening_.store(false, std::memory_order_relaxed);
204
205 fft_ = std::make_unique<FFTReal<T>>(static_cast<size_t>(fftSize_)); // gate ON
206 }
207
209 void setMethod(Method m) noexcept { method_.store(m, std::memory_order_relaxed); }
210
215 void setThreshold(T deltaAboveMean) noexcept
216 {
217 if (!std::isfinite(deltaAboveMean)) return;
218 threshold_.store(std::max(T(0), deltaAboveMean), std::memory_order_relaxed);
219 }
220
222 void setAdaptiveWhitening(bool on) noexcept
223 {
224 whitening_.store(on, std::memory_order_relaxed);
225 }
226
227 // -- Audio path (causal, RT-safe) ---------------------------------------
228
235 {
236 if (fft_ == nullptr || in.getNumChannels() < 1) return;
237 const T* ch0 = in.getChannel(0);
238 pushSamples(std::span<const T>(ch0, static_cast<size_t>(in.getNumSamples())));
239 }
240
248 void pushSamples(std::span<const T> samples) noexcept
249 {
250 if (fft_ == nullptr) return;
251
252 bool firedThisCall = false;
253
254 for (const T s : samples)
255 {
256 const T x = std::isfinite(s) ? s : T(0);
257
258 ring_[static_cast<size_t>(writePos_)] = x;
259 ring_[static_cast<size_t>(writePos_ + fftSize_)] = x;
260 if (++writePos_ >= fftSize_) writePos_ = 0;
261
262 ++totalSamples_;
263
264 // Release any pending onset whose reporting sample has arrived.
265 while (pendingCount_ > 0)
266 {
267 const PendingOnset& p = pending_[static_cast<size_t>(pendingHead_)];
268 if (totalSamples_ - 1 >= p.reportSample)
269 {
270 lastOnsetSample_.store(p.referenceSample, std::memory_order_relaxed);
271 onsetStrength_.store(p.strength, std::memory_order_relaxed);
272 firedThisCall = true;
273 pendingHead_ = (pendingHead_ + 1) % static_cast<int>(pending_.size());
274 --pendingCount_;
275 }
276 else break;
277 }
278
279 if (++hopCounter_ >= hop_)
280 {
281 hopCounter_ = 0;
282 analyzeFrame();
283 }
284 }
285
286 onsetLatched_.store(firedThisCall, std::memory_order_relaxed);
287 }
288
289 // -- Readout (lock-free) -------------------------------------------------
290
292 [[nodiscard]] bool onsetDetected() const noexcept
293 {
294 return onsetLatched_.load(std::memory_order_relaxed);
295 }
296
298 [[nodiscard]] T getOnsetStrength() const noexcept
299 {
300 return onsetStrength_.load(std::memory_order_relaxed);
301 }
302
307 [[nodiscard]] int64_t getLastOnsetSample() const noexcept
308 {
309 return lastOnsetSample_.load(std::memory_order_relaxed);
310 }
311
313 [[nodiscard]] int getLatencySamples() const noexcept
314 {
315 return static_cast<int>(latencySamples_.load(std::memory_order_relaxed));
316 }
317
318 [[nodiscard]] int getFftSize() const noexcept { return fftSize_; }
319 [[nodiscard]] int getHopSize() const noexcept { return hop_; }
320 [[nodiscard]] int getNumBands() const noexcept { return numBands_; }
321
322 // -- Offline convenience -------------------------------------------------
323
332 std::vector<int64_t> detectOffline(AudioBufferView<const T> whole)
333 {
334 std::vector<int64_t> out;
335 if (fft_ == nullptr || whole.getNumChannels() < 1) return out;
336
337 const int n = whole.getNumSamples();
338 const T* x = whole.getChannel(0);
339
340 // Build the full ODF envelope (offline: allocation allowed).
341 std::vector<T> odf;
342 std::vector<int64_t> odfRef; // reference sample per frame
343 odf.reserve(static_cast<size_t>(n / std::max(1, hop_) + 2));
344 odfRef.reserve(odf.capacity());
345
346 resetState();
347 const Method m = method_.load(std::memory_order_relaxed);
348 const bool whiten = whitening_.load(std::memory_order_relaxed);
349
350 int64_t total = 0;
351 int hopc = 0;
352 for (int i = 0; i < n; ++i)
353 {
354 const T v = std::isfinite(x[i]) ? x[i] : T(0);
355 ring_[static_cast<size_t>(writePos_)] = v;
356 ring_[static_cast<size_t>(writePos_ + fftSize_)] = v;
357 if (++writePos_ >= fftSize_) writePos_ = 0;
358 ++total;
359 if (++hopc >= hop_)
360 {
361 hopc = 0;
362 const T value = computeOdf(m, whiten);
363 odf.push_back(value);
364 odfRef.push_back(referenceSample(total));
365 }
366 }
367
368 // Symmetric peak-pick over the whole envelope.
369 const T delta = threshold_.load(std::memory_order_relaxed);
370 const int nf = static_cast<int>(odf.size());
371 int64_t lastFrame = -kBig;
372 for (int f = 0; f < nf; ++f)
373 {
374 if (f < primeFrames_) continue; // warm-up guard (ring priming)
375 bool isMax = true;
376 for (int j = f - preMaxFrames_; j <= f + postMaxFrames_; ++j)
377 {
378 if (j < 0 || j >= nf) continue;
379 if (odf[static_cast<size_t>(j)] > odf[static_cast<size_t>(f)]) { isMax = false; break; }
380 }
381 if (!isMax) continue;
382
383 T sum = T(0); int cnt = 0;
384 for (int j = f - preAvgFrames_; j <= f + postAvgFrames_; ++j)
385 {
386 if (j < 0 || j >= nf) continue;
387 sum += odf[static_cast<size_t>(j)]; ++cnt;
388 }
389 const T mean = (cnt > 0) ? sum / static_cast<T>(cnt) : T(0);
390 if (odf[static_cast<size_t>(f)] < mean + delta) continue;
391 if (f - lastFrame <= waitFrames_) continue;
392
393 out.push_back(odfRef[static_cast<size_t>(f)]);
394 lastFrame = f;
395 }
396
397 resetState();
398 return out;
399 }
400
402 void reset() noexcept { resetState(); }
403
404private:
405 // -- Constants -----------------------------------------------------------
406 static constexpr int kMinFft = 64;
407 static constexpr int kMaxFft = 1 << 16;
408 static constexpr int64_t kBig = int64_t(1) << 60;
409 static constexpr T kDefaultDelta = T(0.03);
410 static constexpr double kLocalizationLead = 0.34;
411 static constexpr double kFMin = 27.5;
412 static constexpr double kFMaxHz = 16000.0;
413 static constexpr int kBandsPerOctave = 24;
414
415 struct PendingOnset
416 {
417 int64_t referenceSample = 0;
418 int64_t reportSample = 0;
419 T strength = T(0);
420 };
421
422 // -- Frame timing --------------------------------------------------------
423
426 [[nodiscard]] int64_t referenceSample(int64_t firePos) const noexcept
427 {
428 return firePos - static_cast<int64_t>(fftSize_) / 2
429 + static_cast<int64_t>(localizationOffset_);
430 }
431
432 [[nodiscard]] int msToFrames(double ms) const noexcept
433 {
434 return std::max(1, static_cast<int>(std::lround(ms * 0.001 * sampleRate_
435 / static_cast<double>(hop_))));
436 }
437
438 // -- STFT + ODF ----------------------------------------------------------
439
442 void computeSpectrum() noexcept
443 {
444 const T* w = window_.data();
445 const T* r = &ring_[static_cast<size_t>(writePos_)]; // oldest..newest, contiguous
446 for (int k = 0; k < fftSize_; ++k)
447 fft_time_[static_cast<size_t>(k)] = r[k] * w[k];
448
449 fft_->forward(fft_time_.data(), fft_spec_.data());
450
451 for (int k = 0; k < numBins_; ++k)
452 {
453 const T re = fft_spec_[static_cast<size_t>(2 * k)];
454 const T im = fft_spec_[static_cast<size_t>(2 * k + 1)];
455 mag_[static_cast<size_t>(k)] = std::sqrt(re * re + im * im);
456 phase_[static_cast<size_t>(k)] = std::atan2(im, re);
457 }
458 }
459
461 void applyWhitening() noexcept
462 {
463 for (int k = 0; k < numBins_; ++k)
464 {
465 T& pk = whitenPeak_[static_cast<size_t>(k)];
466 const T decayed = pk * kWhitenDecay;
467 const T m = mag_[static_cast<size_t>(k)];
468 pk = std::max({ m, kWhitenFloor, decayed });
469 mag_[static_cast<size_t>(k)] = m / pk;
470 }
471 }
472
475 T computeOdf(Method m, bool whiten) noexcept
476 {
477 computeSpectrum();
478 if (whiten) applyWhitening();
479
480 T odf = T(0);
481 switch (m)
482 {
484 {
485 for (int k = 0; k < numBins_; ++k)
486 {
487 const T d = mag_[static_cast<size_t>(k)] - prevMag_[static_cast<size_t>(k)];
488 if (d > T(0)) odf += d;
489 }
490 odf /= static_cast<T>(numBins_);
491 break;
492 }
494 {
495 // Rectified complex-domain deviation (Dixon 2006): phase-predict
496 // each bin, sum |X - Xhat| where magnitude increased.
497 for (int k = 0; k < numBins_; ++k)
498 {
499 const T target = princArg(T(2) * prevPhase_[static_cast<size_t>(k)]
500 - prevPhase2_[static_cast<size_t>(k)]);
501 const T pm = prevMag_[static_cast<size_t>(k)];
502 const T cm = mag_[static_cast<size_t>(k)];
503 const T re = cm * std::cos(phase_[static_cast<size_t>(k)])
504 - pm * std::cos(target);
505 const T im = cm * std::sin(phase_[static_cast<size_t>(k)])
506 - pm * std::sin(target);
507 if (cm >= pm) odf += std::sqrt(re * re + im * im);
508 }
509 odf /= static_cast<T>(numBins_);
510 break;
511 }
513 {
514 // Log-filtered magnitude bands, flux to the mu-th previous
515 // frame after a frequency maximum filter on the reference.
516 filterLogBands(bandCur_);
517 for (int b = 0; b < numBands_; ++b)
518 {
519 const T d = bandCur_[static_cast<size_t>(b)]
520 - bandMaxPrev_[static_cast<size_t>(b)];
521 if (d > T(0)) odf += d;
522 }
523 odf /= static_cast<T>(numBands_);
524 // Rotate: previous <- current, and rebuild the max-filtered
525 // reference from the (new) previous frame.
526 bandPrev_ = bandCur_;
527 maxFilterFreq(bandPrev_, bandMaxPrev_);
528 break;
529 }
530 }
531
532 // Rotate per-bin history (prevPhase2_ <- prevPhase_ <- phase_) and the
533 // previous magnitude, used by SpectralFlux/ComplexDomain next frame.
534 rotatePhaseHistory();
535 std::copy(mag_.begin(), mag_.end(), prevMag_.begin());
536
537 return odf;
538 }
539
541 void rotatePhaseHistory() noexcept
542 {
543 std::copy(prevPhase_.begin(), prevPhase_.end(), prevPhase2_.begin());
544 std::copy(phase_.begin(), phase_.end(), prevPhase_.begin());
545 }
546
548 void analyzeFrame() noexcept
549 {
550 const Method m = method_.load(std::memory_order_relaxed);
551 const bool whiten = whitening_.load(std::memory_order_relaxed);
552 const T value = computeOdf(m, whiten);
553
554 // Push into the causal ODF history ring.
555 odfHist_[static_cast<size_t>(odfWrite_)] = value;
556 odfWrite_ = (odfWrite_ + 1) % odfHistLen_;
557 ++frameIndex_;
558
559 // Causal peak-pick with a single-frame confirmation: we decide whether
560 // the PREVIOUS frame was a maximum now that we have the current one.
561 // This one-hop confirmation is exactly the +hop term of L.
562 if (frameIndex_ < 2) { lastConfirmOdf_ = value; return; }
563
564 const T prev = odfAt(1); // previous frame's ODF (candidate)
565 const T curr = value; // current frame (confirmation)
566
567 // (1) prev is a causal local max over [prev-preMax, prev+1].
568 bool isMax = (prev >= curr);
569 if (isMax)
570 {
571 for (int j = 2; j <= preMaxFrames_ + 1 && j < frameIndex_; ++j)
572 {
573 if (odfAt(j) > prev) { isMax = false; break; }
574 }
575 }
576
577 if (isMax)
578 {
579 // (2) prev exceeds the backward moving mean + delta.
580 T sum = T(0); int cnt = 0;
581 for (int j = 1; j <= preAvgFrames_ && j < frameIndex_; ++j)
582 {
583 sum += odfAt(j); ++cnt;
584 }
585 const T mean = (cnt > 0) ? sum / static_cast<T>(cnt) : T(0);
586 const T delta = threshold_.load(std::memory_order_relaxed);
587
588 // (3) combination width since the last onset, and past warm-up.
589 const int64_t candFrame = frameIndex_ - 1;
590 if (candFrame >= primeFrames_ && prev >= mean + delta
591 && candFrame - lastOnsetFrame_ > waitFrames_)
592 {
593 // The candidate is the previous frame; its fire position was
594 // totalSamples_ - hop (one hop before the current frame's fire).
595 const int64_t candFirePos = totalSamples_ - hop_;
596 const int64_t ref = referenceSample(candFirePos);
597 scheduleOnset(ref, prev);
598 lastOnsetFrame_ = candFrame;
599 }
600 }
601 }
602
604 [[nodiscard]] T odfAt(int j) const noexcept
605 {
606 int idx = odfWrite_ - 1 - j;
607 idx %= odfHistLen_;
608 if (idx < 0) idx += odfHistLen_;
609 return odfHist_[static_cast<size_t>(idx)];
610 }
611
612 void scheduleOnset(int64_t referenceSample, T strength) noexcept
613 {
614 if (pendingCount_ >= static_cast<int>(pending_.size())) return; // saturate
615 const int tail = (pendingHead_ + pendingCount_) % static_cast<int>(pending_.size());
616 PendingOnset& p = pending_[static_cast<size_t>(tail)];
617 p.referenceSample = referenceSample;
618 p.reportSample = referenceSample + latencySamples_.load(std::memory_order_relaxed);
619 p.strength = strength;
620 ++pendingCount_;
621 }
622
623 // -- Filterbank ----------------------------------------------------------
624
628 void buildFilterBank()
629 {
630 fbStart_.clear();
631 fbWeights_.clear();
632 fbOffset_.clear();
633
634 const double binHz = sampleRate_ / static_cast<double>(fftSize_);
635 const double fMax = std::min(kFMaxHz, sampleRate_ * 0.5 * 0.999);
636
637 // Quarter-tone centre bins, strictly increasing and unique.
638 std::vector<int> centres;
639 for (int i = 0; ; ++i)
640 {
641 const double f = kFMin * std::pow(2.0, static_cast<double>(i)
642 / static_cast<double>(kBandsPerOctave));
643 if (f > fMax) break;
644 int bin = static_cast<int>(std::lround(f / binHz));
645 bin = std::clamp(bin, 0, numBins_ - 1);
646 if (centres.empty() || bin > centres.back())
647 centres.push_back(bin);
648 }
649
650 // Triangular filters over consecutive triples (b[j-1], b[j], b[j+1]).
651 numBands_ = 0;
652 for (size_t j = 1; j + 1 < centres.size(); ++j)
653 {
654 const int lo = centres[j - 1];
655 const int ce = centres[j];
656 const int hi = centres[j + 1];
657 if (!(lo < ce && ce < hi)) continue;
658
659 fbStart_.push_back(lo);
660 fbOffset_.push_back(static_cast<int>(fbWeights_.size()));
661 for (int k = lo; k <= hi; ++k)
662 {
663 T wv;
664 if (k <= ce)
665 wv = static_cast<T>(static_cast<double>(k - lo)
666 / static_cast<double>(ce - lo));
667 else
668 wv = static_cast<T>(static_cast<double>(hi - k)
669 / static_cast<double>(hi - ce));
670 fbWeights_.push_back(wv);
671 }
672 ++numBands_;
673 }
674 fbCount_.clear();
675 for (int b = 0; b < numBands_; ++b)
676 {
677 const int off = fbOffset_[static_cast<size_t>(b)];
678 const int nextOff = (b + 1 < numBands_)
679 ? fbOffset_[static_cast<size_t>(b + 1)]
680 : static_cast<int>(fbWeights_.size());
681 fbCount_.push_back(nextOff - off);
682 }
683 if (numBands_ < 1) numBands_ = 1; // degenerate guard (tiny fftSize)
684 }
685
687 void filterLogBands(std::vector<T>& out) noexcept
688 {
689 for (int b = 0; b < numBands_ && b < static_cast<int>(fbStart_.size()); ++b)
690 {
691 const int start = fbStart_[static_cast<size_t>(b)];
692 const int off = fbOffset_[static_cast<size_t>(b)];
693 const int cnt = fbCount_[static_cast<size_t>(b)];
694 T acc = T(0);
695 for (int i = 0; i < cnt; ++i)
696 {
697 const int k = start + i;
698 if (k >= 0 && k < numBins_)
699 acc += mag_[static_cast<size_t>(k)]
700 * fbWeights_[static_cast<size_t>(off + i)];
701 }
702 out[static_cast<size_t>(b)] = std::log10(acc + T(1));
703 }
704 }
705
707 void maxFilterFreq(const std::vector<T>& in, std::vector<T>& out) const noexcept
708 {
709 for (int b = 0; b < numBands_; ++b)
710 {
711 T mx = in[static_cast<size_t>(b)];
712 if (b > 0) mx = std::max(mx, in[static_cast<size_t>(b - 1)]);
713 if (b + 1 < numBands_) mx = std::max(mx, in[static_cast<size_t>(b + 1)]);
714 out[static_cast<size_t>(b)] = mx;
715 }
716 }
717
718 static T princArg(T x) noexcept
719 {
720 // Wrap to (-pi, pi].
721 const T twoPiT = twoPi<T>;
722 T y = x - twoPiT * std::floor(x / twoPiT + T(0.5));
723 return y;
724 }
725
726 void resetState() noexcept
727 {
728 std::fill(ring_.begin(), ring_.end(), T(0));
729 std::fill(prevMag_.begin(), prevMag_.end(), T(0));
730 std::fill(prevPhase_.begin(), prevPhase_.end(), T(0));
731 std::fill(prevPhase2_.begin(), prevPhase2_.end(), T(0));
732 std::fill(whitenPeak_.begin(), whitenPeak_.end(), T(0));
733 std::fill(bandPrev_.begin(), bandPrev_.end(), T(0));
734 std::fill(bandMaxPrev_.begin(), bandMaxPrev_.end(), T(0));
735 std::fill(odfHist_.begin(), odfHist_.end(), T(0));
736
737 writePos_ = 0;
738 hopCounter_ = 0;
739 totalSamples_ = 0;
740 frameIndex_ = 0;
741 odfWrite_ = 0;
742 lastConfirmOdf_ = T(0);
743 lastOnsetFrame_ = -kBig;
744 pendingHead_ = 0;
745 pendingCount_ = 0;
746
747 onsetLatched_.store(false, std::memory_order_relaxed);
748 onsetStrength_.store(T(0), std::memory_order_relaxed);
749 lastOnsetSample_.store(-1, std::memory_order_relaxed);
750 }
751
752 // -- Whitening constants -------------------------------------------------
753 static constexpr T kWhitenDecay = T(0.9995);
754 static constexpr T kWhitenFloor = T(1e-4);
755
756 // -- Members -------------------------------------------------------------
757 double sampleRate_ = 44100.0;
758 int fftSize_ = 2048;
759 int numBins_ = 1025;
760 int hop_ = 221;
761 int numBands_ = 1;
762 int muFrames_ = 1;
763 int localizationOffset_ = 0;
764 int primeFrames_ = 12;
765
766 std::unique_ptr<FFTReal<T>> fft_; // doubles as the "prepared" gate
767
768 std::vector<T> window_;
769 std::vector<T> ring_; // size 2*fftSize
770 std::vector<T> fft_time_; // size fftSize
771 std::vector<T> fft_spec_; // size fftSize+2
772 std::vector<T> mag_; // numBins
773 std::vector<T> phase_; // numBins
774 std::vector<T> prevPhase_, prevPhase2_, prevMag_, whitenPeak_;
775
776 // Filterbank (CSR-style: start bin, flat weights, per-band offset/count).
777 std::vector<int> fbStart_;
778 std::vector<T> fbWeights_;
779 std::vector<int> fbOffset_;
780 std::vector<int> fbCount_;
781
782 std::vector<T> bandCur_, bandPrev_, bandMaxPrev_;
783
784 // Peak-picker windows (frames).
785 int preMaxFrames_ = 6, postMaxFrames_ = 6;
786 int preAvgFrames_ = 20, postAvgFrames_ = 14;
787 int waitFrames_ = 6;
788
789 // Causal ODF history ring.
790 std::vector<T> odfHist_;
791 int odfHistLen_ = 32;
792 int odfWrite_ = 0;
793 int64_t frameIndex_ = 0;
794 T lastConfirmOdf_ = T(0);
795 int64_t lastOnsetFrame_ = -kBig;
796
797 // Streaming counters.
798 int writePos_ = 0;
799 int hopCounter_ = 0;
800 int64_t totalSamples_ = 0;
801
802 // Pending-onset ring (held from detection to report point).
803 std::vector<PendingOnset> pending_;
804 int pendingHead_ = 0;
805 int pendingCount_ = 0;
806
807 // Atomic parameters / readouts.
808 std::atomic<Method> method_ { Method::SuperFlux };
809 std::atomic<T> threshold_ { kDefaultDelta };
810 std::atomic<bool> whitening_ { false };
811 std::atomic<bool> onsetLatched_ { false };
812 std::atomic<T> onsetStrength_ { T(0) };
813 std::atomic<int64_t> lastOnsetSample_ { -1 };
814 std::atomic<int64_t> latencySamples_ { 2269 };
815};
816
817} // 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.
Causal SuperFlux onset detector with lock-free readout.
int64_t getLastOnsetSample() const noexcept
Reference sample index (frame centre) of the most recent onset. The latch fires exactly getLatencySam...
void pushSamples(std::span< const T > samples) noexcept
Feeds a mono stream of samples. Lock-free, allocation-free.
void processBlock(AudioBufferView< const T > in) noexcept
Feeds a mono block; reads channel 0 only. Const, never mutated.
bool onsetDetected() const noexcept
True if an onset was reported during the most recent call.
void setThreshold(T deltaAboveMean) noexcept
Sets the adaptive peak-pick delta (margin above the moving mean). Non-finite values are ignored; nega...
void reset() noexcept
Clears all streaming state. Not concurrent with pushSamples().
int getNumBands() const noexcept
T getOnsetStrength() const noexcept
Onset strength (ODF value) of the most recent reported onset.
void setAdaptiveWhitening(bool on) noexcept
Enables Stowell-Plumbley adaptive whitening (default off).
int getLatencySamples() const noexcept
The single causal reporting latency, L = fftSize + hop (samples).
void setMethod(Method m) noexcept
Selects the ODF family. Lock-free.
std::vector< int64_t > detectOffline(AudioBufferView< const T > whole)
Offline detection over a whole mono buffer (channel 0).
Method
Onset-detection function family. Default SuperFlux.
int getFftSize() const noexcept
void prepare(const AudioSpec &spec, int fftSize=2048, int hop=0)
Allocates all state and configures the STFT front-end.
int getHopSize() const noexcept
Main namespace for the DSPark framework.
Describes the audio environment for a DSP processor.
Definition AudioSpec.h:35
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:43
static void hann(T *output, int size, bool periodic=true) noexcept
Hann (raised cosine) window.