# NullSpritePlaybackAdapter _(no submodules)_ ## Purpose `NullSpritePlaybackAdapter` is the recording adapter that conforms to the `SpritePlaybackAdapter` contract without producing any audio. It is the fixture every test in this package uses, and it is exported so downstream engine adapters can use it as the contract-conformance baseline before plugging in real audio output. Every method on the adapter appends a typed entry to a public read-only `calls` array. Every `play` returns a `SpritePlayHandle` whose `stop`, `setVolume`, and `isPlaying` invocations are also recorded. The timestamps come from an injectable `Clock`, so tests using `FakeClock` (or any deterministic clock) produce identical traces on every run. ## Features - Implements the full `SpritePlaybackAdapter` interface: `init`, `play`, `dispose`. - Records every method call with `{ kind, at, payload }` where `at` is the clock reading and `payload` carries the relevant arguments. - Records every handle lifecycle event (`stop` with `fadeOutMs`, `setVolume` with value). - Honours `AbortSignal`: throws an `aborted` error when invoked with an already-aborted signal. - Pure — no platform globals, no side effects beyond the in-memory `calls` array. ## Usage: Basic Example ```ts import { NullSpritePlaybackAdapter, createSpritePlayer } from "@suaveplan/audio-sprite-player"; const adapter = new NullSpritePlaybackAdapter(); const player = createSpritePlayer({ adapter }); await player.load(sprite); await player.play("hit"); console.log(adapter.calls.map((c) => c.kind)); // → ["init", "play"] ``` ## Usage: Advanced Example ```ts import { createNullSpritePlaybackAdapter, createSpritePlayer, } from "@suaveplan/audio-sprite-player"; import { FakeClock } from "@suaveplan/testing"; const fakeClock = new FakeClock({ startTime: 1_000_000 }); const adapter = createNullSpritePlaybackAdapter({ clock: () => fakeClock.now(), }); const player = createSpritePlayer({ adapter, clock: () => fakeClock.now(), idGenerator: (() => { let i = 0; return () => `id-${++i}`; })(), }); await player.load(sprite); await player.play("hit"); fakeClock.advance(100); await player.play("hit"); const playRecords = adapter.calls.filter((c) => c.kind === "play"); console.log(playRecords.length); // 2 ``` ## API Reference ### `new NullSpritePlaybackAdapter(options?)` `options.clock?: Clock` — defaults to `() => Date.now()`. ### `calls: ReadonlyArray` Append-only log. `NullCall` is `{ kind: "init" | "play" | "dispose" | "stop" | "setVolume"; at: number; payload?: unknown }`. ### `init(sprite, signal?)` Records `{ kind: "init" }`. Throws if `signal.aborted`. ### `play(entry, opts, signal?)` Records `{ kind: "play", payload: { entryName, id, opts } }`. Returns a handle. Throws if `signal.aborted`. ### `dispose()` Records `{ kind: "dispose" }`. ### `createNullSpritePlaybackAdapter(options?)` Functional alias for `new NullSpritePlaybackAdapter(options)`. ## Implementation Notes - The adapter is exported from the package root and from the `./adapter/null/null` subpath so consumers can import it without pulling the rest of the surface. - It is a valid implementation of `SpritePlaybackAdapter` from the type level, so the `adapter contract compiles from type-only imports` smoke test in `null.test.ts` confirms a downstream engine could ship an adapter using nothing but `import type` from this package. - The handle returned by `play` carries the same `id` recorded in the `play` call's payload, so consumers can correlate handle lifecycle calls back to their originating `play` deterministically.