# OpenSpec Change: enterprise-audio-sprite-packer - **Change ID**: enterprise-audio-sprite-packer - **Package**: `@suaveplan/audio-sprite-packer` - **Category**: universal - **Target path**: `universal/compression/audio-sprite-packer` - **Status**: PROPOSED - **Date**: 2026-04-24 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/audio-sprite-packer.md` --- ## Summary Deliver `@suaveplan/audio-sprite-packer` on the genesis branch from scratch at enterprise quality. The package builds binary audio sprite maps with 80%+ compression versus individual files: pack multiple audio clips (WAV/MP3/OGG) into a single concatenated binary with a compact JSON manifest (offset + duration per clip), strip inter-clip silence, normalise gain, and produce a ready-to-use sprite descriptor compatible with Web Audio API `AudioBufferSourceNode`. Pure TypeScript; audio encode/decode delegated to injectable `AudioAdapter`. --- ## Current-state audit (genesis) The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/error` (`workspace:^`) | Base class for `AudioSpriteError`; `AUDIO_SPRITE.*` codes | | `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock` | | `@suaveplan/mime` (`workspace:^`) | Audio MIME-type detection | | `@suaveplan/crypto-utils` (`workspace:^`) | Content hash for deterministic manifest IDs | | `zod` (`catalog:forms`) | Zod schemas for all public option types | | `@opentelemetry/api` (`catalog:observability`) | Spans + counters | Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`). --- ## Public API ### Core types ```ts class AudioSpriteError extends SuaveplanError type AudioFormat = 'wav' | 'mp3' | 'ogg' | 'flac' | 'unknown' interface AudioClipInput { name: string data: Uint8Array format?: AudioFormat // detected from @suaveplan/mime if absent } interface AudioSpriteEntry { name: string offset: number // byte offset in the packed buffer byteLength: number durationSeconds: number sampleRate: number channels: number format: AudioFormat } interface AudioSpriteManifest { version: 1 format: AudioFormat sampleRate: number channels: number totalDurationSeconds: number entries: AudioSpriteEntry[] contentHash: string // SHA-256 of packed buffer } interface AudioSprite { data: Uint8Array // concatenated packed audio bytes manifest: AudioSpriteManifest } interface PackOptions { outputFormat?: AudioFormat // default: 'ogg' silencePaddingMs?: number // between clips; default: 100 stripSilence?: boolean // default: true; remove leading/trailing silence per clip normalise?: boolean // default: false; gain normalisation sampleRate?: number // default: from first clip } interface AudioAdapter { readonly supportedFormats: readonly AudioFormat[] probe(data: Uint8Array): Promise<{ durationSeconds: number; sampleRate: number; channels: number; format: AudioFormat }> decode(data: Uint8Array): Promise // PCM samples encode(samples: Float32Array, sampleRate: number, channels: number, format: AudioFormat): Promise stripSilence(samples: Float32Array, sampleRate: number, thresholdDb?: number): Float32Array normalise(samples: Float32Array): Float32Array } interface AudioSpritePackerOptions { adapter: AudioAdapter logger?: LoggerLike clock?: Clock } class AudioSpritePacker { constructor(options: AudioSpritePackerOptions) pack(clips: AudioClipInput[], options?: PackOptions, signal?: AbortSignal): Promise unpack(sprite: AudioSprite, clipName: string): Uint8Array } function createAudioSpritePacker(options: AudioSpritePackerOptions): AudioSpritePacker ``` ### Error codes ```ts type AudioSpriteErrorCode = | 'AUDIO_SPRITE_UNSUPPORTED_FORMAT' | 'AUDIO_SPRITE_DECODE_FAILED' | 'AUDIO_SPRITE_ENCODE_FAILED' | 'AUDIO_SPRITE_CLIP_NOT_FOUND' | 'AUDIO_SPRITE_INVALID_OPTIONS' | 'AUDIO_SPRITE_EMPTY_INPUT' const AUDIO_SPRITE_ERROR_METADATA: Readonly> function registerAudioSpriteErrorCodes(): void ``` ### Zod schemas ```ts AudioSpriteEntrySchema AudioSpriteManifestSchema PackOptionsSchema AudioSpritePackerOptionsSchema ``` --- ## New additions for genesis 1. **`AudioAdapter` interface** — all PCM operations delegated; `NullAudioAdapter` ships for tests. 2. **Silence stripping** — optional per-clip silence removal using a configurable dB threshold (default: −60 dBFS). 3. **Gain normalisation** — peak normalise each clip to 0 dBFS before packing (optional). 4. **Content hashing** — SHA-256 of packed buffer via `@suaveplan/crypto-utils` stored in manifest for integrity verification on unpack. 5. **`unpack`** — slice-and-return: uses `manifest.entries` to extract a named clip from the sprite buffer without any re-decode. 6. **`AbortSignal` propagation** — passed through `pack`; throws `AudioSpriteError(AUDIO_SPRITE_ENCODE_FAILED)` on abort. 7. **OTel instrumentation** — span `audio-sprite.pack`, counter `audio_sprite_pack_total`, histogram `audio_sprite_pack_duration_ms` + `audio_sprite_compression_ratio`. 8. **Property-based tests** — unpack after pack returns original bytes (null adapter), manifest entry count matches input clip count, silence padding produces correct total duration. 9. **Benchmark suite** — pack 10 short clips (null adapter), pack 50 clips. --- ## File layout ```text universal/compression/audio-sprite-packer/ package.json tsconfig.json vitest.config.ts 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.ts manifest.md manifest.test.ts __benchmarks__/ audio-sprite-packer.bench.ts tests/ integration.test.ts property.test.ts ``` --- ## Impact **Downstream**: game audio engines, `@suaveplan/audio-engine`, any Web Audio sprite loaders. **Breaking changes**: none — fresh build. **Prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/mime`, `@suaveplan/crypto-utils` published to Verdaccio.