# `audio-analyzer` _(no submodules)_ ## 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 `enableDetection(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)`. - `enableDetection(boolean)` / `isDetectionEnabled()` for `Tempo`, `Pitch`, `Spectral`, `Beat`. - `applyAnalyzerPreset(name: AudioAnalyzerPreset)`, `getCurrentPreset()`. - `getFrequencyData(): Float32Array`, `getWaveformData(): Float32Array`, `getBeatPositions(): ReadonlyArray`. - 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` 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.