Skip to content

Making cliamp play sound on WSL2

cliamp on WSL2 often plays (progress bar advances, “Streaming” lights up) while producing no sound. WSLg ships a working PulseAudio server over RDP that paplay can use out of the box, so WSLg itself is fine. cliamp’s Linux audio output uses ALSA, and on a default WSL2 distro there is no ALSA device at all - /dev/snd is absent. cliamp then opens its default PCM, fails silently at startup, and never retries. The fix is the pulseaudio-alsa ALSA PCM plugin, which gives cliamp a default PCM that routes into WSLg’s Pulse server.

Everything here was reproduced on the dev box (Arch WSL2, WSLg, cliamp v1.x): the silent playback state, the missing /dev/snd, and the fixed state after installing the plugin.

FactConsequence
WSL2 has no sound hardware/dev/snd does not exist. Any ALSA call that opens a hardware card (hw:0) fails.
WSLg runs a PulseAudio server in the system distro and projects the socket back as /mnt/wslg/PulseServerA Pulse-native app (paplay, pactl, ffplay) works with zero setup. The host-side audio is carried over the RDP channel by WSLg’s RDPSink plugin.1
WSLg pre-sets PULSE_SERVER=/mnt/wslg/PulseServerDo not override PULSE_SERVER in shell startup files; overriding it is the most common way to break WSLg audio.2
cliamp is a Go app built on Beep + OtoThe Oto release cliamp pins (v3.4.1) outputs on Linux through ALSA only: it opens the ALSA default PCM through libasound (snd_pcm_*). Newer Oto releases use PulseAudio first and fall back to ALSA. cliamp’s own device list comes from pactl.3
With no ALSA PCM plugin installed, aplay -l and cliamp’s default open failcliamp starts with no device, shows the track advancing, and makes no sound. It does not surface the failure.
pulseaudio-alsa installs one file: /etc/alsa/conf.d/99-pulseaudio-default.confThat file defines an ALSA default PCM of type pulse, so any app opening the ALSA default is routed into the running Pulse server.
cliamp reads ~/.config/cliamp/config.toml at startup and writes back on quitSettings chosen in the TUI (theme, EQ, visualiser, provider) persist; the audio device can also be pinned explicitly (below).

Rule out the Windows side first. Generate a test tone and play it through the Pulse socket:

Terminal window
ffmpeg -f lavfi -i 'sine=frequency=440:duration=2' -f wav /tmp/beep.wav -y
paplay /tmp/beep.wav

You should hear a 440 Hz tone on the Windows host. While it plays, confirm the stream exists and the sink is running:

Terminal window
pactl list short sink-inputs # should show a paplay entry
pactl list sinks | grep State # should show: State: RUNNING

If the beep is audible, WSLg is fine and the problem is downstream of Pulse - i.e. in how cliamp reaches it. If the beep is silent, stop here and fix WSLg first (start with wsl --shutdown and a relaunch; check that no shell rc file overrides PULSE_SERVER).

Fix cliamp: install the ALSA-to-Pulse plugin

Section titled “Fix cliamp: install the ALSA-to-Pulse plugin”

cliamp opens ALSA’s default PCM. With no plugin installed, that name resolves to a nonexistent hardware card and the open fails. Install the plugin that maps default to Pulse:

Terminal window
sudo pacman -S --noconfirm pulseaudio-alsa alsa-utils

pulseaudio-alsa provides the PCM routing; alsa-utils provides speaker-test / aplay for verification.

Restart cliamp (it opened its device at startup and never retried), start playback, and confirm cliamp itself now appears as a Pulse stream:

Terminal window
cliamp play
# in another shell, while it plays:
pactl list short sink-inputs

The list should contain a cliamp entry against RDPSink, and the sink should be RUNNING. Before the fix, the symptom was an empty sink-inputs list and a SUSPENDED sink while cliamp’s UI claimed to be streaming.

Work from the Pulse server outward:

CheckCommandWhat it proves
WSLg socket existsls /mnt/wslg/PulseServerWSLg is running; if missing, wsl --shutdown and relaunch
Pulse reachablepactl info | head -3Confirms Server Name: pulseaudio and Default Sink: RDPSink
PULSE_SERVER not overriddenecho $PULSE_SERVERShould be unix:/mnt/wslg/PulseServer (WSLg pre-sets it)
ALSA plugin installedpacman -Ql pulseaudio-alsa | grep conf.dShould list /etc/alsa/conf.d/99-pulseaudio-default.conf
ALSA default opensspeaker-test -D default -c2 -l1Audible pink noise proves the full ALSA -> Pulse -> RDP path
cliamp stream existspactl list short sink-inputs while cliamp playsEmpty list = cliamp never connected; the UI lies about playback state

In the original failure the last two rows showed it: speaker-test had no ALSA default to open (no plugin), and pactl list short sink-inputs stayed empty while cliamp showed “Streaming”.

cliamp writes its state back to ~/.config/cliamp/config.toml on exit, so settings changed in the TUI (theme via cliamp theme <name>, visualiser, EQ curve, repeat/shuffle, volume) survive a restart without any extra step.

Pin the audio device explicitly, because on WSL2 you want cliamp to keep using the Pulse-routed default rather than rediscovering a device each launch. Set it once from the CLI:

Terminal window
cliamp device list # shows the available output devices, e.g. RDPSink
cliamp device RDPSink # selects it; persisted to config.toml

Or edit config.toml directly and add the equivalent key:

~/.config/cliamp/config.toml
audio-device = "RDPSink"

A restart then keeps the choice. Verify with cliamp status (shows the active device) or by checking that pactl list short sink-inputs lists cliamp after playback starts.

Setting up a Subsonic-API server (Navidrome and friends)

Section titled “Setting up a Subsonic-API server (Navidrome and friends)”

cliamp supports Navidrome natively, and Navidrome speaks the Subsonic API - so any Subsonic-compatible server (Gonic, LMS/Lyrion via its Subsonic bridge, Ampache, Airsonic-Advanced, Navidrome itself) works through the same [navidrome] block.4

The fastest path is the interactive wizard, which validates the connection and writes the config:

Terminal window
cliamp setup

Manual configuration is four keys in ~/.config/cliamp/config.toml:

provider = "navidrome" # make it the default source at startup
[navidrome]
url = "http://your-server:4533"
user = "your-username"
password = "${NAVIDROME_PASSWORD}" # read from env, keeps the secret out of the file

cliamp also honours the environment variables NAVIDROME_URL, NAVIDROME_USER, and NAVIDROME_PASS directly - exporting them before launch is enough for a session without touching the config file.

Once configured, cliamp --provider navidrome (or just cliamp with provider = "navidrome" set) opens on the server’s playlists. N inside the TUI opens the full Navidrome browser (by album, by artist, or artist -> album -> track). Streams arrive as MP3 from the server’s Subsonic /rest/stream endpoint.

  • cliamp’s Navidrome provider uses the Subsonic API (/rest/getAlbumList2, /rest/search3, /rest/stream), which is why any server implementing that API works through the same block - the provider name in cliamp stays navidrome regardless of which server sits behind it.
  • Lyrion (formerly Logitech Media Server) has its own native [lyrion] block in cliamp; use that rather than the Subsonic bridge if the server is LMS.
  • The UI says “Streaming” but no sound plays. cliamp does not surface an ALSA open failure - the track timer advances and the UI reports streaming while no Pulse stream exists. pactl list short sink-inputs is the ground truth; trust it over the UI.
  • Audio worked once, then stopped after a restart. cliamp opened its device at startup and never retried. After installing pulseaudio-alsa, fully quit and relaunch cliamp - toggling play/pause in the running instance does not reopen the device.
  • Do not set PULSE_SERVER yourself. WSLg pre-populates it to unix:/mnt/wslg/PulseServer; a stray export PULSE_SERVER=... in .zshrc/.bashrc pointing at a TCP or wrong socket is the usual way to break all WSLg audio.2
  • speaker-test plays but cliamp is still silent. Check that cliamp’s selected device is the Pulse-routed one (cliamp device list / cliamp device RDPSink), not a stale hardware name left over from before the plugin was installed.
  • WSLg audio crackles or underruns under load. A known WSLg issue with no userspace fix; the RDPSink plugin drops samples under buffer pressure.5 It is separate from the no-sound-at-all problem this guide fixes.
CommandPurpose
paplay /tmp/beep.wavSmoke-test the WSLg Pulse path end to end
pactl list short sink-inputsShow live playback streams (cliamp should appear here)
pactl list sinksShow sink state (RDPSink, RUNNING vs SUSPENDED)
speaker-test -D default -c2 -l1Prove the ALSA default PCM opens and reaches Pulse
sudo pacman -S pulseaudio-alsa alsa-utilsInstall the ALSA -> Pulse plugin (the fix)
cliamp device list / cliamp device <name>Inspect / persist the output device
cliamp statusShow the active device and playback state
cliamp setupInteractive wizard for Navidrome and other providers
cliamp --provider navidromeLaunch straight into the Subsonic provider
  1. microsoft/WSLg, project README (PulseAudio server in the system distro, RDPSink plugin, PULSE_SERVER preconfiguration). https://github.com/microsoft/WSLg ↩

  2. microsoft/WSLg, issue #29 (“No audio”) - overriding PULSE_SERVER breaks WSLg audio; do not set it in shell rc files. https://github.com/microsoft/wslg/issues/29 ↩ ↩2

  3. ebitengine/oto, v3.4.1 driver_unix.go (cgo ALSA, snd_pcm_open on default); the current README describes the newer PulseAudio-first backend with an ALSA fallback. https://github.com/hajimehoshi/oto ↩

  4. bjarneo/cliamp, docs/navidrome.md (Subsonic API integration, env vars, provider controls). https://github.com/bjarneo/cliamp/blob/main/docs/navidrome.md ↩

  5. microsoft/WSLg, issue #1429 (“WSLg audio crackling and latency - RDPSink underruns”). https://github.com/microsoft/wslg/issues/1429 ↩