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/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/examples/beep.rs b/examples/beep.rs index 2a6872320..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,14 +63,14 @@ 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(|x| x.name().map(|y| y == opt.device).unwrap_or(false)) + host.default_output_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..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)] @@ -53,7 +56,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(DeviceDescriptionBuilder::new("Custom Device".to_string()).build()) } 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..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,24 +81,24 @@ 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.input_devices()? - .find(|x| x.name().map(|y| y == opt.input_device).unwrap_or(false)) + 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(|x| x.name().map(|y| y == opt.output_device).unwrap_or(false)) + host.default_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..6de0f33c4 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -13,10 +13,8 @@ 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, 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,16 +73,15 @@ 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(), - name => host - .input_devices()? - .find(|x| x.name().map(|y| y == name).unwrap_or(false)), + 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"); - 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/device_description.rs b/src/device_description.rs new file mode 100644 index 000000000..60ff57620 --- /dev/null +++ b/src/device_description.rs @@ -0,0 +1,394 @@ +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::default(), + interface_type: InterfaceType::default(), + direction: DeviceDirection::default(), + 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 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( + 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); + direction_from_caps(has_input, has_output) +} 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 614a40f5e..924294026 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -7,21 +7,91 @@ 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::{ - 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 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; @@ -93,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))) @@ -305,7 +375,7 @@ impl DeviceTrait for Device { fn name(&self) -> Result { match &self.0 { - None => Ok("default".to_owned()), + None => Ok("default".to_string()), Some(info) => { let name = if info.address.is_empty() { format!("{}:{:?}", info.product_name, info.device_type) @@ -320,31 +390,53 @@ impl DeviceTrait for Device { } } - fn id(&self) -> Result { + fn description(&self) -> Result { match &self.0 { - None => Ok(DeviceId::AAudio(-1)), // Default device - Some(info) => Ok(DeviceId::AAudio(info.id)), + None => Ok(DeviceDescriptionBuilder::new("Default Device".to_string()).build()), + Some(info) => { + let mut builder = DeviceDescriptionBuilder::new(info.product_name.clone()) + .device_type(info.device_type.into()) + .interface_type(info.device_type.into()) + .direction(info.direction); + + // Add address if not empty + if !info.address.is_empty() { + builder = builder.address(info.address.clone()); + } + + Ok(builder.build()) + } } } + fn id(&self) -> Result { + let device_str = match &self.0 { + None => "-1".to_string(), // Default device + Some(info) => info.id.to_string(), + }; + Ok(DeviceId(crate::platform::HostId::AAudio, device_str)) + } + 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..3ae312047 100644 --- a/src/host/alsa/enumerate.rs +++ b/src/host/alsa/enumerate.rs @@ -42,21 +42,19 @@ 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(), desc: Some("Default Audio Device".to_string()), + direction: None, handles: Arc::new(Mutex::new(Default::default())), } } @@ -80,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/alsa/mod.rs b/src/host/alsa/mod.rs index 613095e96..f0c5ec64a 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, @@ -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,10 +134,15 @@ 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 { + Device::description(self) + } + fn id(&self) -> Result { Device::id(self) } @@ -283,6 +307,7 @@ impl DeviceHandles { pub struct Device { pcm_id: String, desc: Option, + direction: Option, handles: Arc>, } @@ -378,14 +403,34 @@ impl Device { Ok(stream_inner) } - #[inline] fn name(&self) -> Result { - Ok(self.to_string()) + Ok(self.pcm_id.clone()) + } + + 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()) } - #[inline] fn id(&self) -> Result { - Ok(DeviceId::ALSA(self.pcm_id.clone())) + Ok(DeviceId(crate::platform::HostId::Alsa, self.pcm_id.clone())) } fn supported_configs( @@ -469,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)); } } @@ -1447,13 +1487,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..372ea40bb 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,25 @@ impl Hash for Device { } impl Device { - pub fn name(&self) -> Result { - Ok(self.driver.name().to_string()) + pub 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), + ); + + Ok(DeviceDescriptionBuilder::new(driver_name.clone()) + .driver(driver_name) + .direction(direction) + .build()) } - fn id(&self) -> Result { - Ok(DeviceId::ASIO(self.driver.name().to_string())) + pub fn id(&self) -> Result { + Ok(DeviceId( + crate::platform::HostId::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 5c56b155f..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}; @@ -58,8 +59,8 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn name(&self) -> Result { - Device::name(self) + fn description(&self) -> Result { + Device::description(self) } fn id(&self) -> Result { 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..15d33d5e3 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,30 +66,41 @@ impl HostTrait for Host { } impl Device { - #[inline] - fn name(&self) -> Result { - Ok("Default Device".to_owned()) + 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) + }; + + Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) + .direction(direction) + .build()) } fn id(&self) -> Result { - Ok(DeviceId::IOS("default".to_string())) + Ok(DeviceId( + crate::platform::HostId::CoreAudio, + "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 +109,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 +123,30 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - #[inline] - fn name(&self) -> Result { - Device::name(self) + fn description(&self) -> Result { + Device::description(self) } - #[inline] 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..b7099db5e 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,8 +263,8 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn name(&self) -> Result { - Device::name(self) + fn description(&self) -> Result { + Device::description(self) } fn id(&self) -> Result { @@ -356,12 +357,65 @@ impl Device { Self { audio_device_id } } - fn name(&self) -> Result { - get_device_name(self.audio_device_id).map_err(|err| DeviceNameError::BackendSpecific { - err: BackendSpecificError { - description: err.to_string(), - }, - }) + /// 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 { + 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 { @@ -393,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/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..7bcc94347 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,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; @@ -215,10 +216,15 @@ where T::SupportedOutputConfigs: Clone + 'static, T::Stream: Send + Sync + 'static, { + #[allow(deprecated)] fn name(&self) -> Result { ::name(self) } + fn description(&self) -> Result { + ::description(self) + } + fn id(&self) -> Result { ::id(self) } @@ -337,6 +343,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..ea359b9ec 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,24 +73,25 @@ impl Devices { } impl Device { - #[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] fn id(&self) -> Result { - Ok(DeviceId::Emscripten("default".to_string())) + Ok(DeviceId( + crate::platform::HostId::Emscripten, + "default".to_string(), + )) } - #[inline] fn supported_input_configs( &self, ) -> Result { unimplemented!(); } - #[inline] fn supported_output_configs( &self, ) -> Result { @@ -153,8 +155,8 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn name(&self) -> Result { - Device::name(self) + fn description(&self) -> Result { + Device::description(self) } fn id(&self) -> Result { @@ -390,7 +392,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 +403,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..0d04b6296 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; @@ -17,19 +18,12 @@ 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, } @@ -39,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); @@ -56,7 +50,7 @@ impl Device { min: client.buffer_size(), max: client.buffer_size(), }, - device_type, + direction, start_server_automatically, connect_ports_automatically, }), @@ -65,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( @@ -78,7 +72,7 @@ impl Device { output_client_name, connect_ports_automatically, start_server_automatically, - DeviceType::OutputDevice, + DeviceDirection::Output, ) } @@ -92,7 +86,7 @@ impl Device { input_client_name, connect_ports_automatically, start_server_automatically, - DeviceType::InputDevice, + DeviceDirection::Input, ) } @@ -133,11 +127,11 @@ impl Device { } pub fn is_input(&self) -> bool { - matches!(self.device_type, DeviceType::InputDevice) + matches!(self.direction, DeviceDirection::Input) } pub fn is_output(&self) -> bool { - matches!(self.device_type, DeviceType::OutputDevice) + matches!(self.direction, DeviceDirection::Output) } /// Validate buffer size if Fixed is specified. This is necessary because JACK buffer size @@ -160,8 +154,10 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn name(&self) -> Result { - Ok(self.name.clone()) + fn description(&self) -> Result { + Ok(DeviceDescriptionBuilder::new(self.name.clone()) + .direction(self.direction) + .build()) } fn id(&self) -> Result { @@ -206,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 self.is_output() { // Trying to create an input stream from an output device return Err(BuildStreamError::StreamConfigNotSupported); } @@ -247,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 self.is_input() { // Trying to create an output stream from an input device return Err(BuildStreamError::StreamConfigNotSupported); } 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 60c5daa4c..9f54d580e 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)] @@ -46,36 +46,34 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - #[inline] fn name(&self) -> Result { - Ok("null".to_owned()) + Ok("null".to_string()) + } + + fn description(&self) -> Result { + Ok(DeviceDescriptionBuilder::new("Null Device".to_string()).build()) } - #[inline] fn id(&self) -> Result { - Ok(DeviceId::Null) + Ok(DeviceId(crate::platform::HostId::Null, String::new())) } - #[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..097e7daee 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; @@ -21,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; +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}; @@ -34,6 +48,20 @@ 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: 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: 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)] struct IAudioClientWrapper(Audio::IAudioClient); @@ -54,8 +82,8 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - fn name(&self) -> Result { - Device::name(self) + fn description(&self) -> Result { + Device::description(self) } fn id(&self) -> Result { @@ -273,8 +301,53 @@ unsafe fn format_from_waveformatex_ptr( unsafe impl Send for Device {} unsafe impl Sync for Device {} +/// 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) +} + impl Device { - pub fn name(&self) -> Result { + pub fn description(&self) -> Result { unsafe { // Open the device's property store. let property_store = self @@ -282,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); + } + + // Add interface name to driver field if available + if let Some(iface_name) = interface_name { + builder = builder.driver(iface_name); + } - // Clean up the property. - StructuredStorage::PropVariantClear(&mut property_value).ok(); + // 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()) } } @@ -330,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), @@ -342,7 +458,6 @@ impl Device { } } - #[inline] fn from_immdevice(device: Audio::IMMDevice) -> Self { Device { device, @@ -374,7 +489,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 +902,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. @@ -834,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() } } @@ -876,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: &IPropertyStore, + property_key: *const 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: &IPropertyStore, + property_key: *const 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); @@ -927,7 +1101,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/web_audio_worklet/mod.rs b/src/host/web_audio_worklet/mod.rs index c0f124b3f..1aabd7f48 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,13 +94,18 @@ 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] 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 bfb8491e0..95e914fbf 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,17 +88,19 @@ impl Devices { } impl Device { - #[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] fn id(&self) -> Result { - Ok(DeviceId::WebAudio("default".to_string())) + Ok(DeviceId( + crate::platform::HostId::WebAudio, + "default".to_string(), + )) } - #[inline] fn supported_input_configs( &self, ) -> Result { @@ -106,7 +108,6 @@ impl Device { Ok(Vec::new().into_iter()) } - #[inline] fn supported_output_configs( &self, ) -> Result { @@ -126,13 +127,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 +150,30 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; - #[inline] - fn name(&self) -> Result { - Device::name(self) + fn description(&self) -> Result { + Device::description(self) } - #[inline] 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) } @@ -235,8 +228,8 @@ 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(); - stream_opts.sample_rate(config.sample_rate.0 as f32); + 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 { let description = format!("{:?}", err); @@ -482,6 +475,7 @@ impl Default for Devices { impl Iterator for Devices { type Item = Device; + #[inline] fn next(&mut self) -> Option { if self.0 { @@ -493,13 +487,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/lib.rs b/src/lib.rs index d80fd5d83..5c8d19541 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; @@ -226,37 +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), - IOS(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::IOS(default) => write!(f, "ios:{}", default), - DeviceId::Null => write!(f, "null:null"), - } + write!(f, "{}:{}", self.0, self.1) } } @@ -264,34 +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: {}\nCheck if format matches Audio_API:DeviceId", s) - } - } - )?; - - 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())), - "emscripten" => Ok(DeviceId::Emscripten(data.to_string())), - "ios" => Ok(DeviceId::IOS(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())) } } @@ -938,16 +903,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 +921,7 @@ const COMMON_SAMPLE_RATES: &[SampleRate] = &[ SampleRate(96000), SampleRate(176400), SampleRate(192000), + SampleRate(352800), SampleRate(384000), ]; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index c53e01795..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 { $( @@ -149,6 +186,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`. @@ -308,6 +365,7 @@ macro_rules! impl_platform_host { type SupportedOutputConfigs = SupportedOutputConfigs; type Stream = Stream; + #[allow(deprecated)] fn name(&self) -> Result { match self.0 { $( @@ -317,6 +375,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/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 51e9d00f5..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. @@ -96,9 +96,28 @@ pub trait DeviceTrait { type Stream: StreamTrait; /// The human-readable name of the device. - fn name(&self) -> Result; + #[deprecated( + since = "0.17.0", + note = "Use `id()` to get a unique identifier for the device, or `description().name()` for a human-readable description." + )] + fn name(&self) -> Result { + self.description().map(|desc| desc.name().to_string()) + } - /// The device-id of the device. + /// 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. + /// + /// 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