From b3a66713a9a0500c3892bc9d48fb5f086985fb33 Mon Sep 17 00:00:00 2001 From: Dr George Atkinson Date: Thu, 25 Jul 2024 17:57:22 +0100 Subject: [PATCH 1/9] Implement device enumeration for WASAPI driver backend --- Cargo.lock | 141 +++++++--------- Cargo.toml | 20 +++ build.rs | 3 +- examples/enumerate_alsa.rs | 4 +- examples/enumerate_wasapi.rs | 14 ++ src/backends/mod.rs | 6 +- src/backends/wasapi.rs | 311 +++++++++++++++++++++++++++++++++++ 7 files changed, 412 insertions(+), 87 deletions(-) create mode 100644 examples/enumerate_wasapi.rs create mode 100644 src/backends/wasapi.rs 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 From e78d53c6a5bb84fb345ecb24000f216e593c03ec Mon Sep 17 00:00:00 2001 From: Dr George Atkinson Date: Sun, 28 Jul 2024 23:31:54 +0100 Subject: [PATCH 2/9] WIP output stream --- Cargo.lock | 88 +++++++++ examples/enumerate_alsa.rs | 1 - examples/enumerate_wasapi.rs | 1 - examples/sine_wave.rs | 21 +- examples/util/enumerate.rs | 7 +- examples/util/mod.rs | 2 +- src/audio_buffer.rs | 11 +- src/backends/alsa.rs | 12 +- src/backends/mod.rs | 17 +- src/backends/wasapi.rs | 369 +++++++++++++++++++++++++++++++---- src/channel_map.rs | 14 +- src/lib.rs | 10 +- src/prelude.rs | 2 +- 13 files changed, 476 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 008dde2..13cdb69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,12 @@ 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" @@ -35,12 +41,31 @@ 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" @@ -51,24 +76,60 @@ 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" @@ -125,12 +186,24 @@ 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" @@ -226,6 +299,12 @@ 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" @@ -296,6 +375,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/examples/enumerate_alsa.rs b/examples/enumerate_alsa.rs index 93457d5..ccbabc5 100644 --- a/examples/enumerate_alsa.rs +++ b/examples/enumerate_alsa.rs @@ -11,4 +11,3 @@ fn main() -> Result<(), Box> { fn main() { println!("ALSA driver is not available on this platform"); } - diff --git a/examples/enumerate_wasapi.rs b/examples/enumerate_wasapi.rs index 0109031..05c658c 100644 --- a/examples/enumerate_wasapi.rs +++ b/examples/enumerate_wasapi.rs @@ -11,4 +11,3 @@ fn main() -> Result<(), Box> { fn main() { println!("WASAPI driver is not available on this platform"); } - diff --git a/examples/sine_wave.rs b/examples/sine_wave.rs index 25f22d8..153f34a 100644 --- a/examples/sine_wave.rs +++ b/examples/sine_wave.rs @@ -13,13 +13,15 @@ fn main() -> Result<()> { }; assert!(device.is_config_supported(&config)); println!("Using device {}", device.name()); - let stream = device.create_output_stream( - config, - SineWave { - frequency: 440., - phase: 0., - }, - ).unwrap(); + let stream = device + .create_output_stream( + config, + SineWave { + frequency: 440., + phase: 0., + }, + ) + .unwrap(); println!("Press Enter to stop"); std::io::stdin().read_line(&mut String::new())?; stream.eject().unwrap(); @@ -33,7 +35,10 @@ struct SineWave { impl AudioOutputCallback for SineWave { fn on_output_data(&mut self, context: AudioCallbackContext, mut output: AudioOutput) { - eprintln!("Callback called, timestamp: {:2.3} s", context.timestamp.as_seconds()); + eprintln!( + "Callback called, timestamp: {:2.3} s", + context.timestamp.as_seconds() + ); let sr = context.timestamp.samplerate as f32; for i in 0..output.buffer.num_samples() { output.buffer.set_mono(i, self.next_sample(sr)); diff --git a/examples/util/enumerate.rs b/examples/util/enumerate.rs index b888927..4805b35 100644 --- a/examples/util/enumerate.rs +++ b/examples/util/enumerate.rs @@ -1,7 +1,10 @@ use interflow::{AudioDevice, AudioDriver, DeviceType}; use std::error::Error; -pub fn enumerate_devices(driver: Driver) -> Result<(), Box> where ::Error: 'static { +pub fn enumerate_devices(driver: Driver) -> Result<(), Box> +where + ::Error: 'static, +{ eprintln!("Driver name : {}", Driver::DISPLAY_NAME); eprintln!("Driver version: {}", driver.version()?); eprintln!("Default device"); @@ -19,4 +22,4 @@ pub fn enumerate_devices(driver: Driver) -> Result<(), Box< eprintln!("\t{} ({:?})", device.name(), device.device_type()); } Ok(()) -} \ No newline at end of file +} diff --git a/examples/util/mod.rs b/examples/util/mod.rs index 6ce24a3..3b5980b 100644 --- a/examples/util/mod.rs +++ b/examples/util/mod.rs @@ -1 +1 @@ -pub mod enumerate; \ No newline at end of file +pub mod enumerate; diff --git a/src/audio_buffer.rs b/src/audio_buffer.rs index b0228f8..6e8ed07 100644 --- a/src/audio_buffer.rs +++ b/src/audio_buffer.rs @@ -54,7 +54,7 @@ impl Default for AudioBufferBase { } } -impl> PartialEq> for AudioBufferBase +impl> PartialEq> for AudioBufferBase where S::Elem: PartialEq, { @@ -178,7 +178,7 @@ impl AudioBufferBase { pub fn channels_mut(&mut self) -> impl '_ + Iterator> { self.storage.rows_mut().into_iter() } - + pub fn as_interleaved_mut(&mut self) -> ArrayViewMut2 { self.storage.view_mut().reversed_axes() } @@ -374,11 +374,8 @@ impl<'a, T: Sample> AudioMut<'a, T> { } } - pub fn mix( - &mut self, - other: AudioRef, - other_amplitude: T::Float, - ) where + pub fn mix(&mut self, other: AudioRef, other_amplitude: T::Float) + where T: AddAssign, { for (mut ch_a, ch_b) in self.channels_mut().zip(other.channels()) { diff --git a/src/backends/alsa.rs b/src/backends/alsa.rs index 0c694b8..e184afd 100644 --- a/src/backends/alsa.rs +++ b/src/backends/alsa.rs @@ -1,21 +1,21 @@ use core::fmt; -use std::{borrow::Cow, convert::Infallible, ffi::CStr}; -use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::thread::JoinHandle; use std::time::Duration; +use std::{borrow::Cow, convert::Infallible, ffi::CStr}; use alsa::{device_name::HintIter, pcm, PCM}; use thiserror::Error; +use crate::audio_buffer::{AudioMut, AudioRef}; +use crate::channel_map::{Bitset, ChannelMap32}; +use crate::timestamp::Timestamp; use crate::{ AudioCallbackContext, AudioDevice, AudioDriver, AudioInput, AudioInputCallback, AudioInputDevice, AudioOutput, AudioOutputCallback, AudioOutputDevice, AudioStreamHandle, Channel, DeviceType, StreamConfig, }; -use crate::audio_buffer::{AudioMut, AudioRef}; -use crate::channel_map::{Bitset, ChannelMap32}; -use crate::timestamp::Timestamp; #[derive(Debug, Error)] #[error("ALSA error: ")] @@ -236,7 +236,7 @@ impl AlsaStream { let input = AudioInput { buffer, timestamp }; callback.on_input_data(context, input); timestamp += frames as u64; - + match device.pcm.state() { pcm::State::Suspended => { if hwp.can_resume() { diff --git a/src/backends/mod.rs b/src/backends/mod.rs index fc203b3..628d8a0 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -5,9 +5,14 @@ use crate::{ #[cfg(os_alsa)] pub mod alsa; +#[cfg(os_wasapi)] +pub mod wasapi; + pub fn default_driver() -> impl AudioDriver { #[cfg(os_alsa)] - alsa::AlsaDriver + return alsa::AlsaDriver; + #[cfg(os_wasapi)] + return wasapi::WasapiDriver; } pub fn default_input_device_from(driver: &Driver) -> Driver::Device @@ -26,7 +31,9 @@ where pub fn default_input_device() -> impl AudioInputDevice { #[cfg(os_alsa)] - default_input_device_from(&alsa::AlsaDriver) + return default_input_device_from(&alsa::AlsaDriver); + #[cfg(os_wasapi)] + return default_input_device_from(&wasapi::WasapiDriver); } pub fn default_output_device_from(driver: &Driver) -> Driver::Device @@ -42,7 +49,7 @@ where pub fn default_output_device() -> impl AudioOutputDevice { #[cfg(os_alsa)] - default_output_device_from(&alsa::AlsaDriver) + return default_output_device_from(&alsa::AlsaDriver); + #[cfg(os_wasapi)] + return default_output_device_from(&wasapi::WasapiDriver); } -#[cfg(os_wasapi)] -pub mod wasapi; diff --git a/src/backends/wasapi.rs b/src/backends/wasapi.rs index c053a8e..63d4b63 100644 --- a/src/backends/wasapi.rs +++ b/src/backends/wasapi.rs @@ -1,22 +1,28 @@ use std::{ - borrow::Cow, - ffi::OsString, - os::windows::ffi::OsStringExt, - sync::OnceLock, + borrow::Cow, ffi::OsString, marker::PhantomData, ops::Add, os::windows::ffi::OsStringExt, ptr, sync::{atomic::{AtomicBool, Ordering}, Arc, Mutex, OnceLock}, thread::JoinHandle, time::Duration }; -use crate::{AudioDevice, AudioDriver, Channel, DeviceType, StreamConfig}; +use crate::{ + prelude::{AudioMut, Timestamp}, AudioCallbackContext, AudioDevice, AudioDriver, AudioInputCallback, + AudioInputDevice, AudioOutput, AudioOutputCallback, AudioOutputDevice, AudioStreamHandle, + Channel, DeviceType, StreamConfig, +}; use thiserror::Error; -use windows:: - Win32::{ - Devices::Properties, - Media::Audio, - System::{ - Com::{self, StructuredStorage, STGM_READ}, - Variant::VT_LPWSTR, +use windows::Win32::{ + Devices::Properties, + Foundation::{CloseHandle, HANDLE}, + Media::{ + Audio::{ + self, IAudioCaptureClient, IAudioClient, IAudioClock, IAudioRenderClient, IMMDevice, AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, WAVEFORMATEXTENSIBLE, WAVEFORMATEXTENSIBLE_0 }, - }; - + KernelStreaming, Multimedia, + }, + System::{ + Com::{self, StructuredStorage, STGM_READ}, + Threading, + Variant::VT_LPWSTR, + }, +}; mod util { use std::marker::PhantomData; @@ -107,10 +113,21 @@ impl AudioDriver for WasapiDriver { } /// Type of devices available from the WASAPI driver. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct WasapiDevice { device: windows::Win32::Media::Audio::IMMDevice, device_type: DeviceType, + audio_client: Arc>>, +} + +impl WasapiDevice { + fn new(device: IMMDevice, device_type: DeviceType) -> Self { + WasapiDevice { + device, + device_type, + audio_client: Arc::new(Mutex::new(None)), + } + } } impl AudioDevice for WasapiDevice { @@ -134,25 +151,269 @@ impl AudioDevice for WasapiDevice { todo!() } - fn enumerate_configurations(&self) -> Option> { + fn enumerate_configurations(&self) -> Option> { None::<[StreamConfig; 0]> } - fn channel_map(&self) -> impl IntoIterator { + fn channel_map(&self) -> impl IntoIterator { [] } +} + +// impl AudioInputDevice for WasapiDevice { +// type StreamHandle = WasapiStream; + +// fn create_input_stream( +// &self, +// stream_config: StreamConfig, +// callback: Callback, +// ) -> Result, Self::Error> { +// Ok(WasapiStream::new_input( +// self.name.clone(), +// stream_config, +// callback, +// )) +// } +// } + +impl AudioOutputDevice for WasapiDevice { + type StreamHandle = WasapiStream; + + fn create_output_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error> { + unsafe { + let audio_client: Audio::IAudioClient = + // 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)?; + + let format_attempt = + config_to_waveformatextensible(&stream_config, sample_format).ok_or(err)?; + + let buffer_duration = buffer_size_to_duration( + stream_config.buffer_size_range.0.unwrap_or(0), + stream_config.samplerate.round() as u32, + ); + + audio_client.Initialize( + AUDCLNT_SHAREMODE_SHARED, + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + buffer_duration, + 0, + &format_attempt.Format, + None, + )?; + + // obtaining the size of the samples buffer in number of frames + let max_frames_in_buffer = audio_client.GetBufferSize()? as usize; + + // Creating the event that will be signalled whenever we need to submit some samples. + let event_handle = { + let event_handle = + Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null()))?; + + audio_client.SetEventHandle(event_handle)?; + + event_handle + }; + let render_client = audio_client.GetService::()?; + + let audio_clock = audio_client.GetService::()?; + + let stream_config = StreamConfig { + samplerate: stream_config.samplerate, + channels: stream_config.channels, + buffer_size_range: (Some(max_frames_in_buffer), Some(max_frames_in_buffer)), + }; + + Ok(WasapiStream::new_output( + audio_client, + render_client, + stream_config, + callback, + )) + } + } } -impl WasapiDevice { - fn new(device: Audio::IMMDevice, device_type: DeviceType) -> Self { - WasapiDevice { - device, - device_type, +fn config_to_waveformatextensible( + config: &StreamConfig, + sample_format: SampleFormat, +) -> Option { + let format_tag = match sample_format { + SampleFormat::U8 | SampleFormat::I16 => Audio::WAVE_FORMAT_PCM, + + SampleFormat::I32 | SampleFormat::I64 | SampleFormat::F32 => { + KernelStreaming::WAVE_FORMAT_EXTENSIBLE + } + + _ => return None, + }; + let channels = config.channels as u16; + let sample_rate = config.samplerate as u32; + let sample_bytes = sample_format.sample_size() as u16; + let avg_bytes_per_sec = u32::from(channels) * sample_rate * u32::from(sample_bytes); + let block_align = channels * sample_bytes; + let bits_per_sample = 8 * sample_bytes; + + let cb_size = if format_tag == Audio::WAVE_FORMAT_PCM { + 0 + } else { + let extensible_size = std::mem::size_of::(); + let ex_size = std::mem::size_of::(); + (extensible_size - ex_size) as u16 + }; + + let waveformatex = Audio::WAVEFORMATEX { + wFormatTag: format_tag as u16, + nChannels: channels as u16, + nSamplesPerSec: sample_rate, + nAvgBytesPerSec: avg_bytes_per_sec, + nBlockAlign: block_align, + wBitsPerSample: bits_per_sample, + cbSize: cb_size, + }; + + let channel_mask = KernelStreaming::KSAUDIO_SPEAKER_DIRECTOUT; + + let sub_format = match sample_format { + SampleFormat::U8 | SampleFormat::I16 | SampleFormat::I32 | SampleFormat::I64 => { + KernelStreaming::KSDATAFORMAT_SUBTYPE_PCM + } + + SampleFormat::F32 => Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, + _ => return None, + }; + + let waveformatextensible = WAVEFORMATEXTENSIBLE { + Format: waveformatex, + Samples: WAVEFORMATEXTENSIBLE_0 { + wSamplesPerBlock: bits_per_sample, + }, + dwChannelMask: channel_mask, + SubFormat: sub_format, + }; + + Some(waveformatextensible) +} + +pub enum AudioClientFlow { + Render { render_client: IAudioRenderClient }, + Capture { capture_client: IAudioCaptureClient }, +} + +pub struct WasapiStream { + pub audio_client: Audio::IAudioClient, + pub stream_config: StreamConfig, + pub event_handle: HANDLE, + pub join_handle: JoinHandle>, + _p: PhantomData<*mut Callback>, +} + +impl AudioStreamHandle for WasapiStream { + type Error = WasapiError; + + fn eject(self) -> Result { + unsafe { + CloseHandle(self.event_handle)?; + } + + self.join_handle.join().unwrap() + } +} + +// impl WasapiStream { +// fn new_input(stream_config: StreamConfig, mut callback: Callback) -> Self { + +// } +// } + +impl WasapiStream { + fn new_output( + audio_client: IAudioClient, + render_client: IAudioRenderClient, + audio_clock: IAudioClock, + stream_config: StreamConfig, + event_handle: HANDLE, + mut callback: Callback, + ) -> Self { + let eject_signal = Arc::new(AtomicBool::new(false)); + let join_handle = std::thread::spawn({ + let eject_signal = eject_signal.clone(); + move || { + set_thread_priority(); + + let _try = || loop { + if eject_signal.load(Ordering::Relaxed) { + break Ok(callback); + } + + // Get the number of available frames + let frames_available = unsafe { + let padding = audio_client.GetCurrentPadding()?; + stream_config.buffer_size_range.0.unwrap() as u32 - padding + }; + + unsafe { + let data = render_client.GetBuffer(frames_available)?; + + debug_assert!(!data.is_null()); + + let len = frames_available as usize * stream.bytes_per_frame as usize + / stream.sample_format.sample_size(); + let data: &mut [u8] = std::slice::from_raw_parts_mut(buffer, len); + let timestamp = + output_timestamp(stream, frames_available, stream_config.samplerate)?; + let context = AudioCallbackContext { + stream_config, + timestamp, + }; + let mut buffer = vec![0f32; (frames_available * stream_config.channels) as usize]; + let output = AudioOutput { + buffer: AudioMut::from_interleaved_mut( + &mut buffer, + stream_config.channels as usize, + ) + .unwrap(), + timestamp, + }; + callback.on_output_data(context, output); + + data.write_all() + + render_client.ReleaseBuffer(frames_available, 0)?; + } + }; + + _try() + } + }); + + WasapiStream { + audio_client, + stream_config, + event_handle, + join_handle, + _p: PhantomData::default(), } } } +fn set_thread_priority() { + unsafe { + let thread_id = Threading::GetCurrentThreadId(); + + let _ = Threading::SetThreadPriority( + HANDLE(thread_id as isize as _), + Threading::THREAD_PRIORITY_TIME_CRITICAL, + ); + } +} + fn get_device_name(device: &windows::Win32::Media::Audio::IMMDevice) -> Option { unsafe { // Open the device's property store. @@ -160,14 +421,15 @@ fn get_device_name(device: &windows::Win32::Media::Audio::IMMDevice) -> Option Option Option = OnceLock::new(); fn audio_device_enumerator() -> &'static AudioDeviceEnumerator { @@ -221,19 +484,19 @@ fn audio_device_enumerator() -> &'static AudioDeviceEnumerator { struct AudioDeviceEnumerator(Audio::IMMDeviceEnumerator); impl AudioDeviceEnumerator { - // Returns the default output device. - fn get_default_device(&self, device_type: DeviceType) -> Result, WasapiError> { + 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), + _ => return Ok(None), }; unsafe { - let device = self - .0 - .GetDefaultAudioEndpoint(data_flow, Audio::eConsole)?; + let device = self.0.GetDefaultAudioEndpoint(data_flow, Audio::eConsole)?; Ok(Some(WasapiDevice::new(device, DeviceType::Output))) } @@ -241,7 +504,6 @@ impl AudioDeviceEnumerator { // 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 @@ -308,4 +570,45 @@ impl Iterator for WasapiDeviceList { let rest = (self.total_count - self.next_item) as usize; (rest, Some(rest)) } +} + +fn buffer_size_to_duration(buffer_size: usize, sample_rate: u32) -> i64 { + (buffer_size as i64 / sample_rate as i64) * (1_000_000_000 / 100) +} + +fn buffer_duration_to_frames(buffer_duration: i64, sample_rate: u32) -> i64 { + (buffer_duration * sample_rate as i64) / (100 / 1_000_000_000) +} + +/// Convert the given duration in frames at the given sample rate to a `std::time::Duration`. +fn frames_to_duration(frames: u32, samplerate: f64) -> std::time::Duration { + let secsf = frames as f64 / samplerate; + let secs = secsf as u64; + let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; + std::time::Duration::new(secs, nanos) +} + +fn stream_instant(audio_clock: &IAudioClock) -> Result { + let mut position: u64 = 0; + let mut qpc_position: u64 = 0; + unsafe { + audio_clock + .GetPosition(&mut position, Some(&mut qpc_position))?; + }; + // The `qpc_position` is in 100 nanosecond units. Convert it to nanoseconds. + let qpc_nanos = qpc_position as u64 * 100; + let instant = Duration::from_nanos(qpc_nanos); + Ok(instant) +} + +fn output_timestamp( + audio_clock: &IAudioClock, + frames_available: u32, + samplerate: f64, +) -> Result { + let callback = stream_instant(audio_clock)?; + let buffer_duration = frames_to_duration(frames_available, samplerate); + let playback = callback + .add(buffer_duration); + Ok(Timestamp::from_duration(samplerate, playback)) } \ No newline at end of file diff --git a/src/channel_map.rs b/src/channel_map.rs index 0beeadf..8ad588d 100644 --- a/src/channel_map.rs +++ b/src/channel_map.rs @@ -7,10 +7,10 @@ pub trait Bitset: Sized { fn set_index(&mut self, index: usize, value: bool); - fn indices(&self) -> impl IntoIterator { + fn indices(&self) -> impl IntoIterator { (0..self.capacity()).filter_map(|i| self.get_index(i).then_some(i)) } - + fn count(&self) -> usize { self.indices().into_iter().count() } @@ -19,8 +19,8 @@ pub trait Bitset: Sized { self.set_index(index, value); self } - - fn with_indices(mut self, indices: impl IntoIterator) -> Self { + + fn with_indices(mut self, indices: impl IntoIterator) -> Self { for ix in indices { self.set_index(ix, true); } @@ -65,13 +65,13 @@ fn get_inner_bitset_at(arr: &[T], mut index: usize) -> Option<(usize, let mut acc = 0; move |(i, b)| { return match index.checked_sub(b.capacity()) { - None => Some((i, index)), + None => Some((i, index)), Some(v) => { index = v; acc += b.capacity(); None } - } + }; } }) } @@ -151,4 +151,4 @@ mod test { let result = HashSet::<_, RandomState>::from_iter(bitrate.indices()); assert_eq!(HashSet::from_iter([0, 2, 5, 12, 14, 16]), result); } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 65e9833..7d41769 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,8 +9,8 @@ use crate::timestamp::Timestamp; pub mod audio_buffer; pub mod backends; pub mod channel_map; -pub mod timestamp; pub mod prelude; +pub mod timestamp; /// Audio drivers provide access to the inputs and outputs of physical devices. /// Several drivers might provide the same accesses, some sharing it with other applications, @@ -108,9 +108,7 @@ impl SendEverywhereButOnWeb for T {} pub trait AudioInputDevice: AudioDevice { type StreamHandle: AudioStreamHandle; - fn create_input_stream< - Callback: SendEverywhereButOnWeb + AudioInputCallback, - >( + fn create_input_stream( &self, stream_config: StreamConfig, callback: Callback, @@ -120,9 +118,7 @@ pub trait AudioInputDevice: AudioDevice { pub trait AudioOutputDevice: AudioDevice { type StreamHandle: AudioStreamHandle; - fn create_output_stream< - Callback: SendEverywhereButOnWeb + AudioOutputCallback, - >( + fn create_output_stream( &self, stream_config: StreamConfig, callback: Callback, diff --git a/src/prelude.rs b/src/prelude.rs index cf86cd7..bea0c8c 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,2 +1,2 @@ +pub use crate::backends::*; pub use crate::*; -pub use crate::backends::*; \ No newline at end of file From 78e6059ce6b83e7297730e2562ff408ce7426b61 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 10 Nov 2024 19:21:57 +0100 Subject: [PATCH 3/9] feat(wasapi): working output audio stream --- build.rs | 1 + src/audio_buffer.rs | 5 +- src/backends/mod.rs | 16 +- src/backends/wasapi.rs | 614 --------------------------------- src/backends/wasapi/device.rs | 140 ++++++++ src/backends/wasapi/driver.rs | 113 ++++++ src/backends/wasapi/error.rs | 16 + src/backends/wasapi/mod.rs | 10 + src/backends/wasapi/prelude.rs | 6 + src/backends/wasapi/stream.rs | 391 +++++++++++++++++++++ src/backends/wasapi/util.rs | 130 +++++++ src/lib.rs | 3 + src/prelude.rs | 2 + 13 files changed, 828 insertions(+), 619 deletions(-) delete mode 100644 src/backends/wasapi.rs create mode 100644 src/backends/wasapi/device.rs create mode 100644 src/backends/wasapi/driver.rs create mode 100644 src/backends/wasapi/error.rs create mode 100644 src/backends/wasapi/mod.rs create mode 100644 src/backends/wasapi/prelude.rs create mode 100644 src/backends/wasapi/stream.rs create mode 100644 src/backends/wasapi/util.rs diff --git a/build.rs b/build.rs index 4cb0efb..07a6f07 100644 --- a/build.rs +++ b/build.rs @@ -8,5 +8,6 @@ fn main() { target_os = "netbsd") }, os_coreaudio: { any (target_os = "macos", target_os = "ios") }, os_wasapi: { target_os = "windows" }, + unsupported: { not(any(wasm, os_alsa, os_coreaudio, os_wasapi))} } } diff --git a/src/audio_buffer.rs b/src/audio_buffer.rs index c533504..240f95b 100644 --- a/src/audio_buffer.rs +++ b/src/audio_buffer.rs @@ -173,7 +173,7 @@ impl AudioBufferBase { for (inp, out) in self.as_interleaved().iter().zip(output.iter_mut()) { *out = *inp; } - return true; + true } } @@ -461,7 +461,8 @@ impl<'a, S: DataMut> AudioBufferBase { /// Mix a buffer into this buffer at the specified amplitude. The audio will be mixed into /// this buffer as a result, and the other buffer's amplitude will be changed similarly to - /// applying [`Self::change_amplitude`] first.pub fn mix(&mut self, other: AudioRef, other_amplitude: ::Float) + /// applying [`Self::change_amplitude`] first. + pub fn mix(&mut self, other: AudioRef, other_amplitude: ::Float) where S::Elem: AddAssign, { diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 001cf49..7e2334e 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -4,10 +4,15 @@ //! //! Each backend is provided in its own submodule. Types should be public so that the user isn't //! limited to going through the main API if they want to choose a specific backend. + +use wasapi::driver; use crate::{ AudioDriver, AudioInputDevice, AudioOutputDevice, DeviceType, }; +#[cfg(unsupported)] +compile_error!("Unsupported platform (supports ALSA, CoreAudio, and WASAPI)"); + #[cfg(os_alsa)] pub mod alsa; @@ -30,6 +35,9 @@ pub mod wasapi; /// | **Platform** | **Driver** | /// |:------------:|:----------:| /// | Linux | ALSA | +/// | macOS | CoreAudio | +/// | Windows | WASAPI | +#[allow(clippy::needless_return)] pub fn default_driver() -> impl AudioDriver { #[cfg(os_alsa)] return alsa::AlsaDriver; @@ -58,13 +66,13 @@ where /// "Default" here means both in terms of platform support but also can include runtime selection. /// Therefore, it is better to use this method directly rather than first getting the default /// driver from [`default_driver`]. +#[cfg(any(os_alsa, os_coreaudio))] +#[allow(clippy::needless_return)] pub fn default_input_device() -> impl AudioInputDevice { #[cfg(os_alsa)] return default_input_device_from(&alsa::AlsaDriver); #[cfg(os_coreaudio)] return default_input_device_from(&coreaudio::CoreAudioDriver); - #[cfg(os_wasapi)] - return default_input_device_from(&wasapi::WasapiDriver); } /// Returns the default input device for the given audio driver. @@ -86,11 +94,13 @@ where /// "Default" here means both in terms of platform support but also can include runtime selection. /// Therefore, it is better to use this method directly rather than first getting the default /// driver from [`default_driver`]. +#[cfg(any(os_alsa, os_coreaudio, os_wasapi))] +#[allow(clippy::needless_return)] pub fn default_output_device() -> impl AudioOutputDevice { #[cfg(os_alsa)] return default_output_device_from(&alsa::AlsaDriver); #[cfg(os_coreaudio)] return default_output_device_from(&coreaudio::CoreAudioDriver); #[cfg(os_wasapi)] - return default_output_device_from(&wasapi::WasapiDriver); + return default_output_device_from(&driver::WasapiDriver); } diff --git a/src/backends/wasapi.rs b/src/backends/wasapi.rs deleted file mode 100644 index 63d4b63..0000000 --- a/src/backends/wasapi.rs +++ /dev/null @@ -1,614 +0,0 @@ -use std::{ - borrow::Cow, ffi::OsString, marker::PhantomData, ops::Add, os::windows::ffi::OsStringExt, ptr, sync::{atomic::{AtomicBool, Ordering}, Arc, Mutex, OnceLock}, thread::JoinHandle, time::Duration -}; - -use crate::{ - prelude::{AudioMut, Timestamp}, AudioCallbackContext, AudioDevice, AudioDriver, AudioInputCallback, - AudioInputDevice, AudioOutput, AudioOutputCallback, AudioOutputDevice, AudioStreamHandle, - Channel, DeviceType, StreamConfig, -}; -use thiserror::Error; -use windows::Win32::{ - Devices::Properties, - Foundation::{CloseHandle, HANDLE}, - Media::{ - Audio::{ - self, IAudioCaptureClient, IAudioClient, IAudioClock, IAudioRenderClient, IMMDevice, AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, WAVEFORMATEXTENSIBLE, WAVEFORMATEXTENSIBLE_0 - }, - KernelStreaming, Multimedia, - }, - System::{ - Com::{self, StructuredStorage, STGM_READ}, - Threading, - 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, Clone)] -pub struct WasapiDevice { - device: windows::Win32::Media::Audio::IMMDevice, - device_type: DeviceType, - audio_client: Arc>>, -} - -impl WasapiDevice { - fn new(device: IMMDevice, device_type: DeviceType) -> Self { - WasapiDevice { - device, - device_type, - audio_client: Arc::new(Mutex::new(None)), - } - } -} - -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 AudioInputDevice for WasapiDevice { -// type StreamHandle = WasapiStream; - -// fn create_input_stream( -// &self, -// stream_config: StreamConfig, -// callback: Callback, -// ) -> Result, Self::Error> { -// Ok(WasapiStream::new_input( -// self.name.clone(), -// stream_config, -// callback, -// )) -// } -// } - -impl AudioOutputDevice for WasapiDevice { - type StreamHandle = WasapiStream; - - fn create_output_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error> { - unsafe { - let audio_client: Audio::IAudioClient = - // 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)?; - - let format_attempt = - config_to_waveformatextensible(&stream_config, sample_format).ok_or(err)?; - - let buffer_duration = buffer_size_to_duration( - stream_config.buffer_size_range.0.unwrap_or(0), - stream_config.samplerate.round() as u32, - ); - - audio_client.Initialize( - AUDCLNT_SHAREMODE_SHARED, - AUDCLNT_STREAMFLAGS_EVENTCALLBACK, - buffer_duration, - 0, - &format_attempt.Format, - None, - )?; - - // obtaining the size of the samples buffer in number of frames - let max_frames_in_buffer = audio_client.GetBufferSize()? as usize; - - // Creating the event that will be signalled whenever we need to submit some samples. - let event_handle = { - let event_handle = - Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null()))?; - - audio_client.SetEventHandle(event_handle)?; - - event_handle - }; - - let render_client = audio_client.GetService::()?; - - let audio_clock = audio_client.GetService::()?; - - let stream_config = StreamConfig { - samplerate: stream_config.samplerate, - channels: stream_config.channels, - buffer_size_range: (Some(max_frames_in_buffer), Some(max_frames_in_buffer)), - }; - - Ok(WasapiStream::new_output( - audio_client, - render_client, - stream_config, - callback, - )) - } - } -} - -fn config_to_waveformatextensible( - config: &StreamConfig, - sample_format: SampleFormat, -) -> Option { - let format_tag = match sample_format { - SampleFormat::U8 | SampleFormat::I16 => Audio::WAVE_FORMAT_PCM, - - SampleFormat::I32 | SampleFormat::I64 | SampleFormat::F32 => { - KernelStreaming::WAVE_FORMAT_EXTENSIBLE - } - - _ => return None, - }; - let channels = config.channels as u16; - let sample_rate = config.samplerate as u32; - let sample_bytes = sample_format.sample_size() as u16; - let avg_bytes_per_sec = u32::from(channels) * sample_rate * u32::from(sample_bytes); - let block_align = channels * sample_bytes; - let bits_per_sample = 8 * sample_bytes; - - let cb_size = if format_tag == Audio::WAVE_FORMAT_PCM { - 0 - } else { - let extensible_size = std::mem::size_of::(); - let ex_size = std::mem::size_of::(); - (extensible_size - ex_size) as u16 - }; - - let waveformatex = Audio::WAVEFORMATEX { - wFormatTag: format_tag as u16, - nChannels: channels as u16, - nSamplesPerSec: sample_rate, - nAvgBytesPerSec: avg_bytes_per_sec, - nBlockAlign: block_align, - wBitsPerSample: bits_per_sample, - cbSize: cb_size, - }; - - let channel_mask = KernelStreaming::KSAUDIO_SPEAKER_DIRECTOUT; - - let sub_format = match sample_format { - SampleFormat::U8 | SampleFormat::I16 | SampleFormat::I32 | SampleFormat::I64 => { - KernelStreaming::KSDATAFORMAT_SUBTYPE_PCM - } - - SampleFormat::F32 => Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, - _ => return None, - }; - - let waveformatextensible = WAVEFORMATEXTENSIBLE { - Format: waveformatex, - Samples: WAVEFORMATEXTENSIBLE_0 { - wSamplesPerBlock: bits_per_sample, - }, - dwChannelMask: channel_mask, - SubFormat: sub_format, - }; - - Some(waveformatextensible) -} - -pub enum AudioClientFlow { - Render { render_client: IAudioRenderClient }, - Capture { capture_client: IAudioCaptureClient }, -} - -pub struct WasapiStream { - pub audio_client: Audio::IAudioClient, - pub stream_config: StreamConfig, - pub event_handle: HANDLE, - pub join_handle: JoinHandle>, - _p: PhantomData<*mut Callback>, -} - -impl AudioStreamHandle for WasapiStream { - type Error = WasapiError; - - fn eject(self) -> Result { - unsafe { - CloseHandle(self.event_handle)?; - } - - self.join_handle.join().unwrap() - } -} - -// impl WasapiStream { -// fn new_input(stream_config: StreamConfig, mut callback: Callback) -> Self { - -// } -// } - -impl WasapiStream { - fn new_output( - audio_client: IAudioClient, - render_client: IAudioRenderClient, - audio_clock: IAudioClock, - stream_config: StreamConfig, - event_handle: HANDLE, - mut callback: Callback, - ) -> Self { - let eject_signal = Arc::new(AtomicBool::new(false)); - let join_handle = std::thread::spawn({ - let eject_signal = eject_signal.clone(); - move || { - set_thread_priority(); - - let _try = || loop { - if eject_signal.load(Ordering::Relaxed) { - break Ok(callback); - } - - // Get the number of available frames - let frames_available = unsafe { - let padding = audio_client.GetCurrentPadding()?; - stream_config.buffer_size_range.0.unwrap() as u32 - padding - }; - - unsafe { - let data = render_client.GetBuffer(frames_available)?; - - debug_assert!(!data.is_null()); - - let len = frames_available as usize * stream.bytes_per_frame as usize - / stream.sample_format.sample_size(); - let data: &mut [u8] = std::slice::from_raw_parts_mut(buffer, len); - let timestamp = - output_timestamp(stream, frames_available, stream_config.samplerate)?; - let context = AudioCallbackContext { - stream_config, - timestamp, - }; - let mut buffer = vec![0f32; (frames_available * stream_config.channels) as usize]; - let output = AudioOutput { - buffer: AudioMut::from_interleaved_mut( - &mut buffer, - stream_config.channels as usize, - ) - .unwrap(), - timestamp, - }; - callback.on_output_data(context, output); - - data.write_all() - - render_client.ReleaseBuffer(frames_available, 0)?; - } - }; - - _try() - } - }); - - WasapiStream { - audio_client, - stream_config, - event_handle, - join_handle, - _p: PhantomData::default(), - } - } -} - -fn set_thread_priority() { - unsafe { - let thread_id = Threading::GetCurrentThreadId(); - - let _ = Threading::SetThreadPriority( - HANDLE(thread_id as isize as _), - Threading::THREAD_PRIORITY_TIME_CRITICAL, - ); - } -} - -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)) - } -} - -fn buffer_size_to_duration(buffer_size: usize, sample_rate: u32) -> i64 { - (buffer_size as i64 / sample_rate as i64) * (1_000_000_000 / 100) -} - -fn buffer_duration_to_frames(buffer_duration: i64, sample_rate: u32) -> i64 { - (buffer_duration * sample_rate as i64) / (100 / 1_000_000_000) -} - -/// Convert the given duration in frames at the given sample rate to a `std::time::Duration`. -fn frames_to_duration(frames: u32, samplerate: f64) -> std::time::Duration { - let secsf = frames as f64 / samplerate; - let secs = secsf as u64; - let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; - std::time::Duration::new(secs, nanos) -} - -fn stream_instant(audio_clock: &IAudioClock) -> Result { - let mut position: u64 = 0; - let mut qpc_position: u64 = 0; - unsafe { - audio_clock - .GetPosition(&mut position, Some(&mut qpc_position))?; - }; - // The `qpc_position` is in 100 nanosecond units. Convert it to nanoseconds. - let qpc_nanos = qpc_position as u64 * 100; - let instant = Duration::from_nanos(qpc_nanos); - Ok(instant) -} - -fn output_timestamp( - audio_clock: &IAudioClock, - frames_available: u32, - samplerate: f64, -) -> Result { - let callback = stream_instant(audio_clock)?; - let buffer_duration = frames_to_duration(frames_available, samplerate); - let playback = callback - .add(buffer_duration); - Ok(Timestamp::from_duration(samplerate, playback)) -} \ No newline at end of file diff --git a/src/backends/wasapi/device.rs b/src/backends/wasapi/device.rs new file mode 100644 index 0000000..7f133ae --- /dev/null +++ b/src/backends/wasapi/device.rs @@ -0,0 +1,140 @@ +use super::{error, stream}; +use crate::backends::wasapi::stream::WasapiStream; +use crate::channel_map::Bitset; +use crate::prelude::wasapi::util::WasapiMMDevice; +use crate::{ + AudioDevice, AudioOutputCallback, AudioOutputDevice, Channel, DeviceType, StreamConfig, +}; +use std::borrow::Cow; +use windows::Win32::Media::Audio; + +/// Type of devices available from the WASAPI driver. +#[derive(Debug, Clone)] +pub struct WasapiDevice { + device: WasapiMMDevice, + device_type: DeviceType, +} + +impl WasapiDevice { + pub(crate) fn new(device: Audio::IMMDevice, device_type: DeviceType) -> Self { + WasapiDevice { + device: WasapiMMDevice::new(device), + device_type, + } + } +} + +impl AudioDevice for WasapiDevice { + type Error = error::WasapiError; + + fn name(&self) -> Cow { + match self.device.name() { + Some(std) => Cow::Owned(std), + None => { + eprintln!("Cannot get audio device name"); + Cow::Borrowed("") + } + } + } + + fn device_type(&self) -> DeviceType { + self.device_type + } + + fn channel_map(&self) -> impl IntoIterator { + [] + } + + fn is_config_supported(&self, config: &StreamConfig) -> bool { + match self.device_type { + DeviceType::Output => { + stream::is_output_config_supported(self.device.clone(), config) + } + _ => false, + } + } + + fn enumerate_configurations(&self) -> Option> { + None::<[StreamConfig; 0]> + } +} + +// impl AudioInputDevice for WasapiDevice { +// type StreamHandle = WasapiStream; + +// fn create_input_stream( +// &self, +// stream_config: StreamConfig, +// callback: Callback, +// ) -> Result, Self::Error> { +// Ok(WasapiStream::new_input( +// self.name.clone(), +// stream_config, +// callback, +// )) +// } +// } + +impl AudioOutputDevice for WasapiDevice { + type StreamHandle = WasapiStream; + + fn default_output_config(&self) -> Result { + let audio_client = self.device.activate::()?; + let format = unsafe { + audio_client.GetMixFormat()?.read_unaligned() }; + let frame_size = unsafe { audio_client.GetBufferSize() }.map(|i| i as usize).ok(); + Ok(StreamConfig { + channels: 0u32.with_indices(0..format.nChannels as _), + exclusive: false, + samplerate: format.nSamplesPerSec as _, + buffer_size_range: (frame_size, frame_size), + }) + } + + fn create_output_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error> { + Ok(WasapiStream::new_output( + self.device.clone(), + stream_config, + callback, + )) + } +} + +/// An iterable collection WASAPI devices. +pub struct WasapiDeviceList { + pub(crate) collection: Audio::IMMDeviceCollection, + pub(crate) total_count: u32, + pub(crate) next_item: u32, + pub(crate) 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)) + } +} + +impl ExactSizeIterator for WasapiDeviceList {} diff --git a/src/backends/wasapi/driver.rs b/src/backends/wasapi/driver.rs new file mode 100644 index 0000000..e49eba3 --- /dev/null +++ b/src/backends/wasapi/driver.rs @@ -0,0 +1,113 @@ +use std::borrow::Cow; +use windows::Win32::System::Com; +use windows::Win32::Media::Audio; +use std::sync::OnceLock; +use crate::backends::wasapi::device::{WasapiDevice, WasapiDeviceList}; + +use super::{error, util}; + +use crate::{AudioDriver, DeviceType}; + +/// The WASAPI driver. +#[derive(Debug, Clone, Default)] +pub struct WasapiDriver; + +impl AudioDriver for WasapiDriver { + type Error = error::WasapiError; + type Device = WasapiDevice; + + const DISPLAY_NAME: &'static str = "WASAPI"; + + fn version(&self) -> Result, Self::Error> { + Ok(Cow::Borrowed("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() + } +} + +pub 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) + } + }) +} + +static ENUMERATOR: OnceLock = OnceLock::new(); + +/// Send/Sync wrapper around `IMMDeviceEnumerator`. +pub struct AudioDeviceEnumerator(Audio::IMMDeviceEnumerator); + +impl AudioDeviceEnumerator { + // Returns the default output device. + fn get_default_device( + &self, + device_type: DeviceType, + ) -> Result, error::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, error::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 {} \ No newline at end of file diff --git a/src/backends/wasapi/error.rs b/src/backends/wasapi/error.rs new file mode 100644 index 0000000..7ce2d8d --- /dev/null +++ b/src/backends/wasapi/error.rs @@ -0,0 +1,16 @@ +use thiserror::Error; + +/// Type of errors from the WASAPI backend. +#[derive(Debug, Error)] +#[error("WASAPI error: ")] +pub enum WasapiError { + /// Error originating from WASAPI. + #[error("{} (code {})", .0.message(), .0.code())] + BackendError(#[from] windows::core::Error), + /// Requested WASAPI device configuration is not available + #[error("Configuration not available")] + ConfigurationNotAvailable, + /// Windows Foundation error + #[error("Win32 error: {0}")] + FoundationError(String), +} \ No newline at end of file diff --git a/src/backends/wasapi/mod.rs b/src/backends/wasapi/mod.rs new file mode 100644 index 0000000..3673fd1 --- /dev/null +++ b/src/backends/wasapi/mod.rs @@ -0,0 +1,10 @@ +mod util; + +mod error; + +pub(crate) mod driver; +mod device; +mod stream; +pub mod prelude; + +pub use prelude::*; \ No newline at end of file diff --git a/src/backends/wasapi/prelude.rs b/src/backends/wasapi/prelude.rs new file mode 100644 index 0000000..3fa8663 --- /dev/null +++ b/src/backends/wasapi/prelude.rs @@ -0,0 +1,6 @@ +pub use super::{ + device::WasapiDevice, + driver::WasapiDriver, + error::WasapiError, + stream::WasapiStream, +}; diff --git a/src/backends/wasapi/stream.rs b/src/backends/wasapi/stream.rs new file mode 100644 index 0000000..e07b187 --- /dev/null +++ b/src/backends/wasapi/stream.rs @@ -0,0 +1,391 @@ +use super::error; +use crate::audio_buffer::AudioMut; +use crate::backends::wasapi::util::WasapiMMDevice; +use crate::channel_map::Bitset; +use crate::prelude::Timestamp; +use crate::{ + AudioCallbackContext, AudioOutput, AudioOutputCallback, AudioStreamHandle, StreamConfig, +}; +use std::marker::PhantomData; +use std::ptr::NonNull; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread::JoinHandle; +use std::time::Duration; +use std::{ops, ptr, slice}; +use windows::core::imp::CoTaskMemFree; +use windows::Win32::Foundation; +use windows::Win32::Foundation::{CloseHandle, HANDLE}; +use windows::Win32::Media::{Audio, KernelStreaming, Multimedia}; +use windows::Win32::System::Threading; + +type EjectSignal = Arc; + +struct AudioBuffer<'a, T> { + render_client: &'a Audio::IAudioRenderClient, + data: NonNull, + frame_size: usize, + channels: usize, + __type: PhantomData, +} + +impl<'a, T> ops::Deref for AudioBuffer<'a, T> { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + unsafe { slice::from_raw_parts(self.data.cast().as_ptr(), self.channels * self.frame_size) } + } +} + +impl<'a, T> ops::DerefMut for AudioBuffer<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { + slice::from_raw_parts_mut(self.data.cast().as_ptr(), self.channels * self.frame_size) + } + } +} +impl Drop for AudioBuffer<'_, T> { + fn drop(&mut self) { + unsafe { + self.render_client + .ReleaseBuffer(self.frame_size as _, 0) + .unwrap() + }; + } +} + +impl<'a, T> AudioBuffer<'a, T> { + fn from_render_client( + render_client: &'a Audio::IAudioRenderClient, + channels: usize, + frame_size: usize, + ) -> Result { + let data = NonNull::new(unsafe { render_client.GetBuffer(frame_size as _) }?) + .expect("Audio buffer data is null"); + Ok(Self { + render_client, + data, + frame_size, + channels, + __type: PhantomData, + }) + } +} + +struct RenderThread { + audio_client: Audio::IAudioClient, + render_client: Audio::IAudioRenderClient, + audio_clock: Audio::IAudioClock, + stream_config: StreamConfig, + eject_signal: EjectSignal, + frame_size: usize, + callback: Callback, + event_handle: HANDLE, + clock_start: Duration, +} + +impl RenderThread { + fn new( + device: WasapiMMDevice, + eject_signal: EjectSignal, + mut stream_config: StreamConfig, + callback: Callback, + ) -> Result { + unsafe { + let audio_client: Audio::IAudioClient = device.activate()?; + let sharemode = if stream_config.exclusive { + Audio::AUDCLNT_SHAREMODE_EXCLUSIVE + } else { + Audio::AUDCLNT_SHAREMODE_SHARED + }; + let format = { + let mut format = config_to_waveformatextensible(&stream_config); + let mut actual_format = ptr::null_mut(); + audio_client + .IsFormatSupported( + sharemode, + &format.Format, + (!stream_config.exclusive).then_some(&mut actual_format), + ) + .ok()?; + if !stream_config.exclusive { + assert!(!actual_format.is_null()); + format.Format = actual_format.read_unaligned(); + CoTaskMemFree(actual_format.cast()); + let sample_rate = format.Format.nSamplesPerSec; + stream_config.channels = 0u32.with_indices(0..format.Format.nChannels as _); + stream_config.samplerate = sample_rate as _; + } + format + }; + let frame_size = stream_config + .buffer_size_range + .0 + .or(stream_config.buffer_size_range.1); + let buffer_duration = frame_size + .map(|frame_size| { + buffer_size_to_duration(frame_size, stream_config.samplerate as _) + }) + .unwrap_or(0); + audio_client.Initialize( + sharemode, + Audio::AUDCLNT_STREAMFLAGS_EVENTCALLBACK + | Audio::AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, + buffer_duration, + 0, + &format.Format, + None, + )?; + let buffer_size = audio_client.GetBufferSize()? as usize; + let event_handle = { + let event_handle = + Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null()))?; + audio_client.SetEventHandle(event_handle)?; + event_handle + }; + let render_client = audio_client.GetService::()?; + let audio_clock = audio_client.GetService::()?; + let frame_size = buffer_size; + Ok(Self { + audio_client, + render_client, + audio_clock, + event_handle, + frame_size, + eject_signal, + stream_config: StreamConfig { + buffer_size_range: (Some(frame_size), Some(frame_size)), + ..stream_config + }, + clock_start: Duration::ZERO, + callback, + }) + } + } + + fn finalize(self) -> Result { + if !self.event_handle.is_invalid() { + unsafe { CloseHandle(self.event_handle) }?; + } + Ok(self.callback) + } +} + +impl RenderThread { + fn run(mut self) -> Result { + set_thread_priority(); + unsafe { + self.audio_client.Start()?; + } + self.clock_start = stream_instant(&self.audio_clock)?; + loop { + if self.eject_signal.load(Ordering::Relaxed) { + break self.finalize(); + } + self.await_frame()?; + self.process()?; + } + .inspect_err(|err| eprintln!("Render thread process error: {err}")) + } + + fn process(&mut self) -> Result<(), error::WasapiError> { + let frames_available = unsafe { + let padding = self.audio_client.GetCurrentPadding()? as usize; + self.frame_size - padding + }; + if frames_available == 0 { + eprintln!("WASAPI WTF: 0 output callback requested"); + return Ok(()); + } + let frames_requested = if let Some(max_frames) = self.stream_config.buffer_size_range.1 { + frames_available.min(max_frames) + } else { + frames_available + }; + let mut buffer = AudioBuffer::::from_render_client( + &self.render_client, + self.stream_config.channels.count(), + frames_requested, + )?; + let timestamp = self.output_timestamp()?; + let context = AudioCallbackContext { + stream_config: self.stream_config, + timestamp, + }; + let buffer = + AudioMut::from_interleaved_mut(&mut buffer, self.stream_config.channels.count()) + .unwrap(); + let output = AudioOutput { timestamp, buffer }; + self.callback.on_output_data(context, output); + Ok(()) + } + + fn await_frame(&mut self) -> Result<(), error::WasapiError> { + let _ = unsafe { + let result = Threading::WaitForSingleObject(self.event_handle, Threading::INFINITE); + if result == Foundation::WAIT_FAILED { + let err = Foundation::GetLastError(); + let description = format!("Waiting for event handle failed: {:?}", err); + return Err(error::WasapiError::FoundationError(description)); + } + result + }; + Ok(()) + } + + fn output_timestamp(&self) -> Result { + let clock = stream_instant(&self.audio_clock)?; + let diff = clock - self.clock_start; + Ok(Timestamp::from_duration( + self.stream_config.samplerate, + diff, + )) + } +} + +/// Type representing a WASAPI audio stream. +pub struct WasapiStream { + join_handle: JoinHandle>, + eject_signal: EjectSignal, +} + +impl AudioStreamHandle for WasapiStream { + type Error = error::WasapiError; + + fn eject(self) -> Result { + self.eject_signal.store(true, Ordering::Relaxed); + self.join_handle + .join() + .expect("Audio output thread panicked") + } +} + +impl WasapiStream { + pub(crate) fn new_output( + device: WasapiMMDevice, + stream_config: StreamConfig, + callback: Callback, + ) -> Self { + let eject_signal = EjectSignal::default(); + let join_handle = std::thread::Builder::new() + .name("interflow_wasapi_output_stream".to_string()) + .spawn({ + let eject_signal = eject_signal.clone(); + move || { + let inner = RenderThread::new(device, eject_signal, stream_config, callback) + .inspect_err(|err| eprintln!("Failed to create render thread: {err}"))?; + inner.run() + } + }) + .expect("Cannot spawn audio output thread"); + Self { + join_handle, + eject_signal, + } + } +} + +fn set_thread_priority() { + unsafe { + let thread_id = Threading::GetCurrentThreadId(); + + let _ = Threading::SetThreadPriority( + HANDLE(thread_id as isize as _), + Threading::THREAD_PRIORITY_TIME_CRITICAL, + ); + } +} + +pub fn buffer_size_to_duration(buffer_size: usize, sample_rate: u32) -> i64 { + (buffer_size as i64 / sample_rate as i64) * (1_000_000_000 / 100) +} + +fn stream_instant(audio_clock: &Audio::IAudioClock) -> Result { + let mut position: u64 = 0; + let mut qpc_position: u64 = 0; + unsafe { + audio_clock.GetPosition(&mut position, Some(&mut qpc_position))?; + }; + // The `qpc_position` is in 100 nanosecond units. Convert it to nanoseconds. + let qpc_nanos = qpc_position * 100; + let instant = Duration::from_nanos(qpc_nanos); + Ok(instant) +} + +pub(crate) fn config_to_waveformatextensible(config: &StreamConfig) -> Audio::WAVEFORMATEXTENSIBLE { + let format_tag = KernelStreaming::WAVE_FORMAT_EXTENSIBLE; + let channels = config.channels as u16; + let sample_rate = config.samplerate as u32; + let sample_bytes = size_of::() as u16; + let avg_bytes_per_sec = u32::from(channels) * sample_rate * u32::from(sample_bytes); + let block_align = channels * sample_bytes; + let bits_per_sample = 8 * sample_bytes; + + let cb_size = { + let extensible_size = size_of::(); + let ex_size = size_of::(); + (extensible_size - ex_size) as u16 + }; + + let waveformatex = Audio::WAVEFORMATEX { + wFormatTag: format_tag as u16, + nChannels: channels, + nSamplesPerSec: sample_rate, + nAvgBytesPerSec: avg_bytes_per_sec, + nBlockAlign: block_align, + wBitsPerSample: bits_per_sample, + cbSize: cb_size, + }; + + let channel_mask = KernelStreaming::KSAUDIO_SPEAKER_DIRECTOUT; + + let sub_format = Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; + + let waveformatextensible = Audio::WAVEFORMATEXTENSIBLE { + Format: waveformatex, + Samples: Audio::WAVEFORMATEXTENSIBLE_0 { + wSamplesPerBlock: bits_per_sample, + }, + dwChannelMask: channel_mask, + SubFormat: sub_format, + }; + + waveformatextensible +} + +pub(crate) fn is_output_config_supported(device: WasapiMMDevice, stream_config: &StreamConfig) -> bool { + let mut try_ = || unsafe { + let audio_client: Audio::IAudioClient = device.activate()?; + let sharemode = if stream_config.exclusive { + Audio::AUDCLNT_SHAREMODE_EXCLUSIVE + } else { + Audio::AUDCLNT_SHAREMODE_SHARED + }; + let mut format = config_to_waveformatextensible(&stream_config); + let mut actual_format = ptr::null_mut(); + audio_client + .IsFormatSupported( + sharemode, + &format.Format, + (!stream_config.exclusive).then_some(&mut actual_format), + ) + .ok()?; + if !stream_config.exclusive { + assert!(!actual_format.is_null()); + format.Format = actual_format.read_unaligned(); + CoTaskMemFree(actual_format.cast()); + let sample_rate = format.Format.nSamplesPerSec; + let new_channels = 0u32.with_indices(0..format.Format.nChannels as _); + let new_samplerate = sample_rate as f64; + if stream_config.samplerate != new_samplerate + || stream_config.channels.count() != new_channels.count() + { + return Ok(false); + } + } + Ok::<_, error::WasapiError>(true) + }; + try_() + .inspect_err(|err| eprintln!("Error while checking configuration is valid: {err}")) + .unwrap_or(false) +} diff --git a/src/backends/wasapi/util.rs b/src/backends/wasapi/util.rs new file mode 100644 index 0000000..d45eaf1 --- /dev/null +++ b/src/backends/wasapi/util.rs @@ -0,0 +1,130 @@ +use crate::prelude::wasapi::error; +use std::marker::PhantomData; +use windows::core::Interface; +use windows::Win32::Foundation::RPC_E_CHANGED_MODE; +use windows::Win32::Media::Audio; +use windows::Win32::System::Com; +use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, StructuredStorage, COINIT_APARTMENTTHREADED, STGM_READ}; +use windows::Win32::Devices::Properties; +use windows::Win32::System::Variant::VT_LPWSTR; +use std::ffi::OsString; +use std::os::windows::ffi::OsStringExt; + +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(|_| {}); +} + +#[derive(Debug, Clone)] +pub struct WasapiMMDevice(Audio::IMMDevice); + +unsafe impl Send for WasapiMMDevice {} + +impl WasapiMMDevice { + pub(crate) fn new(device: Audio::IMMDevice) -> Self { + Self(device) + } + + pub(crate) fn activate(&self) -> Result { + unsafe { + self.0 + .Activate::(Com::CLSCTX_ALL, None) + .map_err(|err| error::WasapiError::BackendError(err)) + } + } + + pub(crate) fn name(&self) -> Option { + get_device_name(&self.0) + } +} + +fn get_device_name(device: &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) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index b034b61..9693d73 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,6 +65,9 @@ pub struct StreamConfig { /// honoring this setting, and in future versions may provide additional buffering to ensure /// it, but for now you should not make assumptions on buffer sizes based on this setting. pub buffer_size_range: (Option, Option), + /// Whether the device should be exclusively held (meaning no other application can open the + /// same device). + pub exclusive: bool, } /// Audio channel description. diff --git a/src/prelude.rs b/src/prelude.rs index bea0c8c..77d64d6 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,2 +1,4 @@ pub use crate::backends::*; +#[cfg(os_wasapi)] +pub use crate::backends::wasapi::prelude::*; pub use crate::*; From 3027567ad2a3ff9f703897c9d55f4c3c7ad523f2 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 10 Nov 2024 20:13:29 +0100 Subject: [PATCH 4/9] feat(wasapi): working input audio stream --- src/backends/mod.rs | 5 +- src/backends/wasapi/device.rs | 48 ++++--- src/backends/wasapi/stream.rs | 227 ++++++++++++++++++++++++++-------- 3 files changed, 210 insertions(+), 70 deletions(-) diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 7e2334e..4e18914 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -37,6 +37,7 @@ pub mod wasapi; /// | Linux | ALSA | /// | macOS | CoreAudio | /// | Windows | WASAPI | +#[cfg(any(os_alsa, os_coreaudio, os_wasapi))] #[allow(clippy::needless_return)] pub fn default_driver() -> impl AudioDriver { #[cfg(os_alsa)] @@ -66,13 +67,15 @@ where /// "Default" here means both in terms of platform support but also can include runtime selection. /// Therefore, it is better to use this method directly rather than first getting the default /// driver from [`default_driver`]. -#[cfg(any(os_alsa, os_coreaudio))] +#[cfg(any(os_alsa, os_coreaudio, os_wasapi))] #[allow(clippy::needless_return)] pub fn default_input_device() -> impl AudioInputDevice { #[cfg(os_alsa)] return default_input_device_from(&alsa::AlsaDriver); #[cfg(os_coreaudio)] return default_input_device_from(&coreaudio::CoreAudioDriver); + #[cfg(os_wasapi)] + return default_input_device_from(&wasapi::WasapiDriver); } /// Returns the default input device for the given audio driver. diff --git a/src/backends/wasapi/device.rs b/src/backends/wasapi/device.rs index 7f133ae..f29bcdb 100644 --- a/src/backends/wasapi/device.rs +++ b/src/backends/wasapi/device.rs @@ -2,9 +2,7 @@ use super::{error, stream}; use crate::backends::wasapi::stream::WasapiStream; use crate::channel_map::Bitset; use crate::prelude::wasapi::util::WasapiMMDevice; -use crate::{ - AudioDevice, AudioOutputCallback, AudioOutputDevice, Channel, DeviceType, StreamConfig, -}; +use crate::{AudioDevice, AudioInputCallback, AudioInputDevice, AudioOutputCallback, AudioOutputDevice, Channel, DeviceType, StreamConfig}; use std::borrow::Cow; use windows::Win32::Media::Audio; @@ -59,21 +57,35 @@ impl AudioDevice for WasapiDevice { } } -// impl AudioInputDevice for WasapiDevice { -// type StreamHandle = WasapiStream; - -// fn create_input_stream( -// &self, -// stream_config: StreamConfig, -// callback: Callback, -// ) -> Result, Self::Error> { -// Ok(WasapiStream::new_input( -// self.name.clone(), -// stream_config, -// callback, -// )) -// } -// } + +impl AudioInputDevice for WasapiDevice { + type StreamHandle = WasapiStream; + + fn default_input_config(&self) -> Result { + let audio_client = self.device.activate::()?; + let format = unsafe { + audio_client.GetMixFormat()?.read_unaligned() }; + let frame_size = unsafe { audio_client.GetBufferSize() }.map(|i| i as usize).ok(); + Ok(StreamConfig { + channels: 0u32.with_indices(0..format.nChannels as _), + exclusive: false, + samplerate: format.nSamplesPerSec as _, + buffer_size_range: (frame_size, frame_size), + }) + } + + fn create_input_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error> { + Ok(WasapiStream::new_input( + self.device.clone(), + stream_config, + callback, + )) + } +} impl AudioOutputDevice for WasapiDevice { type StreamHandle = WasapiStream; diff --git a/src/backends/wasapi/stream.rs b/src/backends/wasapi/stream.rs index e07b187..e7f01da 100644 --- a/src/backends/wasapi/stream.rs +++ b/src/backends/wasapi/stream.rs @@ -2,10 +2,12 @@ use super::error; use crate::audio_buffer::AudioMut; use crate::backends::wasapi::util::WasapiMMDevice; use crate::channel_map::Bitset; -use crate::prelude::Timestamp; +use crate::prelude::{AudioRef, Timestamp}; use crate::{ - AudioCallbackContext, AudioOutput, AudioOutputCallback, AudioStreamHandle, StreamConfig, + AudioCallbackContext, AudioInput, AudioInputCallback, AudioOutput, AudioOutputCallback, + AudioStreamHandle, StreamConfig, }; +use duplicate::duplicate_item; use std::marker::PhantomData; use std::ptr::NonNull; use std::sync::atomic::{AtomicBool, Ordering}; @@ -14,6 +16,7 @@ use std::thread::JoinHandle; use std::time::Duration; use std::{ops, ptr, slice}; use windows::core::imp::CoTaskMemFree; +use windows::core::Interface; use windows::Win32::Foundation; use windows::Win32::Foundation::{CloseHandle, HANDLE}; use windows::Win32::Media::{Audio, KernelStreaming, Multimedia}; @@ -21,15 +24,25 @@ use windows::Win32::System::Threading; type EjectSignal = Arc; -struct AudioBuffer<'a, T> { - render_client: &'a Audio::IAudioRenderClient, +#[duplicate_item( +name ty; +[AudioCaptureBuffer] [IAudioCaptureClient]; +[AudioRenderBuffer] [IAudioRenderClient]; +)] +struct name<'a, T> { + interface: &'a Audio::ty, data: NonNull, frame_size: usize, channels: usize, __type: PhantomData, } -impl<'a, T> ops::Deref for AudioBuffer<'a, T> { +#[duplicate_item( +name; +[AudioCaptureBuffer]; +[AudioRenderBuffer]; +)] +impl<'a, T> ops::Deref for name<'a, T> { type Target = [T]; fn deref(&self) -> &Self::Target { @@ -37,25 +50,37 @@ impl<'a, T> ops::Deref for AudioBuffer<'a, T> { } } -impl<'a, T> ops::DerefMut for AudioBuffer<'a, T> { +#[duplicate_item( +name; +[AudioCaptureBuffer]; +[AudioRenderBuffer]; +)] +impl<'a, T> ops::DerefMut for name<'a, T> { fn deref_mut(&mut self) -> &mut Self::Target { unsafe { slice::from_raw_parts_mut(self.data.cast().as_ptr(), self.channels * self.frame_size) } } } -impl Drop for AudioBuffer<'_, T> { + +impl Drop for AudioCaptureBuffer<'_, T> { + fn drop(&mut self) { + unsafe { self.interface.ReleaseBuffer(self.frame_size as _).unwrap() }; + } +} + +impl Drop for AudioRenderBuffer<'_, T> { fn drop(&mut self) { unsafe { - self.render_client + self.interface .ReleaseBuffer(self.frame_size as _, 0) - .unwrap() - }; + .unwrap(); + } } } -impl<'a, T> AudioBuffer<'a, T> { - fn from_render_client( +impl<'a, T> AudioRenderBuffer<'a, T> { + fn from_client( render_client: &'a Audio::IAudioRenderClient, channels: usize, frame_size: usize, @@ -63,7 +88,7 @@ impl<'a, T> AudioBuffer<'a, T> { let data = NonNull::new(unsafe { render_client.GetBuffer(frame_size as _) }?) .expect("Audio buffer data is null"); Ok(Self { - render_client, + interface: render_client, data, frame_size, channels, @@ -71,10 +96,31 @@ impl<'a, T> AudioBuffer<'a, T> { }) } } +impl<'a, T> AudioCaptureBuffer<'a, T> { + fn from_client( + capture_client: &'a Audio::IAudioCaptureClient, + channels: usize, + ) -> Result, error::WasapiError> { + let mut buf_ptr = ptr::null_mut(); + let mut frame_size = 0; + let mut flags = 0; + unsafe { + capture_client.GetBuffer(&mut buf_ptr, &mut frame_size, &mut flags, None, None) + }?; + let Some(data) = NonNull::new(buf_ptr as _) else { return Ok(None); }; + Ok(Some(Self { + interface: capture_client, + data, + frame_size: frame_size as _, + channels, + __type: PhantomData, + })) + } +} -struct RenderThread { +struct AudioThread { audio_client: Audio::IAudioClient, - render_client: Audio::IAudioRenderClient, + interface: Interface, audio_clock: Audio::IAudioClock, stream_config: StreamConfig, eject_signal: EjectSignal, @@ -84,7 +130,16 @@ struct RenderThread { clock_start: Duration, } -impl RenderThread { +impl AudioThread { + fn finalize(self) -> Result { + if !self.event_handle.is_invalid() { + unsafe { CloseHandle(self.event_handle) }?; + } + Ok(self.callback) + } +} + +impl AudioThread { fn new( device: WasapiMMDevice, eject_signal: EjectSignal, @@ -143,12 +198,12 @@ impl RenderThread { audio_client.SetEventHandle(event_handle)?; event_handle }; - let render_client = audio_client.GetService::()?; + let interface = audio_client.GetService::()?; let audio_clock = audio_client.GetService::()?; let frame_size = buffer_size; Ok(Self { audio_client, - render_client, + interface, audio_clock, event_handle, frame_size, @@ -163,15 +218,74 @@ impl RenderThread { } } - fn finalize(self) -> Result { - if !self.event_handle.is_invalid() { - unsafe { CloseHandle(self.event_handle) }?; + fn await_frame(&mut self) -> Result<(), error::WasapiError> { + let _ = unsafe { + let result = Threading::WaitForSingleObject(self.event_handle, Threading::INFINITE); + if result == Foundation::WAIT_FAILED { + let err = Foundation::GetLastError(); + let description = format!("Waiting for event handle failed: {:?}", err); + return Err(error::WasapiError::FoundationError(description)); + } + result + }; + Ok(()) + } + + fn output_timestamp(&self) -> Result { + let clock = stream_instant(&self.audio_clock)?; + let diff = clock - self.clock_start; + Ok(Timestamp::from_duration( + self.stream_config.samplerate, + diff, + )) + } +} + +impl AudioThread { + fn run(mut self) -> Result { + set_thread_priority(); + unsafe { + self.audio_client.Start()?; } - Ok(self.callback) + self.clock_start = stream_instant(&self.audio_clock)?; + loop { + if self.eject_signal.load(Ordering::Relaxed) { + break self.finalize(); + } + self.await_frame()?; + self.process()?; + } + .inspect_err(|err| eprintln!("Render thread process error: {err}")) + } + + fn process(&mut self) -> Result<(), error::WasapiError> { + let frames_available = unsafe { + self.interface.GetNextPacketSize()? as usize + }; + if frames_available == 0 { + return Ok(()); + } + let Some(mut buffer) = AudioCaptureBuffer::::from_client( + &self.interface, + self.stream_config.channels.count(), + )? else { + eprintln!("Null buffer from WASAPI"); + return Ok(()); + }; + let timestamp = self.output_timestamp()?; + let context = AudioCallbackContext { + stream_config: self.stream_config, + timestamp, + }; + let buffer = + AudioRef::from_interleaved(&mut buffer, self.stream_config.channels.count()).unwrap(); + let output = AudioInput { timestamp, buffer }; + self.callback.on_input_data(context, output); + Ok(()) } } -impl RenderThread { +impl AudioThread { fn run(mut self) -> Result { set_thread_priority(); unsafe { @@ -194,7 +308,6 @@ impl RenderThread { self.frame_size - padding }; if frames_available == 0 { - eprintln!("WASAPI WTF: 0 output callback requested"); return Ok(()); } let frames_requested = if let Some(max_frames) = self.stream_config.buffer_size_range.1 { @@ -202,8 +315,8 @@ impl RenderThread { } else { frames_available }; - let mut buffer = AudioBuffer::::from_render_client( - &self.render_client, + let mut buffer = AudioRenderBuffer::::from_client( + &self.interface, self.stream_config.channels.count(), frames_requested, )?; @@ -219,28 +332,6 @@ impl RenderThread { self.callback.on_output_data(context, output); Ok(()) } - - fn await_frame(&mut self) -> Result<(), error::WasapiError> { - let _ = unsafe { - let result = Threading::WaitForSingleObject(self.event_handle, Threading::INFINITE); - if result == Foundation::WAIT_FAILED { - let err = Foundation::GetLastError(); - let description = format!("Waiting for event handle failed: {:?}", err); - return Err(error::WasapiError::FoundationError(description)); - } - result - }; - Ok(()) - } - - fn output_timestamp(&self) -> Result { - let clock = stream_instant(&self.audio_clock)?; - let diff = clock - self.clock_start; - Ok(Timestamp::from_duration( - self.stream_config.samplerate, - diff, - )) - } } /// Type representing a WASAPI audio stream. @@ -260,6 +351,34 @@ impl AudioStreamHandle for WasapiStream { } } +impl WasapiStream { + pub(crate) fn new_input( + device: WasapiMMDevice, + stream_config: StreamConfig, + callback: Callback, + ) -> Self { + let eject_signal = EjectSignal::default(); + let join_handle = std::thread::Builder::new() + .name("interflow_wasapi_output_stream".to_string()) + .spawn({ + let eject_signal = eject_signal.clone(); + move || { + let inner: AudioThread = + AudioThread::new(device, eject_signal, stream_config, callback) + .inspect_err(|err| { + eprintln!("Failed to create render thread: {err}") + })?; + inner.run() + } + }) + .expect("Cannot spawn audio output thread"); + Self { + join_handle, + eject_signal, + } + } +} + impl WasapiStream { pub(crate) fn new_output( device: WasapiMMDevice, @@ -272,8 +391,11 @@ impl WasapiStream { .spawn({ let eject_signal = eject_signal.clone(); move || { - let inner = RenderThread::new(device, eject_signal, stream_config, callback) - .inspect_err(|err| eprintln!("Failed to create render thread: {err}"))?; + let inner: AudioThread = + AudioThread::new(device, eject_signal, stream_config, callback) + .inspect_err(|err| { + eprintln!("Failed to create render thread: {err}") + })?; inner.run() } }) @@ -353,7 +475,10 @@ pub(crate) fn config_to_waveformatextensible(config: &StreamConfig) -> Audio::WA waveformatextensible } -pub(crate) fn is_output_config_supported(device: WasapiMMDevice, stream_config: &StreamConfig) -> bool { +pub(crate) fn is_output_config_supported( + device: WasapiMMDevice, + stream_config: &StreamConfig, +) -> bool { let mut try_ = || unsafe { let audio_client: Audio::IAudioClient = device.activate()?; let sharemode = if stream_config.exclusive { From 8ced12bbb767a623b4a76dc0179977568bdc10f1 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Sun, 10 Nov 2024 20:36:19 +0100 Subject: [PATCH 5/9] fix(wasapi): stop audio stream on finalize --- src/backends/wasapi/stream.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/backends/wasapi/stream.rs b/src/backends/wasapi/stream.rs index e7f01da..783c591 100644 --- a/src/backends/wasapi/stream.rs +++ b/src/backends/wasapi/stream.rs @@ -135,6 +135,11 @@ impl AudioThread { if !self.event_handle.is_invalid() { unsafe { CloseHandle(self.event_handle) }?; } + let _ = unsafe { + self.audio_client + .Stop() + .inspect_err(|err| eprintln!("Cannot stop audio thread: {err}")) + }; Ok(self.callback) } } From 906547bbc894ddd9cb366824c336d93d45334306 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 11 Nov 2024 09:44:19 +0100 Subject: [PATCH 6/9] chore(coreaudio): fix changes on coreaudio --- src/backends/coreaudio.rs | 12 ++++++++++-- src/backends/mod.rs | 3 +-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/backends/coreaudio.rs b/src/backends/coreaudio.rs index 95263d8..6c09e55 100644 --- a/src/backends/coreaudio.rs +++ b/src/backends/coreaudio.rs @@ -160,12 +160,18 @@ impl AudioDevice for CoreAudioDevice { .iter() .copied() .filter(move |sr| samplerate_range.contains(sr)) - .map(move |sr| { + .flat_map(move |sr| { + [false, true] + .into_iter() + .map(move |exclusive| (sr, exclusive)) + }) + .map(move |(samplerate, exclusive)| { let channels = 1 << asbd.mFormat.mChannelsPerFrame as u32 - 1; StreamConfig { - samplerate: sr, + samplerate, channels, buffer_size_range: (None, None), + exclusive, } }) })) @@ -195,6 +201,7 @@ impl AudioInputDevice for CoreAudioDevice { channels: 0b1, // Hardcoded to mono on non-interleaved inputs samplerate, buffer_size_range: (None, None), + exclusive: false, }) } @@ -226,6 +233,7 @@ impl AudioOutputDevice for CoreAudioDevice { samplerate, buffer_size_range: (None, None), channels: 0b11, + exclusive: false, }) } diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 4e18914..baccd85 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -5,7 +5,6 @@ //! Each backend is provided in its own submodule. Types should be public so that the user isn't //! limited to going through the main API if they want to choose a specific backend. -use wasapi::driver; use crate::{ AudioDriver, AudioInputDevice, AudioOutputDevice, DeviceType, }; @@ -105,5 +104,5 @@ pub fn default_output_device() -> impl AudioOutputDevice { #[cfg(os_coreaudio)] return default_output_device_from(&coreaudio::CoreAudioDriver); #[cfg(os_wasapi)] - return default_output_device_from(&driver::WasapiDriver); + return default_output_device_from(&wasapi::WasapiDriver); } From 2658f670d8bb37b71bcd0eac7c64473a3c4e6cfe Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 11 Nov 2024 10:37:09 +0100 Subject: [PATCH 7/9] fix: add enumerate_coreaudio again --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 96192db..436641b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,10 @@ windows = { version = "0.58.0", features = [ name = "enumerate_alsa" path = "examples/enumerate_alsa.rs" +[[example]] +name = "enumerate_coreaudio" +path = "examples/enumerate_coreaudio.rs" + [[example]] name = "enumerate_wasapi" path = "examples/enumerate_wasapi.rs" From 1a422e5d80007b988a144a9bf170f44aa801fc40 Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 11 Nov 2024 10:37:45 +0100 Subject: [PATCH 8/9] fix: remove wasm from not unsupported --- build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.rs b/build.rs index 07a6f07..23f1985 100644 --- a/build.rs +++ b/build.rs @@ -8,6 +8,6 @@ fn main() { target_os = "netbsd") }, os_coreaudio: { any (target_os = "macos", target_os = "ios") }, os_wasapi: { target_os = "windows" }, - unsupported: { not(any(wasm, os_alsa, os_coreaudio, os_wasapi))} + unsupported: { not(any(os_alsa, os_coreaudio, os_wasapi))} } } From ef7aadd64cfae753722875c8ccab278aad88213f Mon Sep 17 00:00:00 2001 From: Nathan Graule Date: Mon, 11 Nov 2024 10:38:22 +0100 Subject: [PATCH 9/9] fix(alsa): fix changes on alsa --- src/backends/alsa.rs | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/backends/alsa.rs b/src/backends/alsa.rs index 17552f2..4c5cd2c 100644 --- a/src/backends/alsa.rs +++ b/src/backends/alsa.rs @@ -10,7 +10,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread::JoinHandle; use std::time::Duration; -use std::{borrow::Cow, ffi::CStr}; +use std::borrow::Cow; use alsa::{device_name::HintIter, pcm, PCM}; use thiserror::Error; @@ -53,10 +53,6 @@ impl AudioDriver for AlsaDriver { } fn list_devices(&self) -> Result, Self::Error> { - const C_PCM: &CStr = match CStr::from_bytes_with_nul(b"pcm\0") { - Ok(cstr) => cstr, - Err(_) => unreachable!(), - }; Ok(HintIter::new(None, c"pcm")? .filter_map(|hint| AlsaDevice::new(hint.name.as_ref()?, hint.direction?).ok())) } @@ -210,11 +206,12 @@ impl AlsaDevice { fn default_config(&self) -> Result { let samplerate = 48000.; // Default ALSA sample rate let channel_count = 2; // Stereo stream - let channels = 1 << channel_count - 1; + let channels = 1 << (channel_count - 1); Ok(StreamConfig { samplerate: samplerate as _, channels, buffer_size_range: (None, None), + exclusive: false, }) } } @@ -259,6 +256,7 @@ impl AlsaStream { channels: ChannelMap32::default() .with_indices(std::iter::repeat(1).take(num_channels)), buffer_size_range: (Some(period_size), Some(period_size)), + exclusive: false, }; let mut timestamp = Timestamp::new(samplerate); let mut buffer = vec![0f32; period_size * num_channels]; @@ -274,13 +272,10 @@ impl AlsaStream { } let frames = device.pcm.avail_update()? as usize; let len = frames * num_channels; - match io.readi(&mut buffer[..len]) { - Err(err) => { - log::warn!("ALSA PCM error, trying to recover ..."); - log::debug!("Error: {err}"); - device.pcm.try_recover(err, true)?; - } - _ => {} + if let Err(err) = io.readi(&mut buffer[..len]) { + log::warn!("ALSA PCM error, trying to recover ..."); + log::debug!("Error: {err}"); + device.pcm.try_recover(err, true)?; } let buffer = AudioRef::from_interleaved(&buffer[..len], num_channels).unwrap(); let context = AudioCallbackContext { @@ -333,6 +328,7 @@ impl AlsaStream { channels: ChannelMap32::default() .with_indices(std::iter::repeat(1).take(num_channels)), buffer_size_range: (Some(period_size), Some(period_size)), + exclusive: false, }; let frames = device.pcm.avail_update()? as usize; let mut timestamp = Timestamp::new(samplerate); @@ -358,10 +354,7 @@ impl AlsaStream { }; callback.on_output_data(context, input); timestamp += frames as u64; - match io.writei(&buffer[..len]) { - Err(err) => device.pcm.try_recover(err, true)?, - _ => {} - } + if let Err(err) = io.writei(&buffer[..len]) { device.pcm.try_recover(err, true)? } match device.pcm.state() { pcm::State::Suspended => { if hwp.can_resume() {