diff --git a/asio-sys/src/bindings/mod.rs b/asio-sys/src/bindings/mod.rs index 914bcbfb0..169c7454a 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 2f0e73d14..ab7067a0e 100644 --- a/examples/custom.rs +++ b/examples/custom.rs @@ -183,6 +183,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/asio/device.rs b/src/host/asio/device.rs index 802deefe3..0b3f256a1 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -26,6 +26,8 @@ pub struct Device { input_sample_format: Option, output_sample_format: Option, supported_sample_rates: Box<[SampleRate]>, + input_channel_names: Box<[String]>, + output_channel_names: Box<[String]>, // Input and/or Output stream. // A driver can only have one of each. @@ -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: Box<[String]> = (0..channels.ins) + .map(|ch| driver.channel_name(ch, true).unwrap_or_default()) + .collect(); + let output_channel_names: Box<[String]> = (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 3d91a1818..52fbb2e16 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/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index bde36af6b..c03212b6e 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -31,8 +31,8 @@ use objc2_core_audio::{ kAudioDevicePropertyLatency, kAudioDevicePropertyNominalSampleRate, kAudioDevicePropertySafetyOffset, kAudioDevicePropertyStreamConfiguration, kAudioDevicePropertyStreamFormat, kAudioObjectPropertyClass, kAudioObjectPropertyElementMain, - kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyScopeInput, - kAudioObjectPropertyScopeOutput, + kAudioObjectPropertyElementName, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyScopeInput, kAudioObjectPropertyScopeOutput, }; use objc2_core_audio_types::{ AudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioValueRange, @@ -357,6 +357,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)] @@ -690,6 +694,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", + )); + } + + get_channel_name_for_device(self.audio_device_id, channel_index, input) + } } impl Device { @@ -1116,3 +1138,45 @@ pub(crate) fn get_device_buffer_frame_size( )?; Ok(frames as usize) } + +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 on 1 here + mElement: channel_index as u32 + 1, + }; + + let status = unsafe { + 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() { + let raw_name = unsafe { CFRetained::from_raw(NonNull::new(channel_name).unwrap()) }; + Ok(raw_name.to_string()) + } else { + Err(Error::with_message( + ErrorKind::Other, + "channel name is null", + )) + } +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 5b2d5aeb6..7c3651cd6 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -564,6 +564,15 @@ macro_rules! impl_platform_host { ) .map(StreamInner::$HostVariant) .map(Stream::from), + )* + } + } + + 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), )* } } diff --git a/src/traits.rs b/src/traits.rs index 2a3f558c1..9bee7ff21 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -539,6 +539,35 @@ pub trait DeviceTrait: PartialEq + Eq + Hash + Debug + Display + Send + Sync { "duplex streams are not supported by this device", )) } + + /// 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 { + Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "device does not support channel names", + )) + } } /// A stream created from [`Device`](DeviceTrait), with methods to control it.