use std::mem;
use std::os::raw::c_void;
use std::ptr;
use std::sync::mpsc;
use std::time::Duration;
use core_foundation::base::TCFType;
use core_foundation::string::CFString;
use coreaudio_sys::*;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DeviceError {
#[error("CoreAudio error: {0}")]
OSStatus(i32),
#[error("no output devices found")]
NoDevices,
#[error("device not found: {0}")]
NotFound(AudioDeviceID),
#[error("device not found by name: {0}")]
NotFoundByName(String),
}
type Result<T> = std::result::Result<T, DeviceError>;
fn check(status: OSStatus) -> Result<()> {
if status == 0 {
Ok(())
} else {
Err(DeviceError::OSStatus(status))
}
}
#[derive(Debug, Clone)]
pub struct AudioDevice {
pub id: AudioDeviceID,
pub name: String,
pub sample_rates: Vec<f64>,
}
pub fn default_output_device() -> Result<AudioDeviceID> {
let property = AudioObjectPropertyAddress {
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain,
};
let mut device_id: AudioDeviceID = 0;
let mut size = mem::size_of::<AudioDeviceID>() as u32;
check(unsafe {
AudioObjectGetPropertyData(
kAudioObjectSystemObject,
&property,
0,
ptr::null(),
&mut size,
&mut device_id as *mut _ as *mut _,
)
})?;
if device_id == 0 {
return Err(DeviceError::NoDevices);
}
Ok(device_id)
}
pub fn list_output_devices() -> Result<Vec<AudioDevice>> {
let property = AudioObjectPropertyAddress {
mSelector: kAudioHardwarePropertyDevices,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain,
};
let mut size: u32 = 0;
check(unsafe {
AudioObjectGetPropertyDataSize(
kAudioObjectSystemObject,
&property,
0,
ptr::null(),
&mut size,
)
})?;
let device_count = size as usize / mem::size_of::<AudioDeviceID>();
let mut device_ids = vec![0u32; device_count];
check(unsafe {
AudioObjectGetPropertyData(
kAudioObjectSystemObject,
&property,
0,
ptr::null(),
&mut size,
device_ids.as_mut_ptr() as *mut _,
)
})?;
let mut devices = Vec::new();
for id in device_ids {
if !has_output_streams(id) {
continue;
}
let name = device_name(id).unwrap_or_else(|_| format!("Unknown ({})", id));
let sample_rates = available_sample_rates(id).unwrap_or_default();
devices.push(AudioDevice {
id,
name,
sample_rates,
});
}
Ok(devices)
}
fn has_output_streams(device_id: AudioDeviceID) -> bool {
let property = AudioObjectPropertyAddress {
mSelector: kAudioDevicePropertyStreams,
mScope: kAudioObjectPropertyScopeOutput,
mElement: kAudioObjectPropertyElementMain,
};
let mut size: u32 = 0;
let status =
unsafe { AudioObjectGetPropertyDataSize(device_id, &property, 0, ptr::null(), &mut size) };
status == 0 && size > 0
}
fn device_name(device_id: AudioDeviceID) -> Result<String> {
let property = AudioObjectPropertyAddress {
mSelector: kAudioObjectPropertyName,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain,
};
let mut name_ref: CFStringRef = ptr::null();
let mut size = mem::size_of::<CFStringRef>() as u32;
check(unsafe {
AudioObjectGetPropertyData(
device_id,
&property,
0,
ptr::null(),
&mut size,
&mut name_ref as *mut _ as *mut _,
)
})?;
if name_ref.is_null() {
return Ok(String::new());
}
let cf_string: CFString = unsafe {
CFString::wrap_under_create_rule(name_ref as core_foundation::string::CFStringRef)
};
Ok(cf_string.to_string())
}
pub fn available_sample_rates(device_id: AudioDeviceID) -> Result<Vec<f64>> {
let property = AudioObjectPropertyAddress {
mSelector: kAudioDevicePropertyAvailableNominalSampleRates,
mScope: kAudioObjectPropertyScopeOutput,
mElement: kAudioObjectPropertyElementMain,
};
let mut size: u32 = 0;
check(unsafe {
AudioObjectGetPropertyDataSize(device_id, &property, 0, ptr::null(), &mut size)
})?;
let count = size as usize / mem::size_of::<AudioValueRange>();
let mut ranges = vec![
AudioValueRange {
mMinimum: 0.0,
mMaximum: 0.0,
};
count
];
check(unsafe {
AudioObjectGetPropertyData(
device_id,
&property,
0,
ptr::null(),
&mut size,
ranges.as_mut_ptr() as *mut _,
)
})?;
let mut rates: Vec<f64> = ranges.iter().map(|r| r.mMaximum).collect();
rates.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
rates.dedup();
Ok(rates)
}
pub fn get_device_sample_rate(device_id: AudioDeviceID) -> Result<f64> {
let property = AudioObjectPropertyAddress {
mSelector: kAudioDevicePropertyNominalSampleRate,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain,
};
let mut rate: f64 = 0.0;
let mut size = mem::size_of::<f64>() as u32;
check(unsafe {
AudioObjectGetPropertyData(
device_id,
&property,
0,
ptr::null(),
&mut size,
&mut rate as *mut _ as *mut _,
)
})?;
Ok(rate)
}
pub fn find_output_device_by_name(name: &str) -> Result<AudioDeviceID> {
let devices = list_output_devices()?;
devices
.iter()
.find(|d| d.name == name)
.map(|d| d.id)
.ok_or_else(|| DeviceError::NotFoundByName(name.to_string()))
}
struct ListenerGuard {
device_id: AudioDeviceID,
property: AudioObjectPropertyAddress,
callback: AudioObjectPropertyListenerProc,
client_data: *mut c_void,
}
impl Drop for ListenerGuard {
fn drop(&mut self) {
unsafe {
AudioObjectRemovePropertyListener(
self.device_id,
&self.property,
self.callback,
self.client_data,
);
}
}
}
unsafe extern "C" fn rate_change_callback(
_in_object_id: AudioObjectID,
_in_number_addresses: UInt32,
_in_addresses: *const AudioObjectPropertyAddress,
in_client_data: *mut c_void,
) -> OSStatus {
let tx = unsafe { &*(in_client_data as *const mpsc::SyncSender<()>) };
let _ = tx.try_send(());
0
}
pub fn set_device_sample_rate(device_id: AudioDeviceID, rate: f64) -> Result<f64> {
let property = AudioObjectPropertyAddress {
mSelector: kAudioDevicePropertyNominalSampleRate,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain,
};
let current = get_device_sample_rate(device_id)?;
if (current - rate).abs() < 0.1 {
return Ok(current);
}
let (tx, rx) = mpsc::sync_channel::<()>(1);
let client_data = &tx as *const mpsc::SyncSender<()> as *mut c_void;
check(unsafe {
AudioObjectAddPropertyListener(
device_id,
&property,
Some(rate_change_callback),
client_data,
)
})?;
let _guard = ListenerGuard {
device_id,
property,
callback: Some(rate_change_callback),
client_data,
};
check(unsafe {
AudioObjectSetPropertyData(
device_id,
&property,
0,
ptr::null(),
mem::size_of::<f64>() as u32,
&rate as *const _ as *const _,
)
})?;
const TIMEOUT: Duration = Duration::from_secs(5);
match rx.recv_timeout(TIMEOUT) {
Ok(()) => {
let actual = get_device_sample_rate(device_id)?;
if (actual - rate).abs() < 0.1 {
return Ok(actual);
}
log::warn!(
"sample rate listener fired but rate mismatch: requested {rate}Hz, device reports {actual}Hz"
);
Ok(actual)
}
Err(_) => {
let actual = get_device_sample_rate(device_id)?;
log::warn!(
"device sample rate switch timed out after {TIMEOUT:?}: requested {rate}Hz, device reports {actual}Hz"
);
Ok(actual)
}
}
}