diff options
Diffstat (limited to 'third_party/rust/cubeb-coreaudio/src/backend/tests/utils.rs')
-rw-r--r-- | third_party/rust/cubeb-coreaudio/src/backend/tests/utils.rs | 1247 |
1 files changed, 1247 insertions, 0 deletions
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/utils.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/utils.rs new file mode 100644 index 0000000000..ef07aeeeb4 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/utils.rs @@ -0,0 +1,1247 @@ +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 +} + +#[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> { + 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) { + 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> { + 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 = 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> { + 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 = unsafe { AudioComponentFindNext(ptr::null_mut(), &desc) }; + if comp.is_null() { + return None; + } + let mut unit: AudioUnit = ptr::null_mut(); + let status = 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> { + 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 = 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, + IncludeAll, +} +pub fn test_get_all_devices(filter: DeviceFilter) -> Vec<AudioObjectID> { + 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 + } + }); + } + _ => {} + } + + devices +} + +pub fn test_get_devices_in_scope(scope: Scope) -> Vec<AudioObjectID> { + 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> { + 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 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 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> { + let address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyStreamConfiguration, + mScope: match scope { + Scope::Input => kAudioDevicePropertyScopeInput, + Scope::Output => kAudioDevicePropertyScopeOutput, + }, + mElement: kAudioObjectPropertyElementMaster, + }; + let mut size: usize = 0; + let status = 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 byte_len = size / mem::size_of::<u8>(); + let mut bytes = vec![0u8; byte_len]; + let status = unsafe { + AudioObjectGetPropertyData( + id, + &address, + 0, + ptr::null(), + &mut size as *mut usize as *mut u32, + bytes.as_mut_ptr() as *mut c_void, + ) + }; + if status != NO_ERR { + return Err(status); + } + let buf_list = unsafe { &*(bytes.as_mut_ptr() as *mut AudioBufferList) }; + let buf_len = buf_list.mNumberBuffers as usize; + if buf_len == 0 { + return Ok(0); + } + let buf_ptr = buf_list.mBuffers.as_ptr() as *const AudioBuffer; + let buffers = unsafe { slice::from_raw_parts(buf_ptr, buf_len) }; + let mut channels: u32 = 0; + for buffer in buffers { + channels += buffer.mNumberChannels; + } + Ok(channels) +} + +pub fn test_device_in_scope(id: AudioObjectID, scope: Scope) -> bool { + 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); + + 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; + + unsafe { + assert_eq!( + 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); + + unsafe { + assert_eq!( + 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); + + let address = AudioObjectPropertyAddress { + mSelector: kAudioAggregateDevicePropertyMasterSubDevice, + 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> { + 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()); + 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!( + 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> { + 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 = 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> { + 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 = 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(); + } + } + } + + 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, +{ + 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> { + 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> { + 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) { + 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> { + 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 = 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 = 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> { + 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 = 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 = 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> { + let address = AudioObjectPropertyAddress { + mSelector: kAudioHardwarePropertyPlugInForBundleID, + 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, + ) + }; + 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 = 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> { + let device = test_get_default_device(scope); + device?; + let device = device.unwrap(); + let uid = 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), +{ + 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<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| { + // 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_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.unwrap(), + ); + 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), +{ + 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, + ); +} |