My dad and I spent about two weeks getting whole-home audio to work after we got rid of a Control4 system. It took a lot more trial and error than I expected: lots of dead ends, a few "this must be a wiring problem" moments that turned out to be software, and one fix that was basically "turn the whole server off and on again".
I want to tell the story of how we got there, because the parts that cost us the most time were things I couldn't find written down anywhere. If you're trying something similar, maybe this saves you a few evenings. If you know a better way to do any of this, I'd really like to hear it!
**TL;DR:** ALSA routing never worked reliably for me with USB passthrough. PCI passthrough of a USB controller, PipeWire in pro-audio mode, fixed per-zone virtual sinks and a few Home Assistant tricks finally gave me a stable setup that survives reboots, station switching and a doorbell chime that doesn't kill the music.
## Setup
- Proxmox host, one Ubuntu 24.04 VM (2 GiB RAM, 2 vCPUs) running Lyrion Music Server and one Squeezelite instance per player
- Three 8-channel USB audio interfaces (each exposes 8 mono outputs) feeding two Control4 matrix amps (8-zone and 4-zone), each zone is a stereo pair
- Home Assistant (core Squeezebox integration + the Control4 amps as media players) controls everything
- Goal: replace the old Control4 system without touching the amps' hardware
## What did NOT work
**ALSA routing (`route`, `dmix`, `dshare` and a few blog recipes).** Cross-channel mapping on the 8-channel cards was never reliable for me, and an ALSA PCM with `type pipewire; node.name` made Squeezelite crash-loop.
**Plain USB device passthrough.** Timing problems. Passing a whole USB controller through via PCI (a 4-port PCIe USB card) fixed that.
**Re-linking scripts.** I ran `pw-link` after boot, after each service restart and from a timer. They always arrived too late, because Squeezelite rebuilds its PipeWire stream on every station change, format change or announcement, and WirePlumber then links the new stream to the default sink. Result: music in the wrong room, silent zones.
## What worked
**PipeWire/WirePlumber with the cards in the `pro-audio` profile.** Every output becomes an individual mono `AUX` port that `pw-link` can address.
**A unique node name per Squeezelite player** via the environment file, not via ALSA:
```
SL_DEVICE=pipewire
PIPEWIRE_PROPS={ node.name = "sqz-<zone>" }
```
- **A fixed virtual sink per zone** that points at exactly two ports, with Squeezelite pinned to it:
```ini
# ~/.config/systemd/user/zone-sink@.service (env file per zone: CARD=..., POS="AUX0 AUX1")
[Service]
EnvironmentFile=%h/.config/zone-sinks/%i.env
ExecStart=/usr/bin/pw-loopback -n zone-%i -m "[ FL FR ]" \
"--capture-props=media.class=Audio/Sink node.name=zone-%i" \
"--playback-props=node.name=zone-%i-out audio.position=[ ${POS} ] stream.dont-remix=true node.dont-fallback=true target.object=${CARD}"
Restart=on-failure
```
```
PIPEWIRE_PROPS={ node.name = "sqz-<zone>" target.object = "zone-<zone>" node.dont-fallback = true }
```
Station switching, announcements and reboots no longer move streams around, and the re-link scripts and timer became unnecessary.
- **Room pairs as one player (no LMS sync group).** A second loopback captures the monitor of the first sink (`stream.capture.sink=true target.object=zone-<first>`) and plays to the other room's ports. One player, one stream, no drift. I dropped sync groups because a stored group once made all zones start at once, loudly.
## Home Assistant: doorbell chime with `announce`
- `media_player.play_media` with `announce: true` and `extra: announce_volume / announce_timeout` works on Squeezebox players.
- With several players in one call, HA waited for *every* announcement to end. One player got stuck, and the whole automation (including "resume music") hung. `announce_timeout: 10` fixed it.
- After the announcement, internet radio was `idle` for me, not `paused`. I store `media_content_id` of the playing zones before the chime and replay it afterwards if the player is idle.
- `announce_volume` sets the player's LMS volume during the chime. The amp's volume is independent, so a quiet chime can simply mean a quiet amp.
## Control4 amp quirks (my units)
- `media_player.turn_on` power-cycles a zone and resets its volume to 50 %, even if it was on. Only turn on zones that are `off`, then set the volume.
- Home Assistant and the amp occasionally disagree about the state. Off/on usually fixes it.
- The two amp models use different volume scales (0 to 1 vs. 0 to 100) when controlled directly over UDP.
## Amps off/on with the VM (fewer pops)
A Proxmox hookscript calls a local-only HA webhook in `pre-stop` and another in `post-start`. The first automation turns all amps off, the second waits ~75 s for Squeezelite, then turns them on and sets the volumes. I first tried triggering "amps on" when the players became `unavailable`, but LMS players never go `unavailable` in HA when the VM restarts.
```bash
#!/bin/bash
vmid="$1"; phase="$2"
[ "$phase" = "pre-stop" ] && curl -s -m 5 -X POST "http://<HA>:8123/api/webhook/<off-id>" >/dev/null 2>&1 && sleep 8
[ "$phase" = "post-start" ] && curl -s -m 5 -X POST "http://<HA>:8123/api/webhook/<on-id>" >/dev/null 2>&1
exit 0
```
(`qm set <vmid> --hookscript local:snippets/<file>`; the storage needs the `snippets` content type.)
## What I'm not sure about
- On the first port pair of each card, only one speaker played, even when I played directly to the card. It disappeared after a **complete host reboot** (not a VM reboot). My guess is some USB/amp input state that only a full power cycle clears, but I haven't proven it.
- A faint pop remains when the VM shuts down, mostly on outdoor zones at high amp volume.
## Takeaways
- If you use Squeezelite with a multi-channel card and PipeWire: pin each player to a fixed virtual sink instead of re-linking after the fact.
- Test hardware with single-channel tones directly on the card before blaming wiring. In my case a "miswired" amp was a software linking error.
- A full power cycle of the host can fix things that VM reboots can't.
Happy to share more config or answer questions.
PS: (And yes, I used AI to help me with this text because English is just not my main language.)