Uncategorized

Raspberry Pi/Debian A2DP Sink or How to Make Your Own Bluetooth Speaker

Requirements:

  • 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)

Rationale:

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:

  1. Install Pulseaudio (PA) and PA bluetooth modules
  2. Enable Pulseaudio system mode
  3. Enable Pulseaudio bluetooth modules
  4. Allow Pulseaudio to talk to Bluez daemon
  5. Start Pulseaudio
  6. Configure Bluez
  7. Pair and trust Bluetooth devices
  8. Disable Bluetooth scanning to eliminate stutter

Step-by-step process

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

Start Pulseaudio

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[10235]: W: [pulseaudio] main.c: Running in system mode, but --disallow-module-loading not set.
Apr 28 08:47:40 raspberrypi pulseaudio[10235]: N: [pulseaudio] main.c: Running in system mode, forcibly disabling exit idle time.
Apr 28 08:47:40 raspberrypi pulseaudio[10235]: 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[10235]: 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:

# bluetoothctl

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:

[Your Phone]#

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

To /etc/bluetooth/main.conf

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.

Troubleshooting

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.

Possible causes:

  • 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

Sound stuttering

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

TODO

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.

Corking streams

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.

One Comment

  • Plamen

    Finally!
    I searched a lot and found mostly outdated tutorials.
    Yours was a very clear explanation and it worked flawlessly.
    Thanks a ton.

Leave a Reply

Your email address will not be published. Required fields are marked *