A single 306 KB GUI-subsystem binary with no tray icon and no settings window: one hidden popup owns the hotkey, the device-change callbacks and the flyout, and the process sleeps in GetMessageW at 0% CPU in between. Windows exposes no public API for setting the default audio endpoint, so this uses the undocumented IPolicyConfig COM interface, confined to src/policy.rs. Verified working on build 26200. Everything else - enumeration, IMMNotificationClient, RegisterHotKey, the flyout - is documented Win32. The flyout uses the DWM acrylic backdrop and rounded corners the shell uses for its own flyouts, with text drawn via DrawThemeTextEx and DTT_COMPOSITED into a 32-bpp DIB, which is what keeps GDI from leaving black fringes on a composited surface. Console and Multimedia roles move by default; Communications stays put so voice chat keeps its headset. Config is a hand-parsed INI reloaded on the next keypress, keeping the dependency list to the windows crate alone. Built by cross-compiling from a devcontainer with mingw-w64, so nothing is installed on the Windows host and the exe imports only system DLLs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
61 lines
2.7 KiB
Rust
61 lines
2.7 KiB
Rust
//! Setting the default audio endpoint.
|
|
//!
|
|
//! Windows exposes no public API for this. `IPolicyConfig` is undocumented, but it is
|
|
//! what every audio switcher has used since Vista. If a Windows update ever breaks this
|
|
//! tool, this is the file it breaks in.
|
|
|
|
#![allow(non_snake_case)]
|
|
|
|
use core::ffi::c_void;
|
|
|
|
use windows::core::{interface, IUnknown, IUnknown_Vtbl, Result, GUID, HRESULT, PCWSTR};
|
|
use windows::Win32::Media::Audio::ERole;
|
|
use windows::Win32::System::Com::{CoCreateInstance, CLSCTX_ALL};
|
|
|
|
/// `CPolicyConfigClient`.
|
|
const CLSID_POLICY_CONFIG_CLIENT: GUID = GUID::from_u128(0x870af99c_171d_4f9e_af0d_e63df40c2bc9);
|
|
|
|
/// Only `SetDefaultEndpoint` is ever called, but every method preceding it in the vtable
|
|
/// has to be declared so the slot indices line up. The signatures of the unused ones are
|
|
/// irrelevant; their *order* is not — get it wrong and we call the wrong method.
|
|
#[interface("f8679f50-850a-41cf-9c72-430f290290c8")]
|
|
unsafe trait IPolicyConfig: IUnknown {
|
|
unsafe fn GetMixFormat(&self, id: PCWSTR, format: *mut *mut c_void) -> HRESULT;
|
|
unsafe fn GetDeviceFormat(&self, id: PCWSTR, def: i32, format: *mut *mut c_void) -> HRESULT;
|
|
unsafe fn ResetDeviceFormat(&self, id: PCWSTR) -> HRESULT;
|
|
unsafe fn SetDeviceFormat(&self, id: PCWSTR, end: *mut c_void, mix: *mut c_void) -> HRESULT;
|
|
unsafe fn GetProcessingPeriod(
|
|
&self,
|
|
id: PCWSTR,
|
|
def: i32,
|
|
default_period: *mut i64,
|
|
min_period: *mut i64,
|
|
) -> HRESULT;
|
|
unsafe fn SetProcessingPeriod(&self, id: PCWSTR, period: *mut i64) -> HRESULT;
|
|
unsafe fn GetShareMode(&self, id: PCWSTR, mode: *mut c_void) -> HRESULT;
|
|
unsafe fn SetShareMode(&self, id: PCWSTR, mode: *mut c_void) -> HRESULT;
|
|
unsafe fn GetPropertyValue(&self, id: PCWSTR, key: *const c_void, value: *mut c_void) -> HRESULT;
|
|
unsafe fn SetPropertyValue(&self, id: PCWSTR, key: *const c_void, value: *mut c_void) -> HRESULT;
|
|
unsafe fn SetDefaultEndpoint(&self, id: PCWSTR, role: ERole) -> HRESULT;
|
|
unsafe fn SetEndpointVisibility(&self, id: PCWSTR, visible: i32) -> HRESULT;
|
|
}
|
|
|
|
/// Make `device_id` the default render endpoint for each of `roles`.
|
|
///
|
|
/// Roles are applied in order and the first failure aborts — a half-applied switch is
|
|
/// more confusing than a clean error.
|
|
pub fn set_default(device_id: &str, roles: &[ERole]) -> Result<()> {
|
|
let wide: Vec<u16> = device_id.encode_utf16().chain(core::iter::once(0)).collect();
|
|
|
|
unsafe {
|
|
let policy: IPolicyConfig =
|
|
CoCreateInstance(&CLSID_POLICY_CONFIG_CLIENT, None, CLSCTX_ALL)?;
|
|
|
|
for role in roles {
|
|
policy.SetDefaultEndpoint(PCWSTR(wide.as_ptr()), *role).ok()?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|