# SpritePlaybackAdapter _(no submodules)_ ## Purpose `SpritePlaybackAdapter` is the engine-tier seam that the universal `@suaveplan/audio-sprite-player` reaches through to make real audio. The interface is intentionally tiny — three methods — so engine packages such as `@suaveplan/audio-engine-browser` (Web Audio API) and `@suaveplan/audio-engine-server` (native bindings) can implement it without inheriting any orchestration concerns. Adapters are responsible for the work that requires platform globals: decoding the sprite blob, allocating mixer nodes, ramping gain for fade-in/out, scheduling pitch (playback rate), and disconnecting the voice after `stop`. They are NOT responsible for manifest validation, entry lookup, voice management, scheduling between sequence entries, or telemetry — those belong to the universal layer. ## Features - Tiny three-method contract: `init`, `play`, `dispose`. - `ResolvedPlayOptions` is the only option shape the adapter ever sees — the universal layer applies defaults and Zod validation upstream. - `init` and `play` accept an optional `AbortSignal`; the contract requires adapters to honour it. - The adapter receives the entire `AudioSprite` (blob + manifest) at `init` time so it can decode the data once and keep decoded buffers for the lifetime of the player. ## Usage: Basic Example ```ts import type { SpritePlaybackAdapter } from "@suaveplan/audio-sprite-player"; class MyAdapter implements SpritePlaybackAdapter { async init() {} async play(entry, opts) { return { id: crypto.randomUUID(), entryName: entry.name, startedAt: Date.now(), async stop() {}, setVolume() {}, isPlaying: () => false, }; } async dispose() {} } ``` ## Usage: Advanced Example ```ts import type { AudioSprite, ResolvedPlayOptions, SpritePlaybackAdapter, SpritePlayHandle, } from "@suaveplan/audio-sprite-player"; class BrowserAdapter implements SpritePlaybackAdapter { private buffer: AudioBuffer | null = null; constructor(private ctx: AudioContext) {} async init(sprite: AudioSprite, signal?: AbortSignal) { if (signal?.aborted) throw new Error("aborted"); this.buffer = await this.ctx.decodeAudioData(sprite.data.slice().buffer); } async play( entry: { name: string; offset: number; byteLength: number; durationSeconds: number }, opts: ResolvedPlayOptions, ): Promise { const source = this.ctx.createBufferSource(); source.buffer = this.buffer; source.playbackRate.value = opts.pitch; source.loop = opts.loop; const gain = this.ctx.createGain(); gain.gain.setValueAtTime(0, this.ctx.currentTime); gain.gain.linearRampToValueAtTime( opts.volume, this.ctx.currentTime + opts.fadeInMs / 1000, ); source.connect(gain).connect(this.ctx.destination); source.start(opts.when / 1000, entry.offset / entry.durationSeconds); // ... build and return SpritePlayHandle throw new Error("snippet only"); } async dispose() { this.buffer = null; } } ``` ## API Reference ### `interface SpritePlaybackAdapter` ```ts interface SpritePlaybackAdapter { init(sprite: AudioSprite, signal?: AbortSignal): Promise; play(entry: AudioSpriteEntry, opts: ResolvedPlayOptions, signal?: AbortSignal): Promise; dispose(): Promise; } ``` ### Contract for `ResolvedPlayOptions` - `volume` ∈ `[0, 1]` — final gain. - `pitch` ∈ `[0.5, 2]` — playback rate. - `loop` — when `true`, the adapter MUST loop the voice and MUST NOT auto-emit completion. - `fadeInMs` / `fadeOutMs` — millisecond gain ramps; `0` means an instant transition. - `startOffsetMs` — offset INTO the entry, not the sprite. - `durationMs` — voice duration the player expects; adapters MAY honour or ignore depending on capability. - `when` — absolute schedule time in the player's clock domain; adapters convert to their own time base. ## Implementation Notes - The adapter SHOULD honour `AbortSignal` during `init` and `play` and throw a real error on abortion; the universal player wraps it into `AUDIO_SPRITE_PLAYER.ABORTED`. - The `SpritePlayHandle` returned by `play` is returned directly to the caller, so its `stop`, `setVolume`, and `isPlaying` methods must be safe to call from any context. - Engine packages MUST NOT re-implement manifest validation, voice management, or sequence scheduling — those live in `@suaveplan/audio-sprite-player`. Adding them in an adapter is a contract violation.