use std::ffi::c_void;
use std::ptr::NonNull;
use objc2_core_audio::{
kAudioDevicePropertyDeviceUID, kAudioDevicePropertyNominalSampleRate,
kAudioDevicePropertyStreamConfiguration, kAudioDevicePropertyStreams,
kAudioHardwarePropertyDefaultOutputDevice, kAudioHardwarePropertyDevices,
kAudioObjectPropertyElementMain, kAudioObjectPropertyName, kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyScopeOutput, kAudioObjectSystemObject, AudioObjectGetPropertyData,
AudioObjectGetPropertyDataSize, AudioObjectID, AudioObjectPropertyAddress,
};
use objc2_core_audio_types::AudioBufferList;
use objc2_core_foundation::{CFRetained, CFString};
use flexaudio_core::types::{DeviceInfo, Result, SourceKind};
use crate::common::FALLBACK_FORMAT;
fn address(selector: u32, scope: u32) -> AudioObjectPropertyAddress {
AudioObjectPropertyAddress {
mSelector: selector,
mScope: scope,
mElement: kAudioObjectPropertyElementMain,
}
}
fn all_device_ids() -> Vec<AudioObjectID> {
let addr = address(
kAudioHardwarePropertyDevices,
kAudioObjectPropertyScopeGlobal,
);
let mut size: u32 = 0;
let status = unsafe {
AudioObjectGetPropertyDataSize(
kAudioObjectSystemObject as AudioObjectID,
NonNull::from(&addr),
0,
core::ptr::null(),
NonNull::from(&mut size),
)
};
if status != 0 || size == 0 {
return Vec::new();
}
let count = size as usize / core::mem::size_of::<AudioObjectID>();
let mut ids: Vec<AudioObjectID> = vec![0; count];
let status = unsafe {
AudioObjectGetPropertyData(
kAudioObjectSystemObject as AudioObjectID,
NonNull::from(&addr),
0,
core::ptr::null(),
NonNull::from(&mut size),
NonNull::new_unchecked(ids.as_mut_ptr().cast::<c_void>()),
)
};
if status != 0 {
return Vec::new();
}
let written = size as usize / core::mem::size_of::<AudioObjectID>();
ids.truncate(written);
ids
}
fn read_cfstring_property(object: AudioObjectID, selector: u32, scope: u32) -> Option<String> {
let addr = address(selector, scope);
let mut cf_ref: *const CFString = core::ptr::null();
let mut size = core::mem::size_of::<*const CFString>() as u32;
let status = unsafe {
AudioObjectGetPropertyData(
object,
NonNull::from(&addr),
0,
core::ptr::null(),
NonNull::from(&mut size),
NonNull::new_unchecked((&mut cf_ref as *mut *const CFString).cast::<c_void>()),
)
};
if status != 0 || cf_ref.is_null() {
return None;
}
let cf = unsafe { CFRetained::from_raw(NonNull::new_unchecked(cf_ref as *mut CFString)) };
Some(cf.to_string())
}
fn device_sample_rate(device: AudioObjectID) -> Option<u32> {
let addr = address(
kAudioDevicePropertyNominalSampleRate,
kAudioObjectPropertyScopeOutput,
);
let mut rate: f64 = 0.0;
let mut size = core::mem::size_of::<f64>() as u32;
let status = unsafe {
AudioObjectGetPropertyData(
device,
NonNull::from(&addr),
0,
core::ptr::null(),
NonNull::from(&mut size),
NonNull::new_unchecked((&mut rate as *mut f64).cast::<c_void>()),
)
};
if status != 0 || rate <= 0.0 {
return None;
}
Some(rate as u32)
}
fn device_channels(device: AudioObjectID) -> Option<u16> {
let addr = address(
kAudioDevicePropertyStreamConfiguration,
kAudioObjectPropertyScopeOutput,
);
let mut size: u32 = 0;
let status = unsafe {
AudioObjectGetPropertyDataSize(
device,
NonNull::from(&addr),
0,
core::ptr::null(),
NonNull::from(&mut size),
)
};
if status != 0 || size == 0 {
return None;
}
let elem = core::mem::size_of::<AudioBufferList>();
let count = (size as usize).div_ceil(elem).max(1);
let mut storage: Vec<AudioBufferList> = vec![unsafe { core::mem::zeroed() }; count];
let status = unsafe {
AudioObjectGetPropertyData(
device,
NonNull::from(&addr),
0,
core::ptr::null(),
NonNull::from(&mut size),
NonNull::new_unchecked(storage.as_mut_ptr().cast::<c_void>()),
)
};
if status != 0 {
return None;
}
let list = storage.as_ptr();
let num_buffers = unsafe { (*list).mNumberBuffers } as usize;
if num_buffers == 0 {
return None;
}
let buffers = unsafe { std::slice::from_raw_parts((*list).mBuffers.as_ptr(), num_buffers) };
let total: u32 = buffers.iter().map(|b| b.mNumberChannels).sum();
if total == 0 {
return None;
}
Some(total.min(u16::MAX as u32) as u16)
}
fn is_output_device(device: AudioObjectID) -> bool {
let addr = address(kAudioDevicePropertyStreams, kAudioObjectPropertyScopeOutput);
let mut size: u32 = 0;
let status = unsafe {
AudioObjectGetPropertyDataSize(
device,
NonNull::from(&addr),
0,
core::ptr::null(),
NonNull::from(&mut size),
)
};
status == 0 && size > 0
}
fn default_output_device() -> AudioObjectID {
let addr = address(
kAudioHardwarePropertyDefaultOutputDevice,
kAudioObjectPropertyScopeGlobal,
);
let mut device: AudioObjectID = 0;
let mut size = core::mem::size_of::<AudioObjectID>() as u32;
let status = unsafe {
AudioObjectGetPropertyData(
kAudioObjectSystemObject as AudioObjectID,
NonNull::from(&addr),
0,
core::ptr::null(),
NonNull::from(&mut size),
NonNull::new_unchecked((&mut device as *mut AudioObjectID).cast::<c_void>()),
)
};
if status != 0 {
return 0;
}
device
}
fn device_name(device: AudioObjectID) -> Option<String> {
read_cfstring_property(
device,
kAudioObjectPropertyName,
kAudioObjectPropertyScopeGlobal,
)
}
fn device_uid(device: AudioObjectID) -> Option<String> {
read_cfstring_property(
device,
kAudioDevicePropertyDeviceUID,
kAudioObjectPropertyScopeGlobal,
)
}
pub fn list_output_devices() -> Result<Vec<DeviceInfo>> {
let default_id = default_output_device();
let mut out: Vec<DeviceInfo> = Vec::new();
for id in all_device_ids() {
if !is_output_device(id) {
continue;
}
let Some(name) = device_name(id) else {
continue;
};
let (fallback_rate, fallback_ch) = FALLBACK_FORMAT;
out.push(DeviceInfo {
id: name.clone(),
name,
source_kind: SourceKind::SystemLoopback,
sample_rate: device_sample_rate(id).unwrap_or(fallback_rate),
channels: device_channels(id).unwrap_or(fallback_ch),
is_loopback: true,
is_default: id == default_id && default_id != 0,
});
}
Ok(out)
}
pub(crate) fn uid_for_device_name(name: &str) -> Option<String> {
for id in all_device_ids() {
if !is_output_device(id) {
continue;
}
if device_name(id).as_deref() == Some(name) {
return device_uid(id);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn list_output_devices_is_well_formed() {
let devices = list_output_devices().expect("list_output_devices should not error");
for d in &devices {
assert_eq!(d.source_kind, SourceKind::SystemLoopback);
assert!(d.is_loopback);
assert!(!d.id.is_empty());
assert_eq!(d.id, d.name);
assert!(d.sample_rate > 0);
assert!(d.channels > 0);
}
}
#[test]
fn uid_for_unknown_device_is_none() {
assert!(uid_for_device_name("flexaudio-no-such-output-device-xyzzy").is_none());
}
}