Introduction

I am at the point where I almost cannot shower without listening to music or a podcast. However, this has historically presented a challenge for me, because it meant that upon getting out of the shower, I would have to pick up the speaker I was using to play music and carry it around with me as I got dressed, or else lose my precious tunes.

One solution to this would be to purchase multiple bluetooth speakers and place them in different rooms. Unfortunately, the bluetooth signal on my phone isn’t quite strong enough to remain connected to a speaker in my kitchen while I’m in the bathroom, and vice-versa. Plus, it’s a bit of a hassle to manually turn on all of the speakers I wanted to use, adding yet another step to my pre-shower ritual.

What I really needed with a bluetooth jukebox, a host that would be in a stationary and ideally central location, and maintain a constant connection to all of the speakers that I wanted to use. Then I could connect to the jukebox with my phone and use it to broadcast the music I wanted to play. The range issue could be solved with a sufficiently powerful plug-and-play bluetooth adapter.

Tutorial

Prerequisites

For my bluetooth jukebox, I chose a Beelink Mini PC. I used NixOS for the operating system, and installed pipewire and wireplumber to use for audio, for their good bluetooth support.

Finding Bluetooth Adapter Address

To get started, I needed to collect some information about the available bluetooth adapters, namely the MAC address of each one.

The simple way to accomplish this is to issue the bluetoothctl list command, to list the current bluetooth adapters on the system, then unplug the plug-and-play adapter, and re-issue the command to see which of them sticks around.

If, for some reason, you can’t or don’t want to unplug the plug-and-play adapter, you can try the following method instead:

  1. Issue lsusb , which will display a list of the bluetooth devices connect to your system, along with their bus and device numbers. For example,
...
Bus 001 Device 003: ID 0bda:a729 Realtek Semiconductor Corp. Bluetooth 5.3 Radio
...
  1. Issue ls -lA /sys/class/bluetooth , which will give you information about the system’s HCI devices. The path to which each HCI device is symlinked will correspond to the bus and device number of its USB device. In the following example, I know hci0 corresponds to the same USB device displayed above because the .../usb1/1-3/1-3:1.0/... in its path have the same Bus and Device numbers.
...
hci0 -> ../../devices/pci0000:00/0000:00:15.0/usb1/1-3/1-3:1.0/bluetooth/hci0
...
  1. Issue hciconfig -a to display information about each of the HCI devices. In these results, the bluetooth MAC address are displayed as BD Address, for example:
hci0:   Type: Primary  Bus: USB
        BD Address: 8C:88:4B:45:CC:11  ACL MTU: 1021:6  SCO MTU: 255:12
        UP RUNNING PSCAN
        RX bytes:244898176 acl:14952 sco:3194773 events:639409 errors:0
        TX bytes:640454708 acl:638153 sco:3191892 commands:580 errors:0
        Features: 0xff 0xff 0xff 0xfe 0xdb 0xfd 0x7b 0x87
        ...

Going forwards, we will assume that the MAC address of your plug-and-play adapter is FF:FF:FF:FF:FF:FF, and the MAC address of your device’s built-in adapter is AA:AA:AA:AA:AA:AA.

Connecting Bluetooth Devices

Next, we will connect the devices to which you wish to play audio from your jukebox. This StackOverflow question gives a good explanation of the limits of the number of devices which can be connected to a single bluetooth adapter.

In order to do this, we want to perform the following steps:

bluetoothctl
select FF:FF:FF:FF:FF:FF
scan on 

You should begin seeing nearby bluetooth devices pop up, along with their associated MAC addresses. Let’s suppose you have a device with a MAC address of 11:11:11:11:11:11 which you have found and you want to connect to.

connect 11:11:11:11:11:11
pair 11:11:11:11:11:11
trust 11:11:11:11:11:11

Depending upon the exact device, you may need to perform some special steps in order to pair it, such as entering a passcode or accepting the pair request.

Once you have all your speakers connected, your next step is to connect your phone. When connecting your phone, you will want to connect using the internal bluetooth adapter.* Supposing your phone’s MAC address is 99:99:99:99:99:99, you would perform the following steps:

bluetoothctl
select AA:AA:AA:AA:AA:AA
scan on
...
connect 99:99:99:99:99:99
pair 99:99:99:99:99:99
trust 99:99:99:99:99:99

*The reason for this is because, in my experimenting, I found that when connecting devices that will function as both sources and sinks for audio, the quality drops significantly for whichever type of devices was connected second. I suspect this is because the adapter prioritizes the profile of the first device which is connected.

It’s possible that this could be fixed by setting MultiProfile = multiple in /etc/bluetooth/main.conf, but I haven’t tried that myself.

Configuring Pipewire and Wireplumber

To set up multi-room playback on our jukebox, we first want to created a combined sink in Pipewire. This is a sink that will stream audio to all other available sinks. In /etc/pipewire/pipewire.conf.d, create a file named 50-combined-sink.conf with the following contents:

context.modules = [
        {
          name = libpipewire-module-combine-stream
          args = {
            combine.mode = sink
            node.name = "broadcast-sink"
            node.description = "broadcast-sink"
            -- combine.latency-compensate = true   # if true, match latencies by adding delays
            combine.props = {
              audio.position = [ MONO ]
            }
            stream.props = {
            }
            stream.rules = [
              {
                matches = [ { media.class = "Audio/Sink" } ]
                actions = { create-stream = { } }
              }
            ]
          }
        } 
      ]

After restarting Pipewire with systemctl --user restart pipewire.service, you should see a new sink created when you issue wpctl status :

Audio
 ├─ Devices:
 │      ...

 ├─ Sinks:
 │      40. broadcast-sink                      [vol: 1.00]
 │      ...

 ├─ Sink endpoints:

 ├─ Sources:
 │      ...

 ├─ Source endpoints:

 └─ Streams:
        ...

Now we need to make sure that your phone connects to this sink as its source when you connect it to your jukebox. We can accomplish this by adding a new BlueZ monitor rule in Wireplumber.

To do this, go to the directory /etc/wireplumber/bluetooth.lua.d/ (creating it if it does not exist), and then add a file named 51-bluez-config.lua with following contents:

      bluez_monitor.enabled = true

      bluez_monitor.properties = {
        ["bluez5.enable-sbc-xq"] = true,
        ["bluez5.enable-msbc"] = true,
        ["bluez5.enable-hw-volume"] = true,
        ["bluez5.codecs"] = "[sbc sbc_xq]",
        ["bluez5.headset-roles"] = "[ hsp_hs hsp_ag hfp_hf hfp_ag ]"
      }

      bluez_monitor.rules = {
        {
          matches = {
            {
              -- This matches all cards.
              { "device.name", "matches", "bluez_card.*" },
            },
          },
          apply_properties = {
            ["bluez5.auto-connect"]  = "[ a2dp_sink a2dp_source hfp_hf hsp_hs ]",
          }
        },
        {
          matches = {
            {
              -- Pixel 4a 5G
              { "node.name", "matches", "bluez_input.99_99_99_99_99_99.2" },
            },
          },
          apply_properties = {
            ["target.object"] = "broadcast-sink",
          }
        },
      }

This configuration has three pieces:

Once this is complete, you should be able to restart Wireplumber with systemctl --user restart wireplumber, and then play audio from your phone and hear it in high quality from all your connected devices.