Everything beyond the README: the full configuration and command-line reference, scripted setup, troubleshooting, building from source, and how the two tricky Windows parts actually work.
Configure in the tray menu opens
%LOCALAPPDATA%\AudioOutputSwitcher\config.yaml in whatever app Windows has
associated with .yaml files, for you to hand-edit:
hotkey— the global shortcut, e.g.win+aorctrl+alt+f9. A config file created from scratch starts out withwin+a. Leave the value empty, set it todisabled, or delete the line to switch the hotkey off entirely — the tray icon and its menu keep working, and the app writes your "off" value back unchanged. Switching to a bad or unregistrable value keeps whatever hotkey was already working active instead of leaving you with none.autostart—true(the default, also when left blank) keeps a shortcut to the installed exe in your Startup folder so the app starts at login;falseremoves it. The app applies this every time it starts and whenever you save the file, and setup does the same on install. The exe itself always lives in%LOCALAPPDATA%\AudioOutputSwitcher, never in the Startup folder.poll_seconds— how often to check this file for hand-edits (and re-check devices as a fallback); defaults to60if left blank or set to0or less. Device changes — plugging in, unplugging, or changing the default output in Windows' own sound settings — are picked up instantly via Windows' device notifications, regardless of this interval.outputs— kept in sync with every device that's ever been seen, matched by its Windows device ID (so renaming a device, or having two with the same name, keeps their settings apart): a newly connected device is added automatically, and a disconnected one keeps its row (never deleted automatically, only by editing it out yourself) — with, per device:id— the Windows device ID the row belongs to; don't edit it.alias— the name shown for it in the right-click menu and the OSD; filled in with the device's Windows name if left blank.last_seen— the date it was last detected as active, updated automatically; useful for spotting stale entries worth deleting by hand.skip— set totrueto leave it out of hotkey/left-click cycling. It's still shown in the right-click menu, greyed out, and stays fully clickable there, so you can switch to it directly at any time.
Edits are picked up automatically, no restart needed. A value that isn't
valid (say poll_seconds: soon, or a hotkey that doesn't parse) falls back
to that setting's default while everything else in the file still applies;
keys the app doesn't know are ignored. A file that isn't YAML at all (e.g.
a missing bracket) is ignored as a whole, keeping the previous settings.
Either way the app tells you on screen, and won't write to the file until
it's fixed.
Setup's window closes itself 5 seconds after a successful install, update or uninstall, and stays open after an error.
Up to v1.0.6 the config lived in %APPDATA%\AudioOutputSwitcher; setup
and the app move it over on their own.
AudioOutputSwitcher.exe takes one flag per setting, which wins over the
config file for as long as that copy runs (the file itself keeps its own
values):
| Flag | Setting |
|---|---|
-hotkey <combo> |
hotkey, e.g. -hotkey ctrl+alt+f9 or -hotkey disabled |
-autostart=false |
autostart — also adds/removes the Startup folder shortcut |
-poll-seconds <n> |
poll_seconds |
-enable-logging |
write AudioOutputSwitcher.log next to the exe |
Only one copy runs at a time: starting a second one while the first is still running does nothing.
Setup_AudioOutputSwitcher.exe shows its window unless started with
-mode install or -mode uninstall, which does that straight away and
prints its progress to the console it was started from — for scripting.
It exits with 0 on success and 1 on failure; it doesn't delete itself
in this mode. Since setup is a GUI program, shells don't wait for it on
their own: use start /wait Setup_AudioOutputSwitcher.exe -mode install in
cmd, or Start-Process -Wait -NoNewWindow in PowerShell.
The switcher has no console window, so if a switch doesn't seem to work,
exit it from the tray and start it again with -enable-logging. It then
writes AudioOutputSwitcher.log next to the exe, in
%LOCALAPPDATA%\AudioOutputSwitcher (hotkey registration failures, COM
errors, etc. are all logged there). Logging is off otherwise.
Requires Go 1.24+ on Windows (the code is Windows-only):
go build -ldflags "-H=windowsgui" -o AudioOutputSwitcher.exe ./cmd/switcher
go build -ldflags "-H=windowsgui" -o Setup_AudioOutputSwitcher.exe ./cmd/setup
go test ./...(Cross-compiling from Linux/WSL also works by prefixing the build commands
with GOOS=windows GOARCH=amd64.)
The icon is drawn in code by tools/genicon, which writes
assets/icons/icon.ico (the tray icon); both exes carry it as their file
icon through the committed rsrc_windows_amd64.syso next to their
main.go, setup's together with assets/setup.manifest (common controls
v6 for its task dialog, sharp text at any display scaling, no admin
prompt). After changing the glyph, regenerate all three:
go run ./tools/genicon assets/icons/icon.ico
go run github.com/akavel/rsrc@latest -arch amd64 -ico assets/icons/icon.ico -o cmd/switcher/rsrc_windows_amd64.syso
go run github.com/akavel/rsrc@latest -arch amd64 -ico assets/icons/icon.ico -manifest assets/setup.manifest -o cmd/setup/rsrc_windows_amd64.sysoA test fails if the committed files don't match what genicon renders.
Both pipelines run on Windows runners:
.github/workflows/ci.yml checks formatting,
builds, vets and tests every push to main and every pull request, and
.github/workflows/build.yml — triggered
by pushing a tag matching v*.*.* — runs the same checks, builds both
binaries and publishes them on a new GitHub release.
Setup always installs the newest release: it reads the version from where
releases/latest redirects to and downloads
releases/latest/download/AudioOutputSwitcher.exe, through WinINet
(Windows' own HTTP stack, with the system proxy and certificates). It never
calls the GitHub API, whose rate limit breaks installs on shared networks.
Windows has no public API to change the default audio device — only to read
it. This tool uses the same undocumented IPolicyConfig COM interface that
Windows' own sound settings UI and tools like EarTrumpet or NirCmd rely on;
it has been stable since Windows 7. See
internal/audio for the implementation.
Windows reserves most bare Win+<letter> combinations for the shell itself
(Win+S for Search, Win+E for Explorer, Win+A for Quick Settings, ...).
The standard way to claim a global hotkey, RegisterHotKey, doesn't help
here: the shell's own shortcut handling sees the keystroke first, so the
app's WM_HOTKEY never fires and the reserved action still happens.
Instead, internal/llhotkey installs a low-level
keyboard hook (WH_KEYBOARD_LL), which runs earlier in the input pipeline
than the shell's shortcut handling. When the configured combo is detected,
the hook consumes the keystroke — rather than passing it on — which is what
stops Quick Settings from also opening. This is the same technique tools like
AutoHotkey use to remap Win+<key> shortcuts.
Trade-off: a global low-level keyboard hook sees every keystroke typed
anywhere on the system (necessary to detect the combo at all — this app
only acts on the one combo it's watching for and otherwise passes every
other keystroke straight through unmodified). Antivirus/EDR software can
flag this pattern heuristically since it overlaps with how keyloggers work;
if that happens, it's a false positive rather than any actual keystroke
logging, and the source in internal/llhotkey is the whole of what runs.
A skipped device stays in the tray menu, greyed out but clickable. Windows
has no flag for that: MF_GRAYED greys an item and makes it unclickable,
and the two can't be separated.
So internal/menupaint draws those entries itself.
It subclasses the tray library's message window, turns the device entries
into owner-draw items each time the menu is about to open (the library
resets them to plain strings whenever it sets a title), and answers the
WM_MEASUREITEM/WM_DRAWITEM messages that follow - sizing each entry so
it lines up with the ones Windows still draws (Configure, Exit) and
painting a skipped one in the system's colour for unavailable text. The
menu's background colour is read off the popup itself rather than assumed,
so the entries stay invisible against whatever the current theme paints.
go test -tags visual -run TestVisualMenu ./internal/menupaint puts that
menu on screen for a few seconds to check it by eye; it's behind a build
tag because it takes over the screen while it runs.
| Path | Purpose |
|---|---|
cmd/switcher |
The tray application |
cmd/setup |
Setup's task dialog and -mode flag |
internal/audio |
Core Audio API + IPolicyConfig bindings, device notifications |
internal/llhotkey |
Global hotkey via a low-level keyboard hook |
internal/hotkeycfg |
Parses hotkey combo strings like "ctrl+alt+f9" |
internal/osd |
The volume-OSD-style on-screen switch notification |
internal/menupaint |
Draws the menu's device entries, so skipped ones can be greyed |
internal/outputconfig |
Persists hotkey/poll interval/per-device settings to config.yaml |
internal/autostart |
Keeps the Startup folder shortcut in line with autostart |
internal/install |
Install/update/uninstall logic and setup's countdowns |
internal/updater |
Latest-release lookup and download over WinINet |
assets/icons |
Embedded tray icon |
tools/genicon |
Draws the icon and checks the committed icon files |