Skip to content

Stop treating an unfilled buffer as the end of the track - #30

Merged
Isma-L154 merged 1 commit into
mainfrom
fix/buffer-slow-first-byte
Aug 23, 2026
Merged

Stop treating an unfilled buffer as the end of the track#30
Isma-L154 merged 1 commit into
mainfrom
fix/buffer-slow-first-byte

Conversation

@Isma-L154

Copy link
Copy Markdown
Owner

Regression from #28, currently live

BufferedAudioSource.read() returned b"" when nothing arrived within one second:

try:
    return self._queue.get(timeout=1.0)
except queue.Empty:
    return b""

discord.py reads b"" as "the source is finished" and stops playback. yt-dlp routinely needs 3 to 8 seconds to produce its first byte.

Every track would have ended the instant it started.

Caught by a functional check against real audio on the host — three queries, zero frames each:

bohemian rhapsody       solo 0 tramas
sc: lofi hip hop        solo 0 tramas
youtube.com/watch?v=... solo 0 tramas

This should have been run before #28 was deployed. The unit tests all passed because every fake source produced its first frame immediately; none of them modelled the multi-second startup that yt-dlp actually has.

The fix

An empty buffer is not the end. Only the sentinel the producer leaves on exit, or a producer thread that has died, means finished:

while not self._stop.is_set():
    try:
        return self._queue.get(timeout=0.5)
    except queue.Empty:
        if not self._thread.is_alive():
            return b""      # producer gone without leaving a sentinel
        continue
return b""

Why that alone was not enough

It would have traded one artefact for another. discord.py starts its playback clock before its first read:

self._start = time.perf_counter()
while not self._end.is_set():
    self.loops += 1
    data = self.source.read()          # <- blocking here starts you behind

So a first read that blocks for five seconds leaves the player five seconds behind schedule, and it catches up by bursting ~250 frames — the exact speed-up #28 set out to remove.

prime() fills the buffer before vc.play(), called through an executor so it never blocks the event loop, and reports whether any audio arrived at all.

Verification

On the host, real audio, all three source types:

bohemian rhapsody                OK   listo en  4.19s   (3.0s de audio)
sc: lofi hip hop                 OK   listo en  3.09s   (3.0s de audio)
https://www.youtube.com/watch?   OK   listo en  5.77s   (3.0s de audio)

The regression test was confirmed non-vacuous. With the bug deliberately reintroduced:

--- corriendo el test con el bug ---
1 failed
--- restaurado ---
1 passed

279 passed overall, 4 new: a slow first byte is not mistaken for the end; prime waits then reports ready; prime gives up on a source that never produces; prime returns promptly on an already-finished source.

Unrelated noise worth knowing about

Teardown logs a traceback from discord.py's own pipe-writer thread:

AttributeError: '_MissingSentinel' object has no attribute 'terminate'

That is a race inside FFmpegPCMAudio.cleanup() — it sets _process = MISSING while its writer thread is still running. It predates these changes, is cosmetic, and only adds noise to journalctl. Not addressed here.

Regression from the read-ahead change in #28, shipped in 09544ae.

BufferedAudioSource.read() returned b"" when nothing arrived within one second.
discord.py reads b"" as "the source is finished" and stops playback - and
yt-dlp routinely needs 3 to 8 seconds to produce its first byte. Every track
would have ended the moment it started.

An empty buffer is not the end. Only the sentinel the producer leaves on exit,
or a producer thread that has died, means finished; otherwise read() keeps
waiting.

Fixing that alone would have traded one artefact for another. discord.py starts
its playback clock *before* its first read, so beginning against an empty
buffer leaves the player seconds behind schedule, and it catches up by bursting
frames - exactly the speed-up #28 set out to remove. prime() fills the buffer
first, called from an executor so it never blocks the event loop, and reports
whether any audio arrived at all.

Verified on the host across a search, a SoundCloud query and a YouTube URL:
all three now buffer and deliver 3 seconds of audio, ready in 3.1-5.8s.

The regression test was confirmed non-vacuous by reintroducing the bug: it
fails with it and passes without it.
@Isma-L154
Isma-L154 merged commit 8c0647a into main Aug 23, 2026
3 checks passed
@Isma-L154
Isma-L154 deleted the fix/buffer-slow-first-byte branch August 23, 2026 06:16
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.

1 participant