Skip to content

Instantly share code, notes, and snippets.

@diogocera
Last active May 13, 2026 12:42
Show Gist options
  • Select an option

  • Save diogocera/19ec909ab2ccc87610096e4dbd7838f2 to your computer and use it in GitHub Desktop.

Select an option

Save diogocera/19ec909ab2ccc87610096e4dbd7838f2 to your computer and use it in GitHub Desktop.
Raspberry Pi as a Bluetooth speaker (headless, persistent)
# Raspberry Pi as a Bluetooth speaker (headless, persistent)
A guide to turning a headless Raspberry Pi into a persistent Bluetooth audio sink — so any paired device can connect and play audio without you needing to SSH in first.
## Prerequisites
- Raspberry Pi running Raspberry Pi OS (Bullseye or later)
- BlueZ and PulseAudio installed (included by default on most Pi OS images)
- An audio output — onboard audio, HDMI, or a USB DAC
- A Bluetooth device to connect from (Mac, iPhone, Android, etc.)
---
## 1. Install bt-agent
`bt-agent` keeps a persistent Bluetooth pairing agent running so the Pi can accept incoming connections without an interactive `bluetoothctl` session.
```bash
sudo apt install bluez-tools
```
Create `/etc/systemd/system/bt-agent.service`:
```ini
[Unit]
Description=Bluetooth Agent
After=bluetooth.service
Requires=bluetooth.service
[Service]
ExecStart=/usr/bin/bt-agent -c NoInputNoOutput
Restart=always
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl enable --now bt-agent
```
---
## 2. Configure BlueZ
Edit `/etc/bluetooth/main.conf`:
```ini
[General]
Discoverable=true
DiscoverableTimeout=0
Class=0x240404
[Policy]
AutoEnable=true
```
- `Discoverable=true` and `DiscoverableTimeout=0` make the Pi permanently visible to Bluetooth devices.
- `Class=0x240404` advertises the Pi as an audio device so connecting devices treat it as a speaker.
```bash
sudo systemctl restart bluetooth
```
Pair your device once interactively, then trust it so it can reconnect automatically:
```bash
bluetoothctl
# inside the shell:
scan on
# wait for your device to appear, then:
pair XX:XX:XX:XX:XX:XX
trust XX:XX:XX:XX:XX:XX
```
---
## 3. Find your audio sink
You need the exact PulseAudio sink name for your audio output. Run:
```bash
pactl list sinks short
```
You'll see output like:
```
0 alsa_output.platform-bcm2835_audio.analog-stereo ... SUSPENDED
1 alsa_output.usb-Cambridge_Audio_DAC100_USB-00.analog-stereo ... RUNNING
```
Pick the sink you want audio to come out of. Common options:
| Output | Sink name pattern |
|---|---|
| Pi onboard 3.5mm jack | `alsa_output.platform-bcm2835_audio.analog-stereo` |
| HDMI | `alsa_output.platform-*.hdmi-stereo` |
| USB DAC | `alsa_output.usb-<manufacturer>_<model>-00.analog-stereo` |
Copy the full sink name — you'll need it in the next steps.
---
## 4. Configure PulseAudio
**`/etc/pulse/daemon.conf`** — disable idle exit so PulseAudio doesn't quit when nothing is playing.
Find the line `; exit-idle-time = 20` and uncomment it, setting the value to `-1`:
```bash
sudo sed -i 's/; exit-idle-time = .*/exit-idle-time = -1/' /etc/pulse/daemon.conf
# verify:
grep exit-idle-time /etc/pulse/daemon.conf
# should show: exit-idle-time = -1
```
> Note: the line may appear as `; exit-idle-time = 20` or `; exit-idle-time = -1` depending on your OS version — the `sed` command handles both.
**`/etc/pulse/default.pa`** — add at the bottom, replacing `<YOUR_SINK>` with the name you found above:
```
set-default-sink <YOUR_SINK>
load-module module-switch-on-connect
```
Example with a USB DAC:
```
set-default-sink alsa_output.usb-Cambridge_Audio_DAC100_USB-00.analog-stereo
load-module module-switch-on-connect
```
Example with onboard audio:
```
set-default-sink alsa_output.platform-bcm2835_audio.analog-stereo
load-module module-switch-on-connect
```
Also confirm that these lines are present in `default.pa` (they usually are by default):
```
load-module module-bluetooth-policy
load-module module-bluetooth-discover
```
---
## 5. Keep PulseAudio alive without a login session
By default PulseAudio only runs while a user is logged in. These steps keep it running on a headless Pi.
**Enable session lingering** so the user's systemd services survive after logout:
```bash
sudo loginctl enable-linger $USER
```
**Add a restart policy** so PulseAudio recovers if it crashes. Use `on-failure` (not `always`) to avoid conflicting with socket activation:
```bash
mkdir -p ~/.config/systemd/user/pulseaudio.service.d
cat > ~/.config/systemd/user/pulseaudio.service.d/override.conf << 'EOF'
[Service]
Restart=on-failure
RestartSec=5
EOF
systemctl --user daemon-reload
systemctl --user restart pulseaudio
```
> **Do not mask `pulseaudio.socket`** — doing so breaks PulseAudio's ability to restart itself after a crash. The combination of `exit-idle-time = -1` and `Restart=on-failure` is sufficient to keep it running.
---
## 6. Create the Bluetooth loopback service
When a Bluetooth device connects, PulseAudio sees it as an audio *source*. This script watches for that source to appear and connects it to your output sink via a loopback module.
**Find your device's Bluetooth source name** after pairing and connecting once:
```bash
pactl list sources short
```
Look for a line like:
```
2 bluez_source.F8_4D_89_90_30_F0.a2dp_source ... RUNNING
```
The format is always `bluez_source.<MAC_WITH_UNDERSCORES>.a2dp_source`.
**A note on loopback latency**
The loopback module buffers audio between the Bluetooth source and the output sink. Setting this too low causes buffer underruns and audible glitches. The minimum usable value depends on your hardware — Bluetooth alone needs ~70ms, and a USB DAC adds another ~60ms, so the combined minimum is around 130ms. Use `400ms` as a safe starting point:
- If you hear glitches, increase it (try `500` or `600`).
- If you need lower audio latency (e.g. for video sync), try reducing it carefully — watch `journalctl --user -u pulseaudio` for `Too many underruns` warnings.
**Create the script** at `~/.config/pulse/bluetooth-loopback.sh`, replacing `<SOURCE>` and `<SINK>` with your values:
```bash
#!/bin/bash
SOURCE="bluez_source.<MAC_WITH_UNDERSCORES>.a2dp_source"
SINK="<YOUR_SINK>"
while true; do
if pactl list sources short | grep -q "$SOURCE"; then
if ! pactl list modules short | grep -q "source=$SOURCE"; then
pactl unload-module module-loopback 2>/dev/null
pactl load-module module-loopback source="$SOURCE" sink="$SINK" latency_msec=400
fi
fi
sleep 3
done
```
```bash
chmod +x ~/.config/pulse/bluetooth-loopback.sh
```
**Create the systemd service** at `~/.config/systemd/user/bt-loopback.service`:
```ini
[Unit]
Description=Bluetooth Loopback Manager
After=pulseaudio.service
Requires=pulseaudio.service
[Service]
ExecStart=/home/<YOUR_USERNAME>/.config/pulse/bluetooth-loopback.sh
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
```
> Replace `<YOUR_USERNAME>` with your actual user (e.g. `pi`).
```bash
systemctl --user daemon-reload
systemctl --user enable --now bt-loopback
```
---
## 7. Reboot and test
```bash
sudo reboot
```
After the Pi comes back up, connect your device via Bluetooth without SSH open. Audio should route through within a few seconds of connecting.
---
## Troubleshooting
| Symptom | Check |
|---|---|
| Device won't connect | `bluetoothctl show` — is `Discoverable: yes`? |
| Connects but no audio | `pactl list sources short` — does `bluez_source.*` appear? |
| Audio stops after SSH exit | `systemctl --user status pulseaudio` — is it still `active (running)`? |
| PulseAudio failed to restart | Check `exit-idle-time = -1` is uncommented in `daemon.conf`; check `pulseaudio.socket` is **not** masked |
| Loopback not loading | `journalctl --user -u bt-loopback -n 30` |
| Audio glitches / dropouts | `journalctl --user -u pulseaudio -n 50 \| grep underrun` — if you see `Too many underruns`, increase `latency_msec` in the loopback script |
| PulseAudio keeps restarting | `journalctl --user -u pulseaudio -n 30` — check for module load errors |
| Wrong output device | `pactl list sinks short` — confirm your sink name and update the script |
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment