Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

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.