/** * 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; /** * Signal end-of-stream. Flushes the decoder and returns any residual decoded buffers (including the * end-trimmed final buffer). */ complete(): Promise; /** * 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. * * @param landingTimeSeconds The actual presentation time of the resolved seek page (t_page ≤ target). * @param targetTimeSeconds The user-requested seek position. The decoder trims the leading * `(target - landing) * sampleRate` frames so playback lands at target * (AC9 fine re-sync, §3.4a step 4). */ reinitializeForRangeContinuation(landingTimeSeconds: number, targetTimeSeconds: number): void; /** Tear down the underlying WebCodecs decoder and release resources. */ dispose(): void; }