Files
audioswitch/README.md
khannurien 70494e96bc Add --refresh to repair stale device IDs, and rename to audioswitch
Windows gives a device a new endpoint ID when it reappears as another
instance, typically a DAC moved to a different USB port, which also
gains a "2- " name prefix. The old config line then silently dropped out
of the cycle. --refresh re-points each stale line at the present output
with the same name (ignoring that prefix), keeps order and comments, adds
unmentioned outputs as commented-out lines, and keeps a .bak of the
previous file.

Also replace every remaining "win-audio-output-switcher" with
"audioswitch": the config now lives in %APPDATA%\audioswitch, and the
autostart Run value and single-instance mutex use the new name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:43:55 +00:00

141 lines
5.7 KiB
Markdown

# audioswitch
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 --refresh Update config device IDs to match current outputs
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%\audioswitch\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)
```
Windows sometimes gives a device a new endpoint ID. Plugging a DAC into a different USB
port is the usual cause, and its name gains a `2- ` prefix as well. The old line then
silently drops out of the cycle. `audioswitch --refresh` fixes this. It moves each stale
line to the present output with the same name (ignoring that prefix), keeps your order and
comments, and adds outputs the file doesn't mention yet as commented-out lines. The
previous file is kept as `config.ini.bak`.
`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.