Files
skills/docs/media/audio/audio-sprite-player/src/adapter/adapter.md
2026-05-29 08:20:56 +01:00

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.
  • 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

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 — 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.