5.5 KiB
audio-player
(no submodules)
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-typedsetLoop. - 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 aPitchShifterPlugindownstream. load(url, { signal? })fetches a raw Float32 PCM buffer (same encoding asSamplerPlugin.loadSample/ReverbPlugin.loadImpulseResponse). Pre-aborted signal throwsAUDIO_FX.ABORTED.play()before anyload()throwsAUDIO_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 flipsstate.playingtofalse.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
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
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 extendsAbstractAudioFxPlugin)new AudioPlayerPlugin(options?: AudioPlayerPluginOptions)— throwsAUDIO_FX.INVALID_OPTIONSon malformed options.load(url, { signal? }): Promise<void>— fetches and registers the playback buffer.play(): void— throwsAUDIO_FX.SAMPLE_NOT_LOADEDif no buffer is loaded.pause(): void,stop(): voidseek(timeSec: number): void— throwsAUDIO_FX.INVALID_OPTIONSon negative or non-finite values.setVolume(v: 0..1): void,setLoop(loop: boolean): void,setPlaybackRate(rate: 0.25..4): voidgetState(): 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.