Files
2026-05-27 23:47:53 +01:00

5.5 KiB
Raw Permalink Blame History

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-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

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 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.