From d68c8855e911949a7eb163572a461328a90ce910 Mon Sep 17 00:00:00 2001 From: sidit77 Date: Fri, 24 Feb 2023 10:20:17 +0100 Subject: [PATCH 01/13] use ActivateAudioInterfaceAsync to retrieve the default devices to enable automatic stream routing in case the default device changes --- Cargo.toml | 2 +- src/host/wasapi/device.rs | 284 +++++++++++++++++++++++++------------- 2 files changed, 188 insertions(+), 98 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5010ea8be..a1ac256d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ clap = { version = "4.0", features = ["derive"] } ndk-glue = "0.7" [target.'cfg(target_os = "windows")'.dependencies] -windows = { version = "0.44.0", features = ["Win32_Media_Audio", "Win32_Foundation", "Win32_System_Com", "Win32_Devices_Properties", "Win32_Media_KernelStreaming", "Win32_System_Com_StructuredStorage", "Win32_System_Ole", "Win32_System_Threading", "Win32_Security", "Win32_System_SystemServices", "Win32_System_WindowsProgramming", "Win32_Media_Multimedia", "Win32_UI_Shell_PropertiesSystem"]} +windows = { version = "0.44.0", features = ["Win32_Media_Audio", "Win32_Foundation", "Win32_System_Com", "Win32_Devices_Properties", "Win32_Media_KernelStreaming", "Win32_System_Com_StructuredStorage", "Win32_System_Ole", "Win32_System_Threading", "Win32_Security", "Win32_System_SystemServices", "Win32_System_WindowsProgramming", "Win32_Media_Multimedia", "Win32_UI_Shell_PropertiesSystem", "implement"]} asio-sys = { version = "0.2", path = "asio-sys", optional = true } num-traits = { version = "0.2.6", optional = true } parking_lot = "0.12" diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index d670ea7b4..87b21abc9 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -14,18 +14,20 @@ use std::os::windows::ffi::OsStringExt; use std::ptr; use std::slice; use std::sync::{Arc, Mutex, MutexGuard}; +use std::sync::mpsc::Sender; use std::time::Duration; use super::com; use super::{windows_err_to_cpal_err, windows_err_to_cpal_err_message}; -use windows::core::Interface; +use windows::core::{Interface, PCWSTR, implement, IUnknown, HRESULT, Result as WinResult}; use windows::core::GUID; use windows::Win32::Devices::Properties; use windows::Win32::Foundation; -use windows::Win32::Media::Audio::IAudioRenderClient; +use windows::Win32::Media::Audio::{ActivateAudioInterfaceAsync, DEVINTERFACE_AUDIO_CAPTURE, DEVINTERFACE_AUDIO_RENDER, IActivateAudioInterfaceAsyncOperation, IActivateAudioInterfaceCompletionHandler, IActivateAudioInterfaceCompletionHandler_Impl, IAudioRenderClient}; use windows::Win32::Media::{Audio, KernelStreaming, Multimedia}; use windows::Win32::System::Com; -use windows::Win32::System::Com::{StructuredStorage, STGM_READ, VT_LPWSTR}; +use windows::Win32::System::Com::{StructuredStorage, STGM_READ, VT_LPWSTR, StringFromIID, CoTaskMemFree}; +use windows::Win32::System::Com::StructuredStorage::PROPVARIANT; use windows::Win32::System::Threading; use super::stream::{AudioClientFlow, Stream, StreamInner}; @@ -40,10 +42,17 @@ struct IAudioClientWrapper(Audio::IAudioClient); unsafe impl Send for IAudioClientWrapper {} unsafe impl Sync for IAudioClientWrapper {} +#[derive(Debug, Clone)] +enum DeviceType { + DefaultOutput, + DefaultInput, + Specific(Audio::IMMDevice) +} + /// An opaque type that identifies an end point. #[derive(Clone)] pub struct Device { - device: Audio::IMMDevice, + device: DeviceType, /// We cache an uninitialized `IAudioClient` so that we can call functions from it without /// having to create/destroy audio clients all the time. future_audio_client: Arc>>, // TODO: add NonZero around the ptr @@ -314,66 +323,115 @@ unsafe fn format_from_waveformatex_ptr( Some(format) } +#[implement(IActivateAudioInterfaceCompletionHandler)] +struct CompletionHandler(Sender>); + +fn retrieve_result(operation: &IActivateAudioInterfaceAsyncOperation) -> WinResult { + let mut result = HRESULT::default(); + let mut interface: Option = None; + unsafe { operation.GetActivateResult(&mut result, &mut interface)?; } + result.ok()?; + Ok(interface.unwrap()) +} + +impl IActivateAudioInterfaceCompletionHandler_Impl for CompletionHandler { + fn ActivateCompleted(&self, operation: &Option) -> WinResult<()> { + let result = retrieve_result(operation.as_ref().unwrap()); + let _ = self.0.send(result); + Ok(()) + } +} + +#[allow(non_snake_case)] +unsafe fn ActivateAudioInterfaceSync(deviceinterfacepath: P0, activationparams: Option<*const PROPVARIANT>) -> WinResult where + P0: Into<::windows::core::InParam>, + T: Interface { + let (sender, receiver) = std::sync::mpsc::channel(); + let completion: IActivateAudioInterfaceCompletionHandler = CompletionHandler(sender).into(); + ActivateAudioInterfaceAsync(deviceinterfacepath, &T::IID, activationparams, &completion)?; + let result = receiver.recv_timeout(Duration::from_secs(2)).unwrap()?; + result.cast() +} + unsafe impl Send for Device {} unsafe impl Sync for Device {} impl Device { pub fn name(&self) -> Result { - unsafe { - // Open the device's property store. - let property_store = self - .device - .OpenPropertyStore(STGM_READ) - .expect("could not open property store"); - - // Get the endpoint's friendly-name property. - let mut property_value = property_store - .GetValue(&Properties::DEVPKEY_Device_FriendlyName as *const _ as *const _) - .map_err(|err| { - let description = - format!("failed to retrieve name from property store: {}", err); - let err = BackendSpecificError { description }; - DeviceNameError::from(err) - })?; + match &self.device { + DeviceType::DefaultOutput => Ok("Default Ouput".to_string()), + DeviceType::DefaultInput => Ok("Default Input".to_string()), + DeviceType::Specific(device) => unsafe { + // Open the device's property store. + let property_store = device + .OpenPropertyStore(STGM_READ) + .expect("could not open property store"); + + // Get the endpoint's friendly-name property. + let mut property_value = property_store + .GetValue(&Properties::DEVPKEY_Device_FriendlyName as *const _ as *const _) + .map_err(|err| { + let description = + format!("failed to retrieve name from property store: {}", err); + let err = BackendSpecificError { description }; + DeviceNameError::from(err) + })?; - let prop_variant = &property_value.Anonymous.Anonymous; + let prop_variant = &property_value.Anonymous.Anonymous; - // Read the friendly-name from the union data field, expecting a *const u16. - if prop_variant.vt != VT_LPWSTR { - let description = format!( - "property store produced invalid data: {:?}", - prop_variant.vt - ); - let err = BackendSpecificError { description }; - return Err(err.into()); - } - let ptr_utf16 = *(&prop_variant.Anonymous as *const _ as *const *const u16); + // Read the friendly-name from the union data field, expecting a *const u16. + if prop_variant.vt != VT_LPWSTR { + let description = format!( + "property store produced invalid data: {:?}", + prop_variant.vt + ); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + let ptr_utf16 = *(&prop_variant.Anonymous as *const _ as *const *const u16); - // Find the length of the friendly name. - let mut len = 0; - while *ptr_utf16.offset(len) != 0 { - len += 1; - } + // Find the length of the friendly name. + let mut len = 0; + while *ptr_utf16.offset(len) != 0 { + len += 1; + } - // Create the utf16 slice and convert it into a string. - let name_slice = slice::from_raw_parts(ptr_utf16, len as usize); - let name_os_string: OsString = OsStringExt::from_wide(name_slice); - let name_string = match name_os_string.into_string() { - Ok(string) => string, - Err(os_string) => os_string.to_string_lossy().into(), - }; + // Create the utf16 slice and convert it into a string. + let name_slice = slice::from_raw_parts(ptr_utf16, len as usize); + let name_os_string: OsString = OsStringExt::from_wide(name_slice); + let name_string = match name_os_string.into_string() { + Ok(string) => string, + Err(os_string) => os_string.to_string_lossy().into(), + }; - // Clean up the property. - StructuredStorage::PropVariantClear(&mut property_value).ok(); + // Clean up the property. + StructuredStorage::PropVariantClear(&mut property_value).ok(); - Ok(name_string) + Ok(name_string) + } } } #[inline] fn from_immdevice(device: Audio::IMMDevice) -> Self { Device { - device, + device: DeviceType::Specific(device), + future_audio_client: Arc::new(Mutex::new(None)), + } + } + + #[inline] + fn default_output() -> Self { + Device { + device: DeviceType::DefaultOutput, + future_audio_client: Arc::new(Mutex::new(None)), + } + } + + #[inline] + fn default_input() -> Self { + Device { + device: DeviceType::DefaultInput, future_audio_client: Arc::new(Mutex::new(None)), } } @@ -388,9 +446,26 @@ impl Device { } let audio_client: Audio::IAudioClient = unsafe { - // can fail if the device has been disconnected since we enumerated it, or if - // the device doesn't support playback for some reason - self.device.Activate(Com::CLSCTX_ALL, None)? + match &self.device { + DeviceType::DefaultOutput => { + let default_audio = StringFromIID(&DEVINTERFACE_AUDIO_RENDER)?; + let result = ActivateAudioInterfaceSync(PCWSTR(default_audio.as_ptr()), None); + CoTaskMemFree(Some(default_audio.as_ptr()as _)); + result? + } + DeviceType::DefaultInput => { + let default_audio = StringFromIID(&DEVINTERFACE_AUDIO_CAPTURE)?; + let result = ActivateAudioInterfaceSync(PCWSTR(default_audio.as_ptr()), None); + CoTaskMemFree(Some(default_audio.as_ptr() as _)); + result? + } + DeviceType::Specific(device) => { + // can fail if the device has been disconnected since we enumerated it, or if + // the device doesn't support playback for some reason + device.Activate(Com::CLSCTX_ALL, None)? + } + } + }; *lock = Some(IAudioClientWrapper(audio_client)); @@ -560,8 +635,14 @@ impl Device { } pub(crate) fn data_flow(&self) -> Audio::EDataFlow { - let endpoint = Endpoint::from(self.device.clone()); - endpoint.data_flow() + match &self.device { + DeviceType::DefaultOutput => Audio::eRender, + DeviceType::DefaultInput => Audio::eCapture, + DeviceType::Specific(device) => { + let endpoint = Endpoint::from(device.clone()); + endpoint.data_flow() + } + } } pub fn default_input_config(&self) -> Result { @@ -811,40 +892,47 @@ impl Device { impl PartialEq for Device { #[inline] fn eq(&self, other: &Device) -> bool { - // Use case: In order to check whether the default device has changed - // the client code might need to compare the previous default device with the current one. - // The pointer comparison (`self.device == other.device`) don't work there, - // because the pointers are different even when the default device stays the same. - // - // In this code section we're trying to use the GetId method for the device comparison, cf. - // https://docs.microsoft.com/en-us/windows/desktop/api/mmdeviceapi/nf-mmdeviceapi-immdevice-getid - unsafe { - struct IdRAII(windows::core::PWSTR); - /// RAII for device IDs. - impl Drop for IdRAII { - fn drop(&mut self) { - unsafe { Com::CoTaskMemFree(Some(self.0 .0 as *mut _)) } - } - } - // GetId only fails with E_OUTOFMEMORY and if it does, we're probably dead already. - // Plus it won't do to change the device comparison logic unexpectedly. - let id1 = self.device.GetId().expect("cpal: GetId failure"); - let id1 = IdRAII(id1); - let id2 = other.device.GetId().expect("cpal: GetId failure"); - let id2 = IdRAII(id2); - // 16-bit null-terminated comparison. - let mut offset = 0; - loop { - let w1: u16 = *(id1.0).0.offset(offset); - let w2: u16 = *(id2.0).0.offset(offset); - if w1 == 0 && w2 == 0 { - return true; - } - if w1 != w2 { - return false; + match (&self.device, &other.device) { + (DeviceType::DefaultOutput, DeviceType::DefaultOutput) => true, + (DeviceType::DefaultInput, DeviceType::DefaultInput) => true, + (DeviceType::Specific(dev1), DeviceType::Specific(dev2)) => { + // Use case: In order to check whether the default device has changed + // the client code might need to compare the previous default device with the current one. + // The pointer comparison (`self.device == other.device`) don't work there, + // because the pointers are different even when the default device stays the same. + // + // In this code section we're trying to use the GetId method for the device comparison, cf. + // https://docs.microsoft.com/en-us/windows/desktop/api/mmdeviceapi/nf-mmdeviceapi-immdevice-getid + unsafe { + struct IdRAII(windows::core::PWSTR); + /// RAII for device IDs. + impl Drop for IdRAII { + fn drop(&mut self) { + unsafe { Com::CoTaskMemFree(Some(self.0 .0 as *mut _)) } + } + } + // GetId only fails with E_OUTOFMEMORY and if it does, we're probably dead already. + // Plus it won't do to change the device comparison logic unexpectedly. + let id1 = dev1.GetId().expect("cpal: GetId failure"); + let id1 = IdRAII(id1); + let id2 = dev2.GetId().expect("cpal: GetId failure"); + let id2 = IdRAII(id2); + // 16-bit null-terminated comparison. + let mut offset = 0; + loop { + let w1: u16 = *(id1.0).0.offset(offset); + let w2: u16 = *(id2.0).0.offset(offset); + if w1 == 0 && w2 == 0 { + return true; + } + if w1 != w2 { + return false; + } + offset += 1; + } } - offset += 1; - } + }, + _ => false, } } } @@ -952,23 +1040,25 @@ impl Iterator for Devices { } } -fn default_device(data_flow: Audio::EDataFlow) -> Option { - unsafe { - let device = ENUMERATOR - .0 - .GetDefaultAudioEndpoint(data_flow, Audio::eConsole) - .ok()?; - // TODO: check specifically for `E_NOTFOUND`, and panic otherwise - Some(Device::from_immdevice(device)) - } -} +//fn default_device(data_flow: Audio::EDataFlow) -> Option { +// unsafe { +// let device = ENUMERATOR +// .0 +// .GetDefaultAudioEndpoint(data_flow, Audio::eConsole) +// .ok()?; +// // TODO: check specifically for `E_NOTFOUND`, and panic otherwise +// Some(Device::from_immdevice(device)) +// } +//} pub fn default_input_device() -> Option { - default_device(Audio::eCapture) + //default_device(Audio::eCapture) + Some(Device::default_input()) } pub fn default_output_device() -> Option { - default_device(Audio::eRender) + //default_device(Audio::eRender) + Some(Device::default_output()) } /// Get the audio clock used to produce `StreamInstant`s. From dd00d9edf6c8abb757e2730fbc9a6fd81a39577b Mon Sep 17 00:00:00 2001 From: sidit77 Date: Fri, 24 Feb 2023 10:37:07 +0100 Subject: [PATCH 02/13] applied rustfmt --- src/host/wasapi/device.rs | 61 +++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 87b21abc9..77cdef3af 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -13,21 +13,23 @@ use std::ops::{Deref, DerefMut}; use std::os::windows::ffi::OsStringExt; use std::ptr; use std::slice; -use std::sync::{Arc, Mutex, MutexGuard}; use std::sync::mpsc::Sender; +use std::sync::{Arc, Mutex, MutexGuard}; use std::time::Duration; use super::com; use super::{windows_err_to_cpal_err, windows_err_to_cpal_err_message}; -use windows::core::{Interface, PCWSTR, implement, IUnknown, HRESULT, Result as WinResult}; use windows::core::GUID; +use windows::core::{implement, IUnknown, Interface, Result as WinResult, HRESULT, PCWSTR}; use windows::Win32::Devices::Properties; use windows::Win32::Foundation; -use windows::Win32::Media::Audio::{ActivateAudioInterfaceAsync, DEVINTERFACE_AUDIO_CAPTURE, DEVINTERFACE_AUDIO_RENDER, IActivateAudioInterfaceAsyncOperation, IActivateAudioInterfaceCompletionHandler, IActivateAudioInterfaceCompletionHandler_Impl, IAudioRenderClient}; +use windows::Win32::Media::Audio::IAudioRenderClient; use windows::Win32::Media::{Audio, KernelStreaming, Multimedia}; use windows::Win32::System::Com; -use windows::Win32::System::Com::{StructuredStorage, STGM_READ, VT_LPWSTR, StringFromIID, CoTaskMemFree}; use windows::Win32::System::Com::StructuredStorage::PROPVARIANT; +use windows::Win32::System::Com::{ + CoTaskMemFree, StringFromIID, StructuredStorage, STGM_READ, VT_LPWSTR, +}; use windows::Win32::System::Threading; use super::stream::{AudioClientFlow, Stream, StreamInner}; @@ -46,7 +48,7 @@ unsafe impl Sync for IAudioClientWrapper {} enum DeviceType { DefaultOutput, DefaultInput, - Specific(Audio::IMMDevice) + Specific(Audio::IMMDevice), } /// An opaque type that identifies an end point. @@ -323,32 +325,50 @@ unsafe fn format_from_waveformatex_ptr( Some(format) } -#[implement(IActivateAudioInterfaceCompletionHandler)] +#[implement(Audio::IActivateAudioInterfaceCompletionHandler)] struct CompletionHandler(Sender>); -fn retrieve_result(operation: &IActivateAudioInterfaceAsyncOperation) -> WinResult { +fn retrieve_result( + operation: &Audio::IActivateAudioInterfaceAsyncOperation, +) -> WinResult { let mut result = HRESULT::default(); let mut interface: Option = None; - unsafe { operation.GetActivateResult(&mut result, &mut interface)?; } + unsafe { + operation.GetActivateResult(&mut result, &mut interface)?; + } result.ok()?; Ok(interface.unwrap()) } -impl IActivateAudioInterfaceCompletionHandler_Impl for CompletionHandler { - fn ActivateCompleted(&self, operation: &Option) -> WinResult<()> { +impl Audio::IActivateAudioInterfaceCompletionHandler_Impl for CompletionHandler { + fn ActivateCompleted( + &self, + operation: &Option, + ) -> WinResult<()> { let result = retrieve_result(operation.as_ref().unwrap()); - let _ = self.0.send(result); + let _ = self.0.send(result); Ok(()) } } #[allow(non_snake_case)] -unsafe fn ActivateAudioInterfaceSync(deviceinterfacepath: P0, activationparams: Option<*const PROPVARIANT>) -> WinResult where +unsafe fn ActivateAudioInterfaceSync( + deviceinterfacepath: P0, + activationparams: Option<*const PROPVARIANT>, +) -> WinResult +where P0: Into<::windows::core::InParam>, - T: Interface { + T: Interface, +{ let (sender, receiver) = std::sync::mpsc::channel(); - let completion: IActivateAudioInterfaceCompletionHandler = CompletionHandler(sender).into(); - ActivateAudioInterfaceAsync(deviceinterfacepath, &T::IID, activationparams, &completion)?; + let completion: Audio::IActivateAudioInterfaceCompletionHandler = + CompletionHandler(sender).into(); + Audio::ActivateAudioInterfaceAsync( + deviceinterfacepath, + &T::IID, + activationparams, + &completion, + )?; let result = receiver.recv_timeout(Duration::from_secs(2)).unwrap()?; result.cast() } @@ -408,7 +428,7 @@ impl Device { StructuredStorage::PropVariantClear(&mut property_value).ok(); Ok(name_string) - } + }, } } @@ -448,13 +468,13 @@ impl Device { let audio_client: Audio::IAudioClient = unsafe { match &self.device { DeviceType::DefaultOutput => { - let default_audio = StringFromIID(&DEVINTERFACE_AUDIO_RENDER)?; + let default_audio = StringFromIID(&Audio::DEVINTERFACE_AUDIO_RENDER)?; let result = ActivateAudioInterfaceSync(PCWSTR(default_audio.as_ptr()), None); - CoTaskMemFree(Some(default_audio.as_ptr()as _)); + CoTaskMemFree(Some(default_audio.as_ptr() as _)); result? } DeviceType::DefaultInput => { - let default_audio = StringFromIID(&DEVINTERFACE_AUDIO_CAPTURE)?; + let default_audio = StringFromIID(&Audio::DEVINTERFACE_AUDIO_CAPTURE)?; let result = ActivateAudioInterfaceSync(PCWSTR(default_audio.as_ptr()), None); CoTaskMemFree(Some(default_audio.as_ptr() as _)); result? @@ -465,7 +485,6 @@ impl Device { device.Activate(Com::CLSCTX_ALL, None)? } } - }; *lock = Some(IAudioClientWrapper(audio_client)); @@ -931,7 +950,7 @@ impl PartialEq for Device { offset += 1; } } - }, + } _ => false, } } From 7e82d99bfaa8036cf6e7c7d214254014ad050139 Mon Sep 17 00:00:00 2001 From: sidit77 Date: Tue, 2 May 2023 20:16:32 +0200 Subject: [PATCH 03/13] fixed merge conflicts --- src/host/wasapi/device.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 9ebd1dab7..ddff33402 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -344,9 +344,9 @@ fn retrieve_result( impl Audio::IActivateAudioInterfaceCompletionHandler_Impl for CompletionHandler { fn ActivateCompleted( &self, - operation: &Option, + operation: Option<&Audio::IActivateAudioInterfaceAsyncOperation>, ) -> WinResult<()> { - let result = retrieve_result(operation.as_ref().unwrap()); + let result = retrieve_result(operation.unwrap()); let _ = self.0.send(result); Ok(()) } @@ -358,8 +358,8 @@ unsafe fn ActivateAudioInterfaceSync( activationparams: Option<*const PROPVARIANT>, ) -> WinResult where - P0: Into<::windows::core::InParam>, - T: Interface, + P0: windows::core::IntoParam, + T: Interface + ComInterface, { let (sender, receiver) = std::sync::mpsc::channel(); let completion: Audio::IActivateAudioInterfaceCompletionHandler = From 66ed6bec97f25ec7f02a82f50d1aa9aef733a58e Mon Sep 17 00:00:00 2001 From: sidit77 Date: Wed, 29 May 2024 20:27:31 +0200 Subject: [PATCH 04/13] fixed merge conflicts --- Cargo.toml | 1 + src/host/wasapi/device.rs | 37 ++++++++++++++++++------------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7b00ce902..e1bbb0460 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ ndk-glue = "0.7" [target.'cfg(target_os = "windows")'.dependencies] windows = { version = "0.54.0", features = [ + "implement", "Win32_Media_Audio", "Win32_Foundation", "Win32_Devices_Properties", diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 8389071c8..6ab154dab 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -11,21 +11,21 @@ use std::mem; use std::os::windows::ffi::OsStringExt; use std::ptr; use std::slice; -use std::sync::OnceLock; use std::sync::mpsc::Sender; +use std::sync::OnceLock; use std::sync::{Arc, Mutex, MutexGuard}; use std::time::Duration; use super::com; use super::{windows_err_to_cpal_err, windows_err_to_cpal_err_message}; -use windows::core::Interface; use windows::core::GUID; +use windows::core::{implement, IUnknown, Interface, HRESULT, PCWSTR, PROPVARIANT}; use windows::Win32::Devices::Properties; use windows::Win32::Foundation; use windows::Win32::Media::Audio::IAudioRenderClient; use windows::Win32::Media::{Audio, KernelStreaming, Multimedia}; use windows::Win32::System::Com; -use windows::Win32::System::Com::{StructuredStorage, STGM_READ}; +use windows::Win32::System::Com::{CoTaskMemFree, StringFromIID, StructuredStorage, STGM_READ}; use windows::Win32::System::Threading; use windows::Win32::System::Variant::VT_LPWSTR; @@ -284,11 +284,11 @@ unsafe fn format_from_waveformatex_ptr( } #[implement(Audio::IActivateAudioInterfaceCompletionHandler)] -struct CompletionHandler(Sender>); +struct CompletionHandler(Sender>); fn retrieve_result( operation: &Audio::IActivateAudioInterfaceAsyncOperation, -) -> WinResult { +) -> windows::core::Result { let mut result = HRESULT::default(); let mut interface: Option = None; unsafe { @@ -302,7 +302,7 @@ impl Audio::IActivateAudioInterfaceCompletionHandler_Impl for CompletionHandler fn ActivateCompleted( &self, operation: Option<&Audio::IActivateAudioInterfaceAsyncOperation>, - ) -> WinResult<()> { + ) -> windows::core::Result<()> { let result = retrieve_result(operation.unwrap()); let _ = self.0.send(result); Ok(()) @@ -313,10 +313,10 @@ impl Audio::IActivateAudioInterfaceCompletionHandler_Impl for CompletionHandler unsafe fn ActivateAudioInterfaceSync( deviceinterfacepath: P0, activationparams: Option<*const PROPVARIANT>, -) -> WinResult +) -> windows::core::Result where P0: windows::core::IntoParam, - T: Interface + ComInterface, + T: Interface, { let (sender, receiver) = std::sync::mpsc::channel(); let completion: Audio::IActivateAudioInterfaceCompletionHandler = @@ -357,16 +357,16 @@ impl Device { let prop_variant = &property_value.as_raw().Anonymous.Anonymous; - // Read the friendly-name from the union data field, expecting a *const u16. - if prop_variant.vt != VT_LPWSTR.0 { - let description = format!( - "property store produced invalid data: {:?}", - prop_variant.vt - ); - let err = BackendSpecificError { description }; - return Err(err.into()); - } - let ptr_utf16 = *(&prop_variant.Anonymous as *const _ as *const *const u16); + // Read the friendly-name from the union data field, expecting a *const u16. + if prop_variant.vt != VT_LPWSTR.0 { + let description = format!( + "property store produced invalid data: {:?}", + prop_variant.vt + ); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + let ptr_utf16 = *(&prop_variant.Anonymous as *const _ as *const *const u16); // Find the length of the friendly name. let mut len = 0; @@ -1029,7 +1029,6 @@ impl Iterator for Devices { // } //} - pub fn default_input_device() -> Option { //default_device(Audio::eCapture) Some(Device::default_input()) From c7395ce414cfc83c936d0770b522c3c87f95748a Mon Sep 17 00:00:00 2001 From: Philpax Date: Wed, 1 Oct 2025 19:22:46 +0200 Subject: [PATCH 05/13] fix(wasapi): get name of default device --- src/host/wasapi/device.rs | 125 +++++++++++++++++++------------------- 1 file changed, 64 insertions(+), 61 deletions(-) diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 500514a69..de943b523 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -327,57 +327,59 @@ unsafe impl Sync for Device {} impl Device { pub fn name(&self) -> Result { - match &self.device { - DeviceType::DefaultOutput => Ok("Default Output".to_string()), - DeviceType::DefaultInput => Ok("Default Input".to_string()), - DeviceType::Specific(device) => unsafe { - // Open the device's property store. - let property_store = device - .OpenPropertyStore(STGM_READ) - .expect("could not open property store"); - - // Get the endpoint's friendly-name property. - let mut property_value = property_store - .GetValue(&Properties::DEVPKEY_Device_FriendlyName as *const _ as *const _) - .map_err(|err| { - let description = - format!("failed to retrieve name from property store: {}", err); - let err = BackendSpecificError { description }; - DeviceNameError::from(err) - })?; + let device = self + .immdevice() + .ok_or(DeviceNameError::from(BackendSpecificError { + description: "device not found while getting name".to_string(), + }))?; - let prop_variant = &property_value.Anonymous.Anonymous; - - // Read the friendly-name from the union data field, expecting a *const u16. - if prop_variant.vt != VT_LPWSTR { - let description = format!( - "property store produced invalid data: {:?}", - prop_variant.vt - ); + unsafe { + // Open the device's property store. + let property_store = device + .OpenPropertyStore(STGM_READ) + .expect("could not open property store"); + + // Get the endpoint's friendly-name property. + let mut property_value = property_store + .GetValue(&Properties::DEVPKEY_Device_FriendlyName as *const _ as *const _) + .map_err(|err| { + let description = + format!("failed to retrieve name from property store: {}", err); let err = BackendSpecificError { description }; - return Err(err.into()); - } - let ptr_utf16 = *(&prop_variant.Anonymous as *const _ as *const *const u16); + DeviceNameError::from(err) + })?; - // Find the length of the friendly name. - let mut len = 0; - while *ptr_utf16.offset(len) != 0 { - len += 1; - } + let prop_variant = &property_value.Anonymous.Anonymous; - // Create the utf16 slice and convert it into a string. - let name_slice = slice::from_raw_parts(ptr_utf16, len as usize); - let name_os_string: OsString = OsStringExt::from_wide(name_slice); - let name_string = match name_os_string.into_string() { - Ok(string) => string, - Err(os_string) => os_string.to_string_lossy().into(), - }; + // Read the friendly-name from the union data field, expecting a *const u16. + if prop_variant.vt != VT_LPWSTR { + let description = format!( + "property store produced invalid data: {:?}", + prop_variant.vt + ); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + let ptr_utf16 = *(&prop_variant.Anonymous as *const _ as *const *const u16); - // Clean up the property. - StructuredStorage::PropVariantClear(&mut property_value).ok(); + // Find the length of the friendly name. + let mut len = 0; + while *ptr_utf16.offset(len) != 0 { + len += 1; + } - Ok(name_string) - }, + // Create the utf16 slice and convert it into a string. + let name_slice = slice::from_raw_parts(ptr_utf16, len as usize); + let name_os_string: OsString = OsStringExt::from_wide(name_slice); + let name_string = match name_os_string.into_string() { + Ok(string) => string, + Err(os_string) => os_string.to_string_lossy().into(), + }; + + // Clean up the property. + StructuredStorage::PropVariantClear(&mut property_value).ok(); + + Ok(name_string) } } @@ -405,9 +407,23 @@ impl Device { } } - // pub fn immdevice(&self) -> &Audio::IMMDevice { - // &self.device - // } + pub fn immdevice(&self) -> Option { + match &self.device { + DeviceType::DefaultOutput => unsafe { + get_enumerator() + .0 + .GetDefaultAudioEndpoint(Audio::eRender, Audio::eConsole) + .ok() + }, + DeviceType::DefaultInput => unsafe { + get_enumerator() + .0 + .GetDefaultAudioEndpoint(Audio::eCapture, Audio::eConsole) + .ok() + }, + DeviceType::Specific(device) => Some(device.clone()), + } + } /// Ensures that `future_audio_client` contains a `Some` and returns a locked mutex to it. fn ensure_future_audio_client( @@ -1015,24 +1031,11 @@ impl Iterator for Devices { } } -//fn default_device(data_flow: Audio::EDataFlow) -> Option { -// unsafe { -// let device = get_enumerator() -// .0 -// .GetDefaultAudioEndpoint(data_flow, Audio::eConsole) -// .ok()?; -// // TODO: check specifically for `E_NOTFOUND`, and panic otherwise -// Some(Device::from_immdevice(device)) -// } -//} - pub fn default_input_device() -> Option { - //default_device(Audio::eCapture) Some(Device::default_input()) } pub fn default_output_device() -> Option { - //default_device(Audio::eRender) Some(Device::default_output()) } From 57224832cceb3f1cabfb1a9357da384120641c65 Mon Sep 17 00:00:00 2001 From: Philpax Date: Wed, 1 Oct 2025 19:26:03 +0200 Subject: [PATCH 06/13] fix(wasapi): ensure acquire errors are propagated --- src/host/wasapi/device.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index de943b523..c083d9493 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -286,7 +286,12 @@ fn retrieve_result( operation.GetActivateResult(&mut result, &mut interface)?; } result.ok()?; - Ok(interface.unwrap()) + interface.ok_or_else(|| { + windows::core::Error::new( + Audio::AUDCLNT_E_DEVICE_INVALIDATED, + "audio interface could not be retrieved during activation", + ) + }) } impl Audio::IActivateAudioInterfaceCompletionHandler_Impl for CompletionHandler_Impl { @@ -294,7 +299,7 @@ impl Audio::IActivateAudioInterfaceCompletionHandler_Impl for CompletionHandler_ &self, operation: windows::core::Ref, ) -> windows::core::Result<()> { - let result = retrieve_result(operation.ok()?); + let result = operation.ok().and_then(retrieve_result); let _ = self.0.send(result); Ok(()) } From 6a175829004e550dca9ff83f4af49907707fc9e4 Mon Sep 17 00:00:00 2001 From: Philpax Date: Wed, 1 Oct 2025 19:36:11 +0200 Subject: [PATCH 07/13] chore: bump windows crate min to 0.61 due to lack-of-implement feature --- .github/workflows/cpal.yml | 2 -- Cargo.toml | 7 ++++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cpal.yml b/.github/workflows/cpal.yml index fa35370ef..17dc366ef 100644 --- a/.github/workflows/cpal.yml +++ b/.github/workflows/cpal.yml @@ -320,8 +320,6 @@ jobs: fail-fast: false matrix: include: - - windows-version: "0.59.0" - - windows-version: "0.60.0" - windows-version: "0.61.3" # Skip 0.62.x since we already test current version in test-native name: test-windows-v${{ matrix.windows-version }} diff --git a/Cargo.toml b/Cargo.toml index f4b6a4bea..24eae3686 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,12 @@ clap = { version = "4.5", features = ["derive"] } # versions when bumping to a new release, and only increase the minimum when absolutely necessary. # When updating this, also update the "windows-version" matrix in the CI workflow. [target.'cfg(target_os = "windows")'.dependencies] -windows = { version = ">=0.58, <=0.62", features = [ +# The `implement` feature was removed in windows-0.61, which means that we can't +# use older versions of the `windows` crate without explicitly activating `implement` +# for them, which will cause problems for >=0.61. +# +# See . +windows = { version = ">=0.61, <=0.62", features = [ "Win32_Media_Audio", "Win32_Foundation", "Win32_Devices_Properties", From 04c8eaa517d9632dbd9f50f9d924410e9605fa0c Mon Sep 17 00:00:00 2001 From: Philpax Date: Wed, 1 Oct 2025 19:49:43 +0200 Subject: [PATCH 08/13] refactor(wasapi): move virtual default devices to default-on feature --- Cargo.toml | 9 ++++ src/host/wasapi/device.rs | 105 +++++++++++++++++++++----------------- 2 files changed, 68 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 24eae3686..0b66821b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,20 @@ edition = "2021" rust-version = "1.70" [features] +default = ["wasapi-virtual-default-devices"] + asio = [ "asio-sys", "num-traits", ] # Only available on Windows. See README for setup instructions. +# Enable virtual default devices for WASAPI, so that audio will be +# automatically rerouted when the default input or output device is changed. +# +# Note that this only works on Windows 8 and above. It is turned on by default, +# but consider turning it off if you are supporting an older version of Windows. +wasapi-virtual-default-devices = [] + # Deprecated, the `oboe` backend has been removed oboe-shared-stdcxx = [] diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index c083d9493..c4dc0f37e 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -11,21 +11,20 @@ use std::mem; use std::os::windows::ffi::OsStringExt; use std::ptr; use std::slice; -use std::sync::mpsc::Sender; use std::sync::OnceLock; use std::sync::{Arc, Mutex, MutexGuard}; use std::time::Duration; use super::com; use super::{windows_err_to_cpal_err, windows_err_to_cpal_err_message}; +use windows::core::Interface; use windows::core::GUID; -use windows::core::{implement, IUnknown, Interface, HRESULT, PCWSTR}; use windows::Win32::Devices::Properties; use windows::Win32::Foundation; use windows::Win32::Media::Audio::IAudioRenderClient; use windows::Win32::Media::{Audio, KernelStreaming, Multimedia}; use windows::Win32::System::Com; -use windows::Win32::System::Com::{CoTaskMemFree, StringFromIID, StructuredStorage, STGM_READ}; +use windows::Win32::System::Com::{StructuredStorage, STGM_READ}; use windows::Win32::System::Threading; use windows::Win32::System::Variant::VT_LPWSTR; @@ -274,53 +273,50 @@ unsafe fn format_from_waveformatex_ptr( Some(format) } -#[implement(Audio::IActivateAudioInterfaceCompletionHandler)] -struct CompletionHandler(Sender>); +#[cfg(feature = "wasapi-virtual-default-devices")] +unsafe fn activate_audio_interface_sync( + deviceinterfacepath: windows::core::PWSTR, +) -> windows::core::Result { + use windows::core::IUnknown; -fn retrieve_result( - operation: &Audio::IActivateAudioInterfaceAsyncOperation, -) -> windows::core::Result { - let mut result = HRESULT::default(); - let mut interface: Option = None; - unsafe { - operation.GetActivateResult(&mut result, &mut interface)?; + #[windows::core::implement(Audio::IActivateAudioInterfaceCompletionHandler)] + struct CompletionHandler(std::sync::mpsc::Sender>); + + fn retrieve_result( + operation: &Audio::IActivateAudioInterfaceAsyncOperation, + ) -> windows::core::Result { + let mut result = windows::core::HRESULT::default(); + let mut interface: Option = None; + unsafe { + operation.GetActivateResult(&mut result, &mut interface)?; + } + result.ok()?; + interface.ok_or_else(|| { + windows::core::Error::new( + Audio::AUDCLNT_E_DEVICE_INVALIDATED, + "audio interface could not be retrieved during activation", + ) + }) } - result.ok()?; - interface.ok_or_else(|| { - windows::core::Error::new( - Audio::AUDCLNT_E_DEVICE_INVALIDATED, - "audio interface could not be retrieved during activation", - ) - }) -} -impl Audio::IActivateAudioInterfaceCompletionHandler_Impl for CompletionHandler_Impl { - fn ActivateCompleted( - &self, - operation: windows::core::Ref, - ) -> windows::core::Result<()> { - let result = operation.ok().and_then(retrieve_result); - let _ = self.0.send(result); - Ok(()) + impl Audio::IActivateAudioInterfaceCompletionHandler_Impl for CompletionHandler_Impl { + fn ActivateCompleted( + &self, + operation: windows::core::Ref, + ) -> windows::core::Result<()> { + let result = operation.ok().and_then(retrieve_result); + let _ = self.0.send(result); + Ok(()) + } } -} -#[allow(non_snake_case)] -unsafe fn ActivateAudioInterfaceSync( - deviceinterfacepath: P0, - activationparams: Option<*const StructuredStorage::PROPVARIANT>, -) -> windows::core::Result -where - P0: windows::core::Param, - T: Interface, -{ let (sender, receiver) = std::sync::mpsc::channel(); let completion: Audio::IActivateAudioInterfaceCompletionHandler = CompletionHandler(sender).into(); Audio::ActivateAudioInterfaceAsync( deviceinterfacepath, - &T::IID, - activationparams, + &Audio::IAudioClient::IID, + None, &completion, )?; let result = receiver.recv_timeout(Duration::from_secs(2)).unwrap()?; @@ -439,18 +435,25 @@ impl Device { return Ok(lock); } + // When using virtual default devices, we use `ActivateAudioInterfaceAsync` to get + // an `IAudioClient` for the default output or input device. When retrieved this way, + // streams will be automatically rerouted if the default device is changed. + // + // Otherwise, we use `Activate` to get an `IAudioClient` for the current device. + + #[cfg(feature = "wasapi-virtual-default-devices")] let audio_client: Audio::IAudioClient = unsafe { match &self.device { DeviceType::DefaultOutput => { - let default_audio = StringFromIID(&Audio::DEVINTERFACE_AUDIO_RENDER)?; - let result = ActivateAudioInterfaceSync(PCWSTR(default_audio.as_ptr()), None); - CoTaskMemFree(Some(default_audio.as_ptr() as _)); + let default_audio = Com::StringFromIID(&Audio::DEVINTERFACE_AUDIO_RENDER)?; + let result = activate_audio_interface_sync(default_audio); + Com::CoTaskMemFree(Some(default_audio.as_ptr() as _)); result? } DeviceType::DefaultInput => { - let default_audio = StringFromIID(&Audio::DEVINTERFACE_AUDIO_CAPTURE)?; - let result = ActivateAudioInterfaceSync(PCWSTR(default_audio.as_ptr()), None); - CoTaskMemFree(Some(default_audio.as_ptr() as _)); + let default_audio = Com::StringFromIID(&Audio::DEVINTERFACE_AUDIO_CAPTURE)?; + let result = activate_audio_interface_sync(default_audio); + Com::CoTaskMemFree(Some(default_audio.as_ptr() as _)); result? } DeviceType::Specific(device) => { @@ -461,6 +464,16 @@ impl Device { } }; + #[cfg(not(feature = "wasapi-virtual-default-devices"))] + let audio_client = unsafe { + self.immdevice() + .ok_or(windows::core::Error::new( + Audio::AUDCLNT_E_DEVICE_INVALIDATED, + "device not found while getting audio client", + ))? + .Activate(Com::CLSCTX_ALL, None)? + }; + *lock = Some(IAudioClientWrapper(audio_client)); Ok(lock) } From 0830b3f189a643d448c6fcee22c3b0c49a67791d Mon Sep 17 00:00:00 2001 From: Philpax Date: Wed, 1 Oct 2025 20:38:41 +0200 Subject: [PATCH 09/13] fix: remove constraint on windows-core version in Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 0b66821b4..f4ca6d01a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,7 @@ windows = { version = ">=0.61, <=0.62", features = [ ] } # Explicitly depend on windows-core for use with the `windows::core::implement` macro. # Ensure this is the same version as the `windows` dependency. -windows-core = { version = ">0.58, <=0.62" } +windows-core = "*" audio_thread_priority = { version = "0.34.0", optional = true } asio-sys = { version = "0.2", path = "asio-sys", optional = true } num-traits = { version = "0.2.6", optional = true } From 87d5df39d30515344721b6ca9455f92e6feee9ae Mon Sep 17 00:00:00 2001 From: Philpax Date: Wed, 1 Oct 2025 21:33:46 +0200 Subject: [PATCH 10/13] fix(ci): re-resolve `windows-core` version --- .github/workflows/cpal.yml | 6 ++++++ Cargo.toml | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cpal.yml b/.github/workflows/cpal.yml index 17dc366ef..5de0bcbc7 100644 --- a/.github/workflows/cpal.yml +++ b/.github/workflows/cpal.yml @@ -347,6 +347,12 @@ jobs: # Use cargo update --precise to lock the windows crate to a specific version cargo update --precise ${{ matrix.windows-version }} windows + # Remove and re-add `windows-core` to force Cargo to re-resolve it + # I'd prefer a more surgical approach, but things get complicated once multiple `windows-core` + # versions are in play + cargo rm --target='cfg(target_os = "windows")' windows-core + cargo add --target='cfg(target_os = "windows")' windows-core@* + # Verify the version was locked correctly echo "Locked windows crate version:" cargo tree | grep "windows v" || echo "Windows crate not found in dependency tree" diff --git a/Cargo.toml b/Cargo.toml index f4ca6d01a..d343836a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,6 @@ windows = { version = ">=0.61, <=0.62", features = [ "Win32_UI_Shell_PropertiesSystem", ] } # Explicitly depend on windows-core for use with the `windows::core::implement` macro. -# Ensure this is the same version as the `windows` dependency. windows-core = "*" audio_thread_priority = { version = "0.34.0", optional = true } asio-sys = { version = "0.2", path = "asio-sys", optional = true } From f65a156c0c0ad0927dda1f3e92bb75704d72b2ad Mon Sep 17 00:00:00 2001 From: Philpax Date: Thu, 9 Oct 2025 01:23:16 +0200 Subject: [PATCH 11/13] chore(wasapi): review feedback --- Cargo.toml | 9 +++++---- src/host/wasapi/com.rs | 12 +++++++++++- src/host/wasapi/device.rs | 37 +++++++++++++++++++------------------ 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 47e8c6ba7..afb9037d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,11 +17,12 @@ asio = [ "num-traits", ] # Only available on Windows. See README for setup instructions. -# Enable virtual default devices for WASAPI, so that audio will be -# automatically rerouted when the default input or output device is changed. +# Enable virtual default devices for WASAPI. When enabled: +# - Audio automatically reroutes when the default device changes +# - Streams survive device changes (e.g., plugging in headphones) +# - Requires Windows 8 or later # -# Note that this only works on Windows 8 and above. It is turned on by default, -# but consider turning it off if you are supporting an older version of Windows. +# Disable this feature if supporting Windows 7 or earlier. wasapi-virtual-default-devices = [] # Deprecated, the `oboe` backend has been removed diff --git a/src/host/wasapi/com.rs b/src/host/wasapi/com.rs index 710f333c0..fc0885323 100644 --- a/src/host/wasapi/com.rs +++ b/src/host/wasapi/com.rs @@ -4,7 +4,9 @@ use super::IoError; use std::marker::PhantomData; use windows::Win32::Foundation::RPC_E_CHANGED_MODE; -use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED}; +use windows::Win32::System::Com::{ + CoInitializeEx, CoTaskMemFree, CoUninitialize, COINIT_APARTMENTTHREADED, +}; thread_local!(static COM_INITIALIZED: ComInitialized = { unsafe { @@ -50,6 +52,14 @@ impl Drop for ComInitialized { } } +/// RAII for COM-originating strings that need to be freed with `CoTaskMemFree`. +pub struct ComString(pub windows::core::PWSTR); +impl Drop for ComString { + fn drop(&mut self) { + unsafe { CoTaskMemFree(Some(self.0.as_ptr() as *mut _)) } + } +} + /// Ensures that COM is initialized in this thread. #[inline] pub fn com_initialized() { diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index c4dc0f37e..ccff9a04b 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -1,3 +1,4 @@ +use crate::host::wasapi::com::ComString; use crate::FrameCount; use crate::{ BackendSpecificError, BufferSize, Data, DefaultStreamConfigError, DeviceNameError, @@ -319,7 +320,16 @@ unsafe fn activate_audio_interface_sync( None, &completion, )?; - let result = receiver.recv_timeout(Duration::from_secs(2)).unwrap()?; + // The choice of 2 seconds here is arbitrary; it is a failsafe in the event that + // `ActivateAudioInterfaceAsync` never resolves. + let result = receiver + .recv_timeout(Duration::from_secs(2)) + .map_err(|_| { + windows::core::Error::new( + Audio::AUDCLNT_E_DEVICE_INVALIDATED, + "timeout waiting for audio interface activation", + ) + })??; result.cast() } @@ -445,16 +455,14 @@ impl Device { let audio_client: Audio::IAudioClient = unsafe { match &self.device { DeviceType::DefaultOutput => { - let default_audio = Com::StringFromIID(&Audio::DEVINTERFACE_AUDIO_RENDER)?; - let result = activate_audio_interface_sync(default_audio); - Com::CoTaskMemFree(Some(default_audio.as_ptr() as _)); - result? + let default_audio = + ComString(Com::StringFromIID(&Audio::DEVINTERFACE_AUDIO_RENDER)?); + activate_audio_interface_sync(default_audio.0)? } DeviceType::DefaultInput => { - let default_audio = Com::StringFromIID(&Audio::DEVINTERFACE_AUDIO_CAPTURE)?; - let result = activate_audio_interface_sync(default_audio); - Com::CoTaskMemFree(Some(default_audio.as_ptr() as _)); - result? + let default_audio = + ComString(Com::StringFromIID(&Audio::DEVINTERFACE_AUDIO_CAPTURE)?); + activate_audio_interface_sync(default_audio.0)? } DeviceType::Specific(device) => { // can fail if the device has been disconnected since we enumerated it, or if @@ -909,19 +917,12 @@ impl PartialEq for Device { // In this code section we're trying to use the GetId method for the device comparison, cf. // https://docs.microsoft.com/en-us/windows/desktop/api/mmdeviceapi/nf-mmdeviceapi-immdevice-getid unsafe { - struct IdRAII(windows::core::PWSTR); - /// RAII for device IDs. - impl Drop for IdRAII { - fn drop(&mut self) { - unsafe { Com::CoTaskMemFree(Some(self.0 .0 as *mut _)) } - } - } // GetId only fails with E_OUTOFMEMORY and if it does, we're probably dead already. // Plus it won't do to change the device comparison logic unexpectedly. let id1 = dev1.GetId().expect("cpal: GetId failure"); - let id1 = IdRAII(id1); + let id1 = ComString(id1); let id2 = dev2.GetId().expect("cpal: GetId failure"); - let id2 = IdRAII(id2); + let id2 = ComString(id2); // 16-bit null-terminated comparison. let mut offset = 0; loop { From ad2f408d90c2dd6e5bda6f254c70b324c31bf4f6 Mon Sep 17 00:00:00 2001 From: Philpax Date: Mon, 22 Dec 2025 15:59:32 +1100 Subject: [PATCH 12/13] feat: explicitly pin to windows 0.62.2 --- .github/workflows/platforms.yml | 39 --------------------------------- Cargo.toml | 12 ++-------- 2 files changed, 2 insertions(+), 49 deletions(-) diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml index 25e67ffe6..0bd6a78c7 100644 --- a/.github/workflows/platforms.yml +++ b/.github/workflows/platforms.yml @@ -351,44 +351,6 @@ jobs: - name: Build beep example run: cargo build --example beep --target ${{ env.TARGET }} - # Windows crate version compatibility - windows-versions: - runs-on: windows-latest - strategy: - fail-fast: false - matrix: - version: ["0.59.0", "0.60.0", "0.61.3"] - - name: windows-crate-v${{ matrix.version }} - steps: - - uses: actions/checkout@v5 - - - name: Install dependencies - run: choco install llvm - - - name: Install Rust MSRV (${{ env.MSRV_WINDOWS }}) - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.MSRV_WINDOWS }} - - - name: Rust Cache - uses: Swatinem/rust-cache@v2 - with: - key: windows-v${{ matrix.version }} - - - name: Lock windows crate to specific version - shell: bash - run: | - cargo generate-lockfile - cargo update -p windows --precise ${{ matrix.version }} - echo "Locked windows crate version:" - cargo tree | grep "windows v" || echo "Windows crate not found in dependency tree" - echo "Cargo.lock entry:" - grep -A 5 "name = \"windows\"" Cargo.lock | head -10 - - - name: Check WASAPI with windows v${{ matrix.version }} - run: cargo check --verbose - # cpal publishing (only on cpal release events) publish-cpal: if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v') @@ -403,7 +365,6 @@ jobs: - wasm-bindgen - wasm-audioworklet - wasm-wasip1 - - windows-versions runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 diff --git a/Cargo.toml b/Cargo.toml index 8df0731bb..ee1f8e490 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,16 +73,8 @@ hound = "3.5" ringbuf = "0.4" clap = { version = ">=4.0, <=4.5", features = ["derive"] } -# Support a range of versions in order to avoid duplication of this crate. Make sure to test all -# versions when bumping to a new release, and only increase the minimum when absolutely necessary. -# When updating this, also update the "windows-version" matrix in the CI workflow. [target.'cfg(target_os = "windows")'.dependencies] -# The `implement` feature was removed in windows-0.61, which means that we can't -# use older versions of the `windows` crate without explicitly activating `implement` -# for them, which will cause problems for >=0.61. -# -# See . -windows = { version = ">=0.61, <=0.62", features = [ +windows = { version = "0.62.2", features = [ "Win32_Media_Audio", "Win32_Foundation", "Win32_Devices_Properties", @@ -96,7 +88,7 @@ windows = { version = ">=0.61, <=0.62", features = [ "Win32_UI_Shell_PropertiesSystem", ] } # Explicitly depend on windows-core for use with the `windows::core::implement` macro. -windows-core = "*" +windows-core = "0.62.2" audio_thread_priority = { version = "0.34", optional = true } asio-sys = { version = "0.2", path = "asio-sys", optional = true } num-traits = { version = "0.2", optional = true } From e06fe0479d8a80144a734c7fe605049ad2d98620 Mon Sep 17 00:00:00 2001 From: Philpax Date: Mon, 22 Dec 2025 16:09:26 +1100 Subject: [PATCH 13/13] feat: default wasapi-virtual-default-devices -> windows-legacy --- Cargo.toml | 20 ++++++++++++-------- src/host/wasapi/device.rs | 15 ++++++++------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ee1f8e490..ff00840f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" rust-version = "1.77" [features] -default = ["wasapi-virtual-default-devices"] +default = [] # ASIO backend for Windows # Provides low-latency audio I/O by bypassing the Windows audio stack @@ -21,14 +21,18 @@ asio = [ "dep:num-traits", ] - -# Enable virtual default devices for WASAPI. When enabled: -# - Audio automatically reroutes when the default device changes -# - Streams survive device changes (e.g., plugging in headphones) -# - Requires Windows 8 or later +# Legacy Windows audio device activation mode. When enabled: +# - Uses IMMDevice::Activate instead of ActivateAudioInterfaceAsync +# - Audio does NOT automatically reroute when the default device changes +# - Streams will break when the default device changes (e.g., plugging in headphones) +# +# By default (without this feature), CPAL uses virtual default devices that: +# - Automatically reroute audio when the default device changes +# - Allow streams to survive device changes +# - Require Windows 8 or later # -# Disable this feature if supporting Windows 7 or earlier. -wasapi-virtual-default-devices = [] +# Enable this feature only if supporting Windows 7 or earlier. +windows-legacy = [] # JACK Audio Connection Kit backend # Provides low-latency connections between applications and audio hardware diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 5d09e4a58..8938dcbb3 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -305,7 +305,7 @@ unsafe fn format_from_waveformatex_ptr( Some(format) } -#[cfg(feature = "wasapi-virtual-default-devices")] +#[cfg(not(feature = "windows-legacy"))] unsafe fn activate_audio_interface_sync( deviceinterfacepath: windows::core::PWSTR, ) -> windows::core::Result { @@ -587,13 +587,14 @@ impl Device { return Ok(lock); } - // When using virtual default devices, we use `ActivateAudioInterfaceAsync` to get - // an `IAudioClient` for the default output or input device. When retrieved this way, - // streams will be automatically rerouted if the default device is changed. + // By default, we use `ActivateAudioInterfaceAsync` to get an `IAudioClient` for + // virtual default devices. When retrieved this way, streams will be automatically + // rerouted if the default device is changed. // - // Otherwise, we use `Activate` to get an `IAudioClient` for the current device. + // When the `windows-legacy` feature is enabled, we use `Activate` to get an + // `IAudioClient` for the current device, which does not support automatic rerouting. - #[cfg(feature = "wasapi-virtual-default-devices")] + #[cfg(not(feature = "windows-legacy"))] let audio_client: Audio::IAudioClient = unsafe { match &self.device { DeviceHandle::DefaultOutput => { @@ -614,7 +615,7 @@ impl Device { } }; - #[cfg(not(feature = "wasapi-virtual-default-devices"))] + #[cfg(feature = "windows-legacy")] let audio_client = unsafe { self.immdevice() .ok_or(windows::core::Error::new(