Skip to content

FLAC loses its tail: #448 and #463 are two halves of one bug #497

Closed

Description

@Technologicat

The OGG/Opus truncation fix (#447 / #448) and the WAV chunk-end pop fix (#463) are two halves of the same
underlying bug, and a third format is still affected: FLAC currently loses the tail of every encode.

We hit this while re-vendoring streaming_audio_writer.py into another project. Sharing the diagnosis
because the pattern predicts which formats break, rather than waiting for each one to be reported.

Mechanism

write_chunk() truncates the output buffer after every chunk:

data = self.output_buffer.getvalue()
self.output_buffer.seek(0)
self.output_buffer.truncate(0)
return data

That is correct for streaming, but it means position 0 of the buffer is no longer the start of the file —
it is whatever was written since the last chunk.

Container formats then split into two families with opposite requirements at finalize:

  • Formats that seek back to patch a header they could not fill in until the length was known — WAV (RIFF
    chunk sizes) and FLAC (STREAMINFO total-samples and MD5). For these, close() writes the patch at
    position 0, which now lands on top of the audio just flushed by stream.encode(None). The buffer must be
    read before close().
  • Formats whose final data only appears during close() — OGG/Opus, whose last page is written there.
    For these the buffer must be read after close().

#448 reordered finalize to close-then-read, which is right for the second family and wrong for the first.
#463 was the first family's symptom surfacing on WAV three months later, and was fixed by special-casing WAV
with return b"". FLAC has the identical header-patching behaviour and was not covered.

Reproduction

0.2 s sine at 16 kHz — 3200 samples in, 2304 out:

import io
import numpy as np
from api.src.services.streaming_audio_writer import StreamingAudioWriter

SR = 16000
t = np.arange(int(0.2 * SR)) / SR
audio = (np.sin(2 * np.pi * 440 * t) * 20000).astype(np.int16)   # 3200 samples

w = StreamingAudioWriter(format="flac", sample_rate=SR, channels=1)
blob = w.write_chunk(audio) + w.write_chunk(finalize=True)

import av                                    # count what survives
with av.open(io.BytesIO(blob)) as c:
    n = sum(f.to_ndarray().shape[-1] for f in c.decode(audio=0))
print(n)                                     # 2304, expected >= 3200

Measured against a copy of streaming_audio_writer.py at e54ea70 that differs from yours only in imports
(we drop loguru and soundfile), so the encode path is unmodified.

Swapping the two lines back — reading the buffer before close() — restores 3200 exactly, and reverts
OGG/Opus to losing its final page. That is the trade #448 and #463 have been split across.

Suggested fix

Choose the order by format family, rather than adding a special case per format as symptoms arrive:

if self.format in ("wav", "flac"):
    # These seek back to position 0 to patch a header (RIFF sizes; FLAC STREAMINFO). The buffer has
    # been truncated after every chunk, so that write lands on the flushed audio instead of on the
    # header. Read first, then close.
    data = self.output_buffer.getvalue()
    self.container.close()
else:
    # OGG/Opus write their final page during close(), so their last audio only exists afterwards.
    self.container.close()
    data = self.output_buffer.getvalue()
self.output_buffer.close()
return data

This also lets the return b"" special case for WAV go away, since with the correct order WAV no longer
picks up the size-field junk that caused the pop in #463 — it round-trips sample-exact.

Verified across wav, flac, mp3, opus and aac: the two lossless formats round-trip sample-exact, the
lossy ones pad as expected.

Happy to open a PR if useful, though our copy has diverged enough that the diagnosis is probably worth more
than the patch.

— Claude (Opus 5)

Activity

  1. Technologicat commented on Aug 3, 2026

    @Technologicat
    Author

    ...and just so that this isn't an AI-only report, I was at the helm, and we ran the repro example locally.

    — Technologicat

  2. remsky commented on Aug 3, 2026

    @remsky
    Owner

    Thanks for the report and the catch; looks like a cleaner fix than the juggle we've had. Will repro and take a look.

  3. linked a pull request that will close this issuefix: flac loses its tail at finalize (#497) #499on Aug 4, 2026
  4. remsky commented on Aug 4, 2026

    @remsky
    Owner

    @Technologicat Will be merging this in, likely tomorrow at some point but let me know if you spot anything worth adjusting

  5. added a commit that references this issue on Aug 5, 2026
  6. added a commit that references this issue on Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions