Use a Bluetooth speaker as a Lyrion Music Server (formerly Logitech Media Server) player, on any Linux machine with a Bluetooth radio.
Your speaker appears in Lyrion like any other player. Switch it on and it connects and starts playing; switch it off and it gets out of the way. There's a four-band parametric EQ, two-way volume sync, an idle timeout so the speaker can power itself down, and optional controls inside the Lyrion web UI.
squeezebt status
squeezebt eq gentle
squeezebt check
Bluetooth speakers and LMS have never got on well. The existing options are squeezelite-bluetooth (ALSA/bluealsa) and piCorePlayer's built-in support. Both work, but they leave several problems unsolved:
Volume that fights itself. squeezelite applies Lyrion's volume as software attenuation while the speaker's own AVRCP volume is a separate stage. The two multiply, so the speaker's setting becomes a ceiling Lyrion can never exceed — turn the speaker down and the app simply cannot get louder. squeezebt drives the hardware volume properly and keeps both ends in sync.
SBC when your speaker can do better. bluealsa is effectively SBC-only. squeezebt uses PipeWire, so you get whatever the speaker actually supports — aptX, aptX HD, LDAC, AAC — negotiated automatically.
Bluetooth silently disabled on headless machines. WirePlumber gates its Bluetooth monitor on an active seat session. On a headless server there isn't one, so it never starts, no A2DP endpoints are registered, and every connection attempt fails with an error that explains nothing. This is the single most common reason "it just doesn't work" on a server, and squeezebt fixes it.
Speakers that never power down. Holding the audio stream open keeps the speaker awake indefinitely, flattening its battery. squeezebt disconnects when nothing is playing so the speaker's own auto-power-off can do its job.
It also adds a parametric EQ (useful for taming a bright speaker, or a hard tiled bathroom), battery and link-quality reporting, a health check that diagnoses the failure modes below, and a control panel for the Lyrion web UI.
Not distro-specific — it needs a stack, not a particular package manager:
| BlueZ 5.50+ | and a working Bluetooth adapter |
| PipeWire 0.3.50+ | with libspa-0.2-bluetooth (this is what provides A2DP) |
| WirePlumber | or another PipeWire session manager |
| squeezelite | built with PulseAudio output — the ALSA-only build cannot reach PipeWire's Bluetooth sink |
| systemd | user services, with lingering enabled |
| curl, python3 |
Known good: Debian 12+, Raspberry Pi OS (Bookworm/Trixie), Ubuntu 23.04+, Fedora 38+, Arch. Tested on Raspberry Pi OS Trixie (arm64) on a Pi 4.
PulseAudio instead of PipeWire is not supported: the EQ uses PipeWire's filter-chain, and the codec handling differs.
Check before installing anything:
./install.sh --checkIt reports exactly what's missing and why it matters.
git clone https://github.com/wheelybird/squeezebt.git
cd squeezebt
./install.sh --check # verify prerequisites
./install.sh --deps # optional: install them (apt / dnf / pacman)
./install.sh # install files and units
squeezebt setup # pair your speaker, interactively
./install.sh --services # start everything
squeezebt status~/.local/bin needs to be on your PATH.
squeezebt setup scans, lets you pick your speaker from a list, pairs it, and
asks for the Lyrion server address. Re-running it is safe.
To remove everything: ./install.sh --uninstall.
squeezebt mode radio # or: resumeradio — the player runs permanently, so it's always present in Lyrion and always playable, even with the speaker switched off. When the speaker connects you join whatever is already playing, mid-track. Like walking into a room where the radio is already on.
resume — the player runs only while the speaker is connected, so it comes and goes in Lyrion. Lyrion's per-player Power On Resume setting restarts the queue where you left off. Nothing plays to an empty room.
Both share one audio path; the mode only decides whether the player runs:
squeezelite → squeezebt_eq (filter chain, EQ) → squeezebt_eq_out → speaker
squeezebt eq list # all presets, with what each does
squeezebt eq gentle
squeezebt eq custom 100 0 1000 0 7000 2.0 -6 6500 -2Four bands per channel: low shelf, mid peak, presence peak, high shelf. Presets are pushed to the live filter chain at runtime — switching never restarts PipeWire, drops the Bluetooth link, or interrupts playback, so you can A/B while a track plays.
Presets fall into two groups. Taming a bright or harsh speaker — gentle,
sibilance, strong, warm, hardroom (the last trims treble and bass for
tiled bathrooms and kitchens). And conventional voicings — flat,
extrabass, rock, pop, jazz, classical, vocal.
Note that rock/pop/jazz/classical all lift treble, so on an already-bright
speaker they make things worse. squeezebt eq list says so too.
EQ corrects frequency response, not distortion. If your speaker has a response peak this fixes it; if the harshness is nonlinear driver distortion, a cut just turns that band down along with the music.
Lyrion's volume slider drives the speaker's real hardware volume over AVRCP, and the speaker's own buttons update the slider. Both directions, automatically.
This is less trivial than it sounds — see gotcha 7 below.
squeezebt idle # show
squeezebt idle 30 # minutes
squeezebt idle 0 # never disconnectDefault 15 minutes. With nothing playing for that long, squeezebt disconnects so the speaker reaches its own auto-power-off instead of being held awake.
Waking it again differs by mode, and this is inherent rather than a limitation of the code:
- resume — the Lyrion player doesn't exist while the speaker is away, so there's no play button. Switch the speaker on; it reconnects itself.
- radio — the player is always there, so pressing play re-pages the speaker. That works only while it's still powered on. Once it has actually switched itself off, nothing can wake it remotely — no Bluetooth source can. That's the speaker's hardware.
squeezebt check # verify the whole chain, say what is wrong
squeezebt info # battery and radio link quality
squeezebt codec # list codecs the speaker offers; switch with: codec sbc_xq
squeezebt disconnect # free the speaker for a phone
squeezebt connect # take it back
squeezebt logs # recent watcher activitysqueezebt check is the first thing to run when something is wrong. Every one
of its checks corresponds to a real failure mode that produces a
healthy-looking system doing the wrong thing silently.
Battery needs the speaker to expose a GATT Battery Service and BlueZ to
have resolved it — many speakers don't, and some do so only intermittently. It
reports unknown rather than guessing. Link quality needs passwordless
sudo for hcitool. RSSI here is dB relative to the receiver's golden range,
not dBm: 0 is ideal, within about ±10 is healthy. Both are useful when
deciding where to put the machine.
If you use Material Skin, squeezebt can add controls to the player's menu — no plugin required, because Material supports user-defined actions.
You get a control panel (live status, battery, all EQ presets with descriptions, playback mode, connect/disconnect) plus one-tap actions for the things worth a single press. The section is keyed by player id, so nothing appears under your other players.
squeezebt lms-actions --install--install deliberately refuses rather than guessing at a remote path. Print
the JSON and put it on the server:
# on the squeezebt machine
squeezebt lms-actions > actions.json
scp actions.json you@lyrion-host:/tmp/
# on the Lyrion machine
sudo mkdir -p /var/lib/squeezeboxserver/prefs/material-skin
sudo cp /tmp/actions.json /var/lib/squeezeboxserver/prefs/material-skin/actions.json
sudo chown -R squeezeboxserver: /var/lib/squeezeboxserver/prefs/material-skin
sudo chmod 644 /var/lib/squeezeboxserver/prefs/material-skin/actions.jsonThe prefs directory varies by install — check for
/var/lib/squeezeboxserver/prefs, /var/lib/lyrion/prefs,
/usr/local/slimserver/prefs or ~/.squeezeboxserver/prefs, and match the
ownership of the files already there.
If you already have an actions.json, merge rather than replace: it is a
single JSON object keyed by section, so add squeezebt's "<player-id>": [...]
entry alongside your existing ones.
curl http://LYRION-HOST:9000/material/customactions.jsonThat is the file as Material sees it. Then force-reload the Lyrion page — Material caches it, so a normal refresh may not pick up changes.
The actions are keyed by player id, which is derived from the speaker's MAC.
Pair a different speaker and the id changes, so the old entries become
orphaned and silently stop appearing. Re-run squeezebt lms-actions and
reinstall.
The controls talk to a small HTTP API on port 8099 (/status, /eq/<preset>,
/check, …). It has no authentication. It can change EQ and mode and
connect the speaker — nothing destructive, no shell or filesystem access from a
request — but keep it on your LAN.
Each of these presents as "it doesn't work" with no useful error. They're documented because they cost real time to find, and because you may hit them with a different setup.
1. Bluetooth ships rfkill soft-blocked on many distros. The adapter reports
Powered: no, scans fail with org.bluez.Error.NotReady, and the device list
comes back empty — indistinguishable from the speaker being switched off.
2. WirePlumber gates Bluetooth on an active seat session. Its main profile
enables support.logind and monitor.bluez.seat-monitoring; a headless box
reached over SSH has no seat, so the Bluetooth monitor silently never starts.
PipeWire then registers no A2DP endpoints and every connection fails with:
org.bluez.Error.Failed br-connection-profile-unavailable
a2dp-source profile connect failed: Protocol not available
Upstream disables both in mixin.systemwide-session, which does not apply to
a lingering user session. Verify with:
sudo journalctl -u bluetooth | grep -c "Endpoint registered" # want ~20, not 03. Pairing without Pairable: yes bonds nothing. Pairing reports success
and Paired: yes, but no link key is written, so the speaker can never
reconnect after a power cycle — it drops into pairing mode looking like it
forgot your machine. Bonded: yes is the check that matters, not Paired.
Note PairableTimeout = 0 only stops pairability expiring; it doesn't enable
it at boot.
4. PipeWire will switch your speaker to the HFP headset profile, which uses
mSBC — narrowband mono. Symptom: squeezebt reports codec msbc and it sounds
like a phone call. Fixed by removing the headset roles entirely.
5. squeezelite does not survive a PipeWire restart. Its sink is destroyed,
it never reconnects, and Lyrion goes on cheerfully reporting "playing" while no
audio is produced. Fixed with PartOf=pipewire.service.
6. A stale output target silently bypasses everything. If the player is
wired straight to the Bluetooth sink rather than through the EQ, every preset
applies correctly to a filter nothing passes through, and nothing you do has
any audible effect. squeezebt check catches this; so does:
pactl list sink-inputs # squeezelite must be on squeezebt_eq7. Lyrion's volume and the speaker's volume multiply. squeezelite has no
mixer option in its PulseAudio build (-V is ALSA-only), so it attenuates in
software while the speaker's AVRCP volume applies separately. Symptom: the
speaker's setting acts as a ceiling.
The obvious fix — setting the player's digitalVolumeControl pref to 0 — does
stop the software attenuation, but it also greys out the volume slider in the
Lyrion web UI. squeezebt instead pins the player's stream to unity and drives
the hardware volume, so the slider keeps working.
Four user services:
squeezebt-player |
squeezelite, playing into the EQ sink |
squeezebt-watch |
connect/disconnect handling, routing, idle timeout |
squeezebt-volume |
two-way volume sync |
squeezebt-api |
HTTP control API and web panel |
Config lives in ~/.config/squeezebt/config, runtime state in
~/.config/squeezebt/state/. Drop-ins are installed to
~/.config/pipewire/pipewire.conf.d/ and
~/.config/wireplumber/wireplumber.conf.d/ — all per-user, nothing system-wide
except the BlueZ adapter settings.
The Lyrion player id is derived from the speaker's MAC (locally-administered bit set), so it's stable across restarts and unique per speaker. That's what lets the player keep its playlist and settings when it comes and goes.
- One speaker per machine. Multi-speaker support would need templated units; not done yet.
- Battery reporting is best-effort — see above.
- Tested against one speaker (KEF Muo 2) on one machine. Bluetooth is notoriously device-specific; reports of other speakers are welcome.
- Codec choice doesn't persist: PipeWire re-picks by priority on reconnect.
- The web panel is served over plain HTTP, so it will be blocked as mixed content if you put Lyrion behind HTTPS.
MIT — see LICENSE.