5.8 KiB
codecs/wasm-base
Shared factory for the seven lazy WASM-backed codec wrappers.
Purpose
The MP3, FLAC, Ogg Vorbis, Opus, AAC, WebM, and QOA codecs are all thin
adapters over external @audio/* WASM modules declared as
optionalDependencies in package.json. They share the same structural
contract — lazy-load the WASM module on first use, throw a stable
AUDIO_CODECS.NOT_SUPPORTED if the optional dependency is not
installed, and wrap codec errors into the package's AudioCodecsError
hierarchy.
createWasmCodec(spec) is the factory that captures that contract once
and produces the seven concrete AudioCodec instances. Without it,
every wrapper would re-implement memoised loading, error remapping, and
encode-not-supported branching — and any divergence between wrappers
would silently degrade some formats relative to others.
Features
createWasmCodec(spec: WasmCodecSpec): AudioCodec— returns anAudioCodecwhosename,extensions,mimeTypes, andcanDecodeare taken directly fromspec, and whosedecode/encodeinvoke the lazily-loaded WASM module's corresponding methods.- Lazy module load via
spec.load()— typically a closure capturing() => import("@audio/decode-<format>"). The promise is memoised after the first call, so repeated decode operations reuse the same loaded module. - Missing-module behaviour: if
spec.load()rejects (theoptionalDependencyis not installed), the codec throwsAudioCodecsError("AUDIO_CODECS.NOT_SUPPORTED")carrying the module name incontext.module. - Encode gating:
spec.supportsEncode === falseshort-circuits the encode method withNOT_SUPPORTEDbefore any module load — so a decode-only codec costs nothing extra on the encode path. - Decode-only modules: if the loaded module exposes no
decodemethod, the codec throwsNOT_SUPPORTEDwith the module name; same shape applies for missingencode. - Non-API errors from the underlying module are wrapped —
decodefailures becomeAUDIO_CODECS.DECODE_FAILED,encodefailures becomeAUDIO_CODECS.ENCODE_FAILED, both withcauseset to the original error andcontext.module+context.formatfor triage. AudioCodecsErrorthrown from inside the module passes through unchanged — already-typed errors don't get re-wrapped.
Usage: Basic Example
import { createWasmCodec } from "@suaveplan/audio-codecs/codecs/wasm-base";
export const FooCodec = createWasmCodec({
name: "foo",
extensions: ["foo"],
mimeTypes: ["audio/foo"],
module: "@audio/decode-foo",
magic: (bytes) => bytes.length >= 4 && bytes[0] === 0xfo && bytes[1] === 0x0d,
supportsEncode: false,
load: () => import("@audio/decode-foo"),
});
Usage: Advanced Example
Constructing a codec for an internal WASM module with custom decode
adaptation (e.g. when the underlying module's response shape doesn't
match DecodeResult exactly):
import { createWasmCodec } from "@suaveplan/audio-codecs/codecs/wasm-base";
export const SnakeoilCodec = createWasmCodec({
name: "snakeoil",
extensions: ["snkl"],
mimeTypes: ["audio/x-snakeoil"],
module: "@internal/snakeoil-decoder",
magic: (bytes) =>
bytes.length >= 8 &&
bytes[0] === 0x53 &&
bytes[1] === 0x4e &&
bytes[2] === 0x4b &&
bytes[3] === 0x4c,
supportsEncode: true,
load: async () => {
const mod = await import("@internal/snakeoil-decoder");
// Module exposes `decodeBuffer` / `encodeBuffer` not `decode` / `encode`.
// Adapt at the boundary so wasm-base's contract stays satisfied.
return {
decode: async (bytes) => {
const raw = await mod.decodeBuffer(bytes);
return {
channelData: raw.channels,
sampleRate: raw.sampleRate,
format: "snakeoil",
};
},
encode: async (channelData, opts) =>
mod.encodeBuffer(channelData, opts?.bitDepth ?? 16),
};
},
});
API Reference
interface WasmCodecModule— the shape that loaded modules must conform to:decode?,encode?,canDecode?. The factory tolerates modules that expose only a subset (e.g. decode-only) — thesupportsEncodeflag inWasmCodecSpecis the authoritative gate.interface WasmCodecSpec—name,extensions,mimeTypes,module(the bare specifier string for diagnostics),magic((bytes) => boolean),supportsEncode(boolean),load(() => Promise<WasmCodecModule>).function createWasmCodec(spec: WasmCodecSpec): AudioCodec— closure-capturesspecand returns a frozenAudioCodecwith lazy-loaded behaviour and the documented error-wrapping policy.
Implementation Notes
- The
loadclosure stores the promise (not the resolved module) so paralleldecodecalls during initial load all share the same promise — no double-load race. - Errors from
spec.load()are caught once and re-thrown asNOT_SUPPORTED; subsequent calls reuse the cached rejected promise so the same error surfaces on every retry without re-importing. - The
decodeandencodemethods always re-wrap non-API errors;AudioCodecsErrorinstances pass through. This means downstream callers can rely on a consistent error type from every WASM codec. - The factory does not eagerly probe
spec.modulefor availability — importing the package barrel pulls in only the factory, not any WASM modules. WASM is loaded on first decode/encode call, not at module import time, matching the spec'sSubpath exports per feature moduleandWASM-backed codecs loaded on demandrequirements. - Encode paths short-circuit when
supportsEncode === falseso a decode-only codec never invokes its loader for an encode call.