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

4.4 KiB

SpritePlayer

(no submodules)

Purpose

SpritePlayer is the public orchestrator for @suaveplan/audio-sprite-player. It accepts a validated AudioSprite (a binary blob plus a JSON manifest produced by @suaveplan/audio-sprite-packer) and exposes fire-and-forget playback primitives — play, playSequence, playRandom, stopAll — on top of an injectable SpritePlaybackAdapter that engine-tier packages implement.

The class deliberately owns no audio output of its own. Every actual decode, gain ramp, pitch scheduling, and mixer routing concern is reached through the adapter. The orchestrator's job is manifest validation, entry lookup, option defaulting, voice management with stealing, sequence and random scheduling, abort propagation, telemetry, and a typed event surface.

Features

  • Strict Zod validation of SpritePlayerOptions, the loaded manifest, and every PlayOptions / SequenceOptions bag at the public boundary.
  • Voice manager with oldest-first stealing capped at maxConcurrentVoices (default 32, hard ceiling 256).
  • Sequence playback with durationMs + gapMs scheduling and rollback of started voices on failure.
  • Uniform random playback with optional filter predicate and Rng injection for determinism in tests.
  • Pre-aborted and mid-flight AbortSignal paths across load, play, playSequence, and playRandom.
  • Typed event bus (play, stop, complete, voice-stolen, error) and OTel span/counter/gauge/histogram on every invocation.

Usage: Basic Example

import {
  createSpritePlayer,
  NullSpritePlaybackAdapter,
} from "@suaveplan/audio-sprite-player";

const adapter = new NullSpritePlaybackAdapter();
const player = createSpritePlayer({ adapter });
await player.load(sprite);
const handle = await player.play("hit");
await handle.stop();
await player.dispose();

Usage: Advanced Example

import { createSpritePlayer } from "@suaveplan/audio-sprite-player";
import { BrowserSpritePlaybackAdapter } from "@suaveplan/audio-engine-browser";

const adapter = new BrowserSpritePlaybackAdapter({ audioContext });
const player = createSpritePlayer({
  adapter,
  maxConcurrentVoices: 16,
  clock: () => audioContext.currentTime * 1000,
  rng: seededRng(42),
});

player.events.on("voice-stolen", (e) => {
  metrics.spritePlayerVoiceStolen.add(1, { entry: e.entryName });
});

await player.load(sprite);
const ctrl = new AbortController();
await player.playSequence(["step", "step", "land"], {
  gapMs: 50,
  fadeInMs: 10,
  fadeOutMs: 20,
  signal: ctrl.signal,
});

API Reference

class SpritePlayer

  • constructor(options: SpritePlayerOptions) — validates options; throws INVALID_OPTIONS on failure.
  • load(sprite, signal?) — validates manifest, initialises adapter. Throws INVALID_MANIFEST / ADAPTER_INIT_FAILED / ABORTED.
  • play(entryName, opts?) — looks up entry, allocates voice (with stealing), delegates to adapter. Throws NOT_LOADED / UNKNOWN_ENTRY / INVALID_OPTIONS / PLAYBACK_FAILED / VOICE_LIMIT_EXCEEDED / ABORTED.
  • playSequence(entryNames, opts?) — see spec §playSequence.
  • playRandom(filterFn?, opts?) — uniform random pick from filtered entries.
  • stopAll(fadeOutMs?) — Promise.all over every active voice.
  • list() / has(name) — read accessors for loaded entries.
  • dispose() — idempotent teardown.
  • readonly events: SpritePlayerEvents — typed event subscriber.
  • readonly telemetry: SpritePlayerTelemetry — OTel instruments.

createSpritePlayer(options)

Functional alias for new SpritePlayer(options).

Implementation Notes

  • The player never imports a runtime symbol from @suaveplan/audio-sprite-packer; the producer is consumed as a types-only dependency.
  • Auto-completion is scheduled via setTimeout(resolved.durationMs) so the universal layer can fire complete events without knowing the adapter's actual end-of-voice signal — adapter implementations are free to stop the voice earlier and the player will skip its own completion emission via the isPlaying() guard.
  • playRandom uses rng.nextUint32() % entries.length so tests can inject a deterministic Rng.
  • The wrapper handle returned by play is the same handle registered in the VoiceManager, so a stolen handle's stop() is the same call path as a user-driven stop() — the only difference is which event the player emits.