use std::ffi::{c_char, c_void};
pub type DeviceId = u32;
const SYSTEM_OBJECT: DeviceId = 1;
const PROP_DEVICES: u32 = fourcc(b"dev#");
const PROP_DEFAULT_OUTPUT: u32 = fourcc(b"dOut");
const PROP_NAME: u32 = fourcc(b"lnam");
const PROP_UID: u32 = fourcc(b"uid ");
const PROP_STREAM_CONFIG: u32 = fourcc(b"slay");
const SCOPE_GLOBAL: u32 = fourcc(b"glob");
const SCOPE_OUTPUT: u32 = fourcc(b"outp");
const ELEMENT_MAIN: u32 = 0;
const CF_UTF8: u32 = 0x0800_0100;
const fn fourcc(s: &[u8; 4]) -> u32 {
((s[0] as u32) << 24) | ((s[1] as u32) << 16) | ((s[2] as u32) << 8) | (s[3] as u32)
}
#[repr(C)]
struct PropertyAddress {
selector: u32,
scope: u32,
element: u32,
}
impl PropertyAddress {
const fn global(selector: u32) -> Self {
PropertyAddress {
selector,
scope: SCOPE_GLOBAL,
element: ELEMENT_MAIN,
}
}
}
#[repr(C)]
struct BufferListHeader {
count: u32,
}
#[link(name = "CoreAudio", kind = "framework")]
unsafe extern "C" {
fn AudioObjectGetPropertyDataSize(
id: DeviceId,
addr: *const PropertyAddress,
qualifier_size: u32,
qualifier: *const c_void,
size: *mut u32,
) -> i32;
fn AudioObjectGetPropertyData(
id: DeviceId,
addr: *const PropertyAddress,
qualifier_size: u32,
qualifier: *const c_void,
size: *mut u32,
data: *mut c_void,
) -> i32;
fn AudioObjectSetPropertyData(
id: DeviceId,
addr: *const PropertyAddress,
qualifier_size: u32,
qualifier: *const c_void,
size: u32,
data: *const c_void,
) -> i32;
}
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFStringGetCString(s: *const c_void, buf: *mut c_char, size: isize, encoding: u32) -> u8;
fn CFRelease(cf: *const c_void);
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Device {
pub id: DeviceId,
pub name: String,
pub uid: String,
}
fn string_property(id: DeviceId, selector: u32) -> Option<String> {
let addr = PropertyAddress::global(selector);
let mut cf: *const c_void = std::ptr::null();
let mut size = std::mem::size_of::<*const c_void>() as u32;
let status = unsafe {
AudioObjectGetPropertyData(
id,
&addr,
0,
std::ptr::null(),
&mut size,
&mut cf as *mut _ as *mut c_void,
)
};
if status != 0 || cf.is_null() {
return None;
}
let mut buf = [0i8; 512];
let ok = unsafe { CFStringGetCString(cf, buf.as_mut_ptr(), buf.len() as isize, CF_UTF8) };
unsafe { CFRelease(cf) };
if ok == 0 {
return None;
}
let bytes: Vec<u8> = buf
.iter()
.take_while(|&&c| c != 0)
.map(|&c| c as u8)
.collect();
String::from_utf8(bytes).ok()
}
fn has_output(id: DeviceId) -> bool {
let addr = PropertyAddress {
selector: PROP_STREAM_CONFIG,
scope: SCOPE_OUTPUT,
element: ELEMENT_MAIN,
};
let mut size = 0u32;
if unsafe { AudioObjectGetPropertyDataSize(id, &addr, 0, std::ptr::null(), &mut size) } != 0 {
return false;
}
if (size as usize) < std::mem::size_of::<BufferListHeader>() {
return false;
}
let mut buf = vec![0u8; size as usize];
let status = unsafe {
AudioObjectGetPropertyData(
id,
&addr,
0,
std::ptr::null(),
&mut size,
buf.as_mut_ptr() as *mut c_void,
)
};
if status != 0 {
return false;
}
let header = unsafe { &*(buf.as_ptr() as *const BufferListHeader) };
header.count > 0
}
pub fn output_devices() -> Vec<Device> {
let addr = PropertyAddress::global(PROP_DEVICES);
let mut size = 0u32;
if unsafe {
AudioObjectGetPropertyDataSize(SYSTEM_OBJECT, &addr, 0, std::ptr::null(), &mut size)
} != 0
{
return Vec::new();
}
let count = size as usize / std::mem::size_of::<DeviceId>();
let mut ids = vec![0u32; count];
if unsafe {
AudioObjectGetPropertyData(
SYSTEM_OBJECT,
&addr,
0,
std::ptr::null(),
&mut size,
ids.as_mut_ptr() as *mut c_void,
)
} != 0
{
return Vec::new();
}
ids.into_iter()
.filter(|&id| has_output(id))
.map(|id| Device {
id,
name: string_property(id, PROP_NAME).unwrap_or_default(),
uid: string_property(id, PROP_UID).unwrap_or_default(),
})
.collect()
}
pub fn default_output() -> Option<Device> {
let addr = PropertyAddress::global(PROP_DEFAULT_OUTPUT);
let mut id: DeviceId = 0;
let mut size = std::mem::size_of::<DeviceId>() as u32;
let status = unsafe {
AudioObjectGetPropertyData(
SYSTEM_OBJECT,
&addr,
0,
std::ptr::null(),
&mut size,
&mut id as *mut _ as *mut c_void,
)
};
if status != 0 || id == 0 {
return None;
}
Some(Device {
id,
name: string_property(id, PROP_NAME).unwrap_or_default(),
uid: string_property(id, PROP_UID).unwrap_or_default(),
})
}
pub fn set_default_output(id: DeviceId) -> Result<(), String> {
let addr = PropertyAddress::global(PROP_DEFAULT_OUTPUT);
let status = unsafe {
AudioObjectSetPropertyData(
SYSTEM_OBJECT,
&addr,
0,
std::ptr::null(),
std::mem::size_of::<DeviceId>() as u32,
&id as *const _ as *const c_void,
)
};
if status == 0 {
Ok(())
} else {
Err(format!(
"CoreAudio refused the output switch (status {status})"
))
}
}
pub fn find_output(needle: &str) -> Option<Device> {
let needle = needle.to_ascii_lowercase();
output_devices()
.into_iter()
.find(|d| d.name.to_ascii_lowercase().contains(&needle))
}
pub fn builtin_output() -> Option<Device> {
let devices = output_devices();
devices
.iter()
.find(|d| d.uid == "BuiltInSpeakerDevice")
.or_else(|| {
devices
.iter()
.find(|d| d.name.to_ascii_lowercase().contains("built-in"))
})
.or_else(|| {
devices.iter().find(|d| {
!d.name
.to_ascii_lowercase()
.contains(super::stream::LOOPBACK_NAME)
})
})
.cloned()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fourcc_matches_the_documented_constants() {
assert_eq!(PROP_DEVICES, 0x6465_7623);
assert_eq!(PROP_DEFAULT_OUTPUT, 0x644F_7574);
assert_eq!(SCOPE_GLOBAL, 0x676C_6F62);
assert_eq!(SCOPE_OUTPUT, 0x6F75_7470);
assert_eq!(fourcc(b"lnam"), PROP_NAME);
}
#[test]
fn enumerates_real_devices_consistently() {
let devices = output_devices();
assert!(
!devices.is_empty(),
"a Mac always has at least one output device"
);
if let Some(def) = default_output() {
assert!(
devices.iter().any(|d| d.id == def.id),
"the default output should appear in the device list"
);
}
}
#[test]
fn builtin_output_never_returns_the_loopback_device() {
if let Some(d) = builtin_output() {
assert!(
!d.name
.to_ascii_lowercase()
.contains(super::super::stream::LOOPBACK_NAME),
"restoring output must never pick the loopback device"
);
}
}
#[test]
fn find_output_is_case_insensitive_and_absence_is_none() {
if let Some(first) = output_devices().into_iter().find(|d| !d.name.is_empty()) {
let shouty = first.name.to_ascii_uppercase();
assert_eq!(find_output(&shouty).map(|d| d.id), Some(first.id));
}
assert!(find_output("no such audio device anywhere").is_none());
}
}