Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

squeezebt

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

Why this exists

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.


Requirements

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 --check

It reports exactly what's missing and why it matters.


Install

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.


Two playback models

squeezebt mode radio     # or: resume

radio — 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

EQ

squeezebt eq list            # all presets, with what each does
squeezebt eq gentle
squeezebt eq custom 100 0 1000 0 7000 2.0 -6 6500 -2

Four 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 speakergentle, sibilance, strong, warm, hardroom (the last trims treble and bass for tiled bathrooms and kitchens). And conventional voicingsflat, 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.


Volume

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.


Idle timeout

squeezebt idle          # show
squeezebt idle 30       # minutes
squeezebt idle 0        # never disconnect

Default 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.

Other commands

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 activity

squeezebt 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.


Controls in the Lyrion web UI

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.

If Lyrion runs on this same machine

squeezebt lms-actions --install

If Lyrion runs elsewhere (the usual case)

--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.json

The 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.

Checking it worked

curl http://LYRION-HOST:9000/material/customactions.json

That 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.

Re-run it if the player id 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.


Seven gotchas this handles for you

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 0

3. 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_eq

7. 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.


How it works

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.


Limitations

  • 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.

Licence

MIT — see LICENSE.

About

Use a Bluetooth speaker as a Lyrion Music Server (LMS) player — PipeWire-based, with parametric EQ, two-way volume sync and idle power-down

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages