September 02, 2022
Do you have an old pair of PC speakers, or an old Hi-Fi, that you would like to convert into a pair of Bluetooth® speakers to play music from your phone? A Raspberry Pi can be easily used as an audio bridge between a Bluetooth® device and an analog speaker system, to make this possible. In this quick guide, I will show you how to set up the software on a Pi, using PipeWire, to achieve this.
In my demonstration setup, I am using a Raspberry Pi 3, which embeds a Bluetooth® chipset, and I am connecting an analog speaker to the 3.5mm jack. For the software, I am using RaspberryPi OS Lite with a recent PipeWire version installed from the Debian testing (Bookworm) repositories, as the version included in RaspberryPi OS (which is based on Debian Bullseye) is too old to support this use case.
PipeWire is able to output sound to the internal audio chipset without any special configuration. It provides Bluetooth® A2DP support with optional codecs (SBC-XQ, LDAC, aptX, aptX HD, aptX-LL, FastStream) out of the box. At the same time, WirePlumber automatically creates the connection between the A2DP source and the audio chipset when a remote device, like a phone or a laptop, connects. This makes the configuration very easy, as PipeWire will work out of the box. We will only need to set up BlueZ to make the system headless.
Let's see how this is done.
First, install RaspberryPi OS Lite (64-bit) to your SD card (assuming
/dev/sdb is the SD card device on your PC/laptop):
$ xzcat 2022-04-04-raspios-bullseye-arm64-lite.img.xz | sudo dd of=/dev/sdb bs=1M status=progress
Connect the Raspberry Pi to a display & keyboard, boot it from this SD card, and complete the OS configuration.
After the OS configuration is complete, install PipeWire and WirePlumber from the testing (Bookworm) repository:
$ echo 'APT::Default-Release "stable";' | sudo tee /etc/apt/apt.conf.d/99defaultrelease $ echo "deb http://ftp.de.debian.org/debian/ testing main contrib non-free" | sudo tee /etc/apt/sources.list.d/testing.list $ sudo apt update $ sudo apt -t testing install pipewire wireplumber libspa-0.2-bluetooth
The RaspberryPi OS Lite version automatically logs in the user created during the setup and this will automatically start PipeWire and WirePlumber. This is all that's needed for setting up PipeWire.
Next, we will need to set up a BlueZ pairing agent to accept pairings and A2DP connections. The reason we are doing this is because the target system is not going to have a user interface and we don't want to connect to it using ssh and type commands every time we want to pair a new device to it.
As this will require the DBus Python support, let's install this first:
$ sudo apt install python3-dbus
Then, copy the
speaker-agent.py python script and its related systemd unit file from GitHub pw_wp_bluetooth_rpi_speaker to your user home directory on the Raspberrry Pi.
speaker-agent.py python script, also shown below, will set the Raspberry Pi Bluetooth® adapter as always discoverable and will allow pairing and A2DP connections:
#!/usr/bin/python3 # SPDX-License-Identifier: LGPL-2.1-or-later import dbus import dbus.service import dbus.mainloop.glib from gi.repository import GLib BUS_NAME = 'org.bluez' AGENT_INTERFACE = 'org.bluez.Agent1' AGENT_PATH = "/speaker/agent" A2DP = '0000110d-0000-1000-8000-00805f9b34fb' AVRCP = '0000110e-0000-1000-8000-00805f9b34fb' bus = None 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): # Always authorize A2DP and AVRCP connection if uuid in [A2DP, AVRCP]: print("AuthorizeService (%s, %s)" % (device, uuid)) return else: print("Service rejected (%s, %s)" % (device, uuid)) raise Rejected("Connection rejected by user") @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) mainloop = GLib.MainLoop() # By default Bluetooth adapter is not discoverable and there's # a 3 min timeout # Set it as always discoverable adapter = dbus.Interface(bus.get_object(BUS_NAME, "/org/bluez/hci0"), "org.freedesktop.DBus.Properties") adapter.Set("org.bluez.Adapter1", "DiscoverableTimeout", dbus.UInt32(0)) adapter.Set("org.bluez.Adapter1", "Discoverable", True) print("RPi speaker discoverable") # As the RPi speaker will not have any interface, create a pairing # agent with NoInputNoOutput capability obj = bus.get_object(BUS_NAME, "/org/bluez") manager = dbus.Interface(obj, "org.bluez.AgentManager1") manager.RegisterAgent(AGENT_PATH, "NoInputNoOutput") print("Agent registered") manager.RequestDefaultAgent(AGENT_PATH) mainloop.run()
The systemd unit starts the speaker agent on boot as RaspberryPi OS Lite automatically logs in the user:
[Unit] Description=Bluetooth speaker agent [Service] ExecStart=python speaker-agent.py [Install] WantedBy=default.target
This systemd unit will need to be placed in
~/.config/systemd/user/ and enabled manually using:
$ systemctl --user enable speaker-agent.service
Finally, configure the BlueZ daemon to allow re-pairing without user interaction:
$ sudo sed -i 's/#JustWorksRepairing.*/JustWorksRepairing = always/' /etc/bluetooth/main.conf
Now, connect the audio output of your Raspberry Pi to a speaker or your Hi-Fi system, reboot, pair, and connect your phone.
Enjoy the sound! ;)
Earlier this year, I joined Collabora for a six-month internship to learn how V4L2 (Video4Linux2) supports stateless video hardware decoding,…
With the upcoming 0.5 release, WirePlumber's configuration system will be moving to a JSON syntax to define settings, bringing a more unified…
Venus is a virtual Vulkan driver based on the Virtio-GPU protocol, which defines the serialization of Vulkan commands between guest and…
Taking one step towards democratizing the daunting task of dataset generation by making image synthesis and automatic ground truth data…
Using open source software, Collabora has developed an efficient compression pipeline that enables a face video broadcasting system that…
Introducing new common code for Mesa Vulkan drivers to support a new Vulkan extension, making it easier for app and game authors to manage…