142 lines
6.4 KiB
Markdown
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.
|