From af3dd42991243dbee502f8351e567c071dbe1a8f Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 13 Nov 2025 21:54:54 -0500 Subject: [PATCH 01/14] feat: introduce description() for devices Deprecate DeviceTrait::name in favor of description() for human-readable labels and id() for unique identifiers. Implement description across host backends, update examples to print device IDs, and forward name() to description() where needed. Before this, the name() implementation across hosts was inconsistent. To ease upgrading users, this change keeps the name() behavior as it was in v0.16. Also removes #[inline] pragmas for code outside of the audio hot path. --- examples/beep.rs | 4 +-- examples/custom.rs | 6 +++- examples/enumerate.rs | 19 +++++++++-- examples/feedback.rs | 10 +++--- examples/record_wav.rs | 6 ++-- examples/synth_tones.rs | 4 +-- src/host/aaudio/mod.rs | 47 +++++++++++++++++---------- src/host/alsa/enumerate.rs | 3 -- src/host/alsa/mod.rs | 31 ++++++++++-------- src/host/asio/device.rs | 4 +++ src/host/asio/mod.rs | 4 +++ src/host/coreaudio/ios/enumerate.rs | 3 -- src/host/coreaudio/ios/mod.rs | 19 +++++------ src/host/coreaudio/macos/device.rs | 8 +++++ src/host/coreaudio/macos/enumerate.rs | 1 + src/host/custom/mod.rs | 9 +++++ src/host/emscripten/mod.rs | 16 +++++---- src/host/jack/device.rs | 4 +++ src/host/null/mod.rs | 13 +++----- src/host/wasapi/com.rs | 1 - src/host/wasapi/device.rs | 12 ++++--- src/host/wasapi/stream.rs | 4 +-- src/host/webaudio/mod.rs | 25 ++++++-------- src/platform/mod.rs | 10 ++++++ src/traits.rs | 12 ++++++- 25 files changed, 170 insertions(+), 105 deletions(-) diff --git a/examples/beep.rs b/examples/beep.rs index 2a6872320..9b38f1660 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -67,10 +67,10 @@ fn main() -> anyhow::Result<()> { host.default_output_device() } else { host.output_devices()? - .find(|x| x.name().map(|y| y == opt.device).unwrap_or(false)) + .find(|dev| dev.id().is_ok_and(|id| id.to_string() == opt.device)) } .expect("failed to find output device"); - println!("Output device: {}", device.name()?); + println!("Output device: {}", device.id()?); let config = device.default_output_config().unwrap(); println!("Default output config: {config:?}"); diff --git a/examples/custom.rs b/examples/custom.rs index 0e00d661a..e4b43f3f0 100644 --- a/examples/custom.rs +++ b/examples/custom.rs @@ -53,7 +53,11 @@ impl DeviceTrait for MyDevice { type Stream = MyStream; fn name(&self) -> Result { - Ok(String::from("custom device")) + Ok(String::from("custom")) + } + + fn description(&self) -> Result { + Ok(String::from("Custom Device")) } fn id(&self) -> Result { diff --git a/examples/enumerate.rs b/examples/enumerate.rs index 826044fc4..dccb5368a 100644 --- a/examples/enumerate.rs +++ b/examples/enumerate.rs @@ -12,15 +12,28 @@ fn main() -> Result<(), anyhow::Error> { println!("{}", host_id.name()); let host = cpal::host_from_id(host_id)?; - let default_in = host.default_input_device().map(|e| e.name().unwrap()); - let default_out = host.default_output_device().map(|e| e.name().unwrap()); + let default_in = host + .default_input_device() + .map(|dev| dev.id().unwrap()) + .map(|id| id.to_string()); + let default_out = host + .default_output_device() + .map(|dev| dev.id().unwrap()) + .map(|id| id.to_string()); println!(" Default Input Device:\n {default_in:?}"); println!(" Default Output Device:\n {default_out:?}"); let devices = host.devices()?; println!(" Devices: "); for (device_index, device) in devices.enumerate() { - println!(" {}. \"{}\"", device_index + 1, device.name()?); + let id = device + .id() + .map_or("Unknown ID".to_string(), |id| id.to_string()); + if let Ok(desc) = device.description() { + println!(" {}. {id} ({})", device_index + 1, desc); + } else { + println!(" {}. {id}", device_index + 1); + } // Input configs if let Ok(conf) = device.default_input_config() { diff --git a/examples/feedback.rs b/examples/feedback.rs index 23b7386e9..6f73f624b 100644 --- a/examples/feedback.rs +++ b/examples/feedback.rs @@ -84,8 +84,8 @@ fn main() -> anyhow::Result<()> { let input_device = if opt.input_device == "default" { host.default_input_device() } else { - host.input_devices()? - .find(|x| x.name().map(|y| y == opt.input_device).unwrap_or(false)) + host.output_devices()? + .find(|dev| dev.id().is_ok_and(|id| id.to_string() == opt.input_device)) } .expect("failed to find input device"); @@ -93,12 +93,12 @@ fn main() -> anyhow::Result<()> { host.default_output_device() } else { host.output_devices()? - .find(|x| x.name().map(|y| y == opt.output_device).unwrap_or(false)) + .find(|dev| dev.id().is_ok_and(|id| id.to_string() == opt.output_device)) } .expect("failed to find output device"); - println!("Using input device: \"{}\"", input_device.name()?); - println!("Using output device: \"{}\"", output_device.name()?); + println!("Using input device: \"{}\"", input_device.id()?); + println!("Using output device: \"{}\"", output_device.id()?); // We'll try and use the same configuration between streams to keep it simple. let config: cpal::StreamConfig = input_device.default_input_config()?.into(); diff --git a/examples/record_wav.rs b/examples/record_wav.rs index ec7283774..f3897ac3d 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -78,13 +78,13 @@ fn main() -> Result<(), anyhow::Error> { let device = match opt.device.as_str() { "default" => host.default_input_device(), "default-output" => host.default_output_device(), - name => host + device => host .input_devices()? - .find(|x| x.name().map(|y| y == name).unwrap_or(false)), + .find(|dev| dev.id().is_ok_and(|id| id.to_string() == device)), } .expect("failed to find input device"); - println!("Input device: {}", device.name()?); + println!("Input device: {}", device.id()?); let config = if device.supports_input() { device.default_input_config() diff --git a/examples/synth_tones.rs b/examples/synth_tones.rs index fb641b815..bc0adaf65 100644 --- a/examples/synth_tones.rs +++ b/examples/synth_tones.rs @@ -121,10 +121,10 @@ pub fn host_device_setup( let device = host .default_output_device() .ok_or_else(|| anyhow::Error::msg("Default output device is not available"))?; - println!("Output device : {}", device.name()?); + println!("Output device: {}", device.id()?); let config = device.default_output_config()?; - println!("Default output config : {config:?}"); + println!("Default output config: {config:?}"); Ok((host, device, config)) } diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 614a40f5e..9720140ed 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -304,47 +304,58 @@ impl DeviceTrait for Device { type Stream = Stream; fn name(&self) -> Result { - match &self.0 { - None => Ok("default".to_owned()), + let name = match &self.0 { + None => "default".to_owned(), Some(info) => { - let name = if info.address.is_empty() { + if info.address.is_empty() { format!("{}:{:?}", info.product_name, info.device_type) } else { format!( "{}:{:?}:{}", info.product_name, info.device_type, info.address ) - }; - Ok(name) + } } - } + }; + Ok(name) + } + + fn description(&self) -> Result { + let description = match &self.0 { + None => "Default Device".to_owned(), + Some(info) => format!("{}, {:?}", info.product_name, info.device_type), + }; + Ok(description) } fn id(&self) -> Result { - match &self.0 { - None => Ok(DeviceId::AAudio(-1)), // Default device - Some(info) => Ok(DeviceId::AAudio(info.id)), - } + let id = match &self.0 { + None => DeviceId::AAudio(-1), // Default device + Some(info) => DeviceId::AAudio(info.id), + }; + Ok(id) } fn supported_input_configs( &self, ) -> Result { - if let Some(info) = &self.0 { - Ok(device_supported_configs(info)) + let configs = if let Some(info) = &self.0 { + device_supported_configs(info) } else { - Ok(default_supported_configs()) - } + default_supported_configs() + }; + Ok(configs) } fn supported_output_configs( &self, ) -> Result { - if let Some(info) = &self.0 { - Ok(device_supported_configs(info)) + let configs = if let Some(info) = &self.0 { + device_supported_configs(info) } else { - Ok(default_supported_configs()) - } + default_supported_configs() + }; + Ok(configs) } fn default_input_config(&self) -> Result { diff --git a/src/host/alsa/enumerate.rs b/src/host/alsa/enumerate.rs index 03ddd733a..48a2e1b58 100644 --- a/src/host/alsa/enumerate.rs +++ b/src/host/alsa/enumerate.rs @@ -42,17 +42,14 @@ impl Iterator for Devices { } } -#[inline] pub fn default_input_device() -> Option { Some(default_device()) } -#[inline] pub fn default_output_device() -> Option { Some(default_device()) } -#[inline] pub fn default_device() -> Device { Device { pcm_id: "default".to_string(), diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 613095e96..0ef19ac48 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -3,7 +3,7 @@ extern crate libc; use std::{ cell::Cell, - cmp, fmt, + cmp, sync::{Arc, Mutex}, thread::{self, JoinHandle}, time::Duration, @@ -119,6 +119,10 @@ impl DeviceTrait for Device { Device::name(self) } + fn description(&self) -> Result { + Device::description(self) + } + fn id(&self) -> Result { Device::id(self) } @@ -378,12 +382,21 @@ impl Device { Ok(stream_inner) } - #[inline] fn name(&self) -> Result { - Ok(self.to_string()) + Ok(self.pcm_id.clone()) + } + + fn description(&self) -> Result { + self.desc + .clone() + .ok_or(DeviceNameError::BackendSpecific { + err: BackendSpecificError { + description: String::from("no description for this device"), + }, + }) + .map(|desc| desc.replace('\n', ", ")) } - #[inline] fn id(&self) -> Result { Ok(DeviceId::ALSA(self.pcm_id.clone())) } @@ -1447,13 +1460,3 @@ impl From for StreamError { err.into() } } - -impl fmt::Display for Device { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(desc) = &self.desc { - write!(f, "{} ({})", self.pcm_id, desc.replace('\n', ", ")) - } else { - write!(f, "{}", self.pcm_id) - } - } -} diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index 85d851039..d5e5f9acd 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -53,6 +53,10 @@ impl Hash for Device { impl Device { pub fn name(&self) -> Result { + self.description() + } + + fn description(&self) -> Result { Ok(self.driver.name().to_string()) } diff --git a/src/host/asio/mod.rs b/src/host/asio/mod.rs index 5c56b155f..9328cc2a6 100644 --- a/src/host/asio/mod.rs +++ b/src/host/asio/mod.rs @@ -62,6 +62,10 @@ impl DeviceTrait for Device { Device::name(self) } + fn description(&self) -> Result { + Device::description(self) + } + fn id(&self) -> Result { Device::id(self) } diff --git a/src/host/coreaudio/ios/enumerate.rs b/src/host/coreaudio/ios/enumerate.rs index 0bfc7f3e9..c6a16daa2 100644 --- a/src/host/coreaudio/ios/enumerate.rs +++ b/src/host/coreaudio/ios/enumerate.rs @@ -26,18 +26,15 @@ impl Default for Devices { impl Iterator for Devices { type Item = Device; - #[inline] fn next(&mut self) -> Option { self.0.next() } } -#[inline] pub fn default_input_device() -> Option { Some(Device) } -#[inline] pub fn default_output_device() -> Option { Some(Device) } diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index aba697804..e972faa45 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -66,8 +66,11 @@ impl HostTrait for Host { } impl Device { - #[inline] fn name(&self) -> Result { + self.description() + } + + fn description(&self) -> Result { Ok("Default Device".to_owned()) } @@ -75,21 +78,18 @@ impl Device { Ok(DeviceId::IOS("default".to_string())) } - #[inline] fn supported_input_configs( &self, ) -> Result { Ok(get_supported_stream_configs(true)) } - #[inline] fn supported_output_configs( &self, ) -> Result { Ok(get_supported_stream_configs(false)) } - #[inline] fn default_input_config(&self) -> Result { // Get the primary (exact channel count) config from supported configs get_supported_stream_configs(true) @@ -98,7 +98,6 @@ impl Device { .ok_or_else(|| DefaultStreamConfigError::StreamTypeNotSupported) } - #[inline] fn default_output_config(&self) -> Result { // Get the maximum channel count config from supported configs get_supported_stream_configs(false) @@ -113,36 +112,34 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - #[inline] fn name(&self) -> Result { Device::name(self) } - #[inline] + fn description(&self) -> Result { + Device::description(self) + } + fn id(&self) -> Result { Device::id(self) } - #[inline] fn supported_input_configs( &self, ) -> Result { Device::supported_input_configs(self) } - #[inline] fn supported_output_configs( &self, ) -> Result { Device::supported_output_configs(self) } - #[inline] fn default_input_config(&self) -> Result { Device::default_input_config(self) } - #[inline] fn default_output_config(&self) -> Result { Device::default_output_config(self) } diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index 91470e393..c96d9082a 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -266,6 +266,10 @@ impl DeviceTrait for Device { Device::name(self) } + fn description(&self) -> Result { + Device::description(self) + } + fn id(&self) -> Result { Device::id(self) } @@ -357,6 +361,10 @@ impl Device { } fn name(&self) -> Result { + self.description() + } + + fn description(&self) -> Result { get_device_name(self.audio_device_id).map_err(|err| DeviceNameError::BackendSpecific { err: BackendSpecificError { description: err.to_string(), diff --git a/src/host/coreaudio/macos/enumerate.rs b/src/host/coreaudio/macos/enumerate.rs index 4f337352f..89713f92f 100644 --- a/src/host/coreaudio/macos/enumerate.rs +++ b/src/host/coreaudio/macos/enumerate.rs @@ -75,6 +75,7 @@ impl Devices { impl Iterator for Devices { type Item = Device; + fn next(&mut self) -> Option { self.0.next().map(|id| Device { audio_device_id: id, diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs index 94fca54c1..6241264b5 100644 --- a/src/host/custom/mod.rs +++ b/src/host/custom/mod.rs @@ -141,6 +141,7 @@ type OutputCallback = Box Result; + fn description(&self) -> Result; fn id(&self) -> Result; fn supports_input(&self) -> bool; fn supports_output(&self) -> bool; @@ -219,6 +220,10 @@ where ::name(self) } + fn description(&self) -> Result { + ::description(self) + } + fn id(&self) -> Result { ::id(self) } @@ -337,6 +342,10 @@ impl DeviceTrait for Device { self.0.name() } + fn description(&self) -> Result { + self.0.description() + } + fn id(&self) -> Result { self.0.id() } diff --git a/src/host/emscripten/mod.rs b/src/host/emscripten/mod.rs index 82c6e2188..675561fe8 100644 --- a/src/host/emscripten/mod.rs +++ b/src/host/emscripten/mod.rs @@ -72,24 +72,24 @@ impl Devices { } impl Device { - #[inline] fn name(&self) -> Result { + self.description() + } + + fn description(&self) -> Result { Ok("Default Device".to_owned()) } - #[inline] fn id(&self) -> Result { Ok(DeviceId::Emscripten("default".to_string())) } - #[inline] fn supported_input_configs( &self, ) -> Result { unimplemented!(); } - #[inline] fn supported_output_configs( &self, ) -> Result { @@ -157,6 +157,10 @@ impl DeviceTrait for Device { Device::name(self) } + fn description(&self) -> Result { + Device::description(self) + } + fn id(&self) -> Result { Device::id(self) } @@ -390,7 +394,7 @@ impl Default for Devices { } impl Iterator for Devices { type Item = Device; - #[inline] + fn next(&mut self) -> Option { if self.0 { self.0 = false; @@ -401,12 +405,10 @@ impl Iterator for Devices { } } -#[inline] fn default_input_device() -> Option { unimplemented!(); } -#[inline] fn default_output_device() -> Option { if is_webaudio_available() { Some(Device) diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index adedc288a..2e2a5ea48 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -161,6 +161,10 @@ impl DeviceTrait for Device { type Stream = Stream; fn name(&self) -> Result { + self.description() + } + + fn description(&self) -> Result { Ok(self.name.clone()) } diff --git a/src/host/null/mod.rs b/src/host/null/mod.rs index 60c5daa4c..338f42cb7 100644 --- a/src/host/null/mod.rs +++ b/src/host/null/mod.rs @@ -46,36 +46,34 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - #[inline] fn name(&self) -> Result { Ok("null".to_owned()) } - #[inline] + fn description(&self) -> Result { + Ok("Null Device".to_owned()) + } + fn id(&self) -> Result { Ok(DeviceId::Null) } - #[inline] fn supported_input_configs( &self, ) -> Result { unimplemented!() } - #[inline] fn supported_output_configs( &self, ) -> Result { unimplemented!() } - #[inline] fn default_input_config(&self) -> Result { unimplemented!() } - #[inline] fn default_output_config(&self) -> Result { unimplemented!() } @@ -146,7 +144,6 @@ impl StreamTrait for Stream { impl Iterator for Devices { type Item = Device; - #[inline] fn next(&mut self) -> Option { None } @@ -155,7 +152,6 @@ impl Iterator for Devices { impl Iterator for SupportedInputConfigs { type Item = SupportedStreamConfigRange; - #[inline] fn next(&mut self) -> Option { None } @@ -164,7 +160,6 @@ impl Iterator for SupportedInputConfigs { impl Iterator for SupportedOutputConfigs { type Item = SupportedStreamConfigRange; - #[inline] fn next(&mut self) -> Option { None } diff --git a/src/host/wasapi/com.rs b/src/host/wasapi/com.rs index 710f333c0..973d8f444 100644 --- a/src/host/wasapi/com.rs +++ b/src/host/wasapi/com.rs @@ -40,7 +40,6 @@ struct ComInitialized { } impl Drop for ComInitialized { - #[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. diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index a3a60b967..75fada55b 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -58,6 +58,10 @@ impl DeviceTrait for Device { Device::name(self) } + fn description(&self) -> Result { + Device::description(self) + } + fn id(&self) -> Result { Device::id(self) } @@ -275,6 +279,10 @@ unsafe impl Sync for Device {} impl Device { pub fn name(&self) -> Result { + self.description() + } + + pub fn description(&self) -> Result { unsafe { // Open the device's property store. let property_store = self @@ -342,7 +350,6 @@ impl Device { } } - #[inline] fn from_immdevice(device: Audio::IMMDevice) -> Self { Device { device, @@ -374,7 +381,6 @@ impl Device { } /// Returns an uninitialized `IAudioClient`. - #[inline] pub(crate) fn build_audioclient(&self) -> Result { let mut lock = self.ensure_future_audio_client()?; Ok(lock.take().unwrap().0) @@ -788,7 +794,6 @@ impl Device { } impl PartialEq for Device { - #[inline] fn eq(&self, other: &Device) -> bool { // Use case: In order to check whether the default device has changed // the client code might need to compare the previous default device with the current one. @@ -927,7 +932,6 @@ impl Iterator for Devices { } } - #[inline] fn size_hint(&self) -> (usize, Option) { let num = self.total_count - self.next_item; let num = num as usize; diff --git a/src/host/wasapi/stream.rs b/src/host/wasapi/stream.rs index 4565670d5..7aad1f4b1 100644 --- a/src/host/wasapi/stream.rs +++ b/src/host/wasapi/stream.rs @@ -166,7 +166,6 @@ impl Stream { } } - #[inline] fn push_command(&self, command: Command) -> Result<(), SendError> { self.commands.send(command)?; unsafe { @@ -177,7 +176,6 @@ impl Stream { } impl Drop for Stream { - #[inline] fn drop(&mut self) { if self.push_command(Command::Terminate).is_ok() { self.thread.take().unwrap().join().unwrap(); @@ -194,6 +192,7 @@ impl StreamTrait for Stream { .map_err(|_| crate::error::PlayStreamError::DeviceNotAvailable)?; Ok(()) } + fn pause(&self) -> Result<(), PauseStreamError> { self.push_command(Command::PauseStream) .map_err(|_| crate::error::PauseStreamError::DeviceNotAvailable)?; @@ -202,7 +201,6 @@ impl StreamTrait for Stream { } impl Drop for StreamInner { - #[inline] fn drop(&mut self) { unsafe { let _ = Foundation::CloseHandle(self.event); diff --git a/src/host/webaudio/mod.rs b/src/host/webaudio/mod.rs index bfb8491e0..5bef545a0 100644 --- a/src/host/webaudio/mod.rs +++ b/src/host/webaudio/mod.rs @@ -88,17 +88,18 @@ impl Devices { } impl Device { - #[inline] fn name(&self) -> Result { + self.description() + } + + fn description(&self) -> Result { Ok("Default Device".to_owned()) } - #[inline] fn id(&self) -> Result { Ok(DeviceId::WebAudio("default".to_string())) } - #[inline] fn supported_input_configs( &self, ) -> Result { @@ -106,7 +107,6 @@ impl Device { Ok(Vec::new().into_iter()) } - #[inline] fn supported_output_configs( &self, ) -> Result { @@ -126,13 +126,11 @@ impl Device { Ok(configs.into_iter()) } - #[inline] fn default_input_config(&self) -> Result { // TODO Err(DefaultStreamConfigError::StreamTypeNotSupported) } - #[inline] fn default_output_config(&self) -> Result { const EXPECT: &str = "expected at least one valid webaudio stream config"; let config = self @@ -151,36 +149,34 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - #[inline] fn name(&self) -> Result { Device::name(self) } - #[inline] + fn description(&self) -> Result { + Device::description(self) + } + fn id(&self) -> Result { Device::id(self) } - #[inline] fn supported_input_configs( &self, ) -> Result { Device::supported_input_configs(self) } - #[inline] fn supported_output_configs( &self, ) -> Result { Device::supported_output_configs(self) } - #[inline] fn default_input_config(&self) -> Result { Device::default_input_config(self) } - #[inline] fn default_output_config(&self) -> Result { Device::default_output_config(self) } @@ -236,7 +232,7 @@ impl DeviceTrait for Device { // Create the WebAudio stream. let mut stream_opts = AudioContextOptions::new(); - stream_opts.sample_rate(config.sample_rate.0 as f32); + stream_opts.set_sample_rate(config.sample_rate.0 as f32); let ctx = AudioContext::new_with_context_options(&stream_opts).map_err( |err| -> BuildStreamError { let description = format!("{:?}", err); @@ -482,6 +478,7 @@ impl Default for Devices { impl Iterator for Devices { type Item = Device; + #[inline] fn next(&mut self) -> Option { if self.0 { @@ -493,13 +490,11 @@ impl Iterator for Devices { } } -#[inline] fn default_input_device() -> Option { // TODO None } -#[inline] fn default_output_device() -> Option { if is_webaudio_available() { Some(Device) diff --git a/src/platform/mod.rs b/src/platform/mod.rs index c53e01795..4acf598b1 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -308,6 +308,7 @@ macro_rules! impl_platform_host { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; + #[allow(deprecated)] fn name(&self) -> Result { match self.0 { $( @@ -317,6 +318,15 @@ macro_rules! impl_platform_host { } } + fn description(&self) -> Result { + match self.0 { + $( + $(#[cfg($feat)])? + DeviceInner::$HostVariant(ref d) => d.description(), + )* + } + } + fn id(&self) -> Result { match self.0 { $( diff --git a/src/traits.rs b/src/traits.rs index 51e9d00f5..b2e53b7e3 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -96,9 +96,19 @@ pub trait DeviceTrait { type Stream: StreamTrait; /// The human-readable name of the device. + #[deprecated( + since = "0.17.0", + note = "Use `id()` to get a unique identifier for the device, or `description()` for a human-readable description." + )] fn name(&self) -> Result; - /// The device-id of the device. + /// The human-readable description of the device. + fn description(&self) -> Result; + + /// The ID of the device. + /// + /// This ID uniquely identifies the device on the host. It should be stable across program + /// runs, device disconnections, and system reboots where possible. fn id(&self) -> Result; /// True if the device supports audio input, otherwise false From 5cde9543e4d411aea6d1af16e1cdf4ed7320b26e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 14 Nov 2025 16:55:35 -0500 Subject: [PATCH 02/14] refactor: use device_by_id with parsed IDs in examples --- examples/beep.rs | 12 ++++++------ examples/feedback.rs | 24 ++++++++++++------------ examples/record_wav.rs | 15 +++++++-------- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/examples/beep.rs b/examples/beep.rs index 9b38f1660..e9d74d0c5 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -8,8 +8,8 @@ use cpal::{ #[command(version, about = "CPAL beep example", long_about = None)] struct Opt { /// The audio device to use - #[arg(short, long, default_value_t = String::from("default"))] - device: String, + #[arg(short, long)] + device: Option, /// Use the JACK host #[cfg(all( @@ -63,11 +63,11 @@ fn main() -> anyhow::Result<()> { ))] let host = cpal::default_host(); - let device = if opt.device == "default" { - host.default_output_device() + let device = if let Some(device) = opt.device { + let id = &device.parse().expect("failed to parse device id"); + host.device_by_id(id) } else { - host.output_devices()? - .find(|dev| dev.id().is_ok_and(|id| id.to_string() == opt.device)) + host.default_output_device() } .expect("failed to find output device"); println!("Output device: {}", device.id()?); diff --git a/examples/feedback.rs b/examples/feedback.rs index 6f73f624b..76a0eaf14 100644 --- a/examples/feedback.rs +++ b/examples/feedback.rs @@ -17,12 +17,12 @@ use ringbuf::{ #[command(version, about = "CPAL feedback example", long_about = None)] struct Opt { /// The input audio device to use - #[arg(short, long, value_name = "IN", default_value_t = String::from("default"))] - input_device: String, + #[arg(short, long, value_name = "IN")] + input_device: Option, /// The output audio device to use - #[arg(short, long, value_name = "OUT", default_value_t = String::from("default"))] - output_device: String, + #[arg(short, long, value_name = "OUT")] + output_device: Option, /// Specify the delay between input and output #[arg(short, long, value_name = "DELAY_MS", default_value_t = 150.0)] @@ -81,19 +81,19 @@ fn main() -> anyhow::Result<()> { let host = cpal::default_host(); // Find devices. - let input_device = if opt.input_device == "default" { - host.default_input_device() + let input_device = if let Some(device) = opt.input_device { + let id = &device.parse().expect("failed to parse input device id"); + host.device_by_id(id) } else { - host.output_devices()? - .find(|dev| dev.id().is_ok_and(|id| id.to_string() == opt.input_device)) + host.default_input_device() } .expect("failed to find input device"); - let output_device = if opt.output_device == "default" { - host.default_output_device() + let output_device = if let Some(device) = opt.output_device { + let id = &device.parse().expect("failed to parse output device id"); + host.device_by_id(id) } else { - host.output_devices()? - .find(|dev| dev.id().is_ok_and(|id| id.to_string() == opt.output_device)) + host.default_output_device() } .expect("failed to find output device"); diff --git a/examples/record_wav.rs b/examples/record_wav.rs index f3897ac3d..1c86ce41f 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -15,8 +15,8 @@ struct Opt { /// The audio device to use. /// For the default microphone, use "default". /// For recording system output, use "default-output". - #[arg(short, long, default_value_t = String::from("default"))] - device: String, + #[arg(short, long)] + device: Option, /// How long to record, in seconds #[arg(long, default_value_t = 3)] @@ -75,12 +75,11 @@ fn main() -> Result<(), anyhow::Error> { let host = cpal::default_host(); // Set up the input device and stream with the default input config. - let device = match opt.device.as_str() { - "default" => host.default_input_device(), - "default-output" => host.default_output_device(), - device => host - .input_devices()? - .find(|dev| dev.id().is_ok_and(|id| id.to_string() == device)), + let device = if let Some(device) = opt.device { + let id = &device.parse().expect("failed to parse input device id"); + host.device_by_id(id) + } else { + host.default_input_device() } .expect("failed to find input device"); From 5aaf7e95c127e76578c6dfca67a453b7154dc1ee Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 14 Nov 2025 20:54:46 -0500 Subject: [PATCH 03/14] refactor: normalize DeviceId variant names and drop IOS * Update Display and FromStr implementations to match renamed variants (Wasapi, Asio, Alsa). This is consistent with the naming conventions used in other parts of the codebase. * Remove the IOS variant and change iOS device id to return CoreAudio. Rationale: iOS uses CoreAudio as its underlying audio API, so having a separate IOS variant is redundant and may cause confusion. iOS and macOS should never occur in the same build. * Improve DeviceId parse error message format. --- src/host/coreaudio/ios/mod.rs | 2 +- src/lib.rs | 27 +++++++++++---------------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index e972faa45..e7c4d91c3 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -75,7 +75,7 @@ impl Device { } fn id(&self) -> Result { - Ok(DeviceId::IOS("default".to_string())) + Ok(DeviceId::CoreAudio("default".to_string())) } fn supported_input_configs( diff --git a/src/lib.rs b/src/lib.rs index d80fd5d83..a4443d6cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -226,35 +226,32 @@ pub type FrameCount = u32; /// A stable identifier for an audio device across all supported platforms. /// /// Device IDs should remain stable across application restarts and can be serialized using `Display`/`FromStr`. - #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum DeviceId { CoreAudio(String), - WASAPI(String), - ASIO(String), - ALSA(String), + Wasapi(String), + Asio(String), + Alsa(String), AAudio(i32), Jack(String), WebAudio(String), WebAudioWorklet(String), Emscripten(String), - IOS(String), Null, } impl std::fmt::Display for DeviceId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - DeviceId::WASAPI(guid) => write!(f, "wasapi:{}", guid), - DeviceId::ASIO(guid) => write!(f, "asio:{}", guid), + DeviceId::Wasapi(guid) => write!(f, "wasapi:{}", guid), + DeviceId::Asio(guid) => write!(f, "asio:{}", guid), DeviceId::CoreAudio(uid) => write!(f, "coreaudio:{}", uid), - DeviceId::ALSA(pcm_id) => write!(f, "alsa:{}", pcm_id), + DeviceId::Alsa(pcm_id) => write!(f, "alsa:{}", pcm_id), DeviceId::AAudio(id) => write!(f, "aaudio:{}", id), DeviceId::Jack(name) => write!(f, "jack:{}", name), DeviceId::WebAudio(default) => write!(f, "webaudio:{}", default), DeviceId::WebAudioWorklet(default) => write!(f, "webaudioworklet:{}", default), DeviceId::Emscripten(default) => write!(f, "emscripten:{}", default), - DeviceId::IOS(default) => write!(f, "ios:{}", default), DeviceId::Null => write!(f, "null:null"), } } @@ -264,19 +261,18 @@ impl std::str::FromStr for DeviceId { type Err = DeviceIdError; fn from_str(s: &str) -> Result { - let (platform, data) = s.split_once(':').ok_or( - DeviceIdError::BackendSpecific { + let (platform, data) = s.split_once(':').ok_or(DeviceIdError::BackendSpecific { err: BackendSpecificError { - description: format!("Failed to parse device id from: {}\nCheck if format matches Audio_API:DeviceId", s) + description: format!("Failed to parse device id from: {s}\nCheck if format matches \"host:device_id\"") } } )?; match platform { - "wasapi" => Ok(DeviceId::WASAPI(data.to_string())), - "asio" => Ok(DeviceId::ASIO(data.to_string())), + "wasapi" => Ok(DeviceId::Wasapi(data.to_string())), + "asio" => Ok(DeviceId::Asio(data.to_string())), "coreaudio" => Ok(DeviceId::CoreAudio(data.to_string())), - "alsa" => Ok(DeviceId::ALSA(data.to_string())), + "alsa" => Ok(DeviceId::Alsa(data.to_string())), "aaudio" => { let id = data.parse().map_err(|_| DeviceIdError::BackendSpecific { err: BackendSpecificError { @@ -288,7 +284,6 @@ impl std::str::FromStr for DeviceId { "jack" => Ok(DeviceId::Jack(data.to_string())), "webaudio" => Ok(DeviceId::WebAudio(data.to_string())), "emscripten" => Ok(DeviceId::Emscripten(data.to_string())), - "ios" => Ok(DeviceId::IOS(data.to_string())), "null" => Ok(DeviceId::Null), &_ => todo!("implement DeviceId::FromStr for {platform}"), } From 146e6be9641a57a68d820e3e0f73e13211e70f53 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 16 Nov 2025 16:18:25 -0500 Subject: [PATCH 04/14] refactor: add DeviceDescription struct and builder * Replace device description string returns with a DeviceDescription type across hosts and traits. * Add enums for device type, interface, and direction, builder and helper conversion/parsing functions, and re-export the new module from the crate. * Provide a default name() shim that uses description().name() for compatibility. --- examples/custom.rs | 9 +- src/device_description.rs | 390 +++++++++++++++++++++++++++++ src/host/aaudio/mod.rs | 125 +++++++-- src/host/alsa/mod.rs | 60 +++-- src/host/asio/device.rs | 21 +- src/host/asio/mod.rs | 6 +- src/host/coreaudio/ios/mod.rs | 34 +-- src/host/coreaudio/macos/device.rs | 88 +++++-- src/host/custom/mod.rs | 14 +- src/host/emscripten/mod.rs | 25 +- src/host/jack/device.rs | 28 ++- src/host/null/mod.rs | 14 +- src/host/wasapi/device.rs | 265 ++++++++++++++++---- src/host/webaudio/mod.rs | 28 +-- src/lib.rs | 4 + src/platform/mod.rs | 2 +- src/samples_formats.rs | 2 + src/traits.rs | 25 +- 18 files changed, 939 insertions(+), 201 deletions(-) create mode 100644 src/device_description.rs diff --git a/examples/custom.rs b/examples/custom.rs index e4b43f3f0..e0ef98747 100644 --- a/examples/custom.rs +++ b/examples/custom.rs @@ -3,7 +3,10 @@ use std::sync::{ Arc, }; -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + DeviceDescription, DeviceDescriptionBuilder, +}; use cpal::{FromSample, Sample}; #[allow(dead_code)] @@ -56,8 +59,8 @@ impl DeviceTrait for MyDevice { Ok(String::from("custom")) } - fn description(&self) -> Result { - Ok(String::from("Custom Device")) + fn description(&self) -> Result { + Ok(DeviceDescriptionBuilder::new("Custom Device".to_string()).build()) } fn id(&self) -> Result { diff --git a/src/device_description.rs b/src/device_description.rs new file mode 100644 index 000000000..7d0c5aea6 --- /dev/null +++ b/src/device_description.rs @@ -0,0 +1,390 @@ +use std::fmt; + +use crate::ChannelCount; + +/// Describes an audio device with structured metadata. +/// +/// This type provides structured information about an audio device beyond just its name. +/// Availability depends on the host implementation and platform capabilities. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceDescription { + /// Human-readable device name + name: String, + + /// Device manufacturer or vendor name + manufacturer: Option, + + /// Driver name + driver: Option, + + /// Categorization of device type + device_type: DeviceType, + + /// Connection/interface type + interface_type: InterfaceType, + + /// Direction: input, output, or duplex + direction: DeviceDirection, + + /// Physical address or connection identifier + address: Option, + + /// Additional description lines with non-structured, detailed information + extended: Vec, +} + +/// Categorization of audio device types. +/// +/// This describes the kind of audio device (speaker, microphone, headset, etc.) +/// regardless of how it connects to the system. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +#[non_exhaustive] +pub enum DeviceType { + /// Speaker (built-in or external) + Speaker, + + /// Microphone (built-in or external) + Microphone, + + /// Headphones (audio output only) + Headphones, + + /// Headset (combined headphones + microphone) + Headset, + + /// Earpiece (phone-style speaker, typically for voice calls) + Earpiece, + + /// Handset (telephone-style handset with speaker and microphone) + Handset, + + /// Hearing aid device + HearingAid, + + /// Docking station audio + Dock, + + /// Radio/TV tuner + Tuner, + + /// Virtual/loopback device (software audio routing) + Virtual, + + /// Unknown or unclassified device type + #[default] + Unknown, +} + +/// How the device connects to the system (interface/connection type). +/// +/// This describes the physical or logical connection between the audio device +/// and the computer system. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +#[non_exhaustive] +pub enum InterfaceType { + /// Built-in to the system (integrated audio chipset) + BuiltIn, + + /// USB connection + Usb, + + /// Bluetooth wireless connection + Bluetooth, + + /// PCI or PCIe card (internal sound card) + Pci, + + /// FireWire connection (IEEE 1394) + FireWire, + + /// Thunderbolt connection + Thunderbolt, + + /// HDMI connection + Hdmi, + + /// Line-level analog connection (line in/out, aux) + Line, + + /// S/PDIF digital audio interface + Spdif, + + /// Network connection (Dante, AVB, AirPlay, IP audio, etc.) + Network, + + /// Virtual/loopback connection (software audio routing, not physical hardware) + Virtual, + + /// DisplayPort audio + DisplayPort, + + /// Aggregate device (combines multiple devices) + Aggregate, + + /// Unknown connection type + #[default] + Unknown, +} + +/// The direction(s) that a device supports. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +#[non_exhaustive] +pub enum DeviceDirection { + /// Input only (capture/recording) + Input, + + /// Output only (playback/rendering) + Output, + + /// Both input and output + Duplex, + + /// Direction unknown or not yet determined + #[default] + Unknown, +} + +impl DeviceDescription { + /// Returns the human-readable device name. + /// + /// This is always available and is the primary user-facing identifier. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the manufacturer/vendor name if available. + pub fn manufacturer(&self) -> Option<&str> { + self.manufacturer.as_deref() + } + + /// Returns the driver name if available. + pub fn driver(&self) -> Option<&str> { + self.driver.as_deref() + } + + /// Returns the device type categorization. + pub fn device_type(&self) -> DeviceType { + self.device_type + } + + /// Returns the interface/connection type. + pub fn interface_type(&self) -> InterfaceType { + self.interface_type + } + + /// Returns the device direction. + pub fn direction(&self) -> DeviceDirection { + self.direction + } + + /// Returns whether this device supports audio input (capture). + /// + /// This is a convenience method that checks if direction is `Input` or `Duplex`. + pub fn supports_input(&self) -> bool { + matches!( + self.direction, + DeviceDirection::Input | DeviceDirection::Duplex + ) + } + + /// Returns whether this device supports audio output (playback). + /// + /// This is a convenience method that checks if direction is `Output` or `Duplex`. + pub fn supports_output(&self) -> bool { + matches!( + self.direction, + DeviceDirection::Output | DeviceDirection::Duplex + ) + } + + /// Returns the physical address or connection identifier if available. + pub fn address(&self) -> Option<&str> { + self.address.as_deref() + } + + /// Returns additional description lines with detailed information. + pub fn extended(&self) -> &[String] { + &self.extended + } +} + +impl fmt::Display for DeviceDescription { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name)?; + + if let Some(mfr) = &self.manufacturer { + write!(f, " ({})", mfr)?; + } + + if self.device_type != DeviceType::Unknown { + write!(f, " [{}]", self.device_type)?; + } + + if self.interface_type != InterfaceType::Unknown { + write!(f, " via {}", self.interface_type)?; + } + + Ok(()) + } +} + +impl fmt::Display for DeviceType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DeviceType::Speaker => write!(f, "Speaker"), + DeviceType::Microphone => write!(f, "Microphone"), + DeviceType::Headphones => write!(f, "Headphones"), + DeviceType::Headset => write!(f, "Headset"), + DeviceType::Earpiece => write!(f, "Earpiece"), + DeviceType::Handset => write!(f, "Handset"), + DeviceType::HearingAid => write!(f, "Hearing Aid"), + DeviceType::Dock => write!(f, "Dock"), + DeviceType::Tuner => write!(f, "Tuner"), + DeviceType::Virtual => write!(f, "Virtual"), + _ => write!(f, "Unknown"), + } + } +} + +impl fmt::Display for InterfaceType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InterfaceType::BuiltIn => write!(f, "Built-in"), + InterfaceType::Usb => write!(f, "USB"), + InterfaceType::Bluetooth => write!(f, "Bluetooth"), + InterfaceType::Pci => write!(f, "PCI"), + InterfaceType::FireWire => write!(f, "FireWire"), + InterfaceType::Thunderbolt => write!(f, "Thunderbolt"), + InterfaceType::Hdmi => write!(f, "HDMI"), + InterfaceType::Line => write!(f, "Line"), + InterfaceType::Spdif => write!(f, "S/PDIF"), + InterfaceType::Network => write!(f, "Network"), + InterfaceType::Virtual => write!(f, "Virtual"), + InterfaceType::DisplayPort => write!(f, "DisplayPort"), + InterfaceType::Aggregate => write!(f, "Aggregate"), + _ => write!(f, "Unknown"), + } + } +} + +impl fmt::Display for DeviceDirection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DeviceDirection::Input => write!(f, "Input"), + DeviceDirection::Output => write!(f, "Output"), + DeviceDirection::Duplex => write!(f, "Duplex"), + _ => write!(f, "Unknown"), + } + } +} + +/// Builder for constructing a `DeviceDescription`. +/// +/// This is primarily used by host implementations and custom hosts +/// to gradually build up device descriptions with available metadata. +#[derive(Debug, Clone)] +pub struct DeviceDescriptionBuilder { + name: String, + manufacturer: Option, + driver: Option, + device_type: DeviceType, + interface_type: InterfaceType, + direction: DeviceDirection, + address: Option, + extended: Vec, +} + +impl DeviceDescriptionBuilder { + /// Creates a new builder with the device name (required). + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + manufacturer: None, + driver: None, + device_type: DeviceType::Unknown, + interface_type: InterfaceType::Unknown, + direction: DeviceDirection::Unknown, + address: None, + extended: Vec::new(), + } + } + + /// Sets the manufacturer name. + pub fn manufacturer(mut self, manufacturer: impl Into) -> Self { + self.manufacturer = Some(manufacturer.into()); + self + } + + /// Sets the driver name. + pub fn driver(mut self, driver: impl Into) -> Self { + self.driver = Some(driver.into()); + self + } + + /// Sets the device type. + pub fn device_type(mut self, device_type: DeviceType) -> Self { + self.device_type = device_type; + self + } + + /// Sets the interface type. + pub fn interface_type(mut self, interface_type: InterfaceType) -> Self { + self.interface_type = interface_type; + self + } + + /// Sets the device direction. + pub fn direction(mut self, direction: DeviceDirection) -> Self { + self.direction = direction; + self + } + + /// Sets the physical address. + pub fn address(mut self, address: impl Into) -> Self { + self.address = Some(address.into()); + self + } + + /// Sets the description lines. + pub fn extended(mut self, lines: Vec) -> Self { + self.extended = lines; + self + } + + /// Adds a single description line. + pub fn add_extended_line(mut self, line: impl Into) -> Self { + self.extended.push(line.into()); + self + } + + /// Builds the [`DeviceDescription`]. + pub fn build(self) -> DeviceDescription { + DeviceDescription { + name: self.name, + manufacturer: self.manufacturer, + driver: self.driver, + device_type: self.device_type, + interface_type: self.interface_type, + direction: self.direction, + address: self.address, + extended: self.extended, + } + } +} + +/// Determines device direction from input/output channel counts. +#[allow(dead_code)] +pub(crate) fn direction_from_counts( + input_channels: Option, + output_channels: Option, +) -> DeviceDirection { + let has_input = input_channels.map(|n| n > 0).unwrap_or(false); + let has_output = output_channels.map(|n| n > 0).unwrap_or(false); + + match (has_input, has_output) { + (true, true) => DeviceDirection::Duplex, + (true, false) => DeviceDirection::Input, + (false, true) => DeviceDirection::Output, + (false, false) => DeviceDirection::Unknown, + } +} diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 9720140ed..752893bd6 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -11,17 +11,98 @@ use java_interface::{AudioDeviceDirection, AudioDeviceInfo, AudioManager}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceId, - DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, InputStreamTimestamp, - OutputCallbackInfo, OutputStreamTimestamp, PauseStreamError, PlayStreamError, SampleFormat, - SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, - SupportedStreamConfigRange, SupportedStreamConfigsError, + BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, + DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, DeviceId, DeviceIdError, + DeviceNameError, DeviceType, DevicesError, InputCallbackInfo, InputStreamTimestamp, + InterfaceType, OutputCallbackInfo, OutputStreamTimestamp, PauseStreamError, PlayStreamError, + SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; mod convert; mod java_interface; use self::ndk::audio::AudioStream; +use java_interface::AudioDeviceType as AndroidDeviceType; + +impl From for DeviceDirection { + fn from(direction: AudioDeviceDirection) -> Self { + match direction { + AudioDeviceDirection::Input => DeviceDirection::Input, + AudioDeviceDirection::Output => DeviceDirection::Output, + AudioDeviceDirection::InputOutput => DeviceDirection::Duplex, + _ => DeviceDirection::Unknown, + } + } +} + +impl From for DeviceType { + fn from(device_type: AndroidDeviceType) -> Self { + match device_type { + AndroidDeviceType::BuiltinSpeaker + | AndroidDeviceType::BuiltinSpeakerSafe + | AndroidDeviceType::BleSpeaker => DeviceType::Speaker, + + AndroidDeviceType::BuiltinMic => DeviceType::Microphone, + + AndroidDeviceType::WiredHeadphones => DeviceType::Headphones, + + AndroidDeviceType::WiredHeadset + | AndroidDeviceType::UsbHeadset + | AndroidDeviceType::BleHeadset + | AndroidDeviceType::BluetoothSCO => DeviceType::Headset, + + AndroidDeviceType::BuiltinEarpiece => DeviceType::Earpiece, + + AndroidDeviceType::HearingAid => DeviceType::HearingAid, + + AndroidDeviceType::Dock => DeviceType::Dock, + + AndroidDeviceType::Fm | AndroidDeviceType::FmTuner | AndroidDeviceType::TvTuner => { + DeviceType::Tuner + } + + AndroidDeviceType::RemoteSubmix => DeviceType::Virtual, + + _ => DeviceType::Unknown, + } + } +} + +impl From for InterfaceType { + fn from(device_type: AndroidDeviceType) -> Self { + match device_type { + AndroidDeviceType::UsbDevice + | AndroidDeviceType::UsbAccessory + | AndroidDeviceType::UsbHeadset => InterfaceType::Usb, + + AndroidDeviceType::BluetoothA2DP + | AndroidDeviceType::BluetoothSCO + | AndroidDeviceType::BleHeadset + | AndroidDeviceType::BleSpeaker + | AndroidDeviceType::BleBroadcast => InterfaceType::Bluetooth, + + AndroidDeviceType::Hdmi | AndroidDeviceType::HdmiArc | AndroidDeviceType::HdmiEarc => { + InterfaceType::Hdmi + } + + AndroidDeviceType::LineAnalog + | AndroidDeviceType::LineDigital + | AndroidDeviceType::AuxLine => InterfaceType::Line, + + AndroidDeviceType::BuiltinEarpiece + | AndroidDeviceType::BuiltinMic + | AndroidDeviceType::BuiltinSpeaker + | AndroidDeviceType::BuiltinSpeakerSafe => InterfaceType::BuiltIn, + + AndroidDeviceType::Ip => InterfaceType::Network, + + AndroidDeviceType::RemoteSubmix => InterfaceType::Virtual, + + _ => InterfaceType::Unknown, + } + } +} // constants from android.media.AudioFormat const CHANNEL_OUT_MONO: i32 = 4; @@ -303,29 +384,23 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn name(&self) -> Result { - let name = match &self.0 { - None => "default".to_owned(), + fn description(&self) -> Result { + match &self.0 { + None => Ok(DeviceDescriptionBuilder::new("Default Device".to_string()).build()), Some(info) => { - if info.address.is_empty() { - format!("{}:{:?}", info.product_name, info.device_type) - } else { - format!( - "{}:{:?}:{}", - info.product_name, info.device_type, info.address - ) + let mut builder = DeviceDescriptionBuilder::new(info.product_name.clone()) + .device_type(info.device_type.into()) + .interface_type(info.device_type.into()) + .direction(info.direction.into()); + + // Add address if not empty + if !info.address.is_empty() { + builder = builder.address(info.address.clone()); } - } - }; - Ok(name) - } - fn description(&self) -> Result { - let description = match &self.0 { - None => "Default Device".to_owned(), - Some(info) => format!("{}, {:?}", info.product_name, info.device_type), - }; - Ok(description) + Ok(builder.build()) + } + } } fn id(&self) -> Result { diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 0ef19ac48..f1d301210 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -16,12 +16,31 @@ pub use self::enumerate::{default_input_device, default_output_device, Devices}; use crate::{ traits::{DeviceTrait, HostTrait, StreamTrait}, BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, - DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, DevicesError, FrameCount, - InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, Sample, SampleFormat, - SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, + DefaultStreamConfigError, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, + DeviceId, DeviceIdError, DeviceNameError, DevicesError, FrameCount, InputCallbackInfo, + OutputCallbackInfo, PauseStreamError, PlayStreamError, Sample, SampleFormat, SampleRate, + StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, I24, U24, }; +impl From for DeviceDirection { + fn from(direction: alsa::Direction) -> Self { + match direction { + alsa::Direction::Capture => DeviceDirection::Input, + alsa::Direction::Playback => DeviceDirection::Output, + } + } +} + +/// Parses ALSA multi-line description into separate lines. +fn parse_alsa_description(description: &str) -> Vec { + description + .lines() + .map(|line| line.trim().to_string()) + .filter(|line| !line.is_empty()) + .collect() +} + // ALSA Buffer Size Behavior // ========================= // @@ -115,11 +134,12 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; + // ALSA overrides name() to return pcm_id directly instead of from description fn name(&self) -> Result { Device::name(self) } - fn description(&self) -> Result { + fn description(&self) -> Result { Device::description(self) } @@ -287,6 +307,7 @@ impl DeviceHandles { pub struct Device { pcm_id: String, desc: Option, + direction: Option, handles: Arc>, } @@ -386,19 +407,30 @@ impl Device { Ok(self.pcm_id.clone()) } - fn description(&self) -> Result { - self.desc - .clone() - .ok_or(DeviceNameError::BackendSpecific { - err: BackendSpecificError { - description: String::from("no description for this device"), - }, - }) - .map(|desc| desc.replace('\n', ", ")) + fn description(&self) -> Result { + let name = self + .desc + .as_ref() + .and_then(|desc| desc.lines().next()) + .unwrap_or(&self.pcm_id) + .to_string(); + + let mut builder = DeviceDescriptionBuilder::new(name).driver(self.pcm_id.clone()); + + if let Some(ref desc) = self.desc { + let lines = parse_alsa_description(desc); + builder = builder.extended(lines); + } + + if let Some(dir) = self.direction { + builder = builder.direction(dir.into()); + } + + Ok(builder.build()) } fn id(&self) -> Result { - Ok(DeviceId::ALSA(self.pcm_id.clone())) + Ok(DeviceId::Alsa(self.pcm_id.clone())) } fn supported_configs( diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index d5e5f9acd..356ff9ba5 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -3,7 +3,11 @@ pub type SupportedOutputConfigs = std::vec::IntoIter use super::sys; use crate::BackendSpecificError; +use crate::ChannelCount; use crate::DefaultStreamConfigError; +use crate::DeviceDescription; +use crate::DeviceDescriptionBuilder; +use crate::DeviceDirection; use crate::DeviceId; use crate::DeviceIdError; use crate::DeviceNameError; @@ -14,6 +18,7 @@ use crate::SupportedBufferSize; use crate::SupportedStreamConfig; use crate::SupportedStreamConfigRange; use crate::SupportedStreamConfigsError; + use std::hash::{Hash, Hasher}; use std::sync::atomic::AtomicI32; use std::sync::{Arc, Mutex}; @@ -52,12 +57,18 @@ impl Hash for Device { } impl Device { - pub fn name(&self) -> Result { - self.description() - } + fn description(&self) -> Result { + let driver_name = self.driver.name().to_string(); + + let direction = crate::device_description::direction_from_counts( + self.driver.channels().ok().map(|c| c.ins as ChannelCount), + self.driver.channels().ok().map(|c| c.outs as ChannelCount), + ); - fn description(&self) -> Result { - Ok(self.driver.name().to_string()) + Ok(DeviceDescriptionBuilder::new(driver_name.clone()) + .driver(driver_name) + .direction(direction) + .build()) } fn id(&self) -> Result { diff --git a/src/host/asio/mod.rs b/src/host/asio/mod.rs index 9328cc2a6..1bcc6cc5a 100644 --- a/src/host/asio/mod.rs +++ b/src/host/asio/mod.rs @@ -58,11 +58,7 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn name(&self) -> Result { - Device::name(self) - } - - fn description(&self) -> Result { + fn description(&self) -> Result { Device::description(self) } diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index e7c4d91c3..b90337a56 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -14,11 +14,11 @@ use super::{asbd_from_config, frames_to_duration, host_time_to_stream_instant}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceId, - DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, - PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, - SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, + BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, + DefaultStreamConfigError, DeviceDescription, DeviceDescriptionBuilder, DeviceId, DeviceIdError, + DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, + PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; use self::enumerate::{ @@ -66,12 +66,20 @@ impl HostTrait for Host { } impl Device { - fn name(&self) -> Result { - self.description() - } + fn description(&self) -> Result { + // Query AVAudioSession to determine actual input/output availability + // SAFETY: AVAudioSession::sharedInstance() returns the global audio session singleton + let direction = unsafe { + let audio_session = AVAudioSession::sharedInstance(); + let input_channels = Some(audio_session.inputNumberOfChannels() as ChannelCount); + let output_channels = Some(audio_session.outputNumberOfChannels() as ChannelCount); + + crate::device_description::direction_from_counts(input_channels, output_channels) + }; - fn description(&self) -> Result { - Ok("Default Device".to_owned()) + Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) + .direction(direction) + .build()) } fn id(&self) -> Result { @@ -112,11 +120,7 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn name(&self) -> Result { - Device::name(self) - } - - fn description(&self) -> Result { + fn description(&self) -> Result { Device::description(self) } diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index c96d9082a..f93b12ff7 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -19,13 +19,14 @@ use objc2_audio_toolbox::{ use objc2_core_audio::kAudioDevicePropertyDeviceUID; use objc2_core_audio::kAudioObjectPropertyElementMain; use objc2_core_audio::{ - kAudioDevicePropertyAvailableNominalSampleRates, kAudioDevicePropertyBufferFrameSize, - kAudioDevicePropertyBufferFrameSizeRange, kAudioDevicePropertyNominalSampleRate, - kAudioDevicePropertyStreamConfiguration, kAudioDevicePropertyStreamFormat, - kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyScopeInput, kAudioObjectPropertyScopeOutput, AudioDeviceID, - AudioObjectGetPropertyData, AudioObjectGetPropertyDataSize, AudioObjectID, - AudioObjectPropertyAddress, AudioObjectPropertyScope, AudioObjectSetPropertyData, + kAudioAggregateDeviceClassID, kAudioDevicePropertyAvailableNominalSampleRates, + kAudioDevicePropertyBufferFrameSize, kAudioDevicePropertyBufferFrameSizeRange, + kAudioDevicePropertyNominalSampleRate, kAudioDevicePropertyStreamConfiguration, + kAudioDevicePropertyStreamFormat, kAudioObjectPropertyClass, kAudioObjectPropertyElementMaster, + kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyScopeInput, + kAudioObjectPropertyScopeOutput, AudioClassID, AudioDeviceID, AudioObjectGetPropertyData, + AudioObjectGetPropertyDataSize, AudioObjectID, AudioObjectPropertyAddress, + AudioObjectPropertyScope, AudioObjectSetPropertyData, }; use objc2_core_audio_types::{ AudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioValueRange, @@ -37,7 +38,7 @@ pub use super::enumerate::{ default_input_device, default_output_device, SupportedInputConfigs, SupportedOutputConfigs, }; use std::fmt; -use std::mem::{self}; +use std::mem::{self, size_of}; use std::ptr::{null, NonNull}; use std::slice; use std::sync::mpsc::{channel, RecvTimeoutError}; @@ -262,11 +263,7 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn name(&self) -> Result { - Device::name(self) - } - - fn description(&self) -> Result { + fn description(&self) -> Result { Device::description(self) } @@ -360,16 +357,65 @@ impl Device { Self { audio_device_id } } - fn name(&self) -> Result { - self.description() + /// Checks if this device is an aggregate device. + /// + /// Aggregate devices combine multiple physical devices into a single logical device. + fn is_aggregate_device(&self) -> bool { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioObjectPropertyClass, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + + let mut class_id: AudioClassID = 0; + let data_size = size_of::() as u32; + + // SAFETY: AudioObjectGetPropertyData is documented to write an AudioClassID + // for kAudioObjectPropertyClass. We check the status before using the value. + let status = unsafe { + AudioObjectGetPropertyData( + self.audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&data_size), + NonNull::from(&mut class_id).cast(), + ) + }; + + // If successful, check if it's an aggregate device + status == 0 && class_id == kAudioAggregateDeviceClassID } - fn description(&self) -> Result { - get_device_name(self.audio_device_id).map_err(|err| DeviceNameError::BackendSpecific { - err: BackendSpecificError { - description: err.to_string(), - }, - }) + fn description(&self) -> Result { + let name = get_device_name(self.audio_device_id).map_err(|err| { + DeviceNameError::BackendSpecific { + err: BackendSpecificError { + description: err.to_string(), + }, + } + })?; + + let input_configs = self + .supported_input_configs() + .map(|configs| configs.count() as ChannelCount) + .ok(); + let output_configs = self + .supported_output_configs() + .map(|configs| configs.count() as ChannelCount) + .ok(); + + let direction = + crate::device_description::direction_from_counts(input_configs, output_configs); + + let mut builder = crate::DeviceDescriptionBuilder::new(name).direction(direction); + + // Check if this is an aggregate device + if self.is_aggregate_device() { + builder = builder.interface_type(crate::InterfaceType::Aggregate); + } + + Ok(builder.build()) } fn id(&self) -> Result { diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs index 6241264b5..49528dfa2 100644 --- a/src/host/custom/mod.rs +++ b/src/host/custom/mod.rs @@ -1,9 +1,9 @@ use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, - DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, - SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, + BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, DeviceId, DeviceIdError, + DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, + PlayStreamError, SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, + SupportedStreamConfigRange, SupportedStreamConfigsError, }; use core::time::Duration; @@ -141,7 +141,7 @@ type OutputCallback = Box Result; - fn description(&self) -> Result; + fn description(&self) -> Result; fn id(&self) -> Result; fn supports_input(&self) -> bool; fn supports_output(&self) -> bool; @@ -220,7 +220,7 @@ where ::name(self) } - fn description(&self) -> Result { + fn description(&self) -> Result { ::description(self) } @@ -342,7 +342,7 @@ impl DeviceTrait for Device { self.0.name() } - fn description(&self) -> Result { + fn description(&self) -> Result { self.0.description() } diff --git a/src/host/emscripten/mod.rs b/src/host/emscripten/mod.rs index 675561fe8..d531805d4 100644 --- a/src/host/emscripten/mod.rs +++ b/src/host/emscripten/mod.rs @@ -7,10 +7,11 @@ use web_sys::AudioContext; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, - DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, - PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, - SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, + BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, + DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError, DevicesError, + InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, + SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, SupportedStreamConfigsError, }; // The emscripten backend currently works by instantiating an `AudioContext` object per `Stream`. @@ -72,12 +73,10 @@ impl Devices { } impl Device { - fn name(&self) -> Result { - self.description() - } - - fn description(&self) -> Result { - Ok("Default Device".to_owned()) + fn description(&self) -> Result { + Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) + .direction(crate::DeviceDirection::Output) + .build()) } fn id(&self) -> Result { @@ -153,11 +152,7 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn name(&self) -> Result { - Device::name(self) - } - - fn description(&self) -> Result { + fn description(&self) -> Result { Device::description(self) } diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 2e2a5ea48..11ad84c7f 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -1,9 +1,10 @@ use crate::traits::DeviceTrait; use crate::{ - BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceId, - DeviceIdError, DeviceNameError, InputCallbackInfo, OutputCallbackInfo, SampleFormat, - SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, - SupportedStreamConfigRange, SupportedStreamConfigsError, + BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, + DeviceDescriptionBuilder, DeviceDirection, DeviceId, DeviceIdError, DeviceNameError, + InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError, + SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, + SupportedStreamConfigsError, }; use std::hash::{Hash, Hasher}; use std::time::Duration; @@ -11,6 +12,15 @@ use std::time::Duration; use super::stream::Stream; use super::JACK_SAMPLE_FORMAT; +impl From for DeviceDirection { + fn from(device_type: DeviceType) -> Self { + match device_type { + DeviceType::InputDevice => DeviceDirection::Input, + DeviceType::OutputDevice => DeviceDirection::Output, + } + } +} + pub type SupportedInputConfigs = std::vec::IntoIter; pub type SupportedOutputConfigs = std::vec::IntoIter; @@ -160,12 +170,10 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn name(&self) -> Result { - self.description() - } - - fn description(&self) -> Result { - Ok(self.name.clone()) + fn description(&self) -> Result { + Ok(DeviceDescriptionBuilder::new(self.name.clone()) + .direction(self.device_type.into()) + .build()) } fn id(&self) -> Result { diff --git a/src/host/null/mod.rs b/src/host/null/mod.rs index 338f42cb7..ff2249130 100644 --- a/src/host/null/mod.rs +++ b/src/host/null/mod.rs @@ -2,10 +2,10 @@ use std::time::Duration; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, - DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, - SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, + BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, DeviceDescriptionBuilder, + DeviceId, DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, + PauseStreamError, PlayStreamError, SampleFormat, StreamConfig, StreamError, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; #[derive(Default)] @@ -47,11 +47,11 @@ impl DeviceTrait for Device { type Stream = Stream; fn name(&self) -> Result { - Ok("null".to_owned()) + Ok("null".to_string()) } - fn description(&self) -> Result { - Ok("Null Device".to_owned()) + fn description(&self) -> Result { + Ok(DeviceDescriptionBuilder::new("Null Device".to_string()).build()) } fn id(&self) -> Result { diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 75fada55b..716ecbedb 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -1,10 +1,22 @@ -use crate::FrameCount; use crate::{ - BackendSpecificError, BufferSize, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, - DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, - StreamConfig, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, COMMON_SAMPLE_RATES, + BackendSpecificError, BufferSize, Data, DefaultStreamConfigError, DeviceDescription, + DeviceDescriptionBuilder, DeviceDirection, DeviceId, DeviceIdError, DeviceNameError, + DeviceType, DevicesError, FrameCount, InputCallbackInfo, InterfaceType, OutputCallbackInfo, + SampleFormat, SampleRate, StreamConfig, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, SupportedStreamConfigsError, COMMON_SAMPLE_RATES, }; + +impl From for DeviceDirection { + fn from(data_flow: Audio::EDataFlow) -> Self { + if data_flow == Audio::eCapture { + DeviceDirection::Input + } else if data_flow == Audio::eRender { + DeviceDirection::Output + } else { + DeviceDirection::Unknown + } + } +} use std::ffi::OsString; use std::fmt; use std::mem; @@ -26,7 +38,7 @@ use windows::Win32::Media::{Audio, KernelStreaming, Multimedia}; use windows::Win32::System::Com; use windows::Win32::System::Com::{StructuredStorage, STGM_READ}; use windows::Win32::System::Threading; -use windows::Win32::System::Variant::VT_LPWSTR; +use windows::Win32::System::Variant::{VT_LPWSTR, VT_UI4}; use super::stream::{AudioClientFlow, Stream, StreamInner}; use crate::{traits::DeviceTrait, BuildStreamError, StreamError}; @@ -34,6 +46,22 @@ use crate::{traits::DeviceTrait, BuildStreamError, StreamError}; pub type SupportedInputConfigs = std::vec::IntoIter; pub type SupportedOutputConfigs = std::vec::IntoIter; +// PKEY_AudioEndpoint properties not yet in windows-rs + +/// PKEY_AudioEndpoint_FormFactor (PID 0) - VT_UI4 containing EndpointFormFactor enum +const PKEY_AUDIOENDPOINT_FORMFACTOR: StructuredStorage::PROPERTYKEY = + StructuredStorage::PROPERTYKEY { + fmtid: GUID::from_u128(0x1da5d803_d492_4edd_8c23_e0c0ffee7f0e), + pid: 0, + }; + +/// PKEY_AudioEndpoint_JackSubType (PID 8) - VT_LPWSTR containing KS node type GUID +const PKEY_AUDIOENDPOINT_JACKSUBTYPE: StructuredStorage::PROPERTYKEY = + StructuredStorage::PROPERTYKEY { + fmtid: GUID::from_u128(0x1da5d803_d492_4edd_8c23_e0c0ffee7f0e), + pid: 8, + }; + /// Wrapper because of that stupid decision to remove `Send` and `Sync` from raw pointers. #[derive(Clone)] struct IAudioClientWrapper(Audio::IAudioClient); @@ -54,11 +82,7 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn name(&self) -> Result { - Device::name(self) - } - - fn description(&self) -> Result { + fn description(&self) -> Result { Device::description(self) } @@ -277,12 +301,53 @@ unsafe fn format_from_waveformatex_ptr( unsafe impl Send for Device {} unsafe impl Sync for Device {} -impl Device { - pub fn name(&self) -> Result { - self.description() +/// Maps PKEY_AudioEndpoint_JackSubType GUID to InterfaceType. +/// +/// The JackSubType property contains a KS node type GUID string from Ksmedia.h +/// that specifies the physical connector type. +fn jacksubtype_to_interface_type(guid_str: &str) -> Option { + let guid_upper = guid_str.to_uppercase(); + let typ = match guid_upper.as_str() { + "{D9E55EA0-0C89-4692-84FF-EB3C4B0D172F}" => InterfaceType::Hdmi, + "{E47E4031-3EA6-418D-8F9B-B73843CCB2AD}" => InterfaceType::DisplayPort, + "{DFF21CE1-F70F-11D0-B917-00A0C9223196}" => InterfaceType::Spdif, + _ => return None, + }; + + Some(typ) +} + +/// Maps WASAPI FormFactor values to DeviceType and optionally InterfaceType. +fn form_factor_to_types(form_factor: u32) -> (crate::DeviceType, Option) { + match form_factor { + 0 => (DeviceType::Unknown, Some(InterfaceType::Network)), // RemoteNetworkDevice + 1 => (DeviceType::Speaker, None), // Speakers + 2 => (DeviceType::Unknown, Some(InterfaceType::Line)), // LineLevel + 3 => (DeviceType::Headphones, None), // Headphones + 4 => (DeviceType::Microphone, None), // Microphone + 5 => (DeviceType::Headset, None), // Headset + 6 => (DeviceType::Handset, None), // Handset + 7 => (DeviceType::Unknown, None), // UnknownDigitalPassthrough + 8 => (DeviceType::Unknown, Some(InterfaceType::Spdif)), // SPDIF + 9 => (DeviceType::Unknown, Some(InterfaceType::Hdmi)), // DigitalAudioDisplayDevice + _ => (DeviceType::Unknown, None), // UnknownFormFactor or future values } +} + +/// Maps WASAPI EnumeratorName to InterfaceType. +fn enumerator_to_interface_type(enumerator: &str) -> Option { + let typ = match enumerator.to_uppercase().as_str() { + "HDAUDIO" => InterfaceType::BuiltIn, + "USB" => InterfaceType::Usb, + "BTHENUM" => InterfaceType::Bluetooth, + "MMDEVAPI" | "SW" => InterfaceType::Virtual, + _ => return None, + }; + Some(typ) +} - pub fn description(&self) -> Result { +impl Device { + pub fn description(&self) -> Result { unsafe { // Open the device's property store. let property_store = self @@ -290,47 +355,90 @@ impl Device { .OpenPropertyStore(STGM_READ) .expect("could not open property store"); - // Get the endpoint's friendly-name property. - let mut property_value = property_store - .GetValue(&Properties::DEVPKEY_Device_FriendlyName as *const _ as *const _) - .map_err(|err| { - let description = - format!("failed to retrieve name from property store: {}", err); - let err = BackendSpecificError { description }; - DeviceNameError::from(err) + // Query all available properties + let friendly_name = get_property_string( + &property_store, + &Properties::DEVPKEY_Device_FriendlyName as *const _ as *const _, + ); + + let device_desc = get_property_string( + &property_store, + &Properties::DEVPKEY_Device_DeviceDesc as *const _ as *const _, + ); + + let interface_name = get_property_string( + &property_store, + &Properties::DEVPKEY_DeviceInterface_FriendlyName as *const _ as *const _, + ); + + let enumerator_name = get_property_string( + &property_store, + &Properties::DEVPKEY_Device_EnumeratorName as *const _ as *const _, + ); + + let form_factor = get_property_u32( + &property_store, + &PKEY_AUDIOENDPOINT_FORMFACTOR as *const _ as *const _, + ); + + let jack_subtype = get_property_string( + &property_store, + &PKEY_AUDIOENDPOINT_JACKSUBTYPE as *const _ as *const _, + ); + + // Prefer DeviceDesc for name, fall back to FriendlyName + let name = device_desc + .clone() + .or(friendly_name.clone()) + .ok_or_else(|| DeviceNameError::BackendSpecific { + err: BackendSpecificError { + description: "failed to retrieve device name".to_string(), + }, })?; - let prop_variant = &property_value.Anonymous.Anonymous; + // Get direction from data flow (eCapture = Input, eRender = Output) + let direction = self.data_flow().into(); - // Read the friendly-name from the union data field, expecting a *const u16. - if prop_variant.vt != VT_LPWSTR { - let description = format!( - "property store produced invalid data: {:?}", - prop_variant.vt - ); - let err = BackendSpecificError { description }; - return Err(err.into()); + // Determine device_type and initial interface_type from FormFactor + let (device_type, mut interface_type) = form_factor + .map(form_factor_to_types) + .unwrap_or((crate::DeviceType::Unknown, None)); + + // Override interface_type from EnumeratorName if available + if let Some(ref enumerator) = enumerator_name { + if let Some(itype) = enumerator_to_interface_type(enumerator) { + interface_type = Some(itype); + } } - 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; + // JackSubType has highest priority for interface_type + if let Some(ref jack_guid) = jack_subtype { + if let Some(itype) = jacksubtype_to_interface_type(jack_guid) { + interface_type = Some(itype); + } } - // Create the utf16 slice and convert it into a string. - let name_slice = slice::from_raw_parts(ptr_utf16, len as usize); - let name_os_string: OsString = OsStringExt::from_wide(name_slice); - let name_string = match name_os_string.into_string() { - Ok(string) => string, - Err(os_string) => os_string.to_string_lossy().into(), - }; + let mut builder = DeviceDescriptionBuilder::new(name) + .direction(direction) + .device_type(device_type); + + if let Some(itype) = interface_type { + builder = builder.interface_type(itype); + } - // Clean up the property. - StructuredStorage::PropVariantClear(&mut property_value).ok(); + // Add interface name to driver field if available + if let Some(iface_name) = interface_name { + builder = builder.driver(iface_name); + } + + // Add FriendlyName to extended if different from the name we used + if let Some(fname) = friendly_name { + if device_desc.is_some() && Some(&fname) != device_desc.as_ref() { + builder = builder.add_extended_line(fname); + } + } - Ok(name_string) + Ok(builder.build()) } } @@ -338,7 +446,7 @@ impl Device { unsafe { match self.device.GetId() { Ok(pwstr) => match pwstr.to_string() { - Ok(id_str) => Ok(DeviceId::WASAPI(id_str)), + Ok(id_str) => Ok(DeviceId::Wasapi(id_str)), Err(e) => Err(DeviceIdError::BackendSpecific { err: BackendSpecificError { description: format!("Failed to convert device ID to string: {}", e), @@ -881,6 +989,67 @@ fn get_enumerator() -> &'static Enumerator { }) } +// Helper function to query a DWORD property from a WASAPI device property store +unsafe fn get_property_u32( + property_store: &Com::IPropertyStore, + property_key: *const Com::StructuredStorage::PROPERTYKEY, +) -> Option { + let mut property_value = property_store.GetValue(property_key).ok()?; + let prop_variant = &property_value.Anonymous.Anonymous; + + // Check if it's a UI4 (unsigned 32-bit integer) + if prop_variant.vt != VT_UI4 { + return None; + } + + let value = *(&prop_variant.Anonymous as *const _ as *const u32); + + // Clean up the property + StructuredStorage::PropVariantClear(&mut property_value).ok(); + + Some(value) +} + +// Helper function to query a string property from a WASAPI device property store +unsafe fn get_property_string( + property_store: &Com::IPropertyStore, + property_key: *const Com::StructuredStorage::PROPERTYKEY, +) -> Option { + let mut property_value = property_store.GetValue(property_key).ok()?; + let prop_variant = &property_value.Anonymous.Anonymous; + + // Read the string from the union data field, expecting a *const u16. + if prop_variant.vt != VT_LPWSTR { + return None; + } + let ptr_utf16 = *(&prop_variant.Anonymous as *const _ as *const *const u16); + + // Find the length of the null-terminated string with a safety limit + const MAX_STRING_LEN: usize = 32768; // 32K characters should be more than enough + let mut len = 0; + while len < MAX_STRING_LEN && *ptr_utf16.add(len) != 0 { + len += 1; + } + + // If we hit the limit, the string is likely malformed (not null-terminated) + if len >= MAX_STRING_LEN { + return None; + } + + // Create the utf16 slice and convert it into a string. + let string_slice = slice::from_raw_parts(ptr_utf16, len); + let os_string: OsString = OsStringExt::from_wide(string_slice); + let result = match os_string.into_string() { + Ok(string) => Some(string), + Err(os_string) => Some(os_string.to_string_lossy().into()), + }; + + // Clean up the property. + StructuredStorage::PropVariantClear(&mut property_value).ok(); + + result +} + /// Send/Sync wrapper around `IMMDeviceEnumerator`. struct Enumerator(Audio::IMMDeviceEnumerator); diff --git a/src/host/webaudio/mod.rs b/src/host/webaudio/mod.rs index 5bef545a0..6bb479546 100644 --- a/src/host/webaudio/mod.rs +++ b/src/host/webaudio/mod.rs @@ -7,11 +7,11 @@ use self::wasm_bindgen::JsCast; use self::web_sys::{AudioContext, AudioContextOptions}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceId, - DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, - PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, - SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, + BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, + DeviceDescription, DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError, + DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, + SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; use std::ops::DerefMut; use std::sync::{Arc, Mutex, RwLock}; @@ -88,12 +88,10 @@ impl Devices { } impl Device { - fn name(&self) -> Result { - self.description() - } - - fn description(&self) -> Result { - Ok("Default Device".to_owned()) + fn description(&self) -> Result { + Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) + .direction(crate::DeviceDirection::Output) + .build()) } fn id(&self) -> Result { @@ -149,11 +147,7 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn name(&self) -> Result { - Device::name(self) - } - - fn description(&self) -> Result { + fn description(&self) -> Result { Device::description(self) } @@ -231,7 +225,7 @@ impl DeviceTrait for Device { let data_callback = Arc::new(Mutex::new(Box::new(data_callback))); // Create the WebAudio stream. - let mut stream_opts = AudioContextOptions::new(); + let stream_opts = AudioContextOptions::new(); stream_opts.set_sample_rate(config.sample_rate.0 as f32); let ctx = AudioContext::new_with_context_options(&stream_opts).map_err( |err| -> BuildStreamError { diff --git a/src/lib.rs b/src/lib.rs index a4443d6cd..fa0a5c7c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -164,6 +164,9 @@ extern crate js_sys; #[cfg(target_os = "emscripten")] extern crate web_sys; +pub use device_description::{ + DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, DeviceType, InterfaceType, +}; pub use error::*; pub use platform::{ available_hosts, default_host, host_from_id, Device, Devices, Host, HostId, Stream, @@ -176,6 +179,7 @@ use std::time::Duration; #[cfg(target_os = "emscripten")] use wasm_bindgen::prelude::*; +pub mod device_description; mod error; mod host; pub mod platform; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 4acf598b1..2b770e310 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -318,7 +318,7 @@ macro_rules! impl_platform_host { } } - fn description(&self) -> Result { + fn description(&self) -> Result { match self.0 { $( $(#[cfg($feat)])? diff --git a/src/samples_formats.rs b/src/samples_formats.rs index 6f39fb084..355a48f9a 100644 --- a/src/samples_formats.rs +++ b/src/samples_formats.rs @@ -48,11 +48,13 @@ pub enum SampleFormat { /// `U24` with a valid range of '0..16777216' with `1 << 23 == 8388608` being the origin U24, + /// `u32` with a valid range of `u32::MIN..=u32::MAX` with `1 << 31` being the origin. U32, /// `U48` with a valid range of '0..(1 << 48)' with `1 << 47` being the origin // U48, + /// `u64` with a valid range of `u64::MIN..=u64::MAX` with `1 << 63` being the origin. U64, diff --git a/src/traits.rs b/src/traits.rs index b2e53b7e3..57c90781e 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -3,10 +3,10 @@ use std::time::Duration; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, - DevicesError, InputCallbackInfo, InputDevices, OutputCallbackInfo, OutputDevices, - PauseStreamError, PlayStreamError, SampleFormat, SizedSample, StreamConfig, StreamError, - SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, + BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, DeviceId, DeviceIdError, + DeviceNameError, DevicesError, InputCallbackInfo, InputDevices, OutputCallbackInfo, + OutputDevices, PauseStreamError, PlayStreamError, SampleFormat, SizedSample, StreamConfig, + StreamError, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; /// A [`Host`] provides access to the available audio devices on the system. @@ -98,12 +98,21 @@ pub trait DeviceTrait { /// The human-readable name of the device. #[deprecated( since = "0.17.0", - note = "Use `id()` to get a unique identifier for the device, or `description()` for a human-readable description." + note = "Use `id()` to get a unique identifier for the device, or `description().name()` for a human-readable description." )] - fn name(&self) -> Result; + fn name(&self) -> Result { + self.description().map(|desc| desc.name().to_string()) + } - /// The human-readable description of the device. - fn description(&self) -> Result; + /// Structured description of the device with metadata. + /// + /// This returns a [`DeviceDescription`] containing structured information about the device, + /// including name, manufacturer (if available), device type, bus type, and other + /// platform-specific metadata. + /// + /// For simple string representation, use `device.description().to_string()` or + /// `device.description().name()`. + fn description(&self) -> Result; /// The ID of the device. /// From 1d274f1420e7ee7400288979ea0b78ce7b7190b0 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 16 Nov 2025 16:37:05 -0500 Subject: [PATCH 05/14] feat: add WebAudioWorklet DeviceId and description --- src/host/alsa/enumerate.rs | 2 ++ src/host/wasapi/device.rs | 28 ++++++++++++++-------------- src/host/web_audio_worklet/mod.rs | 16 +++++++++------- src/lib.rs | 1 + 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/host/alsa/enumerate.rs b/src/host/alsa/enumerate.rs index 48a2e1b58..3ae312047 100644 --- a/src/host/alsa/enumerate.rs +++ b/src/host/alsa/enumerate.rs @@ -54,6 +54,7 @@ pub fn default_device() -> Device { Device { pcm_id: "default".to_string(), desc: Some("Default Audio Device".to_string()), + direction: None, handles: Arc::new(Mutex::new(Default::default())), } } @@ -77,6 +78,7 @@ impl TryFrom for Device { Ok(Self { pcm_id: pcm_id.to_owned(), desc: hint.desc, + direction: None, handles: Arc::new(Mutex::new(Default::default())), }) } diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 716ecbedb..e24c532a0 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -33,12 +33,14 @@ use windows::core::Interface; use windows::core::GUID; use windows::Win32::Devices::Properties; use windows::Win32::Foundation; +use windows::Win32::Foundation::PROPERTYKEY; use windows::Win32::Media::Audio::IAudioRenderClient; use windows::Win32::Media::{Audio, KernelStreaming, Multimedia}; use windows::Win32::System::Com; use windows::Win32::System::Com::{StructuredStorage, STGM_READ}; use windows::Win32::System::Threading; use windows::Win32::System::Variant::{VT_LPWSTR, VT_UI4}; +use windows::Win32::UI::Shell::PropertiesSystem::IPropertyStore; use super::stream::{AudioClientFlow, Stream, StreamInner}; use crate::{traits::DeviceTrait, BuildStreamError, StreamError}; @@ -49,18 +51,16 @@ pub type SupportedOutputConfigs = std::vec::IntoIter // PKEY_AudioEndpoint properties not yet in windows-rs /// PKEY_AudioEndpoint_FormFactor (PID 0) - VT_UI4 containing EndpointFormFactor enum -const PKEY_AUDIOENDPOINT_FORMFACTOR: StructuredStorage::PROPERTYKEY = - StructuredStorage::PROPERTYKEY { - fmtid: GUID::from_u128(0x1da5d803_d492_4edd_8c23_e0c0ffee7f0e), - pid: 0, - }; +const PKEY_AUDIOENDPOINT_FORMFACTOR: PROPERTYKEY = PROPERTYKEY { + fmtid: GUID::from_u128(0x1da5d803_d492_4edd_8c23_e0c0ffee7f0e), + pid: 0, +}; /// PKEY_AudioEndpoint_JackSubType (PID 8) - VT_LPWSTR containing KS node type GUID -const PKEY_AUDIOENDPOINT_JACKSUBTYPE: StructuredStorage::PROPERTYKEY = - StructuredStorage::PROPERTYKEY { - fmtid: GUID::from_u128(0x1da5d803_d492_4edd_8c23_e0c0ffee7f0e), - pid: 8, - }; +const PKEY_AUDIOENDPOINT_JACKSUBTYPE: PROPERTYKEY = PROPERTYKEY { + fmtid: GUID::from_u128(0x1da5d803_d492_4edd_8c23_e0c0ffee7f0e), + pid: 8, +}; /// Wrapper because of that stupid decision to remove `Send` and `Sync` from raw pointers. #[derive(Clone)] @@ -991,8 +991,8 @@ fn get_enumerator() -> &'static Enumerator { // Helper function to query a DWORD property from a WASAPI device property store unsafe fn get_property_u32( - property_store: &Com::IPropertyStore, - property_key: *const Com::StructuredStorage::PROPERTYKEY, + property_store: &IPropertyStore, + property_key: *const PROPERTYKEY, ) -> Option { let mut property_value = property_store.GetValue(property_key).ok()?; let prop_variant = &property_value.Anonymous.Anonymous; @@ -1012,8 +1012,8 @@ unsafe fn get_property_u32( // Helper function to query a string property from a WASAPI device property store unsafe fn get_property_string( - property_store: &Com::IPropertyStore, - property_key: *const Com::StructuredStorage::PROPERTYKEY, + property_store: &IPropertyStore, + property_key: *const PROPERTYKEY, ) -> Option { let mut property_value = property_store.GetValue(property_key).ok()?; let prop_variant = &property_value.Anonymous.Anonymous; diff --git a/src/host/web_audio_worklet/mod.rs b/src/host/web_audio_worklet/mod.rs index c0f124b3f..0695424b2 100644 --- a/src/host/web_audio_worklet/mod.rs +++ b/src/host/web_audio_worklet/mod.rs @@ -6,11 +6,11 @@ use wasm_bindgen::prelude::*; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BackendSpecificError, BuildStreamError, ChannelCount, Data, DefaultStreamConfigError, DeviceId, - DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, - PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, - SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, + BackendSpecificError, BuildStreamError, ChannelCount, Data, DefaultStreamConfigError, + DeviceDescription, DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError, + DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, + SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; use std::time::Duration; @@ -94,8 +94,10 @@ impl DeviceTrait for Device { type Stream = Stream; #[inline] - fn name(&self) -> Result { - Ok("Default Device".to_owned()) + fn description(&self) -> Result { + Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) + .direction(crate::DeviceDirection::Output) + .build()) } #[inline] diff --git a/src/lib.rs b/src/lib.rs index fa0a5c7c5..40ccb2617 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -287,6 +287,7 @@ impl std::str::FromStr for DeviceId { } "jack" => Ok(DeviceId::Jack(data.to_string())), "webaudio" => Ok(DeviceId::WebAudio(data.to_string())), + "webaudioworklet" => Ok(DeviceId::WebAudioWorklet(data.to_string())), "emscripten" => Ok(DeviceId::Emscripten(data.to_string())), "null" => Ok(DeviceId::Null), &_ => todo!("implement DeviceId::FromStr for {platform}"), From 5404e9f08bdb1fce436cbbb2d26590065d03e090 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 16 Nov 2025 16:49:23 -0500 Subject: [PATCH 06/14] refactor: use shared COMMON_SAMPLE_RATES for ALSA * Expose COMMON_SAMPLE_RATES as pub(crate) and use it in the ALSA backend instead of a private RATES array. * Remove the Windows-only cfg and add a few additional sample rates (12000, 24000, 352800). --- src/host/alsa/mod.rs | 11 +++-------- src/host/asio/device.rs | 6 +++--- src/host/asio/mod.rs | 7 ++++--- src/host/jack/device.rs | 36 ++++++++++-------------------------- src/host/wasapi/device.rs | 2 +- src/lib.rs | 11 ++++++----- 6 files changed, 27 insertions(+), 46 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index f1d301210..cc2fe3d23 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -514,15 +514,10 @@ impl Device { let sample_rates = if min_rate == max_rate || hw_params.test_rate(min_rate + 1).is_ok() { vec![(min_rate, max_rate)] } else { - const RATES: [libc::c_uint; 19] = [ - 5512, 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, - 96000, 176400, 192000, 352800, 384000, 705600, 768000, - ]; - let mut rates = Vec::new(); - for &rate in RATES.iter() { - if hw_params.test_rate(rate).is_ok() { - rates.push((rate, rate)); + for &sample_rate in crate::COMMON_SAMPLE_RATES.iter() { + if hw_params.test_rate(sample_rate.0).is_ok() { + rates.push((sample_rate.0, sample_rate.0)); } } diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index 356ff9ba5..ff884a476 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -57,7 +57,7 @@ impl Hash for Device { } impl Device { - fn description(&self) -> Result { + pub fn description(&self) -> Result { let driver_name = self.driver.name().to_string(); let direction = crate::device_description::direction_from_counts( @@ -71,8 +71,8 @@ impl Device { .build()) } - fn id(&self) -> Result { - Ok(DeviceId::ASIO(self.driver.name().to_string())) + pub fn id(&self) -> Result { + Ok(DeviceId::Asio(self.driver.name().to_string())) } /// Gets the supported input configs. diff --git a/src/host/asio/mod.rs b/src/host/asio/mod.rs index 1bcc6cc5a..9530f2568 100644 --- a/src/host/asio/mod.rs +++ b/src/host/asio/mod.rs @@ -2,9 +2,10 @@ extern crate asio_sys as sys; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, - DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, - SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigsError, + BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, DeviceId, DeviceIdError, + DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, + PlayStreamError, SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, + SupportedStreamConfigsError, }; pub use self::device::{Device, Devices, SupportedInputConfigs, SupportedOutputConfigs}; diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 11ad84c7f..d7c70817b 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -12,34 +12,18 @@ use std::time::Duration; use super::stream::Stream; use super::JACK_SAMPLE_FORMAT; -impl From for DeviceDirection { - fn from(device_type: DeviceType) -> Self { - match device_type { - DeviceType::InputDevice => DeviceDirection::Input, - DeviceType::OutputDevice => DeviceDirection::Output, - } - } -} - pub type SupportedInputConfigs = std::vec::IntoIter; pub type SupportedOutputConfigs = std::vec::IntoIter; const DEFAULT_NUM_CHANNELS: u16 = 2; const DEFAULT_SUPPORTED_CHANNELS: [u16; 10] = [1, 2, 4, 6, 8, 16, 24, 32, 48, 64]; -/// If a device is for input or output. -/// Until we have duplex stream support JACK clients and CPAL devices for JACK will be either input or output. -#[derive(Clone, Debug)] -pub enum DeviceType { - InputDevice, - OutputDevice, -} #[derive(Clone, Debug)] pub struct Device { name: String, sample_rate: SampleRate, buffer_size: SupportedBufferSize, - device_type: DeviceType, + direction: DeviceDirection, start_server_automatically: bool, connect_ports_automatically: bool, } @@ -49,7 +33,7 @@ impl Device { name: String, connect_ports_automatically: bool, start_server_automatically: bool, - device_type: DeviceType, + direction: DeviceDirection, ) -> Result { // ClientOptions are bit flags that you can set with the constants provided let client_options = super::get_client_options(start_server_automatically); @@ -66,7 +50,7 @@ impl Device { min: client.buffer_size(), max: client.buffer_size(), }, - device_type, + direction, start_server_automatically, connect_ports_automatically, }), @@ -88,7 +72,7 @@ impl Device { output_client_name, connect_ports_automatically, start_server_automatically, - DeviceType::OutputDevice, + DeviceDirection::Output, ) } @@ -102,7 +86,7 @@ impl Device { input_client_name, connect_ports_automatically, start_server_automatically, - DeviceType::InputDevice, + DeviceDirection::Input, ) } @@ -143,11 +127,11 @@ impl Device { } pub fn is_input(&self) -> bool { - matches!(self.device_type, DeviceType::InputDevice) + self.description().supports_input() } pub fn is_output(&self) -> bool { - matches!(self.device_type, DeviceType::OutputDevice) + self.description().supports_output() } /// Validate buffer size if Fixed is specified. This is necessary because JACK buffer size @@ -172,7 +156,7 @@ impl DeviceTrait for Device { fn description(&self) -> Result { Ok(DeviceDescriptionBuilder::new(self.name.clone()) - .direction(self.device_type.into()) + .direction(self.direction) .build()) } @@ -218,7 +202,7 @@ impl DeviceTrait for Device { D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { - if let DeviceType::OutputDevice = &self.device_type { + if matches!(self.direction, DeviceDirection::Output) { // Trying to create an input stream from an output device return Err(BuildStreamError::StreamConfigNotSupported); } @@ -259,7 +243,7 @@ impl DeviceTrait for Device { D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { - if let DeviceType::InputDevice = &self.device_type { + if matches!(self.direction, DeviceDirection::Input) { // Trying to create an output stream from an input device return Err(BuildStreamError::StreamConfigNotSupported); } diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index e24c532a0..27deaea21 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -947,7 +947,7 @@ impl fmt::Debug for Device { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("Device") .field("device", &self.device) - .field("name", &self.name()) + .field("description", &self.description()) .finish() } } diff --git a/src/lib.rs b/src/lib.rs index 40ccb2617..3856740f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -938,16 +938,16 @@ impl From for StreamConfig { } // If a backend does not provide an API for retrieving supported formats, we query it with a bunch -// of commonly used rates. This is always the case for wasapi and is sometimes the case for alsa. -// -// If a rate you desire is missing from this list, feel free to add it! -#[cfg(target_os = "windows")] -const COMMON_SAMPLE_RATES: &[SampleRate] = &[ +// of commonly used rates. This is always the case for WASAPI and is sometimes the case for ALSA. +#[allow(dead_code)] +pub(crate) const COMMON_SAMPLE_RATES: &[SampleRate] = &[ SampleRate(5512), SampleRate(8000), SampleRate(11025), + SampleRate(12000), SampleRate(16000), SampleRate(22050), + SampleRate(24000), SampleRate(32000), SampleRate(44100), SampleRate(48000), @@ -956,6 +956,7 @@ const COMMON_SAMPLE_RATES: &[SampleRate] = &[ SampleRate(96000), SampleRate(176400), SampleRate(192000), + SampleRate(352800), SampleRate(384000), ]; From 2057ddba270f2d8ffea08383c5685ee21a99abe9 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 16 Nov 2025 17:33:05 -0500 Subject: [PATCH 07/14] fix: use DeviceDirection for input/output checks --- src/host/jack/device.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index d7c70817b..5bbf1e93f 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -127,11 +127,11 @@ impl Device { } pub fn is_input(&self) -> bool { - self.description().supports_input() + matches!(self.direction, DeviceDirection::Input) } pub fn is_output(&self) -> bool { - self.description().supports_output() + matches!(self.direction, DeviceDirection::Output) } /// Validate buffer size if Fixed is specified. This is necessary because JACK buffer size @@ -202,7 +202,7 @@ impl DeviceTrait for Device { D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { - if matches!(self.direction, DeviceDirection::Output) { + if self.is_output() { // Trying to create an input stream from an output device return Err(BuildStreamError::StreamConfigNotSupported); } @@ -243,7 +243,7 @@ impl DeviceTrait for Device { D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { - if matches!(self.direction, DeviceDirection::Input) { + if self.is_input() { // Trying to create an output stream from an input device return Err(BuildStreamError::StreamConfigNotSupported); } From 04dfcbb077fc3d299b90f0d59f226ddb6dbb9561 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 26 Nov 2025 21:31:21 +0100 Subject: [PATCH 08/14] refactor: unify device direction handling across AAudio * Remove the Android-specific AudioDeviceDirection and use the core DeviceDirection throughout the AAudio Java interface. * Add android_device_flags to convert DeviceDirection to AudioManager flags, and direction_from_caps to derive DeviceDirection from isSource/isSink. * Refactor direction_from_counts to call direction_from_caps and update device requests and exports accordingly. --- src/device_description.rs | 18 +++++---- src/host/aaudio/java_interface.rs | 2 +- src/host/aaudio/java_interface/definitions.rs | 39 ++++--------------- .../aaudio/java_interface/devices_info.rs | 19 ++++----- src/host/aaudio/mod.rs | 15 +------ 5 files changed, 32 insertions(+), 61 deletions(-) diff --git a/src/device_description.rs b/src/device_description.rs index 7d0c5aea6..78fffb522 100644 --- a/src/device_description.rs +++ b/src/device_description.rs @@ -372,6 +372,16 @@ impl DeviceDescriptionBuilder { } } +/// Determines device direction from input/output capabilities. +pub(crate) fn direction_from_caps(has_input: bool, has_output: bool) -> DeviceDirection { + match (has_input, has_output) { + (true, true) => DeviceDirection::Duplex, + (true, false) => DeviceDirection::Input, + (false, true) => DeviceDirection::Output, + (false, false) => DeviceDirection::Unknown, + } +} + /// Determines device direction from input/output channel counts. #[allow(dead_code)] pub(crate) fn direction_from_counts( @@ -380,11 +390,5 @@ pub(crate) fn direction_from_counts( ) -> DeviceDirection { let has_input = input_channels.map(|n| n > 0).unwrap_or(false); let has_output = output_channels.map(|n| n > 0).unwrap_or(false); - - match (has_input, has_output) { - (true, true) => DeviceDirection::Duplex, - (true, false) => DeviceDirection::Input, - (false, true) => DeviceDirection::Output, - (false, false) => DeviceDirection::Unknown, - } + direction_from_caps(has_input, has_output) } diff --git a/src/host/aaudio/java_interface.rs b/src/host/aaudio/java_interface.rs index ab778517e..86f20b15a 100644 --- a/src/host/aaudio/java_interface.rs +++ b/src/host/aaudio/java_interface.rs @@ -5,4 +5,4 @@ mod devices_info; mod utils; pub use self::audio_features::*; -pub use self::definitions::*; +pub use self::definitions::{android_device_flags, AudioDeviceInfo, AudioDeviceType, AudioManager}; diff --git a/src/host/aaudio/java_interface/definitions.rs b/src/host/aaudio/java_interface/definitions.rs index b2e35c522..a10e643c3 100644 --- a/src/host/aaudio/java_interface/definitions.rs +++ b/src/host/aaudio/java_interface/definitions.rs @@ -1,6 +1,6 @@ use num_derive::FromPrimitive; -use crate::SampleFormat; +use crate::{DeviceDirection, SampleFormat}; pub(crate) struct Context; @@ -47,7 +47,7 @@ pub struct AudioDeviceInfo { /** * The device can be used for playback and/or capture */ - pub direction: AudioDeviceDirection, + pub direction: DeviceDirection, /** * Device address @@ -115,35 +115,12 @@ pub enum AudioDeviceType { Unsupported = -1, } -/** - * The direction of audio device - */ -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[repr(i32)] -pub enum AudioDeviceDirection { - Dumb = 0, - Input = AudioManager::GET_DEVICES_INPUTS, - Output = AudioManager::GET_DEVICES_OUTPUTS, - InputOutput = AudioManager::GET_DEVICES_ALL, -} - -impl AudioDeviceDirection { - pub fn new(is_input: bool, is_output: bool) -> Self { - use self::AudioDeviceDirection::*; - match (is_input, is_output) { - (true, true) => InputOutput, - (false, true) => Output, - (true, false) => Input, - _ => Dumb, - } - } - - pub fn is_input(&self) -> bool { - 0 < *self as i32 & AudioDeviceDirection::Input as i32 - } - - pub fn is_output(&self) -> bool { - 0 < *self as i32 & AudioDeviceDirection::Output as i32 +/// Converts DeviceDirection to Android AudioManager device flags. +pub(super) fn android_device_flags(direction: DeviceDirection) -> i32 { + match direction { + DeviceDirection::Input => AudioManager::GET_DEVICES_INPUTS, + DeviceDirection::Output => AudioManager::GET_DEVICES_OUTPUTS, + _ => AudioManager::GET_DEVICES_ALL, } } diff --git a/src/host/aaudio/java_interface/devices_info.rs b/src/host/aaudio/java_interface/devices_info.rs index 119f81338..7a9c16b33 100644 --- a/src/host/aaudio/java_interface/devices_info.rs +++ b/src/host/aaudio/java_interface/devices_info.rs @@ -1,22 +1,23 @@ use num_traits::FromPrimitive; -use crate::SampleFormat; +use crate::{DeviceDirection, SampleFormat}; use super::{ + android_device_flags, utils::{ call_method_no_args_ret_bool, call_method_no_args_ret_char_sequence, call_method_no_args_ret_int, call_method_no_args_ret_int_array, call_method_no_args_ret_string, get_context, get_devices, get_system_service, with_attached, JNIEnv, JObject, JResult, }, - AudioDeviceDirection, AudioDeviceInfo, AudioDeviceType, Context, + AudioDeviceInfo, AudioDeviceType, Context, }; impl AudioDeviceInfo { /** * Request audio devices using Android Java API */ - pub fn request(direction: AudioDeviceDirection) -> Result, String> { + pub fn request(direction: DeviceDirection) -> Result, String> { let context = get_context(); with_attached(context, |env, context| { @@ -40,11 +41,11 @@ impl AudioDeviceInfo { fn try_request_devices_info<'j>( env: &mut JNIEnv<'j>, context: &JObject<'j>, - direction: AudioDeviceDirection, + direction: DeviceDirection, ) -> JResult> { let audio_manager = get_system_service(env, context, Context::AUDIO_SERVICE)?; - let devices = get_devices(env, &audio_manager, direction as i32)?; + let devices = get_devices(env, &audio_manager, android_device_flags(direction))?; let length = env.get_array_length(&devices)?; @@ -60,10 +61,10 @@ fn try_request_devices_info<'j>( let device_type = FromPrimitive::from_i32(call_method_no_args_ret_int(env, &device, "getType")?) .unwrap_or(AudioDeviceType::Unsupported); - let direction = AudioDeviceDirection::new( - call_method_no_args_ret_bool(env, &device, "isSource")?, - call_method_no_args_ret_bool(env, &device, "isSink")?, - ); + + let is_source = call_method_no_args_ret_bool(env, &device, "isSource")?; + let is_sink = call_method_no_args_ret_bool(env, &device, "isSink")?; + let direction = crate::device_description::direction_from_caps(is_source, is_sink); let channel_counts = call_method_no_args_ret_int_array(env, &device, "getChannelCounts")?; let sample_rates = call_method_no_args_ret_int_array(env, &device, "getSampleRates")?; diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 752893bd6..37b065a4b 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -7,7 +7,7 @@ use std::vec::IntoIter as VecIntoIter; extern crate ndk; use convert::{stream_instant, to_stream_instant}; -use java_interface::{AudioDeviceDirection, AudioDeviceInfo, AudioManager}; +use java_interface::{AudioDeviceInfo, AudioManager}; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ @@ -25,17 +25,6 @@ mod java_interface; use self::ndk::audio::AudioStream; use java_interface::AudioDeviceType as AndroidDeviceType; -impl From for DeviceDirection { - fn from(direction: AudioDeviceDirection) -> Self { - match direction { - AudioDeviceDirection::Input => DeviceDirection::Input, - AudioDeviceDirection::Output => DeviceDirection::Output, - AudioDeviceDirection::InputOutput => DeviceDirection::Duplex, - _ => DeviceDirection::Unknown, - } - } -} - impl From for DeviceType { fn from(device_type: AndroidDeviceType) -> Self { match device_type { @@ -174,7 +163,7 @@ impl HostTrait for Host { } fn devices(&self) -> Result { - if let Ok(devices) = AudioDeviceInfo::request(AudioDeviceDirection::InputOutput) { + if let Ok(devices) = AudioDeviceInfo::request(DeviceDirection::Duplex) { Ok(devices .into_iter() .map(|d| Device(Some(d))) From 6d0e60df06dbfc045fb12ed9ab0f0997aa95df36 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 26 Nov 2025 21:31:54 +0100 Subject: [PATCH 09/14] refactor: suppress deprecation warnings for name method --- src/host/custom/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs index 49528dfa2..7bcc94347 100644 --- a/src/host/custom/mod.rs +++ b/src/host/custom/mod.rs @@ -216,6 +216,7 @@ where T::SupportedOutputConfigs: Clone + 'static, T::Stream: Send + Sync + 'static, { + #[allow(deprecated)] fn name(&self) -> Result { ::name(self) } From 6c3ce67307163a4225a50b4d8d07a502ca3b07c1 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 26 Nov 2025 21:51:25 +0100 Subject: [PATCH 10/14] refactor: retain legacy Device::name for AAudio for compatibility --- src/host/aaudio/mod.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 37b065a4b..50aa2a1db 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -373,6 +373,23 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; + fn name(&self) -> Result { + match &self.0 { + None => Ok("default".to_string()), + Some(info) => { + let name = if info.address.is_empty() { + format!("{}:{:?}", info.product_name, info.device_type) + } else { + format!( + "{}:{:?}:{}", + info.product_name, info.device_type, info.address + ) + }; + Ok(name) + } + } + } + fn description(&self) -> Result { match &self.0 { None => Ok(DeviceDescriptionBuilder::new("Default Device".to_string()).build()), From 5e52a9ccfc86564e22c0f9469a1b4740aa4326ac Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 26 Nov 2025 21:52:34 +0100 Subject: [PATCH 11/14] refactor: minor cleanups * Use Default for device description fields * Remove stale explanatory device option comments from examples/record_wav.rs --- examples/record_wav.rs | 2 -- src/device_description.rs | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/record_wav.rs b/examples/record_wav.rs index 1c86ce41f..6de0f33c4 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -13,8 +13,6 @@ use std::sync::{Arc, Mutex}; #[command(version, about = "CPAL record_wav example", long_about = None)] struct Opt { /// The audio device to use. - /// For the default microphone, use "default". - /// For recording system output, use "default-output". #[arg(short, long)] device: Option, diff --git a/src/device_description.rs b/src/device_description.rs index 78fffb522..60ff57620 100644 --- a/src/device_description.rs +++ b/src/device_description.rs @@ -301,9 +301,9 @@ impl DeviceDescriptionBuilder { name: name.into(), manufacturer: None, driver: None, - device_type: DeviceType::Unknown, - interface_type: InterfaceType::Unknown, - direction: DeviceDirection::Unknown, + device_type: DeviceType::default(), + interface_type: InterfaceType::default(), + direction: DeviceDirection::default(), address: None, extended: Vec::new(), } From c233675c074313b72c1e683d828f84ac27d74d46 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 26 Nov 2025 22:08:10 +0100 Subject: [PATCH 12/14] fix: aaudio code and CI * Remove the unnecessary --features asio flag from the Android clippy run. * Re-export all definitions from java_interface. * Remove an unneeded .into() when setting device direction in DeviceDescriptionBuilder. --- .github/workflows/cpal.yml | 2 +- src/host/aaudio/java_interface.rs | 2 +- src/host/aaudio/mod.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cpal.yml b/.github/workflows/cpal.yml index 211bc34f2..b6b566f66 100644 --- a/.github/workflows/cpal.yml +++ b/.github/workflows/cpal.yml @@ -56,7 +56,7 @@ jobs: - name: Run clippy run: cargo clippy --all --all-features - name: Run clippy for Android target - run: cargo clippy --all --features asio --target armv7-linux-androideabi + run: cargo clippy --all --target armv7-linux-androideabi cargo-publish: if: github.event_name == 'release' diff --git a/src/host/aaudio/java_interface.rs b/src/host/aaudio/java_interface.rs index 86f20b15a..ab778517e 100644 --- a/src/host/aaudio/java_interface.rs +++ b/src/host/aaudio/java_interface.rs @@ -5,4 +5,4 @@ mod devices_info; mod utils; pub use self::audio_features::*; -pub use self::definitions::{android_device_flags, AudioDeviceInfo, AudioDeviceType, AudioManager}; +pub use self::definitions::*; diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 50aa2a1db..73a6c6d62 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -397,7 +397,7 @@ impl DeviceTrait for Device { let mut builder = DeviceDescriptionBuilder::new(info.product_name.clone()) .device_type(info.device_type.into()) .interface_type(info.device_type.into()) - .direction(info.direction.into()); + .direction(info.direction); // Add address if not empty if !info.address.is_empty() { From c77da594ab3d15c4a7452dc1c50c4e15938206c2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 26 Nov 2025 22:52:37 +0100 Subject: [PATCH 13/14] refactor: simplify DeviceId to tuple struct with HostId Change DeviceId from a per-host enum with 10+ variants to a simple tuple struct containing (HostId, String). This eliminates significant duplication and centralizes platform-specific logic. Benefits: - Single source of truth for platform-specific host matching - Adding new hosts only requires updating impl_platform_host! macro - Cross-platform device ID deserialization fails gracefully with Err(UnsupportedPlatform) instead of panicking This enables users to store device preferences that gracefully degrade when loaded on different platforms, while maintaining type safety for hosts available on the current platform. --- CHANGELOG.md | 1 + src/host/aaudio/mod.rs | 8 ++-- src/host/alsa/mod.rs | 2 +- src/host/asio/device.rs | 5 ++- src/host/coreaudio/ios/mod.rs | 5 ++- src/host/coreaudio/macos/device.rs | 2 +- src/host/emscripten/mod.rs | 5 ++- src/host/jack/device.rs | 2 +- src/host/mod.rs | 12 ++++++ src/host/null/mod.rs | 2 +- src/host/wasapi/device.rs | 2 +- src/host/web_audio_worklet/mod.rs | 5 ++- src/host/webaudio/mod.rs | 5 ++- src/lib.rs | 67 +++++++----------------------- src/platform/mod.rs | 20 +++++++++ 15 files changed, 78 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba30f9fc0..3b6ba3d54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Add `DeviceTrait::id` method that returns a stable audio device ID. - Add `HostTrait::device_by_id` to select a device by its stable ID. +- Add `Display` and `FromStr` implementations for `HostId`. - Add support for custom `Host`s, `Device`s, and `Stream`s. - Add `Sample::bits_per_sample` method. - Update `audio_thread_priority` to 0.34. diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 73a6c6d62..924294026 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -410,11 +410,11 @@ impl DeviceTrait for Device { } fn id(&self) -> Result { - let id = match &self.0 { - None => DeviceId::AAudio(-1), // Default device - Some(info) => DeviceId::AAudio(info.id), + let device_str = match &self.0 { + None => "-1".to_string(), // Default device + Some(info) => info.id.to_string(), }; - Ok(id) + Ok(DeviceId(crate::platform::HostId::AAudio, device_str)) } fn supported_input_configs( diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index cc2fe3d23..f0c5ec64a 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -430,7 +430,7 @@ impl Device { } fn id(&self) -> Result { - Ok(DeviceId::Alsa(self.pcm_id.clone())) + Ok(DeviceId(crate::platform::HostId::Alsa, self.pcm_id.clone())) } fn supported_configs( diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index ff884a476..372ea40bb 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -72,7 +72,10 @@ impl Device { } pub fn id(&self) -> Result { - Ok(DeviceId::Asio(self.driver.name().to_string())) + Ok(DeviceId( + crate::platform::HostId::Asio, + self.driver.name().to_string(), + )) } /// Gets the supported input configs. diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index b90337a56..15d33d5e3 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -83,7 +83,10 @@ impl Device { } fn id(&self) -> Result { - Ok(DeviceId::CoreAudio("default".to_string())) + Ok(DeviceId( + crate::platform::HostId::CoreAudio, + "default".to_string(), + )) } fn supported_input_configs( diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index f93b12ff7..b7099db5e 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -447,7 +447,7 @@ impl Device { // We now check if the returned uid is non-null before use. if !uid.is_null() { let uid_string = unsafe { CFString::wrap_under_create_rule(uid).to_string() }; - Ok(DeviceId::CoreAudio(uid_string)) + Ok(DeviceId(crate::platform::HostId::CoreAudio, uid_string)) } else { Err(DeviceIdError::BackendSpecific { err: BackendSpecificError { diff --git a/src/host/emscripten/mod.rs b/src/host/emscripten/mod.rs index d531805d4..ea359b9ec 100644 --- a/src/host/emscripten/mod.rs +++ b/src/host/emscripten/mod.rs @@ -80,7 +80,10 @@ impl Device { } fn id(&self) -> Result { - Ok(DeviceId::Emscripten("default".to_string())) + Ok(DeviceId( + crate::platform::HostId::Emscripten, + "default".to_string(), + )) } fn supported_input_configs( diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 5bbf1e93f..0d04b6296 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -59,7 +59,7 @@ impl Device { } fn id(&self) -> Result { - Ok(DeviceId::Jack(self.name.clone())) + Ok(DeviceId(crate::platform::HostId::Jack, self.name.clone())) } pub fn default_output_device( diff --git a/src/host/mod.rs b/src/host/mod.rs index daaed0025..a27d7b1e3 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -37,6 +37,18 @@ pub(crate) mod webaudio; #[cfg(feature = "custom")] pub(crate) mod custom; +#[cfg(not(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "macos", + target_os = "ios", + target_os = "emscripten", + target_os = "android", + all(target_arch = "wasm32", feature = "wasm-bindgen"), +)))] pub(crate) mod null; /// Compile-time assertion that a type implements Send. diff --git a/src/host/null/mod.rs b/src/host/null/mod.rs index ff2249130..9f54d580e 100644 --- a/src/host/null/mod.rs +++ b/src/host/null/mod.rs @@ -55,7 +55,7 @@ impl DeviceTrait for Device { } fn id(&self) -> Result { - Ok(DeviceId::Null) + Ok(DeviceId(crate::platform::HostId::Null, String::new())) } fn supported_input_configs( diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 27deaea21..097e7daee 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -446,7 +446,7 @@ impl Device { unsafe { match self.device.GetId() { Ok(pwstr) => match pwstr.to_string() { - Ok(id_str) => Ok(DeviceId::Wasapi(id_str)), + Ok(id_str) => Ok(DeviceId(crate::platform::HostId::Wasapi, id_str)), Err(e) => Err(DeviceIdError::BackendSpecific { err: BackendSpecificError { description: format!("Failed to convert device ID to string: {}", e), diff --git a/src/host/web_audio_worklet/mod.rs b/src/host/web_audio_worklet/mod.rs index 0695424b2..1aabd7f48 100644 --- a/src/host/web_audio_worklet/mod.rs +++ b/src/host/web_audio_worklet/mod.rs @@ -102,7 +102,10 @@ impl DeviceTrait for Device { #[inline] fn id(&self) -> Result { - Ok(DeviceId::WebAudioWorklet("default".to_string())) + Ok(DeviceId( + crate::platform::HostId::WebAudioWorklet, + "default".to_string(), + )) } #[inline] diff --git a/src/host/webaudio/mod.rs b/src/host/webaudio/mod.rs index 6bb479546..95e914fbf 100644 --- a/src/host/webaudio/mod.rs +++ b/src/host/webaudio/mod.rs @@ -95,7 +95,10 @@ impl Device { } fn id(&self) -> Result { - Ok(DeviceId::WebAudio("default".to_string())) + Ok(DeviceId( + crate::platform::HostId::WebAudio, + "default".to_string(), + )) } fn supported_input_configs( diff --git a/src/lib.rs b/src/lib.rs index 3856740f9..5c8d19541 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -230,34 +230,14 @@ pub type FrameCount = u32; /// A stable identifier for an audio device across all supported platforms. /// /// Device IDs should remain stable across application restarts and can be serialized using `Display`/`FromStr`. +/// +/// A device ID consists of a [`HostId`] identifying the audio backend and a device-specific identifier string. #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum DeviceId { - CoreAudio(String), - Wasapi(String), - Asio(String), - Alsa(String), - AAudio(i32), - Jack(String), - WebAudio(String), - WebAudioWorklet(String), - Emscripten(String), - Null, -} +pub struct DeviceId(pub crate::platform::HostId, pub String); impl std::fmt::Display for DeviceId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DeviceId::Wasapi(guid) => write!(f, "wasapi:{}", guid), - DeviceId::Asio(guid) => write!(f, "asio:{}", guid), - DeviceId::CoreAudio(uid) => write!(f, "coreaudio:{}", uid), - DeviceId::Alsa(pcm_id) => write!(f, "alsa:{}", pcm_id), - DeviceId::AAudio(id) => write!(f, "aaudio:{}", id), - DeviceId::Jack(name) => write!(f, "jack:{}", name), - DeviceId::WebAudio(default) => write!(f, "webaudio:{}", default), - DeviceId::WebAudioWorklet(default) => write!(f, "webaudioworklet:{}", default), - DeviceId::Emscripten(default) => write!(f, "emscripten:{}", default), - DeviceId::Null => write!(f, "null:null"), - } + write!(f, "{}:{}", self.0, self.1) } } @@ -265,33 +245,18 @@ impl std::str::FromStr for DeviceId { type Err = DeviceIdError; fn from_str(s: &str) -> Result { - let (platform, data) = s.split_once(':').ok_or(DeviceIdError::BackendSpecific { - err: BackendSpecificError { - description: format!("Failed to parse device id from: {s}\nCheck if format matches \"host:device_id\"") - } - } - )?; - - match platform { - "wasapi" => Ok(DeviceId::Wasapi(data.to_string())), - "asio" => Ok(DeviceId::Asio(data.to_string())), - "coreaudio" => Ok(DeviceId::CoreAudio(data.to_string())), - "alsa" => Ok(DeviceId::Alsa(data.to_string())), - "aaudio" => { - let id = data.parse().map_err(|_| DeviceIdError::BackendSpecific { - err: BackendSpecificError { - description: format!("Failed to parse aaudio device id: {}", data), - }, - })?; - Ok(DeviceId::AAudio(id)) - } - "jack" => Ok(DeviceId::Jack(data.to_string())), - "webaudio" => Ok(DeviceId::WebAudio(data.to_string())), - "webaudioworklet" => Ok(DeviceId::WebAudioWorklet(data.to_string())), - "emscripten" => Ok(DeviceId::Emscripten(data.to_string())), - "null" => Ok(DeviceId::Null), - &_ => todo!("implement DeviceId::FromStr for {platform}"), - } + let (host_str, device_str) = s.split_once(':').ok_or(DeviceIdError::BackendSpecific { + err: BackendSpecificError { + description: format!( + "Failed to parse device id from: {s}\nCheck if format matches \"host:device_id\"" + ), + }, + })?; + + let host_id = crate::platform::HostId::from_str(host_str) + .map_err(|_| DeviceIdError::UnsupportedPlatform)?; + + Ok(DeviceId(host_id, device_str.to_string())) } } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 2b770e310..e22e94296 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -149,6 +149,26 @@ macro_rules! impl_platform_host { } } + impl std::fmt::Display for HostId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name().to_lowercase()) + } + } + + impl std::str::FromStr for HostId { + type Err = crate::HostUnavailable; + + fn from_str(s: &str) -> Result { + $( + $(#[cfg($feat)])? + if stringify!($HostVariant).eq_ignore_ascii_case(s) { + return Ok(HostId::$HostVariant); + } + )* + Err(crate::HostUnavailable) + } + } + impl Devices { /// Returns a reference to the underlying platform specific implementation of this /// `Devices`. From 59486505e7d9dfb72e8266709e811a45f1091457 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 28 Nov 2025 21:14:19 +0100 Subject: [PATCH 14/14] docs: add HostId string representations --- src/platform/mod.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/platform/mod.rs b/src/platform/mod.rs index e22e94296..f3962497c 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -81,6 +81,43 @@ macro_rules! impl_platform_host { pub struct SupportedOutputConfigs(SupportedOutputConfigsInner); /// Unique identifier for available hosts on the platform. + /// + /// Only the hosts supported by the current platform are available as enum variants. + /// For cross-platform code that needs to handle hosts from other platforms, + /// use the string representation via [`Display`]/[`FromStr`]. + /// + /// # Available Host Strings + /// + /// For cross-platform matching, these host strings are available: + /// + /// - `"aaudio"` - Android Audio + /// - `"alsa"` - Advanced Linux Sound Architecture + /// - `"asio"` - ASIO + /// - `"coreaudio"` - CoreAudio + /// - `"custom"` - Custom host (requires `custom` feature) + /// - `"emscripten"` - Emscripten + /// - `"jack"` - JACK Audio Connection Kit + /// - `"null"` - Null host + /// - `"wasapi"` - Windows Audio Session API + /// - `"webaudio"` - Web Audio API + /// - `"webaudioworklet"` - Web Audio Worklet + /// + /// # Cross-Platform Example + /// + /// ```ignore + /// use cpal::{DeviceId, HostId}; + /// + /// fn handle_device(device_id: DeviceId) { + /// // String matching works on all platforms + /// match device_id.0.to_string().as_str() { + /// "alsa" => println!("ALSA device"), + /// "coreaudio" => println!("CoreAudio device"), + /// "jack" => println!("JACK device"), + /// "wasapi" => println!("WASAPI device"), + /// _ => println!("Other host"), + /// } + /// } + /// ``` #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] pub enum HostId { $(