first commit
This commit is contained in:
31
docs/media/audio-fx/audio-fx-plugin-audio-analyzer/README.md
Normal file
31
docs/media/audio-fx/audio-fx-plugin-audio-analyzer/README.md
Normal 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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user