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:
data: Uint8Array— the concatenated, encoded audio bytes for all clips (plus inter-clip silence padding when configured).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 bytesdecode— convert encoded bytes to planarFloat32ArrayPCMencode— convert planar PCM back to encoded bytesstripSilence— remove leading/trailing silence below a dB thresholdnormalise— 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/, nottests/. 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.
stripSilencedelegates to the adapter so that different backends (Web Audio vs FFmpeg) can use their own optimised algorithms. The packer only configures the threshold viaPackOptions. - No
audio-coreruntime dependency. The manifest references clips by name, offset, and duration, not byAudioPageorAudioDocument. Consumers build their own domain objects from the manifest. This prevents a heavy dependency in a build-time tool.