first commit
This commit is contained in:
100
docs/media/audio/audio-codecs/src/stream/stream-decoder.md
Normal file
100
docs/media/audio/audio-codecs/src/stream/stream-decoder.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# `stream/stream-decoder`
|
||||
|
||||
`TransformStream<Uint8Array, AudioPage>` factory — composes a chunked
|
||||
byte stream into an `AudioPage` stream by driving a codec's
|
||||
`StreamingDecoder`.
|
||||
|
||||
## Purpose
|
||||
|
||||
The `CodecRegistry.decodeStream` method returns a
|
||||
`ReadableStream<AudioPage>`. That works when callers want to read
|
||||
pages directly, but does not compose into `@suaveplan/streams`
|
||||
pipelines because pipelines are built from `TransformStream` nodes,
|
||||
not `ReadableStream` sinks. This module's `createDecodeTransform`
|
||||
fills the gap — it returns a `TransformStream<Uint8Array, AudioPage>`
|
||||
suitable for `pipeThrough()` between a raw byte source and a
|
||||
downstream consumer.
|
||||
|
||||
## Features
|
||||
|
||||
- `createDecodeTransform(format, registry?)` — returns a
|
||||
`TransformStream<Uint8Array, AudioPage>` where each input chunk
|
||||
is fed to the codec's `StreamingDecoder` and each decoded
|
||||
`DecodeResult` is emitted as one `AudioPage` on the output stream.
|
||||
- Lazy codec resolution — the transform's `ensure()` helper picks
|
||||
the codec and constructs its decoder on first chunk, not at
|
||||
transform construction. This lets callers tee the stream or
|
||||
build pipeline branches without paying for codec setup on
|
||||
branches that never see data.
|
||||
- Default registry — when `registry` is omitted, the transform
|
||||
lazily loads `WavCodec`, `AiffCodec`, and `CafCodec` into a
|
||||
shared cached registry. Callers needing WASM codecs must pass
|
||||
their own registry.
|
||||
- Error propagation — codec failures (unknown format,
|
||||
no-streaming-factory, or any thrown error from `write` / `flush`)
|
||||
surface as `controller.error(...)` with an `AudioCodecsError`
|
||||
carrying the original on `cause`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createDecodeTransform } from "@suaveplan/audio-codecs/stream/stream-decoder";
|
||||
import { CodecRegistry, WavCodec } from "@suaveplan/audio-codecs";
|
||||
|
||||
const registry = new CodecRegistry({ codecs: [WavCodec] });
|
||||
const transform = createDecodeTransform("wav", registry);
|
||||
|
||||
const upstream: ReadableStream<Uint8Array> = await fetchAudio();
|
||||
const pages = upstream.pipeThrough(transform);
|
||||
|
||||
for await (const page of pages) {
|
||||
consumePage(page);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Compose with a downstream `@suaveplan/streams` transform:
|
||||
|
||||
```ts
|
||||
import { createDecodeTransform } from "@suaveplan/audio-codecs/stream/stream-decoder";
|
||||
import { makeNormaliser, makeRingBuffer } from "./my-pipeline";
|
||||
|
||||
const decodeTx = createDecodeTransform("flac", myRegistry);
|
||||
|
||||
await fetchAudioStream()
|
||||
.pipeThrough(decodeTx)
|
||||
.pipeThrough(makeNormaliser({ targetLufs: -14 }))
|
||||
.pipeThrough(makeRingBuffer({ capacity: 16 }))
|
||||
.pipeTo(myDestinationSink);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `function createDecodeTransform(format: string, registry?: CodecRegistry):
|
||||
TransformStream<Uint8Array, AudioPage>` — produces a transform that
|
||||
emits `AudioPage` per decoded block; throws / errors with
|
||||
`AUDIO_CODECS.NOT_SUPPORTED` for unknown format or
|
||||
`AUDIO_CODECS.STREAM_ERROR` for codecs without `createDecoder()`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `transform()` callback awaits `ensure()` then calls
|
||||
`decoder.write(chunk)`, fanning out the resulting `DecodeResult[]`
|
||||
into the output controller. Each `DecodeResult.channelData` array
|
||||
is wrapped in fresh `Float32Array` copies so downstream consumers
|
||||
cannot accidentally mutate the codec's internal buffers.
|
||||
- `startSample` accumulates across pages so the emitted
|
||||
`AudioPage` sequence forms a contiguous timeline.
|
||||
- The `flush()` callback drains the codec's buffered tail via
|
||||
`decoder.flush()`, emits any remaining pages, then closes the
|
||||
decoder. Errors during flush are translated into
|
||||
`AUDIO_CODECS.STREAM_ERROR` via `controller.error()`.
|
||||
- Codec-thrown `AudioCodecsError` instances pass through unchanged;
|
||||
non-API errors are wrapped with `STREAM_ERROR`.
|
||||
- The lazy registry initialisation (`getDefaultRegistry`) currently
|
||||
registers only the three native codecs (WAV/AIFF/CAF). Callers
|
||||
needing MP3/FLAC/Opus/etc must build their own registry via
|
||||
`new CodecRegistry({ codecs: [...] })` and pass it explicitly.
|
||||
This choice keeps the default-registry path tree-shakeable when
|
||||
callers don't use streaming.
|
||||
110
docs/media/audio/audio-codecs/src/stream/stream-encoder.md
Normal file
110
docs/media/audio/audio-codecs/src/stream/stream-encoder.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# `stream/stream-encoder`
|
||||
|
||||
`TransformStream<readonly Float32Array[], Uint8Array>` factory —
|
||||
composes a PCM-block stream into encoded bytes by driving a codec's
|
||||
`StreamingEncoder`.
|
||||
|
||||
## Purpose
|
||||
|
||||
The mirror of `stream-decoder` for the encode direction. Callers
|
||||
building a `@suaveplan/streams` pipeline (decode → DSP → encode)
|
||||
need a `TransformStream` node for the encode stage; this module
|
||||
provides it.
|
||||
|
||||
## Features
|
||||
|
||||
- `createEncodeTransform(format, sampleRate, channels, registry?)` —
|
||||
returns a `TransformStream<readonly Float32Array[], Uint8Array>`
|
||||
where each input `Float32Array[]` chunk represents one block of
|
||||
PCM (one `Float32Array` per channel, same length per channel) and
|
||||
each output `Uint8Array` carries encoded bytes from the codec's
|
||||
`StreamingEncoder`.
|
||||
- Lazy codec resolution — the transform constructs the codec's
|
||||
encoder on first chunk (or on flush if no chunks arrived). The
|
||||
encoder's `start()` is called once to emit header bytes, then
|
||||
`write()` for every chunk, then `flush()` to emit any trailer.
|
||||
- Sample rate + channel count are captured at construction so the
|
||||
encoder's `start()` can pre-compute its header without waiting
|
||||
for the first chunk.
|
||||
- Empty-source handling — even when no PCM chunks arrive, `flush()`
|
||||
still calls `start()` once and emits the codec's trailer (the
|
||||
44-byte WAV header for the WAV codec). Downstream consumers
|
||||
always receive a structurally-valid file.
|
||||
- Default registry — when `registry` is omitted, the transform
|
||||
lazily loads `WavCodec` into a shared cached registry. Codecs
|
||||
beyond WAV that support encode (FLAC, MP3, Opus, QOA) must be
|
||||
passed explicitly via the `registry` parameter.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createEncodeTransform } from "@suaveplan/audio-codecs/stream/stream-encoder";
|
||||
import { CodecRegistry, WavCodec } from "@suaveplan/audio-codecs";
|
||||
|
||||
const registry = new CodecRegistry({ codecs: [WavCodec] });
|
||||
const transform = createEncodeTransform("wav", 44_100, 2, registry);
|
||||
|
||||
const pcmStream = makePcmBlockStream();
|
||||
const wavBytes = pcmStream.pipeThrough(transform);
|
||||
|
||||
await wavBytes.pipeTo(myFileSink);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Decode-then-encode pipeline (transcode FLAC → WAV via streaming):
|
||||
|
||||
```ts
|
||||
import {
|
||||
createDecodeTransform,
|
||||
createEncodeTransform,
|
||||
} from "@suaveplan/audio-codecs/stream/stream-encoder";
|
||||
|
||||
const decodeTx = createDecodeTransform("flac", registry);
|
||||
const encodeTx = createEncodeTransform("wav", 44_100, 2, registry);
|
||||
|
||||
// The mid pipeline converts AudioPage into Float32Array[] form.
|
||||
const pageToBlock = new TransformStream<AudioPage, readonly Float32Array[]>({
|
||||
transform(page, controller) {
|
||||
controller.enqueue(page.channelData);
|
||||
},
|
||||
});
|
||||
|
||||
await fetchAudio()
|
||||
.pipeThrough(decodeTx)
|
||||
.pipeThrough(pageToBlock)
|
||||
.pipeThrough(encodeTx)
|
||||
.pipeTo(myDestination);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `function createEncodeTransform(format: string, sampleRate: number,
|
||||
channels: number, registry?: CodecRegistry):
|
||||
TransformStream<readonly Float32Array[], Uint8Array>` — produces
|
||||
a transform that emits encoded byte chunks; errors with
|
||||
`AUDIO_CODECS.NOT_SUPPORTED` for unknown formats or
|
||||
`AUDIO_CODECS.STREAM_ERROR` for codecs without `createEncoder()`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `started` flag ensures the encoder's `start(sampleRate,
|
||||
channels)` is invoked exactly once even when no chunks arrive
|
||||
(the `flush()` path also checks the flag and calls `start()`
|
||||
if needed before invoking `flush()`).
|
||||
- WAV's `start()` returns an empty `Uint8Array` because the WAV
|
||||
header includes the total data-chunk size which is only known at
|
||||
flush time; the transform handles that by emitting zero-byte
|
||||
chunks (skipped via the `byteLength > 0` check) so downstream
|
||||
consumers don't see empty data.
|
||||
- Codec-thrown `AudioCodecsError` instances pass through unchanged;
|
||||
non-API errors from `write` or `flush` are wrapped with
|
||||
`STREAM_ERROR`.
|
||||
- The default registry registers only `WavCodec`. Encoding via
|
||||
FLAC, MP3, Opus, or QOA requires the caller to construct a
|
||||
registry with those codecs and pass it as the fourth argument.
|
||||
This choice keeps the default-registry path tree-shakeable when
|
||||
callers don't use streaming.
|
||||
- `cancel()` on the underlying transform stream calls
|
||||
`encoder.close()` to release any internal buffers — even
|
||||
half-streamed encodes leak no resources on cancellation.
|
||||
Reference in New Issue
Block a user