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,91 @@
# `audio-analyzer`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`AudioAnalyzerPlugin` is the universal analysis tap point. It passes input audio through unchanged while accumulating an FFT-sized window of the signal and computing frequency-domain (`getFrequencyData`) and time-domain (`getWaveformData`) views, plus async getters for tempo, pitch, spectral features, energy, and key. Subscribers to `onAnalysisUpdate` receive snapshots of the currently enabled analyses on a configurable cadence.
The plugin is an insert (`io.inputs[0].count === io.outputs[0].count === "preserve"`); it can sit anywhere in a chain without disturbing the downstream signal. The audio path is genuinely bit-exact — output equals input via `Float32Array.set` — so visualisers can drop the plugin into a chain without colouring the sound.
## Features
- Pass-through audio path with `Float32Array.set` copy from input to output per channel.
- Iterative Cooley-Tukey radix-2 FFT (window size configurable: 256 / 512 / 1024 / 2048 / 4096; default 1024). Hann-windowed analysis.
- `getFrequencyData()` returns the magnitude spectrum (length = `fftSize / 2`), smoothed by `smoothingTimeConstant ∈ [0, 1]`.
- `getWaveformData()` returns the latest analysis window (length = `fftSize`).
- Five async getters: `getTempo`, `getPitch`, `getSpectralFeatures`, `getEnergy`, `getKey`. All returnshapes are stable record types — see `audio-analyzer.types.ts` for the contract.
- `applyAnalyzerPreset(name)` for one of `music | speech | percussion | ambient`; each preset toggles a documented subset of `tempo / pitch / spectral / beat` analyses. Per-analysis `enable<X>Detection(boolean)` setters override the preset. (Named `applyAnalyzerPreset` to avoid clashing with the base-class `applyPreset(snapshot)` round-trip method.)
- `onAnalysisUpdate(callback)` returns an unsubscribe function. The plugin fires the callback on the configured `updateIntervalMs` cadence with a snapshot of every enabled analysis.
## Usage: Basic Example
```ts
import { AudioAnalyzerPlugin } from "@suaveplan/audio-fx-plugin-audio-analyzer";
const analyzer = new AudioAnalyzerPlugin();
chain.add(analyzer);
const unsub = analyzer.onAnalysisUpdate((update) => {
if (update.pitch) console.log(`pitch: ${update.pitch.note}`);
});
```
## Usage: Advanced Example
```ts
import { AudioAnalyzerPlugin } from "@suaveplan/audio-fx-plugin-audio-analyzer";
const analyzer = new AudioAnalyzerPlugin({
fftSize: 2048,
smoothingTimeConstant: 0.6,
updateIntervalMs: 50,
initialPreset: "music",
});
// Tighten the CPU budget for a long-form visualiser: keep spectral
// analysis but turn off the heavier beat and tempo detection.
analyzer.enableTempoDetection(false);
analyzer.enableBeatDetection(false);
const unsub = analyzer.onAnalysisUpdate((update) => {
if (update.spectral) {
spectrogram.push(update.spectral.centroid, update.spectral.rolloff);
}
});
// Later, when the visualiser is destroyed:
unsub();
```
## API Reference
- `AudioAnalyzerPlugin` (class extends `AbstractAudioFxPlugin`)
- `new AudioAnalyzerPlugin(options?: AudioAnalyzerPluginOptions)` — throws `AUDIO_FX.INVALID_OPTIONS` on malformed options.
- `getFFTSize()`, `getSmoothingTimeConstant()`, `setSmoothingTimeConstant(v: 0..1)`, `getUpdateInterval()`, `setUpdateInterval(ms: 10..5000)`.
- `enable<X>Detection(boolean)` / `is<X>DetectionEnabled()` for `Tempo`, `Pitch`, `Spectral`, `Beat`.
- `applyAnalyzerPreset(name: AudioAnalyzerPreset)`, `getCurrentPreset()`.
- `getFrequencyData(): Float32Array`, `getWaveformData(): Float32Array`, `getBeatPositions(): ReadonlyArray<BeatPosition>`.
- Async getters: `getTempo`, `getPitch`, `getSpectralFeatures`, `getEnergy`, `getKey`.
- `onAnalysisUpdate(cb: AnalysisCallback): () => void`.
## Implementation Notes
The FFT is an iterative radix-2 Cooley-Tukey implementation operating on two `Float32Array` buffers (real + imaginary). We chose iterative over recursive because the universal guarantee is no per-block allocation; an iterative loop allocates the two buffers once per analysis frame, not per recursive call. The bit-reverse permutation is also iterative. For a 1024-sample FFT this is around 20 K floating-point operations per analysis frame — affordable on every consumer device.
The magnitude spectrum is smoothed exponentially: `mag[i] = α · prev[i] + (1 − α) · |Z[i]|` where `α = smoothingTimeConstant`. This is the same shape used by the Web Audio `AnalyserNode`, so a visualiser written against either renders identically.
The presets in `PRESET_TOGGLES` reflect the standard mappings:
- `music` enables every analysis (worst CPU, best for full track inspection).
- `speech` enables `pitch` + `spectral`; tempo and beat are usually not meaningful for speech.
- `percussion` enables `tempo` + `spectral` + `beat`; pitch is suppressed because percussive content is inharmonic.
- `ambient` enables only `spectral`; ambient material has no clear tempo, pitch, or beat to track.
`onAnalysisUpdate` fires from inside `processBlock` when the accumulated sample count crosses the `updateIntervalMs` threshold. The callback receives a snapshot built from the currently-enabled analyses' async getters. Subscribers MUST NOT call long-running synchronous work from the callback because it blocks the audio thread; for heavy UI updates, post a message to the main thread and process there.
Beat detection is a minimal envelope-threshold scheme: when `lastEnergy.rms > 0.2` and `beat` is enabled, the plugin records the block's `blockOffsetSamples / sampleRate` as a beat position. This is sufficient for the spec's "records timestamps when enabled" contract but is not a music-grade onset detector — a future revision can plug in a proper spectral-flux onset analyzer behind the same surface.
The async getter signatures are kept as `Promise<X>` even though the current implementation is synchronous because the next revision is expected to forward heavy analyses (tempo, key) to a worker. Keeping the promise shape now means a future revision can switch implementations without breaking callers.