fix(linux): stop fighting the A2DP profile, and make the codec choosable - #766
fix(linux): stop fighting the A2DP profile, and make the codec choosable#766injkgz wants to merge 5 commits into
Conversation
Three related problems made playback controls unreliable on Linux. MediaController re-activated its cached A2DP profile on every playback start, even when the card was already using another A2DP variant. Switching profiles recreates the PipeWire sink and stops the stream that triggered the activation, so a play command from the AirPods stem was cancelled about half a second later by the resulting pause. Any active a2dp-sink variant is now left untouched, which also keeps the negotiated codec unchanged. Fixes librepods-org#680. The cached PulseAudio/PipeWire card index went stale across reconnects, since PipeWire may assign a different index. Availability checks then queried a card that no longer existed and concluded A2DP was unavailable. The card is now resolved by Bluetooth MAC before each activation and the cached index refreshed. Fixes librepods-org#681. A freshly connected card can appear before its profiles are enumerated. The first missing-profile result triggered an immediate WirePlumber restart, which interrupts audio for several seconds and races pipewire-pulse rebuilding the card. Enumeration now gets a bounded grace period, the restart remains as a last resort, and post-restart retries are bounded too, without sleeping after the final attempt. Fixes librepods-org#682.
The profile preference order was hardcoded as SBC-XQ, SBC, AAC, so AAC was never selected even though it is the codec AirPods use natively with Apple devices, and it is what PipeWire ranks highest for them. Add a "Preferred audio codec" setting (AAC, SBC-XQ, SBC) to the settings tab, stored as `preferred_codec` in app_settings.json and defaulting to AAC. The chosen codec is tried first and the remaining ones act as fallbacks, so a card that cannot negotiate the preferred codec still gets a working A2DP profile.
The LE monitor decodes left, right and case levels from the proximity
advertisement but only pushed them into the tray. The window kept showing
whatever AACP last reported, and over AACP the case reports itself as
disconnected whenever the buds are outside it, so the case level was
missing from the device view.
Forward the decoded levels to the UI as an AACPEvent::BatteryInfo, the
same event the AACP path already sends, skipping components the
advertisement marks as unavailable (0xff).
While here, move the "Auto-connect is disabled" log into the else branch.
It sat inside the auto-connect branch, so it was printed right after a
successful connection:
Successfully connected to AirPods 34:0E:22:CE:A2:80
Auto-connect is disabled for 34:0E:22:CE:A2:80, not attempting to connect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cAE8Gb2vCuv9WLMh91R9g
async_main's result was unwrapped, so any Bluetooth error at startup
panicked. With panic = "abort" in the release profile, a panic on the
backend thread takes the whole process down, and the user sees a core
dump with no explanation. Powering on a soft-blocked adapter is the
common way to hit this:
thread '<unnamed>' panicked at src/main.rs:98:18:
called `Result::unwrap()` on an `Err` value: Error { kind: Failed, message: "Failed" }
Log what actually went wrong for each startup step - D-Bus connection,
adapter lookup, powering on - and exit with status 1 instead of aborting.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A7MEAowWUpkKaDm8W2ws3C
PipeWire can leave a Bluetooth card on the "off" profile: the card shows up before BlueZ has negotiated the A2DP transport, and nothing revisits that choice afterwards. The buds then sit connected and silent, with no sink to play to, until something activates a profile. Profile activation only ran when playback started, so a user who connected their AirPods and did not immediately press play was left with working Bluetooth and no audio, which reads as the app being broken. Activate a profile right after the device is set up. The bounded wait for profile enumeration added earlier covers the case where the transport is still coming up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJd8jHVKWUebFZmNcSAWU3
|
Some context on how this was tested and how it relates to other open work. Tested on top of #655. These fixes were validated in a local branch that merges this PR with the hi-res microphone work from #655, on AirPods Pro 3 (A3064), Fedora 44, PipeWire 1.6.8, BlueZ 5.87. That combination is what exposed the last commit here: with the microphone running, a freshly connected pair could sit on the Related:
|
1 similar comment
|
Some context on how this was tested and how it relates to other open work. Tested on top of #655. These fixes were validated in a local branch that merges this PR with the hi-res microphone work from #655, on AirPods Pro 3 (A3064), Fedora 44, PipeWire 1.6.8, BlueZ 5.87. That combination is what exposed the last commit here: with the microphone running, a freshly connected pair could sit on the Related:
|
Problem
On Linux, pressing play on the AirPods stem did not resume playback reliably. Watching the MPRIS traffic showed the play command arriving and being cancelled right after:
The cause is in
MediaController: it re-activates its cached A2DP profile on every playback start. Switching the card profile recreates the PipeWire sink, which stops the stream that triggered the activation, and WirePlumber pauses the MPRIS player.While tracing that, two more problems in the same path showed up, both matching existing issues: the cached card index goes stale across reconnects, and a not-yet-enumerated card triggers an immediate WirePlumber restart that silences audio for the better part of a minute.
Changes
fix(linux): stop fighting the A2DP profile on playback start— closes #680, #681, #682a2dp-sink*profile is left untouched; the preferred profile is selected only when the card is off or on a non-A2DP profile. The negotiated codec is preserved.feat(linux): let the user pick the preferred A2DP codecThe preference order was hardcoded as
["a2dp-sink-sbc_xq", "a2dp-sink-sbc", "a2dp-sink"], so AAC was never selected — even though it is what AirPods use natively with Apple devices and what PipeWire ranks highest for them. A Preferred audio codec setting (AAC / SBC-XQ / SBC) now lives in the settings tab, is stored aspreferred_codecinapp_settings.json, and defaults to AAC. The remaining codecs are kept as fallbacks.Testing
Tested on Fedora 44 (KDE Plasma, Wayland), PipeWire 1.6.8, WirePlumber 0.5.14, BlueZ 5.87, with AirPods Pro 3 (model A3064):
kded6sends MPRIS commands now, no cancellingPausefollows, and the card stays ona2dp-sinkthroughout.A2DP profile a2dp-sink already active, leaving it unchangedand performs no profile switch.headset-head-unitwhile a client captures from the AirPods microphone and returns toa2dp-sinkafterwards.cargo build --releaseis clean; no new warnings.