Skip to content

fix(utils): keep whole samples when flushing a partial trailing sample - #6876

Open
rahul2002m wants to merge 1 commit into
livekit:mainfrom
rahul2002m:fix/audio-byte-stream-flush-partial-sample
Open

fix(utils): keep whole samples when flushing a partial trailing sample#6876
rahul2002m wants to merge 1 commit into
livekit:mainfrom
rahul2002m:fix/audio-byte-stream-flush-partial-sample

Conversation

@rahul2002m

Copy link
Copy Markdown

Fixes #6874.

AudioByteStream.flush() bailed out with an empty list whenever the buffer did not end on a sample boundary, so one stray byte took every complete sample with it. On an 8 kHz mono stream with a 200 ms target frame that is up to 200 ms of speech, and the warning called it an incomplete frame while what it actually dropped was the whole buffer.

The buffer was also left in place on that path, since only the success path called clear(). So the misaligned byte stayed in the stream and offset every later frame by a byte, which comes out as noise rather than as missing audio. That second part is the one that worried me more, because it is silent.

The fix drops only the trailing bytes that cannot form a sample, returns the samples before them, and leaves the buffer empty on both paths. The warning now says how many bytes went.

I ran into this on a telephony feed at 8 kHz where the source can hand over an odd number of bytes. It may also be worth a look next to #5158, which tracks truncation on the PCM path from the other end, though I have not confirmed they are the same thing.

Tests are in a new tests/test_utils_audio.py, since AudioByteStream did not have a module of its own. Ten cases: whole samples surviving a partial tail, only the tail being dropped, the buffer ending up empty, the warning naming the byte count, a lone partial byte, an empty buffer staying quiet, flush after whole frames were emitted, idempotence, and the stereo case where a sample is 4 bytes. Eight of the ten fail on main; the other two are guards for behaviour this does not change.

uv run pytest --unit gives 1566 passed against 1556 on main, which is the ten new ones and no regressions. make check is clean, formatting, ruff and mypy. There are 9 errors in the unit run on my machine from the OTLP metrics exporter at teardown, but they are identical on a clean checkout of main so they are not from this change.

AudioByteStream.flush() returned an empty list when the buffer did not end on
a sample boundary, so a single stray byte discarded every complete sample with
it. On an 8 kHz mono stream with a 200 ms target frame that is up to 200 ms of
speech, and the warning called it an incomplete frame while dropping the whole
buffer.

The buffer was also left in place on that path, because only the success path
cleared it. The misaligned byte stayed in the stream and offset every later
frame by one byte, which comes out as noise rather than as missing audio.

Drop just the trailing bytes that cannot form a sample, return the samples
before them, and leave the buffer empty either way. The warning now names the
number of bytes discarded.

Closes livekit#6874
@rahul2002m
rahul2002m requested a review from a team as a code owner August 16, 2026 18:28
@CLAassistant

CLAassistant commented Aug 16, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

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.

AudioByteStream.flush() discards all buffered audio when the buffer ends on a partial sample

2 participants