Stop treating an unfilled buffer as the end of the track - #30
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Regression from #28, currently live
BufferedAudioSource.read()returnedb""when nothing arrived within one second:discord.pyreadsb""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:
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:
Why that alone was not enough
It would have traded one artefact for another.
discord.pystarts its playback clock before its first read: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 beforevc.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:
The regression test was confirmed non-vacuous. With the bug deliberately reintroduced:
279 passedoverall, 4 new: a slow first byte is not mistaken for the end;primewaits then reports ready;primegives up on a source that never produces;primereturns promptly on an already-finished source.Unrelated noise worth knowing about
Teardown logs a traceback from
discord.py's own pipe-writer thread:That is a race inside
FFmpegPCMAudio.cleanup()— it sets_process = MISSINGwhile its writer thread is still running. It predates these changes, is cosmetic, and only adds noise tojournalctl. Not addressed here.