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,31 @@
# @suaveplan/audio-fx-plugin-audio-analyzer - audio-analyzer plugin for the @suaveplan/audio-fx-plugin-* family.
audio-analyzer plugin for the @suaveplan/audio-fx-plugin-* family.
Member of the `@suaveplan/audio-fx-plugin-*` family. Universal (browser / server / offline / worklet identical). Depends on `@suaveplan/audio-fx-plugin-base` for the `AudioFxPlugin` contract; declares no other `@suaveplan/audio-fx-plugin-*` deps.
<!-- begin:auto-docs -->
## Documentation tree
- [audio-analyzer](./src/audio-analyzer/audio-analyzer.md) — `AudioAnalyzerPlugin` is the universal analysis tap point. It passes input audio through unchanged while accumulating a…
- [audio-analyzer](./src/audio-analyzer/audio-analyzer.md) — `AudioAnalyzerPlugin` is the universal analysis tap point. It passes input audio through unchanged while accumulating a…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options, preset enum, and preset payload of `AudioAnalyzerPlugin`. These r…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options, preset enum, and preset payload of `AudioAnalyzerPlugin`. These r…
<!-- end:auto-docs -->
## Composition
```ts
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
import { /* Plugin */ } from "@suaveplan/audio-fx-plugin-audio-analyzer";
const plugin = new /* Plugin */();
const chain = new PluginChain({
buses: new DefaultBusRegistry(),
plugins: [plugin],
});
```
## Status
Replaces the `./audio-analyzer` subpath of the bundled `@suaveplan/audio-plugins-fx`.

View File

@@ -0,0 +1,84 @@
# `audio-analyzer.types`
## Purpose
TypeScript-only type definitions for `AudioAnalyzerPlugin`'s preset enum, construction options, async-result records, subscription payload, and preset payload. Co-located with the implementation so the public type surface stays aligned with the Zod validator in `schemas/schemas.ts`.
The async-result interfaces (`TempoInfo`, `PitchInfo`, `SpectralFeatures`, `EnergyInfo`, `KeyInfo`, `BeatPosition`) are deliberately minimal and stable — adding new fields is a non-breaking change, removing or renaming them is breaking.
## Features
- `AudioAnalyzerPreset` — textual enum `"music" | "speech" | "percussion" | "ambient"` used in options and presets.
- `AudioAnalyzerPluginOptions` — `fftSize`, `smoothingTimeConstant`, `updateIntervalMs`, `initialPreset`.
- Six result types: `TempoInfo`, `PitchInfo`, `SpectralFeatures`, `EnergyInfo`, `KeyInfo`, `BeatPosition`.
- `AnalysisUpdate` — subscription payload with each result type as an optional field; only enabled analyses appear.
- `AnalysisCallback = (update: AnalysisUpdate) => void`.
- `AudioAnalyzerPresetData` — persisted preset record carrying the preset name + the four toggle booleans.
## Usage: Basic Example
```ts
import type {
AnalysisCallback,
AudioAnalyzerPluginOptions,
} from "@suaveplan/audio-fx-plugin-audio-analyzer";
const opts: AudioAnalyzerPluginOptions = { initialPreset: "speech" };
const cb: AnalysisCallback = (u) => {
if (u.pitch) ui.setPitch(u.pitch.frequency);
};
```
## Usage: Advanced Example
```ts
import type {
AnalysisUpdate,
AudioAnalyzerPluginOptions,
SpectralFeatures,
} from "@suaveplan/audio-fx-plugin-audio-analyzer";
interface SpectralRingBuffer {
push(features: SpectralFeatures): void;
read(): ReadonlyArray<SpectralFeatures>;
}
function makeUpdateHandler(buf: SpectralRingBuffer) {
return (update: AnalysisUpdate) => {
if (update.spectral) buf.push(update.spectral);
};
}
const opts: AudioAnalyzerPluginOptions = {
fftSize: 4096,
smoothingTimeConstant: 0.7,
updateIntervalMs: 33,
initialPreset: "music",
};
```
## API Reference
- `AudioAnalyzerPreset = "music" | "speech" | "percussion" | "ambient"`
- `AudioAnalyzerPluginOptions`
- `readonly fftSize?: 256 | 512 | 1024 | 2048 | 4096`
- `readonly smoothingTimeConstant?: number` (0..1)
- `readonly updateIntervalMs?: number` (10..5000)
- `readonly initialPreset?: AudioAnalyzerPreset`
- `TempoInfo` — `{ bpm, confidence }`
- `PitchInfo` — `{ frequency, note, confidence }`
- `SpectralFeatures` — `{ centroid, rolloff, flux, flatness }`
- `EnergyInfo` — `{ rms, peak }`
- `KeyInfo` — `{ key, mode: "major" | "minor", confidence }`
- `BeatPosition` — `{ timeSec, strength }`
- `AnalysisUpdate` — `{ tempo?, pitch?, spectral?, energy?, key? }`
- `AnalysisCallback = (update: AnalysisUpdate) => void`
- `AudioAnalyzerPresetData` — `{ preset, tempo, pitch, spectral, beat }`
## Implementation Notes
`AnalysisUpdate` has every result-type field optional because only the enabled analyses (per the preset + per-analysis toggles) appear in a single update. Callbacks must check `if (update.pitch)` rather than assuming the field is present. The `energy` field is always set because energy tracking has no toggle — it is cheap enough to always compute.
`KeyInfo.mode` is intentionally a literal union (`"major" | "minor"`) rather than a freeform string so callers can drive switch-case rendering without a default branch. The current implementation always returns `"major"` — a future revision adding minor-key detection extends the implementation without changing the type contract.
`AudioAnalyzerPresetData` stores both the preset name *and* the four toggle booleans. This lets the user start from a preset and override specific analyses (e.g., `"music"` + manually disabling beat) and round-trip the override state. The preset name carries the *intent*; the toggles carry the *effective state*. On restore, `applyPresetData` reads both: the preset name is informational; the toggles are authoritative.

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.

View File

@@ -0,0 +1,58 @@
# `audio-analyzer/schemas`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas validating the construction-time options, preset enum, and preset payload of `AudioAnalyzerPlugin`. These run on the public boundary so any malformed object is rejected with `AUDIO_FX.INVALID_OPTIONS` / `AUDIO_FX.INVALID_PRESET` before it can corrupt runtime state.
Separating schemas from the plugin module lets `index.ts` re-export them so callers can introspect the option surface (for forms, RPC validation, or storybook controls) without importing the audio implementation.
## Features
- `AudioAnalyzerPresetSchema` — `z.enum(["music", "speech", "percussion", "ambient"])` reused in options + preset payload.
- `AudioAnalyzerPluginOptionsSchema` — validates `fftSize ∈ {256, 512, 1024, 2048, 4096}`, `smoothingTimeConstant ∈ [0, 1]`, `updateIntervalMs ∈ [10, 5000]`, `initialPreset` enum.
- `AudioAnalyzerPresetDataSchema` — `.strict()` object carrying the preset name + four toggle booleans.
## Usage: Basic Example
```ts
import { AudioAnalyzerPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-audio-analyzer";
const parsed = AudioAnalyzerPluginOptionsSchema.parse({ fftSize: 2048 });
```
## Usage: Advanced Example
```ts
import {
AudioAnalyzerPresetSchema,
AudioAnalyzerPresetDataSchema,
} from "@suaveplan/audio-fx-plugin-audio-analyzer";
const presets = AudioAnalyzerPresetSchema.options;
for (const preset of presets) ui.addOption(preset);
function safeApply(plugin: AudioAnalyzerPlugin, payload: unknown) {
const parsed = AudioAnalyzerPresetDataSchema.safeParse(payload);
if (!parsed.success) return false;
plugin.applyPreset(parsed.data.preset);
return true;
}
```
## API Reference
- `AudioAnalyzerPresetSchema: z.ZodEnum<["music", "speech", "percussion", "ambient"]>`
- `AudioAnalyzerPluginOptionsSchema: z.ZodObject<{ fftSize?, smoothingTimeConstant?, updateIntervalMs?, initialPreset? }>`
- `AudioAnalyzerPresetDataSchema: z.ZodObject<{ preset, tempo, pitch, spectral, beat }, "strict">`
## Implementation Notes
`fftSize` is constrained to a small set of powers of two via `z.refine` rather than `z.enum` because Zod's enum requires string keys. The five legal values (256 / 512 / 1024 / 2048 / 4096) span the practical range — smaller windows give finer time resolution at the expense of frequency resolution; larger windows do the opposite. The error message names the four legal values so a UI driving this schema can produce a useful dropdown.
`AudioAnalyzerPresetDataSchema` uses `.strict()` so unknown fields are rejected. The preset payload encodes both the preset name and the four toggles because the toggles can override the preset (e.g., `"music"` with `beat: false`); restoring the preset name alone would lose the override. The combined "preset + toggles" payload makes restore deterministic.
The `smoothingTimeConstant` range matches the Web Audio `AnalyserNode.smoothingTimeConstant` contract so a consumer porting code from the standardized AudioContext gets identical perceptual behaviour. The same applies to `fftSize` which uses the same set of legal values.