6.0 KiB
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.setcopy 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 bysmoothingTimeConstant ∈ [0, 1].getWaveformData()returns the latest analysis window (length =fftSize).- Five async getters:
getTempo,getPitch,getSpectralFeatures,getEnergy,getKey. All returnshapes are stable record types — seeaudio-analyzer.types.tsfor the contract. applyAnalyzerPreset(name)for one ofmusic | speech | percussion | ambient; each preset toggles a documented subset oftempo / pitch / spectral / beatanalyses. Per-analysisenable<X>Detection(boolean)setters override the preset. (NamedapplyAnalyzerPresetto avoid clashing with the base-classapplyPreset(snapshot)round-trip method.)onAnalysisUpdate(callback)returns an unsubscribe function. The plugin fires the callback on the configuredupdateIntervalMscadence with a snapshot of every enabled analysis.
Usage: Basic Example
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
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 extendsAbstractAudioFxPlugin)new AudioAnalyzerPlugin(options?: AudioAnalyzerPluginOptions)— throwsAUDIO_FX.INVALID_OPTIONSon malformed options.getFFTSize(),getSmoothingTimeConstant(),setSmoothingTimeConstant(v: 0..1),getUpdateInterval(),setUpdateInterval(ms: 10..5000).enable<X>Detection(boolean)/is<X>DetectionEnabled()forTempo,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:
musicenables every analysis (worst CPU, best for full track inspection).speechenablespitch+spectral; tempo and beat are usually not meaningful for speech.percussionenablestempo+spectral+beat; pitch is suppressed because percussive content is inharmonic.ambientenables onlyspectral; 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.