# OpenSpec Change: enterprise-audio-codecs - **Change ID**: enterprise-audio-codecs - **Package**: `@suaveplan/audio-codecs` - **Category**: universal - **Target path**: `packages/media/audio/audio-codecs/` - **Status**: PROPOSED - **Date**: 2026-05-14 - **Design**: `./design.md` - **Spec delta**: `./specs/audio-codecs/spec.md` --- ## Summary Deliver `@suaveplan/audio-codecs` on the genesis branch from scratch at enterprise quality. The package provides a universal format decode/encode registry for the genesis monorepo: a single entry point for loading audio from any source (file, URL, buffer, stream) and saving to any supported format. Codecs are registered as plugins and loaded on demand via dynamic `import()`. Format detection uses `@suaveplan/audio-metadata`'s magic-byte detection. Progressive streaming decode fills `AudioDocument.pages` as codec chunks arrive, enabling editing before full decode completes. --- ## 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/audio-core` (`workspace:^`) | `AudioDocument`, `AudioPage`, `AudioStats` for decoded output | | `@suaveplan/audio-metadata` (`workspace:^`) | Magic-byte format detection and audio metadata extraction | | `@suaveplan/streams` (`workspace:^`) | WHATWG `TransformStream` / `ReadableStream` / `WritableStream` primitives | | `@suaveplan/error` (`workspace:^`) | Base class for `AudioCodecError`; `AUDIO_CODEC.*` codes | | `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock` | | `zod` (`catalog:forms`) | Zod schemas for all public option types | | `@opentelemetry/api` (`catalog:observability`) | Spans + counters on decode, encode, and detect ops | Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`). --- ## Public API ### Core types ```ts class AudioCodecError extends SuaveplanError type Source = | string | URL | ArrayBuffer | Uint8Array | ReadableStream type Destination = | string | WritableStream | ((chunk: Uint8Array) => void) ``` ### CodecRegistry ```ts class CodecRegistry { register(codec: AudioCodec): void unregister(name: string): void detect(bytes: Uint8Array): string | undefined decode(source: Source, opts?: DecodeOpts): Promise encode(doc: AudioDocument, format: string, opts?: EncodeOpts): Promise decodeStream(source: ReadableStream, format: string): ReadableStream encodeStream(doc: AudioDocument, format: string): ReadableStream } ``` ### AudioCodec interface ```ts interface AudioCodec { readonly name: string readonly extensions: string[] readonly mimeTypes: string[] canDecode(bytes: Uint8Array): boolean decode(bytes: Uint8Array | ArrayBuffer): Promise encode(pcm: Float32Array[], opts: EncodeOpts): Promise createDecoder?(): StreamingDecoder createEncoder?(): StreamingEncoder } interface DecodeResult { channelData: Float32Array[] sampleRate: number format: string } interface StreamingDecoder { write(chunk: Uint8Array): Promise flush(): Promise } interface StreamingEncoder { write(pcm: Float32Array[]): Promise flush(): Promise } ``` ### Universal entry points ```ts export async function decodeSource( source: Source, opts?: DecodeSourceOpts ): Promise interface DecodeSourceOpts { sampleRate?: number channels?: number format?: string pages?: AudioPage[] notify?(): void ondata?(delta: AudioStats): void storage?: 'memory' | 'persistent' cache?: PageCache budget?: number } export async function encodeTo( doc: AudioDocument, destination: Destination, format: string, opts?: EncodeOpts ): Promise ``` ### Format detection ```ts const HEADER_SIZE = 256 * 1024 async function detectSource(source: Source): Promise<{ format: string | undefined bytes?: Uint8Array path?: string reader?: ReadableStreamDefaultReader }> ``` ### Progressive streaming decode ```ts async function progressiveDecode( source: Source, opts: DecodeSourceOpts, registry: CodecRegistry ): Promise ``` Key feature: returned `AudioDocument` is usable immediately before decode completes; edits can be chained and extend automatically as more pages arrive. ### Built-in codecs | Format | Decode | Encode | Streaming | Notes | |---|---|---|---|---| | WAV | ✅ | ✅ | ✅ | Native, no deps | | MP3 | ✅ | ✅ | ✅ | WASM (`@audio/decode-mp3`) | | FLAC | ✅ | ✅ | ✅ | WASM (`@audio/decode-flac`) | | Ogg Vorbis | ✅ | ❌ | ✅ | WASM | | Opus | ✅ | ✅ | ✅ | WASM | | AAC | ✅ | ❌ | ❌ | WASM | | AIFF | ✅ | ❌ | ❌ | Native | | CAF | ✅ | ❌ | ❌ | Native | | WebM | ✅ | ❌ | ❌ | WASM | | QOA | ✅ | ✅ | ✅ | WASM | WASM modules are loaded on demand via dynamic `import()` and declared as `optionalDependencies`. ### Error codes ```ts type AudioCodecErrorCode = | 'AUDIO_CODECS.UNKNOWN_FORMAT' | 'AUDIO_CODECS.DECODE_FAILED' | 'AUDIO_CODECS.ENCODE_FAILED' | 'AUDIO_CODECS.STREAM_ERROR' | 'AUDIO_CODECS.NOT_SUPPORTED' | 'AUDIO_CODECS.INVALID_SOURCE' | 'AUDIO_CODECS.BUDGET_EXCEEDED' const AUDIO_CODEC_ERROR_METADATA: Readonly> function registerAudioCodecErrorCodes(): void ``` ### Zod schemas ```ts DecodeOptsSchema EncodeOptsSchema DecodeSourceOptsSchema AudioCodecSchema StreamingDecoderSchema StreamingEncoderSchema ``` --- ## New additions for genesis 1. **Zod boundary validation** — every public options object validated at entry. 2. **OTel instrumentation** — spans on `codec.decode`, `codec.encode`, `codec.detect`; counters for `audio_codec_decode_total`, `audio_codec_encode_total`; histograms for `audio_codec_decode_duration_ms`, `audio_codec_encode_duration_ms`. 3. **Memory budget enforcement** — `budget` option in `DecodeSourceOpts` aborts decode if projected memory exceeds limit. 4. **Source resolution** — automatic `fetch()` vs `fs/promises.readFile()` resolution based on source type and runtime. 5. **Page pre-allocation** — `pages` option allows callers to supply a pre-allocated page array for zero-copy progressive decode. 6. **Property-based tests** — decode-then-encode round-trip for lossless formats (WAV, FLAC) yields identical PCM; format detection never returns undefined for known magic bytes; stream decoder yields same total samples as non-streaming decode. 7. **Benchmark suite** — decode 5-minute WAV, encode 5-minute WAV, progressive stream decode 5-minute MP3. --- ## File layout ```text packages/media/audio/audio-codecs/ package.json tsconfig.json vitest.config.ts src/ index.ts error-codes.ts registry/ registry.ts registry.md registry.types.ts registry.test.ts index.ts decode/ decode-source.ts decode-source.md decode-source.types.ts decode-source.test.ts index.ts encode/ encode-to.ts encode-to.md encode-to.types.ts encode-to.test.ts index.ts detect/ detect-format.ts detect-format.md detect-format.test.ts index.ts stream/ stream-decoder.ts stream-decoder.md stream-decoder.test.ts stream-encoder.ts stream-encoder.md stream-encoder.test.ts index.ts codecs/ wav.ts wav.md wav.test.ts mp3.ts mp3.md mp3.test.ts flac.ts flac.md flac.test.ts index.ts __benchmarks__/ decode.bench.ts encode.bench.ts stream-decode.bench.ts tests/ integration.test.ts ``` --- ## Impact **Breaking changes**: none — fresh build, package does not yet exist on genesis. **Prerequisites**: `@suaveplan/audio-core`, `@suaveplan/audio-metadata`, `@suaveplan/streams`, `@suaveplan/error`, `@suaveplan/types` published to Verdaccio. All five already exist on genesis. ### Affected dependents `@suaveplan/audio-codecs` sits directly above `@suaveplan/audio-core` and below every rendering engine and CLI in the audio family. At the time of this change none of the dependents exist on genesis yet — they land in later changes in this same batch — so there is no existing dependent surface to test or migrate. Each dependent is created against this package's spec delta, not retrofitted. - `@suaveplan/audio-cli` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in `enterprise-audio-cli`) - `@suaveplan/audio-engine-server` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in `enterprise-audio-engine-server`) - `@suaveplan/audio-engine-browser` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created in `enterprise-audio-engine-browser`) Once landed, any subsequent change to `@suaveplan/audio-codecs`' public surface MUST run `bunx turbo test --filter=...@suaveplan/audio-codecs` per `openspec/AGENTS.md` §13 step 10 to sweep these dependents.