From a18f2baf3878a593a2ae205c7e5a3c73659cf047 Mon Sep 17 00:00:00 2001 From: Nicolas Franco Gomez <80042895+nico-franco-gomez@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:20:46 +0200 Subject: [PATCH 01/14] introduce new function to trait doc # Conflicts: # src/platform/mod.rs # src/traits.rs introduce default implementation a update example device --- examples/custom.rs | 7 +++++++ src/platform/mod.rs | 9 +++++++++ src/traits.rs | 29 +++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) 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/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 beeff09d2..f20357560 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -667,6 +667,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. From c1db839dbe7966278b2cd31179e820e35a5dce72 Mon Sep 17 00:00:00 2001 From: Nicolas Franco Gomez <80042895+nico-franco-gomez@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:24:07 +0200 Subject: [PATCH 02/14] feature(asio-sys): make channel name available --- asio-sys/src/bindings/mod.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/asio-sys/src/bindings/mod.rs b/asio-sys/src/bindings/mod.rs index 5b2dd474c..1b9cd40b0 100644 --- a/asio-sys/src/bindings/mod.rs +++ b/asio-sys/src/bindings/mod.rs @@ -981,6 +981,18 @@ impl Driver { drop(dcb); drop(removed); } + + /// 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 { From bb3a90e934fc93224840182f7e015b77aca867c5 Mon Sep 17 00:00:00 2001 From: Nicolas Franco Gomez <80042895+nico-franco-gomez@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:24:29 +0200 Subject: [PATCH 03/14] feature(asio): implement channel name with cache on enumeration # Conflicts: # src/host/asio/device.rs --- src/host/asio/device.rs | 32 ++++++++++++++++++++++++++++++++ src/host/asio/mod.rs | 4 ++++ 2 files changed, 36 insertions(+) diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index f2162b32b..505c16fd5 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]>, pub(super) current_callback_flag: Arc, } @@ -123,6 +125,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 { @@ -209,6 +231,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); return Some(Device { @@ -221,6 +250,9 @@ 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 { From 4fac942cbfa1109e64eab75aac8c28e14abf76cb Mon Sep 17 00:00:00 2001 From: Nicolas Franco Gomez <80042895+nico-franco-gomez@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:59:49 +0200 Subject: [PATCH 04/14] feature(coreaudio): implement channel names simplify unsafe structure --- src/host/coreaudio/macos/device.rs | 68 +++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) 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", + )) + } +} From 112515f20f7ec54e714f60c0c6f6859371c01af8 Mon Sep 17 00:00:00 2001 From: Nicolas Franco Gomez <80042895+nico-franco-gomez@users.noreply.github.com> Date: Thu, 27 Aug 2026 02:19:30 +0200 Subject: [PATCH 05/14] fix(coreaudio): check channel index --- src/host/coreaudio/macos/device.rs | 119 ++++++++++++++++++----------- 1 file changed, 75 insertions(+), 44 deletions(-) diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index c03212b6e..3c0a725d2 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -486,49 +486,7 @@ impl Device { }; unsafe { - // Retrieve the devices audio buffer list. - let mut data_size = 0u32; - let status = AudioObjectGetPropertyDataSize( - self.audio_device_id, - NonNull::from(&property_address), - 0, - null(), - NonNull::from(&mut data_size), - ); - check_os_status(status)?; - - let mut audio_buffer_list: Vec = vec![]; - audio_buffer_list.reserve_exact(data_size as usize); - let status = AudioObjectGetPropertyData( - self.audio_device_id, - NonNull::from(&property_address), - 0, - null(), - NonNull::from(&mut data_size), - NonNull::new(audio_buffer_list.as_mut_ptr()).unwrap().cast(), - ); - check_os_status(status)?; - - let audio_buffer_list = audio_buffer_list.as_mut_ptr() as *mut AudioBufferList; - - // Read the number of buffers without assuming alignment (avoid UB). - let nb_ptr = core::ptr::addr_of!((*audio_buffer_list).mNumberBuffers); - let n_buffers = core::ptr::read_unaligned(nb_ptr) as usize; - // If there are no buffers, skip. - if n_buffers == 0 { - return Ok(vec![].into_iter()); - } - - // Count the number of channels as the sum of all channels in all output buffers. - let first_buf_ptr = - core::ptr::addr_of!((*audio_buffer_list).mBuffers) as *const AudioBuffer; - let mut n_channels = 0usize; - for i in 0..n_buffers { - let buf_ptr = first_buf_ptr.add(i); - // Read potentially unaligned - let buf: AudioBuffer = core::ptr::read_unaligned(buf_ptr); - n_channels += buf.mNumberChannels as usize; - } + let n_channels = get_channel_count_for_device(self.audio_device_id, scope)?; // TODO: macOS should support U8, I16, I32, F32 and F64. This should allow for using // I16 but just use F32 for now as it's the default anyway. @@ -584,7 +542,7 @@ impl Device { let fmts: Vec<_> = ranges .iter() .map(|range| SupportedStreamConfigRange { - channels: n_channels as ChannelCount, + channels: n_channels, min_sample_rate: range.mMinimum as u32, max_sample_rate: range.mMaximum as u32, buffer_size, @@ -710,6 +668,24 @@ impl Device { )); } + let max_channels = get_channel_count_for_device( + self.audio_device_id, + if input { + kAudioObjectPropertyScopeInput + } else { + kAudioObjectPropertyScopeOutput + }, + )?; + if channel_index >= max_channels { + return Err(Error::with_message( + ErrorKind::InvalidInput, + format!( + "channel index {channel_index} is out of range (device has {max_channels} {} channels)", + if input { "input" } else { "output" }, + ), + )); + } + get_channel_name_for_device(self.audio_device_id, channel_index, input) } } @@ -1180,3 +1156,58 @@ fn get_channel_name_for_device( )) } } + +#[allow(clippy::cast_ptr_alignment)] +fn get_channel_count_for_device( + device_id: AudioDeviceID, + scope: AudioObjectPropertyScope, +) -> Result { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyStreamConfiguration, + mScope: scope, + mElement: kAudioObjectPropertyElementMain, + }; + + unsafe { + let mut data_size = 0u32; + let status = AudioObjectGetPropertyDataSize( + device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&mut data_size), + ); + check_os_status(status)?; + + let mut audio_buffer_list: Vec = vec![]; + audio_buffer_list.reserve_exact(data_size as usize); + let status = AudioObjectGetPropertyData( + device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&mut data_size), + NonNull::new(audio_buffer_list.as_mut_ptr()).unwrap().cast(), + ); + check_os_status(status)?; + + let audio_buffer_list = audio_buffer_list.as_mut_ptr() as *mut AudioBufferList; + let n_buffers = + core::ptr::read_unaligned(core::ptr::addr_of!((*audio_buffer_list).mNumberBuffers)) + as usize; + let first_buf_ptr = + core::ptr::addr_of!((*audio_buffer_list).mBuffers) as *const AudioBuffer; + let mut n_channels = 0usize; + for index in 0..n_buffers { + let buffer = core::ptr::read_unaligned(first_buf_ptr.add(index)); + n_channels += buffer.mNumberChannels as usize; + } + + n_channels.try_into().map_err(|_| { + Error::with_message( + ErrorKind::UnsupportedConfig, + format!("device has too many channels ({n_channels})"), + ) + }) + } +} From e7a24421d4020edcefb1a59fd2d4d0422aeece56 Mon Sep 17 00:00:00 2001 From: Nicolas Franco Gomez <80042895+nico-franco-gomez@users.noreply.github.com> Date: Thu, 27 Aug 2026 02:22:45 +0200 Subject: [PATCH 06/14] (asio): use default channel names if empty --- src/host/asio/device.rs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index 505c16fd5..038b4dd6e 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -207,6 +207,12 @@ impl Iterator for Devices { if channels.ins == 0 && channels.outs == 0 { continue; } + let Ok(channels_in) = ChannelCount::try_from(channels.ins) else { + continue; + }; + let Ok(channels_out) = ChannelCount::try_from(channels.outs) else { + continue; + }; // Some drivers (e.g. Realtek ASIO) return 0 for sample_rate() until a // stream is active. Treat 0 as "not yet known" rather than skipping. @@ -231,19 +237,27 @@ 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()) + let input_channel_names: Box<[String]> = (0..channels_in) + .map(|ch| { + driver + .channel_name(ch, true) + .unwrap_or_else(|| format!("Input {ch}")) + }) .collect(); - let output_channel_names: Box<[String]> = (0..channels.outs) - .map(|ch| driver.channel_name(ch, false).unwrap_or_default()) + let output_channel_names: Box<[String]> = (0..channels_out) + .map(|ch| { + driver + .channel_name(ch, false) + .unwrap_or_else(|| format!("Output {ch}")) + }) .collect(); self.current_driver = Some(driver); return Some(Device { name, - channels_in: channels.ins as ChannelCount, - channels_out: channels.outs as ChannelCount, + channels_in, + channels_out, sample_rate: sample_rate as SampleRate, buffer_size_min: buffer_size_range.min as FrameCount, buffer_size_max: buffer_size_range.max as FrameCount, From 2fb7494eeedb0f1f154a50a50b6310b5617a5004 Mon Sep 17 00:00:00 2001 From: Nicolas Franco Gomez <80042895+nico-franco-gomez@users.noreply.github.com> Date: Thu, 27 Aug 2026 02:28:41 +0200 Subject: [PATCH 07/14] fix(asio): avoid reading past vector length --- asio-sys/src/bindings/mod.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/asio-sys/src/bindings/mod.rs b/asio-sys/src/bindings/mod.rs index 1b9cd40b0..5775e613a 100644 --- a/asio-sys/src/bindings/mod.rs +++ b/asio-sys/src/bindings/mod.rs @@ -7,7 +7,7 @@ pub mod errors; #[cfg(target_os = "windows")] use std::os::raw::c_long; use std::{ - ffi::{CStr, CString}, + ffi::CString, os::raw::{c_char, c_double, c_void}, ptr::null_mut, sync::{ @@ -1149,7 +1149,15 @@ fn stream_data_type(is_input: bool) -> Result { /// /// This converts to utf8. fn driver_name_to_utf8(bytes: &[c_char]) -> std::borrow::Cow<'_, str> { - unsafe { CStr::from_ptr(bytes.as_ptr()).to_string_lossy() } + let length = bytes + .iter() + .position(|&byte| byte == 0) + .unwrap_or(bytes.len()); + let bytes = bytes[..length] + .iter() + .map(|&byte| byte as u8) + .collect::>(); + String::from_utf8_lossy(&bytes).into_owned().into() } /// Convert an `ASIOTimeStamp` (high and low 32-bit halves) to a `u64` nanosecond value. From 3a23e25d7ce9875d98750d7a03cff21a29ecf4a1 Mon Sep 17 00:00:00 2001 From: Nicolas Franco Gomez <80042895+nico-franco-gomez@users.noreply.github.com> Date: Thu, 27 Aug 2026 02:32:16 +0200 Subject: [PATCH 08/14] fix(asio): avoid negative channel number by returning error --- asio-sys/src/bindings/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/asio-sys/src/bindings/mod.rs b/asio-sys/src/bindings/mod.rs index 5775e613a..6d63e4df5 100644 --- a/asio-sys/src/bindings/mod.rs +++ b/asio-sys/src/bindings/mod.rs @@ -1119,6 +1119,10 @@ fn asio_get_buffer_sizes() -> Result { /// Retrieve the `ASIOChannelInfo` associated with the channel at the given index on either the /// input or output stream (`true` for input). fn asio_channel_info(channel: c_long, is_input: bool) -> Result { + if channel < 0 { + return Err(AsioError::InvalidInput); + } + let mut channel_info = ai::ASIOChannelInfo { // Which channel we are querying channel, From f79cf461bebc7788ec24b42fa4a4d2f7d067c602 Mon Sep 17 00:00:00 2001 From: Nicolas Franco Gomez <80042895+nico-franco-gomez@users.noreply.github.com> Date: Thu, 27 Aug 2026 02:35:50 +0200 Subject: [PATCH 09/14] (coreaudio): create CFString helper and replace usages --- src/host/coreaudio/macos/device.rs | 67 ++++++++++-------------------- 1 file changed, 22 insertions(+), 45 deletions(-) diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index 3c0a725d2..43982937b 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -37,7 +37,7 @@ use objc2_core_audio::{ use objc2_core_audio_types::{ AudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioValueRange, }; -use objc2_core_foundation::{CFRetained, CFString}; +use objc2_core_foundation::CFString; pub use super::enumerate::{SupportedInputConfigs, SupportedOutputConfigs}; use super::{ @@ -441,36 +441,12 @@ impl Device { mElement: kAudioObjectPropertyElementMain, }; - // CFString is returned under the create rule, so take ownership of the +1 reference. - let mut uid: *mut CFString = std::ptr::null_mut(); - let mut data_size = size_of::<*mut CFString>() as u32; - - // SAFETY: AudioObjectGetPropertyData is documented to write a CFString pointer - // for kAudioDevicePropertyDeviceUID. We check the status code before use. - let status = unsafe { - AudioObjectGetPropertyData( - self.audio_device_id, - NonNull::from(&property_address), - 0, - null(), - NonNull::from(&mut data_size), - NonNull::from(&mut uid).cast(), - ) - }; - check_os_status(status)?; - - // SAFETY: Status was successful, meaning the API call succeeded. - // We now check if the returned uid is non-null before use. - if !uid.is_null() { - let uid_string = - unsafe { CFRetained::from_raw(NonNull::new(uid).unwrap()).to_string() }; - Ok(DeviceId::new( - crate::platform::HostId::CoreAudio, - uid_string, - )) - } else { - Err(ErrorKind::DeviceNotAvailable.into()) - } + let uid_string = get_cf_string_property(self.audio_device_id, &property_address)? + .ok_or(ErrorKind::DeviceNotAvailable)?; + Ok(DeviceId::new( + crate::platform::HostId::CoreAudio, + uid_string, + )) } // Logic re-used between `supported_input_configs` and `supported_output_configs`. @@ -1120,9 +1096,6 @@ fn get_channel_name_for_device( 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 { @@ -1134,27 +1107,31 @@ fn get_channel_name_for_device( mElement: channel_index as u32 + 1, }; + get_cf_string_property(device_id, &property_address)? + .ok_or_else(|| Error::with_message(ErrorKind::Other, "channel name is null")) +} + +fn get_cf_string_property( + device_id: AudioDeviceID, + property_address: &AudioObjectPropertyAddress, +) -> Result, Error> { + let mut value: *mut CFString = std::ptr::null_mut(); + let mut data_size = size_of::<*mut CFString>() as u32; + let status = unsafe { AudioObjectGetPropertyData( device_id, - NonNull::from(&property_address), + NonNull::from(property_address), 0, null(), NonNull::from(&mut data_size), - NonNull::from(&mut channel_name).cast(), + NonNull::from(&mut value).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", - )) - } + Ok(NonNull::new(value) + .map(|value| unsafe { objc2_core_foundation::CFRetained::from_raw(value).to_string() })) } #[allow(clippy::cast_ptr_alignment)] From 6bd13562b5dfce4ffdad20b2420fe1f16453f3c6 Mon Sep 17 00:00:00 2001 From: Nicolas Franco Gomez <80042895+nico-franco-gomez@users.noreply.github.com> Date: Thu, 27 Aug 2026 02:40:29 +0200 Subject: [PATCH 10/14] (coreaudio): remove querying device, channel check covers it --- src/host/coreaudio/macos/device.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index 43982937b..8aa162692 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -630,20 +630,6 @@ impl Device { } 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", - )); - } - let max_channels = get_channel_count_for_device( self.audio_device_id, if input { From 73567ec5f06f29bc5e5804d5f584ebe9d7dcd027 Mon Sep 17 00:00:00 2001 From: Nicolas Franco Gomez <80042895+nico-franco-gomez@users.noreply.github.com> Date: Thu, 27 Aug 2026 06:22:28 -0500 Subject: [PATCH 11/14] update documentation --- src/traits.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/traits.rs b/src/traits.rs index f20357560..2c475d08b 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -670,25 +670,26 @@ pub trait DeviceTrait: PartialEq + Eq + Hash + Debug + Display + Send + Sync { /// 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`]. + /// The CoreAudio (macOS) and ASIO backends provide channel names. All other built-in + /// backends return [`ErrorKind::UnsupportedOperation`]. /// /// # Parameters /// - /// * `channel_index` - Channel index to query name for. - /// * `input` - Whether to query an input channel (true) or output channel (false). + /// * `channel_index` - Channel index to query. + /// * `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`] if the backend does not provide channel names, or + /// if the device does not support the requested direction. + /// - [`ErrorKind::InvalidInput`] if `channel_index` is out of range for the device. + /// - [`ErrorKind::BackendError`] if the underlying audio API returns an error that cannot be + /// mapped to a more specific error kind. + /// - [`ErrorKind::Other`] if the backend returns no channel name. /// /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation /// [`ErrorKind::InvalidInput`]: crate::ErrorKind::InvalidInput + /// [`ErrorKind::BackendError`]: crate::ErrorKind::BackendError /// [`ErrorKind::Other`]: crate::ErrorKind::Other fn get_channel_name(&self, _channel_index: u16, _input: bool) -> Result { Err(Error::with_message( From c454a830a19a20e04a560b39baaf00c3dd53800d Mon Sep 17 00:00:00 2001 From: Nicolas Franco Gomez <80042895+nico-franco-gomez@users.noreply.github.com> Date: Thu, 27 Aug 2026 06:22:44 -0500 Subject: [PATCH 12/14] (coreaudio): correct clippy issue macos --- src/host/coreaudio/macos/device.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index 8aa162692..1c61be99f 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -450,7 +450,6 @@ impl Device { } // Logic re-used between `supported_input_configs` and `supported_output_configs`. - #[expect(clippy::cast_ptr_alignment)] fn supported_configs( &self, scope: AudioObjectPropertyScope, From 786b3efcf4bfe1ef40ded2e6639ea46dc55f8ef3 Mon Sep 17 00:00:00 2001 From: Nicolas Franco Gomez <80042895+nico-franco-gomez@users.noreply.github.com> Date: Thu, 27 Aug 2026 06:30:56 -0500 Subject: [PATCH 13/14] (asio): solve some issues --- src/host/asio/device.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index 038b4dd6e..7d788a422 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -240,15 +240,19 @@ impl Iterator for Devices { let input_channel_names: Box<[String]> = (0..channels_in) .map(|ch| { driver - .channel_name(ch, true) - .unwrap_or_else(|| format!("Input {ch}")) + .channel_name(ch.into(), true) + .ok() + .filter(|name| !name.is_empty()) + .unwrap_or_else(|_| format!("Input {ch}")) }) .collect(); let output_channel_names: Box<[String]> = (0..channels_out) .map(|ch| { driver - .channel_name(ch, false) - .unwrap_or_else(|| format!("Output {ch}")) + .channel_name(ch.into(), false) + .ok() + .filter(|name| !name.is_empty()) + .unwrap_or_else(|_| format!("Output {ch}")) }) .collect(); @@ -266,7 +270,6 @@ impl Iterator for Devices { 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)), }); From 178d099a30c4023df4dc1c8e13074f052a62e88f Mon Sep 17 00:00:00 2001 From: Nicolas Franco Gomez <80042895+nico-franco-gomez@users.noreply.github.com> Date: Thu, 27 Aug 2026 06:39:40 -0500 Subject: [PATCH 14/14] (asio): correction --- src/host/asio/device.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index 7d788a422..74dd34520 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -243,7 +243,7 @@ impl Iterator for Devices { .channel_name(ch.into(), true) .ok() .filter(|name| !name.is_empty()) - .unwrap_or_else(|_| format!("Input {ch}")) + .unwrap_or_else(|| format!("Input {ch}")) }) .collect(); let output_channel_names: Box<[String]> = (0..channels_out) @@ -252,7 +252,7 @@ impl Iterator for Devices { .channel_name(ch.into(), false) .ok() .filter(|name| !name.is_empty()) - .unwrap_or_else(|_| format!("Output {ch}")) + .unwrap_or_else(|| format!("Output {ch}")) }) .collect();