Skip to content

api: allow pipeline draining during flush on short streams - #180

Merged
fabiangreffrath merged 1 commit into
knik0:masterfrom
nschimme:fix-flush-drain
Aug 27, 2026
Merged

api: allow pipeline draining during flush on short streams#180
fabiangreffrath merged 1 commit into
knik0:masterfrom
nschimme:fix-flush-drain

Conversation

@nschimme

@nschimme nschimme commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

This is a correctness fix with a minor performance hit.

Refactors faacEncEncode() during end-of-stream flushing to absorb internal look-ahead priming ticks inside the encoder and extends the flush budget for HE-AAC, preventing premature 0-byte returns on short audio streams and truncated SBR tails.

The Problem

  1. Short-Stream Tail Loss: The API contract for faacEncEncode() requires callers to pass samplesInput == 0 to flush and poll until 0 bytes are returned. However, the encoder previously applied the look-ahead fill check (frameNum <= LOOKAHEAD_DEPTH) unconditionally. For input streams shorter than the look-ahead depth, initial flush calls hit this gate and returned 0 bytes while still priming. Callers adhering to the API contract interpreted this initial 0 as "drain complete" and stopped calling, discarding all buffered audio tail.
  2. HE-AAC Tail Truncation: HE-AAC (HE_V1) utilizes an SBR coded-payload ring (frameFIFO, sized SBR_FRAME_FIFO) that trails the core FIFO by one extra tick. The standard LC flush budget (LOOKAHEAD_DEPTH + 1) returned 0 before trailing SBR envelope payloads were fully emitted, dropping real trailing audio near frame boundaries.

Architectural Fix

Instead of forcing callers to disambiguate "still warming up look-ahead" from "genuinely drained," faacEncEncode() now handles flush ticks via an internal do { ... } while (flushing) loop:

  • Flushing Path (flushing == 1): Loops internally to absorb no-output look-ahead priming ticks until a real frame is produced OR the pipeline is fully drained. This guarantees that a 0-byte return strictly signifies a fully drained pipeline.
  • HE-AAC Flush Budget Expansion: Expands the maximum flush frame threshold from LOOKAHEAD_DEPTH + 1 to SBR_FRAME_FIFO when HE_V1 is active, allowing the SBR payload ring to drain completely before emitting the final 0-byte return.
  • Active Encoding Path (flushing == 0): Retains single-pass execution—returns 0 when accumulating, or proceeds downstream once a full frame is ready.
  • Shared Pipeline: Reuses the existing psychoacoustic and bitstream logic across both paths, avoiding code duplication and keeping binary footprint minimal.
  • Null-Buffer Safety: Treats inputBuffer == NULL as an implicit flush trigger to guard against caller parameter mismatches.

Testing

Benchmark: https://github.com/nschimme/faac/actions/runs/33096630745

image

@nschimme

Copy link
Copy Markdown
Contributor Author

This fixes the LC side but there is still an open HE lag that I'm chasing. It is safe to merge as it is technically correct.

@nschimme

Copy link
Copy Markdown
Contributor Author

Fixed the HE side, this is good to go now

faacEncEncode()'s API contract instructs callers to pass samplesInput == 0
to initiate a flush, and to keep calling until 0 bytes are returned.
However, the encoder previously gated output on a look-ahead depth check
(frameNum <= LOOKAHEAD_DEPTH), returning 0 bytes while filling the pipeline.

For audio streams shorter than LOOKAHEAD_DEPTH, the very first flush call
hit this look-ahead guard and returned 0. Callers adhering to the API contract
interpreted this 0 as "fully drained" and stopped polling, causing the
encoder to discard all buffered audio.

Fix this by bypassing the look-ahead check when flushing is active. This
allows the pipeline to continue emitting encoded frames frame-by-frame
until the core look-ahead delay is completely drained.

Additionally, treat a NULL inputBuffer as an implicit flush trigger to
safely handle samplesInput/inputBuffer parameter mismatches.

HE-AAC needs one more flush tick than LC: its SBR coded-payload ring
(frameFIFO, sized SBR_FRAME_FIFO) trails the core FIFO by one extra tick,
so the shared LOOKAHEAD_DEPTH + 1 budget still dropped real trailing
audio near a frame boundary. Widen the budget to SBR_FRAME_FIFO for
HE_V1 so the ring drains fully before flushFrame's 0-return.
@fabiangreffrath
fabiangreffrath merged commit cc511d2 into knik0:master Aug 27, 2026
7 checks passed
@nschimme
nschimme deleted the fix-flush-drain branch August 28, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants