Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cpal doesn't use get_ for accessors.

}

/// 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> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about the input: bool or is_input: bool arguments in public functions. In the rest of cpal, that's split between supports_input/output, default_input/output_config, etc. That's more readable than channel_name(1, true) - what argument isn't self-explanatory.

let _guard = self.inner.lock_state();
let info = asio_channel_info(channel, is_input)?;
Ok(driver_name_to_utf8(&info.name).into_owned())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was already the case elsewhere with other names, but it's occurring to me that this will UB if there's ever a driver that doesn't NUL-terminate.

}
}

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 @@ -183,6 +183,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
31 changes: 31 additions & 0 deletions src/host/asio/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ pub struct Device {
input_sample_format: Option<SampleFormat>,
output_sample_format: Option<SampleFormat>,
supported_sample_rates: Box<[SampleRate]>,
input_channel_names: Box<[String]>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CoreAudio doesn't seem to cache this. What's the preferred approach? Lazily like CoreAudio or caching it during enumeration here?

output_channel_names: Box<[String]>,

// Input and/or Output stream.
// A driver can only have one of each.
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: Box<[String]> = (0..channels.ins)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to check that the number of channels is greater than (or equal to) 0. I remember that ASIO often returns an i32 and we wouldn't want a negative value overflowing this.

.map(|ch| driver.channel_name(ch, true).unwrap_or_default())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: rather than defaulting to an empty string, what if we returned something recognizable like "Channel {ch}"?

.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 {
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
68 changes: 66 additions & 2 deletions src/host/coreaudio/macos/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -357,6 +357,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 @@ -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<String, Error> {
if input && !self.supports_input() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

supports_input/output aren't free, so if it's just to detail the error message then maybe we should just use the input argument but not re-query the device (or cache it, but that may be scope creep).

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 {
Expand Down Expand Up @@ -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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Querying an out-of-range channel_index will return a BackendError I think, when InvalidInput would be preferred.

input: bool,
) -> Result<String, Error> {
let mut channel_name: *mut CFString = std::ptr::null_mut();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this CFString stuff is getting used more often, we could consider having a helper for it.

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",
))
}
}
9 changes: 9 additions & 0 deletions src/platform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, crate::Error> {
match self.0 {
$(
$(#[cfg($feat)])?
DeviceInner::$HostVariant(ref d) => d.get_channel_name(channel_index, input),
)*
}
}
Expand Down
29 changes: 29 additions & 0 deletions src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Error> {
Err(Error::with_message(
ErrorKind::UnsupportedOperation,
"device does not support channel names",
))
}
}

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