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