98#include "../Core/DspMath.h"
99#include "detail/OnsetFeatures.h"
100#include "../Core/AudioBuffer.h"
101#include "../Core/AudioSpec.h"
126template <FloatType T>
235 int fs = std::clamp(fftSize, kMinFft, kMaxFft);
237 while (pow2 < fs) pow2 <<= 1;
242 hop_ = std::max(1,
static_cast<int>(std::lround(sampleRate_ / 200.0)));
245 hop_ = std::clamp(hop_, 1, fftSize_);
247 latencySamples_.store(
static_cast<int64_t
>(fftSize_) + hop_,
248 std::memory_order_relaxed);
255 localizationOffset_ =
static_cast<int>(std::lround(kLocalizationLead
256 *
static_cast<double>(fftSize_)));
259 primeFrames_ = fftSize_ / hop_ + 2;
261 ring_.assign(
static_cast<size_t>(fftSize_) * 2, T(0));
262 spectrum_.prepare(fftSize_);
263 novelty_.prepare(sampleRate_, fftSize_);
266 preMaxFrames_ = msToFrames(30.0);
267 postMaxFrames_ = msToFrames(30.0);
268 preAvgFrames_ = msToFrames(100.0);
269 postAvgFrames_ = msToFrames(70.0);
270 waitFrames_ = msToFrames(30.0);
274 odfHistLen_ = std::max(preAvgFrames_, preMaxFrames_) + 4;
275 odfHist_.assign(
static_cast<size_t>(odfHistLen_), T(0));
280 const int maxPending = (fftSize_ + hop_) / hop_ + 4;
281 pending_.assign(
static_cast<size_t>(std::max(8, maxPending)),
291 threshold_.store(kDefaultDelta, std::memory_order_relaxed);
292 whitening_.store(
false, std::memory_order_relaxed);
309 if (!std::isfinite(deltaAboveMean))
return;
310 threshold_.store(std::max(T(0), deltaAboveMean), std::memory_order_relaxed);
316 whitening_.store(on, std::memory_order_relaxed);
322 return method_.load(std::memory_order_relaxed);
328 return threshold_.load(std::memory_order_relaxed);
334 return whitening_.load(std::memory_order_relaxed);
346 if (!prepared_ || in.getNumChannels() < 1)
return;
347 const T* ch0 = in.getChannel(0);
348 pushSamples(std::span<const T>(ch0,
static_cast<size_t>(in.getNumSamples())));
360 if (!prepared_)
return;
362 bool firedThisCall =
false;
364 for (
const T s : samples)
366 const T x = std::isfinite(s) ? s : T(0);
368 ring_[
static_cast<size_t>(writePos_)] = x;
369 ring_[
static_cast<size_t>(writePos_ + fftSize_)] = x;
370 if (++writePos_ >= fftSize_) writePos_ = 0;
375 while (pendingCount_ > 0)
377 const PendingOnset& p = pending_[
static_cast<size_t>(pendingHead_)];
378 if (totalSamples_ - 1 >= p.reportSample)
380 lastOnsetSample_.store(p.referenceSample, std::memory_order_relaxed);
381 onsetStrength_.store(p.strength, std::memory_order_relaxed);
382 firedThisCall =
true;
383 pendingHead_ = (pendingHead_ + 1) %
static_cast<int>(pending_.size());
389 if (++hopCounter_ >= hop_)
396 onsetLatched_.store(firedThisCall, std::memory_order_relaxed);
404 return onsetLatched_.load(std::memory_order_relaxed);
411 return onsetStrength_.load(std::memory_order_relaxed);
420 return lastOnsetSample_.load(std::memory_order_relaxed);
426 return static_cast<int>(latencySamples_.load(std::memory_order_relaxed));
433 [[nodiscard]]
int getFftSize() const noexcept {
return fftSize_; }
436 [[nodiscard]]
int getHopSize() const noexcept {
return hop_; }
440 [[nodiscard]]
int getNumBands() const noexcept {
return novelty_.getNumBands(); }
497 return OdfFrame { lastOdfValue_, lastOdfRef_, frameIndex_, lastRegisters_ };
542 return fftSize_ / 2 - localizationOffset_;
598 offlineOpen_ =
false;
599 if (!prepared_)
return;
600 if (expectedSamples > 0)
602 const size_t frames =
static_cast<size_t>(expectedSamples / std::max(1, hop_) + 2);
603 offOdf_.reserve(frames);
604 offRef_.reserve(frames);
607 offMethod_ = method_.load(std::memory_order_relaxed);
608 offWhiten_ = whitening_.load(std::memory_order_relaxed);
617 if (!offlineOpen_)
return;
618 for (
const T s : samples)
620 const T v = std::isfinite(s) ? s : T(0);
621 ring_[
static_cast<size_t>(writePos_)] = v;
622 ring_[
static_cast<size_t>(writePos_ + fftSize_)] = v;
623 if (++writePos_ >= fftSize_) writePos_ = 0;
625 if (++offHop_ >= hop_)
628 offOdf_.push_back(computeOdf(offMethod_, offWhiten_));
629 offRef_.push_back(referenceSample(offTotal_));
644 std::vector<Onset> out;
645 if (!offlineOpen_)
return out;
646 offlineOpen_ =
false;
649 const T delta = threshold_.load(std::memory_order_relaxed);
650 const std::vector<T>& odf = offOdf_;
653 static_cast<size_t>(preMaxFrames_),
static_cast<size_t>(postMaxFrames_),
654 static_cast<size_t>(preAvgFrames_),
static_cast<size_t>(postAvgFrames_),
655 static_cast<size_t>(waitFrames_)
657 for (
size_t f =
static_cast<size_t>(primeFrames_); f < odf.size(); ++f)
659 if (picker.
accept(f, odf.size(), [&](
size_t i) {
return odf[i]; }, windows, delta))
660 out.push_back(
Onset { offRef_[f], odf[f] });
673 offlineOpen_ =
false;
678 [[nodiscard]]
static std::vector<int64_t> positionsOf(
const std::vector<Onset>& onsets)
680 std::vector<int64_t> out;
681 out.reserve(onsets.size());
682 for (
const Onset& o : onsets) out.push_back(o.sample);
687 static constexpr int kMinFft = 64;
688 static constexpr int kMaxFft = 1 << 16;
691 static constexpr int64_t kBig = int64_t(1) << 60;
692 static constexpr T kDefaultDelta = T(0.03);
693 static constexpr double kLocalizationLead = 0.34;
697 int64_t referenceSample = 0;
698 int64_t reportSample = 0;
706 [[nodiscard]] int64_t referenceSample(int64_t firePos)
const noexcept
708 return firePos -
static_cast<int64_t
>(fftSize_) / 2
709 +
static_cast<int64_t
>(localizationOffset_);
712 [[nodiscard]]
int msToFrames(
double ms)
const noexcept
714 return std::max(1,
static_cast<int>(std::lround(ms * 0.001 * sampleRate_
715 /
static_cast<double>(hop_))));
721 T computeOdf(
Method method,
bool whiten)
noexcept
723 spectrum_.compute(&ring_[
static_cast<size_t>(writePos_)]);
728 :
static_cast<FeatureMethod
>(-1);
729 const auto result = novelty_.process(spectrum_.magnitudes(), spectrum_.phases(),
731 curRegisters_ = result.registers;
736 void analyzeFrame() noexcept
738 const Method m = method_.load(std::memory_order_relaxed);
739 const bool whiten = whitening_.load(std::memory_order_relaxed);
740 const T value = computeOdf(m, whiten);
743 odfHist_[
static_cast<size_t>(odfWrite_)] = value;
744 odfWrite_ = (odfWrite_ + 1) % odfHistLen_;
750 lastOdfValue_ = value;
751 lastOdfRef_ = referenceSample(totalSamples_);
752 lastRegisters_ = curRegisters_;
757 if (frameIndex_ < 2) { lastConfirmOdf_ = value;
return; }
759 const T prev = odfAt(1);
760 const T curr = value;
763 bool isMax = (prev >= curr);
766 for (
int j = 2; j <= preMaxFrames_ + 1 && j < frameIndex_; ++j)
768 if (odfAt(j) > prev) { isMax =
false;
break; }
775 T sum = T(0);
int cnt = 0;
776 for (
int j = 1; j <= preAvgFrames_ && j < frameIndex_; ++j)
778 sum += odfAt(j); ++cnt;
780 const T mean = (cnt > 0) ? sum /
static_cast<T
>(cnt) : T(0);
781 const T delta = threshold_.load(std::memory_order_relaxed);
784 const int64_t candFrame = frameIndex_ - 1;
785 if (candFrame >= primeFrames_ && prev >= mean + delta
786 && candFrame - lastOnsetFrame_ > waitFrames_)
790 const int64_t candFirePos = totalSamples_ - hop_;
791 const int64_t ref = referenceSample(candFirePos);
792 scheduleOnset(ref, prev);
793 lastOnsetFrame_ = candFrame;
799 [[nodiscard]] T odfAt(
int j)
const noexcept
801 int idx = odfWrite_ - 1 - j;
803 if (idx < 0) idx += odfHistLen_;
804 return odfHist_[
static_cast<size_t>(idx)];
807 void scheduleOnset(int64_t referenceSample, T strength)
noexcept
809 if (pendingCount_ >=
static_cast<int>(pending_.size()))
return;
810 const int tail = (pendingHead_ + pendingCount_) %
static_cast<int>(pending_.size());
811 PendingOnset& p = pending_[
static_cast<size_t>(tail)];
812 p.referenceSample = referenceSample;
813 p.reportSample = referenceSample + latencySamples_.load(std::memory_order_relaxed);
814 p.strength = strength;
818 void resetState() noexcept
820 std::fill(ring_.begin(), ring_.end(), T(0));
822 std::fill(odfHist_.begin(), odfHist_.end(), T(0));
829 lastOdfValue_ = T(0);
831 lastRegisters_.fill(T(0));
832 curRegisters_.fill(T(0));
833 lastConfirmOdf_ = T(0);
834 lastOnsetFrame_ = -kBig;
838 onsetLatched_.store(
false, std::memory_order_relaxed);
839 onsetStrength_.store(T(0), std::memory_order_relaxed);
840 lastOnsetSample_.store(-1, std::memory_order_relaxed);
844 double sampleRate_ = 44100.0;
847 int localizationOffset_ = 0;
848 int primeFrames_ = 12;
849 bool prepared_ =
false;
850 detail::OnsetSpectrum<T> spectrum_;
851 detail::OnsetNovelty<T> novelty_;
852 std::vector<T> ring_;
855 int preMaxFrames_ = 6, postMaxFrames_ = 6;
856 int preAvgFrames_ = 20, postAvgFrames_ = 14;
860 std::vector<T> odfHist_;
861 int odfHistLen_ = 32;
863 int64_t frameIndex_ = 0;
864 T lastOdfValue_ = T(0);
865 int64_t lastOdfRef_ = 0;
866 std::array<T, kNumRegisters> lastRegisters_ {};
867 std::array<T, kNumRegisters> curRegisters_ {};
868 T lastConfirmOdf_ = T(0);
869 int64_t lastOnsetFrame_ = -kBig;
874 int64_t totalSamples_ = 0;
877 std::vector<PendingOnset> pending_;
878 int pendingHead_ = 0;
879 int pendingCount_ = 0;
883 std::atomic<T> threshold_ { kDefaultDelta };
884 std::atomic<bool> whitening_ {
false };
885 std::atomic<bool> onsetLatched_ {
false };
886 std::atomic<T> onsetStrength_ { T(0) };
887 std::atomic<int64_t> lastOnsetSample_ { -1 };
888 std::atomic<int64_t> latencySamples_ { 2269 };
891 std::vector<T> offOdf_;
892 std::vector<int64_t> offRef_;
893 int64_t offTotal_ = 0;
896 bool offWhiten_ =
false;
897 bool offlineOpen_ =
false;
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.
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.
std::vector< Onset > detectOfflineOnsets(AudioBufferView< const T > whole)
detectOffline() with each onset's strength alongside its position.
int getEnvelopeLatencySamples() const noexcept
How far behind the newest input sample a frame's reference sample sits, in samples.
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...
bool getAdaptiveWhitening() const noexcept
True when adaptive whitening is on.
void reset() noexcept
Clears all streaming state and abandons an open offline session. Not concurrent with pushSamples().
int getNumBands() const noexcept
Number of log-frequency filterbank bands built for the resolved frame; a direct readout of the analys...
std::vector< int64_t > finishOffline()
Closes the session and returns onset positions, as detectOffline(). Empty when no session is open.
T getOnsetStrength() const noexcept
Onset strength (ODF value, frame-invariant scale; see prepare()) of the most recent reported onset.
void beginOffline(int64_t expectedSamples=0)
Opens an incremental offline analysis.
void setAdaptiveWhitening(bool on) noexcept
Enables Stowell-Plumbley adaptive whitening (default off).
void prepare(const AudioSpec &spec, int fftSize=0, int hop=0)
Allocates all state and configures the STFT front-end.
int getWarmupFrames() const noexcept
Frames at the start of a stream whose ODF value is warm-up, not signal.
OdfFrame getLastOdfFrame() const noexcept
The most recent analysis frame's onset-strength value.
int getLatencySamples() const noexcept
The single causal reporting latency, L = fftSize + hop (samples).
T getThreshold() const noexcept
Peak-pick delta in force, after setThreshold()'s clamping.
void setMethod(Method m) noexcept
Selects the ODF family. Lock-free.
std::vector< Onset > finishOfflineOnsets()
Closes the session and returns onsets with their strengths.
void pushOffline(std::span< const T > samples)
Feeds the next piece of an offline session. No-op outside one.
Method getMethod() const noexcept
ODF family in force.
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
Analysis frame in samples actually in effect: the automatic choice when prepare() got fftSize <= 0,...
static constexpr int kNumRegisters
One frame of the onset-strength envelope (the ODF before the peak picker).
int getHopSize() const noexcept
Hop in samples in effect (round(fs/200) unless overridden).
Shared whitening, spectral/complex flux and SuperFlux feature history.
Allocation-free symmetric novelty peak picker with an explicit frame clock.
bool accept(std::size_t index, std::size_t count, Value value, Windows windows, T delta)
Examines increasing indices in an immutable, finite novelty sequence. The accessor returns T....
static int defaultFrameSize(double sampleRate) noexcept
Main namespace for the DSPark framework.
Describes the audio environment for a DSP processor.
double sampleRate
Sample rate in Hz.
int64_t referenceSample
Sample index the frame localises to.
T value
ODF value of the most recent frame.
std::array< T, kNumRegisters > registers
One offline onset: where it is and how strong it was.
int64_t sample
Reference sample (frame centre).
T strength
ODF value at the peak, the scale getOnsetStrength() reports.