9.1 KiB
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
class AudioCodecError extends SuaveplanError
type Source =
| string
| URL
| ArrayBuffer
| Uint8Array
| ReadableStream<Uint8Array>
type Destination =
| string
| WritableStream<Uint8Array>
| ((chunk: Uint8Array) => void)
CodecRegistry
class CodecRegistry {
register(codec: AudioCodec): void
unregister(name: string): void
detect(bytes: Uint8Array): string | undefined
decode(source: Source, opts?: DecodeOpts): Promise<AudioDocument>
encode(doc: AudioDocument, format: string, opts?: EncodeOpts): Promise<Uint8Array>
decodeStream(source: ReadableStream<Uint8Array>, format: string): ReadableStream<AudioPage>
encodeStream(doc: AudioDocument, format: string): ReadableStream<Uint8Array>
}
AudioCodec interface
interface AudioCodec {
readonly name: string
readonly extensions: string[]
readonly mimeTypes: string[]
canDecode(bytes: Uint8Array): boolean
decode(bytes: Uint8Array | ArrayBuffer): Promise<DecodeResult>
encode(pcm: Float32Array[], opts: EncodeOpts): Promise<Uint8Array>
createDecoder?(): StreamingDecoder
createEncoder?(): StreamingEncoder
}
interface DecodeResult {
channelData: Float32Array[]
sampleRate: number
format: string
}
interface StreamingDecoder {
write(chunk: Uint8Array): Promise<DecodeResult | null>
flush(): Promise<DecodeResult | null>
}
interface StreamingEncoder {
write(pcm: Float32Array[]): Promise<Uint8Array | null>
flush(): Promise<Uint8Array | null>
}
Universal entry points
export async function decodeSource(
source: Source,
opts?: DecodeSourceOpts
): Promise<AudioDocument>
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<void>
Format detection
const HEADER_SIZE = 256 * 1024
async function detectSource(source: Source): Promise<{
format: string | undefined
bytes?: Uint8Array
path?: string
reader?: ReadableStreamDefaultReader<Uint8Array>
}>
Progressive streaming decode
async function progressiveDecode(
source: Source,
opts: DecodeSourceOpts,
registry: CodecRegistry
): Promise<AudioDocument>
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
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<Record<AudioCodecErrorCode, CodeMetadata>>
function registerAudioCodecErrorCodes(): void
Zod schemas
DecodeOptsSchema
EncodeOptsSchema
DecodeSourceOptsSchema
AudioCodecSchema
StreamingDecoderSchema
StreamingEncoderSchema
New additions for genesis
- Zod boundary validation — every public options object validated at entry.
- OTel instrumentation — spans on
codec.decode,codec.encode,codec.detect; counters foraudio_codec_decode_total,audio_codec_encode_total; histograms foraudio_codec_decode_duration_ms,audio_codec_encode_duration_ms. - Memory budget enforcement —
budgetoption inDecodeSourceOptsaborts decode if projected memory exceeds limit. - Source resolution — automatic
fetch()vsfs/promises.readFile()resolution based on source type and runtime. - Page pre-allocation —
pagesoption allows callers to supply a pre-allocated page array for zero-copy progressive decode. - 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.
- Benchmark suite — decode 5-minute WAV, encode 5-minute WAV, progressive stream decode 5-minute MP3.
File layout
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 inenterprise-audio-cli)@suaveplan/audio-engine-server— [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created inenterprise-audio-engine-server)@suaveplan/audio-engine-browser— [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: does not exist yet; created inenterprise-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.