diff --git a/Cargo.lock b/Cargo.lock index 17f4323..008dde2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,12 +23,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "anyhow" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" - [[package]] name = "autocfg" version = "1.3.0" @@ -41,31 +35,12 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "console" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" -dependencies = [ - "encode_unicode", - "lazy_static", - "libc", - "unicode-width", - "windows-sys", -] - [[package]] name = "duplicate" version = "1.0.0" @@ -76,59 +51,24 @@ dependencies = [ "proc-macro-error", ] -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -[[package]] -name = "indicatif" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" -dependencies = [ - "console", - "instant", - "number_prefix", - "portable-atomic", - "unicode-width", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "interflow" version = "0.1.0" dependencies = [ "alsa", - "anyhow", "cfg_aliases", "duplicate", - "indicatif", "ndarray", "thiserror", + "windows", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.155" @@ -185,24 +125,12 @@ dependencies = [ "autocfg", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" -[[package]] -name = "portable-atomic" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -298,12 +226,6 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-width" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" - [[package]] name = "version_check" version = "0.9.4" @@ -311,11 +233,66 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] -name = "windows-sys" -version = "0.52.0" +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ + "windows-result", "windows-targets", ] diff --git a/Cargo.toml b/Cargo.toml index 175a5d6..4813530 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,26 @@ cfg_aliases = "0.2.1" [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies] alsa = "0.9.0" +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.58.0", features = [ + "Win32_Media_Audio", + "Win32_Foundation", + "Win32_Devices_Properties", + "Win32_Media_KernelStreaming", + "Win32_System_Com_StructuredStorage", + "Win32_System_Threading", + "Win32_Security", + "Win32_System_SystemServices", + "Win32_System_Variant", + "Win32_Media_Multimedia", + "Win32_UI_Shell_PropertiesSystem" +]} + [[example]] name = "enumerate_alsa" path = "examples/enumerate_alsa.rs" + +[[example]] +name = "enumerate_wasapi" +path = "examples/enumerate_wasapi.rs" + diff --git a/build.rs b/build.rs index abfd10d..9ec3eaf 100644 --- a/build.rs +++ b/build.rs @@ -5,6 +5,7 @@ fn main() { cfg_aliases! { wasm: { any(target_os = "wasm32") }, os_alsa: { any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", - target_os = "netbsd") } + target_os = "netbsd") }, + os_wasapi: { target_os = "windows" } } } diff --git a/examples/enumerate_alsa.rs b/examples/enumerate_alsa.rs index a864085..93457d5 100644 --- a/examples/enumerate_alsa.rs +++ b/examples/enumerate_alsa.rs @@ -1,9 +1,7 @@ -use std::error::Error; - mod util; #[cfg(os_alsa)] -fn main() -> Result<(), Box> { +fn main() -> Result<(), Box> { use crate::util::enumerate::enumerate_devices; use interflow::backends::alsa::AlsaDriver; enumerate_devices(AlsaDriver::default()) diff --git a/examples/enumerate_wasapi.rs b/examples/enumerate_wasapi.rs new file mode 100644 index 0000000..0109031 --- /dev/null +++ b/examples/enumerate_wasapi.rs @@ -0,0 +1,14 @@ +mod util; + +#[cfg(os_wasapi)] +fn main() -> Result<(), Box> { + use crate::util::enumerate::enumerate_devices; + use interflow::backends::wasapi::WasapiDriver; + enumerate_devices(WasapiDriver) +} + +#[cfg(not(os_wasapi))] +fn main() { + println!("WASAPI driver is not available on this platform"); +} + diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 98b7f29..fc203b3 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -1,4 +1,6 @@ -use crate::{AudioDevice, AudioDriver, AudioInputDevice, AudioOutputCallback, AudioOutputDevice, DeviceType}; +use crate::{ + AudioDevice, AudioDriver, AudioInputDevice, AudioOutputCallback, AudioOutputDevice, DeviceType, +}; #[cfg(os_alsa)] pub mod alsa; @@ -42,3 +44,5 @@ pub fn default_output_device() -> impl AudioOutputDevice { #[cfg(os_alsa)] default_output_device_from(&alsa::AlsaDriver) } +#[cfg(os_wasapi)] +pub mod wasapi; diff --git a/src/backends/wasapi.rs b/src/backends/wasapi.rs new file mode 100644 index 0000000..c053a8e --- /dev/null +++ b/src/backends/wasapi.rs @@ -0,0 +1,311 @@ +use std::{ + borrow::Cow, + ffi::OsString, + os::windows::ffi::OsStringExt, + sync::OnceLock, +}; + +use crate::{AudioDevice, AudioDriver, Channel, DeviceType, StreamConfig}; +use thiserror::Error; +use windows:: + Win32::{ + Devices::Properties, + Media::Audio, + System::{ + Com::{self, StructuredStorage, STGM_READ}, + Variant::VT_LPWSTR, + }, + }; + + +mod util { + use std::marker::PhantomData; + + use windows::Win32::Foundation::RPC_E_CHANGED_MODE; + use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED}; + + thread_local!(static COM_INITIALIZER: ComInitializer = { + unsafe { + // Try to initialize COM with STA by default to avoid compatibility issues with the ASIO + // backend (where CoInitialize() is called by the ASIO SDK) or winit (where drag and drop + // requires STA). + // This call can fail with RPC_E_CHANGED_MODE if another library initialized COM with MTA. + // That's OK though since COM ensures thread-safety/compatibility through marshalling when + // necessary. + let result = CoInitializeEx(None, COINIT_APARTMENTTHREADED); + if result.is_ok() || result == RPC_E_CHANGED_MODE { + ComInitializer { + result, + _ptr: PhantomData, + } + } else { + // COM initialization failed in another way, something is really wrong. + panic!( + "Failed to initialize COM: {}", + std::io::Error::from_raw_os_error(result.0) + ); + } + } + }); + + /// RAII object that guards the fact that COM is initialized. + /// + // We store a raw pointer because it's the only way at the moment to remove `Send`/`Sync` from the + // object. + struct ComInitializer { + result: windows::core::HRESULT, + _ptr: PhantomData<*mut ()>, + } + + impl Drop for ComInitializer { + #[inline] + fn drop(&mut self) { + // Need to avoid calling CoUninitialize() if CoInitializeEx failed since it may have + // returned RPC_E_MODE_CHANGED - which is OK, see above. + if self.result.is_ok() { + unsafe { CoUninitialize() }; + } + } + } + + /// Ensures that COM is initialized in this thread. + #[inline] + pub fn com_initializer() { + COM_INITIALIZER.with(|_| {}); + } +} + +/// Type of errors from the WASAPI backend. +#[derive(Debug, Error)] +#[error("WASAPI error: ")] +pub enum WasapiError { + /// Error originating from WASAPI. + BackendError(#[from] windows::core::Error), +} + +/// The WASAPI driver. +#[derive(Debug, Clone, Default)] +pub struct WasapiDriver; + +impl AudioDriver for WasapiDriver { + type Error = WasapiError; + type Device = WasapiDevice; + + const DISPLAY_NAME: &'static str = "WASAPI"; + + fn version(&self) -> Result, Self::Error> { + Ok(Cow::Borrowed("WASAPI (version unknown)")) + } + + fn default_device(&self, device_type: DeviceType) -> Result, Self::Error> { + audio_device_enumerator().get_default_device(device_type) + } + + fn list_devices(&self) -> Result, Self::Error> { + audio_device_enumerator().get_device_list() + } +} + +/// Type of devices available from the WASAPI driver. +#[derive(Debug)] +pub struct WasapiDevice { + device: windows::Win32::Media::Audio::IMMDevice, + device_type: DeviceType, +} + +impl AudioDevice for WasapiDevice { + type Error = WasapiError; + + fn name(&self) -> Cow { + match get_device_name(&self.device) { + Some(std) => Cow::Owned(std), + None => { + eprintln!("Cannot get audio device name"); + Cow::Borrowed("") + } + } + } + + fn device_type(&self) -> DeviceType { + self.device_type + } + + fn is_config_supported(&self, config: &StreamConfig) -> bool { + todo!() + } + + fn enumerate_configurations(&self) -> Option> { + None::<[StreamConfig; 0]> + } + + fn channel_map(&self) -> impl IntoIterator { + [] + } + +} + +impl WasapiDevice { + fn new(device: Audio::IMMDevice, device_type: DeviceType) -> Self { + WasapiDevice { + device, + device_type, + } + } +} + +fn get_device_name(device: &windows::Win32::Media::Audio::IMMDevice) -> Option { + 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, else the interface's friendly-name, else the device description. + let mut property_value = property_store + .GetValue(&Properties::DEVPKEY_Device_FriendlyName as *const _ as *const _) + .or(property_store.GetValue( + &Properties::DEVPKEY_DeviceInterface_FriendlyName as *const _ as *const _, + )) + .or(property_store + .GetValue(&Properties::DEVPKEY_Device_DeviceDesc as *const _ as *const _)).ok()?; + + 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 { + return None; + } + + 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; + } + + // Convert to a string. + let name_slice = std::slice::from_raw_parts(ptr_utf16, len as usize); + let name_os_string: OsString = OsStringExt::from_wide(name_slice); + let name = name_os_string.into_string().unwrap_or_else(|os_string| os_string.to_string_lossy().into()); + + // Clean up. + StructuredStorage::PropVariantClear(&mut property_value).ok()?; + + Some(name) + } +} + + +static ENUMERATOR: OnceLock = OnceLock::new(); + +fn audio_device_enumerator() -> &'static AudioDeviceEnumerator { + ENUMERATOR.get_or_init(|| { + // Make sure COM is initialised. + util::com_initializer(); + + unsafe { + let enumerator = Com::CoCreateInstance::<_, Audio::IMMDeviceEnumerator>( + &Audio::MMDeviceEnumerator, + None, + Com::CLSCTX_ALL, + ) + .unwrap(); + + AudioDeviceEnumerator(enumerator) + } + }) +} + +/// Send/Sync wrapper around `IMMDeviceEnumerator`. +struct AudioDeviceEnumerator(Audio::IMMDeviceEnumerator); + +impl AudioDeviceEnumerator { + + // Returns the default output device. + fn get_default_device(&self, device_type: DeviceType) -> Result, WasapiError> { + let data_flow = match device_type { + DeviceType::Input => Audio::eCapture, + DeviceType::Output => Audio::eRender, + _=> return Ok(None), + }; + + unsafe { + let device = self + .0 + .GetDefaultAudioEndpoint(data_flow, Audio::eConsole)?; + + Ok(Some(WasapiDevice::new(device, DeviceType::Output))) + } + } + + // Returns a chained iterator of output and input devices. + fn get_device_list(&self) -> Result, WasapiError> { + + // Create separate collections for output and input devices and then chain them. + unsafe { + let output_collection = self + .0 + .EnumAudioEndpoints(Audio::eRender, Audio::DEVICE_STATE_ACTIVE)?; + + let count = output_collection.GetCount()?; + + let output_device_list = WasapiDeviceList { + collection: output_collection, + total_count: count, + next_item: 0, + device_type: DeviceType::Output, + }; + + let input_collection = self + .0 + .EnumAudioEndpoints(Audio::eCapture, Audio::DEVICE_STATE_ACTIVE)?; + + let count = input_collection.GetCount()?; + + let input_device_list = WasapiDeviceList { + collection: input_collection, + total_count: count, + next_item: 0, + device_type: DeviceType::Input, + }; + + Ok(output_device_list.chain(input_device_list)) + } + } +} + +unsafe impl Send for AudioDeviceEnumerator {} +unsafe impl Sync for AudioDeviceEnumerator {} + +/// An iterable collection WASAPI devices. +pub struct WasapiDeviceList { + collection: Audio::IMMDeviceCollection, + total_count: u32, + next_item: u32, + device_type: DeviceType, +} + +unsafe impl Send for WasapiDeviceList {} +unsafe impl Sync for WasapiDeviceList {} + +impl Iterator for WasapiDeviceList { + type Item = WasapiDevice; + + fn next(&mut self) -> Option { + if self.next_item >= self.total_count { + return None; + } + + unsafe { + let device = self.collection.Item(self.next_item).unwrap(); + self.next_item += 1; + Some(WasapiDevice::new(device, self.device_type)) + } + } + + fn size_hint(&self) -> (usize, Option) { + let rest = (self.total_count - self.next_item) as usize; + (rest, Some(rest)) + } +} \ No newline at end of file