Repository navigation
Description
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 dataThat 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 bystream.encode(None). The buffer must be
read beforeclose(). - Formats whose final data only appears during
close()— OGG/Opus, whose last page is written there.
For these the buffer must be read afterclose().
#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 >= 3200Measured 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 dataThis 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
Technologicat commentedon Aug 3, 2026
...and just so that this isn't an AI-only report, I was at the helm, and we ran the repro example locally.
— Technologicat
remsky commentedon Aug 3, 2026
Thanks for the report and the catch; looks like a cleaner fix than the juggle we've had. Will repro and take a look.
remsky commentedon Aug 4, 2026
@Technologicat Will be merging this in, likely tomorrow at some point but let me know if you spot anything worth adjusting
Merge pull request #499 from remsky/fix/flac-ogg-wav-tail
fix(audio): read finalize buffer before close except for ogg, fixes f…
chore(release): 0.6.0 [skip ci]