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

2.2 KiB

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

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

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.