Skip to content

Instantly share code, notes, and snippets.

@qcasey
Forked from mill1000/README.md
Last active September 28, 2024 16:44
Show Gist options
  • Save qcasey/5bd4943b27320e65a033948fafb86d19 to your computer and use it in GitHub Desktop.
Save qcasey/5bd4943b27320e65a033948fafb86d19 to your computer and use it in GitHub Desktop.
Headless A2DP Audio Streaming on Ubuntu / Debian for non-raspbian SBCs (ODROID, Orange Pi, Armbian, etc)

About

This gist will show how to setup a generic SBC Debian / Ubuntu install 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

This is forked from another gist specific to the Raspberry Pi on Stretch. A required package isn't in Ubuntu's repos, so in this gist we build it from scratch.

Tested to be working on Armbian/Ubuntu/Debian images of the Orange Pi Zero, ODROID XU4, ODROID N2, and Atomic Pi.

Prerequisites

  • A linux SBC running Debian or Ubuntu
  • Bluetooth Dongle or integrated Bluetooth.
  • Sound card (internal or external) that has been set up to work with Alsa.

Auto-Install Script

curl https://gist.githubusercontent.com/qcasey/5bd4943b27320e65a033948fafb86d19/raw/2a3be1bc78e9ac30b744c2fdd332d035ac13d77d/bt-a2dp-autoinstall.sh | sudo bash

cURL-ing to bash can be dangerous, in the event of a hack. Please ensure what you're downloading first.

Manual 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 device to ensure the latest kernel is loaded.

Now install the required packages.

sudo apt-get install alsa git gcc make autoconf libtool blueman bluez bluetooth libbluetooth-dev libfdk-aac-dev libsbc-dev libasound2-dev libdbus-1-dev python-dbus glib-2.0-dev libperl-dev libgtk2.0-dev

Change over to your /tmp directory, and clone then build another requirement.

cd /tmp
git clone https://github.com/Arkq/bluez-alsa
cd bluez-alsa/
autoreconf --install
mkdir build && cd build
../configure --enable-aac --enable-ofono --enable-debug
make && sudo make install

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

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 connections from Bluetooth devices. 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

A Python 3 version has been generously provided by @abelmatser in another gist

Testing The Agent

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

Note: At this point the device will not output any audio. This step is only to verify the Bluetooth is discoverable and bindable.

  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-00805F9B34FB)
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-00805F9B34FB)
Rejecting non-A2DP Service

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

Thanks to @matthijskooijman for fixing up some issues in the Bluetooth Agent 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.

Install The Audio Playback As A Service

To make the audio playback run on boot copy the included file a2dp-playback.service to /etc/systemd/system. Now run the following command to enable A2DP Playback service

sudo systemctl enable a2dp-playback.service

We also need to install bluealsa as a service, copy that to /etc/systemd/system as well and enable it.

sudo systemctl enable bluealsa.service

Reboot and enjoy!

Low Volume Output

If you are experiencing low volume output, run alsamixer and increase the volume of the Pi's soundcard.

#!/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 == "0000110d-0000-1000-8000-00805f9b34fb":
print("Authorized A2DP Service")
return
print("Rejecting non-A2DP Service")
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()
[Unit]
Description=A2DP Playback
After=bluealsa.service syslog.service
Requires=bluealsa.service
[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
[Install]
WantedBy=multi-user.target
[Unit]
Description=BlueALSA Agent
After=bluetooth.service
Wants=bluetooth.service
[Service]
ExecStart=/bin/sh -c "bluealsa -p a2dp-source -p a2dp-sink"
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=BlueALSA-Agent
[Install]
WantedBy=bluetooth.service
#!/bin/sh
#
# Update, install prereqs
#
apt-get update
apt-get -y install python alsa git gcc make autoconf libtool blueman bluez bluetooth libbluetooth-dev libfdk-aac-dev libsbc-dev libasound2-dev libdbus-1-dev python-dbus glib-2.0-dev libperl-dev libgtk2.0-dev
#
# Clone and build bluez-alsa
#
cd /tmp
git clone https://github.com/Arkq/bluez-alsa
cd bluez-alsa/
autoreconf --install
mkdir build && cd build
../configure --enable-aac --enable-ofono --enable-debug
make && make install
#
# Backup old service files, if they exist
#
if [ -f /usr/local/bin/a2dp-agent ]; then
mv /usr/local/bin/a2dp-agent /usr/local/bin/a2dp-agent.old
fi
if [ -f a2dp-playback.service ]; then
systemctl disable a2dp-playback.service
mv a2dp-playback.service a2dp-playback.service.old
fi
if [ -f bt-agent-a2dp.service ]; then
systemctl disable bt-agent-a2dp.service
mv bt-agent-a2dp.service bt-agent-a2dp.service.old
fi
if [ -f bluealsa.service ]; then
systemctl disable bluealsa.service
mv bluealsa.service bluealsa.service.old
fi
#
# Backup old bluetooth conf if required, then download new configuration
#
if [ -f /etc/bluetooth/main.conf ]; then
mv /etc/bluetooth/main.conf /etc/bluetooth/main.conf.old
fi
curl https://gist.githubusercontent.com/qcasey/5bd4943b27320e65a033948fafb86d19/raw/07bea3ac973276723df70dd234f138b4bda31219/main.conf > /etc/bluetooth/main.conf
#
# Download and enable service files
#
curl https://gist.githubusercontent.com/qcasey/5bd4943b27320e65a033948fafb86d19/raw/07bea3ac973276723df70dd234f138b4bda31219/a2dp-agent > /usr/local/bin/a2dp-agent
curl https://gist.githubusercontent.com/qcasey/5bd4943b27320e65a033948fafb86d19/raw/07bea3ac973276723df70dd234f138b4bda31219/a2dp-playback.service > /etc/systemd/system/a2dp-playback.service
curl https://gist.githubusercontent.com/qcasey/5bd4943b27320e65a033948fafb86d19/raw/07bea3ac973276723df70dd234f138b4bda31219/bt-agent-a2dp.service > /etc/systemd/system/bt-agent-a2dp.service
curl https://gist.githubusercontent.com/qcasey/5bd4943b27320e65a033948fafb86d19/raw/07bea3ac973276723df70dd234f138b4bda31219/bluealsa.service > /etc/systemd/system/bluealsa.service
systemctl enable bt-agent-a2dp.service
systemctl enable a2dp-playback.service
systemctl enable bluealsa.service
chmod +x /usr/local/bin/a2dp-agent
printf "\nStarting services...\n"
systemctl restart bluetooth
systemctl restart bt-agent-a2dp.service
systemctl restart a2dp-playback.service
systemctl restart bluealsa.service
#
# Done!
#
printf "\nDone!\nYour default Alsa sound card looks like:\n"
printf "\n/etc/asound.conf\n"
cat /etc/asound.conf
printf "\n\nBluetooth should be running. Please restart to take effect.\nIf your Alsa sound card is properly set (see asound.conf above) audio should output correctly.\n"
printf "\nBy default the device will appear as 'BluetoothSpeaker'. This can be changed in /etc/bluetooth/main.conf.\n"
printf "\nHave a nice rest of your day :)\n\n"
[Unit]
Description=A2DP Bluetooth Agent
After=bluetooth.service
Wants=bluetooth.service
[Service]
ExecStartPre=/bin/sh -c "echo power on | bluetoothctl && echo discoverable on | bluetoothctl"
ExecStart=/usr/bin/python -u /usr/local/bin/a2dp-agent
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=A2DP-Agent
[Install]
WantedBy=bluetooth.service
[General]
# Default adaper name
# Defaults to 'BlueZ X.YZ'
Name = BluetoothSpeaker
# Default device class. Only the major and minor device class bits are
# considered. Defaults to '0x000000'.
Class = 0x200414
# 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
# How long to stay in pairable mode before going back to non-discoverable
# The value is in seconds. Default is 0.
# 0 = disable timer, i.e. stay pairable forever
#PairableTimeout = 0
# Automatic connection for bonded devices driven by platform/user events.
# If a platform plugin uses this mechanism, automatic connections will be
# enabled during the interval defined below. Initially, this feature
# intends to be used to establish connections to ATT channels. Default is 60.
#AutoConnectTimeout = 60
# Use vendor id source (assigner), vendor, product and version information for
# DID profile support. The values are separated by ":" and assigner, VID, PID
# and version.
# Possible vendor id source values: bluetooth, usb (defaults to usb)
#DeviceID = bluetooth:1234:5678:abcd
# Do reverse service discovery for previously unknown devices that connect to
# us. This option is really only needed for qualification since the BITE tester
# doesn't like us doing reverse SDP for some test cases (though there could in
# theory be other useful purposes for this too). Defaults to 'true'.
#ReverseServiceDiscovery = true
# Enable name resolving after inquiry. Set it to 'false' if you don't need
# remote devices name and want shorter discovery cycle. Defaults to 'true'.
#NameResolving = true
# Enable runtime persistency of debug link keys. Default is false which
# makes debug link keys valid only for the duration of the connection
# that they were created for.
#DebugKeys = false
# Restricts all controllers to the specified transport. Default value
# is "dual", i.e. both BR/EDR and LE enabled (when supported by the HW).
# Possible values: "dual", "bredr", "le"
#ControllerMode = dual
# Enables Multi Profile Specification support. This allows to specify if
# system supports only Multiple Profiles Single Device (MPSD) configuration
# or both Multiple Profiles Single Device (MPSD) and Multiple Profiles Multiple
# Devices (MPMD) configurations.
# Possible values: "off", "single", "multiple"
#MultiProfile = off
# Permanently enables the Fast Connectable setting for adapters that
# support it. When enabled other devices can connect faster to us,
# however the tradeoff is increased power consumptions. This feature
# will fully work only on kernel version 4.1 and newer. Defaults to
# 'false'.
#FastConnectable = false
Enable=Source,Sink,Media,Socket
[Policy]
# The ReconnectUUIDs defines the set of remote services that should try
# to be reconnected to in case of a link loss (link supervision
# timeout). The policy plugin should contain a sane set of values by
# default, but this list can be overridden here. By setting the list to
# empty the reconnection feature gets disabled.
#ReconnectUUIDs=00001112-0000-1000-8000-00805f9b34fb, 0000111f-0000-1000-8000-00805f9b34fb, 0000110a-0000-1000-8000-00805f9b34fb
# ReconnectAttempts define the number of attempts to reconnect after a link
# lost. Setting the value to 0 disables reconnecting feature.
#ReconnectAttempts=7
# ReconnectIntervals define the set of intervals in seconds to use in between
# attempts.
# If the number of attempts defined in ReconnectAttempts is bigger than the
# set of intervals the last interval is repeated until the last attempt.
#ReconnectIntervals=1, 2, 4, 8, 16, 32, 64
# AutoEnable defines option to enable all controllers when they are found.
# This includes adapters present on start as well as adapters that are plugged
# in later on. Defaults to 'false'.
AutoEnable=true
@Dax0s
Copy link

Dax0s commented Jul 30, 2023

This worked for me, thank you so much!

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