# 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 ```text 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.