first commit
This commit is contained in:
112
docs/media/audio/audio-sprite-player/src/adapter/adapter.md
Normal file
112
docs/media/audio/audio-sprite-player/src/adapter/adapter.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# SpritePlaybackAdapter
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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<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`
|
||||
|
||||
```ts
|
||||
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` — 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.
|
||||
Reference in New Issue
Block a user