Use USB Speaker as Audio Output
Let HA output audio via USB Speaker
The Problem#
Home Assistant can generate audio (TTS, alerts), but getting it to actually come out of a USB speaker on a Linux box can be weirdly painful—mostly because audio services love fighting over devices and permissions.
In this post, I’ll show how I routed HA audio through a USB speaker using MPD, how to find the right output device, and how to fix the classic “device busy” / “PipeWire connection refused” traps so sound finally plays where it should.
The Solution#
HA talks to MPD (port 6600), and MPD plays through PipeWire’s PulseAudio-compatible socket. Two things go wrong:
- MPD can’t find PipeWire. MPD runs as a system service, but PipeWire runs inside your user session. A system service has no
XDG_RUNTIME_DIR/PULSE_SERVER, so it doesn’t know where your PipeWire socket is →Connection refused. Fix: tell it where the socket is. - MPD plays to the wrong device. If you have more than one USB audio device, PipeWire’s default sink may not be your speaker. Fix: pin the speaker’s sink name in
mpd.conf.
How to Do It#
1. Find your speaker#
katrina@homeserver:~$ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: Device [USB Audio Device], device 0: USB Audio [USB Audio] # <== This one
Subdevices: 1/1
card 1: HID [USB Audio and HID], device 0: USB Audio [USB Audio] # Another USB device
Subdevices: 1/1
card 2: PCH [HDA Intel PCH], device 3: HDMI 0 [HDMI 0] # HDMI (note: device 3, not 0)
...shkatrina@homeserver:~$ cat /proc/asound/cards
0 [Device ]: USB-Audio - USB Audio Device # <== This one
GeneralPlus USB Audio Device at usb-0000:00:14.0-3, full speed
1 [HID ]: USB-Audio - USB Audio and HID
CSCTEK USB Audio and HID at usb-0000:00:14.0-6, full speedshWhat we actually need is the PipeWire sink name, which is stable:
katrina@homeserver:~$ pactl list short sinks
47 alsa_output.usb-GeneralPlus_USB_Audio_Device-00.analog-stereo PipeWire s16le 2ch 48000Hz SUSPENDED
49 alsa_output.usb-CSCTEK_USB_Audio_and_HID_A34004801402-00.analog-stereo PipeWire s16le 2ch 48000Hz SUSPENDEDshSUSPENDED just means idle. Copy the name of your speaker’s sink (the alsa_output.usb-... string). Not sure which one it is? Play a tone on one sink at a time and listen:
paplay --device=alsa_output.usb-GeneralPlus_USB_Audio_Device-00.analog-stereo /usr/share/sounds/alsa/Front_Center.wavshpaplay needs a WAV file. /usr/share/sounds/alsa/Front_Center.wav ships with alsa-utils. If you don’t have it, any short WAV works.
2. Ensure MPD is installed and running#
katrina@homeserver:~$ systemctl status mpd
● mpd.service - Music Player Daemon
Loaded: loaded (/lib/systemd/system/mpd.service; enabled; preset: enabled)
Active: active (running) since ...shIf it isn’t running:
sudo systemctl enable --now mpdsh3. Configure MPD#
Open the config: sudo nano /etc/mpd.conf
# Listen on all interfaces so HA can reach it
bind_to_address "0.0.0.0"
# Port HA connects to
port "6600"
# Output through PipeWire (via its PulseAudio-compatible socket)
audio_output {
type "pulse"
name "PipeWire Output"
sink "alsa_output.usb-GeneralPlus_USB_Audio_Device-00.analog-stereo" # <== your sink name from step 1
}shAlso check the user line near the top of the file:
grep -n '^\s*#\?\s*user' /etc/mpd.confsh#user "mpd"(commented out) → the system service runs as root. This is what the fix in step 4 was tested with.user "mpd"(active) → runs as thempduser, which cannot enter/run/user/1000/(that directory isdrwx------, owned by you), so step 4 won’t work. Either comment the line out, or use the user-service alternative below.
Restart and check:
sudo systemctl restart mpd
systemctl status mpd
ss -tuln | grep 6600 # port should be listeningsh4. Fix “Connection refused” (the main fix)#
Run a quick test first:
mpc clear
mpc add https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3
mpc play
sleep 3
mpc statusshIf you see this, you’re in the trap:
[paused] #1/1 0:00/6:13 (0%)
ERROR: Failed to enable output "PipeWire Output" (pulse); pa_context_connect() has failed: Connection refusedshpactl list short sinks works for you because your shell knows where PipeWire lives. MPD, as a system service, does not. Confirm the cause:
katrina@homeserver:~$ ps -o user,pid,cmd -C mpd
USER PID CMD
root 218263 /usr/bin/mpd --systemd # system service, no PULSE_SERVER in its environment
katrina@homeserver:~$ ls -ld /run/user/1000/pulse
drwx------ 2 katrina katrina 80 ... /run/user/1000/pulse # only katrina can entershFix: give the service a systemd drop-in that points at your socket. Write it with a heredoc (safer to paste than a long one-liner):
sudo mkdir -p /etc/systemd/system/mpd.service.d
sudo tee /etc/systemd/system/mpd.service.d/pulse.conf <<'EOF'
[Unit]
After=user@1000.service
[Service]
Environment=PULSE_SERVER=unix:/run/user/1000/pulse/native
EOFshPULSE_SERVER=...tells MPD where PipeWire’s socket is.1000iskatrina’s UID (check withid -u).After=user@1000.servicemakes MPD start after your user session (and PipeWire) at boot. This relies on lingering being enabled:loginctl show-user katrina | grep Lingershould sayLinger=yes.
Apply and verify:
sudo systemctl daemon-reload
sudo systemctl restart mpd
systemctl cat mpd | grep PULSE # should print the Environment= linesh5. Test#
mpc clear
mpc add https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3
mpc play
sleep 3
mpc statusshExpected: [playing] with the time advancing and no pa_context_connect error, and sound from the speaker. Then trigger a TTS / media play from Home Assistant.
Alternative: run MPD as a user service#
Cleaner than a root system service, because MPD then lives inside your session and gets XDG_RUNTIME_DIR for free, but I haven’t tested this on my machine yet.
# 1. Stop and disable the system service AND its socket
sudo systemctl disable --now mpd mpd.socket
# 2. Create a user config (copy the system one, then edit paths)
mkdir -p ~/.config/mpd
cp /etc/mpd.conf ~/.config/mpd/mpd.conf
# - remove/comment the `user` line
# - point music_directory, playlist_directory, db_file, state_file at paths under your home
# - keep bind_to_address, port and the audio_output block
# 3. Enable the user unit shipped by Debian
systemctl --user enable --now mpdshLingering must be on so it starts at boot without you logging in (sudo loginctl enable-linger katrina). Note that systemctl disable mpd alone isn’t enough: mpd.socket will re-launch the system service on demand, so disable the socket too.
Troubleshooting#
| Symptom | Cause | Fix |
|---|---|---|
pa_context_connect() ... Connection refused | MPD (system service) can’t find PipeWire’s socket | Step 4 drop-in |
| Sound plays, but from the wrong device | PipeWire default sink isn’t your speaker | Add sink "..." to the audio_output block; or wpctl set-default <id> (wpctl status shows ids) |
speaker-test -D hw:N,0: -2 No such file or directory | N isn’t a USB card. HDMI devices are numbered 3, 7, 8, 9, not 0 | Check aplay -l for the real card/device numbers |
speaker-test: Channels count (1) not available | USB speaker only supports stereo | Use -c 2 |
speaker-test: Device or resource busy | PipeWire is holding the ALSA device | Test through PipeWire instead: speaker-test -D pulse -c 2, or paplay --device=<sink> file.wav |
mpc status: CURL failed: ... 404 on an /api/tts_proxy/...mp3 URL | HA’s TTS URLs are short-lived. MPD got a stale one | Not an audio problem. Trigger a fresh TTS from HA |
mpc volume shows n/a | The pulse output has no hardware mixer | Set volume in PipeWire: wpctl set-volume <sink-id> 0.5 |
| Works, then breaks after reboot | MPD started before your user session/PipeWire | Confirm the drop-in has After=user@1000.service and lingering is on; sudo systemctl restart mpd |
Summary#
Home Assistant audio plays through a USB speaker by routing output through MPD to PipeWire. The two fixes that matter: point the MPD system service at your user’s PipeWire socket with PULSE_SERVER (a systemd drop-in), and pin the speaker’s sink name in mpd.conf so it doesn’t depend on PipeWire’s default device or on USB card numbering.