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>
141 lines
5.7 KiB
Markdown
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.
|