Last active
May 13, 2026 12:42
-
-
Save diogocera/19ec909ab2ccc87610096e4dbd7838f2 to your computer and use it in GitHub Desktop.
Raspberry Pi as a Bluetooth speaker (headless, persistent)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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