Files
skills/docs/media/audio/audio-codecs/src/registry/registry.md
2026-05-29 08:20:56 +01:00

142 lines
6.4 KiB
Markdown

# `registry/registry`
In-memory plugin registry that owns audio-codec lookup and dispatch for the
whole package.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`CodecRegistry` is the single entry point through which `decodeSource`,
`encodeTo`, `progressiveDecode`, and the streaming transform factories
resolve a codec from raw bytes, a format hint, an extension, or a MIME type
and then dispatch decode/encode operations to it. Without a single registry,
every public entry point would re-implement the lookup + dispatch + error
mapping triad — duplicating wiring and risking divergence.
The registry also owns the package's OpenTelemetry instrumentation surface
for decode/encode calls (`codec.decode` / `codec.encode` spans, the
`audio_codec_*` counter and histogram pair). Codecs themselves stay free of
OTel concerns; the registry observes the boundary.
## Features
- Three lookup indexes — by canonical `name`, by file extension, by MIME
type. `get(key)` checks all three so callers do not need to know which
shape they hold.
- Eager initial registration via `new CodecRegistry({ codecs: [...] })` plus
imperative `register(codec)` / `unregister(name)`.
- `detect(bytes)` walks every registered codec's `canDecode` in
registration order and returns the first match's `name` (or `undefined`).
- `decode(bytes, opts?)` resolves the codec, awaits its `decode`, and builds
an `AudioDocument` from the resulting `Float32Array[]` PCM. Non-API codec
errors are wrapped in `AudioCodecsError("AUDIO_CODECS.DECODE_FAILED")`
with the original on `cause`; `AudioCodecsError` instances pass through.
- `encode(doc, format, opts?)` materialises the document's PCM, dispatches
to the codec registered for `format`, and returns the encoded bytes.
Unknown format → `AUDIO_CODECS.NOT_SUPPORTED`. Non-API encode error →
`AUDIO_CODECS.ENCODE_FAILED` with `cause`.
- `decodeStream(source, format)` returns a `ReadableStream<AudioPage>` whose
pull loop drives the codec's `createDecoder()` streaming factory.
Codecs without that factory throw `AUDIO_CODECS.STREAM_ERROR`.
- `encodeStream(doc, format)` returns a `ReadableStream<Uint8Array>` whose
pull loop feeds the document's pages (sorted by `startSample`) into the
codec's `createEncoder()`.
- `list()` returns every registered codec for introspection.
- Zod-validated codec registration via `AudioCodecSchema` — a malformed
codec object is rejected at `register()` with `INVALID_SOURCE`.
- OTel spans, counter, and duration histogram on every successful decode
and encode; spans carry the resolved `format` as an attribute.
## Usage: Basic Example
```ts
import { CodecRegistry } from "@suaveplan/audio-codecs/registry/registry";
import { WavCodec } from "@suaveplan/audio-codecs/codecs/wav";
const registry = new CodecRegistry({ codecs: [WavCodec] });
// decode WAV bytes into an AudioDocument
const doc = await registry.decode(wavBytes);
// re-encode back to WAV at 24-bit PCM
const wav = await registry.encode(doc, "wav", { bitDepth: 24, format: "pcm" });
```
## Usage: Advanced Example
Hot-swap a codec and pipeline a streaming decode into a custom transform:
```ts
import { CodecRegistry } from "@suaveplan/audio-codecs/registry/registry";
import { Mp3Codec, WavCodec, FlacCodec } from "@suaveplan/audio-codecs";
const registry = new CodecRegistry({ codecs: [WavCodec, Mp3Codec] });
registry.register(FlacCodec); // additive — now decodes FLAC too
const sourceStream: ReadableStream<Uint8Array> = await fetchAudio();
const pageStream = registry.decodeStream(sourceStream, "mp3");
// pipe through a downstream normaliser (defined elsewhere)
const normalised = pageStream.pipeThrough(makeNormaliser());
for await (const page of normalised) {
consumePage(page);
}
// detect-then-dispatch for unknown bytes
const fmt = registry.detect(unknownBytes);
if (fmt) {
const doc = await registry.decode(unknownBytes, { format: fmt });
useDoc(doc);
}
```
## API Reference
- `class CodecRegistry`
- `constructor(options?: CodecRegistryOptions)` — `options.codecs` are
each `register`-ed eagerly.
- `register(codec: AudioCodec): void` — throws
`AudioCodecsError("AUDIO_CODECS.INVALID_SOURCE")` if `codec` fails
`AudioCodecSchema` validation. Replaces any prior codec sharing the
same `name`; extension and MIME indexes additively expand.
- `unregister(name: string): void` — no-op for unknown names; clears
matching extension and MIME entries.
- `get(nameOrExtOrMime: string): AudioCodec | undefined`
- `list(): readonly AudioCodec[]`
- `detect(bytes: Uint8Array): string | undefined`
- `decode(bytes, opts?): Promise<AudioDocument>` — `opts.format` is a hint
that wins over `canDecode` detection when supplied; `opts.metadata` is
forwarded onto the resulting `AudioDocument`.
- `encode(doc, format, opts?): Promise<Uint8Array>`
- `decodeStream(source, format): ReadableStream<AudioPage>`
- `encodeStream(doc, format): ReadableStream<Uint8Array>`
## Implementation Notes
- The three lookup indexes are kept independently — `unregister` walks both
extension and MIME maps and deletes only entries that still point at the
removed codec, so registering two codecs that share an extension and
unregistering one leaves the other intact.
- `detect` iteration order is the order codecs were registered. Callers
that need a different priority (e.g. AIFF before WAV when both
canDecode an ambiguous prefix) should re-`register` the higher-priority
codec last so its `canDecode` runs first.
- The `#instrumented` helper centralises span lifetime, counter, and
histogram observations. It catches any thrown error, sets the span
status to ERROR, records the exception, then re-throws — so the
outer-most caller still observes the original (or wrapped) error.
- `pcmFromDocument` walks the document's pages in their stored order, not
by `startSample`. For documents produced by `buildDocument` this is the
same; for hand-constructed documents the caller is responsible for
ordered insertion. `encodeStream` does sort by `startSample` explicitly
to handle reordering at the streaming boundary.
- The constants `TRACER`, `METER`, `DECODE_COUNTER`, `ENCODE_COUNTER`,
`DECODE_HIST`, `ENCODE_HIST` are acquired at module evaluation time but
the underlying OTel SDK is a no-op when no exporter is registered, so
importing the registry has no observable runtime cost in unconfigured
environments.