DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
OnsetDetector.h
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
98#include "../Core/DspMath.h"
99#include "detail/OnsetFeatures.h"
100#include "../Core/AudioBuffer.h"
101#include "../Core/AudioSpec.h"
102
103#include <algorithm>
104#include <array>
105#include <atomic>
106#include <cmath>
107#include <cstddef>
108#include <cstdint>
109#include <memory>
110#include <span>
111#include <vector>
112
113namespace dspark {
114
126template <FloatType T>
127class OnsetDetector final
128{
129public:
132
133 // -- Lifecycle -----------------------------------------------------------
134
215 void prepare(const AudioSpec& spec, int fftSize = 0, int hop = 0)
216 {
217 if (!(spec.sampleRate > 0.0) || !std::isfinite(spec.sampleRate))
218 return;
219
220 prepared_ = false; // gate OFF: the audio path is a no-op while rebuilding
221
222 sampleRate_ = spec.sampleRate;
223
224 if (fftSize <= 0)
225 {
226 // Automatic: hold the analysis TIME SPAN constant across sample
227 // rates (the span 2048 samples cover at 48 kHz). Bin width and the
228 // filterbank's quarter-tone floor are both fs/fftSize, so a
229 // constant span pins them -- and with them the usable low register
230 // -- instead of letting them widen as fs rises.
231 fftSize_ = detail::OnsetSpectrum<T>::defaultFrameSize(sampleRate_);
232 }
233 else
234 {
235 int fs = std::clamp(fftSize, kMinFft, kMaxFft);
236 int pow2 = kMinFft;
237 while (pow2 < fs) pow2 <<= 1;
238 fftSize_ = pow2;
239 }
240
241 if (hop <= 0)
242 hop_ = std::max(1, static_cast<int>(std::lround(sampleRate_ / 200.0)));
243 else
244 hop_ = hop;
245 hop_ = std::clamp(hop_, 1, fftSize_);
246
247 latencySamples_.store(static_cast<int64_t>(fftSize_) + hop_,
248 std::memory_order_relaxed);
249
250 // Analysis-window group-delay compensation: half-wave-rectified spectral
251 // flux peaks on the rising flank of a transient, ~kLocalizationLead*N
252 // before the windowed-energy peak. Adding it back centres the reported
253 // onset on the transient (calibrated on band-limited clicks; softer
254 // onsets localise slightly late but stay within the acceptance window).
255 localizationOffset_ = static_cast<int>(std::lround(kLocalizationLead
256 * static_cast<double>(fftSize_)));
257 // Warm-up: suppress onsets until the analysis ring is fully primed so
258 // the silence->first-input ramp cannot fire a spurious onset.
259 primeFrames_ = fftSize_ / hop_ + 2;
260
261 ring_.assign(static_cast<size_t>(fftSize_) * 2, T(0));
262 spectrum_.prepare(fftSize_);
263 novelty_.prepare(sampleRate_, fftSize_);
264
265 // Peak-picker windows in frames (derived from the ms defaults above).
266 preMaxFrames_ = msToFrames(30.0);
267 postMaxFrames_ = msToFrames(30.0); // offline; causal uses 1
268 preAvgFrames_ = msToFrames(100.0);
269 postAvgFrames_ = msToFrames(70.0); // offline; causal uses 0
270 waitFrames_ = msToFrames(30.0);
271
272 // Causal ODF history: enough for the pre_avg look-back plus the
273 // single-frame causal confirmation.
274 odfHistLen_ = std::max(preAvgFrames_, preMaxFrames_) + 4;
275 odfHist_.assign(static_cast<size_t>(odfHistLen_), T(0));
276
277 // Pending-onset ring: onsets are held from detection until their
278 // release sample (reference + L). At most ceil(L/hop)+2 can be
279 // in flight; size generously to a power-of-two-ish bound.
280 const int maxPending = (fftSize_ + hop_) / hop_ + 4;
281 pending_.assign(static_cast<size_t>(std::max(8, maxPending)),
282 PendingOnset{});
283 pendingHead_ = 0;
284 pendingCount_ = 0;
285
286 resetState();
287
288 method_.store(Method::SuperFlux, std::memory_order_relaxed);
289 // A conservative default delta, tuned against a synthetic corpus of
290 // clicks, soft onsets and noise beds. Callers override per material.
291 threshold_.store(kDefaultDelta, std::memory_order_relaxed);
292 whitening_.store(false, std::memory_order_relaxed);
293
294 prepared_ = true; // gate ON
295 }
296
298 void setMethod(Method m) noexcept { method_.store(m, std::memory_order_relaxed); }
299
307 void setThreshold(T deltaAboveMean) noexcept
308 {
309 if (!std::isfinite(deltaAboveMean)) return;
310 threshold_.store(std::max(T(0), deltaAboveMean), std::memory_order_relaxed);
311 }
312
314 void setAdaptiveWhitening(bool on) noexcept
315 {
316 whitening_.store(on, std::memory_order_relaxed);
317 }
318
320 [[nodiscard]] Method getMethod() const noexcept
321 {
322 return method_.load(std::memory_order_relaxed);
323 }
324
326 [[nodiscard]] T getThreshold() const noexcept
327 {
328 return threshold_.load(std::memory_order_relaxed);
329 }
330
332 [[nodiscard]] bool getAdaptiveWhitening() const noexcept
333 {
334 return whitening_.load(std::memory_order_relaxed);
335 }
336
337 // -- Audio path (causal, RT-safe) ---------------------------------------
338
345 {
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())));
349 }
350
358 void pushSamples(std::span<const T> samples) noexcept
359 {
360 if (!prepared_) return;
361
362 bool firedThisCall = false;
363
364 for (const T s : samples)
365 {
366 const T x = std::isfinite(s) ? s : T(0);
367
368 ring_[static_cast<size_t>(writePos_)] = x;
369 ring_[static_cast<size_t>(writePos_ + fftSize_)] = x;
370 if (++writePos_ >= fftSize_) writePos_ = 0;
371
372 ++totalSamples_;
373
374 // Release any pending onset whose reporting sample has arrived.
375 while (pendingCount_ > 0)
376 {
377 const PendingOnset& p = pending_[static_cast<size_t>(pendingHead_)];
378 if (totalSamples_ - 1 >= p.reportSample)
379 {
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());
384 --pendingCount_;
385 }
386 else break;
387 }
388
389 if (++hopCounter_ >= hop_)
390 {
391 hopCounter_ = 0;
392 analyzeFrame();
393 }
394 }
395
396 onsetLatched_.store(firedThisCall, std::memory_order_relaxed);
397 }
398
399 // -- Readout (lock-free) -------------------------------------------------
400
402 [[nodiscard]] bool onsetDetected() const noexcept
403 {
404 return onsetLatched_.load(std::memory_order_relaxed);
405 }
406
409 [[nodiscard]] T getOnsetStrength() const noexcept
410 {
411 return onsetStrength_.load(std::memory_order_relaxed);
412 }
413
418 [[nodiscard]] int64_t getLastOnsetSample() const noexcept
419 {
420 return lastOnsetSample_.load(std::memory_order_relaxed);
421 }
422
424 [[nodiscard]] int getLatencySamples() const noexcept
425 {
426 return static_cast<int>(latencySamples_.load(std::memory_order_relaxed));
427 }
428
433 [[nodiscard]] int getFftSize() const noexcept { return fftSize_; }
434
436 [[nodiscard]] int getHopSize() const noexcept { return hop_; }
437
440 [[nodiscard]] int getNumBands() const noexcept { return novelty_.getNumBands(); }
441
442 // -- Onset-strength envelope (stream owner only) -------------------------
443
458
459 struct OdfFrame
460 {
461 T value = T(0);
462 int64_t referenceSample = 0;
463 int64_t frameIndex = 0;
470 std::array<T, kNumRegisters> registers {};
471 };
472
495 [[nodiscard]] OdfFrame getLastOdfFrame() const noexcept
496 {
497 return OdfFrame { lastOdfValue_, lastOdfRef_, frameIndex_, lastRegisters_ };
498 }
499
523 [[nodiscard]] int getWarmupFrames() const noexcept { return primeFrames_; }
524
540 [[nodiscard]] int getEnvelopeLatencySamples() const noexcept
541 {
542 return fftSize_ / 2 - localizationOffset_;
543 }
544
545 // -- Offline -------------------------------------------------------------
546
548 struct Onset
549 {
550 int64_t sample = 0;
551 T strength = T(0);
552 };
553
563 std::vector<int64_t> detectOffline(AudioBufferView<const T> whole)
564 {
565 return positionsOf(detectOfflineOnsets(whole));
566 }
567
570 {
571 if (!prepared_ || whole.getNumChannels() < 1) return {};
572 const int n = std::max(0, whole.getNumSamples());
573 beginOffline(n);
574 pushOffline(std::span<const T>(whole.getChannel(0), static_cast<size_t>(n)));
575 return finishOfflineOnsets();
576 }
577
594 void beginOffline(int64_t expectedSamples = 0)
595 {
596 offOdf_.clear();
597 offRef_.clear();
598 offlineOpen_ = false;
599 if (!prepared_) return;
600 if (expectedSamples > 0)
601 {
602 const size_t frames = static_cast<size_t>(expectedSamples / std::max(1, hop_) + 2);
603 offOdf_.reserve(frames);
604 offRef_.reserve(frames);
605 }
606 resetState();
607 offMethod_ = method_.load(std::memory_order_relaxed);
608 offWhiten_ = whitening_.load(std::memory_order_relaxed);
609 offTotal_ = 0;
610 offHop_ = 0;
611 offlineOpen_ = true;
612 }
613
615 void pushOffline(std::span<const T> samples)
616 {
617 if (!offlineOpen_) return;
618 for (const T s : samples)
619 {
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;
624 ++offTotal_;
625 if (++offHop_ >= hop_)
626 {
627 offHop_ = 0;
628 offOdf_.push_back(computeOdf(offMethod_, offWhiten_));
629 offRef_.push_back(referenceSample(offTotal_));
630 }
631 }
632 }
633
636 std::vector<int64_t> finishOffline()
637 {
638 return positionsOf(finishOfflineOnsets());
639 }
640
642 std::vector<Onset> finishOfflineOnsets()
643 {
644 std::vector<Onset> out;
645 if (!offlineOpen_) return out;
646 offlineOpen_ = false;
647
648 // Symmetric peak-pick over the whole envelope.
649 const T delta = threshold_.load(std::memory_order_relaxed);
650 const std::vector<T>& odf = offOdf_;
652 const typename detail::OnsetPeakPicker<T>::Windows windows {
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_)
656 };
657 for (size_t f = static_cast<size_t>(primeFrames_); f < odf.size(); ++f)
658 {
659 if (picker.accept(f, odf.size(), [&](size_t i) { return odf[i]; }, windows, delta))
660 out.push_back(Onset { offRef_[f], odf[f] });
661 }
662
663 offOdf_.clear();
664 offRef_.clear();
665 resetState();
666 return out;
667 }
668
671 void reset() noexcept
672 {
673 offlineOpen_ = false;
674 resetState();
675 }
676
677private:
678 [[nodiscard]] static std::vector<int64_t> positionsOf(const std::vector<Onset>& onsets)
679 {
680 std::vector<int64_t> out;
681 out.reserve(onsets.size());
682 for (const Onset& o : onsets) out.push_back(o.sample);
683 return out;
684 }
685
686 // -- Constants -----------------------------------------------------------
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;
694
695 struct PendingOnset
696 {
697 int64_t referenceSample = 0;
698 int64_t reportSample = 0;
699 T strength = T(0);
700 };
701
702 // -- Frame timing --------------------------------------------------------
703
706 [[nodiscard]] int64_t referenceSample(int64_t firePos) const noexcept
707 {
708 return firePos - static_cast<int64_t>(fftSize_) / 2
709 + static_cast<int64_t>(localizationOffset_);
710 }
711
712 [[nodiscard]] int msToFrames(double ms) const noexcept
713 {
714 return std::max(1, static_cast<int>(std::lround(ms * 0.001 * sampleRate_
715 / static_cast<double>(hop_))));
716 }
717
718 // -- STFT + ODF ----------------------------------------------------------
719
721 T computeOdf(Method method, bool whiten) noexcept
722 {
723 spectrum_.compute(&ring_[static_cast<size_t>(writePos_)]);
724 using FeatureMethod = typename detail::OnsetNovelty<T>::Method;
725 const auto selected = method == Method::SpectralFlux ? FeatureMethod::SpectralFlux
726 : method == Method::ComplexDomain ? FeatureMethod::ComplexDomain
727 : method == Method::SuperFlux ? FeatureMethod::SuperFlux
728 : static_cast<FeatureMethod>(-1);
729 const auto result = novelty_.process(spectrum_.magnitudes(), spectrum_.phases(),
730 selected, whiten);
731 curRegisters_ = result.registers;
732 return result.value;
733 }
734
736 void analyzeFrame() noexcept
737 {
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);
741
742 // Push into the causal ODF history ring.
743 odfHist_[static_cast<size_t>(odfWrite_)] = value;
744 odfWrite_ = (odfWrite_ + 1) % odfHistLen_;
745 ++frameIndex_;
746
747 // Envelope readout for the stream owner (see getLastOdfFrame()). The
748 // frame fired at totalSamples_, so it localises exactly where an onset
749 // decided from it would: one definition of frame time, not two.
750 lastOdfValue_ = value;
751 lastOdfRef_ = referenceSample(totalSamples_);
752 lastRegisters_ = curRegisters_;
753
754 // Causal peak-pick with a single-frame confirmation: we decide whether
755 // the PREVIOUS frame was a maximum now that we have the current one.
756 // This one-hop confirmation is exactly the +hop term of L.
757 if (frameIndex_ < 2) { lastConfirmOdf_ = value; return; }
758
759 const T prev = odfAt(1); // previous frame's ODF (candidate)
760 const T curr = value; // current frame (confirmation)
761
762 // (1) prev is a causal local max over [prev-preMax, prev+1].
763 bool isMax = (prev >= curr);
764 if (isMax)
765 {
766 for (int j = 2; j <= preMaxFrames_ + 1 && j < frameIndex_; ++j)
767 {
768 if (odfAt(j) > prev) { isMax = false; break; }
769 }
770 }
771
772 if (isMax)
773 {
774 // (2) prev exceeds the backward moving mean + delta.
775 T sum = T(0); int cnt = 0;
776 for (int j = 1; j <= preAvgFrames_ && j < frameIndex_; ++j)
777 {
778 sum += odfAt(j); ++cnt;
779 }
780 const T mean = (cnt > 0) ? sum / static_cast<T>(cnt) : T(0);
781 const T delta = threshold_.load(std::memory_order_relaxed);
782
783 // (3) combination width since the last onset, and past warm-up.
784 const int64_t candFrame = frameIndex_ - 1;
785 if (candFrame >= primeFrames_ && prev >= mean + delta
786 && candFrame - lastOnsetFrame_ > waitFrames_)
787 {
788 // The candidate is the previous frame; its fire position was
789 // totalSamples_ - hop (one hop before the current frame's fire).
790 const int64_t candFirePos = totalSamples_ - hop_;
791 const int64_t ref = referenceSample(candFirePos);
792 scheduleOnset(ref, prev);
793 lastOnsetFrame_ = candFrame;
794 }
795 }
796 }
797
799 [[nodiscard]] T odfAt(int j) const noexcept
800 {
801 int idx = odfWrite_ - 1 - j;
802 idx %= odfHistLen_;
803 if (idx < 0) idx += odfHistLen_;
804 return odfHist_[static_cast<size_t>(idx)];
805 }
806
807 void scheduleOnset(int64_t referenceSample, T strength) noexcept
808 {
809 if (pendingCount_ >= static_cast<int>(pending_.size())) return; // saturate
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;
815 ++pendingCount_;
816 }
817
818 void resetState() noexcept
819 {
820 std::fill(ring_.begin(), ring_.end(), T(0));
821 novelty_.reset();
822 std::fill(odfHist_.begin(), odfHist_.end(), T(0));
823
824 writePos_ = 0;
825 hopCounter_ = 0;
826 totalSamples_ = 0;
827 frameIndex_ = 0;
828 odfWrite_ = 0;
829 lastOdfValue_ = T(0);
830 lastOdfRef_ = 0;
831 lastRegisters_.fill(T(0));
832 curRegisters_.fill(T(0));
833 lastConfirmOdf_ = T(0);
834 lastOnsetFrame_ = -kBig;
835 pendingHead_ = 0;
836 pendingCount_ = 0;
837
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);
841 }
842
843 // -- Members -------------------------------------------------------------
844 double sampleRate_ = 44100.0;
845 int fftSize_ = 2048;
846 int hop_ = 221;
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_; // Mirrored chronological input, size 2*fftSize.
853
854 // Peak-picker windows (frames).
855 int preMaxFrames_ = 6, postMaxFrames_ = 6;
856 int preAvgFrames_ = 20, postAvgFrames_ = 14;
857 int waitFrames_ = 6;
858
859 // Causal ODF history ring.
860 std::vector<T> odfHist_;
861 int odfHistLen_ = 32;
862 int odfWrite_ = 0;
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;
870
871 // Streaming counters.
872 int writePos_ = 0;
873 int hopCounter_ = 0;
874 int64_t totalSamples_ = 0;
875
876 // Pending-onset ring (held from detection to report point).
877 std::vector<PendingOnset> pending_;
878 int pendingHead_ = 0;
879 int pendingCount_ = 0;
880
881 // Atomic parameters / readouts.
882 std::atomic<Method> method_ { Method::SuperFlux };
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 };
889
890 // Offline session (beginOffline() .. finishOffline()).
891 std::vector<T> offOdf_;
892 std::vector<int64_t> offRef_;
893 int64_t offTotal_ = 0;
894 int offHop_ = 0;
895 Method offMethod_ = Method::SuperFlux;
896 bool offWhiten_ = false;
897 bool offlineOpen_ = false;
898};
899
900} // 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.
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.
Definition AudioSpec.h:37
double sampleRate
Sample rate in Hz.
Definition AudioSpec.h:45
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.