Files
suaveplan/packages/media/audio/audio-codecs/src/stream/stream-encoder.md
2026-05-27 23:47:53 +01:00

4.3 KiB

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

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):

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.