# win-audio-output-switcher A global hotkey that cycles the Windows 11 default audio output, and a flyout that tells you where you landed. No tray icon, no window, no settings dialog. One 300 KB executable that sits in `GetMessageW` at 0% CPU until you press the key. ``` Ctrl+Alt+A -> Casque (FiiO USB DAC) Ctrl+Alt+A -> Haut-parleurs (Focusrite USB Audio) Ctrl+Alt+A -> Haut-parleurs (Steam Streaming Speakers) ``` ## Behaviour - **One hotkey, forward through a list.** Configure which outputs are in the cycle and in what order, or leave it unconfigured and cycle everything that is plugged in. - **Console and Multimedia roles only.** Windows keeps three separate defaults; the Communications one is left alone by default, so a headset stays pinned for voice chat while media follows the hotkey. Change it with `roles =` if you disagree. - **Stays in sync.** Switch outputs from the Windows volume flyout, or unplug a DAC, and the next press continues from what Windows actually has selected. - **Never steals focus.** The flyout is click-through and non-activating, so pressing the hotkey mid-game or mid-sentence does not pull you out of what you were doing. ## Build Everything happens in the devcontainer — nothing is installed on the Windows side, and the resulting binary imports only system DLLs. ```bash cargo build --release # -> target/x86_64-pc-windows-gnu/release/audioswitch.exe ``` The container cross-compiles with mingw-w64 but cannot launch a Windows process, so deploying is a host-side step. From a **WSL shell outside the container**: ```bash scripts/deploy.sh # copies to %LOCALAPPDATA%\AudioSwitch and (re)starts it ``` ## Use `deploy.sh` puts the binary at `%LOCALAPPDATA%\AudioSwitch\audioswitch.exe`. It is **not on `PATH`**, so out of the box every invocation is a full path. In PowerShell: ```powershell & "$env:LOCALAPPDATA\AudioSwitch\audioswitch.exe" # run in the background & "$env:LOCALAPPDATA\AudioSwitch\audioswitch.exe" --list # and so on ``` or from `cmd.exe`: ```bat "%LOCALAPPDATA%\AudioSwitch\audioswitch.exe" --list ``` That gets old fast. Pick one of these once and the short commands below work as written: ```powershell # A profile function - run `notepad $PROFILE` and add: function audioswitch { & "$env:LOCALAPPDATA\AudioSwitch\audioswitch.exe" @args } ``` ```powershell # ...or put the folder on your user PATH (affects new terminals only): $old = [Environment]::GetEnvironmentVariable('Path', 'User') [Environment]::SetEnvironmentVariable('Path', "$old;$env:LOCALAPPDATA\AudioSwitch", 'User') ``` ### Verbs ``` audioswitch Run in the background (single instance) audioswitch --list List outputs and their endpoint IDs audioswitch --config Create/open the config file audioswitch --quit Stop the running instance audioswitch --autostart on Start with Windows (off to undo) ``` `--autostart` records the full path of the exe as it was when you ran it, so it does not care about `PATH` either way. The verbs that print borrow the terminal they were launched from. Because this is a GUI-subsystem binary, a shell may hand back the prompt before the output arrives; pipe it (`audioswitch --list | Out-String`) if the interleaving bothers you. ## Config `%APPDATA%\win-audio-output-switcher\config.ini`, created by `--config` already populated with the outputs you have right now. Edits apply on the next hotkey press — no restart. ```ini hotkey = ctrl+alt+a roles = console,multimedia osd_position = bottom-center ; top-center, center, bottom-right, top-right osd_duration_ms = 1400 osd_fade = false ; Cycle order. Comment out a line to drop that output. ; Delete the whole section to cycle every active output instead. [devices] {0.0.0.00000000}.{a51fdbbf-...} = Casque (FiiO USB DAC-E07K) {0.0.0.00000000}.{ff879682-...} = Haut-parleurs (Focusrite USB Audio) ``` `osd_fade` is off by default. Fading needs `WS_EX_LAYERED`, which on some builds suppresses the acrylic backdrop that makes the flyout look like part of the shell. Turn it on and see which you prefer on your machine. ## Known limitations - **Elevated windows swallow the hotkey.** `RegisterHotKey` does not fire while a window running as administrator has focus, unless this app is elevated too. Inherent to the API. - **WASAPI exclusive-mode apps do not follow.** Some DAWs and games hold an endpoint open exclusively and keep playing to the old device until restarted. - **Exclusive-fullscreen games hide the flyout.** The switch still happens; a topmost window simply cannot draw over an exclusive-fullscreen swap chain. ## The undocumented bit Windows exposes no public API for setting the default audio endpoint. Like every other tool in this space, this one uses the undocumented `IPolicyConfig` COM interface (`CLSID {870af99c-…}`, `IID {f8679f50-…}`). It has worked from Vista through current Windows 11 builds, but it is not contractual. It is confined to [`src/policy.rs`](src/policy.rs) — about forty lines, all of it the vtable declaration. Everything else (enumeration, change notifications, the hotkey, the flyout) is documented Win32. If a Windows update ever breaks this tool, that is the file to look at first.