Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
# SpritePlayer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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
```ts
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.