Files
2026-05-27 23:29:53 +01:00

5.6 KiB

Design: enterprise-audio-sprite-packer

Change ID: enterprise-audio-sprite-packer Package: @suaveplan/audio-sprite-packer

@suaveplan/audio-sprite-packer packs multiple discrete audio clips into a single concatenated binary with a compact JSON manifest. It is the universal-tier counterpart to browser-specific sprite loaders: it performs the packing side of the pipeline (server-side or build-time), producing a deterministic, hash-verified sprite that browser or server consumers can later unpack by name.

The upstream package (../suaveplan/packages/universal/audio-sprite-packer) compressed only metadata maps ({name: [offset, duration]}) using BitPacker. The genesis migration retains that capability as the manifest layer, but adds the audio layer: actual clip concatenation, format normalisation, silence stripping, and gain normalisation via a pluggable AudioAdapter. The result is a production-grade sprite builder rather than a map compressor.


1. Relationship to the ecosystem

audio-sprite-packer sits above audio-core (it produces AudioDocument-compatible manifests) and below audio-engine-browser / audio-engine-server (which consume sprites at runtime). It is a leaf in the dependency graph: no other genesis package depends on it at build time; it is invoked at build time or on-demand.

audio-sprite-packer does NOT import audio-core directly — the manifest shape is a stable JSON schema, not a runtime class dependency. This keeps the package universal-tier with zero heavy audio dependencies.


2. Architecture

2.1. Two-layer output

Every pack() call produces:

  1. data: Uint8Array — the concatenated, encoded audio bytes for all clips (plus inter-clip silence padding when configured).
  2. manifest: AudioSpriteManifest — JSON-serialisable metadata describing each clip's byte offset, duration, sample rate, channel count, and format.

The separation lets consumers stream data from a CDN while downloading the small manifest separately, or embed both in a single envelope.

2.2. AudioAdapter injection

All PCM operations are delegated to an injectable AudioAdapter:

  • probe — extract duration, sample rate, channels, format from encoded bytes
  • decode — convert encoded bytes to planar Float32Array PCM
  • encode — convert planar PCM back to encoded bytes
  • stripSilence — remove leading/trailing silence below a dB threshold
  • normalise — peak-normalise to 0 dBFS

This makes audio-sprite-packer universal: a browser build passes a WebAudioAdapter (wrapping AudioContext.decodeAudioData), a server build passes an FFmpegAdapter (wrapping fluent-ffmpeg or @ffmpeg/ffmpeg), and unit tests pass NullAudioAdapter which returns deterministic fixture data without any external dependency.

2.3. Pack pipeline

clips[] → detect format → probe → decode → stripSilence? → normalise?
  → append silence padding → encode concatenated → build manifest

The pipeline is serial (not streaming) because the total output size is unknown until all clips are processed. For very large clip sets, the caller can batch externally.

2.4. Manifest integrity

The manifest carries a SHA-256 contentHash of the packed data buffer, computed via @suaveplan/crypto-utils. unpack(sprite, clipName) validates the hash lazily: it slices the requested clip by offset/byteLength and returns the bytes without re-decoding. If the caller requests verifyIntegrity: true, unpack re-computes the hash and throws AUDIO_SPRITE_HASH_MISMATCH on mismatch.


3. File layout

packages/audio/audio-sprite-packer/
  package.json
  tsconfig.json
  vitest.config.ts
  README.md
  src/
    index.ts
    error-codes.ts
    constants/
      index.ts
    types/
      common.ts
      common.md
    core/
      audio-sprite-packer/
        audio-sprite-packer.ts
        audio-sprite-packer.md
        audio-sprite-packer.types.ts
        audio-sprite-packer.test.ts
        index.ts
    adapters/
      null-adapter/
        null-adapter.ts
        null-adapter.md
        null-adapter.test.ts
        index.ts
    utils/
      manifest/
        manifest.ts
        manifest.md
        manifest.test.ts
        index.ts
    __benchmarks__/
      audio-sprite-packer.bench.ts
  tests/
    integration.test.ts
    property.test.ts

4. Decisions and trade-offs

  • Serial pipeline, not streaming. The output is a single concatenated buffer. Streaming would require a two-pass design (write sizes, then rewrite headers) which adds complexity for a build-time tool. Callers with >1000 clips should batch externally.
  • NullAudioAdapter ships in src/, not tests/. It is a legitimate testing fixture that downstream packages can import via @suaveplan/audio-sprite-packer/adapters/null-adapter.
  • Manifest is JSON, not BitPacker. The upstream used BitPacker for metadata compression. The genesis manifest is plain JSON so it is human-readable, diff-friendly, and trivially parsed in any runtime. The 80%+ compression claim comes from the binary audio layer (concatenation + shared format), not from metadata bit-packing.
  • Silence threshold is on the adapter. stripSilence delegates to the adapter so that different backends (Web Audio vs FFmpeg) can use their own optimised algorithms. The packer only configures the threshold via PackOptions.
  • No audio-core runtime dependency. The manifest references clips by name, offset, and duration, not by AudioPage or AudioDocument. Consumers build their own domain objects from the manifest. This prevents a heavy dependency in a build-time tool.