first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
# `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
```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<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.