4.5 KiB
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. ResolvedPlayOptionsis the only option shape the adapter ever sees — the universal layer applies defaults and Zod validation upstream.initandplayaccept an optionalAbortSignal; the contract requires adapters to honour it.- The adapter receives the entire
AudioSprite(blob + manifest) atinittime so it can decode the data once and keep decoded buffers for the lifetime of the player.
Usage: Basic Example
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
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<SpritePlayHandle> {
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
interface SpritePlaybackAdapter {
init(sprite: AudioSprite, signal?: AbortSignal): Promise<void>;
play(entry: AudioSpriteEntry, opts: ResolvedPlayOptions, signal?: AbortSignal): Promise<SpritePlayHandle>;
dispose(): Promise<void>;
}
Contract for ResolvedPlayOptions
volume∈[0, 1]— final gain.pitch∈[0.5, 2]— playback rate.loop— whentrue, the adapter MUST loop the voice and MUST NOT auto-emit completion.fadeInMs/fadeOutMs— millisecond gain ramps;0means 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
AbortSignalduringinitandplayand throw a real error on abortion; the universal player wraps it intoAUDIO_SPRITE_PLAYER.ABORTED. - The
SpritePlayHandlereturned byplayis returned directly to the caller, so itsstop,setVolume, andisPlayingmethods 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.