Source code
Revision control
Copy as Markdown
Other Tools
use super::*;
// Common Utils
// ------------------------------------------------------------------------------------------------
pub extern "C" fn noop_data_callback(
stream: *mut ffi::cubeb_stream,
_user_ptr: *mut c_void,
_input_buffer: *const c_void,
output_buffer: *mut c_void,
nframes: i64,
) -> i64 {
assert!(!stream.is_null());
// Feed silence data to output buffer
if !output_buffer.is_null() {
let stm = unsafe { &mut *(stream as *mut AudioUnitStream) };
let channels = stm.core_stream_data.output_stream_params.channels();
let samples = nframes as usize * channels as usize;
let sample_size = cubeb_sample_size(stm.core_stream_data.output_stream_params.format());
unsafe {
ptr::write_bytes(output_buffer, 0, samples * sample_size);
}
}
nframes
}
pub extern "C" fn draining_data_callback(
stream: *mut ffi::cubeb_stream,
_user_ptr: *mut c_void,
_input_buffer: *const c_void,
output_buffer: *mut c_void,
nframes: i64,
) -> i64 {
assert!(!stream.is_null());
// Feed silence data to output buffer
if !output_buffer.is_null() {
let stm = unsafe { &mut *(stream as *mut AudioUnitStream) };
let channels = stm.core_stream_data.output_stream_params.channels();
let samples = nframes as usize * channels as usize;
let sample_size = cubeb_sample_size(stm.core_stream_data.output_stream_params.format());
unsafe {
ptr::write_bytes(output_buffer, 0, samples * sample_size);
}
}
nframes - 1
}
#[derive(Default)]
pub struct StateCallbackData {
started_cnt: AtomicU32,
stopped_cnt: AtomicU32,
drained_cnt: AtomicU32,
error_cnt: AtomicU32,
}
impl StateCallbackData {
pub fn started_cnt(&self) -> u32 {
self.started_cnt.load(Ordering::SeqCst)
}
pub fn stopped_cnt(&self) -> u32 {
self.stopped_cnt.load(Ordering::SeqCst)
}
pub fn drained_cnt(&self) -> u32 {
self.drained_cnt.load(Ordering::SeqCst)
}
pub fn error_cnt(&self) -> u32 {
self.error_cnt.load(Ordering::SeqCst)
}
}
pub extern "C" fn state_tracking_cb(
stream: *mut ffi::cubeb_stream,
_usr_ptr: *mut c_void,
state: u32,
) {
let data = unsafe { (_usr_ptr as *mut StateCallbackData).as_mut() }.unwrap();
match state {
ffi::CUBEB_STATE_STARTED => {
data.started_cnt.fetch_add(1, Ordering::SeqCst);
cubeb_log!("({:p}) state is now started", stream);
}
ffi::CUBEB_STATE_STOPPED => {
data.stopped_cnt.fetch_add(1, Ordering::SeqCst);
cubeb_log!("({:p}) state is now stopped", stream);
}
ffi::CUBEB_STATE_DRAINED => {
data.drained_cnt.fetch_add(1, Ordering::SeqCst);
cubeb_log!("({:p}) state is now drained", stream);
}
ffi::CUBEB_STATE_ERROR => {
data.error_cnt.fetch_add(1, Ordering::SeqCst);
cubeb_log!("({:p}) state is now error", stream);
}
_ => unreachable!("unknown state"),
};
}
#[derive(Clone, Debug, PartialEq)]
pub enum Scope {
Input,
Output,
}
impl From<Scope> for DeviceType {
fn from(scope: Scope) -> Self {
match scope {
Scope::Input => DeviceType::INPUT,
Scope::Output => DeviceType::OUTPUT,
}
}
}
#[derive(Clone)]
pub enum PropertyScope {
Input,
Output,
}
pub fn test_get_default_device(scope: Scope) -> Option<AudioObjectID> {
debug_assert_not_running_serially();
run_serially_forward_panics(|| {
let address = AudioObjectPropertyAddress {
mSelector: match scope {
Scope::Input => kAudioHardwarePropertyDefaultInputDevice,
Scope::Output => kAudioHardwarePropertyDefaultOutputDevice,
},
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
let mut devid: AudioObjectID = kAudioObjectUnknown;
let mut size = mem::size_of::<AudioObjectID>();
let status = unsafe {
AudioObjectGetPropertyData(
kAudioObjectSystemObject,
&address,
0,
ptr::null(),
&mut size as *mut usize as *mut UInt32,
&mut devid as *mut AudioObjectID as *mut c_void,
)
};
if status != NO_ERR || devid == kAudioObjectUnknown {
return None;
}
Some(devid)
})
}
// TODO: Create a GetProperty trait and add a default implementation for it, then implement it
// for TestAudioUnit so the member method like `get_buffer_frame_size` can reuse the trait
// method get_property_data.
#[derive(Debug)]
pub struct TestAudioUnit(AudioUnit);
impl TestAudioUnit {
fn new(unit: AudioUnit) -> Self {
assert!(!unit.is_null());
Self(unit)
}
pub fn get_inner(&self) -> AudioUnit {
self.0
}
pub fn get_buffer_frame_size(
&self,
scope: Scope,
prop_scope: PropertyScope,
) -> std::result::Result<u32, OSStatus> {
test_audiounit_get_buffer_frame_size(self.0, scope, prop_scope)
}
}
impl Drop for TestAudioUnit {
fn drop(&mut self) {
run_serially_forward_panics(|| unsafe {
AudioUnitUninitialize(self.0);
AudioComponentInstanceDispose(self.0);
});
}
}
// TODO: 1. Return Result with custom errors.
// 2. Allow to create a in-out unit.
pub fn test_get_default_audiounit(scope: Scope) -> Option<TestAudioUnit> {
debug_assert_not_running_serially();
let device = test_get_default_device(scope.clone());
let unit = test_create_audiounit(ComponentSubType::HALOutput);
if device.is_none() || unit.is_none() {
return None;
}
let unit = unit.unwrap();
let device = device.unwrap();
match scope {
Scope::Input => {
if test_enable_audiounit_in_scope(unit.get_inner(), Scope::Input, true).is_err()
|| test_enable_audiounit_in_scope(unit.get_inner(), Scope::Output, false).is_err()
{
return None;
}
}
Scope::Output => {
if test_enable_audiounit_in_scope(unit.get_inner(), Scope::Input, false).is_err()
|| test_enable_audiounit_in_scope(unit.get_inner(), Scope::Output, true).is_err()
{
return None;
}
}
}
let status = run_serially(|| unsafe {
AudioUnitSetProperty(
unit.get_inner(),
kAudioOutputUnitProperty_CurrentDevice,
kAudioUnitScope_Global,
0, // Global bus
&device as *const AudioObjectID as *const c_void,
mem::size_of::<AudioObjectID>() as u32,
)
});
if status == NO_ERR {
Some(unit)
} else {
None
}
}
pub enum ComponentSubType {
HALOutput,
DefaultOutput,
}
// TODO: Return Result with custom errors.
// Surprisingly the AudioUnit can be created even when there is no any device on the platform,
// no matter its subtype is HALOutput or DefaultOutput.
pub fn test_create_audiounit(unit_type: ComponentSubType) -> Option<TestAudioUnit> {
debug_assert_not_running_serially();
let desc = AudioComponentDescription {
componentType: kAudioUnitType_Output,
componentSubType: match unit_type {
ComponentSubType::HALOutput => kAudioUnitSubType_HALOutput,
ComponentSubType::DefaultOutput => kAudioUnitSubType_DefaultOutput,
},
componentManufacturer: kAudioUnitManufacturer_Apple,
componentFlags: 0,
componentFlagsMask: 0,
};
let comp = run_serially(|| unsafe { AudioComponentFindNext(ptr::null_mut(), &desc) });
if comp.is_null() {
return None;
}
let mut unit: AudioUnit = ptr::null_mut();
let status = run_serially(|| unsafe { AudioComponentInstanceNew(comp, &mut unit) });
// TODO: Is unit possible to be null when no error returns ?
if status != NO_ERR || unit.is_null() {
None
} else {
Some(TestAudioUnit::new(unit))
}
}
fn test_enable_audiounit_in_scope(
unit: AudioUnit,
scope: Scope,
enable: bool,
) -> std::result::Result<(), OSStatus> {
debug_assert_not_running_serially();
assert!(!unit.is_null());
let (scope, element) = match scope {
Scope::Input => (kAudioUnitScope_Input, AU_IN_BUS),
Scope::Output => (kAudioUnitScope_Output, AU_OUT_BUS),
};
let on_off: u32 = if enable { 1 } else { 0 };
let status = run_serially(|| unsafe {
AudioUnitSetProperty(
unit,
kAudioOutputUnitProperty_EnableIO,
scope,
element,
&on_off as *const u32 as *const c_void,
mem::size_of::<u32>() as u32,
)
});
if status == NO_ERR {
Ok(())
} else {
Err(status)
}
}
pub enum DeviceFilter {
ExcludeCubebAggregateAndVPIO,
ExcludeVPIO,
IncludeAll,
}
pub fn test_get_all_devices(filter: DeviceFilter) -> Vec<AudioObjectID> {
debug_assert_not_running_serially();
// To avoid races, the devices getter and the device name filtering have
// to run in the same serial task. If not, a device may exist when the
// getter runs but not when getting its uid.
run_serially_forward_panics(|| {
let mut devices = Vec::new();
let address = AudioObjectPropertyAddress {
mSelector: kAudioHardwarePropertyDevices,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
let mut size: usize = 0;
let status = unsafe {
AudioObjectGetPropertyDataSize(
kAudioObjectSystemObject,
&address,
0,
ptr::null(),
&mut size as *mut usize as *mut u32,
)
};
// size will be 0 if there is no device at all.
if status != NO_ERR || size == 0 {
return devices;
}
assert_eq!(size % mem::size_of::<AudioObjectID>(), 0);
let elements = size / mem::size_of::<AudioObjectID>();
devices.resize(elements, kAudioObjectUnknown);
let status = unsafe {
AudioObjectGetPropertyData(
kAudioObjectSystemObject,
&address,
0,
ptr::null(),
&mut size as *mut usize as *mut u32,
devices.as_mut_ptr() as *mut c_void,
)
};
if status != NO_ERR {
devices.clear();
return devices;
}
for device in devices.iter() {
assert_ne!(*device, kAudioObjectUnknown);
}
match filter {
DeviceFilter::ExcludeCubebAggregateAndVPIO => {
devices.retain(|&device| {
if let Ok(uid) = get_device_global_uid(device) {
let uid = uid.into_string();
!uid.contains(PRIVATE_AGGREGATE_DEVICE_NAME)
&& !uid.contains(VOICEPROCESSING_AGGREGATE_DEVICE_NAME)
} else {
true
}
});
}
DeviceFilter::ExcludeVPIO => {
devices.retain(|&device| {
if let Ok(uid) = get_device_global_uid(device) {
let uid = uid.into_string();
!uid.contains(VOICEPROCESSING_AGGREGATE_DEVICE_NAME)
} else {
true
}
});
}
_ => {}
}
devices
})
}
pub fn test_get_devices_in_scope(scope: Scope) -> Vec<AudioObjectID> {
debug_assert_not_running_serially();
let mut devices = test_get_all_devices(DeviceFilter::ExcludeCubebAggregateAndVPIO);
devices.retain(|device| test_device_in_scope(*device, scope.clone()));
devices
}
pub fn get_devices_info_in_scope(scope: Scope) -> Vec<TestDeviceInfo> {
debug_assert_not_running_serially();
fn print_info(info: &TestDeviceInfo) {
println!("{:>4}: {}\n\tuid: {}", info.id, info.label, info.uid);
}
println!(
"\n{:?} devices\n\
--------------------",
scope
);
let mut infos = vec![];
let devices = test_get_devices_in_scope(scope.clone());
for device in devices {
infos.push(TestDeviceInfo::new(device, scope.clone()));
print_info(infos.last().unwrap());
}
println!();
infos
}
#[derive(Debug)]
pub struct TestDeviceInfo {
pub id: AudioObjectID,
pub label: String,
pub uid: String,
}
impl TestDeviceInfo {
pub fn new(id: AudioObjectID, scope: Scope) -> Self {
Self {
id,
label: Self::get_label(id, scope.clone()),
uid: Self::get_uid(id, scope),
}
}
fn get_label(id: AudioObjectID, scope: Scope) -> String {
match run_serially_forward_panics(|| get_device_uid(id, scope.into())) {
Ok(uid) => uid.into_string(),
Err(status) => format!("Unknow. Error: {}", status).to_string(),
}
}
fn get_uid(id: AudioObjectID, scope: Scope) -> String {
match run_serially_forward_panics(|| get_device_label(id, scope.into())) {
Ok(label) => label.into_string(),
Err(status) => format!("Unknown. Error: {}", status).to_string(),
}
}
}
pub fn test_device_channels_in_scope(
id: AudioObjectID,
scope: Scope,
) -> std::result::Result<u32, OSStatus> {
debug_assert_not_running_serially();
let address = AudioObjectPropertyAddress {
mSelector: kAudioDevicePropertyStreams,
mScope: match scope {
Scope::Input => kAudioDevicePropertyScopeInput,
Scope::Output => kAudioDevicePropertyScopeOutput,
},
mElement: kAudioObjectPropertyElementMaster,
};
let mut size: usize = 0;
let status = run_serially(|| unsafe {
AudioObjectGetPropertyDataSize(
id,
&address,
0,
ptr::null(),
&mut size as *mut usize as *mut u32,
)
});
if status != NO_ERR {
return Err(status);
}
if size == 0 {
return Ok(0);
}
let mut stream_list = vec![0, (size / mem::size_of::<AudioObjectID>()) as u32];
let status = run_serially(|| unsafe {
AudioObjectGetPropertyData(
id,
&address,
0,
ptr::null(),
&mut size as *mut usize as *mut u32,
stream_list.as_mut_ptr() as *mut c_void,
)
});
if status != NO_ERR {
return Err(status);
}
let channels = stream_list
.iter()
.filter(|s: &&AudioObjectID| {
if scope != Scope::Input {
return true;
}
let address = AudioObjectPropertyAddress {
mSelector: kAudioStreamPropertyTerminalType,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
let mut ttype: u32 = 0;
let status = unsafe {
AudioObjectGetPropertyData(
**s,
&address,
0,
ptr::null(),
&mut mem::size_of::<u32>() as *mut usize as *mut u32,
&mut ttype as *mut u32 as *mut c_void,
)
};
if status != NO_ERR {
return false;
}
ttype == kAudioStreamTerminalTypeMicrophone
|| (INPUT_MICROPHONE..OUTPUT_UNDEFINED).contains(&ttype)
})
.map(|s: &AudioObjectID| {
let address = AudioObjectPropertyAddress {
mSelector: kAudioStreamPropertyVirtualFormat,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
let mut format = AudioStreamBasicDescription::default();
let status = unsafe {
AudioObjectGetPropertyData(
*s,
&address,
0,
ptr::null(),
&mut mem::size_of::<AudioStreamBasicDescription>() as *mut usize as *mut u32,
&mut format as *mut AudioStreamBasicDescription as *mut c_void,
)
};
if status != NO_ERR {
return 0;
}
format.mChannelsPerFrame
})
.sum();
Ok(channels)
}
pub fn test_device_in_scope(id: AudioObjectID, scope: Scope) -> bool {
debug_assert_not_running_serially();
let channels = test_device_channels_in_scope(id, scope);
channels.is_ok() && channels.unwrap() > 0
}
pub fn test_get_all_onwed_devices(id: AudioDeviceID) -> Vec<AudioObjectID> {
assert_ne!(id, kAudioObjectUnknown);
debug_assert_running_serially();
let address = AudioObjectPropertyAddress {
mSelector: kAudioObjectPropertyOwnedObjects,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
let qualifier_data_size = mem::size_of::<AudioObjectID>();
let class_id: AudioClassID = kAudioSubDeviceClassID;
let qualifier_data = &class_id;
let mut size: usize = 0;
assert_eq!(
unsafe {
AudioObjectGetPropertyDataSize(
id,
&address,
qualifier_data_size as u32,
qualifier_data as *const u32 as *const c_void,
&mut size as *mut usize as *mut u32,
)
},
NO_ERR
);
assert_ne!(size, 0);
let elements = size / mem::size_of::<AudioObjectID>();
let mut devices: Vec<AudioObjectID> = allocate_array(elements);
assert_eq!(
unsafe {
AudioObjectGetPropertyData(
id,
&address,
qualifier_data_size as u32,
qualifier_data as *const u32 as *const c_void,
&mut size as *mut usize as *mut u32,
devices.as_mut_ptr() as *mut c_void,
)
},
NO_ERR
);
devices
}
pub fn test_get_master_device(id: AudioObjectID) -> String {
assert_ne!(id, kAudioObjectUnknown);
debug_assert_running_serially();
let address = AudioObjectPropertyAddress {
mSelector: kAudioAggregateDevicePropertyMainSubDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
let mut master: CFStringRef = ptr::null_mut();
let mut size = mem::size_of::<CFStringRef>();
assert_eq!(
audio_object_get_property_data(id, &address, &mut size, &mut master),
NO_ERR
);
assert!(!master.is_null());
let master = StringRef::new(master as _);
master.into_string()
}
pub fn test_get_drift_compensations(id: AudioObjectID) -> std::result::Result<u32, OSStatus> {
debug_assert_running_serially();
let address = AudioObjectPropertyAddress {
mSelector: kAudioSubDevicePropertyDriftCompensation,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
let mut size = mem::size_of::<u32>();
let mut compensation = u32::max_value();
let status = unsafe {
AudioObjectGetPropertyData(
id,
&address,
0,
ptr::null(),
&mut size as *mut usize as *mut u32,
&mut compensation as *mut u32 as *mut c_void,
)
};
if status == NO_ERR {
Ok(compensation)
} else {
Err(status)
}
}
pub fn test_audiounit_scope_is_enabled(unit: AudioUnit, scope: Scope) -> bool {
assert!(!unit.is_null());
debug_assert_not_running_serially();
let mut has_io: UInt32 = 0;
let (scope, element) = match scope {
Scope::Input => (kAudioUnitScope_Input, AU_IN_BUS),
Scope::Output => (kAudioUnitScope_Output, AU_OUT_BUS),
};
let mut size = mem::size_of::<UInt32>();
assert_eq!(
run_serially(|| audio_unit_get_property(
unit,
kAudioOutputUnitProperty_HasIO,
scope,
element,
&mut has_io,
&mut size
)),
NO_ERR
);
has_io != 0
}
pub fn test_audiounit_get_buffer_frame_size(
unit: AudioUnit,
scope: Scope,
prop_scope: PropertyScope,
) -> std::result::Result<u32, OSStatus> {
debug_assert_not_running_serially();
let element = match scope {
Scope::Input => AU_IN_BUS,
Scope::Output => AU_OUT_BUS,
};
let prop_scope = match prop_scope {
PropertyScope::Input => kAudioUnitScope_Input,
PropertyScope::Output => kAudioUnitScope_Output,
};
let mut buffer_frames: u32 = 0;
let mut size = mem::size_of::<u32>();
let status = run_serially(|| unsafe {
AudioUnitGetProperty(
unit,
kAudioDevicePropertyBufferFrameSize,
prop_scope,
element,
&mut buffer_frames as *mut u32 as *mut c_void,
&mut size as *mut usize as *mut u32,
)
});
if status == NO_ERR {
Ok(buffer_frames)
} else {
Err(status)
}
}
// Surprisingly it's ok to set
// 1. a unknown device
// 2. a non-input/non-output device
// 3. the current default input/output device
// as the new default input/output device by apple's API. We need to check the above things by ourselves.
// This function returns an Ok containing the previous default device id on success.
// Otherwise, it returns an Err containing the error code with OSStatus type
pub fn test_set_default_device(
device: AudioObjectID,
scope: Scope,
) -> std::result::Result<AudioObjectID, OSStatus> {
debug_assert_not_running_serially();
assert!(test_device_in_scope(device, scope.clone()));
let default = test_get_default_device(scope.clone()).unwrap();
if default == device {
// Do nothing if device is already the default device
return Ok(device);
}
let address = AudioObjectPropertyAddress {
mSelector: match scope {
Scope::Input => kAudioHardwarePropertyDefaultInputDevice,
Scope::Output => kAudioHardwarePropertyDefaultOutputDevice,
},
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
let size = mem::size_of::<AudioObjectID>();
let status = run_serially(|| unsafe {
AudioObjectSetPropertyData(
kAudioObjectSystemObject,
&address,
0,
ptr::null(),
size as u32,
&device as *const AudioObjectID as *const c_void,
)
});
let new_default = test_get_default_device(scope.clone()).unwrap();
if new_default == default {
Err(-1)
} else if status == NO_ERR {
Ok(default)
} else {
Err(status)
}
}
pub struct TestDeviceSwitcher {
scope: Scope,
devices: Vec<AudioObjectID>,
current_device_index: usize,
}
impl TestDeviceSwitcher {
pub fn new(scope: Scope) -> Self {
let infos = get_devices_info_in_scope(scope.clone());
let devices: Vec<AudioObjectID> = infos.into_iter().map(|info| info.id).collect();
let current = test_get_default_device(scope.clone()).unwrap();
let index = devices
.iter()
.position(|device| *device == current)
.unwrap();
Self {
scope,
devices,
current_device_index: index,
}
}
pub fn next(&mut self) {
let current = self.devices[self.current_device_index];
let next_index = (self.current_device_index + 1) % self.devices.len();
let next = self.devices[next_index];
println!(
"Switch device for {:?}: {} -> {}",
self.scope, current, next
);
match self.set_device(next) {
Ok(prev) => {
assert_eq!(prev, current);
self.current_device_index = next_index;
}
_ => {
self.devices.remove(next_index);
if next_index < self.current_device_index {
self.current_device_index -= 1;
}
self.next();
}
}
}
pub fn current(&self) -> AudioObjectID {
self.devices[self.current_device_index]
}
fn set_device(&self, device: AudioObjectID) -> std::result::Result<AudioObjectID, OSStatus> {
test_set_default_device(device, self.scope.clone())
}
}
pub fn test_create_device_change_listener<F>(scope: Scope, listener: F) -> TestPropertyListener<F>
where
F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus,
{
debug_assert_running_serially();
let address = AudioObjectPropertyAddress {
mSelector: match scope {
Scope::Input => kAudioHardwarePropertyDefaultInputDevice,
Scope::Output => kAudioHardwarePropertyDefaultOutputDevice,
},
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
TestPropertyListener::new(kAudioObjectSystemObject, address, listener)
}
pub struct TestPropertyListener<F>
where
F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus,
{
device: AudioObjectID,
property: AudioObjectPropertyAddress,
callback: F,
}
impl<F> TestPropertyListener<F>
where
F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus,
{
pub fn new(device: AudioObjectID, property: AudioObjectPropertyAddress, callback: F) -> Self {
Self {
device,
property,
callback,
}
}
pub fn start(&self) -> std::result::Result<(), OSStatus> {
debug_assert_running_serially();
let status = unsafe {
AudioObjectAddPropertyListener(
self.device,
&self.property,
Some(Self::render),
self as *const Self as *mut c_void,
)
};
if status == NO_ERR {
Ok(())
} else {
Err(status)
}
}
pub fn stop(&self) -> std::result::Result<(), OSStatus> {
debug_assert_running_serially();
let status = unsafe {
AudioObjectRemovePropertyListener(
self.device,
&self.property,
Some(Self::render),
self as *const Self as *mut c_void,
)
};
if status == NO_ERR {
Ok(())
} else {
Err(status)
}
}
extern "C" fn render(
id: AudioObjectID,
number_of_addresses: u32,
addresses: *const AudioObjectPropertyAddress,
data: *mut c_void,
) -> OSStatus {
let listener = unsafe { &*(data as *mut Self) };
assert_eq!(id, listener.device);
let addrs = unsafe { slice::from_raw_parts(addresses, number_of_addresses as usize) };
(listener.callback)(addrs)
}
}
impl<F> Drop for TestPropertyListener<F>
where
F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus,
{
fn drop(&mut self) {
run_serially(|| self.stop());
}
}
// TODO: It doesn't work if default input or output is an aggregate device! Probably we need to do
// the same thing as what audiounit_set_aggregate_sub_device_list does.
#[derive(Debug)]
pub struct TestDevicePlugger {
scope: Scope,
plugin_id: AudioObjectID,
device_id: AudioObjectID,
}
impl TestDevicePlugger {
pub fn new(scope: Scope) -> std::result::Result<Self, OSStatus> {
let plugin_id = Self::get_system_plugin_id()?;
Ok(Self {
scope,
plugin_id,
device_id: kAudioObjectUnknown,
})
}
pub fn get_device_id(&self) -> AudioObjectID {
self.device_id
}
pub fn plug(&mut self) -> std::result::Result<(), OSStatus> {
self.device_id = self.create_aggregate_device()?;
Ok(())
}
pub fn unplug(&mut self) -> std::result::Result<(), OSStatus> {
self.destroy_aggregate_device()
}
fn is_plugging(&self) -> bool {
self.device_id != kAudioObjectUnknown
}
fn destroy_aggregate_device(&mut self) -> std::result::Result<(), OSStatus> {
debug_assert_not_running_serially();
assert_ne!(self.plugin_id, kAudioObjectUnknown);
assert_ne!(self.device_id, kAudioObjectUnknown);
let address = AudioObjectPropertyAddress {
mSelector: kAudioPlugInDestroyAggregateDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
let mut size: usize = 0;
let status = run_serially(|| unsafe {
AudioObjectGetPropertyDataSize(
self.plugin_id,
&address,
0,
ptr::null(),
&mut size as *mut usize as *mut u32,
)
});
if status != NO_ERR {
return Err(status);
}
assert_ne!(size, 0);
let status = run_serially(|| unsafe {
// This call can simulate removing a device.
AudioObjectGetPropertyData(
self.plugin_id,
&address,
0,
ptr::null(),
&mut size as *mut usize as *mut u32,
&mut self.device_id as *mut AudioDeviceID as *mut c_void,
)
});
if status == NO_ERR {
self.device_id = kAudioObjectUnknown;
Ok(())
} else {
Err(status)
}
}
fn create_aggregate_device(&self) -> std::result::Result<AudioObjectID, OSStatus> {
debug_assert_not_running_serially();
use std::time::{SystemTime, UNIX_EPOCH};
const TEST_AGGREGATE_DEVICE_NAME: &str = "TestAggregateDevice";
assert_ne!(self.plugin_id, kAudioObjectUnknown);
let sub_devices = Self::get_sub_devices(self.scope.clone());
if sub_devices.is_none() {
return Err(kAudioCodecUnspecifiedError as OSStatus);
}
let sub_devices = sub_devices.unwrap();
let address = AudioObjectPropertyAddress {
mSelector: kAudioPlugInCreateAggregateDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
let mut size: usize = 0;
let status = run_serially(|| unsafe {
AudioObjectGetPropertyDataSize(
self.plugin_id,
&address,
0,
ptr::null(),
&mut size as *mut usize as *mut u32,
)
});
if status != NO_ERR {
return Err(status);
}
assert_ne!(size, 0);
let sys_time = SystemTime::now();
let time_id = sys_time.duration_since(UNIX_EPOCH).unwrap().as_nanos();
let device_name = format!("{}_{}", TEST_AGGREGATE_DEVICE_NAME, time_id);
let device_uid = format!("org.mozilla.{}", device_name);
let mut device_id = kAudioObjectUnknown;
let status = unsafe {
let device_dict = CFDictionaryCreateMutable(
kCFAllocatorDefault,
0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks,
);
// Set the name of this device.
let device_name = cfstringref_from_string(&device_name);
CFDictionaryAddValue(
device_dict,
cfstringref_from_static_string(AGGREGATE_DEVICE_NAME_KEY) as *const c_void,
device_name as *const c_void,
);
CFRelease(device_name as *const c_void);
// Set the uid of this device.
let device_uid = cfstringref_from_string(&device_uid);
CFDictionaryAddValue(
device_dict,
cfstringref_from_static_string(AGGREGATE_DEVICE_UID_KEY) as *const c_void,
device_uid as *const c_void,
);
CFRelease(device_uid as *const c_void);
// Make this device NOT private to the process creating it.
// On MacOS 14 devicechange events are not triggered when it is private.
let private_value: i32 = 0;
let device_private_key = CFNumberCreate(
kCFAllocatorDefault,
i64::from(kCFNumberIntType),
&private_value as *const i32 as *const c_void,
);
CFDictionaryAddValue(
device_dict,
cfstringref_from_static_string(AGGREGATE_DEVICE_PRIVATE_KEY) as *const c_void,
device_private_key as *const c_void,
);
CFRelease(device_private_key as *const c_void);
// Set this device to be a stacked aggregate (i.e. multi-output device).
let stacked_value: i32 = 0; // 1 for normal aggregate device.
let device_stacked_key = CFNumberCreate(
kCFAllocatorDefault,
i64::from(kCFNumberIntType),
&stacked_value as *const i32 as *const c_void,
);
CFDictionaryAddValue(
device_dict,
cfstringref_from_static_string(AGGREGATE_DEVICE_STACKED_KEY) as *const c_void,
device_stacked_key as *const c_void,
);
CFRelease(device_stacked_key as *const c_void);
// Set sub devices for this device.
CFDictionaryAddValue(
device_dict,
cfstringref_from_static_string(AGGREGATE_DEVICE_SUB_DEVICE_LIST_KEY)
as *const c_void,
sub_devices as *const c_void,
);
CFRelease(sub_devices as *const c_void);
// This call can simulate adding a device.
let status = {
run_serially(|| {
AudioObjectGetPropertyData(
self.plugin_id,
&address,
mem::size_of_val(&device_dict) as u32,
&device_dict as *const CFMutableDictionaryRef as *const c_void,
&mut size as *mut usize as *mut u32,
&mut device_id as *mut AudioDeviceID as *mut c_void,
)
})
};
CFRelease(device_dict as *const c_void);
status
};
if status == NO_ERR {
assert_ne!(device_id, kAudioObjectUnknown);
Ok(device_id)
} else {
Err(status)
}
}
fn get_system_plugin_id() -> std::result::Result<AudioObjectID, OSStatus> {
debug_assert_not_running_serially();
let address = AudioObjectPropertyAddress {
mSelector: kAudioHardwarePropertyPlugInForBundleID,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
let mut size: usize = 0;
let status = run_serially(|| unsafe {
AudioObjectGetPropertyDataSize(
kAudioObjectSystemObject,
&address,
0,
ptr::null(),
&mut size as *mut usize as *mut u32,
)
});
if status != NO_ERR {
return Err(status);
}
assert_ne!(size, 0);
let mut plugin_id = kAudioObjectUnknown;
let mut in_bundle_ref = cfstringref_from_static_string("com.apple.audio.CoreAudio");
let mut translation_value = AudioValueTranslation {
mInputData: &mut in_bundle_ref as *mut CFStringRef as *mut c_void,
mInputDataSize: mem::size_of::<CFStringRef>() as u32,
mOutputData: &mut plugin_id as *mut AudioObjectID as *mut c_void,
mOutputDataSize: mem::size_of::<AudioObjectID>() as u32,
};
assert_eq!(size, mem::size_of_val(&translation_value));
let status = unsafe {
let status = run_serially(|| {
AudioObjectGetPropertyData(
kAudioObjectSystemObject,
&address,
0,
ptr::null(),
&mut size as *mut usize as *mut u32,
&mut translation_value as *mut AudioValueTranslation as *mut c_void,
)
});
CFRelease(in_bundle_ref as *const c_void);
status
};
if status == NO_ERR {
assert_ne!(plugin_id, kAudioObjectUnknown);
Ok(plugin_id)
} else {
Err(status)
}
}
// TODO: This doesn't work as what we expect when the default deivce in the scope is an
// aggregate device. We should get the list of all the active sub devices and put
// them into the array, if the device is an aggregate device. See the code in
// AggregateDevice::get_sub_devices and audiounit_set_aggregate_sub_device_list.
fn get_sub_devices(scope: Scope) -> Option<CFArrayRef> {
debug_assert_not_running_serially();
let device = test_get_default_device(scope);
device?;
let device = device.unwrap();
let uid = run_serially(|| get_device_global_uid(device));
if uid.is_err() {
return None;
}
let uid = uid.unwrap();
unsafe {
let list = CFArrayCreateMutable(ptr::null(), 0, &kCFTypeArrayCallBacks);
let sub_device_dict = CFDictionaryCreateMutable(
ptr::null(),
0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks,
);
CFDictionaryAddValue(
sub_device_dict,
cfstringref_from_static_string(SUB_DEVICE_UID_KEY) as *const c_void,
uid.get_raw() as *const c_void,
);
CFArrayAppendValue(list, sub_device_dict as *const c_void);
CFRelease(sub_device_dict as *const c_void);
Some(list)
}
}
}
impl Drop for TestDevicePlugger {
fn drop(&mut self) {
if self.is_plugging() {
self.unplug();
}
}
}
// Test Templates
// ------------------------------------------------------------------------------------------------
pub fn test_ops_context_operation<F>(name: &'static str, operation: F)
where
F: FnOnce(*mut ffi::cubeb),
{
debug_assert_not_running_serially();
let name_c_string = CString::new(name).expect("Failed to create context name");
let mut context = ptr::null_mut::<ffi::cubeb>();
assert_eq!(
unsafe { OPS.init.unwrap()(&mut context, name_c_string.as_ptr()) },
ffi::CUBEB_OK
);
assert!(!context.is_null());
operation(context);
unsafe { OPS.destroy.unwrap()(context) }
}
// The in-out stream initializeed with different device will create an aggregate_device and
// result in firing device-collection-changed callbacks. Run in-out streams with tests
// capturing device-collection-changed callbacks may cause troubles.
pub fn test_ops_stream_operation_on_context<F>(
name: &'static str,
context_ptr: *mut ffi::cubeb,
input_device: ffi::cubeb_devid,
input_stream_params: *mut ffi::cubeb_stream_params,
output_device: ffi::cubeb_devid,
output_stream_params: *mut ffi::cubeb_stream_params,
latency_frames: u32,
data_callback: ffi::cubeb_data_callback,
state_callback: ffi::cubeb_state_callback,
user_ptr: *mut c_void,
operation: F,
) where
F: FnOnce(*mut ffi::cubeb_stream),
{
// Do nothing if there is no input/output device to perform input/output tests.
if !input_stream_params.is_null() && test_get_default_device(Scope::Input).is_none() {
println!("No input device to perform input tests for \"{}\".", name);
return;
}
if !output_stream_params.is_null() && test_get_default_device(Scope::Output).is_none() {
println!("No output device to perform output tests for \"{}\".", name);
return;
}
let mut stream: *mut ffi::cubeb_stream = ptr::null_mut();
let stream_name = CString::new(name).expect("Failed to create stream name");
assert_eq!(
unsafe {
OPS.stream_init.unwrap()(
context_ptr,
&mut stream,
stream_name.as_ptr(),
input_device,
input_stream_params,
output_device,
output_stream_params,
latency_frames,
data_callback,
state_callback,
user_ptr,
)
},
ffi::CUBEB_OK
);
assert!(!stream.is_null());
operation(stream);
unsafe {
OPS.stream_destroy.unwrap()(stream);
}
}
pub fn test_ops_stream_operation<F>(
name: &'static str,
input_device: ffi::cubeb_devid,
input_stream_params: *mut ffi::cubeb_stream_params,
output_device: ffi::cubeb_devid,
output_stream_params: *mut ffi::cubeb_stream_params,
latency_frames: u32,
data_callback: ffi::cubeb_data_callback,
state_callback: ffi::cubeb_state_callback,
user_ptr: *mut c_void,
operation: F,
) where
F: FnOnce(*mut ffi::cubeb_stream),
{
test_ops_context_operation("context: stream operation", |context_ptr| {
test_ops_stream_operation_on_context(
name,
context_ptr,
input_device,
input_stream_params,
output_device,
output_stream_params,
latency_frames,
data_callback,
state_callback,
user_ptr,
operation,
);
});
}
pub fn test_get_raw_context<F>(operation: F)
where
F: FnOnce(&mut AudioUnitContext),
{
let mut context = AudioUnitContext::new();
operation(&mut context);
}
pub fn test_get_default_raw_stream<F>(operation: F)
where
F: FnOnce(&mut AudioUnitStream),
{
test_get_raw_stream(ptr::null_mut(), None, None, 0, operation);
}
fn test_get_raw_stream<F>(
user_ptr: *mut c_void,
data_callback: ffi::cubeb_data_callback,
state_callback: ffi::cubeb_state_callback,
latency_frames: u32,
operation: F,
) where
F: FnOnce(&mut AudioUnitStream),
{
let mut context = AudioUnitContext::new();
// Add a stream to the context since we are about to create one.
// AudioUnitStream::drop() will check the context has at least one stream.
let global_latency_frames = context.update_latency_by_adding_stream(latency_frames);
let mut stream = AudioUnitStream::new(
&mut context,
user_ptr,
data_callback,
state_callback,
global_latency_frames,
);
stream.core_stream_data = CoreStreamData::new(&stream, None, None);
operation(&mut stream);
}
pub fn test_get_stream_with_default_data_callback_by_type<F>(
name: &'static str,
stm_type: StreamType,
input_device: Option<AudioObjectID>,
output_device: Option<AudioObjectID>,
state_callback: extern "C" fn(*mut ffi::cubeb_stream, *mut c_void, ffi::cubeb_state),
data: *mut c_void,
operation: F,
) where
F: FnOnce(&mut AudioUnitStream),
{
debug_assert_not_running_serially();
let mut input_params = get_dummy_stream_params(Scope::Input);
let mut output_params = get_dummy_stream_params(Scope::Output);
let in_params = if stm_type.contains(StreamType::INPUT) {
&mut input_params as *mut ffi::cubeb_stream_params
} else {
ptr::null_mut()
};
let out_params = if stm_type.contains(StreamType::OUTPUT) {
&mut output_params as *mut ffi::cubeb_stream_params
} else {
ptr::null_mut()
};
let in_device = if let Some(id) = input_device {
id as ffi::cubeb_devid
} else {
ptr::null_mut()
};
let out_device = if let Some(id) = output_device {
id as ffi::cubeb_devid
} else {
ptr::null_mut()
};
test_ops_stream_operation_with_default_data_callback(
name,
in_device,
in_params,
out_device,
out_params,
state_callback,
data,
|stream| {
let stm = unsafe { &mut *(stream as *mut AudioUnitStream) };
operation(stm);
},
);
}
bitflags! {
pub struct StreamType: u8 {
const INPUT = 0x01;
const OUTPUT = 0x02;
const DUPLEX = 0x03;
}
}
fn get_dummy_stream_params(scope: Scope) -> ffi::cubeb_stream_params {
// The stream format for input and output must be same.
const STREAM_FORMAT: u32 = ffi::CUBEB_SAMPLE_FLOAT32NE;
// Make sure the parameters meet the requirements of AudioUnitContext::stream_init
// (in the comments).
let mut stream_params = ffi::cubeb_stream_params::default();
stream_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
let (format, rate, channels, layout) = match scope {
Scope::Input => (STREAM_FORMAT, 48000, 1, ffi::CUBEB_LAYOUT_MONO),
Scope::Output => (STREAM_FORMAT, 44100, 2, ffi::CUBEB_LAYOUT_STEREO),
};
stream_params.format = format;
stream_params.rate = rate;
stream_params.channels = channels;
stream_params.layout = layout;
stream_params
}
fn test_ops_stream_operation_with_default_data_callback<F>(
name: &'static str,
input_device: ffi::cubeb_devid,
input_stream_params: *mut ffi::cubeb_stream_params,
output_device: ffi::cubeb_devid,
output_stream_params: *mut ffi::cubeb_stream_params,
state_callback: extern "C" fn(*mut ffi::cubeb_stream, *mut c_void, ffi::cubeb_state),
data: *mut c_void,
operation: F,
) where
F: FnOnce(*mut ffi::cubeb_stream),
{
test_ops_stream_operation(
name,
input_device,
input_stream_params,
output_device,
output_stream_params,
4096, // TODO: Get latency by get_min_latency instead ?
Some(noop_data_callback),
Some(state_callback),
data,
operation,
);
}