Skip to content

Bluetooth audio sink

ArtMG edited this page Oct 21, 2024 · 3 revisions

Bluetooth audio sink

Intro

This introduces the software stack in Bluetooth audio receiver servers, also known as A2DP audio sinks. Although it covers information on the stack that might be relevant on many devices and distros, the main focus here is Raspberry Pi hardware and the Raspberry Pi OS (formerly Raspbian)

NOTE: this article has recently been separated from multi-room-audio so may need some tidying up

see also:

A2DP sink

The Bluetooth profile Advanced Audio Distribution Profile (A2DP) is sometimes also called Bluetooth Audio Streaming. Some devices also implement the Audio/Video Remote Control Profile (AVRCP) for volume and other controls.

Pi radio issues

Note that several sources mention contention issues when both Bluetooth and Wifi radios are connected on the Raspberry Pi system on chip (SoC). This would impact the performance, and therefore are very disruptive to audio quality.

Suggested solutions include:

  • not connecting via both at the same time
  • using an external (USB) radio device for one

Software in the stack

About linux sound software

See Lubuild's about Sound software in Ubuntu to understand more about the difference between the linux ALSA sound driver and Pulse sound mixer (or 'sound server'). In theory a sound driver like ALSA sound driver should be less complex than a sound mixer like Pulse Audio. If all else were equal, it makes sense that ALSA could make for more efficient Bluetooth sound transfers.

This may be one of the reasons why some people suggest that blue-alsa is more efficient than BlueZ over Pulse Audio. But efficiency is not everything, and simplicity and supportability can also be important. Let's look at them now.

Linux Bluetooth

BlueZ is the 'official' Linux bluetooth stack, and is the one most commonly shipped by distros (other than Android). The code was originally created and maintained by Qualcomm, chip manufacturer.

Up until version 4, you could connect directly between Alsa and Bluetooth streams. Since version 5, however you must go via PulseAudio. Although this gives practical flexibility for most common scenarios, adding a separate sound mixer layer into the stack might not be ideal for all applications.

bluealsa

For applications that are highly sensitive to latency (e.g. high quality audio) on low-powered hardware (e.g. RaspberryPi), there is an alternative. The bluealsa project, formerly known as bluez-alsa, skips the PulseAudio layer. It will register the audio profiles with BlueZ so that, via an alsa plugin, apps can simply access the ALSA PCM device called bluealsa.

https://github.com/Arkq/bluez-alsa

The core depends on only:

  • alsa-lib
  • bluez >= 5.0
  • glib with GIO support
  • sbc

Optional components also add support for (with package):

  • AAC (fdk-aac)
  • apt-X, apple's HQ audio codec (openaptx)
  • rfcomm for serial port modem (readline)
  • hcitop diagnostics utility (libbsd, ncurses)

BlueALSA acts as a proxy between BlueZ and ALSA. During system startup, bluealsa runs as a root, registers org.bluealsa in D-Bus for audio device access.

For details, like user context being member of audio group see also https://wiki.archlinux.org/index.php/Bluetooth_headset#Headset_via_Bluez5.2Fbluez-alsa

Pi OS

Raspberry Pi OS (currently Bullseye) Lite edition includes bluez as well as its own pi-bluetooth driver (including bthelper and hciuart services). However that low level bluetooth software does not predicate other choices. The base image also includes alsa-utils, but pulseaudio is not installed.

See this very useful in-depth article on the stack in Pi OS and how to work with devices and diagnose issues. It covers the difference between alsa and pulse, and their impact. Note that where it says that pulse is installed, it may be referring to the Desktop edition.

A2DP agents

On top of the base bluetooth and sound driver software, you will need:

  • A2DP audio stream API
    • bluealsa is a Linux daemon giving applications access to Bluetooth audio streams using the Bluetooth A2DP, HFP and/or HSP profiles. It provides a D-Bus API to applications, and can be used by ALSA applications via libasound plugins.
  • A2DP audio player service
    • bluealsa-aplay uses that API to capture audio streams from Bluetooth devices and play them to an ALSA playback device (help). You can wrap this utility as a service.
  • Bluetooth open pairing agent
    • this should start after the bluetooth service, and promiscuously allow connections from devices, either not asking for authentication or using a default 0000 passkey. This could be done with an agent using NoInputNoOutput mode. Most examples use a python agent, with dbus APIs to allow the incoming bluetooth connections. An alternative might be using shell script and bluetoothctl's scan and connect commands (e.g. this).

example solutions

Legacy Rpi Audio Receiver

This is inspired by a historical version of the Rpi ready install below.

This previous version of it used

NB the script below may require a workaround with later OS versions due to security improvements:

curl -O http://archive.raspbian.org/raspbian/pool/main/r/raspbian-archive-keyring/raspbian-archive-keyring_20120528.2_all.deb
sudo dpkg -i raspbian-archive-keyring_20120528.2_all.deb

Scripted install for Bluetooth open A2DP receiver

#### Bluetooth A2DP sink using bluealsa

# This is based on using the built-in Pi 3 bluetooth radio, but networking with 
# ethernet ONLY, so that the internal wifi radio is disabled and can't interfere

# Pi OS 11 'Bullseye' Lite includes bluetooth and bthelper@hci0 services only

# bluealsa not present in Pi bullseye repos - get it from 'Bookworm Raspbian'
# credit https://www.sigmdel.ca/michel/ha/rpi/bluetooth_in_rpios_02_en.html#bluealsa3
echo "deb http://archive.raspbian.org/raspbian/ bookworm main" | sudo tee /etc/apt/sources.list.d/armbian.list
printf 'Package: *\nPin: release n=bookworm\nPin-Priority: 100\n' | sudo tee --append /etc/apt/preferences.d/limit-bookworm
sudo apt update
sudo apt install -y bluez-alsa-utils


##### options for newly-installed bluez-alsa.service

sudo tee /etc/default/bluez-alsa <<EOF!
# Config file for blues-alsa.service - set the service runtime options
# credit https://www.sigmdel.ca/michel/ha/rpi/bluetooth_in_rpios_02_en.html#bluealsa3
#OPTIONS="-p a2dp-source -p a2dp-sink"
# but we only need the sink
OPTIONS="--profile=a2dp-sink"
# although using a USB Bluetooth radio you might need "--device=hciX"
# help - https://github.com/Arkq/bluez-alsa/blob/master/doc/bluealsa.8.rst
EOF!
sudo systemctl restart bluez-alsa.service
sudo systemctl status bluez-alsa.service


# These sections are derived from 
# https://github.com/artmg/rpi-audio-receiver/blob/master/install-bluetooth.sh
# but original credit to https://github.com/nicokaiser/rpi-audio-receiver


##### bluealsa-aplay.service starts after bluez-alsa to pass A2DP to Alsa device

sudo tee /etc/systemd/system/bluealsa-aplay.service <<EOF!
[Unit]
Description=BlueALSA player
Requires=bluez-alsa.service
After=bluez-alsa.service
Wants=bluetooth.target sound.target
[Service]
Type=simple
User=root
ExecStartPre=/bin/sleep 2
ExecStart=/usr/bin/bluealsa-aplay --pcm=default --pcm-buffer-time=250000 00:00:00:00:00:00
# help - https://github.com/Arkq/bluez-alsa/blob/master/doc/bluealsa-aplay.1.rst
[Install]
WantedBy=graphical.target
EOF!

##### udev rule restarts the A2DP player when a device connects to bluetooth

sudo tee /etc/udev/rules.d/61-bluealsa-aplay.rules <<EOF!
# syntax generated by connecting to bluetooth whilst using 
#udevadm monitor --environment --udev --kernel
ACTION=="add", SUBSYSTEM=="bluetooth", RUN+="/bin/systemctl restart bluealsa-aplay.service"
EOF!
# not bothered with udev script for connect/disconnect sounds from original


##### Bluetooth settings

sudo tee /etc/bluetooth/main.conf <<EOF!
[General]
# With bluetooth Class of Device (CoD), the Service Class (>0x2000) might be ignored
#Class = 0x200414
# Major device class: Audio/Video; Minor device class: Hifi audio device;
#Class = 0x0428
# Major device class: Audio/Video; Minor device class: Loudspeaker;
Class = 0x0414
DiscoverableTimeout = 0
[Policy]
AutoEnable=true
EOF!

sudo service bluetooth restart
sudo hciconfig hci0 piscan
sudo hciconfig hci0 sspmode 1

##### ALSA settings

sudo sed -i.orig 's/^options snd-usb-audio index=-2$/#options snd-usb-audio index=-2/' /lib/modprobe.d/aliases.conf


##### Bluetooth open pairing agent

# use modified version of BlueZ 'simple agent' python test script
wget https://raw.githubusercontent.com/bluez/bluez/master/test/simple-agent
# credit - https://github.com/RPi-Distro/repo/issues/291#issuecomment-1149427137
sed -i.orig 's/capability = "KeyboardDisplay"/capability = "NoInputNoOutput"/' simple-agent
sed -i.orig 's/return raw_input(prompt)/return "yes"/' simple-agent
sudo cp simple-agent /usr/local/bin/bluetooth-open-pairing
sudo chmod 755 /usr/local/bin/bluetooth-open-pairing
# and required library script
wget https://raw.githubusercontent.com/bluez/bluez/master/test/bluezutils.py
sudo cp bluezutils.py /usr/local/bin/
sudo chmod 755 /usr/local/bin/bluezutils.py
# I chose this script as it comes from the Bluez project itself. 
# If it ever changes and you can't make it work, consider a simpler derivative like 
# https://gist.github.com/mill1000/74c7473ee3b4a5b13f6325e9994ff84c#file-a2dp-agent

sudo tee /etc/systemd/system/bluetooth-open-pairing.service <<EOF!
# runs a modified copy of bluez simple-agent to silently allow all pairing requests
[Unit]
Description=Bluetooth open pairing agent
Requires=bluetooth.service
After=bluetooth.service
[Install]
WantedBy=bluetooth.target
[Service]
Type=simple
ExecStartPre=/bin/hciconfig hci0 piscan
ExecStartPre=/bin/hciconfig hci0 sspmode 1
ExecStart=/usr/local/bin/bluetooth-open-pairing
Restart=always
RestartSec=10
EOF!

# install pairing agent script dependencies 
sudo apt install -y python3-gi python3-dbus

# now let's run it all
sudo systemctl daemon-reload
sudo systemctl enable --now bluealsa-aplay
sudo systemctl enable --now bluetooth-open-pairing.service

# Now you should be able to pair with your Pi and send sound 
# that you can hear when you plug into the internal headphone jack

For any subsequent Snapcast configuration see Multi-room audio # Bluetooth sink

systemctl status bluetooth.service
systemctl status bluez-alsa.service
systemctl status bluealsa-aplay.service
systemctl status bluetooth-open-pairing.service

Rpi ready install

If you really want to go for simplicity and reliability, you might want to consider this fully scripted solution.

https://github.com/nicokaiser/rpi-audio-receiver/blob/main/install-bluetooth.sh

  • It currently uses:
    • bluez-tools
    • pulseaudio-module-bluetooth
      • yes that does mean more dependencies and 'potentially worse performance' (as yet unmeasured) but the Pi Foundation removed bluealsa from their repos for bullseye
How to use it

NB: If you later wish to alter the 'pretty name' that Bluetooth announces, then see https://github.com/artmg/lubuild/blob/master/help/diagnose/hardware.md#bluetooth-devices

Reconnection issues

Trying the version 19th July 2022 it worked on first connection. However after disconnecting it would not reconnect. I had to 1) remove the device on the client (e.g. android) 2) bluetoothctl / devices / remove to clear the device on the server (otherwise it would give me a 'PIN error')

This is not currently usable

Raspbian Stretch, bluez-alsa

It uses:

  • python-dbus, unknown depends
  • bluetoothctl, manage devices
  • a2dp-agent, pair & auth, what source? bt-agent-a2dp.service

example solution: see https://gist.github.com/mill1000/74c7473ee3b4a5b13f6325e9994ff84c

According to this https://wiki.debian.org/Bluetooth/Alsa BlueZ5 no longer supports alsa

BT-speaker daemon

https://github.com/lukasjapan/bt-speaker

git bluez python python-gobject python-cffi python-dbus python-alsaaudio python-configparser sound-theme-freedesktop vorbis-tools

  • BT-Speaker daemon written in Python
  • uses Bluez5, via Bluez DBUS interface
  • BT-Speaker registers itself as A2DP capable,
  • then route the received audio fully decoded to ALSAs aplay command

Although it might be a slightly heavyweight implementation, there is some good detail in the docs that might help other design ideas

The scripts are python-based and include hooks for actions on device connect and disconnect.

further ways to extend

Rasbian Wheezy bluez and pulse

bluez pulseaudio-module-bluetooth python-gobject python-gobject-2 bluez-tools

  • Uses udev 99-input.rules to launch script on BT connection
  • shell script runs pactl to set loopback between BT source and local audio sink
  • source is bluez_source device

https://www.instructables.com/id/Turn-your-Raspberry-Pi-into-a-Portable-Bluetooth-A/

Alternative with less script:

alsa-utils bluez bluez-tools pulseaudio-module-bluetooth python-gobject python-gobject-2

https://gist.github.com/oleq/24e09112b07464acbda1

Alternative

https://www.raspberrypi.org/forums/viewtopic.php?t=161944

Uses hacktooth script

needs categorising

old info on Snapcast sources

COPY of info for snapcast Bluetooth sources:

According to https://github.com/badaix/snapcast/blob/master/doc/player_setup.md#alsa the suggested input pipelines to snapserver's snapfifo pipe are:

  • Alsa: alsa file plugin, configured in asound.conf using pcm.writeFile
  • Pulse: pulseAudio pipe sink, configured by command pacmd load-module module-pipe-sink

You are at liberty to configure multiple pipe files, e.g. /tmp/snapMPDfifo for MPD and /tmp/snapBluefifo for Bluetooth.

Config ideas

Note: not checked that these are all needed or even work!

# Set Bluetooth Class of Device (for clients scanning)
hciconfig hci0 up
# Loudspeaker class
hciconfig hci0 class 0x240414
# could add Networking Service Class bit 0x260414

tee -a /etc/bluetooth/audio.conf << EOF!
[General]
Enable=Source
EOF!

# example code from python dbus
# detect connection
bus.add_signal_receiver(property_changed, bus_name="org.bluez", signal_name = "PropertyChanged", path_keyword="path", interface_keyword="interface")
# set up pulse loopback
pactl load-module module-loopback source=bluez_source.%s sink=alsa_output.pci-0000_00_1b.0.analog-stereo" % bt_addr


Buildroot components required

  • rpi-bt-firmware
  • bluez-tools bluez5-utils
  • modules: bluetooth, bnep, btbcm, hci_uart

Agent Install from mill1000

# credit https://gist.github.com/mill1000/74c7473ee3b4a5b13f6325e9994ff84c
sudo apt install -y bluealsa python-dbus

# install the agent from the gist
sudo wget --output-document=/usr/local/bin/a2dp-agent https://gist.githubusercontent.com/mill1000/74c7473ee3b4a5b13f6325e9994ff84c/raw/244a0acf2278569525c8577f4960cd552c81c697/a2dp-agent
sudo chmod +x /usr/local/bin/a2dp-agent

# set timeout and enable discovery
sudo sed -i 's/#DiscoverableTimeout = 0/DiscoverableTimeout = 0/' /etc/bluetooth/main.conf
sudo bluetoothctl
power on
discoverable on
exit

# make sure it appears as an audio sink
sudo sed -i 's|ExecStart=/usr/bin/bluealsa$|ExecStart=/usr/bin/bluealsa -p a2dp-sink|' /lib/systemd/system/bluealsa.service
sudo systemctl daemon-reload
sudo systemctl restart bluealsa.service

 
# test the agent
sudo /usr/local/bin/a2dp-agent
  
# install the agent as a service
sudo wget --output-document=/lib/systemd/system/bt-agent-a2dp.service https://gist.githubusercontent.com/mill1000/74c7473ee3b4a5b13f6325e9994ff84c/raw/244a0acf2278569525c8577f4960cd552c81c697/bt-agent-a2dp.service
  
# and create a playback service
sudo tee /lib/systemd/system/a2dp-playback.service <<EOF!
[Unit]
Description=A2DP Playback
After=bluealsa.service syslog.service
Requires=bluealsa.service
  
StartLimitInterval=200
StartLimitBurst=5
  
[Service]
ExecStartPre=/bin/sleep 3
ExecStart=/usr/bin/bluealsa-aplay --profile-a2dp 00:00:00:00:00:00
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=A2DP-Playback
User=pi
  
Restart=always
RestartSec=10
  
[Install]
WantedBy=multi-user.target
EOF!
  
sudo systemctl daemon-reload
sudo systemctl enable bt-agent-a2dp.service
sudo systemctl enable a2dp-playback.service
sudo systemctl start bt-agent-a2dp.service
sudo systemctl start a2dp-playback.service

This failed at the point of testing the agent as it would not allow connections and not show up in device search lists

Technical background

about A2DP channels

A single paired connection may set up multiple A2DP services in each direction. Each separate service is a uni-directional transfer of an audio stream in up to 2 channel stereo. The profile relies on AVDTP and GAVDP.

It includes mandatory support for the low-complexity SBC codec (not to be confused with Bluetooth's voice-signal codecs such as CVSDM) and supports optionally MPEG-1 Part 3/MPEG-2 Part 3 (MP2 and MP3), MPEG-2 Part 7/MPEG-4 Part 3 (AAC and HE-AAC), and ATRAC, and is extensible to support manufacturer-defined codecs

If a Bluetooth stack enforces the SCMS-T digital rights management (DRM) scheme, it may limit the transfer of high quality audio.

The A2DP standard operates in stereo and supports most of the standard audio compression codecs. The recommended sub-band coding (SBC) codec supports up to 345 kilobits per second at 48 kilohertz. That’s approximately one third the quality of standard CD audio—roughly the equivalent of a high-quality MP3 recording. Due to high “lossy” compression in the SBC codec, the reality of the audio quality is considerably lower, somewhere in the range of 256kbit/s.

The system also supports other popular methods of encoding and compressing audio, like MP3 itself. If the audio source is already compressed in a format like MP3, AAC, or ATRAC, then it doesn’t need to be re-encoded in SBC in order to be broadcast from the source device. With A2DP’s maximum audio bandwidth of 728kbit/s, it’s at least possible to start approaching what we’d call “high-quality audio” with the basic standard alone. (CD quality audio, uncompressed, is approximately 1400kbit/s.)

Unfortunately, very few hardware makers seem to be actually using this capability, and most A2DP-only devices are re-encoding audio to SBC and de-encoding on the receiver end. This makes the whole process more complicated, resulting in poorer audio quality.
—— <cite>[HowToGeek][1]</cite>

[1]:https://www.howtogeek.com/338750/whats-the-difference-between-bluetooth-a2dp-and-aptx/
Clone this wiki locally