Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# chord-matching
|
||||
|
||||
## Purpose
|
||||
|
||||
`chord-matching.ts` identifies the closest chord template (major / minor / diminished / augmented over twelve roots — 48 candidates) for a given per-frame chromagram, then smooths and merges the per-frame labels into a time-ordered chord progression. Smoothing is mandatory rather than optional because raw per-frame labels are noisy and a single off-frame would otherwise spawn a spurious chord.
|
||||
|
||||
The module exposes the raw matcher (`matchChord`), the smoothing pass (`smoothLabels`), and the run-merger (`mergeRuns`) so consumers can swap in their own smoothing strategy without losing the chromagram matching.
|
||||
|
||||
## Features
|
||||
|
||||
- 48 triad templates rotated to all twelve roots.
|
||||
- Cosine-similarity scoring with normalisation against template and chromagram norms.
|
||||
- Median-style single-pass smoother coercing solitary disagreements between identical neighbours.
|
||||
- `mergeRuns` collapses identical adjacent labels into `Chord` records with `timestamp`, `duration`, and `confidence`.
|
||||
- `minDuration` knob drops chord runs below a minimum length so transient frames never emit a standalone chord.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { matchChord, mergeRuns } from "@suaveplan/audio-analysis-pitch/algorithms/chord-matching";
|
||||
|
||||
const frameChord = matchChord(chroma); // { root: "C", quality: "major", confidence }
|
||||
const chords = mergeRuns([frameChord, frameChord], { hopSeconds: 0.1, minDuration: 0 });
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
type FrameChord,
|
||||
matchChord,
|
||||
mergeRuns,
|
||||
smoothLabels,
|
||||
} from "@suaveplan/audio-analysis-pitch/algorithms/chord-matching";
|
||||
|
||||
function detect(frames: Float32Array[], hopSeconds: number) {
|
||||
const labels: FrameChord[] = frames.map(matchChord);
|
||||
const smoothed = smoothLabels(labels);
|
||||
return mergeRuns(smoothed, { hopSeconds, minDuration: 0.25 });
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type ChordQuality = "major" | "minor" | "dim" | "aug"`.
|
||||
- `interface FrameChord { root: string; quality: ChordQuality; confidence: number }`.
|
||||
- `interface Chord { timestamp: number; duration: number; root: string; quality: ChordQuality; confidence: number }`.
|
||||
- `matchChord(chroma: Float32Array): FrameChord`.
|
||||
- `smoothLabels(labels: readonly FrameChord[]): FrameChord[]`.
|
||||
- `mergeRuns(labels: readonly FrameChord[], { hopSeconds, minDuration }): Chord[]`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The matcher uses cosine similarity, so a chromagram with all-zero energy returns zero confidence rather than picking a random template by floating-point tiebreak. The smoother is single-pass and idempotent on already-clean runs. `mergeRuns` runs in O(N) over the smoothed label sequence and returns a fresh array — callers may mutate the result safely.
|
||||
@@ -0,0 +1,51 @@
|
||||
# chromagram
|
||||
|
||||
## Purpose
|
||||
|
||||
`chromagram.ts` folds a spectral frame into the twelve pitch classes used by Western tonal analysis. Each magnitude bin is mapped to a pitch class via `pitchClass = round(12 * log2(freq / A4)) mod 12` and accumulated into a 12-element `Float32Array`. The chromagram is the shared substrate for both key-finding and chord template matching in this package.
|
||||
|
||||
The module relies on the vendored real FFT in `./yin.ts` so the chromagram path stays self-contained and does not require `@suaveplan/audio-analysis-spectrum` at runtime.
|
||||
|
||||
## Features
|
||||
|
||||
- `chromagramFromFrame(frame, sampleRate, minFreq?)` — per-frame 12-bin pitch-class profile.
|
||||
- `normaliseChroma(chroma)` — in-place max-normalisation; idempotent on all-zero vectors.
|
||||
- `accumulateChroma(acc, add)` — in-place sum, clamped to the shorter length so callers can pass differently sized buffers safely.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { chromagramFromFrame } from "@suaveplan/audio-analysis-pitch/algorithms/chromagram";
|
||||
|
||||
const chroma = chromagramFromFrame(frame, 48_000);
|
||||
console.log("strongest pitch class:", chroma.indexOf(Math.max(...chroma)));
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
accumulateChroma,
|
||||
chromagramFromFrame,
|
||||
normaliseChroma,
|
||||
} from "@suaveplan/audio-analysis-pitch/algorithms/chromagram";
|
||||
|
||||
async function aggregateChroma(stream: AsyncIterable<Float32Array>, sr: number) {
|
||||
const acc = new Float32Array(12);
|
||||
for await (const frame of stream) {
|
||||
accumulateChroma(acc, chromagramFromFrame(frame, sr));
|
||||
}
|
||||
normaliseChroma(acc);
|
||||
return acc;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `chromagramFromFrame(frame: Float32Array, sampleRate: number, minFreq = 30): Float32Array` (length 12).
|
||||
- `normaliseChroma(chroma: Float32Array): Float32Array` — in-place, returns the same buffer.
|
||||
- `accumulateChroma(acc: Float32Array, add: Float32Array): Float32Array` — clamped to `min(acc.length, add.length)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Frames shorter than eight samples short-circuit to an all-zero chromagram. Energy below `minFreq` (default 30 Hz) and at or above the Nyquist is dropped to suppress hum and aliasing. All operations are O(N) over the FFT magnitude vector. Buffers are caller-owned — the module never allocates on the hot path beyond the result.
|
||||
@@ -0,0 +1,48 @@
|
||||
# downmix
|
||||
|
||||
## Purpose
|
||||
|
||||
`downmix.ts` provides the per-block mono-folding utilities shared by all three detectors in `@suaveplan/audio-analysis-pitch`. Detectors only operate on a single mono channel, so each block emitted by `AudioDocument.stream()` is folded to a single `Float32Array` before analysis. Centralising this in one module avoids three duplicated implementations across the detector files and gives one well-tested code path for every block in the package.
|
||||
|
||||
## Features
|
||||
|
||||
- `downmixMono` averages an arbitrary number of channels into one `Float32Array`.
|
||||
- Handles zero-channel, sparse-channel, and single-channel inputs without branching at every call site.
|
||||
- `blockLength` is a tiny helper that returns the first channel's sample count or zero — the canonical way of reading the block size from a `doc.stream()` yield.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { blockLength, downmixMono } from "@suaveplan/audio-analysis-pitch/algorithms/downmix";
|
||||
|
||||
for await (const block of doc.stream()) {
|
||||
const length = blockLength(block);
|
||||
const mono = downmixMono(block, length);
|
||||
// ... analyse mono
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { blockLength, downmixMono } from "@suaveplan/audio-analysis-pitch/algorithms/downmix";
|
||||
|
||||
function streamAnalyse(block: Float32Array[], analyse: (m: Float32Array) => number) {
|
||||
const length = blockLength(block);
|
||||
if (length === 0) return 0;
|
||||
return analyse(downmixMono(block, length));
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `downmixMono(channels: readonly Float32Array[], length: number): Float32Array` — averages across channels.
|
||||
- `blockLength(block: readonly Float32Array[]): number` — first-channel length or zero.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function tolerates sparse channel arrays (TypeScript's `readonly Float32Array[]` could in theory hold `undefined`); it checks each channel and skips undefined slots. The output length is caller-determined so callers can pre-allocate for known block sizes. The function never allocates per-sample and runs in O(channels × length).
|
||||
|
||||
The averaging is unweighted across channels — for a stereo block of equal amplitude tones in both ears the output is the same single-channel waveform. For richer downmixing (ITU-R BS.775 surround coefficients, equal-power panning, etc.) callers should fold the input themselves before passing a one-channel array to the detector entry points. Pitch and key detection are mostly invariant to the choice of downmix, so the simple mean is the right default and any pluggable mixer would be premature complexity in this module.
|
||||
|
||||
`blockLength` returns a number rather than the channel reference because every consumer in this package wants the length, never the array. Reading the length once and passing it through `downmixMono` lets the compiler hoist the bounds check and avoids re-reading `block[0]?.length` inside the hot loop.
|
||||
@@ -0,0 +1,47 @@
|
||||
# key-finding
|
||||
|
||||
## Purpose
|
||||
|
||||
`key-finding.ts` correlates an aggregated chromagram against 24 major/minor key profiles to estimate the musical key of a passage. Two profile sets ship: the classic Krumhansl-Schmuckler perceptual weights and Temperley's revised weights which reduce relative-major/minor confusion.
|
||||
|
||||
The estimator returns the best-scoring key together with a `confidence` derived from the margin between the top and runner-up correlation, giving callers a usable proxy for how decisive the result was.
|
||||
|
||||
## Features
|
||||
|
||||
- 12 tonics × 2 modes = 24 candidate keys scored per call.
|
||||
- Two interchangeable profile sets via the `profileSet` argument.
|
||||
- Pearson correlation as the scoring function — robust to multiplicative gain.
|
||||
- Stable pitch-class naming via `KEY_PROFILE_NAMES`.
|
||||
- Numerically safe: zero-denom Pearson collapses to zero rather than NaN.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { detectKey } from "@suaveplan/audio-analysis-pitch/algorithms/key-finding";
|
||||
|
||||
const chroma = new Float32Array([6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88]);
|
||||
const { key, confidence } = detectKey(chroma);
|
||||
console.log(key); // "C major"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { detectKey, type KeyProfileSet } from "@suaveplan/audio-analysis-pitch/algorithms/key-finding";
|
||||
|
||||
function compareProfiles(chroma: Float32Array) {
|
||||
const sets: KeyProfileSet[] = ["krumhansl-schmuckler", "temperley"];
|
||||
return sets.map((set) => ({ set, ...detectKey(chroma, set) }));
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `type KeyProfileSet = "krumhansl-schmuckler" | "temperley"`.
|
||||
- `interface KeyEstimate { key: string; mode: "major" | "minor"; tonic: number; confidence: number }`.
|
||||
- `detectKey(chroma: Float32Array, profileSet?: KeyProfileSet): KeyEstimate`.
|
||||
- `KEY_PROFILE_NAMES: readonly string[]` — `["C", "C#", "D", …]`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Profiles are rotated to each tonic with O(1) lookup; for 24 candidates this is 24 Pearson computations per call, each O(12). The confidence is the normalised margin `(best - second) / max(|best|, ε)` clamped to `[0, 1]`. If the chromagram has zero variance the Pearson denominator collapses to zero and the function returns C major with zero confidence rather than NaN.
|
||||
@@ -0,0 +1,57 @@
|
||||
# yin
|
||||
|
||||
## Purpose
|
||||
|
||||
`yin.ts` ships three fundamental-frequency estimators behind a unified `PitchEstimate` shape so the `PitchDetector` can swap algorithms via the `algorithm` option without callers branching on the result type. The default is YIN (de Cheveigné & Kawahara, 2002), with McLeod Pitch Method (MPM) and a real-cepstrum estimator available for material where they outperform YIN.
|
||||
|
||||
This file also vendors a small radix-2 real FFT plus `nextPow2` / `realFftMagnitude` helpers consumed by the chromagram and cepstrum paths so the package has no hard dependency on `@suaveplan/audio-analysis-spectrum`.
|
||||
|
||||
## Features
|
||||
|
||||
- `yinPitch` — difference function, cumulative mean normalised difference, absolute threshold pick at 0.1, parabolic interpolation.
|
||||
- `mcleodPitch` — NSDF + key-maxima picking with threshold-relative-to-global-max.
|
||||
- `cepstrumPitch` — log-magnitude inverse FFT with quefrency peak in the lag window.
|
||||
- Robust no-pitch result: returns `pitch: 0`, `confidence: 0` when the input is silent or has no detectable periodicity.
|
||||
- Vendored radix-2 FFT for self-contained operation.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { yinPitch } from "@suaveplan/audio-analysis-pitch/algorithms/yin";
|
||||
|
||||
const frame = new Float32Array(4096);
|
||||
for (let i = 0; i < frame.length; i++) {
|
||||
frame[i] = Math.sin((2 * Math.PI * 440 * i) / 48_000);
|
||||
}
|
||||
const { pitch, confidence } = yinPitch(frame, 48_000, 20, 20_000);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
cepstrumPitch,
|
||||
mcleodPitch,
|
||||
yinPitch,
|
||||
} from "@suaveplan/audio-analysis-pitch/algorithms/yin";
|
||||
|
||||
function multiAlgorithmEnsemble(frame: Float32Array, sr: number) {
|
||||
const yin = yinPitch(frame, sr, 50, 2_000);
|
||||
const mpm = mcleodPitch(frame, sr, 50, 2_000);
|
||||
const cep = cepstrumPitch(frame, sr, 50, 2_000);
|
||||
return [yin, mpm, cep].sort((a, b) => b.confidence - a.confidence)[0];
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface PitchEstimate { pitch: number; confidence: number }`.
|
||||
- `yinPitch(frame, sampleRate, minFreq, maxFreq): PitchEstimate`.
|
||||
- `mcleodPitch(frame, sampleRate, minFreq, maxFreq, threshold = 0.85): PitchEstimate`.
|
||||
- `cepstrumPitch(frame, sampleRate, minFreq, maxFreq): PitchEstimate`.
|
||||
- `nextPow2(n: number): number`.
|
||||
- `realFftMagnitude(frame: Float32Array): Float32Array`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
YIN's CMNDF gives lag-zero a value of 1, suppressing the trivial autocorrelation peak. MPM walks the NSDF from the first negative crossing onward to avoid the same trivial peak. Parabolic interpolation around the chosen lag yields sub-sample precision. The cepstrum path is sample-rate-bound in resolution (one bin equals `sr/period²`), so its accuracy is coarser than YIN/MPM — typically within ±20 Hz at 48 kHz for a 4096-sample frame.
|
||||
@@ -0,0 +1,49 @@
|
||||
# chord-detector
|
||||
|
||||
## Purpose
|
||||
|
||||
`chord-detector.ts` ships the `ChordDetector` Analysis Op which produces a time-ordered chord progression for an `AudioDocument`. It computes a per-frame chromagram, template-matches every frame against major/minor/dim/aug triad templates across all twelve roots, then smooths and merges adjacent identical labels so the result is a sequence of `Chord` records rather than 48 templates × thousands of frames.
|
||||
|
||||
The smoothing pass is part of the algorithm, not a tuning knob — raw per-frame labels are too noisy to be usable by application code without it.
|
||||
|
||||
## Features
|
||||
|
||||
- 48 triad templates over twelve roots × four qualities (major, minor, diminished, augmented).
|
||||
- Built-in median-style smoothing pass eliminating single-frame spurious labels.
|
||||
- `minDuration` knob drops chord runs shorter than the smoothing window.
|
||||
- `analyze` aggregates the whole document; `analyzeStream` yields refining progressions.
|
||||
- `AbortSignal` honoured at every block boundary; Zod validation on options; OTel instrumentation per run.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { ChordDetector } from "@suaveplan/audio-analysis-pitch";
|
||||
|
||||
const detector = new ChordDetector();
|
||||
const { chords } = await detector.analyze(doc);
|
||||
for (const c of chords) console.log(c.timestamp, c.root, c.quality);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { ChordDetector } from "@suaveplan/audio-analysis-pitch";
|
||||
|
||||
const detector = new ChordDetector();
|
||||
const opts = { frameSize: 8_192, minDuration: 0.5 };
|
||||
let latest = "(none)";
|
||||
for await (const r of detector.analyzeStream(doc, opts)) {
|
||||
const last = r.chords[r.chords.length - 1];
|
||||
if (last !== undefined) latest = `${last.root} ${last.quality}`;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class ChordDetector { name: "chord-detector"; version: string; description: string; analyze(doc, opts?); analyzeStream(doc, opts?) }`.
|
||||
- `interface ChordDetectorOptions { frameSize?, minDuration?, signal? }`.
|
||||
- `interface ChordResult extends AnalysisResult { op: "chord-detector"; chords: readonly Chord[]; timestamp; duration }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Per-frame matching is O(48 × 12) = O(576) cosine similarities per frame and easily fits in the FFT budget. The streaming variant recomputes the merged progression on every yield, which is O(N) over the per-frame labels collected so far; for hour-long documents callers may prefer `analyze` once to avoid the cumulative-merge cost. `ChordResult.chords` is a fresh array on every yield so consumers may keep references safely.
|
||||
@@ -0,0 +1,51 @@
|
||||
# key-detector
|
||||
|
||||
## Purpose
|
||||
|
||||
`key-detector.ts` ships the `KeyDetector` Analysis Op (plugin slot 3) which estimates the musical key of an `AudioDocument` by accumulating a 12-bin chromagram across the analysed range and correlating it against Krumhansl-Schmuckler or Temperley key profiles. It surfaces both whole-document analysis and a streaming generator whose confidence rises as more audio is consumed.
|
||||
|
||||
The detector consumes only the universal chromagram and key-finding utilities in this package; it ships no DOM or Node-only code paths and runs identically in browser, Bun, and Node.
|
||||
|
||||
## Features
|
||||
|
||||
- Two interchangeable profile sets (`krumhansl-schmuckler`, `temperley`).
|
||||
- `analyze` aggregates a chromagram across the full document.
|
||||
- `analyzeStream` yields refining `KeyResult`s as audio is consumed.
|
||||
- `AbortSignal` support honoured at every block boundary.
|
||||
- Zod validation on every options object.
|
||||
- OTel spans + counters + histograms per run.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { KeyDetector } from "@suaveplan/audio-analysis-pitch";
|
||||
|
||||
const detector = new KeyDetector();
|
||||
const result = await detector.analyze(doc);
|
||||
console.log(result.key, "(confidence", result.confidence, ")");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { KeyDetector } from "@suaveplan/audio-analysis-pitch";
|
||||
|
||||
const detector = new KeyDetector();
|
||||
const opts = { profile: "temperley" as const, frameSize: 8_192 };
|
||||
for await (const r of detector.analyzeStream(doc, opts)) {
|
||||
if (r.confidence > 0.8) {
|
||||
console.log("settled key:", r.key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class KeyDetector { name: "key-detector"; version: string; description: string; analyze(doc, opts?); analyzeStream(doc, opts?) }`.
|
||||
- `interface KeyDetectorOptions { profile?, frameSize?, signal? }`.
|
||||
- `interface KeyResult extends AnalysisResult { op: "key-detector"; key; confidence; chromagram; timestamp; duration }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The detector accumulates chromagrams in a single 12-element `Float32Array` reused across the entire stream — no per-block allocation beyond the chroma vector itself. `KeyResult.chromagram` is a defensive copy via `chroma.slice()` so consumers cannot mutate the internal accumulator. The streaming variant computes `detectKey` on every yield, which is O(12 × 24) and well below the cost of the FFT it runs alongside.
|
||||
@@ -0,0 +1,51 @@
|
||||
# pitch-detector
|
||||
|
||||
## Purpose
|
||||
|
||||
`pitch-detector.ts` exposes the `PitchDetector` class — the Analysis Op plugin (plugin slot 3 in the `@suaveplan/audio-*` family) that estimates the fundamental frequency of an `AudioDocument`. It supports whole-document analysis (`analyze`), streaming refinement (`analyzeStream`), and synchronous single-block analysis (`analyzeBlock`) for real-time visualisation.
|
||||
|
||||
Three estimators are selectable per call — YIN (default), McLeod, and cepstrum — letting callers pick the right trade-off for the material at hand without coupling to a different package.
|
||||
|
||||
## Features
|
||||
|
||||
- Three pitch algorithms behind the `algorithm` option.
|
||||
- Configurable frequency range via `minFreq` / `maxFreq` and frame size.
|
||||
- `AbortSignal` support across all three entry points.
|
||||
- Zod boundary validation on every options object.
|
||||
- OpenTelemetry spans per detector run, counter for total runs, histograms for run and block duration.
|
||||
- Stable error codes (`INVALID_OPTIONS`, `ABORTED`, `UNSUPPORTED_FORMAT`, `INSUFFICIENT_DATA`) all routable through `AudioAnalysisError`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { PitchDetector } from "@suaveplan/audio-analysis-pitch";
|
||||
|
||||
const detector = new PitchDetector();
|
||||
const result = await detector.analyze(doc);
|
||||
console.log(result.pitch, "Hz", "midi:", result.midiNote);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { PitchDetector } from "@suaveplan/audio-analysis-pitch";
|
||||
|
||||
const detector = new PitchDetector();
|
||||
const controller = new AbortController();
|
||||
const opts = { algorithm: "mcleod" as const, minFreq: 50, maxFreq: 1_000, signal: controller.signal };
|
||||
const out: number[] = [];
|
||||
for await (const r of detector.analyzeStream(doc, opts)) {
|
||||
out.push(r.pitch);
|
||||
if (out.length >= 100) controller.abort();
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class PitchDetector { name: "pitch-detector"; version: string; description: string; analyze(doc, opts?); analyzeStream(doc, opts?); analyzeBlock(block, ctx) }`.
|
||||
- `interface PitchDetectorOptions { algorithm?, minFreq?, maxFreq?, frameSize?, hopSize?, signal? }`.
|
||||
- `interface PitchResult extends AnalysisResult { op: "pitch-detector"; pitch; midiNote; confidence; timestamp; duration }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`analyze` aggregates results by keeping the highest-confidence pitch across all frames. `analyzeStream` yields per-block estimates with a running max confidence so the stream is non-decreasing in expectation. Both honour an `AbortSignal` checked at every block boundary. `analyzeBlock` is allocation-light: it skips the Zod schema (the block path is hot) and uses the package defaults.
|
||||
@@ -0,0 +1,64 @@
|
||||
# error-codes
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`error-codes.ts` defines the stable `AUDIO_ANALYSIS_PITCH.*` error code registry, the metadata table describing each code, and the `AudioAnalysisError` class used by every entry point in the package. The codes give callers a stable contract for distinguishing categories of failures (option-validation, abort, unsupported format, insufficient data) without parsing error messages.
|
||||
|
||||
The class extends `SuaveplanError` so it integrates with the project-wide error pipeline (OTel span events, cause chaining, redacted serialisation) while carrying a discriminant `kind` property and a derived `category` field so consumers can switch on the failure shape without inspecting the string code.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen metadata table covering all four codes with description and severity.
|
||||
- `AudioAnalysisError` extends `SuaveplanError<AudioAnalysisPitchErrorCode>` with `kind` and `category` discriminants.
|
||||
- `registerAudioAnalysisPitchErrorCodes()` idempotent registration shim.
|
||||
- Self-registers at import time so the codes are reachable to global error registries.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUDIO_ANALYSIS_PITCH_CODES,
|
||||
AudioAnalysisError,
|
||||
} from "@suaveplan/audio-analysis-pitch";
|
||||
|
||||
throw new AudioAnalysisError(
|
||||
AUDIO_ANALYSIS_PITCH_CODES.INVALID_OPTIONS,
|
||||
"audio.analysis.pitch.invalid-options",
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUDIO_ANALYSIS_PITCH_CODES,
|
||||
AUDIO_ANALYSIS_PITCH_ERROR_METADATA,
|
||||
AudioAnalysisError,
|
||||
} from "@suaveplan/audio-analysis-pitch";
|
||||
|
||||
try {
|
||||
await detector.analyze(doc, opts);
|
||||
} catch (err) {
|
||||
if (err instanceof AudioAnalysisError) {
|
||||
const meta = AUDIO_ANALYSIS_PITCH_ERROR_METADATA[err.code];
|
||||
logger.warn({ code: err.code, severity: meta.severity }, err.message);
|
||||
if (err.category === "abort") return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AUDIO_ANALYSIS_PITCH_CODES` — frozen `{ INVALID_OPTIONS, ABORTED, UNSUPPORTED_FORMAT, INSUFFICIENT_DATA }`.
|
||||
- `AUDIO_ANALYSIS_PITCH_ERROR_METADATA: Record<AudioAnalysisPitchErrorCode, { description, severity }>`.
|
||||
- `class AudioAnalysisError extends SuaveplanError<AudioAnalysisPitchErrorCode>` — `kind: "AudioAnalysisError"`, `category: "options" | "abort" | "data" | "format"`.
|
||||
- `registerAudioAnalysisPitchErrorCodes(): void` — idempotent.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The class adds a `category` field whose computation runs in the constructor body, ensuring the explicit constructor cannot be auto-collapsed by formatters. The metadata table is frozen with `Object.freeze` for shallow immutability. The `_registered` guard ensures `registerAudioAnalysisPitchErrorCodes` may be called multiple times safely from package consumers without side effects.
|
||||
Reference in New Issue
Block a user