1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
//! WinMix: Change Windows Mixer Volume via Rust
//!
//! This is a rust library that allows you to individually change the volume of each program in the Windows Mixer.
//!
//! For example, you can set the volume of `chrome.exe` to `0` while leaving other apps alone.
//!
//! ⚠ This libary uses **unsafe** functions from the [windows](https://crates.io/crates/windows) crate. ⚠
//!
//! # Usage
//!
//! ```no_run
//! use winmix::WinMix;
//!
//! unsafe {
//! let winmix = WinMix::default();
//!
//! // Enumerate all audio sessions, one for each program
//! let sessions = winmix.enumerate()?;
//!
//! for session in sessions {
//! // You get the PID and path of the process that controls this audio session
//! println!("pid: {} path: {}", session.pid, session.path);
//!
//! // You can mute or change the volume
//! session.vol.set_mute(true)?;
//! session.vol.set_mute(false)?;
//!
//! // 50% volume
//! session.vol.set_master_volume(0.5)?;
//! // Back to 100% volume
//! session.vol.set_master_volume(1.0)?;
//!
//! // You can also get the current volume, or see if it's muted
//! let vol = session.vol.get_master_volume()?;
//! let is_muted = session.vol.get_mute()?;
//!
//! println!("Vol: {} Muted: {}", vol, is_muted);
//! println!();
//! }
//! }
//! ```
//!
use std::ptr;
use windows::{
core::Interface,
Win32::{
Foundation::{CloseHandle, MAX_PATH},
Media::Audio::{
eRender, IAudioSessionControl, IAudioSessionControl2, IAudioSessionEnumerator,
IAudioSessionManager2, IMMDeviceCollection, IMMDeviceEnumerator, ISimpleAudioVolume,
MMDeviceEnumerator, DEVICE_STATE_ACTIVE,
},
System::{
Com::{CoCreateInstance, CoInitialize, CoUninitialize, CLSCTX_ALL},
ProcessStatus::GetModuleFileNameExW,
Threading::{OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ},
},
},
};
use windows_result::Error;
pub struct WinMix {
// Whether or not we initialized COM; if so, we have to clean up later
com_initialized: bool,
}
impl WinMix {
/// Enumerate all audio sessions from all audio endpoints via WASAPI.
///
/// # Safety
/// This function calls other unsafe functions from the [windows](https://crates.io/crates/windows) crate.
pub unsafe fn enumerate(&self) -> Result<Vec<Session>, Error> {
let mut result = Vec::<Session>::new();
let res: IMMDeviceEnumerator = CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)?;
let collection: IMMDeviceCollection =
res.EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE)?;
let device_count = collection.GetCount()?;
for device_id in 0..device_count {
let dev = collection.Item(device_id)?;
let manager: IAudioSessionManager2 = dev.Activate(CLSCTX_ALL, None)?;
let enumerator: IAudioSessionEnumerator = manager.GetSessionEnumerator()?;
let session_count = enumerator.GetCount()?;
for session_id in 0..session_count {
let ctrl: IAudioSessionControl = enumerator.GetSession(session_id)?;
let ctrl2: IAudioSessionControl2 = ctrl.cast()?;
let pid = ctrl2.GetProcessId()?;
if pid == 0 {
// System sounds session, so we ignore it.
//
// We use this PID == 0 hack because ctrl2.IsSystemSoundsSession() from the windows crate doesn't work yet.
// https://github.com/microsoft/win32metadata/issues/1664
continue;
}
let proc = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid)?;
let mut path: [u16; MAX_PATH as usize] = [0; MAX_PATH as usize];
let res = GetModuleFileNameExW(proc, None, &mut path);
CloseHandle(proc)?;
if res == 0 {
// Failed to get filename from PID (insufficient permissions?)
//continue
}
let vol: ISimpleAudioVolume = ctrl2.cast()?;
result.push(Session {
pid,
path: String::from_utf16_lossy(&path),
vol: SimpleVolume { handle: vol },
});
}
}
Ok(result)
}
}
impl Default for WinMix {
/// Create a default instance of WinMix.
fn default() -> WinMix {
unsafe {
let hres = CoInitialize(None);
// If we initialized COM, we are responsible for cleaning it up later.
// If it was already initialized, we don't have to do anything.
WinMix {
com_initialized: hres.is_ok(),
}
}
}
}
impl Drop for WinMix {
fn drop(&mut self) {
unsafe {
if self.com_initialized {
// We initialized COM, so we uninitialize it
CoUninitialize();
}
}
}
}
pub struct Session {
/// The PID of the process that controls this audio session.
pub pid: u32,
/// The exe path for the process that controls this audio session.
pub path: String,
/// A wrapper that lets you control the volume for this audio session.
pub vol: SimpleVolume,
}
pub struct SimpleVolume {
handle: ISimpleAudioVolume,
}
impl SimpleVolume {
/// Get the master volume for this session.
///
/// # Safety
/// This function calls [ISimpleAudioVolume.GetMasterVolume](https://learn.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-isimpleaudiovolume-getmastervolume) which is unsafe.
pub unsafe fn get_master_volume(&self) -> Result<f32, Error> {
self.handle.GetMasterVolume()
}
/// Set the master volume for this session.
///
/// * `level` - the volume level, between `0.0` and `1.0`\
///
/// # Safety
/// This function calls [ISimpleAudioVolume.SetMasterVolume](https://learn.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-isimpleaudiovolume-setmastervolume) which is unsafe.
pub unsafe fn set_master_volume(&self, level: f32) -> Result<(), Error> {
self.handle.SetMasterVolume(level, ptr::null())
}
/// Check if this session is muted.
///
/// # Safety
/// This function calls [ISimpleAudioVolume.GetMute](https://learn.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-isimpleaudiovolume-getmute) which is unsafe.
pub unsafe fn get_mute(&self) -> Result<bool, Error> {
match self.handle.GetMute() {
Ok(val) => Ok(val.as_bool()),
Err(e) => Err(e),
}
}
/// Mute or unmute this session.
///
/// * `val` - `true` to mute, `false` to unmute
///
/// # Safety
/// This function calls [ISimpleAudioVolume.SetMute](https://learn.microsoft.com/en-us/windows/win32/api/audioclient/nf-audioclient-isimpleaudiovolume-setmute) which is unsafe.
pub unsafe fn set_mute(&self, val: bool) -> Result<(), Error> {
self.handle.SetMute(val, ptr::null())
}
}