fix(audio): prevent stale PCM buildup under A2DP backpressure - #4
Open
s-qin wants to merge 1 commit into
Open
Conversation
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.
Problem
guest/udp_to_sink.pydrains UDP audio intopw-playthrough a blockingstdin pipe. When PipeWire / BlueZ A2DP temporarily consumes slower than UDP
arrives (for example while the same Bluetooth radio is busy with BLE input),
the blocking pipe write stops the receiver from draining the socket.
The large UDP receive buffer can then retain old PCM. Playback consumes that
queue later, so latency grows and pausing the Windows source can leave an
audible stale-audio tail.
Root cause
The receiver currently does:
Backpressure on the second line prevents the first line from running. UDP
therefore changes from the intended real-time transport into a backlog of
historical audio.
Fix
pw-playstdin fd nonblocking.PIPE_BUF, so writes are atomic.EAGAINinstead of blocking the UDP receiver.
replaying stale PCM.
pw-playcushion,16 KB pipe setting, 100 ms silence debounce/feed, default-sink behavior,
and service restart loop.
The local hardware investigation also tested lower
pw-play/ pipe sizes,but those are deployment-specific latency tuning and are intentionally not
part of this minimal upstream fix.
Why dropping is correct
This stream is live audio over UDP; packets are already not recoverable or
ordered like a file transfer. Once the sink is behind, replaying every old
frame increases user-visible latency. Dropping the frame that cannot be
accepted keeps the receiver near live time and bounds stale backlog growth.
Validation
Automated regression tests cover:
frame;
BlockingIOError/EAGAINwithout a crash;Commands/results:
python3 -m py_compile udp_to_sink.py test_udp_to_sink.py— PASSpython3 -m unittest -v test_udp_to_sink— 6/6 PASS on Debian 12git diff --check— PASSReal-hardware evidence
Manually tested on the OpenSpan Windows -> UDP 4010 -> Debian/PipeWire ->
BlueZ A2DP path with one Intel Bluetooth radio shared by BLE HID and A2DP:
Recv-Qmeasured 237,184–248,064 bytes, approximately1.24–1.29 seconds of 48 kHz stereo s16 PCM;
receive queue;
These hardware numbers are additional field evidence, not an upstream CI
benchmark.
Regression considerations
BlueZ transport idle-suspend issue.
pw-playthrough the existing service loop afterpipe/process failures.