Built with Claude Code. See how these projects were built for what that means in practice.
SteelSeries Sonar gets the model right. Every kind of audio gets its own virtual device, your programs land on them, and each one can sit at a different level in your headphones than in whatever is recording you. That is genuinely the correct design, and I use it.
I just cannot stand using it.
So s0nar.slop is that model with a different engine and a different face. Three buckets — Game, Chat, Media — each with a volume. Turn Discord down without turning your game down. Underneath it is Voicemeeter Potato rather than SteelSeries' driver stack, which means it works with whatever headset is already plugged in.
Why it needs to exist at all
Windows can already send an application to a specific output device. It forgets on the next launch.
That is not an opinion, it is measured. The call the Settings app uses is named
SetPersistedDefaultAudioEndpoint, and despite the name it does not persist:
it moves a running process immediately, and the stored preference is not
honoured when that executable starts again. Relaunch Discord and it is back on
your default device.
So a rule — "discord.exe belongs in Chat" — is not something Windows can hold for you. s0nar.slop holds it, watches for the program to start making sound, and reapplies. You set it once. That single behaviour is the reason the app exists; everything else is a nicer surface on things that already worked.
The undocumented part
There is no public API for "put this app on that device". The Settings app does
it through IAudioPolicyConfigFactory, a WinRT interface that ships no
headers, no documentation and no type library. Every implementation reaches it
by counting vtable slots by hand, and the count differs by Windows build.
Getting that wrong does not throw an exception. It calls whatever function pointer happens to sit at that offset, with the wrong arguments.
So the slot number was confirmed against four independent implementations before a line was written — a Python script of mine that already worked, EarTrumpet's C#, a Rust project called nodio, and finally a throwaway Go spike that only ever read. Six base slots plus nineteen padding methods is twenty-five. The spike returned real data, against a live Discord process, at exactly the endpoint the mixer said Discord was on.
Only then did anything write.
What is actually mine
More than I expected. The obvious dependency for the Windows audio side is a library called go-wca, and the plan was to fork it and fix two open pull requests. I checked whether those bugs were real first:
GetMutepasses a pointer to Go's one-byteboolwhere COM writes a four-byteBOOL, quietly corrupting three adjacent bytes of stack — on a call the app list makes constantly- the metering interface declares two of its methods in the wrong order, so they dispatch to each other
Both were real. But the app needs four interfaces out of that library's forty, so they are written here instead — smaller than maintaining a fork of something last touched in 2023, and no third-party bugs on the hot path. Every vtable index is counted from the SDK header and named in a comment.
The bugs worth remembering
Three, all of which were invisible to every test that existed.
The feature was dead in the shipped binary and alive in every test of it.
RoInitialize returns RPC_E_CHANGED_MODE when a thread is already in a COM
apartment of the other kind, and the code treated that as fatal. Fyne puts its
threads in a single-threaded apartment; a plain console test program has no
apartment at all. So routing worked perfectly in isolation and silently
reported "unavailable" in the actual app. It only surfaced by running the real
thing with its error output captured.
A recover that hid its own damage. The level sampler unlocked a mutex with
a plain call placed after a line that can panic, with a recover swallowing
the evidence. One panic would have frozen every meter and fader and hung the
window on close. Task Manager territory, from a line that looked like
defensiveness.
The setup wizard silenced the machine it was testing. The wizard plays a tone and asks whether you heard it — because a device can bind successfully, show a moving meter, and deliver silence, which is a thing I have watched happen. But the same speakers appear once per driver family, so it preselected the MME entry for a device already bound under WDM, rebound it, and produced exactly the silence it exists to detect. It now leaves a working output alone and restores what it found.
What it looks like
Windows 98, carried over from this site and slop.kit: real beveled chrome, hard-edged segment meters, no rounded corners anywhere. The faders, meters, lamps and buttons are all drawn rather than themed, because Fyne will let you recolour a widget but not change its shape.
Each bucket shows a live waveform — an actual oscilloscope trace at 50Hz, not a bar — because a bar tells you there is signal and a trace tells you whether your gate is chopping the start of words.
Where it is
Beta, with one tester. Everything above was verified against a live Potato console rather than assumed: apps moved between buckets and read back, meters tracked a real test tone, the microphone chain driven end to end.
What is untested is the only thing that matters next: it has never run on a computer that is not mine.