- An up-to-date Debian or Raspberry Pi Buster installation. Equivalent Ubuntu Server version should also work, but hasn’t been tested. Might work with other distributions, mutatis mutandis.
- Bluetooth dongle/card that supports A2DP (anything even remotely modern will do)
Streaming audio over LAN is a mess, an even bigger mess if WiFi is involved. Sure, many apps do that for their own output, Pulseaudio allows streaming audio over RTP, but there is no universal, application-independent solution that works on all platforms. Audio over Bluetooth, using the Advanced Audio Distribution Profile, is, however, universally supported and requires next to zero configuration on the client beyond pairing.
Configuring a host to receive an audio stream over Bluetooth and play it is more complicated, but doable. On Linux, recent Pulseaudio versions work hand-in-hand with Bluez 5, but they do need a bit of configuration and several tweaks to work well. While there is quite a bit of guides on doing so, I haven’t found a comprehensive one.
Note: This guide assumes a barebones, headless installation of Debian or Raspberry and access to the command line. These instructions have been tested on a clean RaspiOS 04.03.2021 image. Some steps (e.g. installing Pulseaudio) might not be necessary in beefier installations.
The process at a glance:
- Install Pulseaudio (PA) and PA bluetooth modules
- Enable Pulseaudio system mode
- Enable Pulseaudio bluetooth modules
- Allow Pulseaudio to talk to Bluez daemon
- Start Pulseaudio
- Configure Bluez
- Pair and trust Bluetooth devices
- Disable Bluetooth scanning to eliminate stutter
Install Pulseaudio (PA), PA bluetooth modules and Bluez
From a root prompt:
# apt-get update # apt-get install --no-install-recommends pulseaudio pulseaudio-module-bluetooth bluez
The “–no-install-recommends” will prevent apt from installing a bunch of Mesa packages that weight over 500 MB
Enable Pulseaudio system mode
NOTE: The documentation rightly warns against running Pulseaudio in system mode, i.e. as a system-wide service. In a usual desktop environment, each logged-in user should have their own instance of PA running with the user’s privileges. However, we are running a headless system and no user will be logged-in. In this case, we either need to automatically log in a regular user and make him/her spawn a PA instance or run Pulseaudio in system mode. The latter is a cleaner choice and an exception to the “no system mode Pulseaudio” rule.
Debian has, for some time, stopped shipping Pulseaudio system-mode systemd unit files, so we have to create our own. Create /etc/systemd/system/pulseaudio.service with the following content:
[Unit] Description=Pulseaudio sound server After=avahi-daemon.service network.target [Service] ExecStart=/usr/bin/pulseaudio --system --disallow-exit --disable-shm --daemonize=no ExecReload=/bin/kill -HUP $MAINPID [Install] WantedBy=multi-user.target
And enable the service to make Pulseaudio start on boot:
# systemctl enable pulseaudio
System-mode Pulseaudio instance will run under pulse user, this will be important later.
Enable Pulseaudio Bluetooth modules
System-mode Pulseaudio configuration resides in /etc/pulse/system.pa. Edit the file and add:
.ifexists module-bluetooth-policy.so load-module module-bluetooth-policy .endif .ifexists module-bluetooth-discover.so load-module module-bluetooth-discover .endif
at the end.
Allow Pulseaudio to talk to Bluez daemon
Pulseaudio communicates with Bluez daemon via D-Bus system bus, which by default denies processes from initiating connection unless explicitly granted access. To do so, we also need to add pulse user to bluetooth group:
# adduser pulse bluetooth
For some reason, it is also necessary to restart the D-Bus daemon for the change to take effect:
# systemctl restart dbus
Now we can start Pulseaudio:
# systemctl start pulseaudio
This will result in a stern warning in syslog about running it in system mode, but this is OK, this (i.e. running on a headless system) is literally the use-case for system-mode. Additionally, do not set –disallow-module-loading, as the message suggests, as it would break Pulseaudio Bluetooth modules.
Apr 28 08:47:40 raspberrypi pulseaudio: W: [pulseaudio] main.c: Running in system mode, but --disallow-module-loading not set. Apr 28 08:47:40 raspberrypi pulseaudio: N: [pulseaudio] main.c: Running in system mode, forcibly disabling exit idle time. Apr 28 08:47:40 raspberrypi pulseaudio: W: [pulseaudio] main.c: OK, so you are running PA in system mode. Please make sure that you actually do want to do that. Apr 28 08:47:40 raspberrypi pulseaudio: W: [pulseaudio] main.c: Please read http://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/WhatIsWrongWithSystemWide/ for an explanation why system mode is usually a bad idea.
Configure Bluez and pair devices
Run bluetoothctl to configure Bluez and pair devices:
If you have multiple Bluetooth controllers (i.e. cards/dongles), select the appropriate one:
[bluetooth]# select XX:XX:XX:XX:XX:XX
You can also set a controller’s name, it will show up on other devices when pairing:
[bluetooth]# system-alias SoundOfTux
Now enable KeyboardOnly agent and set our session as the default agent to handle pairing requests:
[bluetooth]# agent KeyboardOnly Agent registered [bluetooth]# default-agent Default agent request successful
Now make your controller discoverable:
[bluetooth]# discoverable on Changing discoverable on succeeded [CHG] Controller XX:XX:XX:XX:XX:XX Discoverable: yes
Make sure your controller is allowed to pair with other Bluetooth devices (it should be by default, but better safe than sorry):
[bluetooth]# pairable on
try to pair from a phone or another computer and confirm passkey on both devices:
[CHG] Device YY:YY:YY:YY:YY:YY Connected: yes Request confirmation [agent] Confirm passkey 146741 (yes/no): yes
If the pairing is successful and the devices, you should see a bunch of lines like this:
[CHG] Device YY:YY:YY:YY:YY:YY UUIDs: 00001105-0000-1000-8000-00805f9b34fb
and the command prompt will show the connected device’s name:
Don’t worry, all commands you type here will still take effect on your controller, not on the connected device.
You might need to authorize A2DP and HFP services:
Authorize service [agent] Authorize service 00001108-0000-1000-8000-00805f9b34fb (yes/no): yes Authorize service [agent] Authorize service 0000110d-0000-1000-8000-00805f9b34fb (yes/no): yes
Make Bluez trust your device to allow it to connect in the future without service authorisation:
[Your Phone]# trust YY:YY:YY:YY:YY:YY
Make your controller no longer discoverable:
[Your Phone]# discoverable off
Setting Bluetooth class (optional)
Before pairing, the controller will present itself to other devices as a generic Bluetooth device. To make it present as an audio device, add
Class = 0x00041C
Change output volume (optional)
In this setup, the A2DP source (e.g. a phone playing music) doesn’t control the audio mixer at A2DP sink. Instead, it controls the volume of the stream it transmits via Bluetooth. If the volume at A2DP sink mixer is set too low or too high, this might unduly limit the volume range you can achieve. To set volume via CLI, you need to add your regular user to pulse-access group,:
# adduser username pulse-access
And use a tool such as alsamixer.
Please note that there can be several mixers to control, to select one mixer in alsamixer, press F6.On Raspberry Pi, Pulseaudio exposes the default software mixer (set to 100% by default) and the HW mixer on a soundcard (built-in bcm2835 or any USB or HAT soundcard you might have plugged in) – you will need to use the second one to up the volume from the somewhat low default level.
Device pairs but disconnects moments after connecting
Try connecting to the device from bluetoothctl:
[bluetooth]# connect YY:YY:YY:YY:YY:YY
and check syslog (e.g. using journalctl). If you see
a2dp-source profile connect failed for 00:B8:B6:08:13:23: Protocol not available
then Bluez is unable to talk to Pulseaudio.
- Pulseaudio Bluetooth modules are not set to be loaded in system.pa (see “Enable Pulseaudio Bluetooth modules”)
- User pulse has been not been added to bluetooth group (see “Allow Pulseaudio to talk to Bluez daemon”)
- Bluez daemon has been restarted, but Pulseaudio hasn’t. Just restart Pulseaudio.
After correcting the above, restart Pulseaudio for changes to take effect:
# systemctl restart pulseaudio
“dbus-daemon[pid]: [system] Rejected send message” in syslog
User pulse has not been been added to bluetooth group (see “Allow Pulseaudio to talk to Bluez daemon”).
After correcting the above, restart Pulseaudio for changes to take effect:
# systemctl restart pulseaudio
It is possible Bluetooth signal is not good enough. Try moving the devices closer. It might also take a short while (and perhaps a few pause/play cycles) for Pulseaudio to adapt latency and thus eliminate
If this does not help, there are two possibilities:
Controller is scanning for Bluetooth devices
Disable scanning from bluetoothctl:
[bluetooth]# scan off
Interference from WiFis
Coexistence between Bluetooth and WiFi on RaspberryPi used to be quite wonky. On my system they are now working together, but if they don’t, there are two things you can do:
- Get an external Bluetooth dongle
- Block built-in WiFi using rfkill (e.g. rfkill block wlan). Bluetooth used
Running Pulseaudio in user mode
This setup should be doable when Pulseaudio is running in user mode. One could route sound via one’s desktop computer or a laptop.
Improving sound quality with better A2DP codecs
The default A2DP codec, SBC, is “not great, not terrible”. There are several others that offer a much better quality: AAC, AptX, AptX HD, LDAC. There is a (sadly, unmantained since quite recently) project to implement these in Pulseaudio and it should be relatively easy to get it going on a RaspberryPi as long proper packages can be built.
Pulseaudio allows “corking” (pre-empting) audio streams when a new one starts playing. If you are using the system for playing audio from different sources (e.g. DLNA renderer or MPD), it should be possible to make Pulseaudio stop a stream that is already playing when it receives audio via Bluetooth.
AVCTP/AVRCP volume control
Might be possible with triggerhappy, but not likely to be useful in this scenario.
Automatically pair new devices
It is easily doable (see e.g. https://gist.github.com/Pindar/e259bec5c3ab862f4ff5f1fbcb11bfc1), but it would require a bit of work to make it secure. You don’t want random people playing music (or worse, Polish reggae) in your living room.