Onset detection#

The onsets/ layer implements a complete offline onset-detection pipeline inspired by the state of the art (Böck & Widmer; Böck, Krebs & Schedl): a magnitude spectrogram, an optional log-spaced filter bank, a spectral-flux onset detection function, and a generic peak picker with optional sample-accurate refinement.

The pipeline, in order:

        flowchart LR
    Audio --> Spectrogram --> FilterBank --> ODF --> PeakPicker --> OnsetTimes
    

Spectrogram#

Spectrogram computes an STFT magnitude spectrogram from an offline buffer. Frames are stored flat, row-major as [numFrames x numBins], where numBins is the filter-bank band count when a filter bank is attached, otherwise fftSize / 2.

yup::Spectrogram::Parameters params;
params.fftSize  = 2048;                 // power of two, >= 64
params.fps      = 200;                  // frames per second (hop = round (fs / fps))
params.windowType = yup::WindowType::hann;
params.useLog   = true;                 // log10 (logMul * mag + logAdd)
params.computeLGD = false;              // Local Group Delay (needed by ComplexFluxODF)
params.filterBank = nullptr;            // optional FilterBank*

yup::Spectrogram spec;
spec.prepare (params, 44100.0f);
spec.processOffline (samples, numSamples);

processOffline zero-pads at the edges and emits getMagnitudeData(). When computeLGD is enabled it also computes the Local Group Delay (the negative frequency-derivative of the unwrapped STFT phase) at raw resolution [numFrames x fftSize/2], available via getLGDData().

FilterBank#

FilterBank maps FFT bins onto a smaller set of perceptually motivated, log-spaced triangular bands — for example 24 bands per octave (quarter-tone resolution). Band centers are generated log-spaced from A440 and rounded to FFT bins; each band’s triangle spans from the previous center to the next.

yup::FilterBank bank;
bank.build (24, 30.0f, 17000.0f, fftSize / 2, 44100.0f);
bank.applySingleFrame (magnitudeIn, magnitudeOut);      // bins -> bands
bank.applyMultipleFrames (spectrogram, filtered, numFrames);

By default every band has height 1.0; pass equalizeArea = true to scale each band to unit area. fMax is clamped to Nyquist.

Onset detection functions#

OnsetDetectionFunction is the abstract interface: compute (spec) fills a one-value-per-frame activation signal whose peaks mark onsets (getActivations()). Two implementations are provided.

SuperFluxODF#

SuperFlux (“Maximum Filter Vibrato Suppression for Onset Detection”, Böck & Widmer, DAFx-13) computes the positive first-order difference of a max-filtered magnitude spectrogram, summed across bins per frame. The max filter (width maxFilterBins, default 3) suppresses vibrato:

yup::SuperFluxODF::Parameters p;
p.diffFrames = 0;           // 0 = auto-derive from the window magnitude ratio
p.windowMagRatio = 0.5f;
p.maxFilterBins = 3;

yup::SuperFluxODF odf;
odf.prepare (p, window.data(), windowSize, hopSize);
odf.compute (spec);

ComplexFluxODF#

ComplexFlux (“Local group delay based vibrato and tremolo suppression for onset detection”, Böck & Widmer, ISMIR 2013) extends SuperFlux by weighting the difference spectrogram with a mask derived from the STFT Local Group Delay. It requires the spectrogram to have been computed with computeLGD = true:

yup::ComplexFluxODF::Parameters p;
p.temporalFilter = 3;       // temporal max-filter size for LGD smoothing; 0 disables

yup::ComplexFluxODF odf;
odf.prepare (p, window.data(), windowSize, hopSize);
odf.compute (spec);

OnsetPeakPicker#

OnsetPeakPicker is algorithm-agnostic: it detects peaks in any float activation array (“Evaluating the Online Capabilities of Onset Detection Methods”, Böck, Krebs & Schedl, ISMIR 2012). A frame is an onset when it is the local moving maximum over [frame preMax, frame + postMax] and exceeds movingAverage + threshold. A combineSec window suppresses double-detections.

yup::OnsetPeakPicker::Parameters p;
p.threshold  = 1.1f;    // higher = fewer detections
p.combineSec = 0.03f;   // min spacing between onsets
p.preAvgSec  = 0.15f;
p.preMaxSec  = 0.01f;
p.postAvgSec = 0.0f;    // 0 = online
p.postMaxSec = 0.05f;

yup::OnsetPeakPicker picker;
picker.prepare (p, fps);
picker.detect (activations.data(), numFrames);
auto times = picker.getOnsetTimes();   // seconds

onlineMode forces the future windows to zero so onsets are reported with no look-ahead (at the cost of accuracy). The optional refineOnsetTimes (samples, numSamples, sampleRate, maxRefineSec, threshold) moves each detected onset to a sample-accurate position by walking back from the peak of the RMS envelope (with dynamic-threshold triggering) to the nearest zero crossing.

OnsetDetector#

OnsetDetector orchestrates the whole chain — it owns the Spectrogram, the FilterBank, an OnsetDetectionFunction (SuperFlux or ComplexFlux) and an OnsetPeakPicker:

yup::OnsetDetector detector;
detector.prepare ({
    .spectrogram   = { .fftSize = 2048, .fps = 200 },
    .useFilterBank = true,
    .bandsPerOctave = 24,
    .useComplexFlux = true,          // vs. SuperFlux
    .peakPicker    = { .threshold = 0.25f },   // lower for ComplexFlux
    .refineOnsets  = false,          // optional sample-accurate refinement
}, 44100.0f);

detector.processOffline (audioBuffer);        // AudioBuffer<float> (stereo = L+R average) or raw samples

for (auto t : detector.getOnsetTimes())
    DBG ("Onset at " << t << "s");

Key Parameters fields: spectrogram, superFluxODF, complexFluxODF, peakPicker, useFilterBank / bandsPerOctave / fMin / fMax / equalizeFilterArea, useComplexFlux, and the refinement options (refineOnsets, refineMaxSec, refineThreshold).

Accessors: getActivationFunction() (ODF activations), getOnsetTimes() (seconds), getNumFrames(), getSpectrogram(), getParameters().

Typical default chain#

With the default parameters at 44.1 kHz: Spectrogram (2048-point FFT, 200 fps → hop ≈ 221 samples, Hann window, log10(mag + 1)) → FilterBank (24 bands/octave, 30 Hz–17 kHz) → SuperFluxODF (auto diffFrames, max filter width 3) → OnsetPeakPicker (threshold 1.1, combine 0.03 s, pre-average 0.15 s, pre-max 0.01 s, post-max 0.05 s) → onset times in seconds.