summaryrefslogtreecommitdiffstats
path: root/third_party/rust/cubeb-coreaudio/src/backend/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /third_party/rust/cubeb-coreaudio/src/backend/tests
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/cubeb-coreaudio/src/backend/tests')
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/aggregate_device.rs400
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/api.rs1663
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/backlog.rs36
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/device_change.rs885
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/device_property.rs473
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/interfaces.rs1215
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/manual.rs614
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/mod.rs12
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/parallel.rs572
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/tone.rs215
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/utils.rs1247
11 files changed, 7332 insertions, 0 deletions
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<AudioObjectID>) -> Vec<String> {
+ 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<AudioObjectID>) -> Vec<u32> {
+ 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::<AudioChannelLayout>()
+ + (channels - 1) * mem::size_of::<AudioChannelDescription>();
+ 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::<AudioChannelLayout>();
+ let padding_size = 10;
+ assert_ne!(mem::size_of::<AudioChannelDescription>(), 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<u32>) {
+ let guard = context.latency_controller.lock().unwrap();
+ assert_eq!(guard.latency, latency);
+}
+
+// make_silent
+// ------------------------------------
+#[test]
+fn test_make_silent() {
+ let mut array = allocate_array::<u32>(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::<u32>()) 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(&params).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::<u32, String>::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::<u32, String>::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<std::result::Result<ffi::cubeb_device_info, Error>> {
+ 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::<AudioObjectID>());
+ 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<AudioObjectID> = input_devices.iter().cloned().collect();
+ let output: HashSet<AudioObjectID> = output_devices.iter().cloned().collect();
+ let union: HashSet<AudioObjectID> = input.union(&output).cloned().collect();
+ let mut union_devices: Vec<AudioObjectID> = 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<DeviceType, extern "C" fn(*mut ffi::cubeb, *mut c_void)> = 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<DeviceType, extern "C" fn(*mut ffi::cubeb, *mut c_void)> = 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<DeviceType, extern "C" fn(*mut ffi::cubeb, *mut c_void)> = 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<F>(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<Counts>;
+
+ 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<Counts>) };
+ {
+ 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<Counts>) };
+ {
+ 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<u32>;
+
+ 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<u32>) };
+ 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<ffi::cubeb_state>,
+ }
+
+ 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<Data>;
+
+ 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(&notifier);
+ 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<Data>) };
+ 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<Data>) };
+ let mut data_guard = notifier.lock().unwrap();
+ data_guard.changed_count += 1;
+ notifier.notify(data_guard);
+ }
+}
+
+struct Notifier<T> {
+ value: Mutex<T>,
+ cvar: Condvar,
+}
+
+impl<T> Notifier<T> {
+ fn new(value: T) -> Self {
+ Self {
+ value: Mutex::new(value),
+ cvar: Condvar::new(),
+ }
+ }
+
+ fn lock(&self) -> LockResult<MutexGuard<'_, T>> {
+ self.value.lock()
+ }
+
+ fn notify(&self, _guard: MutexGuard<'_, T>) {
+ self.cvar.notify_all();
+ }
+}
+
+struct Watcher<T: Clone + PartialEq> {
+ notifier: Arc<Notifier<T>>,
+}
+
+impl<T: Clone + PartialEq> Watcher<T> {
+ fn new(value: &Arc<Notifier<T>>) -> Self {
+ Self {
+ notifier: Arc::clone(value),
+ }
+ }
+
+ fn lock(&self) -> LockResult<MutexGuard<'_, T>> {
+ self.notifier.lock()
+ }
+
+ fn wait_while<'a, F>(
+ &self,
+ guard: MutexGuard<'a, T>,
+ condition: F,
+ ) -> LockResult<MutexGuard<'a, T>>
+ 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<F>(
+ name: &'static str,
+ stm_type: StreamType,
+ input_device: Option<AudioObjectID>,
+ output_device: Option<AudioObjectID>,
+ 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::<Vec<std::result::Result<AudioStreamBasicDescription, OSStatus>>>();
+ 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::<Vec<std::result::Result<AudioStreamBasicDescription, OSStatus>>>();
+ 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<DeviceType> {
+ #[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<u32> = vec![1, 2, 3, 4, 6];
+ let freq_values: Vec<u32> = vec![16000, 24000, 44100, 48000];
+ let is_float_values: Vec<bool> = 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<F>(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<F>(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<F>(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<F>(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<F>(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::<u32>(),
+ );
+ 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::<u32>(),
+ );
+ 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::<u32>(),
+ );
+ 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<bool> = 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<bool> = 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::<u32>();
+ 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::<u32>());
+ 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<F>(
+ 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<F>(
+ 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::<u32>(),
+ );
+ (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<Scope> for DeviceType {
+ fn from(scope: Scope) -> Self {
+ match scope {
+ Scope::Input => DeviceType::INPUT,
+ Scope::Output => DeviceType::OUTPUT,
+ }
+ }
+}
+
+#[derive(Clone)]
+pub enum PropertyScope {
+ Input,
+ Output,
+}
+
+pub fn test_get_default_device(scope: Scope) -> Option<AudioObjectID> {
+ let address = AudioObjectPropertyAddress {
+ mSelector: match scope {
+ Scope::Input => kAudioHardwarePropertyDefaultInputDevice,
+ Scope::Output => kAudioHardwarePropertyDefaultOutputDevice,
+ },
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let mut devid: AudioObjectID = kAudioObjectUnknown;
+ let mut size = mem::size_of::<AudioObjectID>();
+ let status = unsafe {
+ AudioObjectGetPropertyData(
+ kAudioObjectSystemObject,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut UInt32,
+ &mut devid as *mut AudioObjectID as *mut c_void,
+ )
+ };
+ if status != NO_ERR || devid == kAudioObjectUnknown {
+ return None;
+ }
+ Some(devid)
+}
+
+// TODO: Create a GetProperty trait and add a default implementation for it, then implement it
+// for TestAudioUnit so the member method like `get_buffer_frame_size` can reuse the trait
+// method get_property_data.
+#[derive(Debug)]
+pub struct TestAudioUnit(AudioUnit);
+
+impl TestAudioUnit {
+ fn new(unit: AudioUnit) -> Self {
+ assert!(!unit.is_null());
+ Self(unit)
+ }
+ pub fn get_inner(&self) -> AudioUnit {
+ self.0
+ }
+ pub fn get_buffer_frame_size(
+ &self,
+ scope: Scope,
+ prop_scope: PropertyScope,
+ ) -> std::result::Result<u32, OSStatus> {
+ test_audiounit_get_buffer_frame_size(self.0, scope, prop_scope)
+ }
+}
+
+impl Drop for TestAudioUnit {
+ fn drop(&mut self) {
+ unsafe {
+ AudioUnitUninitialize(self.0);
+ AudioComponentInstanceDispose(self.0);
+ }
+ }
+}
+
+// TODO: 1. Return Result with custom errors.
+// 2. Allow to create a in-out unit.
+pub fn test_get_default_audiounit(scope: Scope) -> Option<TestAudioUnit> {
+ let device = test_get_default_device(scope.clone());
+ let unit = test_create_audiounit(ComponentSubType::HALOutput);
+ if device.is_none() || unit.is_none() {
+ return None;
+ }
+ let unit = unit.unwrap();
+ let device = device.unwrap();
+ match scope {
+ Scope::Input => {
+ if test_enable_audiounit_in_scope(unit.get_inner(), Scope::Input, true).is_err()
+ || test_enable_audiounit_in_scope(unit.get_inner(), Scope::Output, false).is_err()
+ {
+ return None;
+ }
+ }
+ Scope::Output => {
+ if test_enable_audiounit_in_scope(unit.get_inner(), Scope::Input, false).is_err()
+ || test_enable_audiounit_in_scope(unit.get_inner(), Scope::Output, true).is_err()
+ {
+ return None;
+ }
+ }
+ }
+
+ let status = unsafe {
+ AudioUnitSetProperty(
+ unit.get_inner(),
+ kAudioOutputUnitProperty_CurrentDevice,
+ kAudioUnitScope_Global,
+ 0, // Global bus
+ &device as *const AudioObjectID as *const c_void,
+ mem::size_of::<AudioObjectID>() as u32,
+ )
+ };
+ if status == NO_ERR {
+ Some(unit)
+ } else {
+ None
+ }
+}
+
+pub enum ComponentSubType {
+ HALOutput,
+ DefaultOutput,
+}
+
+// TODO: Return Result with custom errors.
+// Surprisingly the AudioUnit can be created even when there is no any device on the platform,
+// no matter its subtype is HALOutput or DefaultOutput.
+pub fn test_create_audiounit(unit_type: ComponentSubType) -> Option<TestAudioUnit> {
+ let desc = AudioComponentDescription {
+ componentType: kAudioUnitType_Output,
+ componentSubType: match unit_type {
+ ComponentSubType::HALOutput => kAudioUnitSubType_HALOutput,
+ ComponentSubType::DefaultOutput => kAudioUnitSubType_DefaultOutput,
+ },
+ componentManufacturer: kAudioUnitManufacturer_Apple,
+ componentFlags: 0,
+ componentFlagsMask: 0,
+ };
+ let comp = unsafe { AudioComponentFindNext(ptr::null_mut(), &desc) };
+ if comp.is_null() {
+ return None;
+ }
+ let mut unit: AudioUnit = ptr::null_mut();
+ let status = unsafe { AudioComponentInstanceNew(comp, &mut unit) };
+ // TODO: Is unit possible to be null when no error returns ?
+ if status != NO_ERR || unit.is_null() {
+ None
+ } else {
+ Some(TestAudioUnit::new(unit))
+ }
+}
+
+fn test_enable_audiounit_in_scope(
+ unit: AudioUnit,
+ scope: Scope,
+ enable: bool,
+) -> std::result::Result<(), OSStatus> {
+ assert!(!unit.is_null());
+ let (scope, element) = match scope {
+ Scope::Input => (kAudioUnitScope_Input, AU_IN_BUS),
+ Scope::Output => (kAudioUnitScope_Output, AU_OUT_BUS),
+ };
+ let on_off: u32 = if enable { 1 } else { 0 };
+ let status = unsafe {
+ AudioUnitSetProperty(
+ unit,
+ kAudioOutputUnitProperty_EnableIO,
+ scope,
+ element,
+ &on_off as *const u32 as *const c_void,
+ mem::size_of::<u32>() as u32,
+ )
+ };
+ if status == NO_ERR {
+ Ok(())
+ } else {
+ Err(status)
+ }
+}
+
+pub enum DeviceFilter {
+ ExcludeCubebAggregateAndVPIO,
+ IncludeAll,
+}
+pub fn test_get_all_devices(filter: DeviceFilter) -> Vec<AudioObjectID> {
+ let mut devices = Vec::new();
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioHardwarePropertyDevices,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+ let mut size: usize = 0;
+ let status = unsafe {
+ AudioObjectGetPropertyDataSize(
+ kAudioObjectSystemObject,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ )
+ };
+ // size will be 0 if there is no device at all.
+ if status != NO_ERR || size == 0 {
+ return devices;
+ }
+ assert_eq!(size % mem::size_of::<AudioObjectID>(), 0);
+ let elements = size / mem::size_of::<AudioObjectID>();
+ devices.resize(elements, kAudioObjectUnknown);
+ let status = unsafe {
+ AudioObjectGetPropertyData(
+ kAudioObjectSystemObject,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ devices.as_mut_ptr() as *mut c_void,
+ )
+ };
+ if status != NO_ERR {
+ devices.clear();
+ return devices;
+ }
+ for device in devices.iter() {
+ assert_ne!(*device, kAudioObjectUnknown);
+ }
+
+ match filter {
+ DeviceFilter::ExcludeCubebAggregateAndVPIO => {
+ devices.retain(|&device| {
+ if let Ok(uid) = get_device_global_uid(device) {
+ let uid = uid.into_string();
+ !uid.contains(PRIVATE_AGGREGATE_DEVICE_NAME)
+ && !uid.contains(VOICEPROCESSING_AGGREGATE_DEVICE_NAME)
+ } else {
+ true
+ }
+ });
+ }
+ _ => {}
+ }
+
+ devices
+}
+
+pub fn test_get_devices_in_scope(scope: Scope) -> Vec<AudioObjectID> {
+ let mut devices = test_get_all_devices(DeviceFilter::ExcludeCubebAggregateAndVPIO);
+ devices.retain(|device| test_device_in_scope(*device, scope.clone()));
+ devices
+}
+
+pub fn get_devices_info_in_scope(scope: Scope) -> Vec<TestDeviceInfo> {
+ fn print_info(info: &TestDeviceInfo) {
+ println!("{:>4}: {}\n\tuid: {}", info.id, info.label, info.uid);
+ }
+
+ println!(
+ "\n{:?} devices\n\
+ --------------------",
+ scope
+ );
+
+ let mut infos = vec![];
+ let devices = test_get_devices_in_scope(scope.clone());
+ for device in devices {
+ infos.push(TestDeviceInfo::new(device, scope.clone()));
+ print_info(infos.last().unwrap());
+ }
+ println!();
+
+ infos
+}
+
+#[derive(Debug)]
+pub struct TestDeviceInfo {
+ pub id: AudioObjectID,
+ pub label: String,
+ pub uid: String,
+}
+impl TestDeviceInfo {
+ pub fn new(id: AudioObjectID, scope: Scope) -> Self {
+ Self {
+ id,
+ label: Self::get_label(id, scope.clone()),
+ uid: Self::get_uid(id, scope),
+ }
+ }
+
+ fn get_label(id: AudioObjectID, scope: Scope) -> String {
+ match get_device_uid(id, scope.into()) {
+ Ok(uid) => uid.into_string(),
+ Err(status) => format!("Unknow. Error: {}", status).to_string(),
+ }
+ }
+
+ fn get_uid(id: AudioObjectID, scope: Scope) -> String {
+ match get_device_label(id, scope.into()) {
+ Ok(label) => label.into_string(),
+ Err(status) => format!("Unknown. Error: {}", status).to_string(),
+ }
+ }
+}
+
+pub fn test_device_channels_in_scope(
+ id: AudioObjectID,
+ scope: Scope,
+) -> std::result::Result<u32, OSStatus> {
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioDevicePropertyStreamConfiguration,
+ mScope: match scope {
+ Scope::Input => kAudioDevicePropertyScopeInput,
+ Scope::Output => kAudioDevicePropertyScopeOutput,
+ },
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+ let mut size: usize = 0;
+ let status = unsafe {
+ AudioObjectGetPropertyDataSize(
+ id,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ )
+ };
+ if status != NO_ERR {
+ return Err(status);
+ }
+ if size == 0 {
+ return Ok(0);
+ }
+ let byte_len = size / mem::size_of::<u8>();
+ let mut bytes = vec![0u8; byte_len];
+ let status = unsafe {
+ AudioObjectGetPropertyData(
+ id,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ bytes.as_mut_ptr() as *mut c_void,
+ )
+ };
+ if status != NO_ERR {
+ return Err(status);
+ }
+ let buf_list = unsafe { &*(bytes.as_mut_ptr() as *mut AudioBufferList) };
+ let buf_len = buf_list.mNumberBuffers as usize;
+ if buf_len == 0 {
+ return Ok(0);
+ }
+ let buf_ptr = buf_list.mBuffers.as_ptr() as *const AudioBuffer;
+ let buffers = unsafe { slice::from_raw_parts(buf_ptr, buf_len) };
+ let mut channels: u32 = 0;
+ for buffer in buffers {
+ channels += buffer.mNumberChannels;
+ }
+ Ok(channels)
+}
+
+pub fn test_device_in_scope(id: AudioObjectID, scope: Scope) -> bool {
+ let channels = test_device_channels_in_scope(id, scope);
+ channels.is_ok() && channels.unwrap() > 0
+}
+
+pub fn test_get_all_onwed_devices(id: AudioDeviceID) -> Vec<AudioObjectID> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioObjectPropertyOwnedObjects,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let qualifier_data_size = mem::size_of::<AudioObjectID>();
+ let class_id: AudioClassID = kAudioSubDeviceClassID;
+ let qualifier_data = &class_id;
+ let mut size: usize = 0;
+
+ unsafe {
+ assert_eq!(
+ AudioObjectGetPropertyDataSize(
+ id,
+ &address,
+ qualifier_data_size as u32,
+ qualifier_data as *const u32 as *const c_void,
+ &mut size as *mut usize as *mut u32
+ ),
+ NO_ERR
+ );
+ }
+ assert_ne!(size, 0);
+
+ let elements = size / mem::size_of::<AudioObjectID>();
+ let mut devices: Vec<AudioObjectID> = allocate_array(elements);
+
+ unsafe {
+ assert_eq!(
+ AudioObjectGetPropertyData(
+ id,
+ &address,
+ qualifier_data_size as u32,
+ qualifier_data as *const u32 as *const c_void,
+ &mut size as *mut usize as *mut u32,
+ devices.as_mut_ptr() as *mut c_void
+ ),
+ NO_ERR
+ );
+ }
+
+ devices
+}
+
+pub fn test_get_master_device(id: AudioObjectID) -> String {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioAggregateDevicePropertyMasterSubDevice,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let mut master: CFStringRef = ptr::null_mut();
+ let mut size = mem::size_of::<CFStringRef>();
+ assert_eq!(
+ audio_object_get_property_data(id, &address, &mut size, &mut master),
+ NO_ERR
+ );
+ assert!(!master.is_null());
+
+ let master = StringRef::new(master as _);
+ master.into_string()
+}
+
+pub fn test_get_drift_compensations(id: AudioObjectID) -> std::result::Result<u32, OSStatus> {
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioSubDevicePropertyDriftCompensation,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+ let mut size = mem::size_of::<u32>();
+ let mut compensation = u32::max_value();
+ let status = unsafe {
+ AudioObjectGetPropertyData(
+ id,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ &mut compensation as *mut u32 as *mut c_void,
+ )
+ };
+ if status == NO_ERR {
+ Ok(compensation)
+ } else {
+ Err(status)
+ }
+}
+
+pub fn test_audiounit_scope_is_enabled(unit: AudioUnit, scope: Scope) -> bool {
+ assert!(!unit.is_null());
+ let mut has_io: UInt32 = 0;
+ let (scope, element) = match scope {
+ Scope::Input => (kAudioUnitScope_Input, AU_IN_BUS),
+ Scope::Output => (kAudioUnitScope_Output, AU_OUT_BUS),
+ };
+ let mut size = mem::size_of::<UInt32>();
+ assert_eq!(
+ audio_unit_get_property(
+ unit,
+ kAudioOutputUnitProperty_HasIO,
+ scope,
+ element,
+ &mut has_io,
+ &mut size
+ ),
+ NO_ERR
+ );
+ has_io != 0
+}
+
+pub fn test_audiounit_get_buffer_frame_size(
+ unit: AudioUnit,
+ scope: Scope,
+ prop_scope: PropertyScope,
+) -> std::result::Result<u32, OSStatus> {
+ let element = match scope {
+ Scope::Input => AU_IN_BUS,
+ Scope::Output => AU_OUT_BUS,
+ };
+ let prop_scope = match prop_scope {
+ PropertyScope::Input => kAudioUnitScope_Input,
+ PropertyScope::Output => kAudioUnitScope_Output,
+ };
+ let mut buffer_frames: u32 = 0;
+ let mut size = mem::size_of::<u32>();
+ let status = unsafe {
+ AudioUnitGetProperty(
+ unit,
+ kAudioDevicePropertyBufferFrameSize,
+ prop_scope,
+ element,
+ &mut buffer_frames as *mut u32 as *mut c_void,
+ &mut size as *mut usize as *mut u32,
+ )
+ };
+ if status == NO_ERR {
+ Ok(buffer_frames)
+ } else {
+ Err(status)
+ }
+}
+
+// Surprisingly it's ok to set
+// 1. a unknown device
+// 2. a non-input/non-output device
+// 3. the current default input/output device
+// as the new default input/output device by apple's API. We need to check the above things by ourselves.
+// This function returns an Ok containing the previous default device id on success.
+// Otherwise, it returns an Err containing the error code with OSStatus type
+pub fn test_set_default_device(
+ device: AudioObjectID,
+ scope: Scope,
+) -> std::result::Result<AudioObjectID, OSStatus> {
+ assert!(test_device_in_scope(device, scope.clone()));
+ let default = test_get_default_device(scope.clone()).unwrap();
+ if default == device {
+ // Do nothing if device is already the default device
+ return Ok(device);
+ }
+
+ let address = AudioObjectPropertyAddress {
+ mSelector: match scope {
+ Scope::Input => kAudioHardwarePropertyDefaultInputDevice,
+ Scope::Output => kAudioHardwarePropertyDefaultOutputDevice,
+ },
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+ let size = mem::size_of::<AudioObjectID>();
+ let status = unsafe {
+ AudioObjectSetPropertyData(
+ kAudioObjectSystemObject,
+ &address,
+ 0,
+ ptr::null(),
+ size as u32,
+ &device as *const AudioObjectID as *const c_void,
+ )
+ };
+ let new_default = test_get_default_device(scope.clone()).unwrap();
+ if new_default == default {
+ Err(-1)
+ } else if status == NO_ERR {
+ Ok(default)
+ } else {
+ Err(status)
+ }
+}
+
+pub struct TestDeviceSwitcher {
+ scope: Scope,
+ devices: Vec<AudioObjectID>,
+ current_device_index: usize,
+}
+
+impl TestDeviceSwitcher {
+ pub fn new(scope: Scope) -> Self {
+ let infos = get_devices_info_in_scope(scope.clone());
+ let devices: Vec<AudioObjectID> = infos.into_iter().map(|info| info.id).collect();
+ let current = test_get_default_device(scope.clone()).unwrap();
+ let index = devices
+ .iter()
+ .position(|device| *device == current)
+ .unwrap();
+ Self {
+ scope,
+ devices,
+ current_device_index: index,
+ }
+ }
+
+ pub fn next(&mut self) {
+ let current = self.devices[self.current_device_index];
+ let next_index = (self.current_device_index + 1) % self.devices.len();
+ let next = self.devices[next_index];
+ println!(
+ "Switch device for {:?}: {} -> {}",
+ self.scope, current, next
+ );
+ match self.set_device(next) {
+ Ok(prev) => {
+ assert_eq!(prev, current);
+ self.current_device_index = next_index;
+ }
+ _ => {
+ self.devices.remove(next_index);
+ if next_index < self.current_device_index {
+ self.current_device_index -= 1;
+ }
+ self.next();
+ }
+ }
+ }
+
+ fn set_device(&self, device: AudioObjectID) -> std::result::Result<AudioObjectID, OSStatus> {
+ test_set_default_device(device, self.scope.clone())
+ }
+}
+
+pub fn test_create_device_change_listener<F>(scope: Scope, listener: F) -> TestPropertyListener<F>
+where
+ F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus,
+{
+ let address = AudioObjectPropertyAddress {
+ mSelector: match scope {
+ Scope::Input => kAudioHardwarePropertyDefaultInputDevice,
+ Scope::Output => kAudioHardwarePropertyDefaultOutputDevice,
+ },
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+ TestPropertyListener::new(kAudioObjectSystemObject, address, listener)
+}
+
+pub struct TestPropertyListener<F>
+where
+ F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus,
+{
+ device: AudioObjectID,
+ property: AudioObjectPropertyAddress,
+ callback: F,
+}
+
+impl<F> TestPropertyListener<F>
+where
+ F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus,
+{
+ pub fn new(device: AudioObjectID, property: AudioObjectPropertyAddress, callback: F) -> Self {
+ Self {
+ device,
+ property,
+ callback,
+ }
+ }
+
+ pub fn start(&self) -> std::result::Result<(), OSStatus> {
+ let status = unsafe {
+ AudioObjectAddPropertyListener(
+ self.device,
+ &self.property,
+ Some(Self::render),
+ self as *const Self as *mut c_void,
+ )
+ };
+ if status == NO_ERR {
+ Ok(())
+ } else {
+ Err(status)
+ }
+ }
+
+ pub fn stop(&self) -> std::result::Result<(), OSStatus> {
+ let status = unsafe {
+ AudioObjectRemovePropertyListener(
+ self.device,
+ &self.property,
+ Some(Self::render),
+ self as *const Self as *mut c_void,
+ )
+ };
+ if status == NO_ERR {
+ Ok(())
+ } else {
+ Err(status)
+ }
+ }
+
+ extern "C" fn render(
+ id: AudioObjectID,
+ number_of_addresses: u32,
+ addresses: *const AudioObjectPropertyAddress,
+ data: *mut c_void,
+ ) -> OSStatus {
+ let listener = unsafe { &*(data as *mut Self) };
+ assert_eq!(id, listener.device);
+ let addrs = unsafe { slice::from_raw_parts(addresses, number_of_addresses as usize) };
+ (listener.callback)(addrs)
+ }
+}
+
+impl<F> Drop for TestPropertyListener<F>
+where
+ F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus,
+{
+ fn drop(&mut self) {
+ self.stop();
+ }
+}
+
+// TODO: It doesn't work if default input or output is an aggregate device! Probably we need to do
+// the same thing as what audiounit_set_aggregate_sub_device_list does.
+#[derive(Debug)]
+pub struct TestDevicePlugger {
+ scope: Scope,
+ plugin_id: AudioObjectID,
+ device_id: AudioObjectID,
+}
+
+impl TestDevicePlugger {
+ pub fn new(scope: Scope) -> std::result::Result<Self, OSStatus> {
+ let plugin_id = Self::get_system_plugin_id()?;
+ Ok(Self {
+ scope,
+ plugin_id,
+ device_id: kAudioObjectUnknown,
+ })
+ }
+
+ pub fn get_device_id(&self) -> AudioObjectID {
+ self.device_id
+ }
+
+ pub fn plug(&mut self) -> std::result::Result<(), OSStatus> {
+ self.device_id = self.create_aggregate_device()?;
+ Ok(())
+ }
+
+ pub fn unplug(&mut self) -> std::result::Result<(), OSStatus> {
+ self.destroy_aggregate_device()
+ }
+
+ fn is_plugging(&self) -> bool {
+ self.device_id != kAudioObjectUnknown
+ }
+
+ fn destroy_aggregate_device(&mut self) -> std::result::Result<(), OSStatus> {
+ assert_ne!(self.plugin_id, kAudioObjectUnknown);
+ assert_ne!(self.device_id, kAudioObjectUnknown);
+
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioPlugInDestroyAggregateDevice,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let mut size: usize = 0;
+ let status = unsafe {
+ AudioObjectGetPropertyDataSize(
+ self.plugin_id,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ )
+ };
+ if status != NO_ERR {
+ return Err(status);
+ }
+ assert_ne!(size, 0);
+
+ let status = unsafe {
+ // This call can simulate removing a device.
+ AudioObjectGetPropertyData(
+ self.plugin_id,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ &mut self.device_id as *mut AudioDeviceID as *mut c_void,
+ )
+ };
+ if status == NO_ERR {
+ self.device_id = kAudioObjectUnknown;
+ Ok(())
+ } else {
+ Err(status)
+ }
+ }
+
+ fn create_aggregate_device(&self) -> std::result::Result<AudioObjectID, OSStatus> {
+ use std::time::{SystemTime, UNIX_EPOCH};
+
+ const TEST_AGGREGATE_DEVICE_NAME: &str = "TestAggregateDevice";
+
+ assert_ne!(self.plugin_id, kAudioObjectUnknown);
+
+ let sub_devices = Self::get_sub_devices(self.scope.clone());
+ if sub_devices.is_none() {
+ return Err(kAudioCodecUnspecifiedError as OSStatus);
+ }
+ let sub_devices = sub_devices.unwrap();
+
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioPlugInCreateAggregateDevice,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let mut size: usize = 0;
+ let status = unsafe {
+ AudioObjectGetPropertyDataSize(
+ self.plugin_id,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ )
+ };
+ if status != NO_ERR {
+ return Err(status);
+ }
+ assert_ne!(size, 0);
+
+ let sys_time = SystemTime::now();
+ let time_id = sys_time.duration_since(UNIX_EPOCH).unwrap().as_nanos();
+ let device_name = format!("{}_{}", TEST_AGGREGATE_DEVICE_NAME, time_id);
+ let device_uid = format!("org.mozilla.{}", device_name);
+
+ let mut device_id = kAudioObjectUnknown;
+ let status = unsafe {
+ let device_dict = CFDictionaryCreateMutable(
+ kCFAllocatorDefault,
+ 0,
+ &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks,
+ );
+
+ // Set the name of this device.
+ let device_name = cfstringref_from_string(&device_name);
+ CFDictionaryAddValue(
+ device_dict,
+ cfstringref_from_static_string(AGGREGATE_DEVICE_NAME_KEY) as *const c_void,
+ device_name as *const c_void,
+ );
+ CFRelease(device_name as *const c_void);
+
+ // Set the uid of this device.
+ let device_uid = cfstringref_from_string(&device_uid);
+ CFDictionaryAddValue(
+ device_dict,
+ cfstringref_from_static_string(AGGREGATE_DEVICE_UID_KEY) as *const c_void,
+ device_uid as *const c_void,
+ );
+ CFRelease(device_uid as *const c_void);
+
+ // Make this device NOT private to the process creating it.
+ // On MacOS 14 devicechange events are not triggered when it is private.
+ let private_value: i32 = 0;
+ let device_private_key = CFNumberCreate(
+ kCFAllocatorDefault,
+ i64::from(kCFNumberIntType),
+ &private_value as *const i32 as *const c_void,
+ );
+ CFDictionaryAddValue(
+ device_dict,
+ cfstringref_from_static_string(AGGREGATE_DEVICE_PRIVATE_KEY) as *const c_void,
+ device_private_key as *const c_void,
+ );
+ CFRelease(device_private_key as *const c_void);
+
+ // Set this device to be a stacked aggregate (i.e. multi-output device).
+ let stacked_value: i32 = 0; // 1 for normal aggregate device.
+ let device_stacked_key = CFNumberCreate(
+ kCFAllocatorDefault,
+ i64::from(kCFNumberIntType),
+ &stacked_value as *const i32 as *const c_void,
+ );
+ CFDictionaryAddValue(
+ device_dict,
+ cfstringref_from_static_string(AGGREGATE_DEVICE_STACKED_KEY) as *const c_void,
+ device_stacked_key as *const c_void,
+ );
+ CFRelease(device_stacked_key as *const c_void);
+
+ // Set sub devices for this device.
+ CFDictionaryAddValue(
+ device_dict,
+ cfstringref_from_static_string(AGGREGATE_DEVICE_SUB_DEVICE_LIST_KEY)
+ as *const c_void,
+ sub_devices as *const c_void,
+ );
+ CFRelease(sub_devices as *const c_void);
+
+ // This call can simulate adding a device.
+ let status = AudioObjectGetPropertyData(
+ self.plugin_id,
+ &address,
+ mem::size_of_val(&device_dict) as u32,
+ &device_dict as *const CFMutableDictionaryRef as *const c_void,
+ &mut size as *mut usize as *mut u32,
+ &mut device_id as *mut AudioDeviceID as *mut c_void,
+ );
+ CFRelease(device_dict as *const c_void);
+ status
+ };
+ if status == NO_ERR {
+ assert_ne!(device_id, kAudioObjectUnknown);
+ Ok(device_id)
+ } else {
+ Err(status)
+ }
+ }
+
+ fn get_system_plugin_id() -> std::result::Result<AudioObjectID, OSStatus> {
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioHardwarePropertyPlugInForBundleID,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let mut size: usize = 0;
+ let status = unsafe {
+ AudioObjectGetPropertyDataSize(
+ kAudioObjectSystemObject,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ )
+ };
+ if status != NO_ERR {
+ return Err(status);
+ }
+ assert_ne!(size, 0);
+
+ let mut plugin_id = kAudioObjectUnknown;
+ let mut in_bundle_ref = cfstringref_from_static_string("com.apple.audio.CoreAudio");
+ let mut translation_value = AudioValueTranslation {
+ mInputData: &mut in_bundle_ref as *mut CFStringRef as *mut c_void,
+ mInputDataSize: mem::size_of::<CFStringRef>() as u32,
+ mOutputData: &mut plugin_id as *mut AudioObjectID as *mut c_void,
+ mOutputDataSize: mem::size_of::<AudioObjectID>() as u32,
+ };
+ assert_eq!(size, mem::size_of_val(&translation_value));
+
+ let status = unsafe {
+ let status = AudioObjectGetPropertyData(
+ kAudioObjectSystemObject,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ &mut translation_value as *mut AudioValueTranslation as *mut c_void,
+ );
+ CFRelease(in_bundle_ref as *const c_void);
+ status
+ };
+ if status == NO_ERR {
+ assert_ne!(plugin_id, kAudioObjectUnknown);
+ Ok(plugin_id)
+ } else {
+ Err(status)
+ }
+ }
+
+ // TODO: This doesn't work as what we expect when the default deivce in the scope is an
+ // aggregate device. We should get the list of all the active sub devices and put
+ // them into the array, if the device is an aggregate device. See the code in
+ // AggregateDevice::get_sub_devices and audiounit_set_aggregate_sub_device_list.
+ fn get_sub_devices(scope: Scope) -> Option<CFArrayRef> {
+ let device = test_get_default_device(scope);
+ device?;
+ let device = device.unwrap();
+ let uid = get_device_global_uid(device);
+ if uid.is_err() {
+ return None;
+ }
+ let uid = uid.unwrap();
+ unsafe {
+ let list = CFArrayCreateMutable(ptr::null(), 0, &kCFTypeArrayCallBacks);
+ let sub_device_dict = CFDictionaryCreateMutable(
+ ptr::null(),
+ 0,
+ &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks,
+ );
+ CFDictionaryAddValue(
+ sub_device_dict,
+ cfstringref_from_static_string(SUB_DEVICE_UID_KEY) as *const c_void,
+ uid.get_raw() as *const c_void,
+ );
+ CFArrayAppendValue(list, sub_device_dict as *const c_void);
+ CFRelease(sub_device_dict as *const c_void);
+ Some(list)
+ }
+ }
+}
+
+impl Drop for TestDevicePlugger {
+ fn drop(&mut self) {
+ if self.is_plugging() {
+ self.unplug();
+ }
+ }
+}
+
+// Test Templates
+// ------------------------------------------------------------------------------------------------
+pub fn test_ops_context_operation<F>(name: &'static str, operation: F)
+where
+ F: FnOnce(*mut ffi::cubeb),
+{
+ let name_c_string = CString::new(name).expect("Failed to create context name");
+ let mut context = ptr::null_mut::<ffi::cubeb>();
+ assert_eq!(
+ unsafe { OPS.init.unwrap()(&mut context, name_c_string.as_ptr()) },
+ ffi::CUBEB_OK
+ );
+ assert!(!context.is_null());
+ operation(context);
+ unsafe { OPS.destroy.unwrap()(context) }
+}
+
+// The in-out stream initializeed with different device will create an aggregate_device and
+// result in firing device-collection-changed callbacks. Run in-out streams with tests
+// capturing device-collection-changed callbacks may cause troubles.
+pub fn test_ops_stream_operation<F>(
+ name: &'static str,
+ input_device: ffi::cubeb_devid,
+ input_stream_params: *mut ffi::cubeb_stream_params,
+ output_device: ffi::cubeb_devid,
+ output_stream_params: *mut ffi::cubeb_stream_params,
+ latency_frames: u32,
+ data_callback: ffi::cubeb_data_callback,
+ state_callback: ffi::cubeb_state_callback,
+ user_ptr: *mut c_void,
+ operation: F,
+) where
+ F: FnOnce(*mut ffi::cubeb_stream),
+{
+ test_ops_context_operation("context: stream operation", |context_ptr| {
+ // Do nothing if there is no input/output device to perform input/output tests.
+ if !input_stream_params.is_null() && test_get_default_device(Scope::Input).is_none() {
+ println!("No input device to perform input tests for \"{}\".", name);
+ return;
+ }
+
+ if !output_stream_params.is_null() && test_get_default_device(Scope::Output).is_none() {
+ println!("No output device to perform output tests for \"{}\".", name);
+ return;
+ }
+
+ let mut stream: *mut ffi::cubeb_stream = ptr::null_mut();
+ let stream_name = CString::new(name).expect("Failed to create stream name");
+ assert_eq!(
+ unsafe {
+ OPS.stream_init.unwrap()(
+ context_ptr,
+ &mut stream,
+ stream_name.as_ptr(),
+ input_device,
+ input_stream_params,
+ output_device,
+ output_stream_params,
+ latency_frames,
+ data_callback,
+ state_callback,
+ user_ptr,
+ )
+ },
+ ffi::CUBEB_OK
+ );
+ assert!(!stream.is_null());
+ operation(stream);
+ unsafe {
+ OPS.stream_destroy.unwrap()(stream);
+ }
+ });
+}
+
+pub fn test_get_raw_context<F>(operation: F)
+where
+ F: FnOnce(&mut AudioUnitContext),
+{
+ let mut context = AudioUnitContext::new();
+ operation(&mut context);
+}
+
+pub fn test_get_default_raw_stream<F>(operation: F)
+where
+ F: FnOnce(&mut AudioUnitStream),
+{
+ test_get_raw_stream(ptr::null_mut(), None, None, 0, operation);
+}
+
+fn test_get_raw_stream<F>(
+ user_ptr: *mut c_void,
+ data_callback: ffi::cubeb_data_callback,
+ state_callback: ffi::cubeb_state_callback,
+ latency_frames: u32,
+ operation: F,
+) where
+ F: FnOnce(&mut AudioUnitStream),
+{
+ let mut context = AudioUnitContext::new();
+
+ // Add a stream to the context since we are about to create one.
+ // AudioUnitStream::drop() will check the context has at least one stream.
+ let global_latency_frames = context.update_latency_by_adding_stream(latency_frames);
+
+ let mut stream = AudioUnitStream::new(
+ &mut context,
+ user_ptr,
+ data_callback,
+ state_callback,
+ global_latency_frames.unwrap(),
+ );
+ stream.core_stream_data = CoreStreamData::new(&stream, None, None);
+
+ operation(&mut stream);
+}
+
+pub fn test_get_stream_with_default_data_callback_by_type<F>(
+ name: &'static str,
+ stm_type: StreamType,
+ input_device: Option<AudioObjectID>,
+ output_device: Option<AudioObjectID>,
+ state_callback: extern "C" fn(*mut ffi::cubeb_stream, *mut c_void, ffi::cubeb_state),
+ data: *mut c_void,
+ operation: F,
+) where
+ F: FnOnce(&mut AudioUnitStream),
+{
+ let mut input_params = get_dummy_stream_params(Scope::Input);
+ let mut output_params = get_dummy_stream_params(Scope::Output);
+
+ let in_params = if stm_type.contains(StreamType::INPUT) {
+ &mut input_params as *mut ffi::cubeb_stream_params
+ } else {
+ ptr::null_mut()
+ };
+ let out_params = if stm_type.contains(StreamType::OUTPUT) {
+ &mut output_params as *mut ffi::cubeb_stream_params
+ } else {
+ ptr::null_mut()
+ };
+ let in_device = if let Some(id) = input_device {
+ id as ffi::cubeb_devid
+ } else {
+ ptr::null_mut()
+ };
+ let out_device = if let Some(id) = output_device {
+ id as ffi::cubeb_devid
+ } else {
+ ptr::null_mut()
+ };
+
+ test_ops_stream_operation_with_default_data_callback(
+ name,
+ in_device,
+ in_params,
+ out_device,
+ out_params,
+ state_callback,
+ data,
+ |stream| {
+ let stm = unsafe { &mut *(stream as *mut AudioUnitStream) };
+ operation(stm);
+ },
+ );
+}
+
+bitflags! {
+ pub struct StreamType: u8 {
+ const INPUT = 0x01;
+ const OUTPUT = 0x02;
+ const DUPLEX = 0x03;
+ }
+}
+
+fn get_dummy_stream_params(scope: Scope) -> ffi::cubeb_stream_params {
+ // The stream format for input and output must be same.
+ const STREAM_FORMAT: u32 = ffi::CUBEB_SAMPLE_FLOAT32NE;
+
+ // Make sure the parameters meet the requirements of AudioUnitContext::stream_init
+ // (in the comments).
+ let mut stream_params = ffi::cubeb_stream_params::default();
+ stream_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+ let (format, rate, channels, layout) = match scope {
+ Scope::Input => (STREAM_FORMAT, 48000, 1, ffi::CUBEB_LAYOUT_MONO),
+ Scope::Output => (STREAM_FORMAT, 44100, 2, ffi::CUBEB_LAYOUT_STEREO),
+ };
+ stream_params.format = format;
+ stream_params.rate = rate;
+ stream_params.channels = channels;
+ stream_params.layout = layout;
+ stream_params
+}
+
+fn test_ops_stream_operation_with_default_data_callback<F>(
+ name: &'static str,
+ input_device: ffi::cubeb_devid,
+ input_stream_params: *mut ffi::cubeb_stream_params,
+ output_device: ffi::cubeb_devid,
+ output_stream_params: *mut ffi::cubeb_stream_params,
+ state_callback: extern "C" fn(*mut ffi::cubeb_stream, *mut c_void, ffi::cubeb_state),
+ data: *mut c_void,
+ operation: F,
+) where
+ F: FnOnce(*mut ffi::cubeb_stream),
+{
+ test_ops_stream_operation(
+ name,
+ input_device,
+ input_stream_params,
+ output_device,
+ output_stream_params,
+ 4096, // TODO: Get latency by get_min_latency instead ?
+ Some(noop_data_callback),
+ Some(state_callback),
+ data,
+ operation,
+ );
+}