diff --git a/asio-sys/src/bindings/mod.rs b/asio-sys/src/bindings/mod.rs index 36be4e102..c30e059fd 100644 --- a/asio-sys/src/bindings/mod.rs +++ b/asio-sys/src/bindings/mod.rs @@ -943,6 +943,18 @@ impl Driver { let mut dcb = DRIVER_EVENT_CALLBACKS.lock().unwrap(); dcb.retain(|&(id, _)| id != rem_id); } + + /// Returns the name of the channel at the given index. + /// + /// `channel` is a 0-based channel index. `is_input` selects the input (`true`) or output + /// (`false`) direction. + /// + /// The driver must already be loaded (i.e. this `Driver` instance must be alive). + pub fn channel_name(&self, channel: i32, is_input: bool) -> Result { + let _guard = self.inner.lock_state(); + let info = asio_channel_info(channel, is_input)?; + Ok(driver_name_to_utf8(&info.name).into_owned()) + } } impl DriverState { diff --git a/examples/custom.rs b/examples/custom.rs index 4b09cf911..b38ae7072 100644 --- a/examples/custom.rs +++ b/examples/custom.rs @@ -184,6 +184,13 @@ impl DeviceTrait for MyDevice { handle: Some(handle), }) } + + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + Ok(format!( + "{} {channel_index}", + if input { "Input" } else { "Output" } + )) + } } impl fmt::Display for MyDevice { diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index b6cdb4584..4b41d3dfa 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -736,6 +736,10 @@ impl DeviceTrait for Device { sample_format, ) } + + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + Err(Error::new(ErrorKind::UnsupportedOperation)) + } } impl PartialEq for Device { diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index e4bdc492f..cf4b4d72c 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -304,6 +304,10 @@ impl DeviceTrait for Device { ); Ok(stream) } + + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + Err(Error::new(ErrorKind::UnsupportedOperation)) + } } #[derive(Debug)] diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index 713192cd0..d89dd7dd0 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -25,6 +25,8 @@ pub struct Device { buffer_size_max: FrameCount, input_sample_format: Option, output_sample_format: Option, + input_channel_names: Vec, + output_channel_names: Vec, supported_sample_rates: Box<[SampleRate]>, // Input and/or Output stream. @@ -127,6 +129,26 @@ impl Device { } configs } + + pub fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + let names = if input { + &self.input_channel_names + } else { + &self.output_channel_names + }; + + names.get(channel_index as usize).cloned().ok_or_else(|| { + Error::with_message( + ErrorKind::InvalidInput, + format!( + "channel index {} is out of range (device has {} {} channels)", + channel_index, + names.len(), + if input { "input" } else { "output" }, + ), + ) + }) + } } impl PartialEq for Device { @@ -213,6 +235,13 @@ impl Iterator for Devices { .filter(|&r| driver.can_sample_rate(r.into()).unwrap_or(false)) .collect(); + let input_channel_names: Vec = (0..channels.ins) + .map(|ch| driver.channel_name(ch, true).unwrap_or_default()) + .collect(); + let output_channel_names: Vec = (0..channels.outs) + .map(|ch| driver.channel_name(ch, false).unwrap_or_default()) + .collect(); + self.current_driver = Some(driver); let asio_streams = Arc::new(Mutex::new(sys::AsioStreams { @@ -230,6 +259,8 @@ impl Iterator for Devices { input_sample_format, output_sample_format, supported_sample_rates, + input_channel_names, + output_channel_names, asio_streams, // Initialize with sentinel value so it never matches global flag state (0 or 1). current_callback_flag: Arc::new(AtomicU32::new(u32::MAX)), diff --git a/src/host/asio/mod.rs b/src/host/asio/mod.rs index c529ecee3..67591e2d7 100644 --- a/src/host/asio/mod.rs +++ b/src/host/asio/mod.rs @@ -143,6 +143,10 @@ impl DeviceTrait for Device { timeout, ) } + + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + Device::get_channel_name(self, channel_index, input) + } } impl StreamTrait for Stream { diff --git a/src/host/audioworklet/mod.rs b/src/host/audioworklet/mod.rs index a6173f16e..16b4163a8 100644 --- a/src/host/audioworklet/mod.rs +++ b/src/host/audioworklet/mod.rs @@ -443,6 +443,10 @@ impl DeviceTrait for Device { _latency_poller: latency_poller, }) } + + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + Err(Error::new(ErrorKind::UnsupportedOperation)) + } } impl StreamTrait for Stream { diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index cdd0b4807..0e3ea76d3 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -28,10 +28,11 @@ use objc2_core_audio::{ kAudioDevicePropertyDeviceUID, kAudioDevicePropertyLatency, kAudioDevicePropertyNominalSampleRate, kAudioDevicePropertySafetyOffset, kAudioDevicePropertyStreamConfiguration, kAudioDevicePropertyStreamFormat, - kAudioObjectPropertyClass, kAudioObjectPropertyElementMain, kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyScopeInput, kAudioObjectPropertyScopeOutput, AudioClassID, AudioDeviceID, - AudioObjectGetPropertyData, AudioObjectGetPropertyDataSize, AudioObjectID, - AudioObjectPropertyAddress, AudioObjectPropertyScope, AudioObjectSetPropertyData, + kAudioObjectPropertyClass, kAudioObjectPropertyElementMain, kAudioObjectPropertyElementName, + kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyScopeInput, + kAudioObjectPropertyScopeOutput, AudioClassID, AudioDeviceID, AudioObjectGetPropertyData, + AudioObjectGetPropertyDataSize, AudioObjectID, AudioObjectPropertyAddress, + AudioObjectPropertyScope, AudioObjectSetPropertyData, }; use objc2_core_audio_types::{ AudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioValueRange, @@ -354,6 +355,10 @@ impl DeviceTrait for Device { timeout, ) } + + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + Device::get_channel_name(self, channel_index, input) + } } #[derive(Clone)] @@ -687,6 +692,24 @@ impl Device { .map(|mut configs| configs.next().is_some()) .unwrap_or(false) } + + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + if input && !self.supports_input() { + return Err(Error::with_message( + ErrorKind::InvalidInput, + "Device does not support input", + )); + } + + if !input && !self.supports_output() { + return Err(Error::with_message( + ErrorKind::InvalidInput, + "Device does not support output", + )); + } + + unsafe { get_channel_name_for_device(self.audio_device_id, channel_index, input) } + } } impl Device { @@ -1084,3 +1107,42 @@ pub(crate) fn get_device_buffer_frame_size( )?; Ok(frames as usize) } + +unsafe fn get_channel_name_for_device( + device_id: AudioDeviceID, + channel_index: u16, + input: bool, +) -> Result { + let mut channel_name: *mut CFString = std::ptr::null_mut(); + let mut data_size = size_of::<*mut CFString>() as u32; + + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioObjectPropertyElementName, + mScope: if input { + kAudioObjectPropertyScopeInput + } else { + kAudioObjectPropertyScopeOutput + }, + // Channels numbers start at 1 here + mElement: channel_index as u32 + 1, + }; + + let status = AudioObjectGetPropertyData( + device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&mut data_size), + NonNull::from(&mut channel_name).cast(), + ); + check_os_status(status)?; + + if !channel_name.is_null() { + Ok(CFRetained::from_raw(NonNull::new(channel_name).unwrap()).to_string()) + } else { + Err(Error::with_message( + ErrorKind::Other, + "channel name is null", + )) + } +} diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs index d51acd26d..370ef5f4f 100644 --- a/src/host/custom/mod.rs +++ b/src/host/custom/mod.rs @@ -451,6 +451,10 @@ impl DeviceTrait for Device { timeout, ) } + + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + Err(Error::new(ErrorKind::UnsupportedOperation)) + } } impl StreamTrait for Stream { diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 9052c7d42..ea0763eed 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -347,6 +347,10 @@ impl DeviceTrait for Device { build() } } + + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + Err(Error::new(ErrorKind::UnsupportedOperation)) + } } impl PartialEq for Device { diff --git a/src/host/null/mod.rs b/src/host/null/mod.rs index e675339ec..56ea7378c 100644 --- a/src/host/null/mod.rs +++ b/src/host/null/mod.rs @@ -104,6 +104,10 @@ impl DeviceTrait for Device { { unimplemented!() } + + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + Err(Error::new(ErrorKind::UnsupportedOperation)) + } } impl HostTrait for Host { diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index a0c374532..d0d4b8e5f 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -692,6 +692,10 @@ impl DeviceTrait for Device { stream.signal_ready(); Ok(stream) } + + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + Err(Error::new(ErrorKind::UnsupportedOperation)) + } } #[derive(Clone, Default)] diff --git a/src/host/pulseaudio/mod.rs b/src/host/pulseaudio/mod.rs index 7ddad8d9e..6f59b5ac7 100644 --- a/src/host/pulseaudio/mod.rs +++ b/src/host/pulseaudio/mod.rs @@ -524,6 +524,10 @@ impl DeviceTrait for Device { String::from_utf8_lossy(name.as_bytes()), )) } + + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + Err(Error::new(ErrorKind::UnsupportedOperation)) + } } fn make_sample_spec(config: StreamConfig, format: protocol::SampleFormat) -> protocol::SampleSpec { diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index a9330ad85..9c90439bb 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -156,6 +156,10 @@ impl DeviceTrait for Device { stream.signal_ready(); Ok(stream) } + + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + Err(Error::new(ErrorKind::UnsupportedOperation)) + } } struct Endpoint { diff --git a/src/host/webaudio/mod.rs b/src/host/webaudio/mod.rs index 3c9119a20..c0580ba90 100644 --- a/src/host/webaudio/mod.rs +++ b/src/host/webaudio/mod.rs @@ -576,6 +576,10 @@ impl DeviceTrait for Device { is_started, }) } + + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + Err(Error::new(ErrorKind::UnsupportedOperation)) + } } impl Stream { diff --git a/src/platform/mod.rs b/src/platform/mod.rs index eb6a482f6..9a07ed657 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -525,6 +525,15 @@ macro_rules! impl_platform_host { )* } } + + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result { + match self.0 { + $( + $(#[cfg($feat)])? + DeviceInner::$HostVariant(ref d) => d.get_channel_name(channel_index, input), + )* + } + } } impl crate::traits::HostTrait for Host { diff --git a/src/traits.rs b/src/traits.rs index 4491ad979..60d884dbb 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -407,6 +407,30 @@ pub trait DeviceTrait: PartialEq + Eq + Hash + Debug + Display { where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static; + + /// Obtain the associated string name for a channel index. + /// + /// This method is only implemented for CoreAudio (macOS) and ASIO (Windows). All other + /// backends will return [`ErrorKind::UnsupportedOperation`]. + /// + /// # Parameters + /// + /// * `channel_index` - Channel index to query name for. + /// * `input` - Whether to query an input channel (true) or output channel (false). + /// + /// # Errors + /// + /// - [`ErrorKind::UnsupportedOperation`] if the backend does not implement channel name + /// queries. + /// - [`ErrorKind::InvalidInput`] if the channel index is out of range for the device, + /// or if the device does not support the requested direction (input/output). + /// - [`ErrorKind::Other`] for unclassifiable backend failures (e.g., the channel name could + /// not be retrieved from the device). + /// + /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation + /// [`ErrorKind::InvalidInput`]: crate::ErrorKind::InvalidInput + /// [`ErrorKind::Other`]: crate::ErrorKind::Other + fn get_channel_name(&self, channel_index: u16, input: bool) -> Result; } /// A stream created from [`Device`](DeviceTrait), with methods to control it.