first commit

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

View File

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

View 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.