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