From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../src/backend/tests/aggregate_device.rs | 400 +++++ .../rust/cubeb-coreaudio/src/backend/tests/api.rs | 1663 ++++++++++++++++++++ .../cubeb-coreaudio/src/backend/tests/backlog.rs | 36 + .../src/backend/tests/device_change.rs | 885 +++++++++++ .../src/backend/tests/device_property.rs | 473 ++++++ .../src/backend/tests/interfaces.rs | 1215 ++++++++++++++ .../cubeb-coreaudio/src/backend/tests/manual.rs | 614 ++++++++ .../rust/cubeb-coreaudio/src/backend/tests/mod.rs | 12 + .../cubeb-coreaudio/src/backend/tests/parallel.rs | 572 +++++++ .../rust/cubeb-coreaudio/src/backend/tests/tone.rs | 215 +++ .../cubeb-coreaudio/src/backend/tests/utils.rs | 1247 +++++++++++++++ 11 files changed, 7332 insertions(+) create mode 100644 third_party/rust/cubeb-coreaudio/src/backend/tests/aggregate_device.rs create mode 100644 third_party/rust/cubeb-coreaudio/src/backend/tests/api.rs create mode 100644 third_party/rust/cubeb-coreaudio/src/backend/tests/backlog.rs create mode 100644 third_party/rust/cubeb-coreaudio/src/backend/tests/device_change.rs create mode 100644 third_party/rust/cubeb-coreaudio/src/backend/tests/device_property.rs create mode 100644 third_party/rust/cubeb-coreaudio/src/backend/tests/interfaces.rs create mode 100644 third_party/rust/cubeb-coreaudio/src/backend/tests/manual.rs create mode 100644 third_party/rust/cubeb-coreaudio/src/backend/tests/mod.rs create mode 100644 third_party/rust/cubeb-coreaudio/src/backend/tests/parallel.rs create mode 100644 third_party/rust/cubeb-coreaudio/src/backend/tests/tone.rs create mode 100644 third_party/rust/cubeb-coreaudio/src/backend/tests/utils.rs (limited to 'third_party/rust/cubeb-coreaudio/src/backend/tests') diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/aggregate_device.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/aggregate_device.rs new file mode 100644 index 0000000000..1d3c341ae8 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/aggregate_device.rs @@ -0,0 +1,400 @@ +use super::utils::{ + test_get_all_devices, test_get_all_onwed_devices, test_get_default_device, + test_get_drift_compensations, test_get_master_device, DeviceFilter, Scope, +}; +use super::*; + +// AggregateDevice::set_sub_devices +// ------------------------------------ +#[test] +#[should_panic] +fn test_aggregate_set_sub_devices_for_an_unknown_aggregate_device() { + // If aggregate device id is kAudioObjectUnknown, we are unable to set device list. + let default_input = test_get_default_device(Scope::Input); + let default_output = test_get_default_device(Scope::Output); + if default_input.is_none() || default_output.is_none() { + panic!("No input or output device."); + } + + let default_input = default_input.unwrap(); + let default_output = default_output.unwrap(); + assert!( + AggregateDevice::set_sub_devices(kAudioObjectUnknown, default_input, default_output) + .is_err() + ); +} + +#[test] +#[should_panic] +fn test_aggregate_set_sub_devices_for_unknown_devices() { + // If aggregate device id is kAudioObjectUnknown, we are unable to set device list. + assert!(AggregateDevice::set_sub_devices( + kAudioObjectUnknown, + kAudioObjectUnknown, + kAudioObjectUnknown + ) + .is_err()); +} + +// AggregateDevice::get_sub_devices +// ------------------------------------ +// You can check this by creating an aggregate device in `Audio MIDI Setup` +// application and print out the sub devices of them! +#[test] +fn test_aggregate_get_sub_devices() { + let devices = test_get_all_devices(DeviceFilter::ExcludeCubebAggregateAndVPIO); + for device in devices { + // `AggregateDevice::get_sub_devices(device)` will return a single-element vector + // containing `device` itself if it's not an aggregate device. This test assumes devices + // is not an empty aggregate device (Test will panic when calling get_sub_devices with + // an empty aggregate device). + let sub_devices = AggregateDevice::get_sub_devices(device).unwrap(); + // TODO: If the device is a blank aggregate device, then the assertion fails! + assert!(!sub_devices.is_empty()); + } +} + +#[test] +#[should_panic] +fn test_aggregate_get_sub_devices_for_a_unknown_device() { + let devices = AggregateDevice::get_sub_devices(kAudioObjectUnknown).unwrap(); + assert!(devices.is_empty()); +} + +// AggregateDevice::set_master_device +// ------------------------------------ +#[test] +#[should_panic] +fn test_aggregate_set_master_device_for_an_unknown_aggregate_device() { + assert!(AggregateDevice::set_master_device(kAudioObjectUnknown, kAudioObjectUnknown).is_err()); +} + +// AggregateDevice::activate_clock_drift_compensation +// ------------------------------------ +#[test] +#[should_panic] +fn test_aggregate_activate_clock_drift_compensation_for_an_unknown_aggregate_device() { + assert!(AggregateDevice::activate_clock_drift_compensation(kAudioObjectUnknown).is_err()); +} + +// AggregateDevice::destroy_device +// ------------------------------------ +#[test] +#[should_panic] +fn test_aggregate_destroy_device_for_unknown_plugin_and_aggregate_devices() { + assert!(AggregateDevice::destroy_device(kAudioObjectUnknown, kAudioObjectUnknown).is_err()) +} + +#[test] +#[should_panic] +fn test_aggregate_destroy_aggregate_device_for_a_unknown_aggregate_device() { + let plugin = AggregateDevice::get_system_plugin_id().unwrap(); + assert!(AggregateDevice::destroy_device(plugin, kAudioObjectUnknown).is_err()); +} + +// Default Ignored Tests +// ================================================================================================ +// The following tests that calls `AggregateDevice::create_blank_device` are marked `ignore` by +// default since the device-collection-changed callbacks will be fired upon +// `AggregateDevice::create_blank_device` is called (it will plug a new device in system!). +// Some tests rely on the device-collection-changed callbacks in a certain way. The callbacks +// fired from a unexpected `AggregateDevice::create_blank_device` will break those tests. + +// AggregateDevice::create_blank_device_sync +// ------------------------------------ +#[test] +#[ignore] +fn test_aggregate_create_blank_device() { + // TODO: Test this when there is no available devices. + let plugin = AggregateDevice::get_system_plugin_id().unwrap(); + let device = AggregateDevice::create_blank_device_sync(plugin).unwrap(); + let devices = test_get_all_devices(DeviceFilter::IncludeAll); + let device = devices.into_iter().find(|dev| dev == &device).unwrap(); + let uid = get_device_global_uid(device).unwrap().into_string(); + assert!(uid.contains(PRIVATE_AGGREGATE_DEVICE_NAME)); + assert!(AggregateDevice::destroy_device(plugin, device).is_ok()); +} + +// AggregateDevice::get_sub_devices +// ------------------------------------ +#[test] +#[ignore] +#[should_panic] +fn test_aggregate_get_sub_devices_for_blank_aggregate_devices() { + // TODO: Test this when there is no available devices. + let plugin = AggregateDevice::get_system_plugin_id().unwrap(); + let device = AggregateDevice::create_blank_device_sync(plugin).unwrap(); + // There is no sub device in a blank aggregate device! + // AggregateDevice::get_sub_devices guarantees returning a non-empty devices vector, so + // the following call will panic! + let sub_devices = AggregateDevice::get_sub_devices(device).unwrap(); + assert!(sub_devices.is_empty()); + assert!(AggregateDevice::destroy_device(plugin, device).is_ok()); +} + +// AggregateDevice::set_sub_devices_sync +// ------------------------------------ +#[test] +#[ignore] +fn test_aggregate_set_sub_devices() { + let input_device = test_get_default_device(Scope::Input); + let output_device = test_get_default_device(Scope::Output); + if input_device.is_none() || output_device.is_none() || input_device == output_device { + println!("No input or output device to create an aggregate device."); + return; + } + + let input_device = input_device.unwrap(); + let output_device = output_device.unwrap(); + + let plugin = AggregateDevice::get_system_plugin_id().unwrap(); + let device = AggregateDevice::create_blank_device_sync(plugin).unwrap(); + assert!(AggregateDevice::set_sub_devices_sync(device, input_device, output_device).is_ok()); + + let sub_devices = AggregateDevice::get_sub_devices(device).unwrap(); + let input_sub_devices = AggregateDevice::get_sub_devices(input_device).unwrap(); + let output_sub_devices = AggregateDevice::get_sub_devices(output_device).unwrap(); + + // TODO: There may be overlapping devices between input_sub_devices and output_sub_devices, + // but now AggregateDevice::set_sub_devices will add them directly. + assert_eq!( + sub_devices.len(), + input_sub_devices.len() + output_sub_devices.len() + ); + for dev in &input_sub_devices { + assert!(sub_devices.contains(dev)); + } + for dev in &output_sub_devices { + assert!(sub_devices.contains(dev)); + } + + let onwed_devices = test_get_all_onwed_devices(device); + let onwed_device_uids = get_device_uids(&onwed_devices); + let input_sub_device_uids = get_device_uids(&input_sub_devices); + let output_sub_device_uids = get_device_uids(&output_sub_devices); + for uid in &input_sub_device_uids { + assert!(onwed_device_uids.contains(uid)); + } + for uid in &output_sub_device_uids { + assert!(onwed_device_uids.contains(uid)); + } + + assert!(AggregateDevice::destroy_device(plugin, device).is_ok()); +} + +#[test] +#[ignore] +#[should_panic] +fn test_aggregate_set_sub_devices_for_unknown_input_devices() { + let output_device = test_get_default_device(Scope::Output); + if output_device.is_none() { + panic!("Need a output device for the test!"); + } + let output_device = output_device.unwrap(); + + let plugin = AggregateDevice::get_system_plugin_id().unwrap(); + let device = AggregateDevice::create_blank_device_sync(plugin).unwrap(); + + assert!(AggregateDevice::set_sub_devices(device, kAudioObjectUnknown, output_device).is_err()); + + assert!(AggregateDevice::destroy_device(plugin, device).is_ok()); +} + +#[test] +#[ignore] +#[should_panic] +fn test_aggregate_set_sub_devices_for_unknown_output_devices() { + let input_device = test_get_default_device(Scope::Input); + if input_device.is_none() { + panic!("Need a input device for the test!"); + } + let input_device = input_device.unwrap(); + + let plugin = AggregateDevice::get_system_plugin_id().unwrap(); + let device = AggregateDevice::create_blank_device_sync(plugin).unwrap(); + + assert!(AggregateDevice::set_sub_devices(device, input_device, kAudioObjectUnknown).is_err()); + + assert!(AggregateDevice::destroy_device(plugin, device).is_ok()); +} + +fn get_device_uids(devices: &Vec) -> Vec { + devices + .iter() + .map(|device| get_device_global_uid(*device).unwrap().into_string()) + .collect() +} + +// AggregateDevice::set_master_device +// ------------------------------------ +#[test] +#[ignore] +fn test_aggregate_set_master_device() { + let input_device = test_get_default_device(Scope::Input); + let output_device = test_get_default_device(Scope::Output); + if input_device.is_none() || output_device.is_none() || input_device == output_device { + println!("No input or output device to create an aggregate device."); + return; + } + + let input_device = input_device.unwrap(); + let output_device = output_device.unwrap(); + + let plugin = AggregateDevice::get_system_plugin_id().unwrap(); + let device = AggregateDevice::create_blank_device_sync(plugin).unwrap(); + assert!(AggregateDevice::set_sub_devices_sync(device, input_device, output_device).is_ok()); + assert!(AggregateDevice::set_master_device(device, output_device).is_ok()); + + // Check if master is set to the first sub device of the default output device. + let first_output_sub_device_uid = + get_device_uid(AggregateDevice::get_sub_devices(device).unwrap()[0]); + let master_device_uid = test_get_master_device(device); + assert_eq!(first_output_sub_device_uid, master_device_uid); + + assert!(AggregateDevice::destroy_device(plugin, device).is_ok()); +} + +#[test] +#[ignore] +fn test_aggregate_set_master_device_for_a_blank_aggregate_device() { + let output_device = test_get_default_device(Scope::Output); + if output_device.is_none() { + println!("No output device to test."); + return; + } + + let plugin = AggregateDevice::get_system_plugin_id().unwrap(); + let device = AggregateDevice::create_blank_device_sync(plugin).unwrap(); + assert!(AggregateDevice::set_master_device(device, output_device.unwrap()).is_ok()); + + // TODO: it's really weird the aggregate device actually own nothing + // but its master device can be set successfully! + // The sub devices of this blank aggregate device (by `AggregateDevice::get_sub_devices`) + // and the own devices (by `test_get_all_onwed_devices`) is empty since the size returned + // from `audio_object_get_property_data_size` is 0. + // The CFStringRef of the master device returned from `test_get_master_device` is actually + // non-null. + + assert!(AggregateDevice::destroy_device(plugin, device).is_ok()); +} + +fn get_device_uid(id: AudioObjectID) -> String { + get_device_global_uid(id).unwrap().into_string() +} + +// AggregateDevice::activate_clock_drift_compensation +// ------------------------------------ +#[test] +#[ignore] +fn test_aggregate_activate_clock_drift_compensation() { + let input_device = test_get_default_device(Scope::Input); + let output_device = test_get_default_device(Scope::Output); + if input_device.is_none() || output_device.is_none() || input_device == output_device { + println!("No input or output device to create an aggregate device."); + return; + } + + let input_device = input_device.unwrap(); + let output_device = output_device.unwrap(); + + let plugin = AggregateDevice::get_system_plugin_id().unwrap(); + let device = AggregateDevice::create_blank_device_sync(plugin).unwrap(); + assert!(AggregateDevice::set_sub_devices_sync(device, input_device, output_device).is_ok()); + assert!(AggregateDevice::set_master_device(device, output_device).is_ok()); + assert!(AggregateDevice::activate_clock_drift_compensation(device).is_ok()); + + // Check the compensations. + let devices = test_get_all_onwed_devices(device); + let compensations = get_drift_compensations(&devices); + assert!(!compensations.is_empty()); + assert_eq!(devices.len(), compensations.len()); + + for (i, compensation) in compensations.iter().enumerate() { + assert_eq!(*compensation, if i == 0 { 0 } else { DRIFT_COMPENSATION }); + } + + assert!(AggregateDevice::destroy_device(plugin, device).is_ok()); +} + +#[test] +#[ignore] +fn test_aggregate_activate_clock_drift_compensation_for_an_aggregate_device_without_master_device() +{ + let input_device = test_get_default_device(Scope::Input); + let output_device = test_get_default_device(Scope::Output); + if input_device.is_none() || output_device.is_none() || input_device == output_device { + println!("No input or output device to create an aggregate device."); + return; + } + + let input_device = input_device.unwrap(); + let output_device = output_device.unwrap(); + + let plugin = AggregateDevice::get_system_plugin_id().unwrap(); + let device = AggregateDevice::create_blank_device_sync(plugin).unwrap(); + assert!(AggregateDevice::set_sub_devices_sync(device, input_device, output_device).is_ok()); + + // TODO: Is the master device the first output sub device by default if we + // don't set that ? Is it because we add the output sub device list + // before the input's one ? (See implementation of + // AggregateDevice::set_sub_devices). + let first_output_sub_device_uid = + get_device_uid(AggregateDevice::get_sub_devices(output_device).unwrap()[0]); + let master_device_uid = test_get_master_device(device); + assert_eq!(first_output_sub_device_uid, master_device_uid); + + // Compensate the drift directly without setting master device. + assert!(AggregateDevice::activate_clock_drift_compensation(device).is_ok()); + + // Check the compensations. + let devices = test_get_all_onwed_devices(device); + let compensations = get_drift_compensations(&devices); + assert!(!compensations.is_empty()); + assert_eq!(devices.len(), compensations.len()); + + for (i, compensation) in compensations.iter().enumerate() { + assert_eq!(*compensation, if i == 0 { 0 } else { DRIFT_COMPENSATION }); + } + + assert!(AggregateDevice::destroy_device(plugin, device).is_ok()); +} + +#[test] +#[should_panic] +#[ignore] +fn test_aggregate_activate_clock_drift_compensation_for_a_blank_aggregate_device() { + let plugin = AggregateDevice::get_system_plugin_id().unwrap(); + let device = AggregateDevice::create_blank_device_sync(plugin).unwrap(); + + let sub_devices = AggregateDevice::get_sub_devices(device).unwrap(); + assert!(sub_devices.is_empty()); + let onwed_devices = test_get_all_onwed_devices(device); + assert!(onwed_devices.is_empty()); + + // Get a panic since no sub devices to be set compensation. + assert!(AggregateDevice::activate_clock_drift_compensation(device).is_err()); + + assert!(AggregateDevice::destroy_device(plugin, device).is_ok()); +} + +fn get_drift_compensations(devices: &Vec) -> Vec { + assert!(!devices.is_empty()); + let mut compensations = Vec::new(); + for device in devices { + let compensation = test_get_drift_compensations(*device).unwrap(); + compensations.push(compensation); + } + + compensations +} + +// AggregateDevice::destroy_device +// ------------------------------------ +#[test] +#[ignore] +#[should_panic] +fn test_aggregate_destroy_aggregate_device_for_a_unknown_plugin_device() { + let plugin = AggregateDevice::get_system_plugin_id().unwrap(); + let device = AggregateDevice::create_blank_device_sync(plugin).unwrap(); + assert!(AggregateDevice::destroy_device(kAudioObjectUnknown, device).is_err()); +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/api.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/api.rs new file mode 100644 index 0000000000..4cd86c094e --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/api.rs @@ -0,0 +1,1663 @@ +use super::utils::{ + test_audiounit_get_buffer_frame_size, test_audiounit_scope_is_enabled, test_create_audiounit, + test_device_channels_in_scope, test_device_in_scope, test_get_all_devices, + test_get_default_audiounit, test_get_default_device, test_get_default_raw_stream, + test_get_devices_in_scope, test_get_raw_context, ComponentSubType, DeviceFilter, PropertyScope, + Scope, +}; +use super::*; + +// make_sized_audio_channel_layout +// ------------------------------------ +#[test] +fn test_make_sized_audio_channel_layout() { + for channels in 1..10 { + let size = mem::size_of::() + + (channels - 1) * mem::size_of::(); + let _ = make_sized_audio_channel_layout(size); + } +} + +#[test] +#[should_panic] +fn test_make_sized_audio_channel_layout_with_wrong_size() { + // let _ = make_sized_audio_channel_layout(0); + let one_channel_size = mem::size_of::(); + let padding_size = 10; + assert_ne!(mem::size_of::(), padding_size); + let wrong_size = one_channel_size + padding_size; + let _ = make_sized_audio_channel_layout(wrong_size); +} + +// active_streams +// update_latency_by_adding_stream +// update_latency_by_removing_stream +// ------------------------------------ +#[test] +fn test_increase_and_decrease_context_streams() { + use std::thread; + const STREAMS: u32 = 10; + + let context = AudioUnitContext::new(); + let context_ptr_value = &context as *const AudioUnitContext as usize; + + let mut join_handles = vec![]; + for i in 0..STREAMS { + join_handles.push(thread::spawn(move || { + let context = unsafe { &*(context_ptr_value as *const AudioUnitContext) }; + + context.update_latency_by_adding_stream(i) + })); + } + let mut latencies = vec![]; + for handle in join_handles { + latencies.push(handle.join().unwrap()); + } + assert_eq!(context.active_streams(), STREAMS); + check_streams(&context, STREAMS); + + check_latency(&context, latencies[0]); + for i in 0..latencies.len() - 1 { + assert_eq!(latencies[i], latencies[i + 1]); + } + + let mut join_handles = vec![]; + for _ in 0..STREAMS { + join_handles.push(thread::spawn(move || { + let context = unsafe { &*(context_ptr_value as *const AudioUnitContext) }; + context.update_latency_by_removing_stream(); + })); + } + for handle in join_handles { + let _ = handle.join(); + } + check_streams(&context, 0); + + check_latency(&context, None); +} + +fn check_streams(context: &AudioUnitContext, number: u32) { + let guard = context.latency_controller.lock().unwrap(); + assert_eq!(guard.streams, number); +} + +fn check_latency(context: &AudioUnitContext, latency: Option) { + let guard = context.latency_controller.lock().unwrap(); + assert_eq!(guard.latency, latency); +} + +// make_silent +// ------------------------------------ +#[test] +fn test_make_silent() { + let mut array = allocate_array::(10); + for data in array.iter_mut() { + *data = 0xFFFF; + } + + let mut buffer = AudioBuffer::default(); + buffer.mData = array.as_mut_ptr() as *mut c_void; + buffer.mDataByteSize = (array.len() * mem::size_of::()) as u32; + buffer.mNumberChannels = 1; + + audiounit_make_silent(&mut buffer); + for data in array { + assert_eq!(data, 0); + } +} + +// minimum_resampling_input_frames +// ------------------------------------ +#[test] +fn test_minimum_resampling_input_frames() { + let input_rate = 48000_f64; + let output_rate = 44100_f64; + + let frames = 100; + let times = input_rate / output_rate; + let expected = (frames as f64 * times).ceil() as usize; + + assert_eq!( + minimum_resampling_input_frames(input_rate, output_rate, frames), + expected + ); +} + +#[test] +#[should_panic] +fn test_minimum_resampling_input_frames_zero_input_rate() { + minimum_resampling_input_frames(0_f64, 44100_f64, 1); +} + +#[test] +#[should_panic] +fn test_minimum_resampling_input_frames_zero_output_rate() { + minimum_resampling_input_frames(48000_f64, 0_f64, 1); +} + +#[test] +fn test_minimum_resampling_input_frames_equal_input_output_rate() { + let frames = 100; + assert_eq!( + minimum_resampling_input_frames(44100_f64, 44100_f64, frames), + frames + ); +} + +// create_device_info +// ------------------------------------ +#[test] +fn test_create_device_info_from_unknown_input_device() { + if let Some(default_device_id) = test_get_default_device(Scope::Input) { + let default_device = create_device_info(kAudioObjectUnknown, DeviceType::INPUT).unwrap(); + assert_eq!(default_device.id, default_device_id); + assert_eq!( + default_device.flags, + device_flags::DEV_INPUT | device_flags::DEV_SELECTED_DEFAULT + ); + } else { + println!("No input device to perform test."); + } +} + +#[test] +fn test_create_device_info_from_unknown_output_device() { + if let Some(default_device_id) = test_get_default_device(Scope::Output) { + let default_device = create_device_info(kAudioObjectUnknown, DeviceType::OUTPUT).unwrap(); + assert_eq!(default_device.id, default_device_id); + assert_eq!( + default_device.flags, + device_flags::DEV_OUTPUT | device_flags::DEV_SELECTED_DEFAULT + ); + } else { + println!("No output device to perform test."); + } +} + +#[test] +#[should_panic] +fn test_set_device_info_to_system_input_device() { + let _device = create_device_info(kAudioObjectSystemObject, DeviceType::INPUT); +} + +#[test] +#[should_panic] +fn test_set_device_info_to_system_output_device() { + let _device = create_device_info(kAudioObjectSystemObject, DeviceType::OUTPUT); +} + +// FIXME: Is it ok to set input device to a nonexistent device ? +#[ignore] +#[test] +#[should_panic] +fn test_set_device_info_to_nonexistent_input_device() { + let nonexistent_id = std::u32::MAX; + let _device = create_device_info(nonexistent_id, DeviceType::INPUT); +} + +// FIXME: Is it ok to set output device to a nonexistent device ? +#[ignore] +#[test] +#[should_panic] +fn test_set_device_info_to_nonexistent_output_device() { + let nonexistent_id = std::u32::MAX; + let _device = create_device_info(nonexistent_id, DeviceType::OUTPUT); +} + +// add_listener (for default output device) +// ------------------------------------ +#[test] +fn test_add_listener_unknown_device() { + extern "C" fn callback( + _id: AudioObjectID, + _number_of_addresses: u32, + _addresses: *const AudioObjectPropertyAddress, + _data: *mut c_void, + ) -> OSStatus { + assert!(false, "Should not be called."); + kAudioHardwareUnspecifiedError as OSStatus + } + + test_get_default_raw_stream(|stream| { + let listener = device_property_listener::new( + kAudioObjectUnknown, + get_property_address( + Property::HardwareDefaultOutputDevice, + DeviceType::INPUT | DeviceType::OUTPUT, + ), + callback, + ); + let mut res: OSStatus = 0; + stream + .queue + .run_sync(|| res = stream.add_device_listener(&listener)); + assert_eq!(res, kAudioHardwareBadObjectError as OSStatus); + }); +} + +// remove_listener (for default output device) +// ------------------------------------ +#[test] +fn test_add_listener_then_remove_system_device() { + extern "C" fn callback( + _id: AudioObjectID, + _number_of_addresses: u32, + _addresses: *const AudioObjectPropertyAddress, + _data: *mut c_void, + ) -> OSStatus { + assert!(false, "Should not be called."); + kAudioHardwareUnspecifiedError as OSStatus + } + + test_get_default_raw_stream(|stream| { + let listener = device_property_listener::new( + kAudioObjectSystemObject, + get_property_address( + Property::HardwareDefaultOutputDevice, + DeviceType::INPUT | DeviceType::OUTPUT, + ), + callback, + ); + let mut res: OSStatus = 0; + stream + .queue + .run_sync(|| res = stream.add_device_listener(&listener)); + assert_eq!(res, NO_ERR); + stream + .queue + .run_sync(|| res = stream.remove_device_listener(&listener)); + assert_eq!(res, NO_ERR); + }); +} + +#[test] +fn test_remove_listener_without_adding_any_listener_before_system_device() { + extern "C" fn callback( + _id: AudioObjectID, + _number_of_addresses: u32, + _addresses: *const AudioObjectPropertyAddress, + _data: *mut c_void, + ) -> OSStatus { + assert!(false, "Should not be called."); + kAudioHardwareUnspecifiedError as OSStatus + } + + test_get_default_raw_stream(|stream| { + let listener = device_property_listener::new( + kAudioObjectSystemObject, + get_property_address( + Property::HardwareDefaultOutputDevice, + DeviceType::INPUT | DeviceType::OUTPUT, + ), + callback, + ); + let mut res: OSStatus = 0; + stream + .queue + .run_sync(|| res = stream.remove_device_listener(&listener)); + assert_eq!(res, NO_ERR); + }); +} + +#[test] +fn test_remove_listener_unknown_device() { + extern "C" fn callback( + _id: AudioObjectID, + _number_of_addresses: u32, + _addresses: *const AudioObjectPropertyAddress, + _data: *mut c_void, + ) -> OSStatus { + assert!(false, "Should not be called."); + kAudioHardwareUnspecifiedError as OSStatus + } + + test_get_default_raw_stream(|stream| { + let listener = device_property_listener::new( + kAudioObjectUnknown, + get_property_address( + Property::HardwareDefaultOutputDevice, + DeviceType::INPUT | DeviceType::OUTPUT, + ), + callback, + ); + let mut res: OSStatus = 0; + stream + .queue + .run_sync(|| res = stream.remove_device_listener(&listener)); + assert_eq!(res, kAudioHardwareBadObjectError as OSStatus); + }); +} + +// get_default_device_id +// ------------------------------------ +#[test] +fn test_get_default_device_id() { + if test_get_default_device(Scope::Input).is_some() { + assert_ne!( + get_default_device_id(DeviceType::INPUT).unwrap(), + kAudioObjectUnknown, + ); + } + + if test_get_default_device(Scope::Output).is_some() { + assert_ne!( + get_default_device_id(DeviceType::OUTPUT).unwrap(), + kAudioObjectUnknown, + ); + } +} + +#[test] +#[should_panic] +fn test_get_default_device_id_with_unknown_type() { + assert!(get_default_device_id(DeviceType::UNKNOWN).is_err()); +} + +#[test] +#[should_panic] +fn test_get_default_device_id_with_inout_type() { + assert!(get_default_device_id(DeviceType::INPUT | DeviceType::OUTPUT).is_err()); +} + +// convert_channel_layout +// ------------------------------------ +#[test] +fn test_convert_channel_layout() { + let pairs = [ + (vec![kAudioObjectUnknown], vec![mixer::Channel::Silence]), + ( + vec![kAudioChannelLabel_Mono], + vec![mixer::Channel::FrontCenter], + ), + ( + vec![kAudioChannelLabel_Mono, kAudioChannelLabel_LFEScreen], + vec![mixer::Channel::FrontCenter, mixer::Channel::LowFrequency], + ), + ( + vec![kAudioChannelLabel_Left, kAudioChannelLabel_Right], + vec![mixer::Channel::FrontLeft, mixer::Channel::FrontRight], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Unknown, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::Silence, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Unused, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::Silence, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_ForeignLanguage, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::Silence, + ], + ), + // The SMPTE layouts. + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_LFEScreen, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::LowFrequency, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::FrontCenter, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + kAudioChannelLabel_LFEScreen, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::FrontCenter, + mixer::Channel::LowFrequency, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_CenterSurround, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::BackCenter, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_CenterSurround, + kAudioChannelLabel_LFEScreen, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::BackCenter, + mixer::Channel::LowFrequency, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + kAudioChannelLabel_CenterSurround, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::FrontCenter, + mixer::Channel::BackCenter, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + kAudioChannelLabel_CenterSurround, + kAudioChannelLabel_LFEScreen, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::FrontCenter, + mixer::Channel::BackCenter, + mixer::Channel::LowFrequency, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_LeftSurroundDirect, + kAudioChannelLabel_RightSurroundDirect, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::SideLeft, + mixer::Channel::SideRight, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_LeftSurroundDirect, + kAudioChannelLabel_RightSurroundDirect, + kAudioChannelLabel_LFEScreen, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::SideLeft, + mixer::Channel::SideRight, + mixer::Channel::LowFrequency, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_LeftSurround, + kAudioChannelLabel_RightSurround, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::BackLeft, + mixer::Channel::BackRight, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_LeftSurround, + kAudioChannelLabel_RightSurround, + kAudioChannelLabel_LFEScreen, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::BackLeft, + mixer::Channel::BackRight, + mixer::Channel::LowFrequency, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + kAudioChannelLabel_LeftSurroundDirect, + kAudioChannelLabel_RightSurroundDirect, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::FrontCenter, + mixer::Channel::SideLeft, + mixer::Channel::SideRight, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + kAudioChannelLabel_LeftSurroundDirect, + kAudioChannelLabel_RightSurroundDirect, + kAudioChannelLabel_LFEScreen, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::FrontCenter, + mixer::Channel::SideLeft, + mixer::Channel::SideRight, + mixer::Channel::LowFrequency, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_LeftSurround, + kAudioChannelLabel_RightSurround, + kAudioChannelLabel_Center, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::BackLeft, + mixer::Channel::BackRight, + mixer::Channel::FrontCenter, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_LeftSurround, + kAudioChannelLabel_RightSurround, + kAudioChannelLabel_Center, + kAudioChannelLabel_LFEScreen, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::BackLeft, + mixer::Channel::BackRight, + mixer::Channel::FrontCenter, + mixer::Channel::LowFrequency, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + kAudioChannelLabel_LFEScreen, + kAudioChannelLabel_CenterSurround, + kAudioChannelLabel_LeftSurroundDirect, + kAudioChannelLabel_RightSurroundDirect, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::FrontCenter, + mixer::Channel::LowFrequency, + mixer::Channel::BackCenter, + mixer::Channel::SideLeft, + mixer::Channel::SideRight, + ], + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + kAudioChannelLabel_LFEScreen, + kAudioChannelLabel_LeftSurround, + kAudioChannelLabel_RightSurround, + kAudioChannelLabel_LeftSurroundDirect, + kAudioChannelLabel_RightSurroundDirect, + ], + vec![ + mixer::Channel::FrontLeft, + mixer::Channel::FrontRight, + mixer::Channel::FrontCenter, + mixer::Channel::LowFrequency, + mixer::Channel::BackLeft, + mixer::Channel::BackRight, + mixer::Channel::SideLeft, + mixer::Channel::SideRight, + ], + ), + ]; + + const MAX_CHANNELS: usize = 10; + // A Rust mapping structure of the AudioChannelLayout with MAX_CHANNELS channels + // https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.13.sdk/System/Library/Frameworks/CoreAudio.framework/Versions/A/Headers/CoreAudioTypes.h#L1332 + #[repr(C)] + struct TestLayout { + tag: AudioChannelLayoutTag, + map: AudioChannelBitmap, + number_channel_descriptions: UInt32, + channel_descriptions: [AudioChannelDescription; MAX_CHANNELS], + } + + impl Default for TestLayout { + fn default() -> Self { + Self { + tag: AudioChannelLayoutTag::default(), + map: AudioChannelBitmap::default(), + number_channel_descriptions: UInt32::default(), + channel_descriptions: [AudioChannelDescription::default(); MAX_CHANNELS], + } + } + } + + let mut layout = TestLayout::default(); + layout.tag = kAudioChannelLayoutTag_UseChannelDescriptions; + + for (labels, expected_layout) in pairs.iter() { + assert!(labels.len() <= MAX_CHANNELS); + layout.number_channel_descriptions = labels.len() as u32; + for (idx, label) in labels.iter().enumerate() { + layout.channel_descriptions[idx].mChannelLabel = *label; + } + let layout_ref = unsafe { &(*(&layout as *const TestLayout as *const AudioChannelLayout)) }; + assert_eq!( + &audiounit_convert_channel_layout(layout_ref).unwrap(), + expected_layout + ); + } +} + +// get_preferred_channel_layout +// ------------------------------------ +#[test] +fn test_get_preferred_channel_layout_output() { + match test_get_default_audiounit(Scope::Output) { + Some(unit) => assert!(!audiounit_get_preferred_channel_layout(unit.get_inner()) + .unwrap() + .is_empty()), + None => println!("No output audiounit for test."), + } +} + +// get_current_channel_layout +// ------------------------------------ +#[test] +fn test_get_current_channel_layout_output() { + match test_get_default_audiounit(Scope::Output) { + Some(unit) => assert!(!audiounit_get_current_channel_layout(unit.get_inner()) + .unwrap() + .is_empty()), + None => println!("No output audiounit for test."), + } +} + +// create_stream_description +// ------------------------------------ +#[test] +fn test_create_stream_description() { + let mut channels = 0; + for (bits, format, flags) in [ + ( + 16_u32, + ffi::CUBEB_SAMPLE_S16LE, + kAudioFormatFlagIsSignedInteger, + ), + ( + 16_u32, + ffi::CUBEB_SAMPLE_S16BE, + kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsBigEndian, + ), + (32_u32, ffi::CUBEB_SAMPLE_FLOAT32LE, kAudioFormatFlagIsFloat), + ( + 32_u32, + ffi::CUBEB_SAMPLE_FLOAT32BE, + kAudioFormatFlagIsFloat | kAudioFormatFlagIsBigEndian, + ), + ] + .iter() + { + let bytes = bits / 8; + channels += 1; + + let mut raw = ffi::cubeb_stream_params::default(); + raw.format = *format; + raw.rate = 48_000; + raw.channels = channels; + raw.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + raw.prefs = ffi::CUBEB_STREAM_PREF_NONE; + let params = StreamParams::from(raw); + let description = create_stream_description(¶ms).unwrap(); + assert_eq!(description.mFormatID, kAudioFormatLinearPCM); + assert_eq!( + description.mFormatFlags, + flags | kLinearPCMFormatFlagIsPacked + ); + assert_eq!(description.mSampleRate as u32, raw.rate); + assert_eq!(description.mChannelsPerFrame, raw.channels); + assert_eq!(description.mBytesPerFrame, bytes * raw.channels); + assert_eq!(description.mFramesPerPacket, 1); + assert_eq!(description.mBytesPerPacket, bytes * raw.channels); + assert_eq!(description.mReserved, 0); + } +} + +// create_blank_audiounit +// ------------------------------------ +#[test] +fn test_create_blank_audiounit() { + let unit = create_blank_audiounit().unwrap(); + assert!(!unit.is_null()); + // Destroy the AudioUnit + unsafe { + AudioUnitUninitialize(unit); + AudioComponentInstanceDispose(unit); + } +} + +// enable_audiounit_scope +// ------------------------------------ +#[test] +fn test_enable_audiounit_scope() { + // It's ok to enable and disable the scopes of input or output + // for the unit whose subtype is kAudioUnitSubType_HALOutput + // even when there is no available input or output devices. + if let Some(unit) = test_create_audiounit(ComponentSubType::HALOutput) { + assert!(enable_audiounit_scope(unit.get_inner(), DeviceType::OUTPUT, true).is_ok()); + assert!(enable_audiounit_scope(unit.get_inner(), DeviceType::OUTPUT, false).is_ok()); + assert!(enable_audiounit_scope(unit.get_inner(), DeviceType::INPUT, true).is_ok()); + assert!(enable_audiounit_scope(unit.get_inner(), DeviceType::INPUT, false).is_ok()); + } else { + println!("No audiounit to perform test."); + } +} + +#[test] +fn test_enable_audiounit_scope_for_default_output_unit() { + if let Some(unit) = test_create_audiounit(ComponentSubType::DefaultOutput) { + assert_eq!( + enable_audiounit_scope(unit.get_inner(), DeviceType::OUTPUT, true).unwrap_err(), + kAudioUnitErr_InvalidProperty + ); + assert_eq!( + enable_audiounit_scope(unit.get_inner(), DeviceType::OUTPUT, false).unwrap_err(), + kAudioUnitErr_InvalidProperty + ); + assert_eq!( + enable_audiounit_scope(unit.get_inner(), DeviceType::INPUT, true).unwrap_err(), + kAudioUnitErr_InvalidProperty + ); + assert_eq!( + enable_audiounit_scope(unit.get_inner(), DeviceType::INPUT, false).unwrap_err(), + kAudioUnitErr_InvalidProperty + ); + } +} + +#[test] +#[should_panic] +fn test_enable_audiounit_scope_with_null_unit() { + let unit: AudioUnit = ptr::null_mut(); + assert!(enable_audiounit_scope(unit, DeviceType::INPUT, false).is_err()); +} + +// create_audiounit +// ------------------------------------ +#[test] +fn test_for_create_audiounit() { + let flags_list = [device_flags::DEV_INPUT, device_flags::DEV_OUTPUT]; + + let default_input = test_get_default_device(Scope::Input); + let default_output = test_get_default_device(Scope::Output); + + for flags in flags_list.iter() { + let mut device = device_info::default(); + device.flags |= *flags; + + // Check the output scope is enabled. + if device.flags.contains(device_flags::DEV_OUTPUT) && default_output.is_some() { + device.id = default_output.unwrap(); + let unit = create_audiounit(&device).unwrap(); + assert!(!unit.is_null()); + assert!(test_audiounit_scope_is_enabled(unit, Scope::Output)); + + // Destroy the AudioUnit. + unsafe { + AudioUnitUninitialize(unit); + AudioComponentInstanceDispose(unit); + } + } + + // Check the input scope is enabled. + if device.flags.contains(device_flags::DEV_INPUT) && default_input.is_some() { + let device_id = default_input.unwrap(); + device.id = device_id; + let unit = create_audiounit(&device).unwrap(); + assert!(!unit.is_null()); + assert!(test_audiounit_scope_is_enabled(unit, Scope::Input)); + // Destroy the AudioUnit. + unsafe { + AudioUnitUninitialize(unit); + AudioComponentInstanceDispose(unit); + } + } + } +} + +#[test] +#[should_panic] +fn test_create_audiounit_with_unknown_scope() { + let device = device_info::default(); + let _unit = create_audiounit(&device); +} + +// set_buffer_size_sync +// ------------------------------------ +#[test] +fn test_set_buffer_size_sync() { + test_set_buffer_size_by_scope(Scope::Input); + test_set_buffer_size_by_scope(Scope::Output); + fn test_set_buffer_size_by_scope(scope: Scope) { + let unit = test_get_default_audiounit(scope.clone()); + if unit.is_none() { + println!("No audiounit for {:?}.", scope); + return; + } + let unit = unit.unwrap(); + let prop_scope = match scope { + Scope::Input => PropertyScope::Output, + Scope::Output => PropertyScope::Input, + }; + let mut buffer_frames = test_audiounit_get_buffer_frame_size( + unit.get_inner(), + scope.clone(), + prop_scope.clone(), + ) + .unwrap(); + assert_ne!(buffer_frames, 0); + buffer_frames *= 2; + assert!( + set_buffer_size_sync(unit.get_inner(), scope.clone().into(), buffer_frames).is_ok() + ); + let new_buffer_frames = + test_audiounit_get_buffer_frame_size(unit.get_inner(), scope.clone(), prop_scope) + .unwrap(); + assert_eq!(buffer_frames, new_buffer_frames); + } +} + +#[test] +#[should_panic] +fn test_set_buffer_size_sync_for_input_with_null_input_unit() { + test_set_buffer_size_sync_by_scope_with_null_unit(Scope::Input); +} + +#[test] +#[should_panic] +fn test_set_buffer_size_sync_for_output_with_null_output_unit() { + test_set_buffer_size_sync_by_scope_with_null_unit(Scope::Output); +} + +fn test_set_buffer_size_sync_by_scope_with_null_unit(scope: Scope) { + let unit: AudioUnit = ptr::null_mut(); + assert!(set_buffer_size_sync(unit, scope.into(), 2048).is_err()); +} + +// get_volume, set_volume +// ------------------------------------ +#[test] +fn test_stream_get_volume() { + if let Some(unit) = test_get_default_audiounit(Scope::Output) { + let expected_volume: f32 = 0.5; + set_volume(unit.get_inner(), expected_volume); + assert_eq!(expected_volume, get_volume(unit.get_inner()).unwrap()); + } else { + println!("No output audiounit."); + } +} + +// convert_uint32_into_string +// ------------------------------------ +#[test] +fn test_convert_uint32_into_string() { + let empty = convert_uint32_into_string(0); + assert_eq!(empty, CString::default()); + + let data: u32 = ('R' as u32) << 24 | ('U' as u32) << 16 | ('S' as u32) << 8 | 'T' as u32; + let data_string = convert_uint32_into_string(data); + assert_eq!(data_string, CString::new("RUST").unwrap()); +} + +// get_channel_count +// ------------------------------------ +#[test] +fn test_get_channel_count() { + test_channel_count(Scope::Input); + test_channel_count(Scope::Output); + + fn test_channel_count(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + let channels = get_channel_count(device, DeviceType::from(scope.clone())).unwrap(); + assert!(channels > 0); + assert_eq!( + channels, + test_device_channels_in_scope(device, scope).unwrap() + ); + } else { + println!("No device for {:?}.", scope); + } + } +} + +#[test] +fn test_get_channel_count_of_input_for_a_output_only_deivce() { + let devices = test_get_devices_in_scope(Scope::Output); + for device in devices { + // Skip in-out devices. + if test_device_in_scope(device, Scope::Input) { + continue; + } + let count = get_channel_count(device, DeviceType::INPUT).unwrap(); + assert_eq!(count, 0); + } +} + +#[test] +fn test_get_channel_count_of_output_for_a_input_only_deivce() { + let devices = test_get_devices_in_scope(Scope::Input); + for device in devices { + // Skip in-out devices. + if test_device_in_scope(device, Scope::Output) { + continue; + } + let count = get_channel_count(device, DeviceType::OUTPUT).unwrap(); + assert_eq!(count, 0); + } +} + +#[test] +#[should_panic] +fn test_get_channel_count_of_unknown_device() { + assert!(get_channel_count(kAudioObjectUnknown, DeviceType::OUTPUT).is_err()); +} + +#[test] +fn test_get_channel_count_of_inout_type() { + test_channel_count(Scope::Input); + test_channel_count(Scope::Output); + + fn test_channel_count(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + assert_eq!( + get_channel_count(device, DeviceType::INPUT | DeviceType::OUTPUT), + get_channel_count(device, DeviceType::INPUT).map(|c| c + get_channel_count( + device, + DeviceType::OUTPUT + ) + .unwrap_or(0)) + ); + } else { + println!("No device for {:?}.", scope); + } + } +} + +#[test] +#[should_panic] +fn test_get_channel_count_of_unknwon_type() { + test_channel_count(Scope::Input); + test_channel_count(Scope::Output); + + fn test_channel_count(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + assert!(get_channel_count(device, DeviceType::UNKNOWN).is_err()); + } else { + panic!("Panic by default: No device for {:?}.", scope); + } + } +} + +// get_range_of_sample_rates +// ------------------------------------ +#[test] +fn test_get_range_of_sample_rates() { + test_get_range_of_sample_rates_in_scope(Scope::Input); + test_get_range_of_sample_rates_in_scope(Scope::Output); + + fn test_get_range_of_sample_rates_in_scope(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + let ranges = test_get_available_samplerate_of_device(device); + for range in ranges { + // Surprisingly, we can get the input/output sample rates from a non-input/non-output device. + check_samplerates(range); + } + } else { + println!("No device for {:?}.", scope); + } + } + + fn test_get_available_samplerate_of_device(id: AudioObjectID) -> Vec<(f64, f64)> { + let scopes = [ + DeviceType::INPUT, + DeviceType::OUTPUT, + DeviceType::INPUT | DeviceType::OUTPUT, + ]; + let mut ranges = Vec::new(); + for scope in scopes.iter() { + ranges.push(get_range_of_sample_rates(id, *scope).unwrap()); + } + ranges + } + + fn check_samplerates((min, max): (f64, f64)) { + assert!(min > 0.0); + assert!(max > 0.0); + assert!(min <= max); + } +} + +// get_presentation_latency +// ------------------------------------ +#[test] +fn test_get_device_presentation_latency() { + test_get_device_presentation_latencies_in_scope(Scope::Input); + test_get_device_presentation_latencies_in_scope(Scope::Output); + + fn test_get_device_presentation_latencies_in_scope(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + // TODO: The latencies very from devices to devices. Check nothing here. + let latency = get_fixed_latency(device, scope.clone().into()); + println!( + "present latency on the device {} in scope {:?}: {}", + device, scope, latency + ); + } else { + println!("No device for {:?}.", scope); + } + } +} + +// get_device_group_id +// ------------------------------------ +#[test] +fn test_get_device_group_id() { + if let Some(device) = test_get_default_device(Scope::Input) { + match get_device_group_id(device, DeviceType::INPUT) { + Ok(id) => println!("input group id: {:?}", id), + Err(e) => println!("No input group id. Error: {}", e), + } + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + match get_device_group_id(device, DeviceType::OUTPUT) { + Ok(id) => println!("output group id: {:?}", id), + Err(e) => println!("No output group id. Error: {}", e), + } + } else { + println!("No output device."); + } +} + +#[test] +fn test_get_same_group_id_for_builtin_device_pairs() { + use std::collections::HashMap; + + // These device sources have custom group id. See `get_custom_group_id`. + const IMIC: u32 = 0x696D_6963; // "imic" + const ISPK: u32 = 0x6973_706B; // "ispk" + const EMIC: u32 = 0x656D_6963; // "emic" + const HDPN: u32 = 0x6864_706E; // "hdpn" + let pairs = [(IMIC, ISPK), (EMIC, HDPN)]; + + let mut input_group_ids = HashMap::::new(); + let input_devices = test_get_devices_in_scope(Scope::Input); + for device in input_devices.iter() { + match get_device_source(*device, DeviceType::INPUT) { + Ok(source) => match get_device_group_id(*device, DeviceType::INPUT) { + Ok(id) => assert!(input_group_ids + .insert(source, id.into_string().unwrap()) + .is_none()), + Err(e) => assert!(input_group_ids + .insert(source, format!("Error {}", e)) + .is_none()), + }, + _ => {} // do nothing when failing to get source. + } + } + + let mut output_group_ids = HashMap::::new(); + let output_devices = test_get_devices_in_scope(Scope::Output); + for device in output_devices.iter() { + match get_device_source(*device, DeviceType::OUTPUT) { + Ok(source) => match get_device_group_id(*device, DeviceType::OUTPUT) { + Ok(id) => assert!(output_group_ids + .insert(source, id.into_string().unwrap()) + .is_none()), + Err(e) => assert!(output_group_ids + .insert(source, format!("Error {}", e)) + .is_none()), + }, + _ => {} // do nothing when failing to get source. + } + } + + for (input, output) in pairs.iter() { + let input_group_id = input_group_ids.get(input); + let output_group_id = output_group_ids.get(output); + + if input_group_id.is_some() && output_group_id.is_some() { + assert_eq!(input_group_id, output_group_id); + } + + input_group_ids.remove(input); + output_group_ids.remove(output); + } +} + +#[test] +#[should_panic] +fn test_get_device_group_id_by_unknown_device() { + assert!(get_device_group_id(kAudioObjectUnknown, DeviceType::INPUT).is_err()); +} + +// get_device_label +// ------------------------------------ +#[test] +fn test_get_device_label() { + if let Some(device) = test_get_default_device(Scope::Input) { + let name = get_device_label(device, DeviceType::INPUT).unwrap(); + println!("input device label: {}", name.into_string()); + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + let name = get_device_label(device, DeviceType::OUTPUT).unwrap(); + println!("output device label: {}", name.into_string()); + } else { + println!("No output device."); + } +} + +#[test] +#[should_panic] +fn test_get_device_label_by_unknown_device() { + assert!(get_device_label(kAudioObjectUnknown, DeviceType::INPUT).is_err()); +} + +// get_device_global_uid +// ------------------------------------ +#[test] +fn test_get_device_global_uid() { + // Input device. + if let Some(input) = test_get_default_device(Scope::Input) { + let uid = get_device_global_uid(input).unwrap(); + let uid = uid.into_string(); + assert!(!uid.is_empty()); + } + + // Output device. + if let Some(output) = test_get_default_device(Scope::Output) { + let uid = get_device_global_uid(output).unwrap(); + let uid = uid.into_string(); + assert!(!uid.is_empty()); + } +} + +#[test] +#[should_panic] +fn test_get_device_global_uid_by_unknwon_device() { + // Unknown device. + assert!(get_device_global_uid(kAudioObjectUnknown).is_err()); +} + +// create_cubeb_device_info +// destroy_cubeb_device_info +// ------------------------------------ +#[test] +fn test_create_cubeb_device_info() { + use std::collections::VecDeque; + + test_create_device_from_hwdev_in_scope(Scope::Input); + test_create_device_from_hwdev_in_scope(Scope::Output); + + fn test_create_device_from_hwdev_in_scope(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + let is_input = test_device_in_scope(device, Scope::Input); + let is_output = test_device_in_scope(device, Scope::Output); + let mut results = test_create_device_infos_by_device(device); + assert_eq!(results.len(), 2); + // Input device type: + let input_result = results.pop_front().unwrap(); + if is_input { + let mut input_device_info = input_result.unwrap(); + check_device_info_by_device(&input_device_info, device, Scope::Input); + destroy_cubeb_device_info(&mut input_device_info); + } else { + assert_eq!(input_result.unwrap_err(), Error::error()); + } + // Output device type: + let output_result = results.pop_front().unwrap(); + if is_output { + let mut output_device_info = output_result.unwrap(); + check_device_info_by_device(&output_device_info, device, Scope::Output); + destroy_cubeb_device_info(&mut output_device_info); + } else { + assert_eq!(output_result.unwrap_err(), Error::error()); + } + } else { + println!("No device for {:?}.", scope); + } + } + + fn test_create_device_infos_by_device( + id: AudioObjectID, + ) -> VecDeque> { + let dev_types = [DeviceType::INPUT, DeviceType::OUTPUT]; + let mut results = VecDeque::new(); + for dev_type in dev_types.iter() { + results.push_back(create_cubeb_device_info(id, *dev_type)); + } + results + } + + fn check_device_info_by_device(info: &ffi::cubeb_device_info, id: AudioObjectID, scope: Scope) { + assert!(!info.devid.is_null()); + assert!(mem::size_of_val(&info.devid) >= mem::size_of::()); + assert_eq!(info.devid as AudioObjectID, id); + assert!(!info.device_id.is_null()); + assert!(!info.friendly_name.is_null()); + assert!(!info.group_id.is_null()); + + // TODO: Hit a kAudioHardwareUnknownPropertyError for AirPods + // assert!(!info.vendor_name.is_null()); + + // FIXME: The device is defined to input-only or output-only, but some device is in-out! + assert_eq!(info.device_type, DeviceType::from(scope.clone()).bits()); + assert_eq!(info.state, ffi::CUBEB_DEVICE_STATE_ENABLED); + // TODO: The preference is set when the device is default input/output device if the device + // info is created from input/output scope. Should the preference be set if the + // device is a default input/output device if the device info is created from + // output/input scope ? The device may be a in-out device! + assert_eq!(info.preferred, get_cubeb_device_pref(id, scope)); + + assert_eq!(info.format, ffi::CUBEB_DEVICE_FMT_ALL); + assert_eq!(info.default_format, ffi::CUBEB_DEVICE_FMT_F32NE); + assert!(info.max_channels > 0); + assert!(info.min_rate <= info.max_rate); + assert!(info.min_rate <= info.default_rate); + assert!(info.default_rate <= info.max_rate); + + assert!(info.latency_lo > 0); + assert!(info.latency_hi > 0); + assert!(info.latency_lo <= info.latency_hi); + + fn get_cubeb_device_pref(id: AudioObjectID, scope: Scope) -> ffi::cubeb_device_pref { + let default_device = test_get_default_device(scope); + if default_device.is_some() && default_device.unwrap() == id { + ffi::CUBEB_DEVICE_PREF_ALL + } else { + ffi::CUBEB_DEVICE_PREF_NONE + } + } + } +} + +#[test] +#[should_panic] +fn test_create_device_info_by_unknown_device() { + assert!(create_cubeb_device_info(kAudioObjectUnknown, DeviceType::OUTPUT).is_err()); +} + +#[test] +fn test_create_device_info_with_unknown_type() { + test_create_device_info_with_unknown_type_by_scope(Scope::Input); + test_create_device_info_with_unknown_type_by_scope(Scope::Output); + + fn test_create_device_info_with_unknown_type_by_scope(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + assert!(create_cubeb_device_info(device, DeviceType::UNKNOWN).is_err()); + } + } +} + +#[test] +#[should_panic] +fn test_device_destroy_empty_device() { + let mut device = ffi::cubeb_device_info::default(); + + assert!(device.device_id.is_null()); + assert!(device.group_id.is_null()); + assert!(device.friendly_name.is_null()); + assert!(device.vendor_name.is_null()); + + // `friendly_name` must be set. + destroy_cubeb_device_info(&mut device); + + assert!(device.device_id.is_null()); + assert!(device.group_id.is_null()); + assert!(device.friendly_name.is_null()); + assert!(device.vendor_name.is_null()); +} + +#[test] +fn test_create_device_from_hwdev_with_inout_type() { + test_create_device_from_hwdev_with_inout_type_by_scope(Scope::Input); + test_create_device_from_hwdev_with_inout_type_by_scope(Scope::Output); + + fn test_create_device_from_hwdev_with_inout_type_by_scope(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + // Get a kAudioHardwareUnknownPropertyError in get_channel_count actually. + assert!( + create_cubeb_device_info(device, DeviceType::INPUT | DeviceType::OUTPUT).is_err() + ); + } else { + println!("No device for {:?}.", scope); + } + } +} + +// get_devices_of_type +// ------------------------------------ +#[test] +fn test_get_devices_of_type() { + use std::collections::HashSet; + + let all_devices = audiounit_get_devices_of_type(DeviceType::INPUT | DeviceType::OUTPUT); + let input_devices = audiounit_get_devices_of_type(DeviceType::INPUT); + let output_devices = audiounit_get_devices_of_type(DeviceType::OUTPUT); + + let mut expected_all = test_get_all_devices(DeviceFilter::ExcludeCubebAggregateAndVPIO); + expected_all.sort(); + assert_eq!(all_devices, expected_all); + for device in all_devices.iter() { + if test_device_in_scope(*device, Scope::Input) { + assert!(input_devices.contains(device)); + } + if test_device_in_scope(*device, Scope::Output) { + assert!(output_devices.contains(device)); + } + } + + let input: HashSet = input_devices.iter().cloned().collect(); + let output: HashSet = output_devices.iter().cloned().collect(); + let union: HashSet = input.union(&output).cloned().collect(); + let mut union_devices: Vec = union.iter().cloned().collect(); + union_devices.sort(); + assert_eq!(all_devices, union_devices); +} + +#[test] +#[should_panic] +fn test_get_devices_of_type_unknown() { + let no_devs = audiounit_get_devices_of_type(DeviceType::UNKNOWN); + assert!(no_devs.is_empty()); +} + +// add_devices_changed_listener +// ------------------------------------ +#[test] +fn test_add_devices_changed_listener() { + use std::collections::HashMap; + + extern "C" fn inout_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + extern "C" fn in_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + extern "C" fn out_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + + let mut map: HashMap = HashMap::new(); + map.insert(DeviceType::INPUT, in_callback); + map.insert(DeviceType::OUTPUT, out_callback); + map.insert(DeviceType::INPUT | DeviceType::OUTPUT, inout_callback); + + test_get_raw_context(|context| { + for (devtype, callback) in map.iter() { + assert!(get_devices_changed_callback(context, Scope::Input).is_none()); + assert!(get_devices_changed_callback(context, Scope::Output).is_none()); + + // Register a callback within a specific scope. + assert!(context + .add_devices_changed_listener(*devtype, Some(*callback), ptr::null_mut()) + .is_ok()); + + if devtype.contains(DeviceType::INPUT) { + let cb = get_devices_changed_callback(context, Scope::Input); + assert!(cb.is_some()); + assert_eq!(cb.unwrap(), *callback); + } else { + let cb = get_devices_changed_callback(context, Scope::Input); + assert!(cb.is_none()); + } + + if devtype.contains(DeviceType::OUTPUT) { + let cb = get_devices_changed_callback(context, Scope::Output); + assert!(cb.is_some()); + assert_eq!(cb.unwrap(), *callback); + } else { + let cb = get_devices_changed_callback(context, Scope::Output); + assert!(cb.is_none()); + } + + // Unregister the callbacks within all scopes. + assert!(context + .remove_devices_changed_listener(DeviceType::INPUT | DeviceType::OUTPUT) + .is_ok()); + + assert!(get_devices_changed_callback(context, Scope::Input).is_none()); + assert!(get_devices_changed_callback(context, Scope::Output).is_none()); + } + }); +} + +#[test] +#[should_panic] +fn test_add_devices_changed_listener_in_unknown_scope() { + extern "C" fn callback(_: *mut ffi::cubeb, _: *mut c_void) {} + + test_get_raw_context(|context| { + let _ = context.add_devices_changed_listener( + DeviceType::UNKNOWN, + Some(callback), + ptr::null_mut(), + ); + }); +} + +#[test] +#[should_panic] +fn test_add_devices_changed_listener_with_none_callback() { + test_get_raw_context(|context| { + for devtype in &[DeviceType::INPUT, DeviceType::OUTPUT] { + assert!(context + .add_devices_changed_listener(*devtype, None, ptr::null_mut()) + .is_ok()); + } + }); +} + +// remove_devices_changed_listener +// ------------------------------------ +#[test] +fn test_remove_devices_changed_listener() { + use std::collections::HashMap; + + extern "C" fn in_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + extern "C" fn out_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + + let mut map: HashMap = HashMap::new(); + map.insert(DeviceType::INPUT, in_callback); + map.insert(DeviceType::OUTPUT, out_callback); + + test_get_raw_context(|context| { + for (devtype, _callback) in map.iter() { + assert!(get_devices_changed_callback(context, Scope::Input).is_none()); + assert!(get_devices_changed_callback(context, Scope::Output).is_none()); + + // Register callbacks within all scopes. + for (scope, listener) in map.iter() { + assert!(context + .add_devices_changed_listener(*scope, Some(*listener), ptr::null_mut()) + .is_ok()); + } + + let input_callback = get_devices_changed_callback(context, Scope::Input); + assert!(input_callback.is_some()); + assert_eq!( + input_callback.unwrap(), + *(map.get(&DeviceType::INPUT).unwrap()) + ); + let output_callback = get_devices_changed_callback(context, Scope::Output); + assert!(output_callback.is_some()); + assert_eq!( + output_callback.unwrap(), + *(map.get(&DeviceType::OUTPUT).unwrap()) + ); + + // Unregister the callbacks within one specific scopes. + assert!(context.remove_devices_changed_listener(*devtype).is_ok()); + + if devtype.contains(DeviceType::INPUT) { + let cb = get_devices_changed_callback(context, Scope::Input); + assert!(cb.is_none()); + } else { + let cb = get_devices_changed_callback(context, Scope::Input); + assert!(cb.is_some()); + assert_eq!(cb.unwrap(), *(map.get(&DeviceType::INPUT).unwrap())); + } + + if devtype.contains(DeviceType::OUTPUT) { + let cb = get_devices_changed_callback(context, Scope::Output); + assert!(cb.is_none()); + } else { + let cb = get_devices_changed_callback(context, Scope::Output); + assert!(cb.is_some()); + assert_eq!(cb.unwrap(), *(map.get(&DeviceType::OUTPUT).unwrap())); + } + + // Unregister the callbacks within all scopes. + assert!(context + .remove_devices_changed_listener(DeviceType::INPUT | DeviceType::OUTPUT) + .is_ok()); + } + }); +} + +#[test] +fn test_remove_devices_changed_listener_without_adding_listeners() { + test_get_raw_context(|context| { + for devtype in &[ + DeviceType::INPUT, + DeviceType::OUTPUT, + DeviceType::INPUT | DeviceType::OUTPUT, + ] { + assert!(context.remove_devices_changed_listener(*devtype).is_ok()); + } + }); +} + +#[test] +fn test_remove_devices_changed_listener_within_all_scopes() { + use std::collections::HashMap; + + extern "C" fn inout_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + extern "C" fn in_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + extern "C" fn out_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + + let mut map: HashMap = HashMap::new(); + map.insert(DeviceType::INPUT, in_callback); + map.insert(DeviceType::OUTPUT, out_callback); + map.insert(DeviceType::INPUT | DeviceType::OUTPUT, inout_callback); + + test_get_raw_context(|context| { + for (devtype, callback) in map.iter() { + assert!(get_devices_changed_callback(context, Scope::Input).is_none()); + assert!(get_devices_changed_callback(context, Scope::Output).is_none()); + + assert!(context + .add_devices_changed_listener(*devtype, Some(*callback), ptr::null_mut()) + .is_ok()); + + if devtype.contains(DeviceType::INPUT) { + let cb = get_devices_changed_callback(context, Scope::Input); + assert!(cb.is_some()); + assert_eq!(cb.unwrap(), *callback); + } + + if devtype.contains(DeviceType::OUTPUT) { + let cb = get_devices_changed_callback(context, Scope::Output); + assert!(cb.is_some()); + assert_eq!(cb.unwrap(), *callback); + } + + assert!(context + .remove_devices_changed_listener(DeviceType::INPUT | DeviceType::OUTPUT) + .is_ok()); + + assert!(get_devices_changed_callback(context, Scope::Input).is_none()); + assert!(get_devices_changed_callback(context, Scope::Output).is_none()); + } + }); +} + +fn get_devices_changed_callback( + context: &AudioUnitContext, + scope: Scope, +) -> ffi::cubeb_device_collection_changed_callback { + let devices_guard = context.devices.lock().unwrap(); + match scope { + Scope::Input => devices_guard.input.changed_callback, + Scope::Output => devices_guard.output.changed_callback, + } +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/backlog.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/backlog.rs new file mode 100644 index 0000000000..5342ec0f39 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/backlog.rs @@ -0,0 +1,36 @@ +// Copyright © 2018 Mozilla Foundation +// +// This program is made available under an ISC-style license. See the +// accompanying file LICENSE for details. +use super::utils::test_get_default_raw_stream; +use super::*; + +// Interface +// ============================================================================ +// Remove these after test_ops_stream_register_device_changed_callback works. +#[test] +fn test_stream_register_device_changed_callback() { + extern "C" fn callback(_: *mut c_void) {} + + test_get_default_raw_stream(|stream| { + assert!(stream + .register_device_changed_callback(Some(callback)) + .is_ok()); + assert!(stream.register_device_changed_callback(None).is_ok()); + }); +} + +#[test] +fn test_stream_register_device_changed_callback_twice() { + extern "C" fn callback1(_: *mut c_void) {} + extern "C" fn callback2(_: *mut c_void) {} + + test_get_default_raw_stream(|stream| { + assert!(stream + .register_device_changed_callback(Some(callback1)) + .is_ok()); + assert!(stream + .register_device_changed_callback(Some(callback2)) + .is_err()); + }); +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/device_change.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/device_change.rs new file mode 100644 index 0000000000..c27dada7ad --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/device_change.rs @@ -0,0 +1,885 @@ +// NOTICE: +// Avoid running TestDeviceSwitcher with TestDevicePlugger or active full-duplex streams +// sequentially! +// +// The TestDeviceSwitcher cannot work with any test that will create an aggregate device that is +// soon being destroyed. The TestDeviceSwitcher will cache the available devices, upon it's +// created, as the candidates for the default device. Therefore, those created aggregate devices +// may be cached in TestDeviceSwitcher. However, those aggregate devices may be destroyed when +// TestDeviceSwitcher is using them or they are in the cached list of TestDeviceSwitcher. +// +// Running those tests by setting `test-threads=1` doesn't really help (e.g., +// `cargo test test_register_device_changed_callback -- --ignored --nocapture --test-threads=1`). +// The aggregate device won't be destroyed immediately when `kAudioPlugInDestroyAggregateDevice` +// is set. As a result, the following tests requiring changing the devices will be run separately +// in the run_tests.sh script and marked by `ignore` by default. + +use super::utils::{ + get_devices_info_in_scope, test_create_device_change_listener, test_device_in_scope, + test_get_default_device, test_get_devices_in_scope, + test_get_stream_with_default_data_callback_by_type, test_ops_stream_operation, + test_set_default_device, Scope, StreamType, TestDevicePlugger, TestDeviceSwitcher, +}; +use super::*; +use std::sync::{LockResult, MutexGuard, WaitTimeoutResult}; + +// Switch default devices used by the active streams, to test stream reinitialization +// ================================================================================================ +#[ignore] +#[test] +fn test_switch_device() { + test_switch_device_in_scope(Scope::Input); + test_switch_device_in_scope(Scope::Output); +} + +fn test_switch_device_in_scope(scope: Scope) { + println!( + "Switch default device for {:?} while the stream is working.", + scope + ); + + // Do nothing if there is no 2 available devices at least. + let devices = test_get_devices_in_scope(scope.clone()); + if devices.len() < 2 { + println!("Need 2 devices for {:?} at least. Skip.", scope); + return; + } + + let mut device_switcher = TestDeviceSwitcher::new(scope.clone()); + + let notifier = Arc::new(Notifier::new(0)); + let also_notifier = notifier.clone(); + let listener = test_create_device_change_listener(scope.clone(), move |_addresses| { + let mut cnt = notifier.lock().unwrap(); + *cnt += 1; + notifier.notify(cnt); + NO_ERR + }); + listener.start(); + + let changed_watcher = Watcher::new(&also_notifier); + test_get_started_stream_in_scope(scope.clone(), move |_stream| loop { + let mut guard = changed_watcher.lock().unwrap(); + let start_cnt = guard.clone(); + device_switcher.next(); + guard = changed_watcher + .wait_while(guard, |cnt| *cnt == start_cnt) + .unwrap(); + if *guard >= devices.len() { + break; + } + }); +} + +fn test_get_started_stream_in_scope(scope: Scope, operation: F) +where + F: FnOnce(*mut ffi::cubeb_stream), +{ + use std::f32::consts::PI; + const SAMPLE_FREQUENCY: u32 = 48_000; + + // 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.format = ffi::CUBEB_SAMPLE_S16NE; + stream_params.rate = SAMPLE_FREQUENCY; + stream_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; + stream_params.channels = 1; + stream_params.layout = ffi::CUBEB_LAYOUT_MONO; + + let (input_params, output_params) = match scope { + Scope::Input => ( + &mut stream_params as *mut ffi::cubeb_stream_params, + ptr::null_mut(), + ), + Scope::Output => ( + ptr::null_mut(), + &mut stream_params as *mut ffi::cubeb_stream_params, + ), + }; + + extern "C" fn state_callback( + stream: *mut ffi::cubeb_stream, + user_ptr: *mut c_void, + state: ffi::cubeb_state, + ) { + assert!(!stream.is_null()); + assert!(!user_ptr.is_null()); + assert_ne!(state, ffi::CUBEB_STATE_ERROR); + } + + extern "C" fn input_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()); + assert!(!user_ptr.is_null()); + assert!(!input_buffer.is_null()); + assert!(output_buffer.is_null()); + nframes + } + + let mut position: i64 = 0; // TODO: Use Atomic instead. + + fn f32_to_i16_sample(x: f32) -> i16 { + (x * f32::from(i16::max_value())) as i16 + } + + extern "C" fn output_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()); + assert!(!user_ptr.is_null()); + assert!(input_buffer.is_null()); + assert!(!output_buffer.is_null()); + + let buffer = unsafe { + let ptr = output_buffer as *mut i16; + let len = nframes as usize; + slice::from_raw_parts_mut(ptr, len) + }; + + let position = unsafe { &mut *(user_ptr as *mut i64) }; + + // Generate tone on the fly. + for data in buffer.iter_mut() { + let t1 = (2.0 * PI * 350.0 * (*position) as f32 / SAMPLE_FREQUENCY as f32).sin(); + let t2 = (2.0 * PI * 440.0 * (*position) as f32 / SAMPLE_FREQUENCY as f32).sin(); + *data = f32_to_i16_sample(0.5 * (t1 + t2)); + *position += 1; + } + + nframes + } + + test_ops_stream_operation( + "stream", + ptr::null_mut(), // Use default input device. + input_params, + ptr::null_mut(), // Use default output device. + output_params, + 4096, // TODO: Get latency by get_min_latency instead ? + match scope { + Scope::Input => Some(input_data_callback), + Scope::Output => Some(output_data_callback), + }, + Some(state_callback), + &mut position as *mut i64 as *mut c_void, + |stream| { + assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); + operation(stream); + assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK); + }, + ); +} + +// Plug and unplug devices, to test device collection changed callback +// ================================================================================================ +#[ignore] +#[test] +fn test_plug_and_unplug_device() { + test_plug_and_unplug_device_in_scope(Scope::Input); + test_plug_and_unplug_device_in_scope(Scope::Output); +} + +fn test_plug_and_unplug_device_in_scope(scope: Scope) { + let default_device = test_get_default_device(scope.clone()); + if default_device.is_none() { + println!("No device for {:?} to test", scope); + return; + } + + println!("Run test for {:?}", scope); + println!("NOTICE: The test will hang if the default input or output is an aggregate device.\nWe will fix this later."); + + let default_device = default_device.unwrap(); + let is_input = test_device_in_scope(default_device, Scope::Input); + let is_output = test_device_in_scope(default_device, Scope::Output); + + let mut context = AudioUnitContext::new(); + + // Register the devices-changed callbacks. + #[derive(Clone, PartialEq)] + struct Counts { + input: u32, + output: u32, + } + impl Counts { + fn new() -> Self { + Self { + input: 0, + output: 0, + } + } + } + let counts = Arc::new(Notifier::new(Counts::new())); + let counts_notifier_ptr = counts.as_ref() as *const Notifier; + + assert!(context + .register_device_collection_changed( + DeviceType::INPUT, + Some(input_changed_callback), + counts_notifier_ptr as *mut c_void, + ) + .is_ok()); + + assert!(context + .register_device_collection_changed( + DeviceType::OUTPUT, + Some(output_changed_callback), + counts_notifier_ptr as *mut c_void, + ) + .is_ok()); + + let counts_watcher = Watcher::new(&counts); + + let mut device_plugger = TestDevicePlugger::new(scope).unwrap(); + + { + // Simulate adding devices and monitor the devices-changed callbacks. + let mut counts_guard = counts.lock().unwrap(); + let counts_start = counts_guard.clone(); + + assert!(device_plugger.plug().is_ok()); + + counts_guard = counts_watcher + .wait_while(counts_guard, |counts| { + (is_input && counts.input == counts_start.input) + || (is_output && counts.output == counts_start.output) + }) + .unwrap(); + + // Check changed count. + assert_eq!(counts_guard.input, if is_input { 1 } else { 0 }); + assert_eq!(counts_guard.output, if is_output { 1 } else { 0 }); + } + + { + // Simulate removing devices and monitor the devices-changed callbacks. + let mut counts_guard = counts.lock().unwrap(); + let counts_start = counts_guard.clone(); + + assert!(device_plugger.unplug().is_ok()); + + counts_guard = counts_watcher + .wait_while(counts_guard, |counts| { + (is_input && counts.input == counts_start.input) + || (is_output && counts.output == counts_start.output) + }) + .unwrap(); + + // Check changed count. + assert_eq!(counts_guard.input, if is_input { 2 } else { 0 }); + assert_eq!(counts_guard.output, if is_output { 2 } else { 0 }); + } + + extern "C" fn input_changed_callback(context: *mut ffi::cubeb, data: *mut c_void) { + println!( + "Input device collection @ {:p} is changed. Data @ {:p}", + context, data + ); + let notifier = unsafe { &*(data as *const Notifier) }; + { + let mut counts = notifier.lock().unwrap(); + counts.input += 1; + notifier.notify(counts); + } + } + + extern "C" fn output_changed_callback(context: *mut ffi::cubeb, data: *mut c_void) { + println!( + "output device collection @ {:p} is changed. Data @ {:p}", + context, data + ); + let notifier = unsafe { &*(data as *const Notifier) }; + { + let mut counts = notifier.lock().unwrap(); + counts.output += 1; + notifier.notify(counts); + } + } + + context.register_device_collection_changed(DeviceType::OUTPUT, None, ptr::null_mut()); + context.register_device_collection_changed(DeviceType::INPUT, None, ptr::null_mut()); +} + +// Switch default devices used by the active streams, to test device changed callback +// ================================================================================================ +#[ignore] +#[test] +fn test_register_device_changed_callback_to_check_default_device_changed_input() { + test_register_device_changed_callback_to_check_default_device_changed(StreamType::INPUT); +} + +#[ignore] +#[test] +fn test_register_device_changed_callback_to_check_default_device_changed_output() { + test_register_device_changed_callback_to_check_default_device_changed(StreamType::OUTPUT); +} + +#[ignore] +#[test] +fn test_register_device_changed_callback_to_check_default_device_changed_duplex() { + test_register_device_changed_callback_to_check_default_device_changed(StreamType::DUPLEX); +} + +fn test_register_device_changed_callback_to_check_default_device_changed(stm_type: StreamType) { + println!("NOTICE: The test will hang if the default input or output is an aggregate device.\nWe will fix this later."); + + let inputs = if stm_type.contains(StreamType::INPUT) { + let devices = test_get_devices_in_scope(Scope::Input).len(); + if devices >= 2 { + Some(devices) + } else { + None + } + } else { + None + }; + + let outputs = if stm_type.contains(StreamType::OUTPUT) { + let devices = test_get_devices_in_scope(Scope::Output).len(); + if devices >= 2 { + Some(devices) + } else { + None + } + } else { + None + }; + + if inputs.is_none() && outputs.is_none() { + println!("No enough devices to run the test!"); + return; + } + + let changed_count = Arc::new(Notifier::new(0u32)); + let notifier_ptr = changed_count.as_ref() as *const Notifier; + + test_get_stream_with_device_changed_callback( + "stream: test callback for default device changed", + stm_type, + None, // Use default input device. + None, // Use default output device. + notifier_ptr as *mut c_void, + state_callback, + device_changed_callback, + |stream| { + // If the duplex stream uses different input and output device, + // an aggregate device will be created and it will work for this duplex stream. + // This aggregate device will be added into the device list, but it won't + // be assigned to the default device, since the device list for setting + // default device is cached upon {input, output}_device_switcher is initialized. + + let changed_watcher = Watcher::new(&changed_count); + + if let Some(devices) = inputs { + let mut device_switcher = TestDeviceSwitcher::new(Scope::Input); + for _ in 0..devices { + // While the stream is re-initializing for the default device switch, + // switching for the default device again will be ignored. + while stream.switching_device.load(atomic::Ordering::SeqCst) { + std::hint::spin_loop() + } + let guard = changed_watcher.lock().unwrap(); + let start_cnt = guard.clone(); + device_switcher.next(); + changed_watcher + .wait_while(guard, |cnt| *cnt == start_cnt) + .unwrap(); + } + } + + if let Some(devices) = outputs { + let mut device_switcher = TestDeviceSwitcher::new(Scope::Output); + for _ in 0..devices { + // While the stream is re-initializing for the default device switch, + // switching for the default device again will be ignored. + while stream.switching_device.load(atomic::Ordering::SeqCst) { + std::hint::spin_loop() + } + let guard = changed_watcher.lock().unwrap(); + let start_cnt = guard.clone(); + device_switcher.next(); + changed_watcher + .wait_while(guard, |cnt| *cnt == start_cnt) + .unwrap(); + } + } + }, + ); + + extern "C" fn state_callback( + stream: *mut ffi::cubeb_stream, + _user_ptr: *mut c_void, + state: ffi::cubeb_state, + ) { + assert!(!stream.is_null()); + assert_ne!(state, ffi::CUBEB_STATE_ERROR); + } + + extern "C" fn device_changed_callback(data: *mut c_void) { + println!("Device change callback. data @ {:p}", data); + let notifier = unsafe { &*(data as *const Notifier) }; + let mut count_guard = notifier.lock().unwrap(); + *count_guard += 1; + notifier.notify(count_guard); + } +} + +// Unplug the devices used by the active streams, to test +// 1) device changed callback, or state callback +// 2) stream reinitialization that may race with stream destroying +// ================================================================================================ + +// Input-only stream +// ----------------- + +// Unplug the non-default input device for an input stream +// ------------------------------------------------------------------------------------------------ + +#[ignore] +#[test] +fn test_destroy_input_stream_after_unplugging_a_nondefault_input_device() { + // The stream can be destroyed before running device-changed event handler + test_unplug_a_device_on_an_active_stream(StreamType::INPUT, Scope::Input, false, 0); +} + +#[ignore] +#[test] +fn test_suspend_input_stream_by_unplugging_a_nondefault_input_device() { + // Expect to get an error state callback by device-changed event handler + test_unplug_a_device_on_an_active_stream(StreamType::INPUT, Scope::Input, false, 2000); +} + +// Unplug the default input device for an input stream +// ------------------------------------------------------------------------------------------------ +#[ignore] +#[test] +fn test_destroy_input_stream_after_unplugging_a_default_input_device() { + // Expect to get an device-changed callback by device-changed event handler, + // which will reinitialize the stream behind the scenes, at the same when + // the stream is being destroyed + test_unplug_a_device_on_an_active_stream(StreamType::INPUT, Scope::Input, true, 0); +} + +#[ignore] +#[test] +fn test_reinit_input_stream_by_unplugging_a_default_input_device() { + // Expect to get an device-changed callback by device-changed event handler, + // which will reinitialize the stream behind the scenes + test_unplug_a_device_on_an_active_stream(StreamType::INPUT, Scope::Input, true, 2000); +} + +// Output-only stream +// ------------------ + +// Unplug the non-default output device for an output stream +// ------------------------------------------------------------------------------------------------ +#[ignore] +#[test] +fn test_destroy_output_stream_after_unplugging_a_nondefault_output_device() { + test_unplug_a_device_on_an_active_stream(StreamType::OUTPUT, Scope::Output, false, 0); +} + +#[ignore] +#[test] +fn test_suspend_output_stream_by_unplugging_a_nondefault_output_device() { + test_unplug_a_device_on_an_active_stream(StreamType::OUTPUT, Scope::Output, false, 2000); +} + +// Unplug the default output device for an output stream +// ------------------------------------------------------------------------------------------------ + +#[ignore] +#[test] +fn test_destroy_output_stream_after_unplugging_a_default_output_device() { + // Expect to get an device-changed callback by device-changed event handler, + // which will reinitialize the stream behind the scenes, at the same when + // the stream is being destroyed + test_unplug_a_device_on_an_active_stream(StreamType::OUTPUT, Scope::Output, true, 0); +} + +#[ignore] +#[test] +fn test_reinit_output_stream_by_unplugging_a_default_output_device() { + // Expect to get an device-changed callback by device-changed event handler, + // which will reinitialize the stream behind the scenes + test_unplug_a_device_on_an_active_stream(StreamType::OUTPUT, Scope::Output, true, 2000); +} + +// Duplex stream +// ------------- + +// Unplug the non-default input device for a duplex stream +// ------------------------------------------------------------------------------------------------ + +#[ignore] +#[test] +fn test_destroy_duplex_stream_after_unplugging_a_nondefault_input_device() { + // The stream can be destroyed before running device-changed event handler + test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Input, false, 0); +} + +#[ignore] +#[test] +fn test_suspend_duplex_stream_by_unplugging_a_nondefault_input_device() { + // Expect to get an error state callback by device-changed event handler + test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Input, false, 2000); +} + +// Unplug the non-default output device for a duplex stream +// ------------------------------------------------------------------------------------------------ + +#[ignore] +#[test] +fn test_destroy_duplex_stream_after_unplugging_a_nondefault_output_device() { + test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Output, false, 0); +} + +#[ignore] +#[test] +fn test_suspend_duplex_stream_by_unplugging_a_nondefault_output_device() { + test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Output, false, 2000); +} + +// Unplug the non-default in-out device for a duplex stream +// ------------------------------------------------------------------------------------------------ +// TODO: Implement an in-out TestDevicePlugger + +// Unplug the default input device for a duplex stream +// ------------------------------------------------------------------------------------------------ + +#[ignore] +#[test] +fn test_destroy_duplex_stream_after_unplugging_a_default_input_device() { + // Expect to get an device-changed callback by device-changed event handler, + // which will reinitialize the stream behind the scenes, at the same when + // the stream is being destroyed + test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Input, true, 0); +} + +#[ignore] +#[test] +fn test_reinit_duplex_stream_by_unplugging_a_default_input_device() { + // Expect to get an device-changed callback by device-changed event handler, + // which will reinitialize the stream behind the scenes + test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Input, true, 2000); +} + +// Unplug the default ouput device for a duplex stream +// ------------------------------------------------------------------------------------------------ + +#[ignore] +#[test] +fn test_destroy_duplex_stream_after_unplugging_a_default_output_device() { + // Expect to get an device-changed callback by device-changed event handler, + // which will reinitialize the stream behind the scenes, at the same when + // the stream is being destroyed + test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Output, true, 0); +} + +#[ignore] +#[test] +fn test_reinit_duplex_stream_by_unplugging_a_default_output_device() { + // Expect to get an device-changed callback by device-changed event handler, + // which will reinitialize the stream behind the scenes + test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Output, true, 2000); +} + +fn test_unplug_a_device_on_an_active_stream( + stream_type: StreamType, + device_scope: Scope, + set_device_to_default: bool, + wait_up_to_ms: u64, +) { + let has_input = test_get_default_device(Scope::Input).is_some(); + let has_output = test_get_default_device(Scope::Output).is_some(); + + if stream_type.contains(StreamType::INPUT) && !has_input { + println!("No input device for input or duplex stream."); + return; + } + + if stream_type.contains(StreamType::OUTPUT) && !has_output { + println!("No output device for output or duplex stream."); + return; + } + + let default_device_before_plugging = test_get_default_device(device_scope.clone()).unwrap(); + println!( + "Before plugging, default {:?} device is {}", + device_scope, default_device_before_plugging + ); + + let mut plugger = TestDevicePlugger::new(device_scope.clone()).unwrap(); + assert!(plugger.plug().is_ok()); + assert_ne!(plugger.get_device_id(), kAudioObjectUnknown); + println!( + "Create plugger device: {} for {:?}", + plugger.get_device_id(), + device_scope + ); + + let default_device_after_plugging = test_get_default_device(device_scope.clone()).unwrap(); + println!( + "After plugging, default {:?} device is {}", + device_scope, default_device_after_plugging + ); + + // The new device, plugger, is possible to be set to the default device. + // Before running the test, we need to set the default device to the correct one. + if set_device_to_default { + // plugger should be the default device for the test. + // If it's not, then set it to the default device. + if default_device_after_plugging != plugger.get_device_id() { + let prev_def_dev = + test_set_default_device(plugger.get_device_id(), device_scope.clone()).unwrap(); + assert_eq!(prev_def_dev, default_device_after_plugging); + } + } else { + // plugger should NOT be the default device for the test. + // If it is, reset the default device to another one. + if default_device_after_plugging == plugger.get_device_id() { + let prev_def_dev = + test_set_default_device(default_device_before_plugging, device_scope.clone()) + .unwrap(); + assert_eq!(prev_def_dev, default_device_after_plugging); + } + } + + // Ignore the return devices' info since we only need to print them. + let _ = get_devices_info_in_scope(device_scope.clone()); + println!( + "Current default {:?} device is {}", + device_scope, + test_get_default_device(device_scope.clone()).unwrap() + ); + + let (input_device, output_device) = match device_scope { + Scope::Input => ( + if set_device_to_default { + None // default input device. + } else { + Some(plugger.get_device_id()) + }, + None, + ), + Scope::Output => ( + None, + if set_device_to_default { + None // default output device. + } else { + Some(plugger.get_device_id()) + }, + ), + }; + + #[derive(Clone, PartialEq)] + struct Data { + changed_count: u32, + states: Vec, + } + + impl Data { + fn new() -> Self { + Self { + changed_count: 0, + states: vec![], + } + } + } + + let notifier = Arc::new(Notifier::new(Data::new())); + let notifier_ptr = notifier.as_ref() as *const Notifier; + + test_get_stream_with_device_changed_callback( + "stream: test stream reinit/destroy after unplugging a device", + stream_type, + input_device, + output_device, + notifier_ptr as *mut c_void, + state_callback, + device_changed_callback, + |stream| { + stream.start(); + + let changed_watcher = Watcher::new(¬ifier); + let mut data_guard = notifier.lock().unwrap(); + assert_eq!(data_guard.states.last().unwrap(), &ffi::CUBEB_STATE_STARTED); + + println!( + "Stream runs on the device {} for {:?}", + plugger.get_device_id(), + device_scope + ); + + let dev = plugger.get_device_id(); + let start_changed_count = data_guard.changed_count.clone(); + + assert!(plugger.unplug().is_ok()); + + if set_device_to_default { + // The stream will be reinitialized if it follows the default input or output device. + println!("Waiting for default device to change and reinit"); + data_guard = changed_watcher + .wait_while(data_guard, |data| { + data.changed_count == start_changed_count + || data.states.last().unwrap_or(&ffi::CUBEB_STATE_ERROR) + != &ffi::CUBEB_STATE_STARTED + }) + .unwrap(); + } else if wait_up_to_ms > 0 { + // stream can be dropped immediately before device-changed callback + // so we only check the states if we wait for it explicitly. + println!("Waiting for non-default device to enter error state"); + let (new_guard, timeout_res) = changed_watcher + .wait_timeout_while(data_guard, Duration::from_millis(wait_up_to_ms), |data| { + data.states.last().unwrap_or(&ffi::CUBEB_STATE_STARTED) + != &ffi::CUBEB_STATE_ERROR + }) + .unwrap(); + assert!(!timeout_res.timed_out()); + data_guard = new_guard; + } + + println!( + "Device {} for {:?} has been unplugged. The default {:?} device now is {}", + dev, + device_scope, + device_scope, + test_get_default_device(device_scope.clone()).unwrap() + ); + + println!("The stream is going to be destroyed soon"); + }, + ); + + extern "C" fn state_callback( + stream: *mut ffi::cubeb_stream, + user_ptr: *mut c_void, + state: ffi::cubeb_state, + ) { + println!("Device change callback. user_ptr @ {:p}", user_ptr); + assert!(!stream.is_null()); + println!( + "state: {}", + match state { + ffi::CUBEB_STATE_STARTED => "started", + ffi::CUBEB_STATE_STOPPED => "stopped", + ffi::CUBEB_STATE_DRAINED => "drained", + ffi::CUBEB_STATE_ERROR => "error", + _ => "unknown", + } + ); + let notifier = unsafe { &mut *(user_ptr as *mut Notifier) }; + let mut data_guard = notifier.lock().unwrap(); + data_guard.states.push(state); + notifier.notify(data_guard); + } + + extern "C" fn device_changed_callback(user_ptr: *mut c_void) { + println!("Device change callback. user_ptr @ {:p}", user_ptr); + let notifier = unsafe { &mut *(user_ptr as *mut Notifier) }; + let mut data_guard = notifier.lock().unwrap(); + data_guard.changed_count += 1; + notifier.notify(data_guard); + } +} + +struct Notifier { + value: Mutex, + cvar: Condvar, +} + +impl Notifier { + fn new(value: T) -> Self { + Self { + value: Mutex::new(value), + cvar: Condvar::new(), + } + } + + fn lock(&self) -> LockResult> { + self.value.lock() + } + + fn notify(&self, _guard: MutexGuard<'_, T>) { + self.cvar.notify_all(); + } +} + +struct Watcher { + notifier: Arc>, +} + +impl Watcher { + fn new(value: &Arc>) -> Self { + Self { + notifier: Arc::clone(value), + } + } + + fn lock(&self) -> LockResult> { + self.notifier.lock() + } + + fn wait_while<'a, F>( + &self, + guard: MutexGuard<'a, T>, + condition: F, + ) -> LockResult> + where + F: FnMut(&mut T) -> bool, + { + self.notifier.cvar.wait_while(guard, condition) + } + + fn wait_timeout_while<'a, F>( + &self, + guard: MutexGuard<'a, T>, + dur: Duration, + condition: F, + ) -> LockResult<(MutexGuard<'a, T>, WaitTimeoutResult)> + where + F: FnMut(&mut T) -> bool, + { + self.notifier.cvar.wait_timeout_while(guard, dur, condition) + } +} + +fn test_get_stream_with_device_changed_callback( + name: &'static str, + stm_type: StreamType, + input_device: Option, + output_device: Option, + data: *mut c_void, + state_callback: extern "C" fn(*mut ffi::cubeb_stream, *mut c_void, ffi::cubeb_state), + device_changed_callback: extern "C" fn(*mut c_void), + operation: F, +) where + F: FnOnce(&mut AudioUnitStream), +{ + test_get_stream_with_default_data_callback_by_type( + name, + stm_type, + input_device, + output_device, + state_callback, + data, + |stream| { + assert!(stream + .register_device_changed_callback(Some(device_changed_callback)) + .is_ok()); + operation(stream); + assert!(stream.register_device_changed_callback(None).is_ok()); + }, + ); +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/device_property.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/device_property.rs new file mode 100644 index 0000000000..8277a7642d --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/device_property.rs @@ -0,0 +1,473 @@ +use super::utils::{test_get_default_device, Scope}; +use super::*; + +// get_device_uid +// ------------------------------------ +#[test] +fn test_get_device_uid() { + // Input device. + if let Some(input) = test_get_default_device(Scope::Input) { + let uid = get_device_uid(input, DeviceType::INPUT).unwrap(); + let uid = uid.into_string(); + assert!(!uid.is_empty()); + } + + // Output device. + if let Some(output) = test_get_default_device(Scope::Output) { + let uid = get_device_uid(output, DeviceType::OUTPUT).unwrap(); + let uid = uid.into_string(); + assert!(!uid.is_empty()); + } +} + +#[test] +#[should_panic] +fn test_get_device_uid_by_unknwon_device() { + // Unknown device. + assert!(get_device_uid(kAudioObjectUnknown, DeviceType::INPUT).is_err()); +} + +// get_device_model_uid +// ------------------------------------ +// Some devices (e.g., AirPods) fail to get model uid. +#[test] +fn test_get_device_model_uid() { + if let Some(device) = test_get_default_device(Scope::Input) { + match get_device_model_uid(device, DeviceType::INPUT) { + Ok(uid) => println!("input model uid: {}", uid.into_string()), + Err(e) => println!("No input model uid. Error: {}", e), + } + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + match get_device_model_uid(device, DeviceType::OUTPUT) { + Ok(uid) => println!("output model uid: {}", uid.into_string()), + Err(e) => println!("No output model uid. Error: {}", e), + } + } else { + println!("No output device."); + } +} + +#[test] +#[should_panic] +fn test_get_device_model_uid_by_unknown_device() { + assert!(get_device_model_uid(kAudioObjectUnknown, DeviceType::INPUT).is_err()); +} + +// get_device_transport_type +// ------------------------------------ +#[test] +fn test_get_device_transport_type() { + if let Some(device) = test_get_default_device(Scope::Input) { + match get_device_transport_type(device, DeviceType::INPUT) { + Ok(trans_type) => println!( + "input transport type: {:X}, {:?}", + trans_type, + convert_uint32_into_string(trans_type) + ), + Err(e) => println!("No input transport type. Error: {}", e), + } + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + match get_device_transport_type(device, DeviceType::OUTPUT) { + Ok(trans_type) => println!( + "output transport type: {:X}, {:?}", + trans_type, + convert_uint32_into_string(trans_type) + ), + Err(e) => println!("No output transport type. Error: {}", e), + } + } else { + println!("No output device."); + } +} + +#[test] +#[should_panic] +fn test_get_device_transport_type_by_unknown_device() { + assert!(get_device_transport_type(kAudioObjectUnknown, DeviceType::INPUT).is_err()); +} + +// get_device_source +// ------------------------------------ +// Some USB headsets (e.g., Plantronic .Audio 628) fails to get data source. +#[test] +fn test_get_device_source() { + if let Some(device) = test_get_default_device(Scope::Input) { + match get_device_source(device, DeviceType::INPUT) { + Ok(source) => println!( + "input source: {:X}, {:?}", + source, + convert_uint32_into_string(source) + ), + Err(e) => println!("No input data source. Error: {}", e), + } + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + match get_device_source(device, DeviceType::OUTPUT) { + Ok(source) => println!( + "output source: {:X}, {:?}", + source, + convert_uint32_into_string(source) + ), + Err(e) => println!("No output data source. Error: {}", e), + } + } else { + println!("No output device."); + } +} + +#[test] +#[should_panic] +fn test_get_device_source_by_unknown_device() { + assert!(get_device_source(kAudioObjectUnknown, DeviceType::INPUT).is_err()); +} + +// get_device_source_name +// ------------------------------------ +#[test] +fn test_get_device_source_name() { + if let Some(device) = test_get_default_device(Scope::Input) { + match get_device_source_name(device, DeviceType::INPUT) { + Ok(name) => println!("input: {}", name.into_string()), + Err(e) => println!("No input data source name. Error: {}", e), + } + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + match get_device_source_name(device, DeviceType::OUTPUT) { + Ok(name) => println!("output: {}", name.into_string()), + Err(e) => println!("No output data source name. Error: {}", e), + } + } else { + println!("No output device."); + } +} + +#[test] +#[should_panic] +fn test_get_device_source_name_by_unknown_device() { + assert!(get_device_source_name(kAudioObjectUnknown, DeviceType::INPUT).is_err()); +} + +// get_device_name +// ------------------------------------ +#[test] +fn test_get_device_name() { + if let Some(device) = test_get_default_device(Scope::Input) { + let name = get_device_name(device, DeviceType::INPUT).unwrap(); + println!("input device name: {}", name.into_string()); + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + let name = get_device_name(device, DeviceType::OUTPUT).unwrap(); + println!("output device name: {}", name.into_string()); + } else { + println!("No output device."); + } +} + +#[test] +#[should_panic] +fn test_get_device_name_by_unknown_device() { + assert!(get_device_name(kAudioObjectUnknown, DeviceType::INPUT).is_err()); +} + +// get_device_manufacturer +// ------------------------------------ +#[test] +fn test_get_device_manufacturer() { + if let Some(device) = test_get_default_device(Scope::Input) { + // Some devices like AirPods cannot get the vendor info so we print the error directly. + // TODO: Replace `map` and `unwrap_or_else` by `map_or_else` + let name = get_device_manufacturer(device, DeviceType::INPUT) + .map(|name| name.into_string()) + .unwrap_or_else(|e| format!("Error: {}", e)); + println!("input device vendor: {}", name); + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + // Some devices like AirPods cannot get the vendor info so we print the error directly. + // TODO: Replace `map` and `unwrap_or_else` by `map_or_else` + let name = get_device_manufacturer(device, DeviceType::OUTPUT) + .map(|name| name.into_string()) + .unwrap_or_else(|e| format!("Error: {}", e)); + println!("output device vendor: {}", name); + } else { + println!("No output device."); + } +} + +#[test] +#[should_panic] +fn test_get_device_manufacturer_by_unknown_device() { + assert!(get_device_manufacturer(kAudioObjectUnknown, DeviceType::INPUT).is_err()); +} + +// get_device_buffer_frame_size_range +// ------------------------------------ +#[test] +fn test_get_device_buffer_frame_size_range() { + if let Some(device) = test_get_default_device(Scope::Input) { + let range = get_device_buffer_frame_size_range(device, DeviceType::INPUT).unwrap(); + println!( + "range of input buffer frame size: {}-{}", + range.mMinimum, range.mMaximum + ); + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + let range = get_device_buffer_frame_size_range(device, DeviceType::OUTPUT).unwrap(); + println!( + "range of output buffer frame size: {}-{}", + range.mMinimum, range.mMaximum + ); + } else { + println!("No output device."); + } +} + +#[test] +#[should_panic] +fn test_get_device_buffer_frame_size_range_by_unknown_device() { + assert!(get_device_buffer_frame_size_range(kAudioObjectUnknown, DeviceType::INPUT).is_err()); +} + +// get_device_latency +// ------------------------------------ +#[test] +fn test_get_device_latency() { + if let Some(device) = test_get_default_device(Scope::Input) { + let latency = get_device_latency(device, DeviceType::INPUT).unwrap(); + println!("latency of input device: {}", latency); + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + let latency = get_device_latency(device, DeviceType::OUTPUT).unwrap(); + println!("latency of output device: {}", latency); + } else { + println!("No output device."); + } +} + +#[test] +#[should_panic] +fn test_get_device_latency_by_unknown_device() { + assert!(get_device_latency(kAudioObjectUnknown, DeviceType::INPUT).is_err()); +} + +// get_device_streams +// ------------------------------------ +#[test] +fn test_get_device_streams() { + if let Some(device) = test_get_default_device(Scope::Input) { + let streams = get_device_streams(device, DeviceType::INPUT).unwrap(); + println!("streams on the input device: {:?}", streams); + assert!(!streams.is_empty()); + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + let streams = get_device_streams(device, DeviceType::OUTPUT).unwrap(); + println!("streams on the output device: {:?}", streams); + assert!(!streams.is_empty()); + } else { + println!("No output device."); + } +} + +#[test] +#[should_panic] +fn test_get_device_streams_by_unknown_device() { + assert!(get_device_streams(kAudioObjectUnknown, DeviceType::INPUT).is_err()); +} + +// get_device_sample_rate +// ------------------------------------ +#[test] +fn test_get_device_sample_rate() { + if let Some(device) = test_get_default_device(Scope::Input) { + let rate = get_device_sample_rate(device, DeviceType::INPUT).unwrap(); + println!("input sample rate: {}", rate); + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + let rate = get_device_sample_rate(device, DeviceType::OUTPUT).unwrap(); + println!("output sample rate: {}", rate); + } else { + println!("No output device."); + } +} + +#[test] +#[should_panic] +fn test_get_device_sample_rate_by_unknown_device() { + assert!(get_device_sample_rate(kAudioObjectUnknown, DeviceType::INPUT).is_err()); +} + +// get_ranges_of_device_sample_rate +// ------------------------------------ +#[test] +fn test_get_ranges_of_device_sample_rate() { + if let Some(device) = test_get_default_device(Scope::Input) { + let ranges = get_ranges_of_device_sample_rate(device, DeviceType::INPUT).unwrap(); + println!("ranges of input sample rate: {:?}", ranges); + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + let ranges = get_ranges_of_device_sample_rate(device, DeviceType::OUTPUT).unwrap(); + println!("ranges of output sample rate: {:?}", ranges); + } else { + println!("No output device."); + } +} + +#[test] +#[should_panic] +fn test_get_ranges_of_device_sample_rate_by_unknown_device() { + assert!(get_ranges_of_device_sample_rate(kAudioObjectUnknown, DeviceType::INPUT).is_err()); +} + +// get_stream_latency +// ------------------------------------ +#[test] +fn test_get_stream_latency() { + if let Some(device) = test_get_default_device(Scope::Input) { + let streams = get_device_streams(device, DeviceType::INPUT).unwrap(); + for stream in streams { + let latency = get_stream_latency(stream).unwrap(); + println!("latency of the input stream {} is {}", stream, latency); + } + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + let streams = get_device_streams(device, DeviceType::OUTPUT).unwrap(); + for stream in streams { + let latency = get_stream_latency(stream).unwrap(); + println!("latency of the output stream {} is {}", stream, latency); + } + } else { + println!("No output device."); + } +} + +#[test] +#[should_panic] +fn test_get_stream_latency_by_unknown_device() { + assert!(get_stream_latency(kAudioObjectUnknown).is_err()); +} + +// get_stream_virtual_format +// ------------------------------------ +#[test] +fn test_get_stream_virtual_format() { + if let Some(device) = test_get_default_device(Scope::Input) { + let streams = get_device_streams(device, DeviceType::INPUT).unwrap(); + let formats = streams + .iter() + .map(|s| get_stream_virtual_format(*s)) + .collect::>>(); + println!("input stream formats: {:?}", formats); + assert!(!formats.is_empty()); + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + let streams = get_device_streams(device, DeviceType::OUTPUT).unwrap(); + let formats = streams + .iter() + .map(|s| get_stream_virtual_format(*s)) + .collect::>>(); + println!("output stream formats: {:?}", formats); + assert!(!formats.is_empty()); + } else { + println!("No output device."); + } +} + +#[test] +#[should_panic] +fn test_get_stream_virtual_format_by_unknown_stream() { + assert!(get_stream_virtual_format(kAudioObjectUnknown).is_err()); +} + +// get_stream_terminal_type +// ------------------------------------ + +#[test] +fn test_get_stream_terminal_type() { + fn terminal_type_to_device_type(terminal_type: u32) -> Option { + #[allow(non_upper_case_globals)] + match terminal_type { + kAudioStreamTerminalTypeMicrophone + | kAudioStreamTerminalTypeHeadsetMicrophone + | kAudioStreamTerminalTypeReceiverMicrophone => Some(DeviceType::INPUT), + kAudioStreamTerminalTypeSpeaker + | kAudioStreamTerminalTypeHeadphones + | kAudioStreamTerminalTypeLFESpeaker + | kAudioStreamTerminalTypeReceiverSpeaker => Some(DeviceType::OUTPUT), + t if t > INPUT_UNDEFINED && t < OUTPUT_UNDEFINED => Some(DeviceType::INPUT), + t if t > OUTPUT_UNDEFINED && t < BIDIRECTIONAL_UNDEFINED => Some(DeviceType::OUTPUT), + t => { + println!("UNKNOWN TerminalType {:#06x}", t); + None + } + } + } + if let Some(device) = test_get_default_device(Scope::Input) { + let streams = get_device_streams(device, DeviceType::INPUT).unwrap(); + for stream in streams { + assert_eq!( + terminal_type_to_device_type(get_stream_terminal_type(stream).unwrap()), + Some(DeviceType::INPUT) + ); + } + } else { + println!("No input device."); + } + + if let Some(device) = test_get_default_device(Scope::Output) { + let streams = get_device_streams(device, DeviceType::OUTPUT).unwrap(); + for stream in streams { + assert_eq!( + terminal_type_to_device_type(get_stream_terminal_type(stream).unwrap()), + Some(DeviceType::OUTPUT) + ); + } + } else { + println!("No output device."); + } +} + +#[test] +#[should_panic] +fn test_get_stream_terminal_type_by_unknown_stream() { + assert!(get_stream_terminal_type(kAudioObjectUnknown).is_err()); +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/interfaces.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/interfaces.rs new file mode 100644 index 0000000000..340fec002d --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/interfaces.rs @@ -0,0 +1,1215 @@ +extern crate itertools; + +use self::itertools::iproduct; +use super::utils::{ + get_devices_info_in_scope, noop_data_callback, test_device_channels_in_scope, + test_get_default_device, test_ops_context_operation, test_ops_stream_operation, Scope, +}; +use super::*; + +// Context Operations +// ------------------------------------------------------------------------------------------------ +#[test] +fn test_ops_context_init_and_destroy() { + test_ops_context_operation("context: init and destroy", |_context_ptr| {}); +} + +#[test] +fn test_ops_context_backend_id() { + test_ops_context_operation("context: backend id", |context_ptr| { + let backend = unsafe { + let ptr = OPS.get_backend_id.unwrap()(context_ptr); + CStr::from_ptr(ptr).to_string_lossy().into_owned() + }; + assert_eq!(backend, "audiounit-rust"); + }); +} + +#[test] +fn test_ops_context_max_channel_count() { + test_ops_context_operation("context: max channel count", |context_ptr| { + let output_exists = test_get_default_device(Scope::Output).is_some(); + let mut max_channel_count = 0; + let r = unsafe { OPS.get_max_channel_count.unwrap()(context_ptr, &mut max_channel_count) }; + if output_exists { + assert_eq!(r, ffi::CUBEB_OK); + assert_ne!(max_channel_count, 0); + } else { + assert_eq!(r, ffi::CUBEB_ERROR); + assert_eq!(max_channel_count, 0); + } + }); +} + +#[test] +fn test_ops_context_min_latency() { + test_ops_context_operation("context: min latency", |context_ptr| { + let output_exists = test_get_default_device(Scope::Output).is_some(); + let params = ffi::cubeb_stream_params::default(); + let mut latency = u32::max_value(); + let r = unsafe { OPS.get_min_latency.unwrap()(context_ptr, params, &mut latency) }; + if output_exists { + assert_eq!(r, ffi::CUBEB_OK); + assert!(latency >= SAFE_MIN_LATENCY_FRAMES); + assert!(SAFE_MAX_LATENCY_FRAMES >= latency); + } else { + assert_eq!(r, ffi::CUBEB_ERROR); + assert_eq!(latency, u32::max_value()); + } + }); +} + +#[test] +fn test_ops_context_preferred_sample_rate() { + test_ops_context_operation("context: preferred sample rate", |context_ptr| { + let output_exists = test_get_default_device(Scope::Output).is_some(); + let mut rate = u32::max_value(); + let r = unsafe { OPS.get_preferred_sample_rate.unwrap()(context_ptr, &mut rate) }; + if output_exists { + assert_eq!(r, ffi::CUBEB_OK); + assert_ne!(rate, u32::max_value()); + assert_ne!(rate, 0); + } else { + assert_eq!(r, ffi::CUBEB_ERROR); + assert_eq!(rate, u32::max_value()); + } + }); +} + +#[test] +fn test_ops_context_supported_input_processing_params() { + test_ops_context_operation( + "context: supported input processing params", + |context_ptr| { + let mut params = ffi::CUBEB_INPUT_PROCESSING_PARAM_NONE; + let r = unsafe { + OPS.get_supported_input_processing_params.unwrap()(context_ptr, &mut params) + }; + assert_eq!(r, ffi::CUBEB_OK); + assert_eq!( + params, + ffi::CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION + | ffi::CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION + | ffi::CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL + ); + }, + ); +} + +#[test] +fn test_ops_context_enumerate_devices_unknown() { + test_ops_context_operation("context: enumerate devices (unknown)", |context_ptr| { + let mut coll = ffi::cubeb_device_collection { + device: ptr::null_mut(), + count: 0, + }; + assert_eq!( + unsafe { + OPS.enumerate_devices.unwrap()( + context_ptr, + ffi::CUBEB_DEVICE_TYPE_UNKNOWN, + &mut coll, + ) + }, + ffi::CUBEB_OK + ); + assert_eq!(coll.count, 0); + assert_eq!(coll.device, ptr::null_mut()); + assert_eq!( + unsafe { OPS.device_collection_destroy.unwrap()(context_ptr, &mut coll) }, + ffi::CUBEB_OK + ); + assert_eq!(coll.count, 0); + assert_eq!(coll.device, ptr::null_mut()); + }); +} + +#[test] +fn test_ops_context_enumerate_devices_input() { + test_ops_context_operation("context: enumerate devices (input)", |context_ptr| { + let having_input = test_get_default_device(Scope::Input).is_some(); + let mut coll = ffi::cubeb_device_collection { + device: ptr::null_mut(), + count: 0, + }; + assert_eq!( + unsafe { + OPS.enumerate_devices.unwrap()(context_ptr, ffi::CUBEB_DEVICE_TYPE_INPUT, &mut coll) + }, + ffi::CUBEB_OK + ); + if having_input { + assert_ne!(coll.count, 0); + assert_ne!(coll.device, ptr::null_mut()); + } else { + assert_eq!(coll.count, 0); + assert_eq!(coll.device, ptr::null_mut()); + } + assert_eq!( + unsafe { OPS.device_collection_destroy.unwrap()(context_ptr, &mut coll) }, + ffi::CUBEB_OK + ); + assert_eq!(coll.count, 0); + assert_eq!(coll.device, ptr::null_mut()); + }); +} + +#[test] +fn test_ops_context_enumerate_devices_output() { + test_ops_context_operation("context: enumerate devices (output)", |context_ptr| { + let output_exists = test_get_default_device(Scope::Output).is_some(); + let mut coll = ffi::cubeb_device_collection { + device: ptr::null_mut(), + count: 0, + }; + assert_eq!( + unsafe { + OPS.enumerate_devices.unwrap()( + context_ptr, + ffi::CUBEB_DEVICE_TYPE_OUTPUT, + &mut coll, + ) + }, + ffi::CUBEB_OK + ); + if output_exists { + assert_ne!(coll.count, 0); + assert_ne!(coll.device, ptr::null_mut()); + } else { + assert_eq!(coll.count, 0); + assert_eq!(coll.device, ptr::null_mut()); + } + assert_eq!( + unsafe { OPS.device_collection_destroy.unwrap()(context_ptr, &mut coll) }, + ffi::CUBEB_OK + ); + assert_eq!(coll.count, 0); + assert_eq!(coll.device, ptr::null_mut()); + }); +} + +#[test] +fn test_ops_context_device_collection_destroy() { + // Destroy a dummy device collection, without calling enumerate_devices to allocate memory for the device collection + test_ops_context_operation("context: device collection destroy", |context_ptr| { + let mut coll = ffi::cubeb_device_collection { + device: ptr::null_mut(), + count: 0, + }; + assert_eq!( + unsafe { OPS.device_collection_destroy.unwrap()(context_ptr, &mut coll) }, + ffi::CUBEB_OK + ); + assert_eq!(coll.device, ptr::null_mut()); + assert_eq!(coll.count, 0); + }); +} + +#[test] +fn test_ops_context_register_device_collection_changed_unknown() { + test_ops_context_operation( + "context: register device collection changed (unknown)", + |context_ptr| { + assert_eq!( + unsafe { + OPS.register_device_collection_changed.unwrap()( + context_ptr, + ffi::CUBEB_DEVICE_TYPE_UNKNOWN, + None, + ptr::null_mut(), + ) + }, + ffi::CUBEB_ERROR_INVALID_PARAMETER + ); + }, + ); +} + +#[test] +fn test_ops_context_register_device_collection_changed_twice_input() { + test_ops_context_register_device_collection_changed_twice(ffi::CUBEB_DEVICE_TYPE_INPUT); +} + +#[test] +fn test_ops_context_register_device_collection_changed_twice_output() { + test_ops_context_register_device_collection_changed_twice(ffi::CUBEB_DEVICE_TYPE_OUTPUT); +} + +#[test] +fn test_ops_context_register_device_collection_changed_twice_inout() { + test_ops_context_register_device_collection_changed_twice( + ffi::CUBEB_DEVICE_TYPE_INPUT | ffi::CUBEB_DEVICE_TYPE_OUTPUT, + ); +} + +fn test_ops_context_register_device_collection_changed_twice(devtype: u32) { + extern "C" fn callback(_: *mut ffi::cubeb, _: *mut c_void) {} + let label_input: &'static str = "context: register device collection changed twice (input)"; + let label_output: &'static str = "context: register device collection changed twice (output)"; + let label_inout: &'static str = "context: register device collection changed twice (inout)"; + let label = if devtype == ffi::CUBEB_DEVICE_TYPE_INPUT { + label_input + } else if devtype == ffi::CUBEB_DEVICE_TYPE_OUTPUT { + label_output + } else if devtype == ffi::CUBEB_DEVICE_TYPE_INPUT | ffi::CUBEB_DEVICE_TYPE_OUTPUT { + label_inout + } else { + return; + }; + + test_ops_context_operation(label, |context_ptr| { + // Register a callback within the defined scope. + assert_eq!( + unsafe { + OPS.register_device_collection_changed.unwrap()( + context_ptr, + devtype, + Some(callback), + ptr::null_mut(), + ) + }, + ffi::CUBEB_OK + ); + + assert_eq!( + unsafe { + OPS.register_device_collection_changed.unwrap()( + context_ptr, + devtype, + Some(callback), + ptr::null_mut(), + ) + }, + ffi::CUBEB_ERROR_INVALID_PARAMETER + ); + // Unregister + assert_eq!( + unsafe { + OPS.register_device_collection_changed.unwrap()( + context_ptr, + devtype, + None, + ptr::null_mut(), + ) + }, + ffi::CUBEB_OK + ); + }); +} + +#[test] +fn test_ops_context_register_device_collection_changed() { + extern "C" fn callback(_: *mut ffi::cubeb, _: *mut c_void) {} + test_ops_context_operation( + "context: register device collection changed", + |context_ptr| { + let devtypes: [ffi::cubeb_device_type; 3] = [ + ffi::CUBEB_DEVICE_TYPE_INPUT, + ffi::CUBEB_DEVICE_TYPE_OUTPUT, + ffi::CUBEB_DEVICE_TYPE_INPUT | ffi::CUBEB_DEVICE_TYPE_OUTPUT, + ]; + + for devtype in &devtypes { + // Register a callback in the defined scoped. + assert_eq!( + unsafe { + OPS.register_device_collection_changed.unwrap()( + context_ptr, + *devtype, + Some(callback), + ptr::null_mut(), + ) + }, + ffi::CUBEB_OK + ); + + // Unregister all callbacks regardless of the scope. + assert_eq!( + unsafe { + OPS.register_device_collection_changed.unwrap()( + context_ptr, + ffi::CUBEB_DEVICE_TYPE_INPUT | ffi::CUBEB_DEVICE_TYPE_OUTPUT, + None, + ptr::null_mut(), + ) + }, + ffi::CUBEB_OK + ); + + // Register callback in the defined scoped again. + assert_eq!( + unsafe { + OPS.register_device_collection_changed.unwrap()( + context_ptr, + *devtype, + Some(callback), + ptr::null_mut(), + ) + }, + ffi::CUBEB_OK + ); + + // Unregister callback within the defined scope. + assert_eq!( + unsafe { + OPS.register_device_collection_changed.unwrap()( + context_ptr, + *devtype, + None, + ptr::null_mut(), + ) + }, + ffi::CUBEB_OK + ); + } + }, + ); +} + +#[test] +fn test_ops_context_register_device_collection_changed_with_a_duplex_stream() { + use std::thread; + use std::time::Duration; + + extern "C" fn callback(_: *mut ffi::cubeb, got_called_ptr: *mut c_void) { + let got_called = unsafe { &mut *(got_called_ptr as *mut bool) }; + *got_called = true; + } + + test_ops_context_operation( + "context: register device collection changed and create a duplex stream", + |context_ptr| { + let got_called = Box::new(false); + let got_called_ptr = Box::into_raw(got_called); + + // Register a callback monitoring both input and output device collection. + assert_eq!( + unsafe { + OPS.register_device_collection_changed.unwrap()( + context_ptr, + ffi::CUBEB_DEVICE_TYPE_INPUT | ffi::CUBEB_DEVICE_TYPE_OUTPUT, + Some(callback), + got_called_ptr as *mut c_void, + ) + }, + ffi::CUBEB_OK + ); + + // The aggregate device is very likely to be created in the system + // when creating a duplex stream. We need to make sure it won't trigger + // the callback. + test_default_duplex_stream_operation("duplex stream", |_stream| { + // Do nothing but wait for device-collection change. + thread::sleep(Duration::from_millis(200)); + }); + + // Unregister the callback. + assert_eq!( + unsafe { + OPS.register_device_collection_changed.unwrap()( + context_ptr, + ffi::CUBEB_DEVICE_TYPE_INPUT | ffi::CUBEB_DEVICE_TYPE_OUTPUT, + None, + got_called_ptr as *mut c_void, + ) + }, + ffi::CUBEB_OK + ); + + let got_called = unsafe { Box::from_raw(got_called_ptr) }; + assert!(!got_called.as_ref()); + }, + ); +} + +#[test] +#[ignore] +fn test_ops_context_register_device_collection_changed_manual() { + test_ops_context_operation( + "(manual) context: register device collection changed", + |context_ptr| { + println!("context @ {:p}", context_ptr); + + struct Data { + context: *mut ffi::cubeb, + touched: u32, // TODO: Use AtomicU32 instead + } + + extern "C" fn input_callback(context: *mut ffi::cubeb, user: *mut c_void) { + println!("input > context @ {:p}", context); + let data = unsafe { &mut (*(user as *mut Data)) }; + assert_eq!(context, data.context); + data.touched += 1; + } + + extern "C" fn output_callback(context: *mut ffi::cubeb, user: *mut c_void) { + println!("output > context @ {:p}", context); + let data = unsafe { &mut (*(user as *mut Data)) }; + assert_eq!(context, data.context); + data.touched += 1; + } + + let mut data = Data { + context: context_ptr, + touched: 0, + }; + + // Register a callback for input scope. + assert_eq!( + unsafe { + OPS.register_device_collection_changed.unwrap()( + context_ptr, + ffi::CUBEB_DEVICE_TYPE_INPUT, + Some(input_callback), + &mut data as *mut Data as *mut c_void, + ) + }, + ffi::CUBEB_OK + ); + + // Register a callback for output scope. + assert_eq!( + unsafe { + OPS.register_device_collection_changed.unwrap()( + context_ptr, + ffi::CUBEB_DEVICE_TYPE_OUTPUT, + Some(output_callback), + &mut data as *mut Data as *mut c_void, + ) + }, + ffi::CUBEB_OK + ); + + while data.touched < 2 {} + }, + ); +} + +#[test] +fn test_ops_context_stream_init_no_stream_params() { + let name = "context: stream_init with no stream params"; + test_ops_context_operation(name, |context_ptr| { + 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(), + ptr::null_mut(), // Use default input device. + ptr::null_mut(), // No input parameters. + ptr::null_mut(), // Use default output device. + ptr::null_mut(), // No output parameters. + 4096, // TODO: Get latency by get_min_latency instead ? + Some(noop_data_callback), + None, // No state callback. + ptr::null_mut(), // No user data pointer. + ) + }, + ffi::CUBEB_ERROR_INVALID_PARAMETER + ); + assert!(stream.is_null()); + }); +} + +#[test] +fn test_ops_context_stream_init_no_input_stream_params() { + let name = "context: stream_init with no input stream params"; + let input_device = test_get_default_device(Scope::Input); + if input_device.is_none() { + println!("No input device to perform input tests for \"{}\".", name); + return; + } + test_ops_context_operation(name, |context_ptr| { + 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.unwrap() as ffi::cubeb_devid, + ptr::null_mut(), // No input parameters. + ptr::null_mut(), // Use default output device. + ptr::null_mut(), // No output parameters. + 4096, // TODO: Get latency by get_min_latency instead ? + Some(noop_data_callback), + None, // No state callback. + ptr::null_mut(), // No user data pointer. + ) + }, + ffi::CUBEB_ERROR_INVALID_PARAMETER + ); + assert!(stream.is_null()); + }); +} + +#[test] +fn test_ops_context_stream_init_no_output_stream_params() { + let name = "context: stream_init with no output stream params"; + let output_device = test_get_default_device(Scope::Output); + if output_device.is_none() { + println!("No output device to perform output tests for \"{}\".", name); + return; + } + test_ops_context_operation(name, |context_ptr| { + 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(), + ptr::null_mut(), // Use default input device. + ptr::null_mut(), // No input parameters. + output_device.unwrap() as ffi::cubeb_devid, + ptr::null_mut(), // No output parameters. + 4096, // TODO: Get latency by get_min_latency instead ? + Some(noop_data_callback), + None, // No state callback. + ptr::null_mut(), // No user data pointer. + ) + }, + ffi::CUBEB_ERROR_INVALID_PARAMETER + ); + assert!(stream.is_null()); + }); +} + +#[test] +fn test_ops_context_stream_init_no_data_callback() { + let name = "context: stream_init with no data callback"; + test_ops_context_operation(name, |context_ptr| { + let mut stream: *mut ffi::cubeb_stream = ptr::null_mut(); + let stream_name = CString::new(name).expect("Failed to create stream name"); + + let mut output_params = ffi::cubeb_stream_params::default(); + output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; + output_params.rate = 44100; + output_params.channels = 2; + output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; + + assert_eq!( + unsafe { + OPS.stream_init.unwrap()( + context_ptr, + &mut stream, + stream_name.as_ptr(), + ptr::null_mut(), // Use default input device. + ptr::null_mut(), // No input parameters. + ptr::null_mut(), // Use default output device. + &mut output_params, + 4096, // TODO: Get latency by get_min_latency instead ? + None, // No data callback. + None, // No state callback. + ptr::null_mut(), // No user data pointer. + ) + }, + ffi::CUBEB_ERROR_INVALID_PARAMETER + ); + assert!(stream.is_null()); + }); +} + +#[test] +fn test_ops_context_stream_init_channel_rate_combinations() { + let name = "context: stream_init with various channels and rates"; + test_ops_context_operation(name, |context_ptr| { + let mut stream: *mut ffi::cubeb_stream = ptr::null_mut(); + let stream_name = CString::new(name).expect("Failed to create stream name"); + + const MAX_NUM_CHANNELS: u32 = 32; + let channel_values: Vec = vec![1, 2, 3, 4, 6]; + let freq_values: Vec = vec![16000, 24000, 44100, 48000]; + let is_float_values: Vec = vec![false, true]; + + for (channels, freq, is_float) in iproduct!(channel_values, freq_values, is_float_values) { + assert!(channels < MAX_NUM_CHANNELS); + println!("--------------------------"); + println!( + "Testing {} channel(s), {} Hz, {}\n", + channels, + freq, + if is_float { "float" } else { "short" } + ); + + let mut output_params = ffi::cubeb_stream_params::default(); + output_params.format = if is_float { + ffi::CUBEB_SAMPLE_FLOAT32NE + } else { + ffi::CUBEB_SAMPLE_S16NE + }; + output_params.rate = freq; + output_params.channels = channels; + output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; + + assert_eq!( + unsafe { + OPS.stream_init.unwrap()( + context_ptr, + &mut stream, + stream_name.as_ptr(), + ptr::null_mut(), // Use default input device. + ptr::null_mut(), // No input parameters. + ptr::null_mut(), // Use default output device. + &mut output_params, + 4096, // TODO: Get latency by get_min_latency instead ? + Some(noop_data_callback), // No data callback. + None, // No state callback. + ptr::null_mut(), // No user data pointer. + ) + }, + ffi::CUBEB_OK + ); + assert!(!stream.is_null()); + } + }); +} + +// Stream Operations +// ------------------------------------------------------------------------------------------------ +fn test_default_output_stream_operation(name: &'static str, operation: F) +where + F: FnOnce(*mut ffi::cubeb_stream), +{ + // Make sure the parameters meet the requirements of AudioUnitContext::stream_init + // (in the comments). + let mut output_params = ffi::cubeb_stream_params::default(); + output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; + output_params.rate = 44100; + output_params.channels = 2; + output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; + + test_ops_stream_operation( + name, + ptr::null_mut(), // Use default input device. + ptr::null_mut(), // No input parameters. + ptr::null_mut(), // Use default output device. + &mut output_params, + 4096, // TODO: Get latency by get_min_latency instead ? + Some(noop_data_callback), + None, // No state callback. + ptr::null_mut(), // No user data pointer. + operation, + ); +} + +fn test_default_duplex_stream_operation(name: &'static str, operation: F) +where + F: FnOnce(*mut ffi::cubeb_stream), +{ + // Make sure the parameters meet the requirements of AudioUnitContext::stream_init + // (in the comments). + let mut input_params = ffi::cubeb_stream_params::default(); + input_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; + input_params.rate = 48000; + input_params.channels = 1; + input_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + input_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; + + let mut output_params = ffi::cubeb_stream_params::default(); + output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; + output_params.rate = 44100; + output_params.channels = 2; + output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; + + test_ops_stream_operation( + name, + ptr::null_mut(), // Use default input device. + &mut input_params, + ptr::null_mut(), // Use default output device. + &mut output_params, + 4096, // TODO: Get latency by get_min_latency instead ? + Some(noop_data_callback), + None, // No state callback. + ptr::null_mut(), // No user data pointer. + operation, + ); +} + +fn test_stereo_input_duplex_stream_operation(name: &'static str, operation: F) +where + F: FnOnce(*mut ffi::cubeb_stream), +{ + let mut input_devices = get_devices_info_in_scope(Scope::Input); + input_devices.retain(|d| test_device_channels_in_scope(d.id, Scope::Input).unwrap_or(0) >= 2); + if input_devices.is_empty() { + println!("No stereo input device present. Skipping stereo-input test."); + return; + } + + let mut input_params = ffi::cubeb_stream_params::default(); + input_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; + input_params.rate = 48000; + input_params.channels = 2; + input_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + input_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; + + let mut output_params = ffi::cubeb_stream_params::default(); + output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; + output_params.rate = 48000; + output_params.channels = 2; + output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; + + test_ops_stream_operation( + name, + input_devices[0].id as ffi::cubeb_devid, + &mut input_params, + ptr::null_mut(), // Use default output device. + &mut output_params, + 4096, // TODO: Get latency by get_min_latency instead ? + Some(noop_data_callback), + None, // No state callback. + ptr::null_mut(), // No user data pointer. + operation, + ); +} + +fn test_default_duplex_voice_stream_operation(name: &'static str, operation: F) +where + F: FnOnce(*mut ffi::cubeb_stream), +{ + // Make sure the parameters meet the requirements of AudioUnitContext::stream_init + // (in the comments). + let mut input_params = ffi::cubeb_stream_params::default(); + input_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; + input_params.rate = 44100; + input_params.channels = 1; + input_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + input_params.prefs = ffi::CUBEB_STREAM_PREF_VOICE; + + let mut output_params = ffi::cubeb_stream_params::default(); + output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; + output_params.rate = 48000; + output_params.channels = 2; + output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + output_params.prefs = ffi::CUBEB_STREAM_PREF_VOICE; + + test_ops_stream_operation( + name, + ptr::null_mut(), // Use default input device. + &mut input_params, + ptr::null_mut(), // Use default output device. + &mut output_params, + 4096, // TODO: Get latency by get_min_latency instead ? + Some(noop_data_callback), + None, // No state callback. + ptr::null_mut(), // No user data pointer. + operation, + ); +} + +fn test_stereo_input_duplex_voice_stream_operation(name: &'static str, operation: F) +where + F: FnOnce(*mut ffi::cubeb_stream), +{ + let mut input_devices = get_devices_info_in_scope(Scope::Input); + input_devices.retain(|d| test_device_channels_in_scope(d.id, Scope::Input).unwrap_or(0) >= 2); + if input_devices.is_empty() { + println!("No stereo input device present. Skipping stereo-input test."); + return; + } + + let mut input_params = ffi::cubeb_stream_params::default(); + input_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; + input_params.rate = 44100; + input_params.channels = 2; + input_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + input_params.prefs = ffi::CUBEB_STREAM_PREF_VOICE; + + let mut output_params = ffi::cubeb_stream_params::default(); + output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; + output_params.rate = 44100; + output_params.channels = 2; + output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + output_params.prefs = ffi::CUBEB_STREAM_PREF_VOICE; + + test_ops_stream_operation( + name, + input_devices[0].id as ffi::cubeb_devid, + &mut input_params, + ptr::null_mut(), // Use default output device. + &mut output_params, + 4096, // TODO: Get latency by get_min_latency instead ? + Some(noop_data_callback), + None, // No state callback. + ptr::null_mut(), // No user data pointer. + operation, + ); +} + +#[test] +fn test_ops_stream_init_and_destroy() { + test_default_output_stream_operation("stream: init and destroy", |_stream| {}); +} + +#[test] +fn test_ops_stream_start() { + test_default_output_stream_operation("stream: start", |stream| { + assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); + }); +} + +#[test] +fn test_ops_stream_stop() { + test_default_output_stream_operation("stream: stop", |stream| { + assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK); + }); +} + +#[test] +fn test_ops_stream_position() { + test_default_output_stream_operation("stream: position", |stream| { + let mut position = u64::max_value(); + assert_eq!( + unsafe { OPS.stream_get_position.unwrap()(stream, &mut position) }, + ffi::CUBEB_OK + ); + assert_eq!(position, 0); + }); +} + +#[test] +fn test_ops_stream_latency() { + test_default_output_stream_operation("stream: latency", |stream| { + let mut latency = u32::max_value(); + assert_eq!( + unsafe { OPS.stream_get_latency.unwrap()(stream, &mut latency) }, + ffi::CUBEB_OK + ); + assert_ne!(latency, u32::max_value()); + }); +} + +#[test] +fn test_ops_stream_set_volume() { + test_default_output_stream_operation("stream: set volume", |stream| { + assert_eq!( + unsafe { OPS.stream_set_volume.unwrap()(stream, 0.5) }, + ffi::CUBEB_OK + ); + }); +} + +#[test] +fn test_ops_stream_current_device() { + test_default_output_stream_operation("stream: get current device and destroy it", |stream| { + if test_get_default_device(Scope::Input).is_none() + || test_get_default_device(Scope::Output).is_none() + { + println!("stream_get_current_device only works when the machine has both input and output devices"); + return; + } + + let mut device: *mut ffi::cubeb_device = ptr::null_mut(); + if unsafe { OPS.stream_get_current_device.unwrap()(stream, &mut device) } != ffi::CUBEB_OK { + // It can happen when we fail to get the device source. + println!("stream_get_current_device fails. Skip this test."); + return; + } + + assert!(!device.is_null()); + // Uncomment the below to print out the results. + // let deviceref = unsafe { DeviceRef::from_ptr(device) }; + // println!( + // "output: {}", + // deviceref.output_name().unwrap_or("(no device name)") + // ); + // println!( + // "input: {}", + // deviceref.input_name().unwrap_or("(no device name)") + // ); + assert_eq!( + unsafe { OPS.stream_device_destroy.unwrap()(stream, device) }, + ffi::CUBEB_OK + ); + }); +} + +#[test] +fn test_ops_stream_device_destroy() { + test_default_output_stream_operation("stream: destroy null device", |stream| { + assert_eq!( + unsafe { OPS.stream_device_destroy.unwrap()(stream, ptr::null_mut()) }, + ffi::CUBEB_OK // It returns OK anyway. + ); + }); +} + +#[test] +fn test_ops_stream_register_device_changed_callback() { + extern "C" fn callback(_: *mut c_void) {} + + test_default_output_stream_operation("stream: register device changed callback", |stream| { + assert_eq!( + unsafe { OPS.stream_register_device_changed_callback.unwrap()(stream, Some(callback)) }, + ffi::CUBEB_OK + ); + assert_eq!( + unsafe { OPS.stream_register_device_changed_callback.unwrap()(stream, Some(callback)) }, + ffi::CUBEB_ERROR_INVALID_PARAMETER + ); + assert_eq!( + unsafe { OPS.stream_register_device_changed_callback.unwrap()(stream, None) }, + ffi::CUBEB_OK + ); + }); +} + +#[test] +fn test_ops_stereo_input_duplex_stream_init_and_destroy() { + test_stereo_input_duplex_stream_operation( + "stereo-input duplex stream: init and destroy", + |_stream| {}, + ); +} + +#[test] +fn test_ops_stereo_input_duplex_stream_start() { + test_stereo_input_duplex_stream_operation("stereo-input duplex stream: start", |stream| { + assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); + }); +} + +#[test] +fn test_ops_stereo_input_duplex_stream_stop() { + test_stereo_input_duplex_stream_operation("stereo-input duplex stream: stop", |stream| { + assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK); + }); +} + +#[test] +fn test_ops_duplex_voice_stream_init_and_destroy() { + test_default_duplex_voice_stream_operation( + "duplex voice stream: init and destroy", + |_stream| {}, + ); +} + +#[test] +fn test_ops_duplex_voice_stream_start() { + test_default_duplex_voice_stream_operation("duplex voice stream: start", |stream| { + assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); + }); +} + +#[test] +fn test_ops_duplex_voice_stream_stop() { + test_default_duplex_voice_stream_operation("duplex voice stream: stop", |stream| { + assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK); + }); +} + +#[test] +fn test_ops_duplex_voice_stream_set_input_mute() { + test_default_duplex_voice_stream_operation("duplex voice stream: mute", |stream| { + assert_eq!( + unsafe { OPS.stream_set_input_mute.unwrap()(stream, 1) }, + ffi::CUBEB_OK + ); + }); +} + +#[test] +fn test_ops_duplex_voice_stream_set_input_mute_before_start() { + test_default_duplex_voice_stream_operation( + "duplex voice stream: mute before start", + |stream| { + assert_eq!( + unsafe { OPS.stream_set_input_mute.unwrap()(stream, 1) }, + ffi::CUBEB_OK + ); + assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); + }, + ); +} + +#[test] +fn test_ops_duplex_voice_stream_set_input_mute_before_start_with_reinit() { + test_default_duplex_voice_stream_operation( + "duplex voice stream: mute before start with reinit", + |stream| { + assert_eq!( + unsafe { OPS.stream_set_input_mute.unwrap()(stream, 1) }, + ffi::CUBEB_OK + ); + assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); + + // Hacky cast, but testing this here was simplest for now. + let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; + stm.reinit_async(); + let queue = stm.queue.clone(); + let mut mute_after_reinit = false; + queue.run_sync(|| { + let mut mute: u32 = 0; + let r = audio_unit_get_property( + stm.core_stream_data.input_unit, + kAUVoiceIOProperty_MuteOutput, + kAudioUnitScope_Global, + AU_IN_BUS, + &mut mute, + &mut mem::size_of::(), + ); + assert_eq!(r, NO_ERR); + mute_after_reinit = mute == 1; + }); + assert_eq!(mute_after_reinit, true); + }, + ); +} + +#[test] +fn test_ops_duplex_voice_stream_set_input_mute_after_start() { + test_default_duplex_voice_stream_operation("duplex voice stream: mute after start", |stream| { + assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); + assert_eq!( + unsafe { OPS.stream_set_input_mute.unwrap()(stream, 1) }, + ffi::CUBEB_OK + ); + }); +} + +#[test] +fn test_ops_duplex_voice_stream_set_input_processing_params() { + test_default_duplex_voice_stream_operation("duplex voice stream: processing", |stream| { + let params: ffi::cubeb_input_processing_params = + ffi::CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION + | ffi::CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION + | ffi::CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL; + assert_eq!( + unsafe { OPS.stream_set_input_processing_params.unwrap()(stream, params) }, + ffi::CUBEB_OK + ); + }); +} + +#[test] +fn test_ops_duplex_voice_stream_set_input_processing_params_before_start() { + test_default_duplex_voice_stream_operation( + "duplex voice stream: processing before start", + |stream| { + let params: ffi::cubeb_input_processing_params = + ffi::CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION + | ffi::CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION + | ffi::CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL; + assert_eq!( + unsafe { OPS.stream_set_input_processing_params.unwrap()(stream, params) }, + ffi::CUBEB_OK + ); + assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); + }, + ); +} + +#[test] +fn test_ops_duplex_voice_stream_set_input_processing_params_before_start_with_reinit() { + test_default_duplex_voice_stream_operation( + "duplex voice stream: processing before start with reinit", + |stream| { + let params: ffi::cubeb_input_processing_params = + ffi::CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION + | ffi::CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION + | ffi::CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL; + assert_eq!( + unsafe { OPS.stream_set_input_processing_params.unwrap()(stream, params) }, + ffi::CUBEB_OK + ); + assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); + + // Hacky cast, but testing this here was simplest for now. + let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; + stm.reinit_async(); + let queue = stm.queue.clone(); + let mut params_after_reinit: ffi::cubeb_input_processing_params = + ffi::CUBEB_INPUT_PROCESSING_PARAM_NONE; + queue.run_sync(|| { + let mut params: ffi::cubeb_input_processing_params = + ffi::CUBEB_INPUT_PROCESSING_PARAM_NONE; + let mut agc: u32 = 0; + let r = audio_unit_get_property( + stm.core_stream_data.input_unit, + kAUVoiceIOProperty_VoiceProcessingEnableAGC, + kAudioUnitScope_Global, + AU_IN_BUS, + &mut agc, + &mut mem::size_of::(), + ); + assert_eq!(r, NO_ERR); + if agc == 1 { + params = params | ffi::CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL; + } + let mut bypass: u32 = 0; + let r = audio_unit_get_property( + stm.core_stream_data.input_unit, + kAUVoiceIOProperty_BypassVoiceProcessing, + kAudioUnitScope_Global, + AU_IN_BUS, + &mut bypass, + &mut mem::size_of::(), + ); + assert_eq!(r, NO_ERR); + if bypass == 0 { + params = params + | ffi::CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION + | ffi::CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION; + } + params_after_reinit = params; + }); + assert_eq!(params, params_after_reinit); + }, + ); +} + +#[test] +fn test_ops_duplex_voice_stream_set_input_processing_params_after_start() { + test_default_duplex_voice_stream_operation( + "duplex voice stream: processing after start", + |stream| { + assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); + let params: ffi::cubeb_input_processing_params = + ffi::CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION + | ffi::CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION + | ffi::CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL; + assert_eq!( + unsafe { OPS.stream_set_input_processing_params.unwrap()(stream, params) }, + ffi::CUBEB_OK + ); + }, + ); +} + +#[test] +fn test_ops_stereo_input_duplex_voice_stream_init_and_destroy() { + test_stereo_input_duplex_voice_stream_operation( + "stereo-input duplex voice stream: init and destroy", + |_stream| {}, + ); +} + +#[test] +fn test_ops_stereo_input_duplex_voice_stream_start() { + test_stereo_input_duplex_voice_stream_operation( + "stereo-input duplex voice stream: start", + |stream| { + assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); + }, + ); +} + +#[test] +fn test_ops_stereo_input_duplex_voice_stream_stop() { + test_stereo_input_duplex_voice_stream_operation( + "stereo-input duplex voice stream: stop", + |stream| { + assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK); + }, + ); +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/manual.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/manual.rs new file mode 100644 index 0000000000..b2b2241cc9 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/manual.rs @@ -0,0 +1,614 @@ +use super::utils::{ + test_get_devices_in_scope, test_ops_context_operation, test_ops_stream_operation, Scope, + StreamType, TestDeviceInfo, TestDeviceSwitcher, +}; +use super::*; +use std::io; +use std::sync::atomic::AtomicBool; + +#[ignore] +#[test] +fn test_switch_output_device() { + use std::f32::consts::PI; + + const SAMPLE_FREQUENCY: u32 = 48_000; + + // Do nothing if there is no 2 available output devices at least. + let devices = test_get_devices_in_scope(Scope::Output); + if devices.len() < 2 { + println!("Need 2 output devices at least."); + return; + } + + let mut output_device_switcher = TestDeviceSwitcher::new(Scope::Output); + + // Make sure the parameters meet the requirements of AudioUnitContext::stream_init + // (in the comments). + let mut output_params = ffi::cubeb_stream_params::default(); + output_params.format = ffi::CUBEB_SAMPLE_S16NE; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = 1; + output_params.layout = ffi::CUBEB_LAYOUT_MONO; + output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; + + // Used to calculate the tone's wave. + let mut position: i64 = 0; // TODO: Use Atomic instead. + + test_ops_stream_operation( + "stream: North American dial tone", + ptr::null_mut(), // Use default input device. + ptr::null_mut(), // No input parameters. + ptr::null_mut(), // Use default output device. + &mut output_params, + 4096, // TODO: Get latency by get_min_latency instead ? + Some(data_callback), + Some(state_callback), + &mut position as *mut i64 as *mut c_void, + |stream| { + assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); + println!("Start playing! Enter 's' to switch device. Enter 'q' to quit."); + loop { + let mut input = String::new(); + let _ = io::stdin().read_line(&mut input); + assert_eq!(input.pop().unwrap(), '\n'); + match input.as_str() { + "s" => { + output_device_switcher.next(); + } + "q" => { + println!("Quit."); + break; + } + x => { + println!("Unknown command: {}", x); + } + } + } + assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK); + }, + ); + + extern "C" fn state_callback( + stream: *mut ffi::cubeb_stream, + user_ptr: *mut c_void, + state: ffi::cubeb_state, + ) { + assert!(!stream.is_null()); + assert!(!user_ptr.is_null()); + assert_ne!(state, ffi::CUBEB_STATE_ERROR); + } + + extern "C" fn 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()); + assert!(!user_ptr.is_null()); + assert!(!output_buffer.is_null()); + + let buffer = unsafe { + let ptr = output_buffer as *mut i16; + let len = nframes as usize; + slice::from_raw_parts_mut(ptr, len) + }; + + let position = unsafe { &mut *(user_ptr as *mut i64) }; + + // Generate tone on the fly. + for data in buffer.iter_mut() { + let t1 = (2.0 * PI * 350.0 * (*position) as f32 / SAMPLE_FREQUENCY as f32).sin(); + let t2 = (2.0 * PI * 440.0 * (*position) as f32 / SAMPLE_FREQUENCY as f32).sin(); + *data = f32_to_i16_sample(0.5 * (t1 + t2)); + *position += 1; + } + + nframes + } + + fn f32_to_i16_sample(x: f32) -> i16 { + (x * f32::from(i16::max_value())) as i16 + } +} + +#[ignore] +#[test] +fn test_device_collection_change() { + const DUMMY_PTR: *mut c_void = 0xDEAD_BEEF as *mut c_void; + let mut context = AudioUnitContext::new(); + println!("Context allocated @ {:p}", &context); + + extern "C" fn input_changed_callback(context: *mut ffi::cubeb, data: *mut c_void) { + println!( + "Input device collection @ {:p} is changed. Data @ {:p}", + context, data + ); + assert_eq!(data, DUMMY_PTR); + } + + extern "C" fn output_changed_callback(context: *mut ffi::cubeb, data: *mut c_void) { + println!( + "output device collection @ {:p} is changed. Data @ {:p}", + context, data + ); + assert_eq!(data, DUMMY_PTR); + } + + context.register_device_collection_changed( + DeviceType::INPUT, + Some(input_changed_callback), + DUMMY_PTR, + ); + + context.register_device_collection_changed( + DeviceType::OUTPUT, + Some(output_changed_callback), + DUMMY_PTR, + ); + + println!("Unplug/Plug device to see the event log.\nEnter anything to finish."); + let mut input = String::new(); + let _ = std::io::stdin().read_line(&mut input); +} + +#[ignore] +#[test] +fn test_stream_tester() { + test_ops_context_operation("context: stream tester", |context_ptr| { + let mut stream_ptr: *mut ffi::cubeb_stream = ptr::null_mut(); + let enable_loopback = AtomicBool::new(false); + loop { + println!( + "commands:\n\ + \t'q': quit\n\ + \t'c': create a stream\n\ + \t'd': destroy a stream\n\ + \t's': start the created stream\n\ + \t't': stop the created stream\n\ + \t'r': register a device changed callback\n\ + \t'l': set loopback (DUPLEX-only)\n\ + \t'v': set volume\n\ + \t'm': set input mute\n\ + \t'p': set input processing" + ); + + let mut command = String::new(); + let _ = io::stdin().read_line(&mut command); + assert_eq!(command.pop().unwrap(), '\n'); + + match command.as_str() { + "q" => { + println!("Quit."); + destroy_stream(&mut stream_ptr); + break; + } + "c" => create_stream(&mut stream_ptr, context_ptr, &enable_loopback), + "d" => destroy_stream(&mut stream_ptr), + "s" => start_stream(stream_ptr), + "t" => stop_stream(stream_ptr), + "r" => register_device_change_callback(stream_ptr), + "l" => set_loopback(stream_ptr, &enable_loopback), + "v" => set_volume(stream_ptr), + "m" => set_input_mute(stream_ptr), + "p" => set_input_processing(stream_ptr), + x => println!("Unknown command: {}", x), + } + } + }); + + fn start_stream(stream_ptr: *mut ffi::cubeb_stream) { + if stream_ptr.is_null() { + println!("No stream can start."); + return; + } + assert_eq!( + unsafe { OPS.stream_start.unwrap()(stream_ptr) }, + ffi::CUBEB_OK + ); + println!("Stream {:p} started.", stream_ptr); + } + + fn stop_stream(stream_ptr: *mut ffi::cubeb_stream) { + if stream_ptr.is_null() { + println!("No stream can stop."); + return; + } + assert_eq!( + unsafe { OPS.stream_stop.unwrap()(stream_ptr) }, + ffi::CUBEB_OK + ); + println!("Stream {:p} stopped.", stream_ptr); + } + + fn set_volume(stream_ptr: *mut ffi::cubeb_stream) { + if stream_ptr.is_null() { + println!("No stream can set volume."); + return; + } + const VOL: f32 = 0.5; + assert_eq!( + unsafe { OPS.stream_set_volume.unwrap()(stream_ptr, VOL) }, + ffi::CUBEB_OK + ); + println!("Set stream {:p} volume to {}", stream_ptr, VOL); + } + + fn set_loopback(stream_ptr: *mut ffi::cubeb_stream, enable_loopback: &AtomicBool) { + if stream_ptr.is_null() { + println!("No stream can set loopback."); + return; + } + let stm = unsafe { &mut *(stream_ptr as *mut AudioUnitStream) }; + if !stm.core_stream_data.has_input() || !stm.core_stream_data.has_output() { + println!("Duplex stream needed to set loopback"); + return; + } + let mut loopback: Option = None; + while loopback.is_none() { + println!("Select action:\n1) Enable loopback, 2) Disable loopback"); + let mut input = String::new(); + let _ = io::stdin().read_line(&mut input); + assert_eq!(input.pop().unwrap(), '\n'); + loopback = match input.as_str() { + "1" => Some(true), + "2" => Some(false), + _ => { + println!("Invalid action. Select again.\n"); + None + } + } + } + let loopback = loopback.unwrap(); + enable_loopback.store(loopback, Ordering::SeqCst); + println!( + "Loopback {} for stream {:p}", + if loopback { "enabled" } else { "disabled" }, + stream_ptr + ); + } + + fn set_input_mute(stream_ptr: *mut ffi::cubeb_stream) { + if stream_ptr.is_null() { + println!("No stream can set input mute."); + return; + } + let stm = unsafe { &mut *(stream_ptr as *mut AudioUnitStream) }; + if !stm.core_stream_data.has_input() { + println!("Input stream needed to set loopback"); + return; + } + let mut mute: Option = None; + while mute.is_none() { + println!("Select action:\n1) Mute, 2) Unmute"); + let mut input = String::new(); + let _ = io::stdin().read_line(&mut input); + assert_eq!(input.pop().unwrap(), '\n'); + mute = match input.as_str() { + "1" => Some(true), + "2" => Some(false), + _ => { + println!("Invalid action. Select again.\n"); + None + } + } + } + let mute = mute.unwrap(); + let res = unsafe { OPS.stream_set_input_mute.unwrap()(stream_ptr, mute.into()) }; + println!( + "{} set stream {:p} input {}", + if res == ffi::CUBEB_OK { + "Successfully" + } else { + "Failed to" + }, + stream_ptr, + if mute { "mute" } else { "unmute" } + ); + } + + fn set_input_processing(stream_ptr: *mut ffi::cubeb_stream) { + if stream_ptr.is_null() { + println!("No stream can set input processing."); + return; + } + let stm = unsafe { &mut *(stream_ptr as *mut AudioUnitStream) }; + if !stm.core_stream_data.using_voice_processing_unit() { + println!("Duplex stream with voice processing needed to set input processing params"); + return; + } + let mut params = InputProcessingParams::NONE; + { + let mut bypass = u32::from(true); + let mut size: usize = mem::size_of::(); + assert_eq!( + audio_unit_get_property( + stm.core_stream_data.input_unit, + kAudioUnitProperty_BypassEffect, + kAudioUnitScope_Global, + AU_IN_BUS, + &mut bypass, + &mut size, + ), + NO_ERR + ); + assert_eq!(size, mem::size_of::()); + if bypass == 0 { + params.set(InputProcessingParams::ECHO_CANCELLATION, true); + params.set(InputProcessingParams::NOISE_SUPPRESSION, true); + } + } + let mut done = false; + while !done { + println!( + "Supported params: {:?}\nCurrent params: {:?}\nSelect action:\n\ + \t1) Set None\n\ + \t2) Toggle Echo Cancellation\n\ + \t3) Toggle Noise Suppression\n\ + \t4) Toggle Automatic Gain Control\n\ + \t5) Toggle Voice Isolation\n\ + \t6) Set All\n\ + \t0) Done", + stm.context.supported_input_processing_params().unwrap(), + params + ); + let mut input = String::new(); + let _ = io::stdin().read_line(&mut input); + assert_eq!(input.pop().unwrap(), '\n'); + match input.as_str() { + "1" => params = InputProcessingParams::NONE, + "2" => params.toggle(InputProcessingParams::ECHO_CANCELLATION), + "3" => params.toggle(InputProcessingParams::NOISE_SUPPRESSION), + "4" => params.toggle(InputProcessingParams::AUTOMATIC_GAIN_CONTROL), + "5" => params.toggle(InputProcessingParams::VOICE_ISOLATION), + "6" => params = InputProcessingParams::all(), + "0" => done = true, + _ => println!("Invalid action. Select again.\n"), + } + } + let res = + unsafe { OPS.stream_set_input_processing_params.unwrap()(stream_ptr, params.bits()) }; + println!( + "{} set stream {:p} input processing params to {:?}", + if res == ffi::CUBEB_OK { + "Successfully" + } else { + "Failed to" + }, + stream_ptr, + params, + ); + } + + fn register_device_change_callback(stream_ptr: *mut ffi::cubeb_stream) { + extern "C" fn callback(user_ptr: *mut c_void) { + println!("user pointer @ {:p}", user_ptr); + assert!(user_ptr.is_null()); + } + + if stream_ptr.is_null() { + println!("No stream for registering the callback."); + return; + } + assert_eq!( + unsafe { + OPS.stream_register_device_changed_callback.unwrap()(stream_ptr, Some(callback)) + }, + ffi::CUBEB_OK + ); + println!("Stream {:p} now has a device change callback.", stream_ptr); + } + + fn destroy_stream(stream_ptr: &mut *mut ffi::cubeb_stream) { + if stream_ptr.is_null() { + println!("No need to destroy stream."); + return; + } + unsafe { + OPS.stream_destroy.unwrap()(*stream_ptr); + } + println!("Stream {:p} destroyed.", *stream_ptr); + *stream_ptr = ptr::null_mut(); + } + + fn create_stream( + stream_ptr: &mut *mut ffi::cubeb_stream, + context_ptr: *mut ffi::cubeb, + enable_loopback: &AtomicBool, + ) { + if !stream_ptr.is_null() { + println!("Stream has been created."); + return; + } + + let mut stream_type = StreamType::empty(); + while stream_type.is_empty() { + println!("Select stream type:\n1) Input 2) Output 3) In-Out Duplex 4) Back"); + let mut input = String::new(); + let _ = io::stdin().read_line(&mut input); + assert_eq!(input.pop().unwrap(), '\n'); + stream_type = match input.as_str() { + "1" => StreamType::INPUT, + "2" => StreamType::OUTPUT, + "3" => StreamType::DUPLEX, + "4" => { + println!("Do nothing."); + return; + } + _ => { + println!("Invalid type. Select again.\n"); + StreamType::empty() + } + } + } + + let device_selector = |scope: Scope| -> AudioObjectID { + loop { + println!( + "Select {} device:\n", + if scope == Scope::Input { + "input" + } else { + "output" + } + ); + let mut list = vec![]; + list.push(kAudioObjectUnknown); + println!("{:>4}: System default", 0); + let devices = test_get_devices_in_scope(scope.clone()); + for (idx, device) in devices.iter().enumerate() { + list.push(*device); + let info = TestDeviceInfo::new(*device, scope.clone()); + println!( + "{:>4}: {}\n\tAudioObjectID: {}\n\tuid: {}", + idx + 1, + info.label, + device, + info.uid + ); + } + + let mut input = String::new(); + io::stdin().read_line(&mut input).unwrap(); + let n: usize = match input.trim().parse() { + Err(_) => { + println!("Invalid option. Try again.\n"); + continue; + } + Ok(n) => n, + }; + if n >= list.len() { + println!("Invalid option. Try again.\n"); + continue; + } + return list[n]; + } + }; + + let mut input_params = get_dummy_stream_params(Scope::Input); + let mut output_params = get_dummy_stream_params(Scope::Output); + + let (input_device, input_stream_params) = if stream_type.contains(StreamType::INPUT) { + ( + device_selector(Scope::Input), + &mut input_params as *mut ffi::cubeb_stream_params, + ) + } else { + ( + kAudioObjectUnknown, /* default input device */ + ptr::null_mut(), + ) + }; + + let (output_device, output_stream_params) = if stream_type.contains(StreamType::OUTPUT) { + ( + device_selector(Scope::Output), + &mut output_params as *mut ffi::cubeb_stream_params, + ) + } else { + ( + kAudioObjectUnknown, /* default output device */ + ptr::null_mut(), + ) + }; + + let stream_name = CString::new("stream tester").unwrap(); + + assert_eq!( + unsafe { + OPS.stream_init.unwrap()( + context_ptr, + stream_ptr, + stream_name.as_ptr(), + input_device as ffi::cubeb_devid, + input_stream_params, + output_device as ffi::cubeb_devid, + output_stream_params, + 4096, // latency + Some(data_callback), + Some(state_callback), + enable_loopback as *const AtomicBool as *mut c_void, // user pointer + ) + }, + ffi::CUBEB_OK + ); + assert!(!stream_ptr.is_null()); + println!("Stream {:p} created.", *stream_ptr); + + extern "C" fn state_callback( + stream: *mut ffi::cubeb_stream, + _user_ptr: *mut c_void, + state: ffi::cubeb_state, + ) { + assert!(!stream.is_null()); + let s = State::from(state); + println!("state: {:?}", s); + } + + extern "C" fn 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()); + + let enable_loopback = unsafe { &mut *(user_ptr as *mut AtomicBool) }; + let loopback = enable_loopback.load(Ordering::SeqCst); + if loopback && !input_buffer.is_null() && !output_buffer.is_null() { + // Dupe the mono input to stereo + let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; + assert_eq!(stm.core_stream_data.input_stream_params.channels(), 1); + let channels = stm.core_stream_data.output_stream_params.channels() as usize; + let sample_size = + cubeb_sample_size(stm.core_stream_data.output_stream_params.format()); + for f in 0..(nframes as usize) { + let input_offset = f * sample_size; + let output_offset = input_offset * channels; + for c in 0..channels { + unsafe { + ptr::copy( + input_buffer.add(input_offset) as *const u8, + output_buffer.add(output_offset + (sample_size * c)) as *mut u8, + sample_size, + ) + }; + } + } + } else if !output_buffer.is_null() { + // Feed silence data to output buffer + 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 + } + + 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_VOICE; + 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 + } + } +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/mod.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/mod.rs new file mode 100644 index 0000000000..0c193d0dc8 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/mod.rs @@ -0,0 +1,12 @@ +use super::*; + +mod aggregate_device; +mod api; +mod backlog; +mod device_change; +mod device_property; +mod interfaces; +mod manual; +mod parallel; +mod tone; +mod utils; diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/parallel.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/parallel.rs new file mode 100644 index 0000000000..16063d0011 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/parallel.rs @@ -0,0 +1,572 @@ +use super::utils::{ + noop_data_callback, test_audiounit_get_buffer_frame_size, test_get_default_audiounit, + test_get_default_device, test_ops_context_operation, PropertyScope, Scope, +}; +use super::*; +use std::thread; + +// Ignore the test by default to avoid overwritting the buffer frame size of the device that is +// currently used by other streams in other tests. +#[ignore] +#[test] +fn test_parallel_ops_init_streams_in_parallel_input() { + const THREADS: u32 = 50; + create_streams_by_ops_in_parallel_with_different_latency( + THREADS, + StreamType::Input, + |streams| { + // All the latency frames should be the same value as the first stream's one, since the + // latency frames cannot be changed if another stream is operating in parallel. + let mut latency_frames = vec![]; + let mut in_buffer_frame_sizes = vec![]; + + for stream in streams { + latency_frames.push(stream.latency_frames); + + assert!(!stream.core_stream_data.input_unit.is_null()); + let in_buffer_frame_size = test_audiounit_get_buffer_frame_size( + stream.core_stream_data.input_unit, + Scope::Input, + PropertyScope::Output, + ) + .unwrap(); + in_buffer_frame_sizes.push(in_buffer_frame_size); + + assert!(stream.core_stream_data.output_unit.is_null()); + } + + // Make sure all the latency frames are same as the first stream's one. + for i in 0..latency_frames.len() - 1 { + assert_eq!(latency_frames[i], latency_frames[i + 1]); + } + + // Make sure all the buffer frame sizes on output scope of the input audiounit are same + // as the defined latency of the first initial stream. + for i in 0..in_buffer_frame_sizes.len() - 1 { + assert_eq!(in_buffer_frame_sizes[i], in_buffer_frame_sizes[i + 1]); + } + }, + ); +} + +// Ignore the test by default to avoid overwritting the buffer frame size of the device that is +// currently used by other streams in other tests. +#[ignore] +#[test] +fn test_parallel_ops_init_streams_in_parallel_output() { + const THREADS: u32 = 50; + create_streams_by_ops_in_parallel_with_different_latency( + THREADS, + StreamType::Output, + |streams| { + // All the latency frames should be the same value as the first stream's one, since the + // latency frames cannot be changed if another stream is operating in parallel. + let mut latency_frames = vec![]; + let mut out_buffer_frame_sizes = vec![]; + + for stream in streams { + latency_frames.push(stream.latency_frames); + + assert!(stream.core_stream_data.input_unit.is_null()); + + assert!(!stream.core_stream_data.output_unit.is_null()); + let out_buffer_frame_size = test_audiounit_get_buffer_frame_size( + stream.core_stream_data.output_unit, + Scope::Output, + PropertyScope::Input, + ) + .unwrap(); + out_buffer_frame_sizes.push(out_buffer_frame_size); + } + + // Make sure all the latency frames are same as the first stream's one. + for i in 0..latency_frames.len() - 1 { + assert_eq!(latency_frames[i], latency_frames[i + 1]); + } + + // Make sure all the buffer frame sizes on input scope of the output audiounit are same + // as the defined latency of the first initial stream. + for i in 0..out_buffer_frame_sizes.len() - 1 { + assert_eq!(out_buffer_frame_sizes[i], out_buffer_frame_sizes[i + 1]); + } + }, + ); +} + +// Ignore the test by default to avoid overwritting the buffer frame size of the device that is +// currently used by other streams in other tests. +#[ignore] +#[test] +fn test_parallel_ops_init_streams_in_parallel_duplex() { + const THREADS: u32 = 50; + create_streams_by_ops_in_parallel_with_different_latency( + THREADS, + StreamType::Duplex, + |streams| { + // All the latency frames should be the same value as the first stream's one, since the + // latency frames cannot be changed if another stream is operating in parallel. + let mut latency_frames = vec![]; + let mut in_buffer_frame_sizes = vec![]; + let mut out_buffer_frame_sizes = vec![]; + + for stream in streams { + latency_frames.push(stream.latency_frames); + + assert!(!stream.core_stream_data.input_unit.is_null()); + let in_buffer_frame_size = test_audiounit_get_buffer_frame_size( + stream.core_stream_data.input_unit, + Scope::Input, + PropertyScope::Output, + ) + .unwrap(); + in_buffer_frame_sizes.push(in_buffer_frame_size); + + assert!(!stream.core_stream_data.output_unit.is_null()); + let out_buffer_frame_size = test_audiounit_get_buffer_frame_size( + stream.core_stream_data.output_unit, + Scope::Output, + PropertyScope::Input, + ) + .unwrap(); + out_buffer_frame_sizes.push(out_buffer_frame_size); + } + + // Make sure all the latency frames are same as the first stream's one. + for i in 0..latency_frames.len() - 1 { + assert_eq!(latency_frames[i], latency_frames[i + 1]); + } + + // Make sure all the buffer frame sizes on output scope of the input audiounit are same + // as the defined latency of the first initial stream. + for i in 0..in_buffer_frame_sizes.len() - 1 { + assert_eq!(in_buffer_frame_sizes[i], in_buffer_frame_sizes[i + 1]); + } + + // Make sure all the buffer frame sizes on input scope of the output audiounit are same + // as the defined latency of the first initial stream. + for i in 0..out_buffer_frame_sizes.len() - 1 { + assert_eq!(out_buffer_frame_sizes[i], out_buffer_frame_sizes[i + 1]); + } + }, + ); +} + +fn create_streams_by_ops_in_parallel_with_different_latency( + amount: u32, + stm_type: StreamType, + callback: F, +) where + F: FnOnce(Vec<&AudioUnitStream>), +{ + let default_input = test_get_default_device(Scope::Input); + let default_output = test_get_default_device(Scope::Output); + + let has_input = stm_type == StreamType::Input || stm_type == StreamType::Duplex; + let has_output = stm_type == StreamType::Output || stm_type == StreamType::Duplex; + + if has_input && default_input.is_none() { + println!("No input device to perform the test."); + return; + } + + if has_output && default_output.is_none() { + println!("No output device to perform the test."); + return; + } + + test_ops_context_operation("context: init and destroy", |context_ptr| { + let context_ptr_value = context_ptr as usize; + + let mut join_handles = vec![]; + for i in 0..amount { + // Make sure the parameters meet the requirements of AudioUnitContext::stream_init + // (in the comments). + let mut input_params = ffi::cubeb_stream_params::default(); + input_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; + input_params.rate = 48_000; + input_params.channels = 1; + input_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + input_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; + + let mut output_params = ffi::cubeb_stream_params::default(); + output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; + output_params.rate = 44100; + output_params.channels = 2; + output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; + + // Latency cannot be changed if another stream is operating in parallel. All the latecy + // should be set to the same latency value of the first stream that is operating in the + // context. + let latency_frames = SAFE_MIN_LATENCY_FRAMES + i; + assert!(latency_frames < SAFE_MAX_LATENCY_FRAMES); + + // Create many streams within the same context. The order of the stream creation + // is random (The order of execution of the spawned threads is random.).assert! + // It's super dangerous to pass `context_ptr_value` across threads and convert it back + // to a pointer. However, it's the cheapest way to make sure the inside mutex works. + let thread_name = format!("stream {} @ context {:?}", i, context_ptr); + join_handles.push( + thread::Builder::new() + .name(thread_name) + .spawn(move || { + let context_ptr = context_ptr_value as *mut ffi::cubeb; + let mut stream: *mut ffi::cubeb_stream = ptr::null_mut(); + let stream_name = CString::new(format!("stream {}", i)).unwrap(); + assert_eq!( + unsafe { + OPS.stream_init.unwrap()( + context_ptr, + &mut stream, + stream_name.as_ptr(), + ptr::null_mut(), // Use default input device. + if has_input { + &mut input_params + } else { + ptr::null_mut() + }, + ptr::null_mut(), // Use default output device. + if has_output { + &mut output_params + } else { + ptr::null_mut() + }, + latency_frames, + Some(noop_data_callback), + None, // No state callback. + ptr::null_mut(), // No user data pointer. + ) + }, + ffi::CUBEB_OK + ); + assert!(!stream.is_null()); + stream as usize + }) + .unwrap(), + ); + } + + let mut streams = vec![]; + // Wait for finishing the tasks on the different threads. + for handle in join_handles { + let stream_ptr_value = handle.join().unwrap(); + let stream = unsafe { Box::from_raw(stream_ptr_value as *mut AudioUnitStream) }; + streams.push(stream); + } + + let stream_refs: Vec<&AudioUnitStream> = streams.iter().map(|stm| stm.as_ref()).collect(); + callback(stream_refs); + }); +} + +// Ignore the test by default to avoid overwritting the buffer frame size of the device that is +// currently used by other streams in other tests. +#[ignore] +#[test] +fn test_parallel_init_streams_in_parallel_input() { + const THREADS: u32 = 10; + create_streams_in_parallel_with_different_latency(THREADS, StreamType::Input, |streams| { + // All the latency frames should be the same value as the first stream's one, since the + // latency frames cannot be changed if another stream is operating in parallel. + let mut latency_frames = vec![]; + let mut in_buffer_frame_sizes = vec![]; + + for stream in streams { + latency_frames.push(stream.latency_frames); + + assert!(!stream.core_stream_data.input_unit.is_null()); + let in_buffer_frame_size = test_audiounit_get_buffer_frame_size( + stream.core_stream_data.input_unit, + Scope::Input, + PropertyScope::Output, + ) + .unwrap(); + in_buffer_frame_sizes.push(in_buffer_frame_size); + + assert!(stream.core_stream_data.output_unit.is_null()); + } + + // Make sure all the latency frames are same as the first stream's one. + for i in 0..latency_frames.len() - 1 { + assert_eq!(latency_frames[i], latency_frames[i + 1]); + } + + // Make sure all the buffer frame sizes on output scope of the input audiounit are same + // as the defined latency of the first initial stream. + for i in 0..in_buffer_frame_sizes.len() - 1 { + assert_eq!(in_buffer_frame_sizes[i], in_buffer_frame_sizes[i + 1]); + } + }); +} + +// Ignore the test by default to avoid overwritting the buffer frame size of the device that is +// currently used by other streams in other tests. +#[ignore] +#[test] +fn test_parallel_init_streams_in_parallel_output() { + const THREADS: u32 = 10; + create_streams_in_parallel_with_different_latency(THREADS, StreamType::Output, |streams| { + // All the latency frames should be the same value as the first stream's one, since the + // latency frames cannot be changed if another stream is operating in parallel. + let mut latency_frames = vec![]; + let mut out_buffer_frame_sizes = vec![]; + + for stream in streams { + latency_frames.push(stream.latency_frames); + + assert!(stream.core_stream_data.input_unit.is_null()); + + assert!(!stream.core_stream_data.output_unit.is_null()); + let out_buffer_frame_size = test_audiounit_get_buffer_frame_size( + stream.core_stream_data.output_unit, + Scope::Output, + PropertyScope::Input, + ) + .unwrap(); + out_buffer_frame_sizes.push(out_buffer_frame_size); + } + + // Make sure all the latency frames are same as the first stream's one. + for i in 0..latency_frames.len() - 1 { + assert_eq!(latency_frames[i], latency_frames[i + 1]); + } + + // Make sure all the buffer frame sizes on input scope of the output audiounit are same + // as the defined latency of the first initial stream. + for i in 0..out_buffer_frame_sizes.len() - 1 { + assert_eq!(out_buffer_frame_sizes[i], out_buffer_frame_sizes[i + 1]); + } + }); +} + +// Ignore the test by default to avoid overwritting the buffer frame size of the device that is +// currently used by other streams in other tests. +#[ignore] +#[test] +fn test_parallel_init_streams_in_parallel_duplex() { + const THREADS: u32 = 10; + create_streams_in_parallel_with_different_latency(THREADS, StreamType::Duplex, |streams| { + // All the latency frames should be the same value as the first stream's one, since the + // latency frames cannot be changed if another stream is operating in parallel. + let mut latency_frames = vec![]; + let mut in_buffer_frame_sizes = vec![]; + let mut out_buffer_frame_sizes = vec![]; + + for stream in streams { + latency_frames.push(stream.latency_frames); + + assert!(!stream.core_stream_data.input_unit.is_null()); + let in_buffer_frame_size = test_audiounit_get_buffer_frame_size( + stream.core_stream_data.input_unit, + Scope::Input, + PropertyScope::Output, + ) + .unwrap(); + in_buffer_frame_sizes.push(in_buffer_frame_size); + + assert!(!stream.core_stream_data.output_unit.is_null()); + let out_buffer_frame_size = test_audiounit_get_buffer_frame_size( + stream.core_stream_data.output_unit, + Scope::Output, + PropertyScope::Input, + ) + .unwrap(); + out_buffer_frame_sizes.push(out_buffer_frame_size); + } + + // Make sure all the latency frames are same as the first stream's one. + for i in 0..latency_frames.len() - 1 { + assert_eq!(latency_frames[i], latency_frames[i + 1]); + } + + // Make sure all the buffer frame sizes on output scope of the input audiounit are same + // as the defined latency of the first initial stream. + for i in 0..in_buffer_frame_sizes.len() - 1 { + assert_eq!(in_buffer_frame_sizes[i], in_buffer_frame_sizes[i + 1]); + } + + // Make sure all the buffer frame sizes on input scope of the output audiounit are same + // as the defined latency of the first initial stream. + for i in 0..out_buffer_frame_sizes.len() - 1 { + assert_eq!(out_buffer_frame_sizes[i], out_buffer_frame_sizes[i + 1]); + } + }); +} + +fn create_streams_in_parallel_with_different_latency( + amount: u32, + stm_type: StreamType, + callback: F, +) where + F: FnOnce(Vec<&AudioUnitStream>), +{ + let default_input = test_get_default_device(Scope::Input); + let default_output = test_get_default_device(Scope::Output); + + let has_input = stm_type == StreamType::Input || stm_type == StreamType::Duplex; + let has_output = stm_type == StreamType::Output || stm_type == StreamType::Duplex; + + if has_input && default_input.is_none() { + println!("No input device to perform the test."); + return; + } + + if has_output && default_output.is_none() { + println!("No output device to perform the test."); + return; + } + + let mut context = AudioUnitContext::new(); + + let context_ptr_value = &mut context as *mut AudioUnitContext as usize; + + let mut join_handles = vec![]; + for i in 0..amount { + // Make sure the parameters meet the requirements of AudioUnitContext::stream_init + // (in the comments). + let mut input_params = ffi::cubeb_stream_params::default(); + input_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; + input_params.rate = 48_000; + input_params.channels = 1; + input_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + input_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; + + let mut output_params = ffi::cubeb_stream_params::default(); + output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; + output_params.rate = 44100; + output_params.channels = 2; + output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; + + // Latency cannot be changed if another stream is operating in parallel. All the latecy + // should be set to the same latency value of the first stream that is operating in the + // context. + let latency_frames = SAFE_MIN_LATENCY_FRAMES + i; + assert!(latency_frames < SAFE_MAX_LATENCY_FRAMES); + + // Create many streams within the same context. The order of the stream creation + // is random. (The order of execution of the spawned threads is random.) + // It's super dangerous to pass `context_ptr_value` across threads and convert it back + // to a reference. However, it's the cheapest way to make sure the inside mutex works. + let thread_name = format!("stream {} @ context {:?}", i, context_ptr_value); + join_handles.push( + thread::Builder::new() + .name(thread_name) + .spawn(move || { + let context = unsafe { &mut *(context_ptr_value as *mut AudioUnitContext) }; + let input_params = unsafe { StreamParamsRef::from_ptr(&mut input_params) }; + let output_params = unsafe { StreamParamsRef::from_ptr(&mut output_params) }; + let stream = context + .stream_init( + None, + ptr::null_mut(), // Use default input device. + if has_input { Some(input_params) } else { None }, + ptr::null_mut(), // Use default output device. + if has_output { + Some(output_params) + } else { + None + }, + latency_frames, + Some(noop_data_callback), + None, // No state callback. + ptr::null_mut(), // No user data pointer. + ) + .unwrap(); + assert!(!stream.as_ptr().is_null()); + let stream_ptr_value = stream.as_ptr() as usize; + // Prevent the stream from being destroyed by leaking this stream. + mem::forget(stream); + stream_ptr_value + }) + .unwrap(), + ); + } + + let mut streams = vec![]; + // Wait for finishing the tasks on the different threads. + for handle in join_handles { + let stream_ptr_value = handle.join().unwrap(); + // Retake the leaked stream. + let stream = unsafe { Box::from_raw(stream_ptr_value as *mut AudioUnitStream) }; + streams.push(stream); + } + + let stream_refs: Vec<&AudioUnitStream> = streams.iter().map(|stm| stm.as_ref()).collect(); + callback(stream_refs); +} + +#[derive(Debug, PartialEq)] +enum StreamType { + Input, + Output, + Duplex, +} + +// This is used to interfere other active streams. +// From this testing, it's ok to set the buffer frame size of a device that is currently used by +// other tests. It works on OSX 10.13, not sure if it works on other versions. +// However, other tests may check the buffer frame size they set at the same time, +// so we ignore this by default incase those checks fail. +#[ignore] +#[test] +fn test_set_buffer_frame_size_in_parallel() { + test_set_buffer_frame_size_in_parallel_in_scope(Scope::Input); + test_set_buffer_frame_size_in_parallel_in_scope(Scope::Output); +} + +fn test_set_buffer_frame_size_in_parallel_in_scope(scope: Scope) { + const THREADS: u32 = 100; + + let unit = test_get_default_audiounit(scope.clone()); + if unit.is_none() { + println!("No unit for {:?}", scope); + return; + } + + let (unit_scope, unit_element, prop_scope) = match scope { + Scope::Input => (kAudioUnitScope_Output, AU_IN_BUS, PropertyScope::Output), + Scope::Output => (kAudioUnitScope_Input, AU_OUT_BUS, PropertyScope::Input), + }; + + let mut units = vec![]; + let mut join_handles = vec![]; + for i in 0..THREADS { + let latency_frames = SAFE_MIN_LATENCY_FRAMES + i; + assert!(latency_frames < SAFE_MAX_LATENCY_FRAMES); + units.push(test_get_default_audiounit(scope.clone()).unwrap()); + let unit_value = units.last().unwrap().get_inner() as usize; + join_handles.push(thread::spawn(move || { + let status = audio_unit_set_property( + unit_value as AudioUnit, + kAudioDevicePropertyBufferFrameSize, + unit_scope, + unit_element, + &latency_frames, + mem::size_of::(), + ); + (latency_frames, status) + })); + } + + let mut latencies = vec![]; + let mut statuses = vec![]; + for handle in join_handles { + let (latency, status) = handle.join().unwrap(); + latencies.push(latency); + statuses.push(status); + } + + let mut buffer_frames_list = vec![]; + for unit in units.iter() { + buffer_frames_list.push(unit.get_buffer_frame_size(scope.clone(), prop_scope.clone())); + } + + for status in statuses { + assert_eq!(status, NO_ERR); + } + + for i in 0..buffer_frames_list.len() - 1 { + assert_eq!(buffer_frames_list[i], buffer_frames_list[i + 1]); + } +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/tone.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/tone.rs new file mode 100644 index 0000000000..42cb9ee997 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/tone.rs @@ -0,0 +1,215 @@ +use super::utils::{test_get_default_device, test_ops_stream_operation, Scope}; +use super::*; +use std::sync::atomic::{AtomicI64, Ordering}; + +#[test] +fn test_dial_tone() { + use std::f32::consts::PI; + use std::thread; + use std::time::Duration; + + const SAMPLE_FREQUENCY: u32 = 48_000; + + // Do nothing if there is no available output device. + if test_get_default_device(Scope::Output).is_none() { + println!("No output device."); + return; + } + + // Make sure the parameters meet the requirements of AudioUnitContext::stream_init + // (in the comments). + let mut output_params = ffi::cubeb_stream_params::default(); + output_params.format = ffi::CUBEB_SAMPLE_S16NE; + output_params.rate = SAMPLE_FREQUENCY; + output_params.channels = 1; + output_params.layout = ffi::CUBEB_LAYOUT_MONO; + output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; + + struct Closure { + buffer_size: AtomicI64, + phase: i64, + } + let mut closure = Closure { + buffer_size: AtomicI64::new(0), + phase: 0, + }; + let closure_ptr = &mut closure as *mut Closure as *mut c_void; + + test_ops_stream_operation( + "stream: North American dial tone", + ptr::null_mut(), // Use default input device. + ptr::null_mut(), // No input parameters. + ptr::null_mut(), // Use default output device. + &mut output_params, + 4096, // TODO: Get latency by get_min_latency instead ? + Some(data_callback), + Some(state_callback), + closure_ptr, + |stream| { + assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); + + #[derive(Debug)] + enum State { + WaitingForStart, + PositionIncreasing, + Paused, + Resumed, + End, + } + let mut state = State::WaitingForStart; + let mut position: u64 = 0; + let mut prev_position: u64 = 0; + let mut count = 0; + const CHECK_COUNT: i32 = 10; + loop { + thread::sleep(Duration::from_millis(50)); + assert_eq!( + unsafe { OPS.stream_get_position.unwrap()(stream, &mut position) }, + ffi::CUBEB_OK + ); + println!( + "State: {:?}, position: {}, previous position: {}", + state, position, prev_position + ); + match &mut state { + State::WaitingForStart => { + // It's expected to have 0 for a few iterations here: the stream can take + // some time to start. + if position != prev_position { + assert!(position > prev_position); + prev_position = position; + state = State::PositionIncreasing; + } + } + State::PositionIncreasing => { + // wait a few iterations, check monotony + if position != prev_position { + assert!(position > prev_position); + prev_position = position; + count += 1; + if count > CHECK_COUNT { + state = State::Paused; + count = 0; + assert_eq!( + unsafe { OPS.stream_stop.unwrap()(stream) }, + ffi::CUBEB_OK + ); + // Update the position once paused. + assert_eq!( + unsafe { + OPS.stream_get_position.unwrap()(stream, &mut position) + }, + ffi::CUBEB_OK + ); + prev_position = position; + } + } + } + State::Paused => { + // The cubeb_stream_stop call above should synchrously stop the callbacks, + // hence the clock, the assert below must always holds, modulo the client + // side interpolation. + assert!( + position == prev_position + || position - prev_position + <= closure.buffer_size.load(Ordering::SeqCst) as u64 + ); + count += 1; + prev_position = position; + if count > CHECK_COUNT { + state = State::Resumed; + count = 0; + assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); + } + } + State::Resumed => { + // wait a few iterations, this can take some time to start + if position != prev_position { + assert!(position > prev_position); + prev_position = position; + count += 1; + if count > CHECK_COUNT { + state = State::End; + count = 0; + assert_eq!( + unsafe { OPS.stream_stop.unwrap()(stream) }, + ffi::CUBEB_OK + ); + assert_eq!( + unsafe { + OPS.stream_get_position.unwrap()(stream, &mut position) + }, + ffi::CUBEB_OK + ); + prev_position = position; + } + } + } + State::End => { + // The cubeb_stream_stop call above should synchrously stop the callbacks, + // hence the clock, the assert below must always holds, modulo the client + // side interpolation. + assert!( + position == prev_position + || position - prev_position + <= closure.buffer_size.load(Ordering::SeqCst) as u64 + ); + if position == prev_position { + count += 1; + if count > CHECK_COUNT { + break; + } + } + } + } + } + assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK); + }, + ); + + extern "C" fn state_callback( + stream: *mut ffi::cubeb_stream, + user_ptr: *mut c_void, + state: ffi::cubeb_state, + ) { + assert!(!stream.is_null()); + assert!(!user_ptr.is_null()); + assert_ne!(state, ffi::CUBEB_STATE_ERROR); + } + + extern "C" fn 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()); + assert!(!user_ptr.is_null()); + assert!(!output_buffer.is_null()); + + let buffer = unsafe { + let ptr = output_buffer as *mut i16; + let len = nframes as usize; + slice::from_raw_parts_mut(ptr, len) + }; + + let closure = unsafe { &mut *(user_ptr as *mut Closure) }; + + closure.buffer_size.store(nframes, Ordering::SeqCst); + + // Generate tone on the fly. + for data in buffer.iter_mut() { + let t1 = (2.0 * PI * 350.0 * (closure.phase) as f32 / SAMPLE_FREQUENCY as f32).sin(); + let t2 = (2.0 * PI * 440.0 * (closure.phase) as f32 / SAMPLE_FREQUENCY as f32).sin(); + *data = f32_to_i16_sample(0.5 * (t1 + t2)); + closure.phase += 1; + } + + nframes + } + + fn f32_to_i16_sample(x: f32) -> i16 { + (x * f32::from(i16::max_value())) as i16 + } +} 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 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 { + 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::(); + 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 { + 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 { + 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::() 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 { + 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::() as u32, + ) + }; + if status == NO_ERR { + Ok(()) + } else { + Err(status) + } +} + +pub enum DeviceFilter { + ExcludeCubebAggregateAndVPIO, + IncludeAll, +} +pub fn test_get_all_devices(filter: DeviceFilter) -> Vec { + 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::(), 0); + let elements = size / mem::size_of::(); + 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 { + 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 { + 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 { + 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::(); + 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 { + assert_ne!(id, kAudioObjectUnknown); + + let address = AudioObjectPropertyAddress { + mSelector: kAudioObjectPropertyOwnedObjects, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + let qualifier_data_size = mem::size_of::(); + 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::(); + let mut devices: Vec = 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::(); + 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 { + let address = AudioObjectPropertyAddress { + mSelector: kAudioSubDevicePropertyDriftCompensation, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + let mut size = mem::size_of::(); + 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::(); + 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 { + 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::(); + 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 { + 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::(); + 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, + current_device_index: usize, +} + +impl TestDeviceSwitcher { + pub fn new(scope: Scope) -> Self { + let infos = get_devices_info_in_scope(scope.clone()); + let devices: Vec = 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 { + test_set_default_device(device, self.scope.clone()) + } +} + +pub fn test_create_device_change_listener(scope: Scope, listener: F) -> TestPropertyListener +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 +where + F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus, +{ + device: AudioObjectID, + property: AudioObjectPropertyAddress, + callback: F, +} + +impl TestPropertyListener +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 Drop for TestPropertyListener +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 { + 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 { + 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 { + 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::() as u32, + mOutputData: &mut plugin_id as *mut AudioObjectID as *mut c_void, + mOutputDataSize: mem::size_of::() 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 { + 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(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::(); + 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( + 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(operation: F) +where + F: FnOnce(&mut AudioUnitContext), +{ + let mut context = AudioUnitContext::new(); + operation(&mut context); +} + +pub fn test_get_default_raw_stream(operation: F) +where + F: FnOnce(&mut AudioUnitStream), +{ + test_get_raw_stream(ptr::null_mut(), None, None, 0, operation); +} + +fn test_get_raw_stream( + 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( + name: &'static str, + stm_type: StreamType, + input_device: Option, + output_device: Option, + 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( + 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, + ); +} -- cgit v1.2.3