Skip to content

Instantly share code, notes, and snippets.

@Pindar
Forked from mill1000/README.md
Last active February 13, 2023 21:23
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save Pindar/e259bec5c3ab862f4ff5f1fbcb11bfc1 to your computer and use it in GitHub Desktop.
Save Pindar/e259bec5c3ab862f4ff5f1fbcb11bfc1 to your computer and use it in GitHub Desktop.
Headless A2DP Audio Streaming on Raspbian Stretch

About

This gist will show how to setup Raspbian Stretch as a headless Bluetooth A2DP audio sink. This will allow your phone, laptop or other Bluetooth device to play audio wirelessly through a Rasperry Pi.

Motivation

A quick search will turn up a plethora of tutorials on setting up A2DP on the Raspberry Pi. However, I felt this gist was necessary because this solution is:

  • Automatic & Headless - Once setup, the system is entirely automatic. No user iteration is required to pair, connect or start playback. Therefore the Raspberry Pi can be run headless.
  • Simple - This solution has few dependencies, readily available packages and minimal configuration.
  • Up to date

Prerequisites

  • Raspbian Stretch - I used the Lite version as this is a headless setup. See the official guide if you need help.
  • Bluez-alsa - Available in the Raspbian package repo. This software allows us to stream A2DP audio over Bluetooth without PulseAudio.
  • Raspberry Pi with Bluetooth - The Raspberry Pi 3 has integrated Bluetooth, however there is a known bug when the WiFi is used simultaneously. Cheap USB Bluetooth dongles work equally well.

Disabling Integrated Bluetooth

If you are using a separate USB Bluetooth dongle, disable the integrated Bluetooth to prevent conflicts.

To disable the integrated Bluetooth add the following

# Disable onboard Bluetooth
dtoverlay=pi3-disable-bt

to /boot/config.txt and execute the following command

sudo systemctl disable hciuart.service

Initial Setup

First make sure the system is up to date using the following commands.

sudo apt-get update
sudo apt-get upgrade

Then reboot the Pi to ensure the latest kernel is loaded.

Now install the required packages.

sudo apt-get install bluealsa python-dbus libasound2-dev 

To get the latest features and HD sound quality I recommend to compile and install bluealsa manually. (Enable AAC on macOS sudo defaults write bluetoothaudiod "Enable AAC codec" -bool true && sudo defaults read bluetoothaudiod)

First install libfdk-aac

sudo apt-get install autoconf libtool -y
mkdir ffmpeg
cd ffmpeg
wget -O fdk-aac.zip https://github.com/mstorsjo/fdk-aac/zipball/master
unzip fdk-aac.zip
cd mstorsjo-fdk-aac*
autoreconf -fiv
./configure --prefix="$HOME/ffmpeg_build" --disable-shared
sudo make
sudo make install
export PKG_CONFIG_PATH=/home/pi/ffmpeg_build/lib/pkgconfig

Upgrade bluez to latest version

sudo apt-get install libdbus-1-dev libglib2.0-dev libudev-dev libical-dev libreadline-dev -y
wget http://www.kernel.org/pub/linux/bluetooth/bluez-5.58.tar.xz
tar xvf bluez-5.58.tar.xz && cd bluez-5.58
./configure --prefix=/usr --mandir=/usr/share/man --sysconfdir=/etc --localstatedir=/var --enable-experimental 
make -j4
sudo make install
sudo reboot

Check successful upgrade with bluetoothctl -v

Afterwards install https://github.com/Arkq/bluez-alsa as described in the README and please have the following dependencies installed.

Dependencies to install sudo apt-get install -y libsndfile1 libsndfile1-dev libbluetooth-dev

SBC:

git clone https://git.kernel.org/pub/scm/bluetooth/sbc.git
./bootstrap-configure
./configure
make
sudo make install

(I configured it with ../configure --enable-aac --enable-msbc)

Make it available via systemd as described in the WIKI https://github.com/Arkq/bluez-alsa/wiki/Systemd-integration

Make Bluetooth Discoverable

Normally a Bluetooth device is only discoverable for a limited amount of time. Since this is a headless setup we want the device to always be discoverable.

  1. Set the DiscoverableTimeout in /etc/bluetooth/main.conf to 0
# How long to stay in discoverable mode before going back to non-discoverable
# The value is in seconds. Default is 180, i.e. 3 minutes.
# 0 = disable timer, i.e. stay discoverable forever
DiscoverableTimeout = 0
  1. Enable discovery on the Bluetooth controller
sudo bluetoothctl
power on
discoverable on
exit
  1. Set proper device class in /etc/bluetooth/main.conf.
# Default device class. Only the major and minor device class bits are
# considered. Defaults to '0x000000'.
#Class = 0x000100
Class = 0x200400

Install The A2DP Bluetooth Agent

A Bluetooth agent is a piece of software that handles pairing and authorization of Bluetooth devices. The following agent allows the Raspberry Pi to automatically pair and accept A2DP, HFP, HSP and AVRCP connections from Bluetooth devices (HFP, HSP and AVRCP are required to get macOS, Windows connections working). All other Bluetooth services are rejected.

Copy the included file a2dp-agent to /usr/local/bin and make the file executable with

sudo chmod +x /usr/local/bin/a2dp-agent

Testing The Agent

Before continuing, verify that the agent is functional. The Raspberry Pi should be discoverable, pairable and recognized as an audio device.

  1. Manually run the agent by executing
sudo /usr/local/bin/a2dp-agent
  1. Attempt to pair and connect with the Raspberry Pi using your phone or computer.
  2. The agent should output the accepted and rejected Bluetooth UUIDs
A2DP Agent Registered
AuthorizeService (/org/bluez/hci0/dev_94_01_C2_47_01_AA, 0000111E-0000-1000-8000-00805F9B34FF)
Rejecting non-A2DP Service
AuthorizeService (/org/bluez/hci0/dev_94_01_C2_47_01_AA, 0000110d-0000-1000-8000-00805f9b34fb)
Authorized A2DP Service
AuthorizeService (/org/bluez/hci0/dev_94_01_C2_47_01_AA, 0000111E-0000-1000-8000-00805F9B34FF)
Rejecting non-A2DP Service

If the Raspberry Pi is not recognized as a audio device, ensure that the bluealsa package was installed as part of the Initial Setup

Install The A2DP Bluetooth Agent As A Service

To make the A2DP Bluetooth Agent run on boot copy the included file bt-agent-a2dp.service to /etc/systemd/system. Now run the following command to enable the A2DP Agent service

sudo systemctl enable bt-agent-a2dp.service

Bluetooth devices should now be able to discover, pair and connect to the Raspberry Pi without any user intervention.

Testing Audio Playback

Now that Bluetooth devices can pair and connect with the Raspberry Pi we can test the audio playback.

The tool bluealsa-aplay is used to forward audio from the Bluetooth device to the ALSA output device (sound card).

Execute the following command to accept A2DP audio from any connected Bluetooth device.

bluealsa-aplay -vv 00:00:00:00:00:00

Play a song on the Bluetooth device and the Raspberry Pi should output audio on either the headphone jack or the HDMI port. See this guide for configuring the audio output device of the Raspberry Pi.

Make Volume Control work (AVCTP)

To make the volume control work over AVCTP you have to configure triggerhappy. Create the audio.conf file and change the triggerhappy.service. Type sudo systemctl edit --full triggerhappy.service and change user nobody into pi. Caused by a bug in debian you need to disable triggerhappy.socket by executing sudo systemctl disable triggerhappy.socket.

Reboot and enjoy!

#!/usr/bin/python
from __future__ import absolute_import, print_function, unicode_literals
import sys
import dbus
import dbus.service
import dbus.mainloop.glib
try:
from gi.repository import GObject
except ImportError:
import gobject as GObject
AGENT_INTERFACE = "org.bluez.Agent1"
AGENT_PATH = "/test/agent"
class Rejected(dbus.DBusException):
_dbus_error_name = "org.bluez.Error.Rejected"
class Agent(dbus.service.Object):
exit_on_release = True
def set_exit_on_release(self, exit_on_release):
self.exit_on_release = exit_on_release
@dbus.service.method(AGENT_INTERFACE,
in_signature="", out_signature="")
def Release(self):
print("Release")
if self.exit_on_release:
mainloop.quit()
@dbus.service.method(AGENT_INTERFACE,
in_signature="os", out_signature="")
def AuthorizeService(self, device, uuid):
print("AuthorizeService (%s, %s)" % (device, uuid))
if uuid.lower() == "0000110d-0000-1000-8000-00805f9b34fb":
print("Authorized A2DP Service")
return
if uuid.lower() == "0000111e-0000-1000-8000-00805f9b34fb":
print("Authorized HFP Service")
return
if uuid.lower() == "00001108-0000-1000-8000-00805f9b34fb":
print("Authorized HSP Service")
return
if uuid.lower() == "0000110e-0000-1000-8000-00805f9b34fb":
print("Authorized AVRCP Service")
return
print("Rejecting non-A2DP Service, %s", uuid.lower())
raise Rejected("Connection rejected")
@dbus.service.method(AGENT_INTERFACE,
in_signature="o", out_signature="s")
def RequestPinCode(self, device):
print("RequestPinCode (%s)" % (device))
return "0000"
@dbus.service.method(AGENT_INTERFACE,
in_signature="o", out_signature="u")
def RequestPasskey(self, device):
print("RequestPasskey (%s)" % (device))
return dbus.UInt32("password")
@dbus.service.method(AGENT_INTERFACE,
in_signature="ouq", out_signature="")
def DisplayPasskey(self, device, passkey, entered):
print("DisplayPasskey (%s, %06u entered %u)" %
(device, passkey, entered))
@dbus.service.method(AGENT_INTERFACE,
in_signature="os", out_signature="")
def DisplayPinCode(self, device, pincode):
print("DisplayPinCode (%s, %s)" % (device, pincode))
@dbus.service.method(AGENT_INTERFACE,
in_signature="ou", out_signature="")
def RequestConfirmation(self, device, passkey):
print("RequestConfirmation (%s, %06d)" % (device, passkey))
return
@dbus.service.method(AGENT_INTERFACE,
in_signature="o", out_signature="")
def RequestAuthorization(self, device):
print("RequestAuthorization (%s)" % (device))
raise Rejected("Pairing rejected")
@dbus.service.method(AGENT_INTERFACE,
in_signature="", out_signature="")
def Cancel(self):
print("Cancel")
if __name__ == '__main__':
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
agent = Agent(bus, AGENT_PATH)
obj = bus.get_object("org.bluez", "/org/bluez");
manager = dbus.Interface(obj, "org.bluez.AgentManager1")
manager.RegisterAgent(AGENT_PATH, "NoInputNoOutput")
print("A2DP Agent Registered")
manager.RequestDefaultAgent(AGENT_PATH)
mainloop = GObject.MainLoop()
mainloop.run()
# /etc/triggerhappy/triggers.d/audio.conf
# Change mixer volume when pressing the appropriate keys (or holding them)
KEY_VOLUMEUP 1 /usr/bin/amixer -q -M set Digital 5%+
KEY_VOLUMEUP 2 /usr/bin/amixer -q -M set Digital 5%+
KEY_VOLUMEDOWN 1 /usr/bin/amixer -q -M set Digital 5%-
KEY_VOLUMEDOWN 2 /usr/bin/amixer -q -M set Digital 5%-
# /etc/default/bluealsa
OPTIONS="--a2dp-volume -p a2dp-sink -p hfp-hf -p hsp-hs"
# /etc/default/bluealsa-aplay
OPTIONS="--pcm-buffer-time=170000 --profile-a2dp"
[Unit]
Description=A2DP Bluetooth Agent
After=bluetooth.service
Wants=bluetooth.service
[Service]
ExecStartPre=/bin/sh -c "echo discoverable on | bluetoothctl"
ExecStart=/usr/bin/python -u /usr/local/bin/a2dp-agent
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=A2DP-Agent
RestartSec=1
Restart=always
[Install]
WantedBy=bluetooth.service
@tloki
Copy link

tloki commented Jun 21, 2019

Hi!

I was wondering if I could use your setup to do the following:

Have RPi as A2DP sink (for playing music from phone to it) and as HFP (for receiving calls) at the same time? (i.e. playing music but when someone calls me it would automagicaly switch to HFP? And return to playing music on call end)

Also, in my Citroen C4 2007, I was able to capture steering wheel commands and display arbitrary data on LCD using CAN BUS (the cheapo MCP2515 IC from eBay) - I was wondering if one could display what song is being played back - how do I get that info from python script at the moment of playing? And also - I wanted to change the song (or stop/resume song) using steering wheel commands. And finally - make a simple "UI" for connecting phones via car LCD and steering wheel...

@Pindar
Copy link
Author

Pindar commented Jun 23, 2019

Hi @tloki ! Thank you for following up on this thread. Unfortunately I can't answer your questions because I haven't done any of the things you're referring to. Nevertheless I like your ideas and hope you're going to find a proper solution – maybe you can share some of your findings here? Thank you and have a lot of fun setting it up!

@Ciantic
Copy link

Ciantic commented Jul 7, 2019

I'm looking for exactly what @tloki is doing too. I want to take phone calls using Raspberry Pi 4. My goal though is to use my PC headphones and mic for it. My setup will be:

Phone <---bluetooth---> Raspberry Pi 4 <---ethernet---> PC <---wire---> Headset

So far I think this seems a worthy to look at: http://nohands.sourceforge.net/index.html github: https://github.com/heinervdm/nohands

But also, I want to pipe my keyboard and mouse over bluetooth too, so I can control iPad with PC keyboard and mouse just by clicking a button. Lot's of small things to do.

@crky14
Copy link

crky14 commented Nov 17, 2019

Hey, unfortunately my phone still shows that device do not support Media Volume Sync. Do you have any idea how to fix this ?
Thanks

@Pindar
Copy link
Author

Pindar commented Dec 25, 2019

I'm sorry, I have no idea :/

@v-prgmr
Copy link

v-prgmr commented Apr 25, 2020

Loving this Gist. Thank you so much. Works like a charm (y)

@Pindar
Copy link
Author

Pindar commented Apr 25, 2020

You're welcome and thank you!

@siggemannen
Copy link

Hi,

I followed this gist and it's great, thanks a lot!
A few things that someone might hit a snag with:

  • I had some trouble with building blualsa, they have a wiki page https://github.com/Arkq/bluez-alsa/wiki/Installation-from-source with better instructions than in readme. For raspbian you basically need to install a couple of extra libs to get stuff to work, sbc and a few others.
  • You don't mention bluealsa.service but i installed it as service and it seem to do the trick.

@Gwalchgwyn
Copy link

Gwalchgwyn commented Oct 2, 2020

Thank you very much for this as its been very helpful so far. I had it working perfectly with my OnePlus 6, but i have a need to connect multiple, new devices from time to time. To test this i made the RPi and phone "forget" each other and then try to connect from fresh. When i try now it requests a pairing pin, which means it doesn't auto-connect on the RPi's side. Any ideas?

EDIT: ignore all that. I enabled the service again and it seems to be working fine now even after multible reboots and pairing/forgetting. My new problem is that audio will no longer stream - if i test with bluealsa-aplay -vv 00:00:00:00:00:00, i get an error message: bluealsa-aplay: E: Couldn't open PCM: Device or resource busy

@Gwalchgwyn
Copy link

Gwalchgwyn commented Oct 3, 2020

Thank you very much for this as its been very helpful so far. I had it working perfectly with my OnePlus 6, but i have a need to connect multiple, new devices from time to time. To test this i made the RPi and phone "forget" each other and then try to connect from fresh. When i try now it requests a pairing pin, which means it doesn't auto-connect on the RPi's side. Any ideas?

EDIT: ignore all that. I enabled the service again and it seems to be working fine now even after multible reboots and pairing/forgetting. My new problem is that audio will no longer stream - if i test with bluealsa-aplay -vv 00:00:00:00:00:00, i get an error message: bluealsa-aplay: E: Couldn't open PCM: Device or resource busy

Figured something out - i'm a complete newbie to all this stuff and had never programmed anything until about 2 months ago. I spent ages thinking i mightve forgotten to install some sort of library or codec or something that was preventing the audio coming out of the aux out of the RPi. I'm running the RPi off a touchscreen with a second monitor running from the HDMI socket on the RPi. I had made sure that i was outputting sound to the "Analog" output on the sound settings, rather than HDMI. I also made sure of this in the raspi-config thing, which labelled the HDMI output as "0" and Analog at "1". After many frustrating hours and building again from scratch, i tried again but without the HDMI plugged in. It worked perfectly immediately.

Theres something weird going on where bluealsa was trying to output audio to HDMI rather than Analog despite it being selected to do so. I might be wrong (and probably am, as i know next to nothing about any of this), but if you're having a similar issue, make sure the only option for sound to go is through the option you want, in my case that was the mini-jack/aux/analog output on the RPi board.

@v-prgmr
Copy link

v-prgmr commented Feb 4, 2021

Ahhh god. I spent two days to get it back to a working state. I had issues with the "Resource busy", "No usable service", "GDBus.org,bluealsa protocol not available", and so on.

I finally installed blueman after giving up all hopes using the following command "sudo apt-get install pi-bluetooth blueman" and now I could control using the blueman gui and connect to whichever device I wanted and it works again.

If anyone has any of the above-mentioned problems, feel free to tag me in a comment, I will see how and if I can help you.

@Pindar
Copy link
Author

Pindar commented May 27, 2021

I updated the instructions to the latest blue-alsa and Raspbian 10 (buster).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment