Files
skills/docs/media/audio/audio-analysis-beat/src/algorithms/beat-grid.md
2026-05-29 08:20:56 +01:00

3.6 KiB

algorithms/beat-grid

Purpose

Beat-grid construction: given a known beat period and a list of detected onsets, find the phase that aligns a regular period-spaced grid to the onsets and emit the grid itself. This is package's three-stage detection pipeline — what runs after tempo estimation has produced a BPM and the caller is ready to convert raw onset times into a downbeat-stable beat sequence.

The same phase sweep also yields a confidence score in [0, 1] that quantifies how tightly the detected onsets cluster around the chosen grid — a sharp cluster (every onset within a tenth of a period of a grid line) gives a confidence near 1, scattered onsets give one near 0.

Features

  • buildBeatGrid(opts) — sweep phaseTrials equally-spaced candidate phases across one period, pick the phase that minimises total onset-to-nearest-beat distance, and return the resulting beat grid.
  • Configurable sweep resolution (phaseTrials, default 24) — finer sweeps cost linearly more time but tighten the grid alignment in proportion.
  • Defensive against zero-period, zero-duration, and empty-onsets inputs — returns an empty grid with confidence = 0 rather than throwing.

Usage: Basic Example

import { buildBeatGrid } from "@suaveplan/audio-analysis-beat/algorithms/beat-grid";

const result = buildBeatGrid({
	period: 60 / 120, // 120 BPM
	duration: 10,
	onsets: [0.5, 1.0, 1.5, 2.0],
});
console.log(result.beats.length, result.phase, result.confidence);

Usage: Advanced Example

import { buildBeatGrid } from "@suaveplan/audio-analysis-beat/algorithms/beat-grid";

// Compare two BPM candidates and prefer the one whose grid the onsets fit
// more tightly — this is how the BeatDetector breaks ties between candidate
// tempi when an autocorrelation sweep returns two near-equal peaks.
const onsets = new Float64Array([
	0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0,
]);

const a = buildBeatGrid({ period: 60 / 120, duration: 4.2, onsets, phaseTrials: 48 });
const b = buildBeatGrid({ period: 60 / 60, duration: 4.2, onsets, phaseTrials: 48 });

const winner = a.confidence >= b.confidence ? a : b;
console.log("chosen phase", winner.phase, "beats", Array.from(winner.beats));

API Reference

  • interface BeatGridOptions { period: number; duration: number; onsets: readonly number[] | Float64Array; phaseTrials?: number }.
  • interface BeatGridResult { beats: Float64Array; phase: number; confidence: number }.
  • buildBeatGrid(opts: BeatGridOptions): BeatGridResult — beats is empty when period <= 0 or duration <= 0; confidence is 0 for empty onsets, otherwise clamped to [0, 1].

Implementation Notes

The cost function is L1 (sum of absolute errors) rather than L2. L1 is more robust against the occasional outlier onset that does not actually mark a beat — a snare on the upbeat, a hi-hat trip, a transient false positive — whereas L2 would let that outlier dominate the phase sweep.

phaseTrials defaults to 24, which is one trial every 2.5° of the beat cycle at 120 BPM (≈ 21 ms). Halving the trial count roughly halves the cost function evaluation time at the price of a coarser phase grid; doubling it sharpens alignment until the cost surface becomes flat at the noise floor of the onset times themselves.

The confidence formula 1 - cost / (n * period/4) treats a uniformly distributed onset list (mean absolute error = quarter-period) as confidence 0 and a perfectly aligned list (zero error) as confidence 1. Inputs that are better than uniform — onsets concentrated on the beat — saturate at 1 rather than overshooting, so downstream code can treat the field as a probability without further clamping.