3.5 KiB
algorithms/comb-tempo
Purpose
Comb-filter / autocorrelation tempo estimation. Given an onset-detection function (ODF) and the cadence at which it was sampled, this module finds the dominant periodicity and reports the implied tempo in beats per minute.
It is the universal back-end for package's three-stage
detection pipeline — every algorithm in the package (energy,
spectral-flux, and the explicit comb selection) ends up here once it has
produced a per-block ODF, because tempo estimation is independent of which
ODF you started from.
Features
combScore(odf, period)— single-period autocorrelation score. Cheap building block exposed for callers that want to draw a tempo-confidence curve or implement a custom octave-error policy.combTempo(odf, opts)— full sweep across every integer BPM in[minBpm, maxBpm]. Returns the winning BPM and its raw score, with built-in octave-error resolution that prefers tempi near the perceptual centre (120 BPM) when two candidates score within 90% of each other.
Usage: Basic Example
import { combTempo } from "@suaveplan/audio-analysis-beat/algorithms/comb-tempo";
// ODF sampled every 1024 samples at 48 kHz → hopSeconds = 1024/48000.
const result = combTempo(odf, {
minBpm: 60,
maxBpm: 200,
hopSeconds: 1024 / 48_000,
});
console.log(result.bpm); // e.g. 120
Usage: Advanced Example
import {
combScore,
combTempo,
} from "@suaveplan/audio-analysis-beat/algorithms/comb-tempo";
// Draw a tempo-confidence curve over the search range manually:
const hopSeconds = 0.02; // 50 Hz ODF
const min = 60;
const max = 200;
const curve = new Float64Array(max - min + 1);
for (let bpm = min; bpm <= max; bpm++) {
const period = Math.max(1, Math.round((60 / bpm) / hopSeconds));
curve[bpm - min] = combScore(odf, period);
}
// Fall back to combTempo for the canonical winner including octave handling.
const winner = combTempo(odf, { minBpm: min, maxBpm: max, hopSeconds });
console.log(winner.bpm, winner.score);
API Reference
interface CombTempoOptions { minBpm: number; maxBpm: number; hopSeconds: number }— input bounds and ODF cadence.interface CombTempoResult { bpm: number; score: number }.combScore(odf: Float64Array, periodOdfSamples: number): number— returns zero forperiod <= 0and otherwise the sum ofodf[i] * odf[i - period]across the valid index range.combTempo(odf: Float64Array, opts: CombTempoOptions): CombTempoResult— returns{ bpm: minBpm, score: 0 }for empty ODF, non-positivehopSeconds, or inverted bounds. Otherwise sweeps every integer BPM in the inclusive range.
Implementation Notes
The score for a candidate BPM is the unnormalised autocorrelation of the ODF
at the corresponding lag. The lag is computed as round((60 / bpm) / hopSeconds) and clamped at one ODF sample so candidate BPMs near maxBpm
never collapse to a zero lag.
Octave-error resolution is intentionally cheap: among all candidates whose score is within 90% of the winner's score, the one closest to 120 BPM wins. This is the median tapping tempo across human listeners (Moelants 2002) and matches perception far more often than picking the raw maximum, which is biased toward half-tempo for slow tracks and double-tempo for fast tracks.
Integer BPM resolution is sufficient for the universal use case — the caller can run a quadratic interpolation across the three neighbours of the winning bin if sub-BPM precision is needed, but that machinery is not part of this module to keep the public surface small.