Author: Eiko

Time: 2026-03-31 08:33:26 - 2026-03-31 08:34:04 (UTC)

(The summary was written by GPT, for archiving the debug process)

Debugging a Bluetooth Speaker on Arch Linux: A2DP Broken, mSBC Works

Problem

I connected a Bluetooth speaker on Arch Linux and saw several profile choices in the desktop Bluetooth/audio UI:

  • Audio Gateway
  • Headset Head Unit (CVSD)
  • Headset Head Unit (mSBC)

The observed behavior was:

  • CVSD worked, but the sound quality was terrible.
  • mSBC worked and sounded better.
  • Audio Gateway did not work.

This was confusing, because for a Bluetooth speaker, the high-quality playback path should normally be the music profile, not the headset/call profile.


Initial interpretation of the profiles

There are two different kinds of Bluetooth audio paths involved here.

1. HFP/HSP: headset / call mode

This is the telephony-style path.

Properties:

  • bidirectional audio
  • microphone + speaker together
  • lower quality than the music profile
  • often mono or speech-oriented

Two codecs showed up under this path:

  • CVSD: older narrowband speech codec, poor quality
  • mSBC: better wideband speech codec, still not ideal for music

So it made sense that:

  • CVSD sounded bad
  • mSBC sounded better

2. A2DP: music mode

This is the normal playback path for music.

Properties:

  • high-quality audio output
  • typically one-way playback
  • this is what a Bluetooth speaker should normally use

So in principle, the goal was not “pick the best headset codec”, but rather “make the actual music profile work”.


Key observation from pactl list cards

The important command output was:

Card #85
	Name: bluez_card.F4_4E_FD_8E_A4_04
	device.description = "BOGASING-M5"
	Profiles:
		off: Off (sinks: 0, sources: 0, ...)
		audio-gateway: Audio Gateway (A2DP Source & HSP/HFP AG) (sinks: 0, sources: 0, ...)
		headset-head-unit-cvsd: Headset Head Unit (HSP/HFP, codec CVSD) (sinks: 1, sources: 1, ...)
		headset-head-unit: Headset Head Unit (HSP/HFP, codec MSBC) (sinks: 1, sources: 1, ...)
	Active Profile: audio-gateway

The crucial part was:

audio-gateway ... (sinks: 0, sources: 0)

That meant the supposedly “better” profile was not actually creating any usable playback or capture node.

So although audio-gateway was selected, it was effectively useless in the current state.

By contrast, the HFP/HSP profiles did create working nodes:

  • CVSD: sinks: 1, sources: 1
  • mSBC: sinks: 1, sources: 1

This immediately explained why CVSD and mSBC worked while audio-gateway did not.


Key observation from wpctl status

The status output showed that the Bluetooth device existed, but there was no Bluetooth output sink:

Audio
 ├─ Devices:
 │      71. BOGASING-M5                         [bluez5]
 │
 ├─ Sinks:
 │  *   61. Built-in Audio Analog Stereo        [vol: 0.54]
 │
 ├─ Sources:
 │      62. Built-in Audio Analog Stereo        [vol: 0.24]
 │
 ├─ Filters:
 │    - loopback-2548-18
 │  *   73. bluez_input.F4:4E:FD:8E:A4:04       [Audio/Source]

The important thing was:

  • there was a bluez_input... source
  • there was no bluez_output... sink

So PipeWire had not created the normal playback endpoint for the Bluetooth speaker.

That meant the music transport had failed to come up.


Checking whether the speaker really supports music playback

To determine whether the speaker itself was at fault, the following command was used:

bluetoothctl info F4:4E:FD:8E:A4:04

The output included:

UUID: Audio Source
UUID: Audio Sink
UUID: Handsfree

This was important.

A speaker advertising Audio Sink means it is capable of receiving normal Bluetooth music playback.

So the device was not limited to headset mode. The music path should have been possible.


Checking logs

The journal contained the crucial error lines:

bluetoothd[1106]: src/service.c:btd_service_connect() a2dp-sink profile connect failed for F4:4E:FD:8E:A4:04: Device or resource busy
bluetoothd[1106]: profiles/audio/avctp.c:avctp_control_confirm() Control: Refusing unexpected connect

This suggested that the A2DP music connection was failing during negotiation.

So the picture became:

  • the speaker supports A2DP music playback
  • the system knows about Bluetooth audio roles
  • but the A2DP connection attempt fails
  • only the headset/call-style profiles remain usable

Why the naming is confusing

The profile name audio-gateway is easy to misunderstand.

For a speaker that advertises Audio Sink, the laptop must act as the A2DP Source when sending music to the speaker.

So in WirePlumber’s terminology:

  • the speaker is the sink
  • the laptop is the source

This is why the relevant role on the Linux machine is a2dp_source, even though the goal is “play music to the speaker”.


Hypothesis

The likely issue was not that the speaker lacked A2DP.

The more plausible explanation was that the Bluetooth stack was trying to juggle:

  • the music role (A2DP)
  • and the call/headset role (HFP/HSP)

and that this role negotiation was getting stuck or conflicting, leading to the Device or resource busy failure.

So the strategy was:

Force the stack to use only the music role, and disable the headset/call roles for this device.


Solution

A WirePlumber configuration override was created.

File:

~/.config/wireplumber/wireplumber.conf.d/51-bluez-a2dp.conf

Contents:

monitor.bluez.properties = {
  bluez5.roles = [ a2dp_source ]
}

This tells WirePlumber to use only the A2DP source role for Bluetooth devices.

In effect, this disables the HFP/HSP headset roles and keeps only the music path.


Restarting the audio stack

After creating the config file, restart the user audio services:

systemctl --user restart wireplumber pipewire pipewire-pulse

Then reconnect the Bluetooth device.

A full reconnect sequence can be done with:

bluetoothctl
disconnect F4:4E:FD:8E:A4:04
trust F4:4E:FD:8E:A4:04
connect F4:4E:FD:8E:A4:04

The trust step is not necessarily the core fix, but it is sensible for stable reconnect behavior.


Result

After forcing only a2dp_source, the speaker worked correctly.

So the effective fix was:

  • stop trying to use or negotiate the call/headset roles
  • force the Bluetooth stack onto the proper music role
  • allow PipeWire to create the normal playback sink

In short:

The fix was not “make CVSD better” or “switch to mSBC”. The real fix was to bypass the headset path entirely and force the correct A2DP music role.


Practical conclusion

If A2DP is broken and only CVSD / mSBC work

Then:

  • CVSD is usually the worst fallback
  • mSBC is the best fallback among headset profiles
  • but the real target is still A2DP

If pactl list cards shows:

audio-gateway ... (sinks: 0, sources: 0)

and wpctl status shows no bluez_output... sink, then the music path is not really up.

If the device advertises:

UUID: Audio Sink

then it should be capable of normal Bluetooth music playback, and the issue is likely in negotiation/configuration rather than the hardware simply lacking support.


Minimal debugging checklist

1. Check card profiles

pactl list cards

Look for:

  • whether the Bluetooth card exists
  • whether A2DP-like roles produce a sink
  • whether headset profiles are the only ones with usable nodes

2. Check actual active nodes

wpctl status

Look for:

  • whether there is a bluez_output... sink
  • whether there is only a bluez_input... source

3. Check device capabilities

bluetoothctl info <MAC>

Look for UUIDs such as:

  • Audio Sink
  • Audio Source
  • Handsfree

4. Check the logs

journalctl -b | grep -Ei 'bluetooth|bluez|a2dp'

Look for negotiation or connection failures.

5. If needed, force only the music role

monitor.bluez.properties = {
  bluez5.roles = [ a2dp_source ]
}

and restart WirePlumber/PipeWire.


Final summary

The issue was a Bluetooth speaker on Arch Linux where:

  • headset modes worked
  • music mode did not
  • mSBC sounded better than CVSD, but neither was the correct target

The debugging process showed that:

  • the speaker supported normal music playback (Audio Sink)
  • PipeWire was not creating a Bluetooth playback sink
  • A2DP connection attempts were failing with Device or resource busy

The fix was to force WirePlumber to use only the music role:

monitor.bluez.properties = {
  bluez5.roles = [ a2dp_source ]
}

After restarting the audio services and reconnecting the device, the speaker worked normally.


Commands used

pactl list cards
wpctl status
bluetoothctl info F4:4E:FD:8E:A4:04
journalctl -b | grep -Ei 'bluetooth|bluez|a2dp|mSBC|CVSD'

Config file:

~/.config/wireplumber/wireplumber.conf.d/51-bluez-a2dp.conf

monitor.bluez.properties = {
  bluez5.roles = [ a2dp_source ]
}

Restart:

systemctl --user restart wireplumber pipewire pipewire-pulse