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

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 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-<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 (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

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) — 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<WasmCodecModule>).
  • 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.