Skip to content
12 changes: 12 additions & 0 deletions asio-sys/src/bindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, AsioError> {
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 {
Expand Down
7 changes: 7 additions & 0 deletions examples/custom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,13 @@ impl DeviceTrait for MyDevice {
handle: Some(handle),
})
}

fn get_channel_name(&self, channel_index: u16, input: bool) -> Result<String, Error> {
Ok(format!(
"{} {channel_index}",
if input { "Input" } else { "Output" }
))
}
}

impl fmt::Display for MyDevice {
Expand Down
4 changes: 4 additions & 0 deletions src/host/aaudio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,10 @@ impl DeviceTrait for Device {
sample_format,
)
}

fn get_channel_name(&self, channel_index: u16, input: bool) -> Result<String, Error> {
Err(Error::new(ErrorKind::UnsupportedOperation))
}
}

impl PartialEq for Device {
Expand Down
4 changes: 4 additions & 0 deletions src/host/alsa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,10 @@ impl DeviceTrait for Device {
);
Ok(stream)
}

fn get_channel_name(&self, channel_index: u16, input: bool) -> Result<String, Error> {
Err(Error::new(ErrorKind::UnsupportedOperation))
}
}

#[derive(Debug)]
Expand Down
31 changes: 31 additions & 0 deletions src/host/asio/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub struct Device {
buffer_size_max: FrameCount,
input_sample_format: Option<SampleFormat>,
output_sample_format: Option<SampleFormat>,
input_channel_names: Vec<String>,
output_channel_names: Vec<String>,
supported_sample_rates: Box<[SampleRate]>,

// Input and/or Output stream.
Expand Down Expand Up @@ -127,6 +129,26 @@ impl Device {
}
configs
}

pub fn get_channel_name(&self, channel_index: u16, input: bool) -> Result<String, Error> {
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 {
Expand Down Expand Up @@ -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<String> = (0..channels.ins)
.map(|ch| driver.channel_name(ch, true).unwrap_or_default())
.collect();
let output_channel_names: Vec<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 {
Expand All @@ -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)),
Expand Down
4 changes: 4 additions & 0 deletions src/host/asio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ impl DeviceTrait for Device {
timeout,
)
}

fn get_channel_name(&self, channel_index: u16, input: bool) -> Result<String, Error> {
Device::get_channel_name(self, channel_index, input)
}
}

impl StreamTrait for Stream {
Expand Down
4 changes: 4 additions & 0 deletions src/host/audioworklet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,10 @@ impl DeviceTrait for Device {
_latency_poller: latency_poller,
})
}

fn get_channel_name(&self, channel_index: u16, input: bool) -> Result<String, Error> {
Err(Error::new(ErrorKind::UnsupportedOperation))
}
}

impl StreamTrait for Stream {
Expand Down
70 changes: 66 additions & 4 deletions src/host/coreaudio/macos/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -354,6 +355,10 @@ impl DeviceTrait for Device {
timeout,
)
}

fn get_channel_name(&self, channel_index: u16, input: bool) -> Result<String, Error> {
Device::get_channel_name(self, channel_index, input)
}
}

#[derive(Clone)]
Expand Down Expand Up @@ -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<String, Error> {
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 {
Expand Down Expand Up @@ -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<String, Error> {
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",
))
}
}
4 changes: 4 additions & 0 deletions src/host/custom/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,10 @@ impl DeviceTrait for Device {
timeout,
)
}

fn get_channel_name(&self, channel_index: u16, input: bool) -> Result<String, Error> {
Err(Error::new(ErrorKind::UnsupportedOperation))
}
}

impl StreamTrait for Stream {
Expand Down
4 changes: 4 additions & 0 deletions src/host/jack/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ impl DeviceTrait for Device {
build()
}
}

fn get_channel_name(&self, channel_index: u16, input: bool) -> Result<String, Error> {
Err(Error::new(ErrorKind::UnsupportedOperation))
}
}

impl PartialEq for Device {
Expand Down
4 changes: 4 additions & 0 deletions src/host/null/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ impl DeviceTrait for Device {
{
unimplemented!()
}

fn get_channel_name(&self, channel_index: u16, input: bool) -> Result<String, Error> {
Err(Error::new(ErrorKind::UnsupportedOperation))
}
}

impl HostTrait for Host {
Expand Down
4 changes: 4 additions & 0 deletions src/host/pipewire/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,10 @@ impl DeviceTrait for Device {
stream.signal_ready();
Ok(stream)
}

fn get_channel_name(&self, channel_index: u16, input: bool) -> Result<String, Error> {
Err(Error::new(ErrorKind::UnsupportedOperation))
}
}

#[derive(Clone, Default)]
Expand Down
4 changes: 4 additions & 0 deletions src/host/pulseaudio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Error> {
Err(Error::new(ErrorKind::UnsupportedOperation))
}
}

fn make_sample_spec(config: StreamConfig, format: protocol::SampleFormat) -> protocol::SampleSpec {
Expand Down
4 changes: 4 additions & 0 deletions src/host/wasapi/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ impl DeviceTrait for Device {
stream.signal_ready();
Ok(stream)
}

fn get_channel_name(&self, channel_index: u16, input: bool) -> Result<String, Error> {
Err(Error::new(ErrorKind::UnsupportedOperation))
}
}

struct Endpoint {
Expand Down
4 changes: 4 additions & 0 deletions src/host/webaudio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,10 @@ impl DeviceTrait for Device {
is_started,
})
}

fn get_channel_name(&self, channel_index: u16, input: bool) -> Result<String, Error> {
Err(Error::new(ErrorKind::UnsupportedOperation))
}
}

impl Stream {
Expand Down
9 changes: 9 additions & 0 deletions src/platform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,15 @@ macro_rules! impl_platform_host {
)*
}
}

fn get_channel_name(&self, channel_index: u16, input: bool) -> Result<String, crate::Error> {
match self.0 {
$(
$(#[cfg($feat)])?
DeviceInner::$HostVariant(ref d) => d.get_channel_name(channel_index, input),
)*
}
}
}

impl crate::traits::HostTrait for Host {
Expand Down
24 changes: 24 additions & 0 deletions src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Error>;
}

/// A stream created from [`Device`](DeviceTrait), with methods to control it.
Expand Down
Loading