first commit
This commit is contained in:
81
docs/media/audio/audio-decode-wav/README.md
Normal file
81
docs/media/audio/audio-decode-wav/README.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# @suaveplan/audio-decode-wav - Universal WAV decoder wrapper over @audio/decode-wav. Exposes decodeWav, createWavStreamDecoder, frozen DecodedAudio, AbortSignal propagation, AUDIO_DECODE.WAV.* error codes, and a full OpenTelemetry surface.
|
||||
|
||||
Universal WAV decoder wrapper over the upstream `@audio/decode-wav` library. Exposes a stable, OTel-instrumented, AbortSignal-honouring contract over a frozen, validated `DecodedAudio` shape that every sibling codec wrapper (`audio-decode-mp3`, `audio-decode-flac`, `audio-decode-opus`, …) will reuse. The package is part of the `audio-decode-*` family and
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [decode-wav](./src/decode-wav/decode-wav.md) — `decodeWav(bytes, options?)` is the headline entry-point of `@suaveplan/audio-decode-wav`: it takes a `Uint8Array` of W…
|
||||
- [decode-wav](./src/decode-wav/decode-wav.md) — `decodeWav(bytes, options?)` is the headline entry-point of `@suaveplan/audio-decode-wav`: it takes a `Uint8Array` of W…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module is the canonical registry of every error code raised by `@suaveplan/audio-decode-wav`, alongside the struct…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — This module is the canonical registry of every error code raised by `@suaveplan/audio-decode-wav`, alongside the struct…
|
||||
- [otel](./src/otel/otel.md) — This module wires `@suaveplan/audio-decode-wav` into OpenTelemetry: it builds (and caches) the tracer, meter, counters…
|
||||
- [otel](./src/otel/otel.md) — This module wires `@suaveplan/audio-decode-wav` into OpenTelemetry: it builds (and caches) the tracer, meter, counters…
|
||||
- [stream](./src/stream/stream.md) — `createWavStreamDecoder(options?)` returns a `WavStreamDecoder` for callers who receive WAV bytes incrementally — `Read…
|
||||
- [stream](./src/stream/stream.md) — `createWavStreamDecoder(options?)` returns a `WavStreamDecoder` for callers who receive WAV bytes incrementally — `Read…
|
||||
- [decoded-audio](./src/types/decoded-audio.md) — `DecodedAudio` is the canonical shape every WAV decode in `@suaveplan/audio-decode-wav` resolves to. It is a frozen, va…
|
||||
- [options](./src/types/options.md) — `AudioDecodeOptions` is the option bag accepted by every public entry-point in `@suaveplan/audio-decode-wav` (`decodeWa…
|
||||
- [utils](./src/utils/inspect.md) — `inspectWavHeader(bytes)` is the wrapper-layer pre-scan that classifies a WAV byte buffer **before** the upstream `@aud…
|
||||
- [inspect](./src/utils/inspect.md) — `inspectWavHeader(bytes)` is the wrapper-layer pre-scan that classifies a WAV byte buffer **before** the upstream `@aud…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/audio-decode-wav
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { decodeWav } from "@suaveplan/audio-decode-wav";
|
||||
|
||||
const audio = await decodeWav(wavBytes);
|
||||
console.log(audio.channels, audio.sampleRate, audio.durationSec);
|
||||
```
|
||||
|
||||
Stream a WAV from an HTTP response:
|
||||
|
||||
```ts
|
||||
import { createWavStreamDecoder } from "@suaveplan/audio-decode-wav";
|
||||
|
||||
const dec = createWavStreamDecoder();
|
||||
const reader = response.body!.getReader();
|
||||
try {
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
await dec.feed(value);
|
||||
}
|
||||
const audio = await dec.flush();
|
||||
} finally {
|
||||
dec.free();
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Whole-buffer decode via `decodeWav(bytes, options?)`.
|
||||
- Chunked decode via `createWavStreamDecoder(options?)`.
|
||||
- Seven error codes: `AUDIO_DECODE.WAV.INVALID_INPUT`, `UNSUPPORTED_FORMAT`, `CORRUPT_HEADER`, `TRUNCATED_BODY`, `ABORTED`, `RESAMPLE_UNSUPPORTED`, `STREAM_INVARIANT_VIOLATION`.
|
||||
- Full OpenTelemetry surface — `audio_decode_wav.bytes_in_total`, `audio_decode_wav.frames_out_total`, `audio_decode_wav.errors_total{code}`, `audio_decode_wav.op_duration_ms{op, outcome}`, plus spans on the `@suaveplan/audio-decode-wav` tracer.
|
||||
- AbortSignal propagation, including pre-aborted short-circuit.
|
||||
- Zod-validated `DecodedAudio` + `AudioDecodeOptions` schemas.
|
||||
- Header pre-scan distinguishes corrupt vs. truncated vs. unsupported inputs before invoking upstream.
|
||||
|
||||
## Modules
|
||||
|
||||
- [`decode-wav`](./src/decode-wav/decode-wav.md) — whole-buffer decoder.
|
||||
- [`stream`](./src/stream/stream.md) — chunked streaming decoder.
|
||||
- [`error-codes`](./src/error-codes/error-codes.md) — code registry + `AudioDecodeWavError`.
|
||||
- [`otel`](./src/otel/otel.md) — telemetry surface (tracer, meter, counters, histogram).
|
||||
- [`types/decoded-audio`](./src/types/decoded-audio.md) — frozen `DecodedAudio` type + schema.
|
||||
- [`types/options`](./src/types/options.md) — `AudioDecodeOptions` type + schema.
|
||||
- [`utils/inspect`](./src/utils/inspect.md) — pure-JS WAV header pre-scan.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Resampling — surfaced as `RESAMPLE_UNSUPPORTED`; a future `@suaveplan/audio-resample` will pair with this package.
|
||||
- Non-WAV formats — sibling `audio-decode-{mp3,flac,opus,…}` packages each wrap their own upstream decoder behind the same contract.
|
||||
- Format dispatching — owned by the upcoming `@suaveplan/audio-decode` umbrella package.
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# `decode-wav`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`decodeWav(bytes, options?)` is the headline entry-point of `@suaveplan/audio-decode-wav`: it takes a `Uint8Array` of WAV bytes and resolves to a frozen, validated `DecodedAudio`. It wraps the upstream `@audio/decode-wav` library behind a stable, OTel-instrumented, AbortSignal-honouring contract that classifies failures into seven distinct error codes — every dimension a downstream consumer (starting with `@suaveplan/audio-ir-loader`) might want to react to. It is the wrapper-layer guarantee that callers can treat WAV the same way they will treat MP3, FLAC and Opus when those sibling packages land.
|
||||
|
||||
## Features
|
||||
|
||||
- Pre-screens the buffer via `inspectWavHeader` so `CORRUPT_HEADER`, `TRUNCATED_BODY`, and `UNSUPPORTED_FORMAT` are surfaced before the upstream library is invoked.
|
||||
- Returns `DecodedAudio` validated by `DecodedAudioSchema` and frozen by `freezeDecodedAudio`.
|
||||
- Honours `AbortSignal` synchronously at entry; mid-decode aborts surface as `ABORTED` with the original `AbortError` on `cause`.
|
||||
- Honours `targetSampleRate`: a no-op when it matches the decoded rate, raises `RESAMPLE_UNSUPPORTED` carrying both `decodedSampleRate` and `requestedSampleRate` when it does not.
|
||||
- Emits OTel telemetry on every call — `audio_decode_wav.bytes_in_total`, `audio_decode_wav.frames_out_total`, `audio_decode_wav.errors_total{code}`, `audio_decode_wav.op_duration_ms{op, outcome}`, plus a span per call on the `@suaveplan/audio-decode-wav` tracer.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { decodeWav } from "@suaveplan/audio-decode-wav";
|
||||
|
||||
const audio = await decodeWav(wavBytes);
|
||||
console.log(audio.channels, audio.sampleRate, audio.durationSec);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { AUDIO_DECODE_WAV_CODES, decodeWav } from "@suaveplan/audio-decode-wav";
|
||||
|
||||
async function decodeOrResample(bytes: Uint8Array, target: number) {
|
||||
try {
|
||||
return await decodeWav(bytes, {
|
||||
targetSampleRate: target,
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof Error &&
|
||||
(err as { code?: string }).code ===
|
||||
AUDIO_DECODE_WAV_CODES.RESAMPLE_UNSUPPORTED
|
||||
) {
|
||||
const native = await decodeWav(bytes);
|
||||
return resampleTo(native, target);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `decodeWav(bytes: Uint8Array, options?: AudioDecodeOptions): Promise<DecodedAudio>` — main entry-point.
|
||||
- **Throws** `AudioDecodeWavError` with `code === "AUDIO_DECODE.WAV.INVALID_INPUT"` for empty buffer.
|
||||
- **Throws** `AUDIO_DECODE.WAV.ABORTED` for pre- or mid-decode abort; `cause` carries the originating `AbortError` reason.
|
||||
- **Throws** `AUDIO_DECODE.WAV.CORRUPT_HEADER` for missing magic, malformed `fmt`, no `data` chunk.
|
||||
- **Throws** `AUDIO_DECODE.WAV.TRUNCATED_BODY` when the `data` chunk declares more bytes than the buffer holds. Error carries `bytesAvailable` + `bytesDeclared`.
|
||||
- **Throws** `AUDIO_DECODE.WAV.UNSUPPORTED_FORMAT` for A-law, μ-law, ADPCM variants, WAVE_FORMAT_EXTENSIBLE. Error carries `format`.
|
||||
- **Throws** `AUDIO_DECODE.WAV.RESAMPLE_UNSUPPORTED` when `targetSampleRate` differs from the decoded rate. Error carries both rates.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The header pre-scan is essential because the upstream library's failure mode is undifferentiated — `TypeError("Not a WAV file")`, `TypeError("Unsupported WAV format")`, and an out-of-bounds read on a truncated body all surface as plain errors. We classify them in pure JS before calling upstream, which makes the wrapper's error contract trivially testable and avoids cascading exceptions from inside the library's binary-data DataView reads. `performance.now()` is used for the duration histogram because it is universally available across Node, Bun and the browser; the value is monotonic so the histogram's `outcome` partition is meaningful even under clock skew. The function deliberately does **not** clip the decoded samples — upstream PCM normalisation produces values in `[-1, 1]` for well-formed integer PCM and float samples can legitimately exceed that range, so clipping would be lossy.
|
||||
@@ -0,0 +1,66 @@
|
||||
# `error-codes`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module is the canonical registry of every error code raised by `@suaveplan/audio-decode-wav`, alongside the structured `AudioDecodeWavError` class that carries discriminants for filtering. The registry is the contract — every code is reachable from a real call site exercised by the test suite, per `AGENTS.md` §2c. The error class extends `@suaveplan/error`'s `SuaveplanError` so it threads through the repo-wide error machinery (OTel span event adapter, redaction, i18n resolver, Zod-validated wire format) without bespoke wiring.
|
||||
|
||||
## Features
|
||||
|
||||
- Seven codes via `defineCodes("AUDIO_DECODE.WAV", […])`: `INVALID_INPUT`, `UNSUPPORTED_FORMAT`, `CORRUPT_HEADER`, `TRUNCATED_BODY`, `ABORTED`, `RESAMPLE_UNSUPPORTED`, `STREAM_INVARIANT_VIOLATION`.
|
||||
- `AudioDecodeWavError` carries six dimension-specific discriminants: `format` (for `UNSUPPORTED_FORMAT`), `decodedSampleRate` + `requestedSampleRate` (for `RESAMPLE_UNSUPPORTED`), `op` (every error), `bytesAvailable` + `bytesDeclared` (for `TRUNCATED_BODY`).
|
||||
- `AudioFormatType` union covers the WAV format codes recognised at the inspect layer: `pcm`, `ieee-float`, `alaw`, `mulaw`, `ima-adpcm`, `ms-adpcm`, `extensible`, `unknown`.
|
||||
- Registry is frozen at module load; `Object.values(AUDIO_DECODE_WAV_CODES)` is the authoritative list for exhaustion tests.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUDIO_DECODE_WAV_CODES,
|
||||
AudioDecodeWavError,
|
||||
} from "@suaveplan/audio-decode-wav";
|
||||
|
||||
throw new AudioDecodeWavError(
|
||||
AUDIO_DECODE_WAV_CODES.INVALID_INPUT,
|
||||
"WAV byte buffer is empty",
|
||||
{ op: "decodeWav" }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
AUDIO_DECODE_WAV_CODES,
|
||||
AudioDecodeWavError,
|
||||
decodeWav,
|
||||
} from "@suaveplan/audio-decode-wav";
|
||||
|
||||
try {
|
||||
await decodeWav(bytes, { targetSampleRate: 48000 });
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof AudioDecodeWavError &&
|
||||
err.code === AUDIO_DECODE_WAV_CODES.RESAMPLE_UNSUPPORTED
|
||||
) {
|
||||
console.warn(
|
||||
`cannot decode-and-resample: ${err.decodedSampleRate} → ${err.requestedSampleRate}`
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `const AUDIO_DECODE_WAV_CODES` — `{ INVALID_INPUT: "AUDIO_DECODE.WAV.INVALID_INPUT", … }`. Frozen.
|
||||
- `type AudioDecodeWavErrorCode` — union of every code's string value.
|
||||
- `type AudioFormatType` — `"pcm" | "ieee-float" | "alaw" | "mulaw" | "ima-adpcm" | "ms-adpcm" | "extensible" | "unknown"`.
|
||||
- `interface AudioDecodeWavErrorOptions extends SuaveplanErrorOptions` — adds `format`, `decodedSampleRate`, `requestedSampleRate`, `op`, `bytesAvailable`, `bytesDeclared`.
|
||||
- `class AudioDecodeWavError extends SuaveplanError<AudioDecodeWavErrorCode>` — exposes every option above as a readonly field; `name === "AudioDecodeWavError"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry uses `defineCodes` so the code-string format is uniform across the project (`<namespace>.<reason>`), making OTel attribute filtering by code prefix trivial. The error class follows the same discriminant-surface pattern as `BundleUriError` in `@suaveplan/storage-uri-plugin-bundle` — every dimension a downstream observer might filter on is a typed readonly field rather than buried in the message string. `RESAMPLE_UNSUPPORTED` deliberately carries both rates so the failing call can be replayed against a downstream resampler without re-decoding to discover the source rate.
|
||||
68
docs/media/audio/audio-decode-wav/src/otel/otel.md
Normal file
68
docs/media/audio/audio-decode-wav/src/otel/otel.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# `otel`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
This module wires `@suaveplan/audio-decode-wav` into OpenTelemetry: it builds (and caches) the tracer, meter, counters and histogram the package emits, supports swapping the meter provider for tests, and exposes the two convenience recorders (`recordSuccess`, `recordError`) used by the decoder and the stream wrapper. Every public call in the package routes its telemetry through one of those two functions so the spec's `audio_decode_wav.*` instrument names are emitted from exactly one place.
|
||||
|
||||
## Features
|
||||
|
||||
- `setAudioDecodeWavMeterProvider(provider)` overrides the meter provider; pass `undefined` to revert to the global. Resets the instrument cache so the next access binds to the new provider — essential for tests that snapshot per-test metrics.
|
||||
- Lazy-built, cached instruments via `getInstruments(force?)` — first call builds, subsequent calls return the cache unless `force === true`.
|
||||
- Three counters (`bytes_in_total`, `frames_out_total`, `errors_total`) and one histogram (`op_duration_ms`) on the `@suaveplan/audio-decode-wav` meter.
|
||||
- Tracer spans on the `@suaveplan/audio-decode-wav` tracer, named `audio_decode_wav.<op>` where `op ∈ {"decodeWav", "stream.feed", "stream.flush"}`.
|
||||
- `recordSuccess(input)` and `recordError(input)` are the canonical emit points — both increment the duration histogram, the relevant counter, and start/end a span.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { getInstruments, recordSuccess } from "@suaveplan/audio-decode-wav";
|
||||
|
||||
recordSuccess({
|
||||
op: "decodeWav",
|
||||
bytesIn: 1024,
|
||||
framesOut: 256,
|
||||
durationMs: 1.2,
|
||||
});
|
||||
|
||||
const inst = getInstruments();
|
||||
inst.tracer.startSpan("custom.span").end();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
resetInstruments,
|
||||
setAudioDecodeWavMeterProvider,
|
||||
} from "@suaveplan/audio-decode-wav";
|
||||
|
||||
const counters: Array<{ name: string; value: number }> = [];
|
||||
setAudioDecodeWavMeterProvider({
|
||||
getMeter: () => ({
|
||||
createCounter: (name) => ({ add: (v) => counters.push({ name, value: v }) }),
|
||||
createHistogram: () => ({ record: () => {} }),
|
||||
}),
|
||||
} as never);
|
||||
resetInstruments();
|
||||
// run decode(), then assert against `counters`
|
||||
setAudioDecodeWavMeterProvider(undefined);
|
||||
resetInstruments();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `setAudioDecodeWavMeterProvider(provider: MeterProvider | undefined): void` — override the meter provider; clears the instrument cache.
|
||||
- `getInstruments(force?: boolean): AudioDecodeWavInstruments` — return the cached instruments (or rebuild if `force`).
|
||||
- `resetInstruments(): void` — drop the cache; next `getInstruments` rebuilds.
|
||||
- `recordSuccess(input: SuccessOpInput): void` — emit duration + bytes_in + frames_out + span for a successful op.
|
||||
- `recordError(input: ErrorOpInput): void` — emit duration + errors_total + span for a failed op.
|
||||
- `type AudioDecodeWavOp = "decodeWav" | "stream.feed" | "stream.flush"`.
|
||||
- `type AudioDecodeWavOutcome = "success" | "error"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The cache is module-scoped — there is exactly one set of instruments per process. Tests swap the provider via `setAudioDecodeWavMeterProvider` and call `resetInstruments()` to force a rebuild; the override pointer is read inside `getInstruments` so the swap takes effect on the next access. Span attributes include `audio_decode_wav.outcome` and either `audio_decode_wav.bytes_in`/`audio_decode_wav.frames_out` (success) or `audio_decode_wav.code` (error); these mirror the metric attribute keys so an OTel collector can join spans and metrics on the same dimension.
|
||||
68
docs/media/audio/audio-decode-wav/src/stream/stream.md
Normal file
68
docs/media/audio/audio-decode-wav/src/stream/stream.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# `stream`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`createWavStreamDecoder(options?)` returns a `WavStreamDecoder` for callers who receive WAV bytes incrementally — `ReadableStream<Uint8Array>`, an HTTP chunked response, a sliding window over a large file. The decoder accumulates samples across `feed()` calls and produces a frozen `DecodedAudio` on `flush()`. It wraps the upstream `@audio/decode-wav` incremental decoder behind the same OTel-instrumented contract as `decodeWav`, with the additional `STREAM_INVARIANT_VIOLATION` code for misuse (feeding after `free()`, flushing before any samples were decoded).
|
||||
|
||||
## Features
|
||||
|
||||
- `feed(chunk)` accepts arbitrary `Uint8Array` chunks — header bytes are buffered internally until the upstream `data` chunk is observed, then samples start streaming.
|
||||
- `flush()` resolves to a frozen `DecodedAudio` whose `channelData` is the concatenation of every sample fed since construction (or since the previous `flush()` if you choose to dispose between flushes).
|
||||
- `free()` releases the upstream decoder; idempotent. Any subsequent `feed()` raises `STREAM_INVARIANT_VIOLATION`.
|
||||
- Honours `AbortSignal` on every `feed()` call.
|
||||
- Honours `targetSampleRate` from `flush()` — raises `RESAMPLE_UNSUPPORTED` carrying both rates if it differs from the upstream-detected sample rate.
|
||||
- Emits the same OTel telemetry surface as `decodeWav` with `op ∈ {"stream.feed", "stream.flush"}`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { createWavStreamDecoder } from "@suaveplan/audio-decode-wav";
|
||||
|
||||
const decoder = createWavStreamDecoder();
|
||||
try {
|
||||
for (const chunk of chunks) {
|
||||
await decoder.feed(chunk);
|
||||
}
|
||||
const audio = await decoder.flush();
|
||||
console.log(audio.channels, audio.sampleRate, audio.bytesDecoded);
|
||||
} finally {
|
||||
decoder.free();
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { createWavStreamDecoder } from "@suaveplan/audio-decode-wav";
|
||||
|
||||
async function decodeFromResponse(res: Response, signal?: AbortSignal) {
|
||||
const dec = createWavStreamDecoder({ signal: signal ?? new AbortController().signal });
|
||||
const reader = res.body!.getReader();
|
||||
try {
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
await dec.feed(value);
|
||||
}
|
||||
return await dec.flush();
|
||||
} finally {
|
||||
dec.free();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createWavStreamDecoder(options?: AudioDecodeOptions): WavStreamDecoder`.
|
||||
- `interface WavStreamDecoder` —
|
||||
- `feed(chunk: Uint8Array): Promise<void>` — accumulate bytes. Throws `STREAM_INVARIANT_VIOLATION` after `free()`, `ABORTED` for pre-aborted signal, `CORRUPT_HEADER` for upstream failures.
|
||||
- `flush(): Promise<DecodedAudio>` — produce the cumulative decoded audio. Throws `RESAMPLE_UNSUPPORTED` for mismatched `targetSampleRate`; throws `STREAM_INVARIANT_VIOLATION` if no samples have been decoded yet.
|
||||
- `free(): void` — release the upstream decoder. Idempotent.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The wrapper appends each upstream segment's channels to the accumulator by allocating a fresh `Float32Array(prev + incoming)` per channel; this is `O(n)` per flush rather than `O(n log n)`, which is fine for the realistic case where `flush()` is called once at end-of-stream. Callers that want true streaming output (emitting samples to an output device as they decode) should listen to `feed()` results in a future iteration of this API — the current contract is "feed until end, flush once". The upstream library exposes its own `flush()` and `free()`, but it does not surface partial-decode segments separately; we accumulate at the wrapper layer so the wrapper's `DecodedAudio` invariant holds end-to-end. `bytesDecoded` is the sum of every `chunk.byteLength` passed to `feed()`, not just the decoded body — this lets observers correlate input throughput with frame output.
|
||||
57
docs/media/audio/audio-decode-wav/src/types/decoded-audio.md
Normal file
57
docs/media/audio/audio-decode-wav/src/types/decoded-audio.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# `types/decoded-audio`
|
||||
|
||||
## Purpose
|
||||
|
||||
`DecodedAudio` is the canonical shape every WAV decode in `@suaveplan/audio-decode-wav` resolves to. It is a frozen, validated record carrying PCM samples normalised to `[-1.0, 1.0]`, plus the dimensions every downstream consumer needs to dispatch on (sample rate, channel count, duration in seconds, raw input byte length). The shape is intentionally identical to what the future MP3/FLAC/Opus sibling packages will return, so format-dispatching callers (`@suaveplan/audio-decode`, the upcoming umbrella) can treat every codec uniformly. The schema is exposed alongside the type so callers and tests can validate inputs they did not produce themselves.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen, validated `DecodedAudio` interface — `channelData: readonly Float32Array[]`, `sampleRate`, `channels`, `durationSec`, `bytesDecoded`.
|
||||
- Zod schema (`DecodedAudioSchema`) enforces: at least one channel, every channel same length, `channels === channelData.length`, sample rate is a positive integer up to `MAX_SAMPLE_RATE` (384 kHz), channel count up to `MAX_CHANNELS` (64), duration is non-negative and finite, decoded byte count is a non-negative integer.
|
||||
- `freezeDecodedAudio(audio)` helper deep-freezes the record + channel array so consumers cannot mutate it after the wrapper hands it back.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
type DecodedAudio,
|
||||
DecodedAudioSchema,
|
||||
freezeDecodedAudio,
|
||||
} from "@suaveplan/audio-decode-wav";
|
||||
|
||||
const raw: DecodedAudio = {
|
||||
channelData: [new Float32Array([0, 0.5, 0, -0.5])],
|
||||
sampleRate: 8000,
|
||||
channels: 1,
|
||||
durationSec: 0.0005,
|
||||
bytesDecoded: 44 + 8,
|
||||
};
|
||||
|
||||
DecodedAudioSchema.parse(raw);
|
||||
const frozen = freezeDecodedAudio(raw);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { DecodedAudioSchema, MAX_CHANNELS } from "@suaveplan/audio-decode-wav";
|
||||
|
||||
function assertConformant(value: unknown): asserts value is import("@suaveplan/audio-decode-wav").DecodedAudio {
|
||||
const parsed = DecodedAudioSchema.parse(value);
|
||||
if (parsed.channels > MAX_CHANNELS) {
|
||||
throw new Error("multichannel format exceeds wrapper budget");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface DecodedAudio` — see fields above. All fields readonly.
|
||||
- `const DecodedAudioSchema: ZodSchema` — runtime validator. Throws `ZodError` on invariant violations.
|
||||
- `function freezeDecodedAudio(audio: DecodedAudio): DecodedAudio` — freezes the outer record and the channel array. Returns the same object.
|
||||
- `const MAX_SAMPLE_RATE = 384_000`.
|
||||
- `const MAX_CHANNELS = 64`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema deliberately enforces equal-length channels — the upstream decoder produces this invariant naturally, but we surface it as a hard schema rule so the wrapper's `DecodedAudio` contract is identical for callers regardless of source codec. `channelData` carries `Float32Array` instances directly rather than copying, so consumers should treat the result as immutable. The freeze step does not deep-freeze the underlying `Float32Array` buffers — typed arrays cannot be frozen in any meaningful way, but their `.length` is fixed and the wrapper never hands the same instance back from two distinct decode calls.
|
||||
46
docs/media/audio/audio-decode-wav/src/types/options.md
Normal file
46
docs/media/audio/audio-decode-wav/src/types/options.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# `types/options`
|
||||
|
||||
## Purpose
|
||||
|
||||
`AudioDecodeOptions` is the option bag accepted by every public entry-point in `@suaveplan/audio-decode-wav` (`decodeWav`, `createWavStreamDecoder`). It carries the universal cross-cutting concerns every codec wrapper in the `audio-decode-*` family needs to honour: an `AbortSignal` for cooperative cancellation and a `targetSampleRate` for declaring caller intent about resampling. The shape is intentionally tiny — anything format-specific belongs in a dedicated options bag, not here.
|
||||
|
||||
## Features
|
||||
|
||||
- `signal?: AbortSignal` — propagated synchronously by the decoder. Pre-aborted signals short-circuit before the upstream library is invoked, surfacing `AUDIO_DECODE.WAV.ABORTED`. Mid-decode aborts surface with the original `AbortError` on `cause`.
|
||||
- `targetSampleRate?: number` — declares the caller's intent. The decoder does NOT resample. Supplying a value that differs from the decoded sample rate raises `AUDIO_DECODE.WAV.RESAMPLE_UNSUPPORTED`, carrying both `decodedSampleRate` and `requestedSampleRate` discriminants so callers can wire a resampler step in.
|
||||
- Zod-validated at the wrapper boundary via `AudioDecodeOptionsSchema`. Invalid `targetSampleRate` values (non-positive, non-integer, exceeding `MAX_SAMPLE_RATE`) raise `ZodError` before the upstream library is touched.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { decodeWav } from "@suaveplan/audio-decode-wav";
|
||||
|
||||
const audio = await decodeWav(wavBytes, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { AudioDecodeOptionsSchema, decodeWav } from "@suaveplan/audio-decode-wav";
|
||||
|
||||
function decodeOrResample(bytes: Uint8Array, target: number) {
|
||||
AudioDecodeOptionsSchema.parse({ targetSampleRate: target });
|
||||
return decodeWav(bytes, { targetSampleRate: target }).catch((err) => {
|
||||
if (err.code === "AUDIO_DECODE.WAV.RESAMPLE_UNSUPPORTED") {
|
||||
return decodeWav(bytes).then((a) => resampleTo(a, target));
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface AudioDecodeOptions` — `{ signal?: AbortSignal; targetSampleRate?: number }`. All fields optional.
|
||||
- `const AudioDecodeOptionsSchema: ZodSchema` — runtime validator. Custom `signal` checker validates `instanceof AbortSignal` or `undefined`; `targetSampleRate` is a positive integer up to `MAX_SAMPLE_RATE`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schema uses Zod's `z.custom<AbortSignal>` rather than a structural check because `AbortSignal` is a host-defined interface — duck-typing it would defeat the purpose of validation. Browser, Node, and Bun all expose `AbortSignal` as a constructor with consistent semantics, so the runtime check works uniformly. Resampling is intentionally a separate concern (planned `@suaveplan/audio-resample`) so this wrapper stays focused on byte-to-Float32Array decoding; surfacing `RESAMPLE_UNSUPPORTED` keeps the option in the bag without expanding the package's responsibility surface.
|
||||
60
docs/media/audio/audio-decode-wav/src/utils/inspect.md
Normal file
60
docs/media/audio/audio-decode-wav/src/utils/inspect.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# `utils/inspect`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`inspectWavHeader(bytes)` is the wrapper-layer pre-scan that classifies a WAV byte buffer **before** the upstream `@audio/decode-wav` library is invoked. The upstream library throws an undifferentiated `TypeError` for "Not a WAV file", unsupported formats and missing chunks, so the wrapper's spec requirement to distinguish `CORRUPT_HEADER`, `TRUNCATED_BODY` and `UNSUPPORTED_FORMAT` is satisfied here, in pure JavaScript, without speculative decoding. The function returns a tagged-union result and never throws.
|
||||
|
||||
## Features
|
||||
|
||||
- Walks the RIFF chunk list (`fmt`, `data`, plus any auxiliary chunks) without allocating.
|
||||
- Distinguishes `corrupt` (RIFF/WAVE magic missing, fmt chunk too short, no data chunk, non-progressing chunk size, data-before-fmt) from `truncated` (data chunk declares more bytes than the buffer holds) from `unsupported` (format code recognised but non-PCM/non-IEEE-float).
|
||||
- Recognises seven WAV format codes: PCM (`0x0001`), MS-ADPCM (`0x0002`), IEEE float (`0x0003`), A-law (`0x0006`), μ-law (`0x0007`), IMA-ADPCM (`0x0011`), WAVE_FORMAT_EXTENSIBLE (`0xFFFE`). Unknown codes map to `format: "unknown"`.
|
||||
- Honours odd-byte padding inside the chunk walk (every chunk's `size & 1` adds a pad byte before the next chunk).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { inspectWavHeader } from "@suaveplan/audio-decode-wav";
|
||||
|
||||
const result = inspectWavHeader(wavBytes);
|
||||
if (result.kind === "ok") {
|
||||
console.log(result.sampleRate, result.channels, result.bitsPerSample);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { inspectWavHeader } from "@suaveplan/audio-decode-wav";
|
||||
|
||||
function summarise(bytes: Uint8Array): string {
|
||||
const r = inspectWavHeader(bytes);
|
||||
switch (r.kind) {
|
||||
case "ok":
|
||||
return `${r.channels} ch @ ${r.sampleRate} Hz, ${r.bitsPerSample}-bit, data=${r.dataBytesDeclared}`;
|
||||
case "truncated":
|
||||
return `truncated: ${r.bytesAvailable}/${r.bytesDeclared}`;
|
||||
case "unsupported":
|
||||
return `unsupported: ${r.format}`;
|
||||
case "corrupt":
|
||||
return `corrupt: ${r.reason}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `function inspectWavHeader(bytes: Uint8Array): InspectResult` — never throws.
|
||||
- `type InspectResult = InspectOk | InspectCorrupt | InspectTruncated | InspectUnsupported`.
|
||||
- `interface InspectOk` — `{ kind: "ok"; sampleRate; channels; bitsPerSample; dataStart; dataBytesDeclared }`.
|
||||
- `interface InspectCorrupt` — `{ kind: "corrupt"; reason: string }`.
|
||||
- `interface InspectTruncated` — `{ kind: "truncated"; bytesAvailable; bytesDeclared }`.
|
||||
- `interface InspectUnsupported` — `{ kind: "unsupported"; format: AudioFormatType }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function reads chunk identifiers as ASCII via `String.fromCharCode` rather than decoding UTF-8 — RIFF chunk IDs are guaranteed ASCII per the spec, so this is faster and avoids host text-decoder dependencies. The buffer-overrun guard (`pos + 8 + chunkSize > bytes.length`) catches both a too-short `fmt` chunk and a malformed `data` chunk before the upstream library has a chance to throw. The order of checks matters: `fmt` must precede `data`, so a `data`-before-`fmt` layout is classified `corrupt`, not `truncated`.
|
||||
Reference in New Issue
Block a user