first commit
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.
|
||||
57
docs/media/audio/audio-analysis-pitch/src/algorithms/yin.md
Normal file
57
docs/media/audio/audio-analysis-pitch/src/algorithms/yin.md
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user