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-player - audio-player plugin for the @suaveplan/audio-fx-plugin-* family.
audio-player 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-player](./src/audio-player/audio-player.md) — `AudioPlayerPlugin` is the universal audio-file player. It decodes a raw `Float32Array` PCM stream from a URL into an i…
- [audio-player](./src/audio-player/audio-player.md) — `AudioPlayerPlugin` is the universal audio-file player. It decodes a raw `Float32Array` PCM stream from a URL into an i…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options, load options, and preset payload of `AudioPlayerPlugin`. These ru…
- [schemas](./src/schemas/schemas.md) — Zod schemas validating the construction-time options, load options, and preset payload of `AudioPlayerPlugin`. These ru…
<!-- end:auto-docs -->
## Composition
```ts
import { PluginChain, DefaultBusRegistry } from "@suaveplan/audio-fx-plugin-base/chain";
import { /* Plugin */ } from "@suaveplan/audio-fx-plugin-audio-player";
const plugin = new /* Plugin */();
const chain = new PluginChain({
buses: new DefaultBusRegistry(),
plugins: [plugin],
});
```
## Status
Replaces the `./audio-player` subpath of the bundled `@suaveplan/audio-plugins-fx`.

View File

@@ -0,0 +1,78 @@
# `audio-player.types`
## Purpose
TypeScript-only type definitions for `AudioPlayerPlugin`'s construction options, load options, transport state snapshot, and preset payload. Co-located with the implementation so the public type surface stays aligned with the Zod validator in `schemas/schemas.ts`. Consumers can import the types for typed form state, transport-bar state stores, or RPC contracts without pulling in the plugin runtime.
## Features
- `AudioPlayerLoadOptions` — accepts an `AbortSignal` to cancel an in-flight `load` call.
- `AudioPlayerPluginOptions` — `initialVolume`, `initialLoop`, `initialPlaybackRate`; all optional so `new AudioPlayerPlugin()` is valid.
- `AudioPlayerState` — frozen transport snapshot: `playing`, `paused`, `currentTimeSec`, `durationSec`, `volume`, `loop`, `playbackRate`.
- `AudioPlayerPresetData` — preset payload carrying only the non-parameter `loop` boolean (numeric parameters travel through the universal parameter snapshot).
- All fields are `readonly` so consumers can return state directly without defensive cloning.
## Usage: Basic Example
```ts
import type {
AudioPlayerPluginOptions,
AudioPlayerState,
} from "@suaveplan/audio-fx-plugin-audio-player";
const opts: AudioPlayerPluginOptions = { initialVolume: 0.7 };
function renderTransport(state: AudioPlayerState) {
progress.set(state.currentTimeSec / state.durationSec);
}
```
## Usage: Advanced Example
```ts
import type {
AudioPlayerLoadOptions,
AudioPlayerState,
} from "@suaveplan/audio-fx-plugin-audio-player";
class PlaylistController {
private ac = new AbortController();
async load(player: AudioPlayerPlugin, url: string): Promise<void> {
this.ac.abort();
this.ac = new AbortController();
const opts: AudioPlayerLoadOptions = { signal: this.ac.signal };
await player.load(url, opts);
}
summarise(state: AudioPlayerState): string {
const t = state.currentTimeSec.toFixed(1);
const d = state.durationSec.toFixed(1);
return `${state.playing ? "▶" : "⏸"} ${t} / ${d}`;
}
}
```
## API Reference
- `AudioPlayerLoadOptions`
- `readonly signal?: AbortSignal`
- `AudioPlayerPluginOptions`
- `readonly initialVolume?: number` (0..1)
- `readonly initialLoop?: boolean`
- `readonly initialPlaybackRate?: number` (0.25..4)
- `AudioPlayerState`
- `readonly playing: boolean`
- `readonly paused: boolean`
- `readonly currentTimeSec: number`
- `readonly durationSec: number`
- `readonly volume: number`
- `readonly loop: boolean`
- `readonly playbackRate: number`
- `AudioPlayerPresetData`
- `readonly loop: boolean`
## Implementation Notes
`AudioPlayerState` deliberately includes both `playing` and `paused` even though `playing === true && paused === true` represents the paused-while-active state. Modelling pause as a separate flag matches the `HTMLAudioElement` surface and lets UI code distinguish "stopped" (`!playing`) from "paused" (`playing && paused`) without inferring from `currentTimeSec`. The downside is two booleans for what could be a three-state enum; the upside is direct compatibility with existing transport-bar UI patterns.
`AudioPlayerPresetData` carries only `loop` because `volume` and `playbackRate` are universal parameters that travel through the parameter snapshot built by `AbstractAudioFxPlugin`. Storing them again in the preset would double-store and risk drift. The buffer itself is never serialised — it lives at a URL the caller owns, so presets reference the URL externally (e.g., in a parent playlist record).

View File

@@ -0,0 +1,91 @@
# `audio-player`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`AudioPlayerPlugin` is the universal audio-file player. It decodes a raw `Float32Array` PCM stream from a URL into an internal buffer and plays it back through the universal `BlockProcessor` path. The plugin is a source (`io.inputs[0].count === 0`, `io.outputs[0].count === 1`) so it slots into chains at position 0 — the chain's IO validator rejects placing it elsewhere.
The plugin extends `AbstractAudioFxPlugin`, runs identically across browser / server / worklet renderers, and exposes the classic media-player surface (`load`, `play`, `pause`, `stop`, `seek`, `setVolume`, `setLoop`, `setPlaybackRate`, `getState`) the way a UI consumer expects. `load` is `AbortSignal`-aware: a pre-aborted signal short-circuits before any fetch.
## Features
- Five-method transport: `play` / `pause` / `stop` / `seek(timeSec)` plus the bool-typed `setLoop`.
- Three automatable parameters: `volume` (0..1), `loop` (enum), `playbackRate` (0.25..4).
- Pitch-preserving playback is **not** included — `setPlaybackRate(2)` reads the buffer at 2× speed, so it doubles both pitch and consumption. For pitch-preserving rate change, chain a `PitchShifterPlugin` downstream.
- `load(url, { signal? })` fetches a raw Float32 PCM buffer (same encoding as `SamplerPlugin.loadSample` / `ReverbPlugin.loadImpulseResponse`). Pre-aborted signal throws `AUDIO_FX.ABORTED`.
- `play()` before any `load()` throws `AUDIO_FX.SAMPLE_NOT_LOADED` — the same error code the sampler uses for the same misuse.
- `setLoop(true)` makes playback wrap back to the buffer start without producing silence in between. Without loop, playback stops cleanly at the end of the buffer and flips `state.playing` to `false`.
- `getState()` returns a frozen snapshot of `{ playing, paused, currentTimeSec, durationSec, volume, loop, playbackRate }` so UI code never has to read internal fields.
## Usage: Basic Example
```ts
import { AudioPlayerPlugin } from "@suaveplan/audio-fx-plugin-audio-player";
const player = new AudioPlayerPlugin();
await player.load("/audio/track-01.raw");
chain.add(player);
player.play();
```
## Usage: Advanced Example
```ts
import { AudioPlayerPlugin } from "@suaveplan/audio-fx-plugin-audio-player";
const player = new AudioPlayerPlugin({
initialVolume: 0.8,
initialLoop: true,
initialPlaybackRate: 1,
});
await player.setup(
{ sampleRate: 48_000, channels: 1, blockSize: 512 },
ctx
);
const ac = new AbortController();
try {
await player.load("/audio/loop.raw", { signal: ac.signal });
} catch (error) {
if (error instanceof AudioFxError && error.code === "AUDIO_FX.ABORTED") {
console.warn("audio load aborted");
return;
}
throw error;
}
// Drive a UI scrubber from the live state.
const ticker = setInterval(() => {
const state = player.getState();
scrubber.setValue(state.currentTimeSec / state.durationSec);
}, 50);
player.play();
```
## API Reference
- `AudioPlayerPlugin` (class extends `AbstractAudioFxPlugin`)
- `new AudioPlayerPlugin(options?: AudioPlayerPluginOptions)` — throws `AUDIO_FX.INVALID_OPTIONS` on malformed options.
- `load(url, { signal? }): Promise<void>` — fetches and registers the playback buffer.
- `play(): void` — throws `AUDIO_FX.SAMPLE_NOT_LOADED` if no buffer is loaded.
- `pause(): void`, `stop(): void`
- `seek(timeSec: number): void` — throws `AUDIO_FX.INVALID_OPTIONS` on negative or non-finite values.
- `setVolume(v: 0..1): void`, `setLoop(loop: boolean): void`, `setPlaybackRate(rate: 0.25..4): void`
- `getState(): AudioPlayerState`
- Inherited accessors for `volume`, `loop`, `playbackRate`.
## Implementation Notes
The buffer is a `Float32Array` allocated by `load`. Subsequent `load` calls replace the buffer and reset the read position to zero. There is no in-flight load tracking — concurrent `load` calls race; the last one to resolve wins. Most consumers wrap `load` calls in a single `AbortController` and abort the previous call before starting a new one, which is what the AbortSignal handling is designed for.
`pause` is intentionally a no-op when `playing === false`. Without that guard, a `pause()` before any `play()` would set `paused = true`, which would then make the next `play()` start in a paused state (because `play` only flips `playing`). This matched the behaviour of the main-branch `HTMLAudioElement`-backed implementation and is the principle of least surprise for media-player consumers.
`setPlaybackRate` validates at the setter rather than relying on the parameter's `min`/`max` clamping. The reason: the parameter declares `automatable: true`, so a parameter-curve automation could in principle write any number; the setter rejects out-of-range *direct* set calls so a UI's invalid input is caught at the boundary. Automation curves operate on a separate ramp and are clamped by the parameter range internally.
Linear-interpolated read between adjacent samples handles fractional playback rates and seeks. The interpolator is the same shape used by `SamplerPlugin` — `s0 * (1 - frac) + s1 * frac` — so pitch-preservation behaviour is consistent across the two plugins. Looping wraps with `position -= (buffer.length - 1)` rather than `position = 0` so any fractional carry survives the wrap.
`getState` is the only place that reads the live parameter values without going through `parameterValue` — that helper requires the chain context to compute scheduled automation values. Since `getState` is meant to be safe to call before `setup`, it falls back to `getParameter` (the static-value accessor) instead.

View File

@@ -0,0 +1,58 @@
# `audio-player/schemas`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas validating the construction-time options, load options, and preset payload of `AudioPlayerPlugin`. These run on the public boundary between user code and the plugin: any malformed object hitting the constructor or `applyPreset` 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
- `AudioPlayerPluginOptionsSchema` — validates `initialVolume ∈ [0, 1]`, `initialLoop: boolean`, `initialPlaybackRate ∈ [0.25, 4]`.
- `AudioPlayerLoadOptionsSchema` — `.strict()` with `signal: AbortSignal | undefined`.
- `AudioPlayerPresetDataSchema` — `.strict()` object carrying only `loop: boolean`.
## Usage: Basic Example
```ts
import { AudioPlayerPluginOptionsSchema } from "@suaveplan/audio-fx-plugin-audio-player";
const parsed = AudioPlayerPluginOptionsSchema.parse({ initialVolume: 0.8 });
```
## Usage: Advanced Example
```ts
import {
AudioPlayerLoadOptionsSchema,
AudioPlayerPluginOptionsSchema,
AudioPlayerPresetDataSchema,
} from "@suaveplan/audio-fx-plugin-audio-player";
function isPlayerPreset(blob: unknown): boolean {
return AudioPlayerPresetDataSchema.safeParse(blob).success;
}
function isLoadOptions(blob: unknown): boolean {
return AudioPlayerLoadOptionsSchema.safeParse(blob).success;
}
const fields = AudioPlayerPluginOptionsSchema.keyof().options;
for (const field of fields) form.addRow(field);
```
## API Reference
- `AudioPlayerPluginOptionsSchema: z.ZodObject<{ initialVolume?, initialLoop?, initialPlaybackRate? }>`
- `AudioPlayerLoadOptionsSchema: z.ZodObject<{ signal? }, "strict">`
- `AudioPlayerPresetDataSchema: z.ZodObject<{ loop }, "strict">`
## Implementation Notes
`AudioPlayerLoadOptionsSchema` uses `z.instanceof(AbortSignal)` for the signal field rather than a duck-typed object check. `instanceof` is the only Zod check that verifies the value is a real `AbortSignal` rather than a structurally-similar object — a fake `{ aborted: false, addEventListener: () => {} }` would pass a duck-typed check but break the actual abort wiring. The constructor-identity check is reliable in every runtime that exposes `AbortSignal` as a global (browser, Node 18+, Bun).
`AudioPlayerPresetDataSchema` is `.strict()` so unknown fields are rejected — matching the sibling-plugin policy. The plugin's universal parameters (`volume`, `playbackRate`) travel through the parameter snapshot built by `AbstractAudioFxPlugin`, so the preset payload encodes only the non-parameter `loop` flag. Adding more fields here would require coordinating with the base class's preset round-trip; the current minimal payload keeps the preset stable across revisions.