use std::ffi::c_void;
use objc::runtime::Object;
#[allow(unused_imports)]
use objc::{msg_send, sel, sel_impl};
use serde::{Deserialize, Serialize};
use tracing::debug;
use super::ffi::{
cf_string_to_string, ns_string_from_str, objc_class, AudioObjectGetPropertyData,
AudioObjectGetPropertyDataSize, AudioObjectPropertyAddress,
K_AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN, K_AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
K_AUDIO_OBJECT_PROPERTY_SCOPE_INPUT, K_AUDIO_OBJECT_PROPERTY_SCOPE_OUTPUT,
K_AUDIO_OBJECT_PROPERTY_SELECTOR_DEFAULT_INPUT,
K_AUDIO_OBJECT_PROPERTY_SELECTOR_DEFAULT_OUTPUT, K_AUDIO_OBJECT_PROPERTY_SELECTOR_DEVICES,
K_AUDIO_OBJECT_PROPERTY_SELECTOR_NAME, K_AUDIO_OBJECT_PROPERTY_SELECTOR_NOMINAL_SAMPLE_RATE,
K_AUDIO_OBJECT_PROPERTY_SELECTOR_STREAMS, K_AUDIO_OBJECT_SYSTEM_OBJECT,
};
use super::AudioError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioDevice {
pub name: String,
pub id: String,
pub is_input: bool,
pub is_output: bool,
pub sample_rate: f64,
pub is_default_input: bool,
pub is_default_output: bool,
}
pub fn check_microphone_permission() -> Result<(), AudioError> {
let status = query_av_authorization_status();
match status {
3 => Ok(()),
0 => {
debug!("Microphone permission not determined, will prompt on first capture");
Ok(())
}
_ => Err(AudioError::PermissionDenied),
}
}
#[must_use]
pub fn list_audio_devices() -> Vec<AudioDevice> {
let device_ids = query_device_ids();
if device_ids.is_empty() {
return vec![];
}
let default_input = query_default_device(K_AUDIO_OBJECT_PROPERTY_SELECTOR_DEFAULT_INPUT);
let default_output = query_default_device(K_AUDIO_OBJECT_PROPERTY_SELECTOR_DEFAULT_OUTPUT);
device_ids
.iter()
.filter_map(|&id| build_audio_device(id, default_input, default_output))
.collect()
}
fn query_av_authorization_status() -> i64 {
let media_type = ns_string_from_str("soun");
let cls = objc_class("AVCaptureDevice");
if cls.is_null() || media_type.is_null() {
return 3; }
unsafe { msg_send![cls, authorizationStatusForMediaType: media_type] }
}
fn query_device_ids() -> Vec<u32> {
let addr = AudioObjectPropertyAddress {
selector: K_AUDIO_OBJECT_PROPERTY_SELECTOR_DEVICES,
scope: K_AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
element: K_AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN,
};
let mut size: u32 = 0;
let status = unsafe {
AudioObjectGetPropertyDataSize(
K_AUDIO_OBJECT_SYSTEM_OBJECT,
&addr,
0,
std::ptr::null(),
&mut size,
)
};
if status != 0 || size == 0 {
return vec![];
}
let count = size as usize / std::mem::size_of::<u32>();
let mut ids = vec![0u32; count];
let mut actual = size;
let status = unsafe {
AudioObjectGetPropertyData(
K_AUDIO_OBJECT_SYSTEM_OBJECT,
&addr,
0,
std::ptr::null(),
&mut actual,
ids.as_mut_ptr().cast::<c_void>(),
)
};
if status != 0 {
return vec![];
}
ids
}
fn query_default_device(selector: u32) -> u32 {
let addr = AudioObjectPropertyAddress {
selector,
scope: K_AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
element: K_AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN,
};
let mut device_id: u32 = 0;
let mut size = std::mem::size_of::<u32>() as u32;
let status = unsafe {
AudioObjectGetPropertyData(
K_AUDIO_OBJECT_SYSTEM_OBJECT,
&addr,
0,
std::ptr::null(),
&mut size,
(&mut device_id as *mut u32).cast::<c_void>(),
)
};
if status == 0 {
device_id
} else {
0
}
}
fn build_audio_device(id: u32, default_input: u32, default_output: u32) -> Option<AudioDevice> {
let name = query_device_name(id)?;
let is_input = device_has_streams(id, K_AUDIO_OBJECT_PROPERTY_SCOPE_INPUT);
let is_output = device_has_streams(id, K_AUDIO_OBJECT_PROPERTY_SCOPE_OUTPUT);
let sample_rate = query_nominal_sample_rate(id);
Some(AudioDevice {
name,
id: id.to_string(),
is_input,
is_output,
sample_rate,
is_default_input: id == default_input,
is_default_output: id == default_output,
})
}
fn query_device_name(device_id: u32) -> Option<String> {
let addr = AudioObjectPropertyAddress {
selector: K_AUDIO_OBJECT_PROPERTY_SELECTOR_NAME,
scope: K_AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
element: K_AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN,
};
let mut cf_str: *mut Object = std::ptr::null_mut();
let mut size = std::mem::size_of::<*mut Object>() as u32;
let status = unsafe {
AudioObjectGetPropertyData(
device_id,
&addr,
0,
std::ptr::null(),
&mut size,
(&mut cf_str as *mut *mut Object).cast::<c_void>(),
)
};
if status != 0 || cf_str.is_null() {
return None;
}
Some(cf_string_to_string(cf_str as *const c_void))
}
fn device_has_streams(device_id: u32, scope: u32) -> bool {
let addr = AudioObjectPropertyAddress {
selector: K_AUDIO_OBJECT_PROPERTY_SELECTOR_STREAMS,
scope,
element: K_AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN,
};
let mut size: u32 = 0;
let status =
unsafe { AudioObjectGetPropertyDataSize(device_id, &addr, 0, std::ptr::null(), &mut size) };
status == 0 && size > 0
}
fn query_nominal_sample_rate(device_id: u32) -> f64 {
let addr = AudioObjectPropertyAddress {
selector: K_AUDIO_OBJECT_PROPERTY_SELECTOR_NOMINAL_SAMPLE_RATE,
scope: K_AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
element: K_AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN,
};
let mut rate: f64 = 0.0;
let mut size = std::mem::size_of::<f64>() as u32;
let status = unsafe {
AudioObjectGetPropertyData(
device_id,
&addr,
0,
std::ptr::null(),
&mut size,
(&mut rate as *mut f64).cast::<c_void>(),
)
};
if status == 0 {
rate
} else {
0.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_microphone_permission_returns_result() {
let result = check_microphone_permission();
match result {
Ok(()) => {}
Err(AudioError::PermissionDenied) => {}
Err(e) => panic!("Unexpected error type: {e}"),
}
}
#[test]
fn list_audio_devices_returns_vec() {
let devices = list_audio_devices();
for d in &devices {
assert!(!d.name.is_empty(), "device name must not be empty");
assert!(!d.id.is_empty(), "device id must not be empty");
}
}
#[test]
fn list_audio_devices_serializes_to_json() {
let devices = list_audio_devices();
let json = serde_json::to_string(&devices).unwrap();
assert!(json.starts_with('['));
}
}