# `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 ```ts 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 ```ts 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 for `period <= 0` and otherwise the sum of `odf[i] * odf[i - period]` across the valid index range. - `combTempo(odf: Float64Array, opts: CombTempoOptions): CombTempoResult` — returns `{ bpm: minBpm, score: 0 }` for empty ODF, non-positive `hopSeconds`, 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.