extern crate itertools; use self::itertools::iproduct; use super::utils::{ draining_data_callback, 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, test_ops_stream_operation_on_context, Scope, }; use super::*; use std::thread; // 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() { extern "C" fn callback(_: *mut ffi::cubeb, got_called_ptr: *mut c_void) { let got_called = unsafe { &mut *(got_called_ptr as *mut bool) }; *got_called = true; } test_ops_context_operation( "context: register device collection changed and create a duplex stream", |context_ptr| { let got_called = Box::new(false); let got_called_ptr = Box::into_raw(got_called); // Register a callback monitoring both input and output device collection. assert_eq!( unsafe { OPS.register_device_collection_changed.unwrap()( context_ptr, ffi::CUBEB_DEVICE_TYPE_INPUT | ffi::CUBEB_DEVICE_TYPE_OUTPUT, Some(callback), got_called_ptr as *mut c_void, ) }, ffi::CUBEB_OK ); // The aggregate device is very likely to be created in the system // when creating a duplex stream. We need to make sure it won't trigger // the callback. test_default_duplex_stream_operation("duplex stream", |_stream| { // Do nothing but wait for device-collection change. thread::sleep(Duration::from_millis(200)); }); // Unregister the callback. assert_eq!( unsafe { OPS.register_device_collection_changed.unwrap()( context_ptr, ffi::CUBEB_DEVICE_TYPE_INPUT | ffi::CUBEB_DEVICE_TYPE_OUTPUT, None, got_called_ptr as *mut c_void, ) }, ffi::CUBEB_OK ); let got_called = unsafe { Box::from_raw(got_called_ptr) }; assert!(!got_called.as_ref()); }, ); } #[test] #[ignore] fn test_ops_context_register_device_collection_changed_manual() { test_ops_context_operation( "(manual) context: register device collection changed", |context_ptr| { println!("context @ {:p}", context_ptr); struct Data { context: *mut ffi::cubeb, touched: u32, // TODO: Use AtomicU32 instead } extern "C" fn input_callback(context: *mut ffi::cubeb, user: *mut c_void) { println!("input > context @ {:p}", context); let data = unsafe { &mut (*(user as *mut Data)) }; assert_eq!(context, data.context); data.touched += 1; } extern "C" fn output_callback(context: *mut ffi::cubeb, user: *mut c_void) { println!("output > context @ {:p}", context); let data = unsafe { &mut (*(user as *mut Data)) }; assert_eq!(context, data.context); data.touched += 1; } let mut data = Data { context: context_ptr, touched: 0, }; // Register a callback for input scope. assert_eq!( unsafe { OPS.register_device_collection_changed.unwrap()( context_ptr, ffi::CUBEB_DEVICE_TYPE_INPUT, Some(input_callback), &mut data as *mut Data as *mut c_void, ) }, ffi::CUBEB_OK ); // Register a callback for output scope. assert_eq!( unsafe { OPS.register_device_collection_changed.unwrap()( context_ptr, ffi::CUBEB_DEVICE_TYPE_OUTPUT, Some(output_callback), &mut data as *mut Data as *mut c_void, ) }, ffi::CUBEB_OK ); while data.touched < 2 {} }, ); } #[test] fn test_ops_context_stream_init_no_stream_params() { let name = "context: stream_init with no stream params"; test_ops_context_operation(name, |context_ptr| { let mut stream: *mut ffi::cubeb_stream = ptr::null_mut(); let stream_name = CString::new(name).expect("Failed to create stream name"); assert_eq!( unsafe { OPS.stream_init.unwrap()( context_ptr, &mut stream, stream_name.as_ptr(), ptr::null_mut(), // Use default input device. ptr::null_mut(), // No input parameters. ptr::null_mut(), // Use default output device. ptr::null_mut(), // No output parameters. 4096, // TODO: Get latency by get_min_latency instead ? Some(noop_data_callback), None, // No state callback. ptr::null_mut(), // No user data pointer. ) }, ffi::CUBEB_ERROR_INVALID_PARAMETER ); assert!(stream.is_null()); }); } #[test] fn test_ops_context_stream_init_no_input_stream_params() { let name = "context: stream_init with no input stream params"; let input_device = test_get_default_device(Scope::Input); if input_device.is_none() { println!("No input device to perform input tests for \"{}\".", name); return; } test_ops_context_operation(name, |context_ptr| { let mut stream: *mut ffi::cubeb_stream = ptr::null_mut(); let stream_name = CString::new(name).expect("Failed to create stream name"); assert_eq!( unsafe { OPS.stream_init.unwrap()( context_ptr, &mut stream, stream_name.as_ptr(), input_device.unwrap() as ffi::cubeb_devid, ptr::null_mut(), // No input parameters. ptr::null_mut(), // Use default output device. ptr::null_mut(), // No output parameters. 4096, // TODO: Get latency by get_min_latency instead ? Some(noop_data_callback), None, // No state callback. ptr::null_mut(), // No user data pointer. ) }, ffi::CUBEB_ERROR_INVALID_PARAMETER ); assert!(stream.is_null()); }); } #[test] fn test_ops_context_stream_init_no_output_stream_params() { let name = "context: stream_init with no output stream params"; let output_device = test_get_default_device(Scope::Output); if output_device.is_none() { println!("No output device to perform output tests for \"{}\".", name); return; } test_ops_context_operation(name, |context_ptr| { let mut stream: *mut ffi::cubeb_stream = ptr::null_mut(); let stream_name = CString::new(name).expect("Failed to create stream name"); assert_eq!( unsafe { OPS.stream_init.unwrap()( context_ptr, &mut stream, stream_name.as_ptr(), ptr::null_mut(), // Use default input device. ptr::null_mut(), // No input parameters. output_device.unwrap() as ffi::cubeb_devid, ptr::null_mut(), // No output parameters. 4096, // TODO: Get latency by get_min_latency instead ? Some(noop_data_callback), None, // No state callback. ptr::null_mut(), // No user data pointer. ) }, ffi::CUBEB_ERROR_INVALID_PARAMETER ); assert!(stream.is_null()); }); } #[test] fn test_ops_context_stream_init_no_data_callback() { let name = "context: stream_init with no data callback"; test_ops_context_operation(name, |context_ptr| { let mut stream: *mut ffi::cubeb_stream = ptr::null_mut(); let stream_name = CString::new(name).expect("Failed to create stream name"); let mut output_params = ffi::cubeb_stream_params::default(); output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; output_params.rate = 44100; output_params.channels = 2; output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; assert_eq!( unsafe { OPS.stream_init.unwrap()( context_ptr, &mut stream, stream_name.as_ptr(), ptr::null_mut(), // Use default input device. ptr::null_mut(), // No input parameters. ptr::null_mut(), // Use default output device. &mut output_params, 4096, // TODO: Get latency by get_min_latency instead ? None, // No data callback. None, // No state callback. ptr::null_mut(), // No user data pointer. ) }, ffi::CUBEB_ERROR_INVALID_PARAMETER ); assert!(stream.is_null()); }); } #[test] fn test_ops_context_stream_init_channel_rate_combinations() { let name = "context: stream_init with various channels and rates"; test_ops_context_operation(name, |context_ptr| { let mut stream: *mut ffi::cubeb_stream = ptr::null_mut(); let stream_name = CString::new(name).expect("Failed to create stream name"); const MAX_NUM_CHANNELS: u32 = 32; let channel_values: Vec = vec![1, 2, 3, 4, 6]; let freq_values: Vec = vec![16000, 24000, 44100, 48000]; let is_float_values: Vec = vec![false, true]; for (channels, freq, is_float) in iproduct!(channel_values, freq_values, is_float_values) { assert!(channels < MAX_NUM_CHANNELS); println!("--------------------------"); println!( "Testing {} channel(s), {} Hz, {}\n", channels, freq, if is_float { "float" } else { "short" } ); let mut output_params = ffi::cubeb_stream_params::default(); output_params.format = if is_float { ffi::CUBEB_SAMPLE_FLOAT32NE } else { ffi::CUBEB_SAMPLE_S16NE }; output_params.rate = freq; output_params.channels = channels; output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE; assert_eq!( unsafe { OPS.stream_init.unwrap()( context_ptr, &mut stream, stream_name.as_ptr(), ptr::null_mut(), // Use default input device. ptr::null_mut(), // No input parameters. ptr::null_mut(), // Use default output device. &mut output_params, 4096, // TODO: Get latency by get_min_latency instead ? Some(noop_data_callback), // No data callback. None, // No state callback. ptr::null_mut(), // No user data pointer. ) }, ffi::CUBEB_OK ); assert!(!stream.is_null()); unsafe { OPS.stream_destroy.unwrap()(stream) }; } }); } // Stream Operations // ------------------------------------------------------------------------------------------------ fn test_default_output_stream_operation_on_context_with_callback( name: &'static str, context_ptr: *mut ffi::cubeb, data_callback: ffi::cubeb_data_callback, 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_on_context( name, context_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 ? data_callback, None, // No state callback. ptr::null_mut(), // No user data pointer. operation, ); } fn test_default_output_stream_operation_with_callback( name: &'static str, data_callback: ffi::cubeb_data_callback, operation: F, ) where F: FnOnce(*mut ffi::cubeb_stream), { test_ops_context_operation("context: default output stream operation", |context_ptr| { test_default_output_stream_operation_on_context_with_callback( name, context_ptr, data_callback, operation, ); }); } fn test_default_output_stream_operation(name: &'static str, operation: F) where F: FnOnce(*mut ffi::cubeb_stream), { test_default_output_stream_operation_with_callback(name, Some(noop_data_callback), operation); } fn test_default_duplex_stream_operation_on_context_with_callback( name: &'static str, context_ptr: *mut ffi::cubeb, data_callback: ffi::cubeb_data_callback, 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_on_context( name, context_ptr, 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 ? data_callback, None, // No state callback. ptr::null_mut(), // No user data pointer. operation, ); } fn test_default_duplex_stream_operation_with_callback( name: &'static str, data_callback: ffi::cubeb_data_callback, operation: F, ) where F: FnOnce(*mut ffi::cubeb_stream), { test_ops_context_operation("context: default duplex stream operation", |context_ptr| { test_default_duplex_stream_operation_on_context_with_callback( name, context_ptr, data_callback, operation, ); }); } fn test_default_duplex_stream_operation(name: &'static str, operation: F) where F: FnOnce(*mut ffi::cubeb_stream), { test_default_duplex_stream_operation_with_callback(name, Some(noop_data_callback), operation); } fn test_stereo_input_duplex_stream_operation_on_context_with_callback( name: &'static str, context_ptr: *mut ffi::cubeb, data_callback: ffi::cubeb_data_callback, 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_on_context( name, context_ptr, 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 ? data_callback, None, // No state callback. ptr::null_mut(), // No user data pointer. operation, ); } fn test_stereo_input_duplex_stream_operation_with_callback( name: &'static str, data_callback: ffi::cubeb_data_callback, operation: F, ) where F: FnOnce(*mut ffi::cubeb_stream), { test_ops_context_operation( "context: stereo input duplex stream operation", |context_ptr| { test_stereo_input_duplex_stream_operation_on_context_with_callback( name, context_ptr, data_callback, operation, ); }, ); } fn test_stereo_input_duplex_stream_operation(name: &'static str, operation: F) where F: FnOnce(*mut ffi::cubeb_stream), { test_stereo_input_duplex_stream_operation_with_callback( name, Some(noop_data_callback), operation, ); } fn test_default_input_voice_stream_operation_on_context_with_callback( name: &'static str, context_ptr: *mut ffi::cubeb, data_callback: ffi::cubeb_data_callback, 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; test_ops_stream_operation_on_context( name, context_ptr, ptr::null_mut(), // Use default input device. &mut input_params, ptr::null_mut(), // Use default output device. ptr::null_mut(), // No output parameters. 4096, // TODO: Get latency by get_min_latency instead ? data_callback, None, // No state callback. ptr::null_mut(), // No user data pointer. operation, ); } fn test_default_input_voice_stream_operation_on_context( name: &'static str, context_ptr: *mut ffi::cubeb, operation: F, ) where F: FnOnce(*mut ffi::cubeb_stream), { test_default_input_voice_stream_operation_on_context_with_callback( name, context_ptr, Some(noop_data_callback), operation, ); } fn test_default_input_voice_stream_operation_with_callback( name: &'static str, data_callback: ffi::cubeb_data_callback, operation: F, ) where F: FnOnce(*mut ffi::cubeb_stream), { test_ops_context_operation( "context: default input voice stream operation", |context_ptr| { test_default_input_voice_stream_operation_on_context_with_callback( name, context_ptr, data_callback, operation, ); }, ); } fn test_default_input_voice_stream_operation(name: &'static str, operation: F) where F: FnOnce(*mut ffi::cubeb_stream), { test_default_input_voice_stream_operation_with_callback( name, Some(noop_data_callback), operation, ); } fn test_default_duplex_voice_stream_operation_on_context_with_callback( name: &'static str, context_ptr: *mut ffi::cubeb, data_callback: ffi::cubeb_data_callback, 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_on_context( name, context_ptr, 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 ? data_callback, None, // No state callback. ptr::null_mut(), // No user data pointer. operation, ); } fn test_default_duplex_voice_stream_operation_on_context( name: &'static str, context_ptr: *mut ffi::cubeb, operation: F, ) where F: FnOnce(*mut ffi::cubeb_stream), { test_default_duplex_voice_stream_operation_on_context_with_callback( name, context_ptr, Some(noop_data_callback), operation, ); } fn test_default_duplex_voice_stream_operation_with_callback( name: &'static str, data_callback: ffi::cubeb_data_callback, operation: F, ) where F: FnOnce(*mut ffi::cubeb_stream), { test_ops_context_operation("context: duplex voice stream operation", |context_ptr| { test_default_duplex_voice_stream_operation_on_context_with_callback( name, context_ptr, data_callback, operation, ); }); } fn test_default_duplex_voice_stream_operation(name: &'static str, operation: F) where F: FnOnce(*mut ffi::cubeb_stream), { test_default_duplex_voice_stream_operation_with_callback( name, Some(noop_data_callback), operation, ); } fn test_stereo_input_duplex_voice_stream_operation(name: &'static str, operation: F) where F: FnOnce(*mut ffi::cubeb_stream), { let mut input_devices = get_devices_info_in_scope(Scope::Input); input_devices.retain(|d| test_device_channels_in_scope(d.id, Scope::Input).unwrap_or(0) >= 2); if input_devices.is_empty() { println!("No stereo input device present. Skipping stereo-input test."); return; } let mut input_params = ffi::cubeb_stream_params::default(); input_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; input_params.rate = 44100; input_params.channels = 2; input_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; input_params.prefs = ffi::CUBEB_STREAM_PREF_VOICE; let mut output_params = ffi::cubeb_stream_params::default(); output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE; output_params.rate = 44100; output_params.channels = 2; output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED; output_params.prefs = ffi::CUBEB_STREAM_PREF_VOICE; test_ops_stream_operation( name, input_devices[0].id as ffi::cubeb_devid, &mut input_params, ptr::null_mut(), // Use default output device. &mut output_params, 4096, // TODO: Get latency by get_min_latency instead ? Some(noop_data_callback), None, // No state callback. ptr::null_mut(), // No user data pointer. operation, ); } #[test] fn test_ops_stream_init_and_destroy() { test_default_output_stream_operation("stream: init and destroy", |_stream| {}); } #[test] fn test_ops_stream_start() { test_default_output_stream_operation("stream: start", |stream| { assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); }); } #[test] fn test_ops_stream_stop() { test_default_output_stream_operation("stream: stop", |stream| { assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK); }); } #[test] fn test_ops_stream_drain() { test_default_output_stream_operation_with_callback( "stream: drain", Some(draining_data_callback), |stream| { assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); thread::sleep(Duration::from_millis(10)); }, ); } #[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_stereo_input_duplex_stream_drain() { test_stereo_input_duplex_stream_operation_with_callback( "stereo-input duplex stream: drain", Some(draining_data_callback), |stream| { assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); thread::sleep(Duration::from_millis(10)); }, ); } #[test] fn test_ops_input_voice_stream_init_and_destroy() { test_default_input_voice_stream_operation("input voice stream: init and destroy", |stream| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert_eq!( stm.core_stream_data.using_voice_processing_unit(), macos_kernel_major_version().unwrap() != MACOS_KERNEL_MAJOR_VERSION_MONTEREY ); }); } #[test] fn test_ops_input_voice_stream_start() { test_default_input_voice_stream_operation("input voice stream: start", |stream| { assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert_eq!( stm.core_stream_data.using_voice_processing_unit(), macos_kernel_major_version().unwrap() != MACOS_KERNEL_MAJOR_VERSION_MONTEREY ); }); } #[test] fn test_ops_input_voice_stream_stop() { test_default_input_voice_stream_operation("input voice stream: stop", |stream| { assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK); let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert_eq!( stm.core_stream_data.using_voice_processing_unit(), macos_kernel_major_version().unwrap() != MACOS_KERNEL_MAJOR_VERSION_MONTEREY ); }); } #[test] fn test_ops_duplex_voice_stream_init_and_destroy() { test_default_duplex_voice_stream_operation("duplex voice stream: init and destroy", |stream| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert_eq!( stm.core_stream_data.using_voice_processing_unit(), macos_kernel_major_version().unwrap() != MACOS_KERNEL_MAJOR_VERSION_MONTEREY ); }); } #[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); let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert_eq!( stm.core_stream_data.using_voice_processing_unit(), macos_kernel_major_version().unwrap() != MACOS_KERNEL_MAJOR_VERSION_MONTEREY ); }); } #[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); let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert_eq!( stm.core_stream_data.using_voice_processing_unit(), macos_kernel_major_version().unwrap() != MACOS_KERNEL_MAJOR_VERSION_MONTEREY ); }); } #[test] fn test_ops_duplex_voice_stream_drain() { test_default_duplex_voice_stream_operation_with_callback( "duplex voice stream: drain", Some(draining_data_callback), |stream| { assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert_eq!( stm.core_stream_data.using_voice_processing_unit(), macos_kernel_major_version().unwrap() != MACOS_KERNEL_MAJOR_VERSION_MONTEREY ); thread::sleep(Duration::from_millis(10)); }, ); } #[test] #[ignore] fn test_ops_timing_sensitive_multiple_voice_stream_init_and_destroy() { if macos_kernel_major_version().unwrap() == MACOS_KERNEL_MAJOR_VERSION_MONTEREY { // We disable VPIO on Monterey. return; } let start = Instant::now(); let mut t1 = start; let mut t2 = start; let mut t3 = start; let mut t4 = start; let mut t5 = start; let mut t6 = start; let mut t7 = start; let mut t8 = start; let mut t9 = start; let mut t10 = start; test_ops_context_operation("multiple duplex voice streams", |context_ptr| { // First stream uses vpio, creates the shared vpio unit. test_default_duplex_voice_stream_operation_on_context( "multiple voice streams: stream 1, duplex", context_ptr, |stream| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); // Two concurrent vpio streams are supported. test_default_input_voice_stream_operation_on_context( "multiple voice streams: stream 2, input-only", context_ptr, |stream| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); // Three concurrent vpio streams are supported. test_default_duplex_voice_stream_operation_on_context( "multiple voice streams: stream 3, duplex", context_ptr, |stream| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); }, ); }, ); }, ); t1 = Instant::now(); // Fourth stream uses vpio, allows reuse of one already created. test_default_duplex_voice_stream_operation_on_context( "multiple voice streams: stream 4, duplex", context_ptr, |stream| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); t2 = Instant::now(); // Fifth stream uses vpio, allows reuse of one already created. test_default_duplex_voice_stream_operation_on_context( "multiple voice streams: stream 5, duplex", context_ptr, |stream| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); t3 = Instant::now(); // Sixth stream uses vpio, allows reuse of one already created. test_default_input_voice_stream_operation_on_context( "multiple voice streams: stream 6, input-only", context_ptr, |stream| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); t4 = Instant::now(); // Seventh stream uses vpio, but is created anew. test_default_input_voice_stream_operation_on_context( "multiple voice streams: stream 7, input-only", context_ptr, |stream| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); t5 = Instant::now(); }, ); t6 = Instant::now(); }, ); t7 = Instant::now(); }, ); t8 = Instant::now(); }, ); t9 = Instant::now(); }); t10 = Instant::now(); let reuse_vpio_1 = t2 - t1; let reuse_vpio_2 = t3 - t2; let reuse_vpio_3 = t4 - t3; let create_standalone_vpio = t5 - t4; assert!( create_standalone_vpio > reuse_vpio_1 * 2, "Failed create_standalone_vpio={}s > reuse_vpio_1={}s * 2", create_standalone_vpio.as_secs_f32(), reuse_vpio_1.as_secs_f32() ); assert!( create_standalone_vpio > reuse_vpio_2 * 2, "Failed create_standalone_vpio={}s > reuse_vpio_2={}s * 2", create_standalone_vpio.as_secs_f32(), reuse_vpio_2.as_secs_f32() ); assert!( create_standalone_vpio > reuse_vpio_3 * 2, "Failed create_standalone_vpio={}s > reuse_vpio_3={}s * 2", create_standalone_vpio.as_secs_f32(), reuse_vpio_3.as_secs_f32() ); let recycle_vpio_1 = t6 - t5; let recycle_vpio_2 = t7 - t6; let recycle_vpio_3 = t8 - t7; let recycle_vpio_4 = t9 - t8; let dispose_vpios = t10 - t9; assert!( dispose_vpios > recycle_vpio_1 * 2, "Failed dispose_vpios={}s > recycle_vpio_1 ={}s * 2", dispose_vpios.as_secs_f32(), recycle_vpio_1.as_secs_f32() ); assert!( dispose_vpios > recycle_vpio_2 * 2, "Failed dispose_vpios={}s > recycle_vpio_2 ={}s * 2", dispose_vpios.as_secs_f32(), recycle_vpio_2.as_secs_f32() ); assert!( dispose_vpios > recycle_vpio_3 * 2, "Failed dispose_vpios={}s > recycle_vpio_3 ={}s * 2", dispose_vpios.as_secs_f32(), recycle_vpio_3.as_secs_f32() ); assert!( dispose_vpios > recycle_vpio_4 * 2, "Failed dispose_vpios={}s > recycle_vpio_4 ={}s * 2", dispose_vpios.as_secs_f32(), recycle_vpio_4.as_secs_f32() ); } #[test] #[ignore] fn test_ops_timing_sensitive_multiple_duplex_voice_stream_start() { if macos_kernel_major_version().unwrap() == MACOS_KERNEL_MAJOR_VERSION_MONTEREY { // We disable VPIO on Monterey. return; } test_ops_context_operation("multiple duplex voice streams", |context_ptr| { let start = Instant::now(); // First stream uses vpio, creates the shared vpio unit. test_default_duplex_voice_stream_operation_on_context( "multiple duplex voice streams: stream 1", context_ptr, |stream| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); }, ); let d1 = start.elapsed(); // Second stream uses vpio, allows reuse of the one already created. test_default_duplex_voice_stream_operation_on_context( "multiple duplex voice streams: stream 2", context_ptr, |stream| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK); }, ); let d2 = start.elapsed() - d1; // d1 being significantly longer than d2 is proof we reuse vpio. assert!( d1 > d2 * 2, "Failed d1={}s > d2={}s * s", d1.as_secs_f32(), d2.as_secs_f32() ); }); } #[test] #[ignore] fn test_ops_timing_sensitive_multiple_duplex_voice_stream_params() { if macos_kernel_major_version().unwrap() == MACOS_KERNEL_MAJOR_VERSION_MONTEREY { // We disable VPIO on Monterey. return; } test_ops_context_operation("multiple duplex voice streams with params", |context_ptr| { let start = Instant::now(); // First stream uses vpio, creates the shared vpio unit. test_default_duplex_voice_stream_operation_on_context( "multiple duplex voice streams: stream 1", context_ptr, |stream| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); assert_eq!( unsafe { OPS.stream_set_input_processing_params.unwrap()( stream, ffi::CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION | ffi::CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION, ) }, ffi::CUBEB_OK ); assert_eq!( unsafe { OPS.stream_set_input_mute.unwrap()(stream, 1) }, ffi::CUBEB_OK ); }, ); let d1 = start.elapsed(); // Second stream uses vpio, allows reuse of the one already created. test_default_duplex_voice_stream_operation_on_context( "multiple duplex voice streams: stream 2", context_ptr, |stream| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); let queue = stm.queue.clone(); // Test that input processing params does not carry over when reusing vpio. let mut bypass: u32 = 0; let r = queue .run_sync(|| { audio_unit_get_property( stm.core_stream_data.input_unit, kAUVoiceIOProperty_BypassVoiceProcessing, kAudioUnitScope_Global, AU_IN_BUS, &mut bypass, &mut mem::size_of::(), ) }) .unwrap(); assert_eq!(r, NO_ERR); assert_eq!(bypass, 1); // Test that input mute state does not carry over when reusing vpio. let mut mute: u32 = 0; let r = queue .run_sync(|| { audio_unit_get_property( stm.core_stream_data.input_unit, kAUVoiceIOProperty_MuteOutput, kAudioUnitScope_Global, AU_IN_BUS, &mut mute, &mut mem::size_of::(), ) }) .unwrap(); assert_eq!(r, NO_ERR); assert_eq!(mute, 0); }, ); let d2 = start.elapsed() - d1; // d1 being significantly longer than d2 is proof we reuse vpio. assert!( d1 > d2 * 2, "Failed d1={}s > d2={}s * 2", d1.as_secs_f32(), d2.as_secs_f32() ); }); } #[test] fn test_ops_duplex_voice_stream_set_input_mute() { if macos_kernel_major_version().unwrap() == MACOS_KERNEL_MAJOR_VERSION_MONTEREY { // We disable VPIO on Monterey. return; } 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 ); let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); }); } #[test] fn test_ops_duplex_voice_stream_set_input_mute_before_start() { if macos_kernel_major_version().unwrap() == MACOS_KERNEL_MAJOR_VERSION_MONTEREY { // We disable VPIO on Monterey. return; } 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); let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); }, ); } #[test] fn test_ops_duplex_voice_stream_set_input_mute_before_start_with_reinit() { if macos_kernel_major_version().unwrap() == MACOS_KERNEL_MAJOR_VERSION_MONTEREY { // We disable VPIO on Monterey. return; } 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) }; assert!(stm.core_stream_data.using_voice_processing_unit()); stm.reinit_async(); let queue = stm.queue.clone(); let mut mute_after_reinit = false; queue.run_sync(|| { let mut mute: u32 = 0; let r = audio_unit_get_property( stm.core_stream_data.input_unit, kAUVoiceIOProperty_MuteOutput, kAudioUnitScope_Global, AU_IN_BUS, &mut mute, &mut mem::size_of::(), ); assert_eq!(r, NO_ERR); mute_after_reinit = mute == 1; }); assert_eq!(mute_after_reinit, true); }, ); } #[test] fn test_ops_duplex_voice_stream_set_input_mute_after_start() { if macos_kernel_major_version().unwrap() == MACOS_KERNEL_MAJOR_VERSION_MONTEREY { // We disable VPIO on Monterey. return; } 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 ); let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); }); } #[test] fn test_ops_duplex_voice_stream_set_input_processing_params() { if macos_kernel_major_version().unwrap() == MACOS_KERNEL_MAJOR_VERSION_MONTEREY { // We disable VPIO on Monterey. return; } 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 ); let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); }); } #[test] fn test_ops_duplex_voice_stream_set_input_processing_params_before_start() { if macos_kernel_major_version().unwrap() == MACOS_KERNEL_MAJOR_VERSION_MONTEREY { // We disable VPIO on Monterey. return; } 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); let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); }, ); } #[test] fn test_ops_duplex_voice_stream_set_input_processing_params_before_start_with_reinit() { if macos_kernel_major_version().unwrap() == MACOS_KERNEL_MAJOR_VERSION_MONTEREY { // We disable VPIO on Monterey. return; } 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) }; assert!(stm.core_stream_data.using_voice_processing_unit()); stm.reinit_async(); let queue = stm.queue.clone(); let mut params_after_reinit: ffi::cubeb_input_processing_params = ffi::CUBEB_INPUT_PROCESSING_PARAM_NONE; queue.run_sync(|| { let mut params: ffi::cubeb_input_processing_params = ffi::CUBEB_INPUT_PROCESSING_PARAM_NONE; let mut agc: u32 = 0; let r = audio_unit_get_property( stm.core_stream_data.input_unit, kAUVoiceIOProperty_VoiceProcessingEnableAGC, kAudioUnitScope_Global, AU_IN_BUS, &mut agc, &mut mem::size_of::(), ); assert_eq!(r, NO_ERR); if agc == 1 { params = params | ffi::CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL; } let mut bypass: u32 = 0; let r = audio_unit_get_property( stm.core_stream_data.input_unit, kAUVoiceIOProperty_BypassVoiceProcessing, kAudioUnitScope_Global, AU_IN_BUS, &mut bypass, &mut mem::size_of::(), ); assert_eq!(r, NO_ERR); if bypass == 0 { params = params | ffi::CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION | ffi::CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION; } params_after_reinit = params; }); assert_eq!(params, params_after_reinit); }, ); } #[test] fn test_ops_duplex_voice_stream_set_input_processing_params_after_start() { if macos_kernel_major_version().unwrap() == MACOS_KERNEL_MAJOR_VERSION_MONTEREY { // We disable VPIO on Monterey. return; } test_default_duplex_voice_stream_operation( "duplex voice stream: processing after start", |stream| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert!(stm.core_stream_data.using_voice_processing_unit()); 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| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert_eq!( stm.core_stream_data.using_voice_processing_unit(), macos_kernel_major_version().unwrap() != MACOS_KERNEL_MAJOR_VERSION_MONTEREY ); }, ); } #[test] fn test_ops_stereo_input_duplex_voice_stream_start() { test_stereo_input_duplex_voice_stream_operation( "stereo-input duplex voice stream: start", |stream| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert_eq!( stm.core_stream_data.using_voice_processing_unit(), macos_kernel_major_version().unwrap() != MACOS_KERNEL_MAJOR_VERSION_MONTEREY ); 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| { let stm = unsafe { &mut *(stream as *mut AudioUnitStream) }; assert_eq!( stm.core_stream_data.using_voice_processing_unit(), macos_kernel_major_version().unwrap() != MACOS_KERNEL_MAJOR_VERSION_MONTEREY ); assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK); }, ); }