diff options
Diffstat (limited to 'third_party/rust/cubeb-coreaudio/src/backend/tests/interfaces.rs')
-rw-r--r-- | third_party/rust/cubeb-coreaudio/src/backend/tests/interfaces.rs | 1215 |
1 files changed, 1215 insertions, 0 deletions
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); + }, + ); +} |