# `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 an `AudioCodec` whose `name`, `extensions`, `mimeTypes`, and `canDecode` are taken directly from `spec`, and whose `decode` / `encode` invoke the lazily-loaded WASM module's corresponding methods. - Lazy module load via `spec.load()` — typically a closure capturing `() => import("@audio/decode-")`. The promise is memoised after the first call, so repeated decode operations reuse the same loaded module. - Missing-module behaviour: if `spec.load()` rejects (the `optionalDependency` is not installed), the codec throws `AudioCodecsError("AUDIO_CODECS.NOT_SUPPORTED")` carrying the module name in `context.module`. - Encode gating: `spec.supportsEncode === false` short-circuits the encode method with `NOT_SUPPORTED` before any module load — so a decode-only codec costs nothing extra on the encode path. - Decode-only modules: if the loaded module exposes no `decode` method, the codec throws `NOT_SUPPORTED` with the module name; same shape applies for missing `encode`. - Non-API errors from the underlying module are wrapped — `decode` failures become `AUDIO_CODECS.DECODE_FAILED`, `encode` failures become `AUDIO_CODECS.ENCODE_FAILED`, both with `cause` set to the original error and `context.module` + `context.format` for triage. - `AudioCodecsError` thrown from inside the module passes through unchanged — already-typed errors don't get re-wrapped. ## Usage: Basic Example ```ts 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): ```ts 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) — the `supportsEncode` flag in `WasmCodecSpec` is the authoritative gate. - `interface WasmCodecSpec` — `name`, `extensions`, `mimeTypes`, `module` (the bare specifier string for diagnostics), `magic` (`(bytes) => boolean`), `supportsEncode` (`boolean`), `load` (`() => Promise`). - `function createWasmCodec(spec: WasmCodecSpec): AudioCodec` — closure-captures `spec` and returns a frozen `AudioCodec` with lazy-loaded behaviour and the documented error-wrapping policy. ## Implementation Notes - The `load` closure stores the promise (not the resolved module) so parallel `decode` calls during initial load all share the same promise — no double-load race. - Errors from `spec.load()` are caught once and re-thrown as `NOT_SUPPORTED`; subsequent calls reuse the cached rejected promise so the same error surfaces on every retry without re-importing. - The `decode` and `encode` methods always re-wrap non-API errors; `AudioCodecsError` instances pass through. This means downstream callers can rely on a consistent error type from every WASM codec. - The factory does not eagerly probe `spec.module` for 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's `Subpath exports per feature module` and `WASM-backed codecs loaded on demand` requirements. - Encode paths short-circuit when `supportsEncode === false` so a decode-only codec never invokes its loader for an encode call.