Files
suaveplan/openspec/changes/archive/2026-05-15-enterprise-audio-sprite-packer/proposal.md
2026-05-27 23:29:53 +01:00

198 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<Float32Array> // PCM samples
encode(samples: Float32Array, sampleRate: number, channels: number, format: AudioFormat): Promise<Uint8Array>
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<AudioSprite>
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<Record<AudioSpriteErrorCode, CodeMetadata>>
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.