My headset was connected but not recognized. So I built a one-click app.
A USB headset-with-mic that shows up in lsusb but never appears as an audio device is the most frustrating Linux desktop bug there is. Here's the debug, the manual fix, and the small app I built so nobody else has to remember it.
The app: live health status, four one-click fixes, auto-refreshing every 5 seconds.
Last week I plugged in my Poly Blackwire 3310 headset and... nothing. No audio through it, no mic. The kind of problem where you open the sound settings, see the wrong device selected, click the right one, and it's not even in the list. The headset was physically connected, the OS knew it was there, but PulseAudio had decided it didn't exist.
This is the story of debugging it, the three-line fix, and why I then spent an evening building a tiny desktop app so the next time it happens — to me, or to anyone — it's one click instead of a terminal session.
The problem: detected at every layer, broken at the top one
The first thing I checked was whether the hardware was even seen. It was — at every layer below the one that matters:
$ lsusb
Bus 003 Device 026: ID 047f:430b Plantronics, Inc. Poly Blackwire 3310 Series
$ cat /proc/asound/cards
1 [Series]: USB-Audio - Poly Blackwire 3310 Series
$ aplay -l # card 1, device 0 — playback works
$ arecord -l # card 1, device 0 — capture worksUSB enumerated it. ALSA owned it. I could even aplay and arecord directly to it. The hardware was fine. But PulseAudio — the layer every desktop app actually talks to — had loaded the card and set the profile, yet never created the sink or source. The devices apps need simply didn't exist. A stuck state: the card object was there, the profile said "1 sink, 1 source", and the sink/source themselves were missing.
The manual fix
I tried the obvious thing first — cycling the profile off and back on:
CARD="alsa_card.usb-Plantronics_Poly_Blackwire_3310_Series_..."
pactl set-card-profile "$CARD" off
pactl set-card-profile "$CARD" output:mono-fallback+input:mono-fallbackNo luck — the sink still didn't appear. The daemon itself was wedged. The fix turned out to be the bluntest instrument possible:
pulseaudio -k # kill it; it auto-respawns
# ...wait two seconds...
pactl list sinks short # the headset sink is now thereOn restart, PulseAudio correctly enumerated the headset, created the sink and source, and — because module-switch-on-connect was already loaded — made it the default. Audio and mic, working. Total time once I knew the answer: about five seconds.
But here's the thing: I will forget this. My partner will never run pulseaudio -k. And the stuck state isn't the only failure mode — sometimes the headset works but isn't the default, and sometimes module-switch-on-connect isn't loaded so plugging in doesn't switch anything. Three different bugs, three different incantations, all things a non-technical user will never type.
So I built a one-click app
I build machin — a programming language shaped for AI agents to write cheaply, compiling through C to a single native binary. One of its domains is desktop GUIs via raylib through a C FFI. So the obvious dogfood: turn the three fixes into a small window with a status panel and four buttons. No terminal, no man pages, no remembering.
The result is machin-headset-manager — a 900 kB binary you download and run. It shows live health status (daemon running? headset connected? sink/source OK or stuck? what's the current default? auto-switch enabled?) and gives you one button per failure mode:
curl -LO https://github.com/javimosch/machin-headset-manager/releases/download/v1.0.0/headset-manager-linux-amd64
chmod +x headset-manager-linux-amd64
./headset-manager-linux-amd64Click Restart Daemon for the stuck state. Click Set as Default when the headset works but apps send audio elsewhere. Click Enable Auto-Switch once so every future plug-in selects the headset automatically. Status refreshes itself every 5 seconds. It detects any USB audio headset, not just mine, by matching the alsa_card.usb-* name prefix.
The interesting part: a non-game raylib GUI
machin's raylib FFI path was built for games — it's how the game demo series (Snake, 2048, a 3D physics sandbox, a neural-net lander) all render. This is the first time it's been used for a utility app, and the nice surprise is that it composes cleanly. The whole app is ~380 lines of MFL across three files:
audio.src— drivespactl/pulseaudiovia machin'sexec()andsystem()builtins, parses the output, detects the headset generically.ui.src— the raylib FFI boundary plus drawing helpers, including a word-wrap routine built onMeasureTextso long device names don't overflow the window.app.src— the main loop. Everypactlcall runs in a goroutine; results come back over a channel, so the window never freezes while a command runs.
That last point is the one I'm proudest of. A GUI that freezes for two seconds while pulseaudio -k runs feels broken. Machin's Go-flavored goroutines and channels make the non-blocking version the natural one to write — and the compiler infers it's race-free at compile time, no annotations. The status struct is the only shared state, written only by the main goroutine after receiving from the channel. Share by communicating.
The point
The real bug here isn't PulseAudio's stuck state — it's that the fix for a common desktop problem lives behind a terminal command a normal person will never run. Every Linux user hits some version of this. The headset works in Windows. It works on the Mac. On Linux it "doesn't" — not because the driver is bad, but because the recovery path is hostile.
A 900 kB binary with four buttons doesn't fix PulseAudio. It fixes the recovery path. And it turns out that's the part that matters — for me, for my partner, and for anyone whose headset "doesn't work on Linux" when really it just needs pulseaudio -k wrapped in a button.
The app is open source at github.com/javimosch/machin-headset-manager — download the binary from the v1.0.0 release, or build it from source with ./build.sh. It's listed in the awesome-machin ecosystem.
machin is open source at github.com/javimosch/machin — the ecosystem lives at awesome-machin. Built by Javier Arancibia, the same engineering that goes into intrane.fr.