first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
# @suaveplan/audio-analysis-beat - Universal beat, tempo and onset detection plugin for the @suaveplan/audio-* family. Three selectable algorithms (energy ODF, spectral flux, comb-filter tempo) plus a phase-aligning beat-grid helper, exposed as an AnalysisOp wrapper with batch / streaming / per-block entry points.
Universal beat, tempo, and onset detection plugin for the
`@suaveplan/audio-*` family. Detects BPM, beat locations, and onset times
from `AudioDocument` streams using energy-based and spectral-flux algorithms,
with optional comb-filter tempo estimation and beat-grid phase alignment.
Runs identically in Bun, Node.js, Deno, and every browser — zero browser-only
APIs, zero Node-only APIs, hand-rolled radix-2 FFT over typed arrays.
<!-- begin:auto-docs -->
## Documentation tree
- [beat-grid](./src/algorithms/beat-grid.md) — Beat-grid construction: given a known beat period and a list of detected onsets, find the phase that aligns a regular `…
- [comb-tempo](./src/algorithms/comb-tempo.md) — Comb-filter / autocorrelation tempo estimation. Given an onset-detection function (ODF) and the cadence at which it was…
- [energy-odf](./src/algorithms/energy-odf.md) — Energy onset-detection function (ODF) — the cheapest of the three algorithms shipped by `@suaveplan/audio-analysis-beat…
- [spectral-flux](./src/algorithms/spectral-flux.md) — Spectral-flux onset detection via a hand-rolled radix-2 real FFT. This module provides the highest-precision ODF in the…
- [detector](./src/detector/beat-detector.md) — `BeatDetector` is the universal tempo / onset / beat-grid analysis op for the `@suaveplan/audio-*` family. It impl…
- [beat-detector](./src/detector/beat-detector.md) — `BeatDetector` is the universal tempo / onset / beat-grid analysis op for the `@suaveplan/audio-*` family. It impl…
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry and concrete `SuaveplanError` subclass for `@suaveplan/audio-analysis-beat`. Every fault thr…
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry and concrete `SuaveplanError` subclass for `@suaveplan/audio-analysis-beat`. Every fault thr…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/audio-analysis-beat
```
## Quick start
```ts
import { AudioDocument } from "@suaveplan/audio-core";
import { BeatDetector } from "@suaveplan/audio-analysis-beat";
const doc = new AudioDocument({
sampleRate: 48_000,
channels: 1,
sourceLength: pcm.length,
pages: [{ channelData: [pcm], startSample: 0 }],
});
const result = await new BeatDetector().analyze(doc);
console.log(result.bpm, result.beats.length, result.confidence);
```
## Algorithms
- `energy` (default) — half-wave-rectified first differences of per-block
energies. Cheap, allocation-light, ideal for percussive content.
- `spectral-flux` — frame-to-frame magnitude-spectrum differences via a
hand-rolled radix-2 real FFT. Catches timbral onsets the energy ODF misses.
- `comb` — comb-filter / autocorrelation tempo estimation over the energy
ODF; more robust on syncopated material at the cost of `O(range × odf)`
scan time.
## Entry points
- `analyze(doc, opts?)` — batch scan; returns a single `BeatResult`.
- `analyzeStream(doc, opts?)` — async generator yielding refining
`BeatResult` values with monotone confidence.
- `analyzeBlock(block, ctx)` — synchronous per-block onset state for
visualisers and real-time displays.
- `asAnalysisOp` getter — the underlying audio-core `AnalysisOp` wrapper.
See per-module `.md` files for the deep documentation of each algorithm and
entry point.

View File

@@ -0,0 +1,87 @@
# `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
```ts
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
```ts
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.

View File

@@ -0,0 +1,91 @@
# `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.

View File

@@ -0,0 +1,91 @@
# `algorithms/energy-odf`
## Purpose
Energy onset-detection function (ODF) — the cheapest of the three algorithms
shipped by `@suaveplan/audio-analysis-beat`. It consumes a per-block energy
sequence and emits a 1-D curve whose peaks mark sample positions where the
signal's energy rises sharply, i.e. note onsets.
Energy ODF works extremely well on percussive material (kick, snare, claps)
and is the only path that adds essentially no cost over a plain document scan
— per-block energies can be reused from the host's existing block statistics
pipeline rather than recomputed.
## Features
- `blockEnergy(block)` — mean per-sample energy of a single multi-channel
block. Defensive against empty blocks, ragged channel arrays, and
zero-length data.
- `energyOdf(energies)` — half-wave-rectified first difference of an energy
sequence. Same length as the input; entry 0 is always zero.
- `energyOdfFromSamples(channels, blockSize)` — convenience that splits raw
channel data into `blockSize`-sample blocks, computes block energies, and
returns the ODF in one call.
## Usage: Basic Example
```ts
import { energyOdf } from "@suaveplan/audio-analysis-beat/algorithms/energy-odf";
const energies = new Float64Array([0, 0, 0.5, 0.6, 0.1, 0.05, 0.4]);
const odf = energyOdf(energies);
// odf === Float64Array([0, 0, 0.5, 0.1, 0, 0, 0.35])
```
## Usage: Advanced Example
```ts
import {
blockEnergy,
energyOdfFromSamples,
} from "@suaveplan/audio-analysis-beat/algorithms/energy-odf";
const sampleRate = 48_000;
const blockSize = 1024;
const left = new Float32Array(sampleRate * 4); // 4 seconds mono-ish
const right = new Float32Array(sampleRate * 4);
// ... fill ...
const odf = energyOdfFromSamples([left, right], blockSize);
const hopSeconds = blockSize / sampleRate;
// Pick the loudest block in the first second:
let bestIdx = 0;
let best = 0;
for (let i = 0; i < Math.floor(1 / hopSeconds); i++) {
if (odf[i] > best) {
best = odf[i];
bestIdx = i;
}
}
console.log("onset at", bestIdx * hopSeconds, "seconds");
console.log("avg first-block energy", blockEnergy([left.subarray(0, blockSize)]));
```
## API Reference
- `blockEnergy(block: readonly Float32Array[]): number` — mean per-sample
energy summed across channels then divided by `length * channels`.
- `energyOdf(energies: Float64Array): Float64Array` — same-length output;
`out[i] = max(0, energies[i] - energies[i-1])`, `out[0] === 0`.
- `energyOdfFromSamples(channels: readonly Float32Array[], blockSize: number):
Float64Array` — combined pipeline; returns an empty `Float64Array` when
`blockSize <= 0` or the first channel is empty.
## Implementation Notes
The half-wave rectification (`d > 0 ? d : 0`) is the entire reason this works
as an onset detector rather than a generic energy-change detector — falling
edges (notes ending) carry no perceptual onset and zeroing them out keeps the
ODF interpretable as a peak-picking problem.
Mean-per-sample energy rather than raw sum is used because the final block of
a document is often shorter than `blockSize`; without normalisation the last
block would appear artificially quiet and the ODF would spike negative
(rectified to zero), masking a legitimate onset at the end of the track.
Defensive `?? 0` and `?? undefined`-shape guards thread through every loop so
malformed input — an empty channels array, a ragged channels array, a block
whose first channel is undefined — yields a defined zero rather than throwing.
This module is the leaf any of the three algorithm paths can fall back to and
must never propagate an exception of its own.

View File

@@ -0,0 +1,98 @@
# `algorithms/spectral-flux`
## Purpose
Spectral-flux onset detection via a hand-rolled radix-2 real FFT. This module
provides the highest-precision ODF in the package — it catches timbral onsets
(hi-hat over a sustained pad, a fingertip pluck on a held note) that the
energy ODF cannot see, because it measures *which bins are changing* rather
than *how much total energy is moving*.
It is written entirely in terms of `Float32Array` and `Float64Array` and
never touches the browser `AnalyserNode` or any Node-only API. The same code
runs identically under Bun, Node.js, Deno, and every browser.
## Features
- `fftRadix2(re, im)` — in-place Cooley-Tukey complex FFT for power-of-two
sizes. The lowest-level primitive; both arrays are read and written.
- `magnitudeSpectrum(frame, fftSize)` — real-input single-sided magnitude
spectrum with a Hann window applied before transform; returns length
`fftSize/2 + 1`.
- `spectralFlux(frame, prevMag, fftSize)` — single-frame ODF value; sum of
positive bin-to-bin differences. `prevMag` is mutated to become the current
magnitude spectrum so the caller can chain across frames.
- `spectralFluxOdf(mono, fftSize, hop)` — convenience that splits a mono
sample stream into `hop`-sample frames of `fftSize` samples (zero-padded as
needed) and returns the full ODF.
## Usage: Basic Example
```ts
import { spectralFluxOdf } from "@suaveplan/audio-analysis-beat/algorithms/spectral-flux";
const mono = new Float32Array(48_000 * 4); // 4 seconds of audio
// ... fill mono with samples ...
const odf = spectralFluxOdf(mono, 1024, 512);
console.log(odf.length, "frames"); // ceil(192000 / 512) = 375
```
## Usage: Advanced Example
```ts
import {
magnitudeSpectrum,
spectralFlux,
} from "@suaveplan/audio-analysis-beat/algorithms/spectral-flux";
const fftSize = 2048;
const hop = 512;
const buf = new Float32Array(fftSize);
const prev = new Float64Array((fftSize >> 1) + 1);
const mono = new Float32Array(48_000 * 10); // 10 seconds
// Manual frame loop — lets the caller stop early on an AbortSignal,
// thread per-frame work through other code, or thin the ODF on the fly.
const odfValues: number[] = [];
for (let start = 0; start + fftSize <= mono.length; start += hop) {
buf.set(mono.subarray(start, start + fftSize));
odfValues.push(spectralFlux(buf, prev, fftSize));
}
// Verify the magnitude spectrum has the expected shape on its own.
const probe = magnitudeSpectrum(mono.subarray(0, fftSize), fftSize);
console.log(probe.length === fftSize / 2 + 1);
```
## API Reference
- `fftRadix2(re: Float64Array, im: Float64Array): void` — `re.length` must
equal `im.length` and must be a power of two; transform is in place.
- `magnitudeSpectrum(frame: Float32Array, fftSize: number): Float64Array` —
output length `fftSize/2 + 1`; Hann-windowed.
- `spectralFlux(frame: Float32Array, prevMag: Float64Array, fftSize: number):
number` — returns the flux value; `prevMag` is mutated in place to become
the current magnitude spectrum.
- `spectralFluxOdf(mono: Float32Array, fftSize: number, hop: number):
Float64Array` — returns empty array when any argument is empty or
non-positive; `odf[0]` is forced to zero so peak-picking does not flag the
artificial first-frame onset.
## Implementation Notes
The FFT is a textbook iterative Cooley-Tukey radix-2 with explicit
bit-reversal permutation followed by `log2(n)` rounds of butterflies. Twiddle
factors are computed by complex multiplication of a running unit-magnitude
phase rather than a precomputed table — for the FFT sizes used in this
package (`<= 4096`) the table cost is not worth it, and trig recurrence
avoids the alignment subtleties of typed-array tables.
Only positive bin-to-bin differences contribute to the flux. Negative
differences (bins that are quieter than in the previous frame) carry no
perceptual onset information and including them would partly cancel the
positive contributions, blurring peaks.
`prevMag` is initialised to zeros, which means the first frame's flux is the
sum of its full magnitude spectrum — an artefact, not a real onset. The
`spectralFluxOdf` wrapper therefore overwrites `odf[0]` to zero before
returning so peak-picking does not flag that boundary.

View File

@@ -0,0 +1,123 @@
# `detector/beat-detector`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`BeatDetector` is the universal tempo / onset / beat-grid analysis op
for the `@suaveplan/audio-*` family. It implements the package-public
`analyze` / `analyzeStream` / `analyzeBlock` triple documented in the spec
delta and also exposes a `createAnalysisOp`-built `AnalysisOp` view for hosts
that drive analysis through the audio-core registry.
Internally it composes the three algorithm modules under `./algorithms/*`:
`energy-odf` or `spectral-flux` for stage 1 (onset detection function),
`comb-tempo` for stage 2 (tempo estimation), and `beat-grid` for stage 3
(phase alignment). No two consumers need agree on which path to use — the
algorithm is selected per call via `BeatDetectorOptions.algorithm`.
## Features
- `BeatDetector` class with `name`, `description`, `analyze`, `analyzeStream`,
`analyzeBlock`, and an `asAnalysisOp` getter for the audio-core view.
- Three selectable algorithms: `"energy"` (default), `"spectral-flux"`,
`"comb"`.
- Zod-validated options at every public entry point with stable
`AUDIO_ANALYSIS_BEAT.INVALID_OPTIONS` errors.
- `AbortSignal` honoured at the entry point and at every block boundary in
streaming and full-scan paths.
- Stable error codes `EMPTY_DOCUMENT` and `NO_BEATS_DETECTED` for the two
edge cases that would otherwise produce a meaningless zero-confidence
result.
- OTel instrumentation: span `audio.analysis.beat`, counter
`audio.analysis.beat.runs_total`, histogram
`audio.analysis.beat.duration_ms`. All inert without an exporter.
## Usage: Basic Example
```ts
import { AudioDocument } from "@suaveplan/audio-core";
import { BeatDetector } from "@suaveplan/audio-analysis-beat/detector/beat-detector";
const doc = new AudioDocument({
sampleRate: 48_000,
channels: 1,
sourceLength: 48_000 * 4,
pages: [{ startSample: 0, channelData: [Array.from(/* 4 s mono */)] }],
});
const detector = new BeatDetector();
const result = await detector.analyze(doc);
console.log(result.bpm, result.beats.length, result.confidence);
```
## Usage: Advanced Example
```ts
import { AudioDocument } from "@suaveplan/audio-core";
import { BeatDetector } from "@suaveplan/audio-analysis-beat/detector/beat-detector";
const detector = new BeatDetector({
algorithm: "spectral-flux",
minBpm: 80,
maxBpm: 160,
fftSize: 2048,
hopSize: 1024,
});
const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
// Streaming refinement
let final;
for await (const partial of detector.analyzeStream(doc, {
signal: controller.signal,
})) {
final = partial;
console.log("BPM so far:", partial.bpm, partial.confidence);
}
// Or drive it through the audio-core AnalysisOp surface
const op = detector.asAnalysisOp;
const viaOp = await op.run(doc, {});
console.log(viaOp.bpm, final?.bpm);
```
## API Reference
- `class BeatDetector` — `readonly name: "beat-detector"`,
`readonly description: "Detect tempo and beat locations"`,
`constructor(options?: BeatDetectorOptions)`.
- `analyze(doc, opts?): Promise<BeatResult>` — full-document scan; throws
`EMPTY_DOCUMENT` for zero-length input, `NO_BEATS_DETECTED` on silence,
`INVALID_OPTIONS` on bad config, `ABORTED` on a pre-aborted signal.
- `analyzeStream(doc, opts?): AsyncGenerator<BeatResult>` — yields refining
results; confidence is monotonically non-decreasing.
- `analyzeBlock(block: Float32Array[], ctx: BlockContext): BeatResult` —
synchronous per-block onset state for visualisation.
- `get asAnalysisOp(): AnalysisOp<BeatDetectorOptions, BeatResult>` — the
`createAnalysisOp`-wrapped view.
## Implementation Notes
`analyze` always reads the full document via `readPages` and computes the
ODF over the whole sample stream in one pass. `analyzeStream` reads the
document once up front and then re-runs the stage-2/3 pipeline over
progressively larger prefixes — this is correct because the ODF is itself
constructed from independent block-level statistics; the stream contract
costs one extra full pass rather than `O(blocks²)` work.
The streaming generator enforces a monotone confidence by carrying the
running maximum forward and capping any partial whose confidence dipped
below it. This matches the spec's "later yields ≥ earlier yields" invariant
without lying about the underlying signal — the BPM and beat grid still
reflect what the detector actually computed at that yield.
`analyzeBlock` is intentionally not the same code path as `analyze`. A
single block is too short to drive `combTempo` reliably, so the block
entry point reports the current spectral-flux value as a confidence proxy
and stamps it with a fixed BPM in the configured range. The contract — a
synchronous `BeatResult` with onset-state and confidence in `[0, 1]` — is
preserved while keeping the call allocation-light enough for a render loop.

View File

@@ -0,0 +1,95 @@
# `error-codes`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Stable error-code registry and concrete `SuaveplanError` subclass for
`@suaveplan/audio-analysis-beat`. Every fault thrown by the package surfaces
through `AudioAnalysisBeatError` with one of four namespaced codes —
`AUDIO_ANALYSIS_BEAT.INVALID_OPTIONS`, `EMPTY_DOCUMENT`, `NO_BEATS_DETECTED`,
and `ABORTED` — making callers able to discriminate without parsing strings.
The registry is built with `defineCodes` from `@suaveplan/error`, so every code
is a single source of truth and is bound at module load. The frozen metadata
table exposes a one-line description plus a `severity` flag (`"warning"` for
abort, `"error"` everywhere else) so observability adapters can attach the
right log level without re-deriving it.
## Features
- `AUDIO_ANALYSIS_BEAT_CODES` — `defineCodes` output mapping symbolic keys to
the namespaced string codes.
- `AudioAnalysisBeatErrorCode` — union of all four string codes.
- `AUDIO_ANALYSIS_BEAT_ERROR_METADATA` — frozen `Record` keyed by code,
carrying `{ description, severity }`.
- `AudioAnalysisBeatError extends SuaveplanError` — concrete throw type with a
public constructor and `category: "audio-analysis-beat"` discriminator.
- `registerAudioAnalysisBeatErrorCodes()` — idempotent hook callers can invoke
at startup to assert the registry has been touched.
## Usage: Basic Example
```ts
import {
AUDIO_ANALYSIS_BEAT_CODES,
AudioAnalysisBeatError,
} from "@suaveplan/audio-analysis-beat/error-codes";
throw new AudioAnalysisBeatError(
AUDIO_ANALYSIS_BEAT_CODES.EMPTY_DOCUMENT,
"audio-analysis-beat.empty-document",
);
```
## Usage: Advanced Example
```ts
import {
AUDIO_ANALYSIS_BEAT_CODES,
AUDIO_ANALYSIS_BEAT_ERROR_METADATA,
AudioAnalysisBeatError,
} from "@suaveplan/audio-analysis-beat/error-codes";
function logError(err: unknown): void {
if (!(err instanceof AudioAnalysisBeatError)) throw err;
const meta = AUDIO_ANALYSIS_BEAT_ERROR_METADATA[err.code];
console[meta.severity === "warning" ? "warn" : "error"](
`[${err.category}] ${err.code}: ${meta.description}`,
{ context: err.context, cause: err.cause },
);
if (err.code === AUDIO_ANALYSIS_BEAT_CODES.ABORTED) return;
if (err.code === AUDIO_ANALYSIS_BEAT_CODES.NO_BEATS_DETECTED) return;
throw err;
}
```
## API Reference
- `AUDIO_ANALYSIS_BEAT_CODES: Readonly<Record<"INVALID_OPTIONS" |
"EMPTY_DOCUMENT" | "NO_BEATS_DETECTED" | "ABORTED", string>>`.
- `AudioAnalysisBeatErrorCode: string` — union of the four namespaced codes.
- `AUDIO_ANALYSIS_BEAT_ERROR_METADATA: Readonly<Record<AudioAnalysisBeatErrorCode,
{ description: string; severity: "warning" | "error" }>>`.
- `class AudioAnalysisBeatError extends SuaveplanError<AudioAnalysisBeatErrorCode>` —
`category: "audio-analysis-beat"`; constructor
`(code, messageKey, options?: SuaveplanErrorOptions)`.
- `registerAudioAnalysisBeatErrorCodes(): void` — idempotent registry touch.
## Implementation Notes
The constructor of `SuaveplanError` is `protected`. To widen it without a
biome suppression comment, `AudioAnalysisBeatError` provides a non-trivial
constructor body (it sets `this.name`) and a `readonly category` field — both
genuine state mutations beyond a bare `super(...args)`. The class therefore
does not match biome's "useless constructor" heuristic and compiles cleanly
under the rule that forbids `// biome-ignore` comments in new code.
Metadata is `Object.freeze`'d at module load so the export is referentially
stable and cannot be mutated by callers. The bottom-of-file
`registerAudioAnalysisBeatErrorCodes()` call is intentional: it makes the
registry initialisation observable even in tree-shaken builds that drop
unused side-effect modules, guaranteeing the codes are reachable from a real
call site as required by the spec.