diff options
Diffstat (limited to 'third_party/rust/cubeb-coreaudio/src/backend/aggregate_device.rs')
-rw-r--r-- | third_party/rust/cubeb-coreaudio/src/backend/aggregate_device.rs | 691 |
1 files changed, 691 insertions, 0 deletions
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/aggregate_device.rs b/third_party/rust/cubeb-coreaudio/src/backend/aggregate_device.rs new file mode 100644 index 0000000000..2738631b87 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/aggregate_device.rs @@ -0,0 +1,691 @@ +use super::*; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub const DRIFT_COMPENSATION: u32 = 1; + +#[derive(Debug)] +pub struct AggregateDevice { + plugin_id: AudioObjectID, + device_id: AudioObjectID, + // For log only + input_id: AudioObjectID, + output_id: AudioObjectID, +} + +#[derive(Debug)] +pub enum Error { + OS(OSStatus), + Timeout(std::time::Duration), + LessThan2Devices(usize), +} + +impl From<OSStatus> for Error { + fn from(status: OSStatus) -> Self { + Error::OS(status) + } +} + +impl From<std::time::Duration> for Error { + fn from(duration: std::time::Duration) -> Self { + Error::Timeout(duration) + } +} + +impl From<usize> for Error { + fn from(number: usize) -> Self { + Error::LessThan2Devices(number) + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::OS(status) => write!(f, "OSStatus({})", status), + Error::Timeout(duration) => write!(f, "Timeout({:?})", duration), + Error::LessThan2Devices(number) => write!(f, "LessThan2Devices({} only)", number), + } + } +} + +impl AggregateDevice { + // Aggregate Device is a virtual audio interface which utilizes inputs and outputs + // of one or more physical audio interfaces. It is possible to use the clock of + // one of the devices as a master clock for all the combined devices and enable + // drift compensation for the devices that are not designated clock master. + // + // Creating a new aggregate device programmatically requires [0][1]: + // 1. Locate the base plug-in ("com.apple.audio.CoreAudio") + // 2. Create a dictionary that describes the aggregate device + // (don't add sub-devices in that step, prone to fail [0]) + // 3. Ask the base plug-in to create the aggregate device (blank) + // 4. Add the array of sub-devices. + // 5. Set the master device (1st output device in our case) + // 6. Enable drift compensation for the non-master devices + // + // [0] https://lists.apple.com/archives/coreaudio-api/2006/Apr/msg00092.html + // [1] https://lists.apple.com/archives/coreaudio-api/2005/Jul/msg00150.html + // [2] CoreAudio.framework/Headers/AudioHardware.h + pub fn new( + input_id: AudioObjectID, + output_id: AudioObjectID, + ) -> std::result::Result<Self, Error> { + let plugin_id = Self::get_system_plugin_id()?; + let device_id = Self::create_blank_device_sync(plugin_id)?; + + let mut cleanup = finally(|| { + let r = Self::destroy_device(plugin_id, device_id); + assert!(r.is_ok()); + }); + + Self::set_sub_devices_sync(device_id, input_id, output_id)?; + Self::set_master_device(device_id, output_id)?; + Self::activate_clock_drift_compensation(device_id)?; + Self::workaround_for_airpod(device_id, input_id, output_id)?; + + cleanup.dismiss(); + + cubeb_log!( + "Add devices input {} and output {} into an aggregate device {}", + input_id, + output_id, + device_id + ); + Ok(Self { + plugin_id, + device_id, + input_id, + output_id, + }) + } + + pub fn get_device_id(&self) -> AudioObjectID { + self.device_id + } + + // The following APIs are set to `pub` for testing purpose. + pub fn get_system_plugin_id() -> std::result::Result<AudioObjectID, Error> { + let address = AudioObjectPropertyAddress { + mSelector: kAudioHardwarePropertyPlugInForBundleID, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + let mut size: usize = 0; + let status = + audio_object_get_property_data_size(kAudioObjectSystemObject, &address, &mut size); + if status != NO_ERR { + return Err(Error::from(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 = audio_object_get_property_data( + kAudioObjectSystemObject, + &address, + &mut size, + &mut translation_value, + ); + unsafe { + CFRelease(in_bundle_ref as *const c_void); + } + if status == NO_ERR { + assert_ne!(plugin_id, kAudioObjectUnknown); + Ok(plugin_id) + } else { + Err(Error::from(status)) + } + } + + pub fn create_blank_device_sync( + plugin_id: AudioObjectID, + ) -> std::result::Result<AudioObjectID, Error> { + let waiting_time = Duration::new(5, 0); + + let condvar_pair = Arc::new((Mutex::new(Vec::<AudioObjectID>::new()), Condvar::new())); + let mut cloned_condvar_pair = condvar_pair.clone(); + let data_ptr = &mut cloned_condvar_pair as *mut Arc<(Mutex<Vec<AudioObjectID>>, Condvar)>; + + let address = get_property_address( + Property::HardwareDevices, + DeviceType::INPUT | DeviceType::OUTPUT, + ); + + let status = audio_object_add_property_listener( + kAudioObjectSystemObject, + &address, + devices_changed_callback, + data_ptr as *mut c_void, + ); + assert_eq!(status, NO_ERR); + + let _teardown = finally(|| { + let status = audio_object_remove_property_listener( + kAudioObjectSystemObject, + &address, + devices_changed_callback, + data_ptr as *mut c_void, + ); + assert_eq!(status, NO_ERR); + }); + + let device = Self::create_blank_device(plugin_id)?; + + // Wait until the aggregate is created. + let (lock, cvar) = &*condvar_pair; + let devices = lock.lock().unwrap(); + if !devices.contains(&device) { + let (devs, timeout_res) = cvar.wait_timeout(devices, waiting_time).unwrap(); + if timeout_res.timed_out() { + cubeb_log!( + "Time out for waiting the creation of aggregate device {}!", + device + ); + } + if !devs.contains(&device) { + return Err(Error::from(waiting_time)); + } + } + + extern "C" fn devices_changed_callback( + id: AudioObjectID, + _number_of_addresses: u32, + _addresses: *const AudioObjectPropertyAddress, + data: *mut c_void, + ) -> OSStatus { + assert_eq!(id, kAudioObjectSystemObject); + let pair = unsafe { &mut *(data as *mut Arc<(Mutex<Vec<AudioObjectID>>, Condvar)>) }; + let (lock, cvar) = &**pair; + let mut devices = lock.lock().unwrap(); + *devices = audiounit_get_devices(); + cvar.notify_one(); + NO_ERR + } + + Ok(device) + } + + pub fn create_blank_device( + plugin_id: AudioObjectID, + ) -> std::result::Result<AudioObjectID, Error> { + assert_ne!(plugin_id, kAudioObjectUnknown); + + let address = AudioObjectPropertyAddress { + mSelector: kAudioPlugInCreateAggregateDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + let mut size: usize = 0; + let status = audio_object_get_property_data_size(plugin_id, &address, &mut size); + if status != NO_ERR { + return Err(Error::from(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!("{}_{}", PRIVATE_AGGREGATE_DEVICE_NAME, time_id); + let device_uid = format!("org.mozilla.{}", device_name); + + let mut device_id = kAudioObjectUnknown; + let status = unsafe { + let device_dict = CFMutableDictRef::default(); + + // Set the name of the device. + let device_name = cfstringref_from_string(&device_name); + device_dict.add_value( + 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 the device. + let device_uid = cfstringref_from_string(&device_uid); + device_dict.add_value( + 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 the device private to the process creating it. + let private_value: i32 = 1; + let device_private_key = CFNumberCreate( + kCFAllocatorDefault, + i64::from(kCFNumberIntType), + &private_value as *const i32 as *const c_void, + ); + device_dict.add_value( + 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 the device to 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, + ); + device_dict.add_value( + 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); + + // This call will fire `audiounit_collection_changed_callback` indirectly! + audio_object_get_property_data_with_qualifier( + plugin_id, + &address, + mem::size_of_val(&device_dict), + &device_dict, + &mut size, + &mut device_id, + ) + }; + if status == NO_ERR { + assert_ne!(device_id, kAudioObjectUnknown); + Ok(device_id) + } else { + Err(Error::from(status)) + } + } + + pub fn set_sub_devices_sync( + device_id: AudioDeviceID, + input_id: AudioDeviceID, + output_id: AudioDeviceID, + ) -> std::result::Result<(), Error> { + let address = AudioObjectPropertyAddress { + mSelector: kAudioAggregateDevicePropertyFullSubDeviceList, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + let waiting_time = Duration::new(5, 0); + + let condvar_pair = Arc::new((Mutex::new(AudioObjectID::default()), Condvar::new())); + let mut cloned_condvar_pair = condvar_pair.clone(); + let data_ptr = &mut cloned_condvar_pair as *mut Arc<(Mutex<AudioObjectID>, Condvar)>; + + let status = audio_object_add_property_listener( + device_id, + &address, + devices_changed_callback, + data_ptr as *mut c_void, + ); + if status != NO_ERR { + return Err(Error::from(status)); + } + + let remove_listener = || -> OSStatus { + audio_object_remove_property_listener( + device_id, + &address, + devices_changed_callback, + data_ptr as *mut c_void, + ) + }; + + Self::set_sub_devices(device_id, input_id, output_id)?; + + // Wait until the sub devices are added. + let (lock, cvar) = &*condvar_pair; + let device = lock.lock().unwrap(); + if *device != device_id { + let (dev, timeout_res) = cvar.wait_timeout(device, waiting_time).unwrap(); + if timeout_res.timed_out() { + cubeb_log!( + "Time out for waiting for adding devices({}, {}) to aggregate device {}!", + input_id, + output_id, + device_id + ); + } + if *dev != device_id { + let status = remove_listener(); + // If the error is kAudioHardwareBadObjectError, it implies `device_id` is somehow + // dead, so its listener should receive nothing. It's ok to leave here. + assert!(status == NO_ERR || status == (kAudioHardwareBadObjectError as OSStatus)); + // TODO: Destroy the aggregate device immediately if error is not + // kAudioHardwareBadObjectError. Otherwise the `devices_changed_callback` is able + // to touch the `cloned_condvar_pair` after it's freed. + return Err(Error::from(waiting_time)); + } + } + + extern "C" fn devices_changed_callback( + id: AudioObjectID, + _number_of_addresses: u32, + _addresses: *const AudioObjectPropertyAddress, + data: *mut c_void, + ) -> OSStatus { + let pair = unsafe { &mut *(data as *mut Arc<(Mutex<AudioObjectID>, Condvar)>) }; + let (lock, cvar) = &**pair; + let mut device = lock.lock().unwrap(); + *device = id; + cvar.notify_one(); + NO_ERR + } + + let status = remove_listener(); + assert_eq!(status, NO_ERR); + Ok(()) + } + + pub fn set_sub_devices( + device_id: AudioDeviceID, + input_id: AudioDeviceID, + output_id: AudioDeviceID, + ) -> std::result::Result<(), Error> { + assert_ne!(device_id, kAudioObjectUnknown); + assert_ne!(input_id, kAudioObjectUnknown); + assert_ne!(output_id, kAudioObjectUnknown); + assert_ne!(input_id, output_id); + + let output_sub_devices = Self::get_sub_devices(output_id)?; + let input_sub_devices = Self::get_sub_devices(input_id)?; + + unsafe { + let sub_devices = CFArrayCreateMutable(ptr::null(), 0, &kCFTypeArrayCallBacks); + // The order of the items in the array is significant and is used to determine the order of the streams + // of the AudioAggregateDevice. + for device in output_sub_devices { + let uid = get_device_global_uid(device)?; + CFArrayAppendValue(sub_devices, uid.get_raw() as *const c_void); + } + + for device in input_sub_devices { + let uid = get_device_global_uid(device)?; + CFArrayAppendValue(sub_devices, uid.get_raw() as *const c_void); + } + + let address = AudioObjectPropertyAddress { + mSelector: kAudioAggregateDevicePropertyFullSubDeviceList, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + let size = mem::size_of::<CFMutableArrayRef>(); + let status = audio_object_set_property_data(device_id, &address, size, &sub_devices); + CFRelease(sub_devices as *const c_void); + if status == NO_ERR { + Ok(()) + } else { + Err(Error::from(status)) + } + } + } + + pub fn get_sub_devices( + device_id: AudioDeviceID, + ) -> std::result::Result<Vec<AudioObjectID>, Error> { + assert_ne!(device_id, kAudioObjectUnknown); + + let mut sub_devices = Vec::new(); + let address = AudioObjectPropertyAddress { + mSelector: kAudioAggregateDevicePropertyActiveSubDeviceList, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + let mut size: usize = 0; + let status = audio_object_get_property_data_size(device_id, &address, &mut size); + + if status == kAudioHardwareUnknownPropertyError as OSStatus { + // Return a vector containing the device itself if the device has no sub devices. + sub_devices.push(device_id); + return Ok(sub_devices); + } else if status != NO_ERR { + return Err(Error::from(status)); + } + + assert_ne!(size, 0); + + let count = size / mem::size_of::<AudioObjectID>(); + sub_devices = allocate_array(count); + let status = audio_object_get_property_data( + device_id, + &address, + &mut size, + sub_devices.as_mut_ptr(), + ); + + if status == NO_ERR { + Ok(sub_devices) + } else { + Err(Error::from(status)) + } + } + + pub fn set_master_device( + device_id: AudioDeviceID, + primary_id: AudioDeviceID, + ) -> std::result::Result<(), Error> { + assert_ne!(device_id, kAudioObjectUnknown); + assert_ne!(primary_id, kAudioObjectUnknown); + + cubeb_log!( + "Set master device of the aggregate device {} to device {}", + device_id, + primary_id + ); + + let address = AudioObjectPropertyAddress { + mSelector: kAudioAggregateDevicePropertyMasterSubDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + // Master become the 1st sub device of the primary device + let output_sub_devices = Self::get_sub_devices(primary_id)?; + assert!(!output_sub_devices.is_empty()); + let master_sub_device_uid = get_device_global_uid(output_sub_devices[0]).unwrap(); + let master_sub_device = master_sub_device_uid.get_raw(); + let size = mem::size_of::<CFStringRef>(); + let status = audio_object_set_property_data(device_id, &address, size, &master_sub_device); + if status == NO_ERR { + Ok(()) + } else { + Err(Error::from(status)) + } + } + + pub fn activate_clock_drift_compensation( + device_id: AudioObjectID, + ) -> std::result::Result<(), Error> { + assert_ne!(device_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; + let status = audio_object_get_property_data_size_with_qualifier( + device_id, + &address, + qualifier_data_size, + qualifier_data, + &mut size, + ); + if status != NO_ERR { + return Err(Error::from(status)); + } + assert!(size > 0); + let subdevices_num = size / mem::size_of::<AudioObjectID>(); + if subdevices_num < 2 { + cubeb_log!( + "Aggregate-device {} contains {} sub-devices only.\ + We should have at least one input and one output device.", + device_id, + subdevices_num + ); + return Err(Error::LessThan2Devices(subdevices_num)); + } + let mut sub_devices: Vec<AudioObjectID> = allocate_array(subdevices_num); + let status = audio_object_get_property_data_with_qualifier( + device_id, + &address, + qualifier_data_size, + qualifier_data, + &mut size, + sub_devices.as_mut_ptr(), + ); + if status != NO_ERR { + return Err(Error::from(status)); + } + + let address = AudioObjectPropertyAddress { + mSelector: kAudioSubDevicePropertyDriftCompensation, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + // Start from the second device since the first is the master clock + for device in &sub_devices[1..] { + let status = audio_object_set_property_data( + *device, + &address, + mem::size_of::<u32>(), + &DRIFT_COMPENSATION, + ); + if status != NO_ERR { + cubeb_log!( + "Failed to set drift compensation for {}. Ignore it.", + device + ); + } + } + + Ok(()) + } + + pub fn destroy_device( + plugin_id: AudioObjectID, + mut device_id: AudioDeviceID, + ) -> std::result::Result<(), Error> { + assert_ne!(plugin_id, kAudioObjectUnknown); + assert_ne!(device_id, kAudioObjectUnknown); + + let address = AudioObjectPropertyAddress { + mSelector: kAudioPlugInDestroyAggregateDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + let mut size: usize = 0; + let status = audio_object_get_property_data_size(plugin_id, &address, &mut size); + if status != NO_ERR { + return Err(Error::from(status)); + } + assert!(size > 0); + + let status = audio_object_get_property_data(plugin_id, &address, &mut size, &mut device_id); + if status == NO_ERR { + Ok(()) + } else { + Err(Error::from(status)) + } + } + + pub fn workaround_for_airpod( + device_id: AudioDeviceID, + input_id: AudioDeviceID, + output_id: AudioDeviceID, + ) -> std::result::Result<(), Error> { + assert_ne!(device_id, kAudioObjectUnknown); + assert_ne!(input_id, kAudioObjectUnknown); + assert_ne!(output_id, kAudioObjectUnknown); + assert_ne!(input_id, output_id); + + let label = get_device_label(input_id, DeviceType::INPUT)?; + let input_label = label.into_string(); + + let label = get_device_label(output_id, DeviceType::OUTPUT)?; + let output_label = label.into_string(); + + if input_label.contains("AirPods") && output_label.contains("AirPods") { + let input_rate = + get_device_sample_rate(input_id, DeviceType::INPUT | DeviceType::OUTPUT)?; + cubeb_log!( + "The nominal rate of the input device {}: {}", + input_id, + input_rate + ); + + let output_rate = + match get_device_sample_rate(output_id, DeviceType::INPUT | DeviceType::OUTPUT) { + Ok(rate) => format!("{}", rate), + Err(e) => format!("Error {}", e), + }; + cubeb_log!( + "The nominal rate of the output device {}: {}", + output_id, + output_rate + ); + + let addr = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyNominalSampleRate, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + let status = audio_object_set_property_data( + device_id, + &addr, + mem::size_of::<f64>(), + &input_rate, + ); + if status != NO_ERR { + return Err(Error::from(status)); + } + } + + Ok(()) + } +} + +impl Default for AggregateDevice { + fn default() -> Self { + Self { + plugin_id: kAudioObjectUnknown, + device_id: kAudioObjectUnknown, + input_id: kAudioObjectUnknown, + output_id: kAudioObjectUnknown, + } + } +} + +impl Drop for AggregateDevice { + fn drop(&mut self) { + if self.plugin_id != kAudioObjectUnknown && self.device_id != kAudioObjectUnknown { + if let Err(r) = Self::destroy_device(self.plugin_id, self.device_id) { + cubeb_log!( + "Failed to destroyed aggregate device {}. Error: {}", + self.device_id, + r + ); + } else { + cubeb_log!( + "Destroyed aggregate device {} (input {}, output {})", + self.device_id, + self.input_id, + self.output_id + ); + } + } + } +} |