Initial Commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user