first commit
This commit is contained in:
72
docs/media/audio/audio-codecs/src/codecs/aac.md
Normal file
72
docs/media/audio/audio-codecs/src/codecs/aac.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# `codecs/aac`
|
||||
|
||||
Lazy AAC codec — decode-only wrapper over `@audio/decode-aac` WASM
|
||||
module loaded on first use.
|
||||
|
||||
## Purpose
|
||||
|
||||
AAC (Advanced Audio Coding) is the modern lossy audio successor to MP3,
|
||||
used by every major streaming service and as the default audio format
|
||||
in MP4/M4A containers. The codec is decode-only because (a) the AAC
|
||||
encoder is complex and patent-encumbered in many jurisdictions, and
|
||||
(b) the canonical lossy encode target is Opus.
|
||||
|
||||
Implementation delegates to [`createWasmCodec`](./wasm-base.md) for the
|
||||
lazy-load, error-wrapping, and missing-module behaviour shared by every
|
||||
WASM wrapper.
|
||||
|
||||
## Features
|
||||
|
||||
- `name === "aac"`, extensions `["aac", "m4a"]`, MIME types
|
||||
`["audio/aac", "audio/mp4"]`.
|
||||
- `canDecode(bytes)` — magic-byte sniff for the ADTS sync word
|
||||
(first byte `0xff`, second byte top 4 bits `0b1111`); rejects
|
||||
buffers shorter than 2 bytes.
|
||||
- Decode supported via `@audio/decode-aac`.
|
||||
- `supportsEncode: false` — encode throws `NOT_SUPPORTED`
|
||||
short-circuit.
|
||||
- Streaming factory absent.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { AacCodec } from "@suaveplan/audio-codecs/codecs/aac";
|
||||
|
||||
const decoded = await AacCodec.decode(aacBytes);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CodecRegistry, AacCodec, OpusCodec } from "@suaveplan/audio-codecs";
|
||||
|
||||
const registry = new CodecRegistry({ codecs: [AacCodec, OpusCodec] });
|
||||
|
||||
// transcode AAC → Opus for downstream storage
|
||||
const aacDoc = await registry.decode(aacBytes);
|
||||
const opusBytes = await registry.encode(aacDoc, "opus");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AacCodec: AudioCodec` — frozen constant; decode-only.
|
||||
- `canDecode(bytes)`, `decode(bytes, opts?)` — standard `AudioCodec`
|
||||
contract.
|
||||
- `encode()` — always throws `AUDIO_CODECS.NOT_SUPPORTED`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The ADTS sync word check (`0xff 0xf0`) covers raw ADTS-framed AAC.
|
||||
AAC inside an MP4/M4A container starts with `ftyp` instead; that
|
||||
case is currently NOT handled by `canDecode` — callers passing
|
||||
M4A files need to either use a separate MP4 demuxer first or call
|
||||
`decode` with `format: "aac"` to bypass detection. A future
|
||||
extension may add an MP4-container sniffer.
|
||||
- Decode-only by design. suaveplan's preferred lossy encode target is
|
||||
Opus; AAC encoding requires non-trivial licensing in some
|
||||
jurisdictions and is intentionally out of scope.
|
||||
- First decode lazily loads `@audio/decode-aac`; subsequent calls
|
||||
reuse the memoised module.
|
||||
- Missing `@audio/decode-aac` → `AUDIO_CODECS.NOT_SUPPORTED`.
|
||||
- Streaming factory absent — `CodecRegistry.decodeStream("aac", …)`
|
||||
throws `AUDIO_CODECS.STREAM_ERROR`.
|
||||
117
docs/media/audio/audio-codecs/src/codecs/aiff.md
Normal file
117
docs/media/audio/audio-codecs/src/codecs/aiff.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# `codecs/aiff`
|
||||
|
||||
Native pure-JS AIFF (Audio Interchange File Format) decoder, decode-only.
|
||||
|
||||
## Purpose
|
||||
|
||||
AIFF is the big-endian counterpart to WAV used historically on Apple
|
||||
hardware. The codec supports both the original AIFF container and the
|
||||
later AIFC variant (which uses the same chunk structure but allows
|
||||
non-PCM payloads — though this codec only decodes uncompressed integer
|
||||
PCM).
|
||||
|
||||
Encoding is intentionally not supported: AIFF write is rarely useful
|
||||
outside Apple's own tooling and the canonical archive format
|
||||
is WAV/FLAC. Callers attempting to encode AIFF receive a clean
|
||||
`AUDIO_CODECS.NOT_SUPPORTED` error rather than a silently-degraded
|
||||
fallback.
|
||||
|
||||
## Features
|
||||
|
||||
- `canDecode(bytes)` — magic-byte sniff for `FORM` at byte 0 plus
|
||||
`AIFF` or `AIFC` at byte 8; rejects buffers shorter than 12 bytes
|
||||
or with non-matching magic.
|
||||
- `decode(bytes)` — walks the AIFF chunk list for `COMM` and `SSND`,
|
||||
decodes the 80-bit IEEE 754 extended-precision sample rate, and
|
||||
returns a `DecodeResult` with normalised `Float32Array[]` PCM.
|
||||
- 8-bit signed, 16-bit signed, 24-bit signed, and 32-bit signed PCM
|
||||
in big-endian byte order are all decoded; the 80-bit extended-float
|
||||
sample rate field is parsed in full so the codec round-trips any
|
||||
sample rate AIFF can express, including the unusual ones used by old
|
||||
samplers (37_800, 32_000, 24_000, etc).
|
||||
- `encode` — throws `AudioCodecsError("AUDIO_CODECS.NOT_SUPPORTED")`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { AiffCodec } from "@suaveplan/audio-codecs/codecs/aiff";
|
||||
|
||||
const decoded = await AiffCodec.decode(aiffBytes);
|
||||
console.log(decoded.sampleRate, decoded.channelData.length);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Detecting AIFF vs AIFC and falling through to a separate processor:
|
||||
|
||||
```ts
|
||||
import { AiffCodec } from "@suaveplan/audio-codecs/codecs/aiff";
|
||||
|
||||
async function processAudio(bytes: Uint8Array): Promise<AudioDocument> {
|
||||
if (AiffCodec.canDecode(bytes)) {
|
||||
const result = await AiffCodec.decode(bytes);
|
||||
return buildDocumentFromResult(result);
|
||||
}
|
||||
return processWithFallbackCodec(bytes);
|
||||
}
|
||||
|
||||
// Direct registry registration:
|
||||
import { CodecRegistry } from "@suaveplan/audio-codecs";
|
||||
const registry = new CodecRegistry({ codecs: [AiffCodec] });
|
||||
const doc = await registry.decode(aiffBytes);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `AiffCodec: AudioCodec` — frozen constant; `name === "aiff"`,
|
||||
`extensions === ["aiff", "aif", "aifc"]`, `mimeTypes === ["audio/aiff",
|
||||
"audio/x-aiff"]`.
|
||||
- `canDecode(bytes: Uint8Array): boolean`
|
||||
- `decode(bytes: Uint8Array): Promise<DecodeResult>` — throws
|
||||
`AudioCodecsError("AUDIO_CODECS.DECODE_FAILED")` for:
|
||||
- buffer length < 12 (`too-short`),
|
||||
- non-FORM magic (`bad-form`),
|
||||
- non-AIFF/AIFC type (`bad-type`),
|
||||
- COMM chunk size < 18 (`bad-comm-size`),
|
||||
- missing COMM chunk (`no-common`),
|
||||
- missing SSND chunk (`no-ssnd`),
|
||||
- `sampleSize` = 0 (`unsupported-bits`),
|
||||
- `sampleSize` > 32 (`unsupported-bytes-per-sample`).
|
||||
- `encode(): Promise<Uint8Array>` — always throws
|
||||
`AudioCodecsError("AUDIO_CODECS.NOT_SUPPORTED")`.
|
||||
- The optional streaming-factory methods are absent; `CodecRegistry`
|
||||
honours that and throws `STREAM_ERROR` on attempted streaming
|
||||
operations against this codec.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The 80-bit extended-precision IEEE 754 sample-rate field is decoded
|
||||
by `parseIeee754Extended(bytes, off)` which reads 10 bytes (1 sign
|
||||
bit + 15 exponent bits biased by 16383 + 64-bit explicit mantissa —
|
||||
the AIFF format predates double-precision IEEE 754 and uses an
|
||||
explicit-mantissa variant rather than the more common implicit-1
|
||||
binary64). The function returns `0` when `off + 10 > bytes.byteLength`
|
||||
so a truncated buffer produces a 0-Hz result rather than a thrown
|
||||
error — the parent `decode` then fails with a meaningful chunk-level
|
||||
error.
|
||||
- AIFF chunk sizes are big-endian uint32; the chunk walker advances
|
||||
`pos = body + size + (size & 1)` to honour the AIFF requirement that
|
||||
chunks pad to even boundaries.
|
||||
- The normalisation factor is `(1 << (sampleSize - 1)) >>> 0` — the
|
||||
unsigned-shift handles the 32-bit case where `1 << 31` would
|
||||
otherwise produce a negative number under signed arithmetic.
|
||||
- The 24-bit decoder reads bytes in MSB-first order (AIFF is
|
||||
big-endian) using one signed byte + two unsigned bytes to preserve
|
||||
the sign bit without depending on the JavaScript runtime's
|
||||
sign-extension semantics.
|
||||
- `numSampleFrames` from COMM caps decoded output; the codec also
|
||||
caps via `availableFrames = floor(ssndLength / frameBytes)` so a
|
||||
truncated SSND chunk simply yields fewer frames rather than reading
|
||||
past the buffer.
|
||||
- No streaming factory exists because AIFF's `SSND` chunk is variably
|
||||
positioned within the FORM block — implementing a single-pass
|
||||
streaming decoder would require buffering the whole header section
|
||||
anyway, which negates the streaming benefit. Callers needing
|
||||
streaming PCM can pipe the AIFF through a full decode then push the
|
||||
result via the registry's `encodeStream` into WAV (which is
|
||||
streamable).
|
||||
109
docs/media/audio/audio-codecs/src/codecs/caf.md
Normal file
109
docs/media/audio/audio-codecs/src/codecs/caf.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# `codecs/caf`
|
||||
|
||||
Native pure-JS CAF (Apple Core Audio Format) decoder, decode-only.
|
||||
|
||||
## Purpose
|
||||
|
||||
CAF is the modern Apple-specific container that replaced AIFF for use
|
||||
cases needing 64-bit chunk sizes (files larger than 4 GiB) and embedded
|
||||
metadata. This codec decodes the uncompressed integer PCM (`lpcm`)
|
||||
payload that the vast majority of real-world CAF files carry. Compressed
|
||||
payloads (Apple Lossless, AAC inside CAF) are out of scope and require
|
||||
the corresponding WASM codec.
|
||||
|
||||
Encoding is intentionally not supported. CAF write is essentially never
|
||||
useful outside Apple's own audio toolchain, and the canonical archive
|
||||
formats are WAV and FLAC. The codec throws
|
||||
`AUDIO_CODECS.NOT_SUPPORTED` for any encode attempt.
|
||||
|
||||
## Features
|
||||
|
||||
- `canDecode(bytes)` — magic-byte sniff for `caff` (`0x63 0x61 0x66 0x66`)
|
||||
at byte 0; rejects buffers shorter than 4 bytes or with non-matching
|
||||
magic.
|
||||
- `decode(bytes)` — walks the CAF chunk list (64-bit big-endian chunk
|
||||
sizes — `caff` uses uint64 chunk sizes, distinct from RIFF/AIFF
|
||||
uint32 sizes) for `desc` and `data`, decodes the uncompressed PCM,
|
||||
and returns a `DecodeResult` with normalised `Float32Array[]` PCM
|
||||
and the sample rate parsed from the IEEE 754 binary64 `desc.sampleRate`
|
||||
field.
|
||||
- Sample byte order is determined by the `formatFlags` bit
|
||||
`kCAFLinearPCMFormatFlagIsLittleEndian` (`0x0002`); the decoder honours
|
||||
it per sample so files written by either little-endian or big-endian
|
||||
producers decode correctly.
|
||||
- 16-bit, 24-bit, and 32-bit signed integer PCM are supported. Other
|
||||
bit depths (8, 64, 32 float) are rejected with
|
||||
`AUDIO_CODECS.DECODE_FAILED("unsupported-bytes-per-sample")`.
|
||||
- `encode` — throws `AudioCodecsError("AUDIO_CODECS.NOT_SUPPORTED")`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { CafCodec } from "@suaveplan/audio-codecs/codecs/caf";
|
||||
|
||||
const decoded = await CafCodec.decode(cafBytes);
|
||||
console.log(decoded.sampleRate, decoded.format);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Detect-then-dispatch via the shared registry:
|
||||
|
||||
```ts
|
||||
import { CodecRegistry, CafCodec, WavCodec } from "@suaveplan/audio-codecs";
|
||||
|
||||
const registry = new CodecRegistry({ codecs: [WavCodec, CafCodec] });
|
||||
|
||||
const fmt = registry.detect(bytes);
|
||||
if (fmt === "caf") {
|
||||
// re-encode to WAV for downstream consumers
|
||||
const doc = await registry.decode(bytes);
|
||||
const wav = await registry.encode(doc, "wav", { bitDepth: 16, format: "pcm" });
|
||||
return wav;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `CafCodec: AudioCodec` — frozen constant; `name === "caf"`,
|
||||
`extensions === ["caf"]`, `mimeTypes === ["audio/x-caf"]`.
|
||||
- `canDecode(bytes: Uint8Array): boolean`
|
||||
- `decode(bytes: Uint8Array): Promise<DecodeResult>` — throws
|
||||
`AudioCodecsError("AUDIO_CODECS.DECODE_FAILED")` for:
|
||||
- buffer length < 8 (`too-short`),
|
||||
- non-`caff` magic (`bad-magic`),
|
||||
- `desc` chunk size < 32 (`bad-desc-size`),
|
||||
- missing `desc` chunk (`no-desc`),
|
||||
- missing `data` chunk (`no-data`),
|
||||
- `bitsPerChannel` = 0 (`unsupported-bits`),
|
||||
- `bitsPerChannel` > 32 (`unsupported-bytes-per-sample`).
|
||||
- `encode(): Promise<Uint8Array>` — always throws
|
||||
`AudioCodecsError("AUDIO_CODECS.NOT_SUPPORTED")`.
|
||||
- The optional streaming-factory methods are absent; the registry
|
||||
honours that and throws `STREAM_ERROR` on streaming requests.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- CAF chunk sizes are uint64 big-endian. The codec reads them via two
|
||||
separate uint32 calls (`high * 2^32 + low`) because JavaScript's
|
||||
`DataView.getBigUint64` returns a `BigInt` which would force the
|
||||
caller to convert downstream and complicate arithmetic. The
|
||||
`high * 2^32` form preserves precision for sizes up to
|
||||
`Number.MAX_SAFE_INTEGER` (~9 PiB) which is far beyond any real CAF
|
||||
payload.
|
||||
- The `data` chunk has a 4-byte `editCount` field at the start of its
|
||||
body; the codec skips it (offset by 4) before treating the remaining
|
||||
bytes as PCM.
|
||||
- The sample rate is decoded via `DataView.getFloat64(..., false)`
|
||||
(big-endian IEEE 754 binary64) — this is the modern CAF format and
|
||||
contrasts with AIFF's 80-bit extended-precision representation.
|
||||
- The format ID at offset 8 of `desc` is `lpcm` (`0x6c70636d`) for
|
||||
uncompressed integer PCM. The decoder does not currently dispatch on
|
||||
this field — it assumes `lpcm` and decodes accordingly. Other format
|
||||
IDs (`alac`, `aac `, `lpcm` floats) would produce wrong output;
|
||||
callers needing compressed CAF support should use the corresponding
|
||||
WASM codec for the inner format.
|
||||
- Sample byte order is determined per sample by the
|
||||
`formatFlags & 0x02` bit; the decoder applies it uniformly to 16/24/32
|
||||
bit reads. 24-bit reads use a signed high byte + two unsigned bytes,
|
||||
reordered based on endianness, to preserve sign across either layout.
|
||||
80
docs/media/audio/audio-codecs/src/codecs/flac.md
Normal file
80
docs/media/audio/audio-codecs/src/codecs/flac.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# `codecs/flac`
|
||||
|
||||
Lazy FLAC codec — thin wrapper over `@audio/decode-flac` WASM module
|
||||
loaded on first use.
|
||||
|
||||
## Purpose
|
||||
|
||||
FLAC (Free Lossless Audio Codec) is the canonical lossless format for
|
||||
high-quality audio archival. It compresses raw PCM by 30–60% with no
|
||||
quality loss, making it the standard format for music libraries that
|
||||
need to preserve every bit of the original signal.
|
||||
|
||||
The package treats FLAC as a WASM-backed codec to keep the universal
|
||||
`audio-codecs` package size-constant — pure-JS FLAC implementations
|
||||
are large and slow, and bundling one would punish every consumer who
|
||||
never touches FLAC. The actual decode/encode work delegates to
|
||||
`@audio/decode-flac` loaded via dynamic `import()` on first call.
|
||||
|
||||
Implementation delegates to [`createWasmCodec`](./wasm-base.md) which
|
||||
documents the shared lazy-load, missing-module, and error-wrapping
|
||||
contract.
|
||||
|
||||
## Features
|
||||
|
||||
- `name === "flac"`, extensions `["flac"]`, MIME types
|
||||
`["audio/flac", "audio/x-flac"]`.
|
||||
- `canDecode(bytes)` — magic-byte sniff for `fLaC`
|
||||
(`0x66 0x4c 0x61 0x43`) at byte 0; rejects buffers shorter than 4
|
||||
bytes.
|
||||
- Decode + encode both supported (`supportsEncode: true`).
|
||||
- Streaming factory absent — single-pass full-buffer codec.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { FlacCodec } from "@suaveplan/audio-codecs/codecs/flac";
|
||||
|
||||
const decoded = await FlacCodec.decode(flacBytes);
|
||||
const reEncoded = await FlacCodec.encode(decoded.channelData, {
|
||||
sampleRate: decoded.sampleRate,
|
||||
bitDepth: 24,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Round-trip FLAC → WAV → FLAC via the shared registry:
|
||||
|
||||
```ts
|
||||
import { CodecRegistry, FlacCodec, WavCodec } from "@suaveplan/audio-codecs";
|
||||
|
||||
const registry = new CodecRegistry({ codecs: [FlacCodec, WavCodec] });
|
||||
|
||||
const flacDoc = await registry.decode(flacBytes);
|
||||
const intermediate = await registry.encode(flacDoc, "wav", { bitDepth: 24, format: "pcm" });
|
||||
const wavDoc = await registry.decode(intermediate);
|
||||
const finalFlac = await registry.encode(wavDoc, "flac");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `FlacCodec: AudioCodec` — `name === "flac"`, frozen.
|
||||
- `canDecode(bytes)`, `decode(bytes, opts?)`, `encode(channelData,
|
||||
opts?)` — see [`wasm-base.md`](./wasm-base.md) for the shared
|
||||
contract.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Magic-byte sniff is cheap — no WASM load required for `canDecode`.
|
||||
- The first decode/encode call lazily loads `@audio/decode-flac`;
|
||||
subsequent calls reuse the same memoised module instance.
|
||||
- Missing `@audio/decode-flac` → `AUDIO_CODECS.NOT_SUPPORTED` with the
|
||||
module name in `context.module`.
|
||||
- FLAC's native streaming-decode form (`Subset` profile blocks) is
|
||||
not exposed by this wrapper — adding it requires a per-block
|
||||
callback API which is a future extension.
|
||||
- FLAC files commonly carry Vorbis comments and embedded artwork in
|
||||
the same container; the codec emits PCM only — metadata extraction
|
||||
is `@suaveplan/audio-metadata`'s responsibility via its own FLAC
|
||||
parser, separate from this codec.
|
||||
100
docs/media/audio/audio-codecs/src/codecs/mp3.md
Normal file
100
docs/media/audio/audio-codecs/src/codecs/mp3.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# `codecs/mp3`
|
||||
|
||||
Lazy MP3 codec — thin wrapper that defers decode/encode to the
|
||||
`@audio/decode-mp3` WASM module loaded via dynamic `import()` on first
|
||||
use.
|
||||
|
||||
## Purpose
|
||||
|
||||
MP3 is the canonical lossy audio format. It is treated as a WASM-backed
|
||||
codec because pure-JS MP3 decoders are slow and large enough that bundling
|
||||
one with the universal `audio-codecs` package would punish every
|
||||
consumer who never decodes MP3. Instead the codec ships as a thin lazy
|
||||
wrapper that costs essentially zero bytes to import and pays for itself
|
||||
only when MP3 bytes actually arrive.
|
||||
|
||||
The implementation delegates to `createWasmCodec` from
|
||||
[`wasm-base.md`](./wasm-base.md) — see that document for the lazy-load,
|
||||
error-wrapping, and missing-module-handling contract that every WASM
|
||||
wrapper inherits.
|
||||
|
||||
## Features
|
||||
|
||||
- `name === "mp3"`, extensions `["mp3", "mpeg"]`, MIME types
|
||||
`["audio/mpeg", "audio/mp3"]`.
|
||||
- `canDecode(bytes)` — magic-byte sniff: returns `true` for buffers
|
||||
starting with the ID3v2 tag prefix (`"ID3"`) or the MPEG-1 audio sync
|
||||
word (first byte `0xff`, second byte top 3 bits `0b111`). Returns
|
||||
`false` for buffers shorter than 2 bytes.
|
||||
- Decode supported via `@audio/decode-mp3` once loaded.
|
||||
- Encode supported via the same module (`supportsEncode: true`).
|
||||
- Streaming factory absent — the underlying module exposes only
|
||||
full-buffer decode/encode; streaming MP3 needs a per-frame parser
|
||||
that lives in `@audio/stream-mp3` if/when that ships.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Mp3Codec } from "@suaveplan/audio-codecs/codecs/mp3";
|
||||
|
||||
// magic sniff works without loading the WASM module
|
||||
if (Mp3Codec.canDecode(bytes)) {
|
||||
const decoded = await Mp3Codec.decode(bytes); // first call loads WASM
|
||||
consume(decoded.channelData);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Register MP3 alongside other codecs in a shared registry:
|
||||
|
||||
```ts
|
||||
import { CodecRegistry, Mp3Codec, WavCodec, FlacCodec } from "@suaveplan/audio-codecs";
|
||||
|
||||
const registry = new CodecRegistry({
|
||||
codecs: [WavCodec, Mp3Codec, FlacCodec],
|
||||
});
|
||||
|
||||
// magic-byte detection picks the right codec
|
||||
const fmt = registry.detect(bytes);
|
||||
if (fmt) {
|
||||
const doc = await registry.decode(bytes, { format: fmt });
|
||||
return doc;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `Mp3Codec: AudioCodec` — frozen constant produced by
|
||||
`createWasmCodec({ name: "mp3", extensions, mimeTypes, module:
|
||||
"@audio/decode-mp3", magic: mp3Magic, supportsEncode: true, load })`.
|
||||
- `canDecode(bytes: Uint8Array): boolean` — magic-byte sniff
|
||||
(lightweight, synchronous, no WASM load).
|
||||
- `decode(bytes: Uint8Array, opts?: DecodeOpts): Promise<DecodeResult>` —
|
||||
lazily loads `@audio/decode-mp3` on first call. Throws
|
||||
`AudioCodecsError("AUDIO_CODECS.NOT_SUPPORTED")` if the optional
|
||||
dependency is not installed, or `DECODE_FAILED` on a codec error.
|
||||
- `encode(channelData, opts?: EncodeOpts): Promise<Uint8Array>` — same
|
||||
loading semantics; throws `NOT_SUPPORTED` if the module is missing or
|
||||
exposes no encode method, `ENCODE_FAILED` on codec error.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The magic-byte sniff is intentionally cheap so the registry can probe
|
||||
every codec without paying any WASM-load cost. False positives on
|
||||
the MPEG sync word are possible because the sync pattern is short
|
||||
(12 bits); callers that need stricter detection should layer an MP3
|
||||
frame-header validator on top.
|
||||
- The ID3v2 prefix means real-world MP3 files carrying ID3 metadata
|
||||
still sniff as MP3; the metadata extraction is `@suaveplan/audio-metadata`'s
|
||||
responsibility, not this codec's.
|
||||
- The codec follows the spec's "WASM-backed codecs loaded on demand"
|
||||
requirement — no static import of `@audio/decode-mp3`. Consumers
|
||||
that need full bundling can install the dep explicitly; consumers
|
||||
that only handle WAV/AIFF pay zero bytes for the MP3 path.
|
||||
- The first decode/encode call may take 50–200 ms while the WASM module
|
||||
initialises (instantiate + memory allocation). Subsequent calls reuse
|
||||
the same instance and run at native-adjacent speed.
|
||||
- The streaming-decoder / streaming-encoder factories are intentionally
|
||||
absent on this codec; `CodecRegistry.decodeStream("mp3", …)` therefore
|
||||
throws `AUDIO_CODECS.STREAM_ERROR`.
|
||||
82
docs/media/audio/audio-codecs/src/codecs/ogg-vorbis.md
Normal file
82
docs/media/audio/audio-codecs/src/codecs/ogg-vorbis.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# `codecs/ogg-vorbis`
|
||||
|
||||
Lazy Ogg Vorbis codec — decode-only wrapper over
|
||||
`@audio/decode-ogg-vorbis` WASM module loaded on first use.
|
||||
|
||||
## Purpose
|
||||
|
||||
Ogg Vorbis is the canonical open-source lossy audio format that pre-dates
|
||||
Opus, still widely used in game audio and free-software distributions.
|
||||
The codec is decode-only because (a) the Vorbis encoder is a complex
|
||||
piece of perceptual-audio engineering best left to the canonical
|
||||
`libvorbis` implementation packaged separately, and (b) Opus is the
|
||||
preferred lossy encode target for new content.
|
||||
|
||||
Implementation delegates to [`createWasmCodec`](./wasm-base.md) — see
|
||||
that document for the lazy-load, error-wrapping, and missing-module
|
||||
behaviour shared by every WASM wrapper.
|
||||
|
||||
## Features
|
||||
|
||||
- `name === "ogg-vorbis"`, extensions `["ogg", "oga"]`, MIME types
|
||||
`["audio/ogg", "audio/vorbis"]`.
|
||||
- `canDecode(bytes)` — magic-byte sniff for `OggS`
|
||||
(`0x4f 0x67 0x67 0x53`) at byte 0; rejects buffers shorter than 4
|
||||
bytes.
|
||||
- Decode supported via `@audio/decode-ogg-vorbis`.
|
||||
- `supportsEncode: false` — encode throws
|
||||
`AUDIO_CODECS.NOT_SUPPORTED` short-circuit (no WASM load).
|
||||
- Streaming factory absent.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OggVorbisCodec } from "@suaveplan/audio-codecs/codecs/ogg-vorbis";
|
||||
|
||||
const decoded = await OggVorbisCodec.decode(oggBytes);
|
||||
console.log(decoded.sampleRate, decoded.channelData.length);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Detect-then-dispatch from a mixed source via the registry:
|
||||
|
||||
```ts
|
||||
import { CodecRegistry, OggVorbisCodec, OpusCodec, WavCodec } from "@suaveplan/audio-codecs";
|
||||
|
||||
const registry = new CodecRegistry({
|
||||
codecs: [WavCodec, OggVorbisCodec, OpusCodec],
|
||||
});
|
||||
|
||||
const fmt = registry.detect(bytes);
|
||||
// Ogg containers may carry Vorbis or Opus; the first matching codec by
|
||||
// magic wins. If both Ogg-based codecs are registered, the codec
|
||||
// registered first takes precedence — register OpusCodec first if you
|
||||
// want Opus to win the tie.
|
||||
if (fmt === "ogg-vorbis") {
|
||||
return registry.decode(bytes);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `OggVorbisCodec: AudioCodec` — frozen constant; decode-only.
|
||||
- `canDecode(bytes)`, `decode(bytes, opts?)` — standard
|
||||
`AudioCodec` contract; encode throws `NOT_SUPPORTED`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `OggS` magic is shared with Opus, FLAC-in-Ogg, and Ogg-Speex —
|
||||
callers need to layer downstream sniffing if they need to
|
||||
discriminate. The default registry order in `decode-source.ts`
|
||||
registers Vorbis before Opus, which is the convention
|
||||
for ambiguous Ogg payloads.
|
||||
- Encode is intentionally not supported. Preferred lossy
|
||||
encode target is Opus; callers needing Vorbis output should run
|
||||
`libvorbis` in a separate worker.
|
||||
- The first decode call lazily loads `@audio/decode-ogg-vorbis`;
|
||||
subsequent calls reuse the memoised module.
|
||||
- Missing `@audio/decode-ogg-vorbis` → `AUDIO_CODECS.NOT_SUPPORTED`
|
||||
with the module name in `context.module`.
|
||||
- Streaming factory absent — `CodecRegistry.decodeStream("ogg-vorbis",
|
||||
…)` throws `AUDIO_CODECS.STREAM_ERROR`.
|
||||
73
docs/media/audio/audio-codecs/src/codecs/opus.md
Normal file
73
docs/media/audio/audio-codecs/src/codecs/opus.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# `codecs/opus`
|
||||
|
||||
Lazy Opus codec — wrapper over `@audio/decode-opus` WASM module loaded on
|
||||
first use.
|
||||
|
||||
## Purpose
|
||||
|
||||
Opus is the modern open-source lossy audio codec that supersedes both
|
||||
Vorbis (for general-purpose use) and Speex (for voice). It encodes
|
||||
voice and music at low bitrates with quality that frequently beats
|
||||
MP3 and AAC, and is the default target for WebRTC voice traffic.
|
||||
The package treats Opus as the canonical lossy encode target.
|
||||
|
||||
The codec is implemented as a thin wrapper over `@audio/decode-opus`
|
||||
loaded via dynamic `import()`. Implementation delegates to
|
||||
[`createWasmCodec`](./wasm-base.md) for the lazy-load, error-wrapping,
|
||||
and missing-module behaviour shared by every WASM wrapper.
|
||||
|
||||
## Features
|
||||
|
||||
- `name === "opus"`, extensions `["opus"]`, MIME types `["audio/opus"]`.
|
||||
- `canDecode(bytes)` — magic-byte sniff for `OggS` at byte 0 (Opus
|
||||
payloads are typically Ogg-encapsulated); rejects buffers shorter
|
||||
than 4 bytes.
|
||||
- Decode + encode both supported.
|
||||
- Streaming factory absent — single-pass full-buffer codec.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { OpusCodec } from "@suaveplan/audio-codecs/codecs/opus";
|
||||
|
||||
const decoded = await OpusCodec.decode(opusBytes);
|
||||
const reEncoded = await OpusCodec.encode(decoded.channelData);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Pipeline: decode WAV → re-encode as Opus for transport:
|
||||
|
||||
```ts
|
||||
import { CodecRegistry, OpusCodec, WavCodec } from "@suaveplan/audio-codecs";
|
||||
|
||||
const registry = new CodecRegistry({ codecs: [WavCodec, OpusCodec] });
|
||||
|
||||
const wavDoc = await registry.decode(wavBytes);
|
||||
const opusBytes = await registry.encode(wavDoc, "opus");
|
||||
// Send opusBytes over a low-bandwidth transport.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `OpusCodec: AudioCodec` — frozen constant.
|
||||
- `canDecode(bytes)`, `decode(bytes, opts?)`, `encode(channelData,
|
||||
opts?)` — see [`wasm-base.md`](./wasm-base.md).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `OggS` magic shared with Vorbis and FLAC-in-Ogg means
|
||||
detect-then-dispatch may pick the wrong codec when multiple
|
||||
Ogg-family codecs are registered. Register Opus before Vorbis if
|
||||
you expect Opus content to dominate; otherwise pass `format: "opus"`
|
||||
as a hint to `decode` to bypass detection.
|
||||
- The first decode/encode call lazily loads `@audio/decode-opus`;
|
||||
subsequent calls reuse the memoised module.
|
||||
- Missing `@audio/decode-opus` → `AUDIO_CODECS.NOT_SUPPORTED`.
|
||||
- Streaming Opus (chunked decode at 20 ms frame boundaries) is not
|
||||
exposed by this wrapper — adding it requires a per-frame parser
|
||||
and the `@audio/stream-opus` companion module which is a future
|
||||
extension.
|
||||
- The codec accepts the standard `EncodeOpts` shape but Opus' real
|
||||
encoder tuning (bitrate, complexity, signal type) is module-specific
|
||||
and lives in the `@audio/decode-opus` options surface.
|
||||
74
docs/media/audio/audio-codecs/src/codecs/qoa.md
Normal file
74
docs/media/audio/audio-codecs/src/codecs/qoa.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# `codecs/qoa`
|
||||
|
||||
Lazy QOA (Quite OK Audio) codec — wrapper over `@audio/decode-qoa` WASM
|
||||
module loaded on first use.
|
||||
|
||||
## Purpose
|
||||
|
||||
QOA is a recently-published lossy-but-tiny audio codec optimised for
|
||||
embedded/game-audio scenarios where MP3/Opus add too much complexity.
|
||||
It encodes 16-bit PCM to ~3.2 bits per sample using a fixed-size frame
|
||||
layout (~6× compression) with no perceptual model — making it cheap
|
||||
to decode and stream. It is included for the game-audio /
|
||||
embedded use case.
|
||||
|
||||
Implementation delegates to [`createWasmCodec`](./wasm-base.md) for the
|
||||
shared lazy-load, error-wrapping, and missing-module behaviour.
|
||||
|
||||
## Features
|
||||
|
||||
- `name === "qoa"`, extensions `["qoa"]`, MIME types `["audio/qoa"]`.
|
||||
- `canDecode(bytes)` — magic-byte sniff for `qoaf`
|
||||
(`0x71 0x6f 0x61 0x66`) at byte 0; rejects buffers shorter than 4
|
||||
bytes.
|
||||
- Decode + encode both supported (`supportsEncode: true`).
|
||||
- Streaming factory absent — single-pass full-buffer codec for now;
|
||||
QOA's fixed-frame structure means a streaming variant is feasible
|
||||
as a future extension.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { QoaCodec } from "@suaveplan/audio-codecs/codecs/qoa";
|
||||
|
||||
const decoded = await QoaCodec.decode(qoaBytes);
|
||||
const reEncoded = await QoaCodec.encode(decoded.channelData, {
|
||||
sampleRate: decoded.sampleRate,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Use QOA as a compact game-audio sprite format:
|
||||
|
||||
```ts
|
||||
import { CodecRegistry, QoaCodec, WavCodec } from "@suaveplan/audio-codecs";
|
||||
|
||||
const registry = new CodecRegistry({ codecs: [WavCodec, QoaCodec] });
|
||||
|
||||
// Compress a sound-effect bank from WAV to QOA for shipping.
|
||||
const wavDoc = await registry.decode(wavBytes);
|
||||
const qoaBytes = await registry.encode(wavDoc, "qoa");
|
||||
// Approximately 6× smaller than the input WAV.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `QoaCodec: AudioCodec` — frozen constant.
|
||||
- `canDecode(bytes)`, `decode(bytes, opts?)`, `encode(channelData,
|
||||
opts?)` — see [`wasm-base.md`](./wasm-base.md).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- QOA's `qoaf` magic is unambiguous — no other audio format starts
|
||||
with these bytes, so detect-then-dispatch is reliable.
|
||||
- The first decode/encode call lazily loads `@audio/decode-qoa`;
|
||||
subsequent calls reuse the memoised module.
|
||||
- Missing `@audio/decode-qoa` → `AUDIO_CODECS.NOT_SUPPORTED`.
|
||||
- QOA's fixed-frame structure (256 samples per frame, 8-byte header
|
||||
per channel slice) makes it amenable to a streaming-decoder
|
||||
factory — adding one requires only a per-frame buffer drain and
|
||||
is a future extension.
|
||||
- Quality vs MP3/Opus: QOA is lower fidelity at equal bitrate, but
|
||||
has zero entropy coding so decode is bounded constant-time per
|
||||
sample. The trade-off favours embedded / low-resource scenarios.
|
||||
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.
|
||||
150
docs/media/audio/audio-codecs/src/codecs/wav.md
Normal file
150
docs/media/audio/audio-codecs/src/codecs/wav.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# `codecs/wav`
|
||||
|
||||
Native pure-JS WAV codec implementing the full `AudioCodec` contract —
|
||||
decode, encode, and both streaming factories — without any external
|
||||
dependency.
|
||||
|
||||
## Purpose
|
||||
|
||||
WAV is the round-trip canonical format for `audio-codecs`. Every other
|
||||
codec is allowed to be decode-only or to delegate to a WASM module; the
|
||||
WAV codec is the one path callers can rely on to be present, complete,
|
||||
and dependency-free. It also serves as the worked example of the
|
||||
`AudioCodec` plugin contract for downstream integrators.
|
||||
|
||||
The implementation is a minimal RIFF parser plus a sample converter that
|
||||
handles every common WAV sample format: 8-bit unsigned PCM, 16-bit
|
||||
signed PCM, 24-bit signed PCM, 32-bit signed PCM, and 32-bit IEEE 754
|
||||
float. Decoding normalises every format into `Float32Array[]` in `[-1, 1]`;
|
||||
encoding inverts the conversion respecting the requested bit depth and
|
||||
storage format (PCM int vs IEEE float).
|
||||
|
||||
## Features
|
||||
|
||||
- `WavCodec.canDecode(bytes)` — magic-byte sniff: returns `true` iff the
|
||||
buffer is at least 12 bytes and starts with `RIFF` … `WAVE`.
|
||||
- `WavCodec.decode(bytes)` — parses the RIFF header, walks the chunk list
|
||||
for `fmt ` and `data`, decodes the PCM, and returns a `DecodeResult`
|
||||
with `channelData`, `sampleRate`, and `format: "wav"`.
|
||||
- `WavCodec.encode(channelData, opts?)` — emits a complete RIFF WAV from
|
||||
PCM channels at the requested `sampleRate`, `bitDepth`, and
|
||||
`format: "pcm" | "float"`. Defaults: 44_100 Hz, 16-bit PCM.
|
||||
- `WavCodec.createDecoder()` — streaming decoder that buffers incoming
|
||||
chunks until the header is fully parsed, then emits one
|
||||
`DecodeResult` per `write()` call that brings new full frames.
|
||||
- `WavCodec.createEncoder(opts?)` — streaming encoder that records sample
|
||||
rate + channel count at `start()`, accumulates PCM blocks via
|
||||
`write()`, and writes the complete RIFF header followed by
|
||||
concatenated samples at `flush()` once the total frame count is known.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { WavCodec } from "@suaveplan/audio-codecs/codecs/wav";
|
||||
|
||||
// decode bytes
|
||||
const decoded = await WavCodec.decode(wavBytes);
|
||||
console.log(decoded.sampleRate, decoded.channelData[0].length);
|
||||
|
||||
// re-encode at 24-bit PCM
|
||||
const encoded = await WavCodec.encode(decoded.channelData, {
|
||||
sampleRate: decoded.sampleRate,
|
||||
bitDepth: 24,
|
||||
format: "pcm",
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Streaming decode of a chunked WAV byte source:
|
||||
|
||||
```ts
|
||||
import { WavCodec } from "@suaveplan/audio-codecs/codecs/wav";
|
||||
|
||||
const decoder = WavCodec.createDecoder!();
|
||||
const pcm: Float32Array[] = [];
|
||||
|
||||
for await (const chunk of fetchAudio()) {
|
||||
for (const result of decoder.write(chunk)) {
|
||||
for (let c = 0; c < result.channelData.length; c++) {
|
||||
pcm[c] ??= new Float32Array(0);
|
||||
const merged = new Float32Array(pcm[c]!.length + result.channelData[c]!.length);
|
||||
merged.set(pcm[c]!, 0);
|
||||
merged.set(result.channelData[c]!, pcm[c]!.length);
|
||||
pcm[c] = merged;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const tail of decoder.flush()) {
|
||||
// append remaining
|
||||
}
|
||||
decoder.close();
|
||||
```
|
||||
|
||||
Streaming encode against a custom PCM block source:
|
||||
|
||||
```ts
|
||||
const encoder = WavCodec.createEncoder!({
|
||||
sampleRate: 48_000,
|
||||
bitDepth: 32,
|
||||
format: "float",
|
||||
});
|
||||
const header = encoder.start(48_000, 2);
|
||||
yield header; // empty for WAV — header is written at flush
|
||||
for (const block of pcmBlocks) {
|
||||
yield encoder.write(block);
|
||||
}
|
||||
yield encoder.flush();
|
||||
encoder.close();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `WavCodec: AudioCodec` — frozen constant; `name === "wav"`, `extensions
|
||||
=== ["wav", "wave"]`, `mimeTypes === ["audio/wav", "audio/wave",
|
||||
"audio/x-wav"]`.
|
||||
- `canDecode(bytes: Uint8Array): boolean`
|
||||
- `decode(bytes: Uint8Array, opts?: DecodeOpts): Promise<DecodeResult>` —
|
||||
throws `AudioCodecsError("AUDIO_CODECS.DECODE_FAILED")` on bad magic,
|
||||
missing `fmt ` chunk, missing `data` chunk, truncated `data`, fmt size
|
||||
< 16, or unsupported `bitsPerSample` (`bps === 0` or `bps > 4`).
|
||||
- `encode(channelData, opts?: EncodeOpts): Promise<Uint8Array>` — throws
|
||||
`AudioCodecsError("AUDIO_CODECS.ENCODE_FAILED")` if `channelData` is
|
||||
empty. Defaults: `sampleRate: 44_100`, `bitDepth: 16`, integer PCM.
|
||||
- `createDecoder(opts?: DecodeOpts): StreamingDecoder`
|
||||
- `createEncoder(opts?: EncodeOpts): StreamingEncoder` — the encoder
|
||||
buffers all body bytes until `flush()` because the WAV header carries
|
||||
the total `data` chunk size as a uint32, which is only known at the
|
||||
end of the stream.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The RIFF chunk walker tolerates and skips any chunk whose ID is neither
|
||||
`fmt ` nor `data` — `LIST`, `bext`, `iXML`, and other metadata chunks
|
||||
are passed over silently. This matches the behaviour every real WAV
|
||||
consumer expects.
|
||||
- 24-bit PCM uses three signed-magnitude bytes per sample in
|
||||
little-endian order; the decoder reads them via two unsigned bytes
|
||||
and one signed byte to preserve the sign bit cleanly without
|
||||
bit-twiddling intermediates that could lose precision on edge
|
||||
values.
|
||||
- The normalisation factor is `(1 << (bitsPerSample - 1)) >>> 0` — the
|
||||
unsigned-shift forces a `number` rather than a 32-bit signed integer
|
||||
for the 32-bit PCM case where `1 << 31` would otherwise be negative.
|
||||
- Encode clamps every sample to `[-1, 1]` via `Math.max(-1, Math.min(1,
|
||||
sample))` and rounds via `Math.round` to the nearest integer for the
|
||||
three integer bit depths. The 32-bit float path writes the sample
|
||||
directly — clamping is the caller's responsibility for float storage.
|
||||
- The streaming encoder's `start()` returns an empty `Uint8Array` because
|
||||
the RIFF header includes a `data` chunk size field whose value is the
|
||||
total written sample bytes; that value is only known at `flush()`
|
||||
time. Accordingly the encoder buffers every `write()` output into a
|
||||
chunk array and emits one concatenated payload (header + body) at
|
||||
`flush()`. Memory cost is the full PCM size — callers needing constant
|
||||
memory streaming should use a different format (e.g. raw float headers
|
||||
or Opus).
|
||||
- The streaming decoder's `lenient: true` flag on the shared
|
||||
`parseHeader` helper accepts an under-sized buffer (where the declared
|
||||
`data` chunk extends past the bytes received so far). The non-streaming
|
||||
`decode` path passes the default `lenient: false` so it still validates
|
||||
total payload size.
|
||||
74
docs/media/audio/audio-codecs/src/codecs/webm.md
Normal file
74
docs/media/audio/audio-codecs/src/codecs/webm.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# `codecs/webm`
|
||||
|
||||
Lazy WebM-audio codec — decode-only wrapper over `@audio/decode-webm`
|
||||
WASM module loaded on first use.
|
||||
|
||||
## Purpose
|
||||
|
||||
WebM is an open container format used primarily for video on the web,
|
||||
but with an audio-only profile that wraps Opus or Vorbis payloads in
|
||||
the EBML matroska-derived structure. This codec extracts the audio
|
||||
track from WebM containers and decodes it to PCM.
|
||||
|
||||
The codec is decode-only because WebM-audio is almost always a
|
||||
*receive* format (downloaded from a server), not something callers
|
||||
need to emit. The canonical audio encode targets are WAV,
|
||||
FLAC, and Opus. Implementation delegates to
|
||||
[`createWasmCodec`](./wasm-base.md) for the shared lazy-load and
|
||||
error-wrapping behaviour.
|
||||
|
||||
## Features
|
||||
|
||||
- `name === "webm"`, extensions `["webm"]`, MIME types
|
||||
`["audio/webm"]`.
|
||||
- `canDecode(bytes)` — magic-byte sniff for the EBML header
|
||||
`0x1a 0x45 0xdf 0xa3` at byte 0; rejects buffers shorter than 4
|
||||
bytes.
|
||||
- Decode supported via `@audio/decode-webm`.
|
||||
- `supportsEncode: false` — encode throws `NOT_SUPPORTED`.
|
||||
- Streaming factory absent.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { WebMCodec } from "@suaveplan/audio-codecs/codecs/webm";
|
||||
|
||||
const decoded = await WebMCodec.decode(webmBytes);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CodecRegistry, WebMCodec, WavCodec } from "@suaveplan/audio-codecs";
|
||||
|
||||
const registry = new CodecRegistry({ codecs: [WebMCodec, WavCodec] });
|
||||
|
||||
// Extract audio from a WebM upload and re-encode as WAV for analysis.
|
||||
const webmDoc = await registry.decode(webmBytes);
|
||||
const wavBytes = await registry.encode(webmDoc, "wav", { bitDepth: 24 });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `WebMCodec: AudioCodec` — frozen constant; decode-only.
|
||||
- `canDecode(bytes)`, `decode(bytes, opts?)` — standard `AudioCodec`
|
||||
contract.
|
||||
- `encode()` — always throws `AUDIO_CODECS.NOT_SUPPORTED`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- WebM uses EBML, not RIFF, so the magic-byte signature is
|
||||
fundamentally different from other audio containers. The four-byte
|
||||
EBML header is a hard signal — false positives are rare in
|
||||
practice.
|
||||
- The codec returns PCM regardless of whether the underlying audio
|
||||
track is Opus or Vorbis — `@audio/decode-webm` handles the inner
|
||||
codec dispatch.
|
||||
- WebM containers commonly carry video alongside audio; this codec
|
||||
decodes only the audio track. Video handling is out of scope and
|
||||
belongs to a separate `@suaveplan/video-*` package family.
|
||||
- Decode-only by design; first decode lazily loads
|
||||
`@audio/decode-webm`. Subsequent calls reuse the memoised module.
|
||||
- Missing `@audio/decode-webm` → `AUDIO_CODECS.NOT_SUPPORTED`.
|
||||
- Streaming factory absent — `decodeStream("webm", …)` throws
|
||||
`AUDIO_CODECS.STREAM_ERROR`.
|
||||
Reference in New Issue
Block a user