Files
skills/docs/media/audio-fx/audio-fx-plugin-audio-analyzer/src/audio-analyzer/audio-analyzer.md
2026-05-29 08:20:56 +01:00

6.0 KiB
Raw Blame History

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 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

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