REPRODUCIBILITY: 100%
OS VERSION: 5.2.0.17
HARDWARE: Jolla Phone (2026)
UI LANGUAGE: Polish
REGRESSION: no
TAGS: jolla-2026, 5_2_0
DESCRIPTION:
Which A2DP codec gets used depends on which side initiates the
connection. When the earbuds initiate, they select the SBC XQ 552 kbps
endpoint and audio stutters continuously. When the phone initiates,
PulseAudio selects plain SBC and audio is clean. The user has no way to
influence this from the UI, and the choice is lost on every reconnect.
PRECONDITIONS:
- PulseAudio 17.0 (as shipped)
- True wireless earbuds: JLab GO Sport+
- For comparison: Sony WH-1000XM3
STEPS TO REPRODUCE:
- Pair the earbuds.
- Take them out of the charging case and let them connect by
themselves — the headset initiates. - Play music and check:
pactl send-message /card/bluez_card.XX_XX_XX_XX_XX_XX/bluez get-codec
→ “sbc_xq_552”
Audio stutters in a repeating cycle: roughly a second clean, then
several seconds of crackling and drop-outs. - Disconnect the device from Settings > Bluetooth, leaving the earbuds
out of the case so they stay powered on. - Tap the device in the same list — now the phone initiates.
- Play music and check again:
→ “sbc”
Audio is clean.
EXPECTED RESULT:
A working configuration regardless of which side initiates.
ACTUAL RESULT:
Headset-initiated connections get SBC XQ 552 and are unusable.
ADDITIONAL INFORMATION:
The sample rate is NOT the cause. I initially suspected it, because both
values changed together (48000 Hz with XQ, 44100 Hz with plain SBC). To
isolate the variables I forced default-sample-rate = 48000 in
/etc/pulse/daemon.conf. With that, both cases run the sink at 48000 Hz
and the only difference is the codec — “sbc_xq_552” when the earbuds
initiate (stutters) and “sbc” when the phone initiates (clean). So the
SBC XQ variant is what breaks it.
This matches the source: pa_a2dp_endpoint_configurations in
a2dp-codec-util.c lists plain sbc ahead of the XQ variants, so when
BlueZ asks PulseAudio to choose via SelectConfiguration, plain SBC wins.
When the remote device sends its own SetConfiguration instead,
PulseAudio accepts it, and these earbuds pick the highest-bitrate
endpoint we offered — which they cannot sustain.
This is not broken hardware: the same JLab earbuds play flawlessly on an
iPhone 12 Pro Max (which does not implement SBC XQ at all) and on a
Samsung Galaxy A34 (which does). The Sony WH-1000XM3 play flawlessly on
the Jolla — they negotiate plain “sbc” and never enter XQ.
Workaround:
Disconnect and reconnect over D-Bus, which moves the choice of
configuration to the phone:
DEV=/org/bluez/hci1/dev_XX_XX_XX_XX_XX_XX
dbus-send --system --reply-timeout=15000 --dest=org.bluez
–print-reply $DEV org.bluez.Device1.Disconnect || true
sleep 4
dbus-send --system --reply-timeout=15000 --dest=org.bluez
–print-reply $DEV org.bluez.Device1.Connect || true
Run this after the earbuds have connected by themselves. Note that the
Disconnect call often returns NoReply even though it succeeds, hence the
timeout and the “|| true”.
Side effect: after the reconnect, bluetoothd logs
“AVRCP: failed to create uinput for : No such file or directory”,
so headset media buttons may stop working until the next normal
connection.
Two further notes for anyone debugging this:
- After a manual “switch-codec”, the stream can end up on sink.null,
which looks exactly like Bluetooth dying. Check
“pactl list sink-inputs short” and move it back to the bluez sink. - module-card-restore does not persist the selected Bluetooth codec, so
a manual switch-codec is lost on every reconnect.
Questions:
- Should PulseAudio renegotiate, or at least constrain, the
configuration on remote-initiated connections rather than accepting
whatever the device proposes? The endpoint priority list already
encodes a sensible preference — it just does not get consulted in
this path. - Is there any supported way to restrict which A2DP endpoints are
offered? module-bluez5-discover accepts only headset,
autodetect_mtu, enable_msbc, output_rate_refresh_interval_ms,
enable_native_hsp_hs, enable_native_hfp_hf and
avrcp_absolute_volume, and endpoint registration looks
unconditional. PipeWire exposes bluez5.codecs for exactly this.
MODIFICATIONS: none in the final state. The daemon.conf sample rate
change described above was only used to isolate the variable and has
been reverted.
Write-up drafted with AI assistance (Claude). All commands, log output
and test results were run and verified on my own device; happy to re-run
anything or provide fuller logs.