first commit
This commit is contained in:
136
docs/media/audio/audio-codecs/src/codecs/wasm-base.md
Normal file
136
docs/media/audio/audio-codecs/src/codecs/wasm-base.md
Normal 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.
|
||||
Reference in New Issue
Block a user