Replace broken per-segment Opus decode with WebCodecs AudioDecoder streaming pipeline
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* IStreamingDecoder - the stateful streaming-decode seam, parallel to IFormatDecoder.
|
||||
*
|
||||
* Why a second seam. `IFormatDecoder` (WAV/MP3/FLAC) is a *wrap-and-decode-each-segment* strategy:
|
||||
* `StreamDecoder` cuts the stream into independently-decodable segments, `wrapSegment` makes each a
|
||||
* standalone file, and `decodeAudioData` decodes each in isolation. That model is correct for raw PCM
|
||||
* (WAV) and independently-decodable frames (FLAC), but it is fundamentally wrong for Opus: Opus has
|
||||
* pre-skip (encoder delay) and inter-frame state (MDCT overlap-add, SILK/CELT continuity), so decoding
|
||||
* page-runs independently re-applies the pre-skip and starts from cold codec state at every boundary —
|
||||
* audible glitching and a broken timeline.
|
||||
*
|
||||
* A WebCodecs `AudioDecoder` is the right tool: one stateful decoder fed packets sequentially, decoding
|
||||
* continuously with correct pre-skip-once handling and full inter-frame continuity. But it does NOT fit
|
||||
* `IFormatDecoder` — it is async/callback-driven and owns its own buffering. So Opus gets this seam
|
||||
* instead. `AudioPlayer` dispatches by content-type: WAV/MP3/FLAC keep the `StreamDecoder` path
|
||||
* byte-for-byte; Opus routes here. Both feed the SAME `PlaybackScheduler` — the change is the decode
|
||||
* stage only, never the schedule/playback stage.
|
||||
*
|
||||
* The seam is intentionally minimal and mirrors the lifecycle `StreamDecoder` already exposes so
|
||||
* `AudioPlayer` can treat the two uniformly: initialize -> push chunks -> mark complete, plus a
|
||||
* range-continuation reinit for seek-beyond-buffer.
|
||||
*/
|
||||
|
||||
export interface IStreamingDecoder {
|
||||
/**
|
||||
* Decoded buffers ready to schedule, drained by AudioPlayer after each push/flush. Each entry is a
|
||||
* standard AudioBuffer at the AudioContext's sample rate, ready for PlaybackScheduler.addBuffer.
|
||||
*/
|
||||
readonly hasFatalError: boolean;
|
||||
|
||||
/** True once the decoder has enough to begin playback (header/config established). */
|
||||
readonly ready: boolean;
|
||||
|
||||
/** Total stream duration in seconds if known up front (Opus knows it from the sidecar), else null. */
|
||||
readonly totalDuration: number | null;
|
||||
|
||||
/**
|
||||
* Push raw stream bytes. Returns decoded AudioBuffers that became ready (possibly empty — WebCodecs
|
||||
* decode is async, so a push may return nothing and a later push returns several).
|
||||
*/
|
||||
push(chunk: Uint8Array): Promise<AudioBuffer[]>;
|
||||
|
||||
/**
|
||||
* Signal end-of-stream. Flushes the decoder and returns any residual decoded buffers (including the
|
||||
* end-trimmed final buffer).
|
||||
*/
|
||||
complete(): Promise<AudioBuffer[]>;
|
||||
|
||||
/**
|
||||
* Reinitialize for a Range-continuation after seek-beyond-buffer. The 206 body begins on an Ogg page
|
||||
* boundary and carries no setup pages — the decoder reuses the cached config and resets demux/codec
|
||||
* state so inter-frame continuity restarts cleanly from the new offset.
|
||||
*/
|
||||
reinitializeForRangeContinuation(): void;
|
||||
|
||||
/** Tear down the underlying WebCodecs decoder and release resources. */
|
||||
dispose(): void;
|
||||
}
|
||||
Reference in New Issue
Block a user