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

6.4 KiB

registry/registry

In-memory plugin registry that owns audio-codec lookup and dispatch for the whole package.

(no submodules)

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

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:

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.