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 imperativeregister(codec)/unregister(name). detect(bytes)walks every registered codec'scanDecodein registration order and returns the first match'sname(orundefined).decode(bytes, opts?)resolves the codec, awaits itsdecode, and builds anAudioDocumentfrom the resultingFloat32Array[]PCM. Non-API codec errors are wrapped inAudioCodecsError("AUDIO_CODECS.DECODE_FAILED")with the original oncause;AudioCodecsErrorinstances pass through.encode(doc, format, opts?)materialises the document's PCM, dispatches to the codec registered forformat, and returns the encoded bytes. Unknown format →AUDIO_CODECS.NOT_SUPPORTED. Non-API encode error →AUDIO_CODECS.ENCODE_FAILEDwithcause.decodeStream(source, format)returns aReadableStream<AudioPage>whose pull loop drives the codec'screateDecoder()streaming factory. Codecs without that factory throwAUDIO_CODECS.STREAM_ERROR.encodeStream(doc, format)returns aReadableStream<Uint8Array>whose pull loop feeds the document's pages (sorted bystartSample) into the codec'screateEncoder().list()returns every registered codec for introspection.- Zod-validated codec registration via
AudioCodecSchema— a malformed codec object is rejected atregister()withINVALID_SOURCE. - OTel spans, counter, and duration histogram on every successful decode
and encode; spans carry the resolved
formatas 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 CodecRegistryconstructor(options?: CodecRegistryOptions)—options.codecsare eachregister-ed eagerly.register(codec: AudioCodec): void— throwsAudioCodecsError("AUDIO_CODECS.INVALID_SOURCE")ifcodecfailsAudioCodecSchemavalidation. Replaces any prior codec sharing the samename; 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 | undefinedlist(): readonly AudioCodec[]detect(bytes: Uint8Array): string | undefineddecode(bytes, opts?): Promise<AudioDocument>—opts.formatis a hint that wins overcanDecodedetection when supplied;opts.metadatais forwarded onto the resultingAudioDocument.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 —
unregisterwalks 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. detectiteration 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-registerthe higher-priority codec last so itscanDecoderuns first.- The
#instrumentedhelper 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. pcmFromDocumentwalks the document's pages in their stored order, not bystartSample. For documents produced bybuildDocumentthis is the same; for hand-constructed documents the caller is responsible for ordered insertion.encodeStreamdoes sort bystartSampleexplicitly to handle reordering at the streaming boundary.- The constants
TRACER,METER,DECODE_COUNTER,ENCODE_COUNTER,DECODE_HIST,ENCODE_HISTare 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.