Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,148 @@
# 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.