|
DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
|
Tempo and beat tracking with an offline grid and a causal readout. More...
#include <BeatTracker.h>
Classes | |
| struct | Result |
| What analyze() returns: one tempo, one grid, one number saying how much of the signal that grid explains, and the metrical alternative that lost. More... | |
Public Member Functions | |
| BeatTracker () | |
| void | prepare (const AudioSpec &spec) |
| Allocates all causal state and prepares the shared front end. | |
| void | setTempoRange (T minBpm, T maxBpm) noexcept |
| Restricts the searched tempo range, in BPM. Lock free, no alloc. | |
| void | setTightness (T alpha) noexcept |
| Sets the dynamic-programming tightness (Ellis alpha). | |
| T | getTightness () const noexcept |
| Tightness currently in force. | |
| Result | analyze (AudioBufferView< const T > whole) |
| Tracks tempo and beats over a whole mono buffer (channel 0). | |
| void | beginOffline (int64_t expectedSamples=0) |
| Opens an incremental offline analysis. | |
| void | pushOffline (std::span< const T > samples) |
| Feeds the next piece of an offline session (channel-0 samples). No-op outside one. | |
| Result | finishOffline () |
| Closes the session and tracks tempo and beats over everything pushed, as analyze() does. All-zero Result when no session is open. | |
| void | processBlock (AudioBufferView< const T > in) noexcept |
| Feeds a mono block; reads channel 0 only. Const, never mutated. Lock free and allocation free. Safe no-op before prepare(). | |
| void | pushSamples (std::span< const T > samples) noexcept |
| Feeds a mono stream of samples. Lock free, allocation free. | |
| T | getRunningTempoBpm () const noexcept |
| Running tempo estimate in BPM, 0 before the first frame. | |
| T | getConfidence () const noexcept |
| Coherence of the envelope with the running tempo, in [0,1]. | |
| void | getTempoAndConfidence (T &bpmOut, T &confidenceOut) const noexcept |
| Running tempo and its confidence, from ONE load. | |
| bool | beatNow () const noexcept |
| True if a beat was reported during the most recent call. | |
| int64_t | getLastBeatSample () const noexcept |
| Sample index the most recent causal beat is attributed to; -1 before the first one. | |
| int | getLatencySamples () const noexcept |
| Delay between a beat and the call in which it is announced. | |
| double | getFrameRate () const noexcept |
| Analysis frames per second of the shared envelope. | |
| const OnsetDetector< T > & | getOnsetDetector () const noexcept |
| Read-only access to the front end, for callers that also want onsets and would otherwise run a second copy of the same STFT. | |
| void | reset () noexcept |
| Clears all streaming state and abandons an open offline session. Not concurrent with pushSamples(). | |
Friends | |
| struct | detail::OfflineBeatEngine< T > |
Tempo and beat tracking with an offline grid and a causal readout.
Role: analysis readout. It consumes const audio and never mutates it. All heap use for the causal path happens in prepare(); processBlock() allocates nothing, takes no lock and throws nothing. analyze() is offline and allocates input-dependent envelope and grid working storage.
| T | Sample type (float or double). |
Definition at line 317 of file BeatTracker.h.
|
inline |
Definition at line 324 of file BeatTracker.h.
|
inline |
Tracks tempo and beats over a whole mono buffer (channel 0).
Allocates; not an audio-thread call, and not concurrent with the audio path – it drives the same front end, so it CLEARS the causal state on entry and leaves it cleared.
Returns an all-zero Result with an empty grid when the input is shorter than the analysis needs (kMinAnalysisBeats beats at the slowest searched tempo), rather than a tempo fitted to nothing.
Definition at line 534 of file BeatTracker.h.
|
inlinenoexcept |
True if a beat was reported during the most recent call.
Acquire, paired with the release the processing call ends on: a reader that sees this true and then reads getLastBeatSample() is guaranteed the position written in that same call or a later one, never an earlier one.
That is the order to use, and it is the only safe one. Reading the two the other way round – position first, latch second – is not covered by the pairing: the two loads are separate, the processing call may run between them, and the reader is then told a beat just happened while holding the position of an earlier one. Measured, that order returns a stale position on essentially every beat it observes, by one full beat period and by more when the reader's two loads are further apart than one beat. See the threading notes in the file header.
Definition at line 744 of file BeatTracker.h.
|
inline |
Opens an incremental offline analysis.
For material that arrives in pieces – a file decoded block by block, a recording too long to hold – without concatenating it first. Feed it with pushOffline() in blocks of any size and close it with finishOffline(); the Result is bit-identical to analyze() over the concatenation, whatever the blocking. Only the onset envelope is kept between calls (a few values per 5 ms hop), not the audio.
Same threading as analyze(): it drives the shared front end, so it clears the causal state on entry and must not be interleaved with processBlock() / pushSamples() or run while the audio path is running.
| expectedSamples | Optional length hint, used only to reserve the envelope up front. |
Definition at line 560 of file BeatTracker.h.
|
inline |
Closes the session and tracks tempo and beats over everything pushed, as analyze() does. All-zero Result when no session is open.
Definition at line 603 of file BeatTracker.h.
|
inlinenoexcept |
Coherence of the envelope with the running tempo, in [0,1].
The share of onset strength that falls in phase with the delivered grid, discounted by how nearly another metrical level explained the same envelope. See the file header for the definition and for the closed form it can be checked against.
WHAT IT DOES NOT TELL YOU. It is not a detector of half and double tempo, and no number computed from the signal can be: a click train with alternate events attenuated is at once a backbeat at the fast rate and straight eighths at the slow one, so the two cases that a caller would want told apart are the same waveform. Measured on straight eighths, the tracker reports the eighth level above the amplitude ratios in the file header and reports it with a confidence of 0.99, because that grid does explain nearly all of the onset strength – which is what the number says, and it is true. Check secondaryTempoBpm whenever the metrical level matters; it holds the other reading at every one of those points.
A LOW value means one of two things and they are worth separating: the grid explains little of what happened, or another level explains it just as well. The second is the case the discount produces, and there the number falls to zero rather than to something middling – measured 0.000 on straight eighths at the amplitude where the two readings are level, against 0.99 on either side of it.
Definition at line 704 of file BeatTracker.h.
|
inlinenoexcept |
Analysis frames per second of the shared envelope.
Definition at line 780 of file BeatTracker.h.
|
inlinenoexcept |
Sample index the most recent causal beat is attributed to; -1 before the first one.
This is where the beat WAS, in the caller's own timeline, not when it was announced. The two differ by getLatencySamples() and a caller that aligns anything to the grid wants this one: the announcement carries the analysis delay, the attribution does not.
Relaxed on its own. To pair it with beatNow(), call beatNow() FIRST and this second; the reverse order can hand back the position of an earlier beat than the one the latch announced. See beatNow().
Definition at line 762 of file BeatTracker.h.
|
inlinenoexcept |
Delay between a beat and the call in which it is announced.
Half the analysis frame less the front end's group-delay compensation, plus one hop for the frame the phase turn is observed on. Fixed for a given sample rate and independent of block size.
Definition at line 774 of file BeatTracker.h.
|
inlinenoexcept |
Read-only access to the front end, for callers that also want onsets and would otherwise run a second copy of the same STFT.
Definition at line 784 of file BeatTracker.h.
|
inlinenoexcept |
Running tempo estimate in BPM, 0 before the first frame.
Definition at line 671 of file BeatTracker.h.
|
inlinenoexcept |
Running tempo and its confidence, from ONE load.
The pair is published as a single word, so this hands back two numbers that were measured at the same instant. Calling the two single getters one after the other can straddle an update and pair a fresh tempo with the confidence of the previous one, which is the reading that would let a caller trust a tempo the tracker had already stopped believing.
Definition at line 720 of file BeatTracker.h.
|
inlinenoexcept |
Tightness currently in force.
Definition at line 516 of file BeatTracker.h.
|
inline |
Allocates all causal state and prepares the shared front end.
Not real-time safe. An invalid spec (non-finite or non-positive sample rate) is ignored and the previous configuration is preserved.
The resonator bank is built once here, over the whole supported tempo span (kBankMinBpm..kBankMaxBpm) rather than over the range in force, so that setTempoRange() can stay allocation free and callable from the control thread while audio runs. The span reaches an octave BELOW the slowest searchable tempo on purpose: the octave discriminator needs the candidate's own subharmonic, which is a resonator that must exist.
Definition at line 365 of file BeatTracker.h.
|
inlinenoexcept |
Feeds a mono block; reads channel 0 only. Const, never mutated. Lock free and allocation free. Safe no-op before prepare().
Definition at line 617 of file BeatTracker.h.
|
inline |
Feeds the next piece of an offline session (channel-0 samples). No-op outside one.
Definition at line 582 of file BeatTracker.h.
|
inlinenoexcept |
Feeds a mono stream of samples. Lock free, allocation free.
The stream is handed to the front end in pieces that end exactly on its analysis-frame boundaries, so this call knows precisely which frames it caused and can read each one's envelope value as it appears. The result does not depend on how the caller blocks the stream.
Definition at line 633 of file BeatTracker.h.
|
inlinenoexcept |
Clears all streaming state and abandons an open offline session. Not concurrent with pushSamples().
Definition at line 791 of file BeatTracker.h.
|
inlinenoexcept |
Restricts the searched tempo range, in BPM. Lock free, no alloc.
Clamped into the bank's supported span and to min <= max; a non-finite or inverted request is ignored and the previous range kept. Takes effect on the next analysis frame for the causal path and on the next call for analyze().
Definition at line 402 of file BeatTracker.h.
|
inlinenoexcept |
Sets the dynamic-programming tightness (Ellis alpha).
Weight of the interval-consistency term against the onset evidence: larger holds the grid closer to the estimated period, smaller lets it follow the envelope. Offline path only – the causal path has no transition cost to weigh. Non-finite values are ignored; values are clamped to a positive floor because a tightness of zero removes the term the recursion is built on and leaves the grid free to place a beat at every envelope peak.
Default 25, and the number is measured rather than inherited.
A CAUTION ON MEASURING IT. On isochronous clicks this parameter does nothing at all: the optimal interval is exactly the target period, so the cost it weighs is identically zero, and the grid is BIT-IDENTICAL from alpha = 0.001 – which deletes the term outright – to alpha =
MEASURED, on a bed that does exercise it: expressive timing (rubato at +/-8, +/-10 and +/-15%, ritardando, accelerando), material with onsets off the beat and beats carrying no onset (dropouts, syncopation, straight eighths, swing), and a noise bed under heavy jitter. Reported at the WORST case of that bed, F-measure at +/-70 ms:
alpha 0.001 10 25 50 100 200 400 1000 4000 worst F 0.685 0.760 0.820 0.820 0.820 0.609 0.564 0.520 0.544 mean F 0.938 0.978 0.984 0.982 0.978 0.944 0.913 0.881 0.881
The WORST-case row has a plateau from 25 to 100, and its two edges are real failures, one on each side: below it the grid follows spurious peaks (a sparse pattern with the kick off the beat drops from 0.820 to 0.760, and with the term deleted the same case is 0.760 and the metrical level is wrong twice); above it expressive timing collapses (a +/-15% rubato at 120 BPM scores 1.0000 at alpha = 25 and at 50, 0.9919 at 100 and 0.5645 at 400 – at 400 the tracker loses 43% of that grid).
That plateau is one statistic, and it is not a tie. Compared case by case, alpha = 25 is equal to alpha = 100 on 12 of the 15 cases, better on three – +/-15% rubato at 120 BPM 1.0000 against 0.9919, +/-15% rubato at 90 BPM 1.0000 against 0.9574, the syncopated pattern at 110 BPM 1.0000 against 0.9649 – and worse on none. 25 likewise loses to 50 on no case and beats it on one. The mean row printed above says the same thing in one number, and the ranking, not the plateau, is what picks the default: the bed contains no case that prefers a larger value.
The default is therefore the BOTTOM of the plateau, decided by that per-case comparison. The same order holds OFF this bed, on the sparse and noisy material a larger value might be expected to protect: over a second bed of 16 cases – noise swept to 0.50 rms at two tempi, and beats carrying no onset at all, with and without noise – alpha = 25 is better than 100 on six, worse on one and level on nine. The six are large (F 0.8113 against 0.2885 at 0.45 rms, 0.9346 against 0.6226 at 0.40) and the one is small (0.9720 against 0.9811 at 0.30 rms). A tighter grid does not rescue thin evidence: it holds the grid to a period that thin evidence was too weak to have set correctly in the first place.
It is NOT the 400 the original paper names as its default (Figure 5 caption) and hard-codes in its reference implementation. Against the default, 400 loses on six of the fifteen cases – the four expressive ones, the syncopated pattern and the jittered bed – ties on the other nine and wins on none, the largest single loss being 0.4355 of F. The published values also disagree with each other by most of an order of magnitude: the same paper's best score on its own tuning set is at alpha = 680, and the reference implementation above shipped 400 through its 0.3 series and 100 from 0.4.0 on. All of them use the natural logarithm and the same cost, so the numbers are directly comparable and the disagreement is real rather than a change of units. None of those numbers is a reason here; the sweep above is.
Callers whose material has a genuinely elastic tempo should lower it further. Raising it is not recommended and was not left as advice: on the two beds above, 30 of the 31 cases are flat or worse when it is raised to 100, and the single case that improves does so by 0.009 of F. A caller that raises it anyway should measure the effect on its own material rather than trust the direction.
Definition at line 508 of file BeatTracker.h.
|
friend |
Definition at line 317 of file BeatTracker.h.