# 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, 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.