use std::ffi::c_void;
use std::panic::{catch_unwind, AssertUnwindSafe};
use crate::dispatch;
use crate::error::OsStatus;
use crate::fourcc::FourCharCode;
use crate::io::{IoBuffer, IoOperation};
use crate::object::AudioObjectId;
use crate::raw::abi::{
AudioObjectID, AudioObjectPropertyAddress, AudioServerPlugInDriverRef,
AudioServerPlugInHostRef, AudioServerPlugInIOCycleInfo, Boolean, CFUUIDBytes, OSStatus, UInt32,
};
use crate::raw::marshal;
use crate::raw::platform;
use crate::raw::runtime::DriverObject;
use crate::realtime::RealtimeContext;
const MAX_CYCLE_SAMPLES: usize = 8192;
const IUNKNOWN_UUID: CFUUIDBytes = CFUUIDBytes {
byte0: 0x00,
byte1: 0x00,
byte2: 0x00,
byte3: 0x00,
byte4: 0x00,
byte5: 0x00,
byte6: 0x00,
byte7: 0x00,
byte8: 0xC0,
byte9: 0x00,
byte10: 0x00,
byte11: 0x00,
byte12: 0x00,
byte13: 0x00,
byte14: 0x00,
byte15: 0x46,
};
const ASPL_DRIVER_INTERFACE_UUID: CFUUIDBytes = CFUUIDBytes {
byte0: 0xEE,
byte1: 0xA5,
byte2: 0x77,
byte3: 0x3D,
byte4: 0xCC,
byte5: 0x43,
byte6: 0x49,
byte7: 0xF1,
byte8: 0x8E,
byte9: 0x00,
byte10: 0x8F,
byte11: 0x96,
byte12: 0xE7,
byte13: 0xD2,
byte14: 0x3B,
byte15: 0x17,
};
const E_NOINTERFACE: OSStatus = 0x8000_0004_u32 as OSStatus;
#[cfg(target_os = "macos")]
fn write_cfstring(text: &str, out_data: *mut c_void, capacity: usize) -> Result<usize, OsStatus> {
let needed = core::mem::size_of::<crate::raw::cf::CFStringRef>();
if out_data.is_null() || capacity < needed {
return Err(OsStatus::BAD_PROPERTY_SIZE);
}
let cfstr = crate::raw::cf::create_string(text);
if cfstr.is_null() {
return Err(OsStatus::UNSPECIFIED);
}
unsafe { out_data.cast::<crate::raw::cf::CFStringRef>().write(cfstr) };
Ok(needed)
}
#[cfg(not(target_os = "macos"))]
fn write_cfstring(
_text: &str,
_out_data: *mut c_void,
_capacity: usize,
) -> Result<usize, OsStatus> {
Err(OsStatus::UNSPECIFIED)
}
fn guard<F>(body: F) -> OsStatus
where
F: FnOnce() -> OsStatus,
{
catch_unwind(AssertUnwindSafe(body)).unwrap_or(OsStatus::UNSPECIFIED)
}
unsafe fn out_slice<'a>(ptr: *mut c_void, len: UInt32) -> &'a mut [u8] {
if ptr.is_null() || len == 0 {
return &mut [];
}
unsafe { core::slice::from_raw_parts_mut(ptr.cast::<u8>(), len as usize) }
}
unsafe fn in_slice<'a>(ptr: *const c_void, len: UInt32) -> &'a [u8] {
if ptr.is_null() || len == 0 {
return &[];
}
unsafe { core::slice::from_raw_parts(ptr.cast::<u8>(), len as usize) }
}
unsafe fn in_samples<'a>(ptr: *const c_void, count: usize) -> &'a [f32] {
if ptr.is_null() || count == 0 {
return &[];
}
unsafe { core::slice::from_raw_parts(ptr.cast::<f32>(), count) }
}
unsafe fn out_samples<'a>(ptr: *mut c_void, count: usize) -> &'a mut [f32] {
if ptr.is_null() || count == 0 {
return &mut [];
}
unsafe { core::slice::from_raw_parts_mut(ptr.cast::<f32>(), count) }
}
pub unsafe extern "C" fn query_interface(
this: *mut c_void,
in_uuid_bytes: CFUUIDBytes,
out_interface: *mut *mut c_void,
) -> OSStatus {
let Some(object) = (unsafe { DriverObject::from_ref(this.cast()) }) else {
return OsStatus::BAD_OBJECT.as_i32();
};
if in_uuid_bytes != IUNKNOWN_UUID && in_uuid_bytes != ASPL_DRIVER_INTERFACE_UUID {
if !out_interface.is_null() {
unsafe { *out_interface = core::ptr::null_mut() };
}
return E_NOINTERFACE;
}
object.refcount().add_ref();
if !out_interface.is_null() {
unsafe { *out_interface = this };
}
OsStatus::OK.as_i32()
}
pub unsafe extern "C" fn add_ref(this: *mut c_void) -> u32 {
match unsafe { DriverObject::from_ref(this.cast()) } {
Some(object) => object.refcount().add_ref(),
None => 0,
}
}
pub unsafe extern "C" fn release(this: *mut c_void) -> u32 {
let Some(object) = (unsafe { DriverObject::from_ref(this.cast()) }) else {
return 0;
};
let remaining = object.refcount().release();
if remaining == 0 {
drop(unsafe { Box::from_raw(this.cast::<DriverObject>()) });
}
remaining
}
pub unsafe extern "C" fn initialize(
driver: AudioServerPlugInDriverRef,
host: AudioServerPlugInHostRef,
) -> OSStatus {
let Some(object) = (unsafe { DriverObject::from_ref(driver) }) else {
return OsStatus::BAD_OBJECT.as_i32();
};
let runtime = object.runtime();
runtime.set_host(host);
guard(|| OsStatus::from(runtime.driver().initialize())).as_i32()
}
pub unsafe extern "C" fn start_io(
driver: AudioServerPlugInDriverRef,
_device_id: AudioObjectID,
_client_id: UInt32,
) -> OSStatus {
let Some(object) = (unsafe { DriverObject::from_ref(driver) }) else {
return OsStatus::BAD_OBJECT.as_i32();
};
let runtime = object.runtime();
let status = guard(|| OsStatus::from(runtime.driver().start_io()));
if status.is_ok() {
runtime.state().running = true;
runtime.clock().start(platform::host_time_now());
}
status.as_i32()
}
pub unsafe extern "C" fn stop_io(
driver: AudioServerPlugInDriverRef,
_device_id: AudioObjectID,
_client_id: UInt32,
) -> OSStatus {
let Some(object) = (unsafe { DriverObject::from_ref(driver) }) else {
return OsStatus::BAD_OBJECT.as_i32();
};
let runtime = object.runtime();
let status = guard(|| OsStatus::from(runtime.driver().stop_io()));
if status.is_ok() {
runtime.state().running = false;
runtime.clock().stop();
}
status.as_i32()
}
pub unsafe extern "C" fn get_zero_time_stamp(
driver: AudioServerPlugInDriverRef,
_device_id: AudioObjectID,
_client_id: UInt32,
out_sample_time: *mut crate::raw::abi::Float64,
out_host_time: *mut crate::raw::abi::UInt64,
out_seed: *mut crate::raw::abi::UInt64,
) -> OSStatus {
let Some(object) = (unsafe { DriverObject::from_ref(driver) }) else {
return OsStatus::BAD_OBJECT.as_i32();
};
let runtime = object.runtime();
let period_frames = runtime.objects().spec().sample_rate();
let sample_rate = runtime.state().sample_rate;
let host_ticks_per_period = platform::host_ticks_per_period(period_frames, sample_rate);
let now = platform::host_time_now();
let Some(timestamp) = runtime
.clock()
.zero_timestamp(now, host_ticks_per_period, period_frames)
else {
return OsStatus::NOT_RUNNING.as_i32();
};
if !out_sample_time.is_null() {
unsafe { *out_sample_time = timestamp.sample_time };
}
if !out_host_time.is_null() {
unsafe { *out_host_time = timestamp.host_time };
}
if !out_seed.is_null() {
unsafe { *out_seed = timestamp.seed };
}
OsStatus::OK.as_i32()
}
fn framework_handles(operation: IoOperation) -> bool {
matches!(operation, IoOperation::READ_INPUT | IoOperation::WRITE_MIX)
}
pub unsafe extern "C" fn will_do_io_operation(
driver: AudioServerPlugInDriverRef,
_device_id: AudioObjectID,
_client_id: UInt32,
operation_id: UInt32,
out_will_do: *mut Boolean,
out_will_do_in_place: *mut Boolean,
) -> OSStatus {
let Some(_object) = (unsafe { DriverObject::from_ref(driver) }) else {
return OsStatus::BAD_OBJECT.as_i32();
};
let handled = framework_handles(IoOperation::from(FourCharCode::from_u32(operation_id)));
if !out_will_do.is_null() {
unsafe { *out_will_do = Boolean::from(handled) };
}
if !out_will_do_in_place.is_null() {
unsafe { *out_will_do_in_place = 1 };
}
OsStatus::OK.as_i32()
}
#[allow(clippy::too_many_arguments)] pub unsafe extern "C" fn do_io_operation(
driver: AudioServerPlugInDriverRef,
_device_id: AudioObjectID,
_stream_id: AudioObjectID,
_client_id: UInt32,
operation_id: UInt32,
io_buffer_frame_size: UInt32,
io_cycle_info: *const AudioServerPlugInIOCycleInfo,
io_main_buffer: *mut c_void,
_io_secondary_buffer: *mut c_void,
) -> OSStatus {
let Some(object) = (unsafe { DriverObject::from_ref(driver) }) else {
return OsStatus::BAD_OBJECT.as_i32();
};
let operation = IoOperation::from(FourCharCode::from_u32(operation_id));
if !framework_handles(operation) {
return OsStatus::OK.as_i32();
}
let runtime = object.runtime();
let ring = runtime.ring();
let channels = ring.channels();
let sample_count = io_buffer_frame_size as usize * channels;
if sample_count > MAX_CYCLE_SAMPLES {
return OsStatus::UNSPECIFIED.as_i32();
}
let timestamp = match unsafe { io_cycle_info.as_ref() } {
Some(cycle) if operation == IoOperation::READ_INPUT => {
marshal::timestamp_from_raw(&cycle.mInputTime)
}
Some(cycle) => marshal::timestamp_from_raw(&cycle.mOutputTime),
None => crate::io::Timestamp::ZERO,
};
let sample_time = timestamp.sample_time;
let rt = unsafe { RealtimeContext::new_unchecked() };
let mut scratch = [0.0_f32; MAX_CYCLE_SAMPLES];
let scratch = &mut scratch[..sample_count];
let result = match operation {
IoOperation::WRITE_MIX => {
let client = unsafe { in_samples(io_main_buffer, sample_count) };
let mut io = IoBuffer::new(timestamp, operation, client, scratch);
let result = runtime.driver().process_io(&rt, &mut io);
if result.is_ok() {
ring.write_from(sample_time, io.output);
}
result
}
_ => {
ring.read_into(sample_time, scratch);
let client = unsafe { out_samples(io_main_buffer, sample_count) };
let mut io = IoBuffer::new(timestamp, operation, scratch, client);
runtime.driver().process_io(&rt, &mut io)
}
};
OsStatus::from(result).as_i32()
}
pub unsafe extern "C" fn begin_io_operation(
driver: AudioServerPlugInDriverRef,
_device_id: AudioObjectID,
_client_id: UInt32,
_operation_id: UInt32,
_io_buffer_frame_size: UInt32,
_io_cycle_info: *const crate::raw::abi::AudioServerPlugInIOCycleInfo,
) -> OSStatus {
match unsafe { DriverObject::from_ref(driver) } {
Some(_) => OsStatus::OK.as_i32(),
None => OsStatus::BAD_OBJECT.as_i32(),
}
}
pub unsafe extern "C" fn end_io_operation(
driver: AudioServerPlugInDriverRef,
_device_id: AudioObjectID,
_client_id: UInt32,
_operation_id: UInt32,
_io_buffer_frame_size: UInt32,
_io_cycle_info: *const crate::raw::abi::AudioServerPlugInIOCycleInfo,
) -> OSStatus {
match unsafe { DriverObject::from_ref(driver) } {
Some(_) => OsStatus::OK.as_i32(),
None => OsStatus::BAD_OBJECT.as_i32(),
}
}
pub unsafe extern "C" fn add_device_client(
driver: AudioServerPlugInDriverRef,
_device_id: AudioObjectID,
_client_info: *const crate::raw::abi::AudioServerPlugInClientInfo,
) -> OSStatus {
match unsafe { DriverObject::from_ref(driver) } {
Some(_) => OsStatus::OK.as_i32(),
None => OsStatus::BAD_OBJECT.as_i32(),
}
}
pub unsafe extern "C" fn remove_device_client(
driver: AudioServerPlugInDriverRef,
_device_id: AudioObjectID,
_client_info: *const crate::raw::abi::AudioServerPlugInClientInfo,
) -> OSStatus {
match unsafe { DriverObject::from_ref(driver) } {
Some(_) => OsStatus::OK.as_i32(),
None => OsStatus::BAD_OBJECT.as_i32(),
}
}
pub unsafe extern "C" fn has_property(
driver: AudioServerPlugInDriverRef,
object_id: AudioObjectID,
_client_pid: crate::raw::abi::pid_t,
address: *const AudioObjectPropertyAddress,
) -> Boolean {
let (Some(object), Some(raw_address)) =
(unsafe { (DriverObject::from_ref(driver), address.as_ref()) })
else {
return 0;
};
let runtime = object.runtime();
let address = marshal::address_from_raw(raw_address);
let has = dispatch::get_property_data(
runtime.objects(),
runtime.info(),
&runtime.state(),
AudioObjectId::from_u32(object_id),
&address,
)
.is_ok();
Boolean::from(has)
}
pub unsafe extern "C" fn is_property_settable(
driver: AudioServerPlugInDriverRef,
object_id: AudioObjectID,
_client_pid: crate::raw::abi::pid_t,
address: *const AudioObjectPropertyAddress,
out_is_settable: *mut Boolean,
) -> OSStatus {
let (Some(object), Some(raw_address)) =
(unsafe { (DriverObject::from_ref(driver), address.as_ref()) })
else {
return OsStatus::BAD_OBJECT.as_i32();
};
let runtime = object.runtime();
let address = marshal::address_from_raw(raw_address);
match dispatch::is_property_settable(
runtime.objects(),
runtime.info(),
&runtime.state(),
AudioObjectId::from_u32(object_id),
&address,
) {
Ok(settable) => {
if !out_is_settable.is_null() {
unsafe { *out_is_settable = Boolean::from(settable) };
}
OsStatus::OK.as_i32()
}
Err(status) => status.as_i32(),
}
}
pub unsafe extern "C" fn get_property_data_size(
driver: AudioServerPlugInDriverRef,
object_id: AudioObjectID,
_client_pid: crate::raw::abi::pid_t,
address: *const AudioObjectPropertyAddress,
_qualifier_size: UInt32,
_qualifier_data: *const c_void,
out_data_size: *mut UInt32,
) -> OSStatus {
let (Some(object), Some(raw_address)) =
(unsafe { (DriverObject::from_ref(driver), address.as_ref()) })
else {
return OsStatus::BAD_OBJECT.as_i32();
};
let runtime = object.runtime();
let address = marshal::address_from_raw(raw_address);
match dispatch::property_data_size(
runtime.objects(),
runtime.info(),
&runtime.state(),
AudioObjectId::from_u32(object_id),
&address,
) {
Ok(size) => {
if !out_data_size.is_null() {
unsafe { *out_data_size = size as UInt32 };
}
OsStatus::OK.as_i32()
}
Err(status) => status.as_i32(),
}
}
#[allow(clippy::too_many_arguments)] pub unsafe extern "C" fn get_property_data(
driver: AudioServerPlugInDriverRef,
object_id: AudioObjectID,
_client_pid: crate::raw::abi::pid_t,
address: *const AudioObjectPropertyAddress,
_qualifier_size: UInt32,
_qualifier_data: *const c_void,
data_size: UInt32,
out_data_size: *mut UInt32,
out_data: *mut c_void,
) -> OSStatus {
let (Some(object), Some(raw_address)) =
(unsafe { (DriverObject::from_ref(driver), address.as_ref()) })
else {
return OsStatus::BAD_OBJECT.as_i32();
};
let runtime = object.runtime();
let address = marshal::address_from_raw(raw_address);
let value = match dispatch::get_property_data(
runtime.objects(),
runtime.info(),
&runtime.state(),
AudioObjectId::from_u32(object_id),
&address,
) {
Ok(value) => value,
Err(status) => return status.as_i32(),
};
let written = if let crate::property::PropertyValue::Text(text) = &value {
write_cfstring(text, out_data, data_size as usize)
} else {
let buffer = unsafe { out_slice(out_data, data_size) };
marshal::write_property_value(&value, buffer)
};
match written {
Ok(size) => {
if !out_data_size.is_null() {
unsafe { *out_data_size = size as UInt32 };
}
OsStatus::OK.as_i32()
}
Err(status) => status.as_i32(),
}
}
#[allow(clippy::too_many_arguments)] pub unsafe extern "C" fn set_property_data(
driver: AudioServerPlugInDriverRef,
object_id: AudioObjectID,
_client_pid: crate::raw::abi::pid_t,
address: *const AudioObjectPropertyAddress,
_qualifier_size: UInt32,
_qualifier_data: *const c_void,
data_size: UInt32,
data: *const c_void,
) -> OSStatus {
let (Some(object), Some(raw_address)) =
(unsafe { (DriverObject::from_ref(driver), address.as_ref()) })
else {
return OsStatus::BAD_OBJECT.as_i32();
};
let runtime = object.runtime();
let address = marshal::address_from_raw(raw_address);
let buffer = unsafe { in_slice(data, data_size) };
let mut state = runtime.state();
let result = dispatch::set_property_data(
runtime.objects(),
runtime.info(),
&mut state,
AudioObjectId::from_u32(object_id),
&address,
buffer,
);
OsStatus::from(result).as_i32()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::driver::{AnyDriver, Driver, DriverInstance};
use crate::property::{PropertySelector, PropertyValue};
use crate::raw::runtime::DriverRuntime;
use crate::{DeviceSpec, IoBuffer, RealtimeContext, StreamFormat, StreamSpec};
use std::sync::Arc;
struct Loopback;
impl Driver for Loopback {
const NAME: &'static str = "tympan-aspl entry fixture";
const MANUFACTURER: &'static str = "tympan-aspl";
const VERSION: &'static str = "0.0.0";
fn new() -> Self {
Self
}
fn device(&self) -> DeviceSpec {
let format = StreamFormat::float32(48_000.0, 2);
DeviceSpec::new("com.tympan.test.entry", "Entry Fixture", Self::MANUFACTURER)
.with_input(StreamSpec::input(format))
.with_output(StreamSpec::output(format))
}
fn process_io(&mut self, _rt: &RealtimeContext, buffer: &mut IoBuffer<'_>) {
let n = buffer.output.len().min(buffer.input.len());
buffer.output[..n].copy_from_slice(&buffer.input[..n]);
buffer.output[n..].fill(0.0);
}
}
struct Fixture {
driver_ref: AudioServerPlugInDriverRef,
}
impl Fixture {
fn new() -> Self {
let driver: Arc<dyn AnyDriver> = Arc::new(DriverInstance::<Loopback>::new());
let object = Box::new(DriverObject::new(
crate::raw::vtable::driver_interface(),
DriverRuntime::new(driver),
));
let raw = Box::into_raw(object);
Self {
driver_ref: raw.cast(),
}
}
fn this(&self) -> *mut c_void {
self.driver_ref.cast()
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let remaining = unsafe { release(self.this()) };
assert_eq!(remaining, 0, "fixture leaked a reference");
}
}
fn global(selector: PropertySelector) -> AudioObjectPropertyAddress {
marshal::address_to_raw(&crate::property::PropertyAddress::global(selector))
}
#[test]
fn query_interface_accepts_the_driver_and_iunknown_uuids() {
let fixture = Fixture::new();
for uuid in [ASPL_DRIVER_INTERFACE_UUID, IUNKNOWN_UUID] {
let mut out: *mut c_void = core::ptr::null_mut();
let status = unsafe { query_interface(fixture.this(), uuid, &mut out) };
assert_eq!(status, OsStatus::OK.as_i32());
assert_eq!(out, fixture.this());
}
unsafe {
release(fixture.this());
release(fixture.this());
}
}
#[test]
fn query_interface_rejects_an_unknown_uuid() {
let fixture = Fixture::new();
let bogus = CFUUIDBytes {
byte0: 0xFF,
..CFUUIDBytes::default()
};
let mut out: *mut c_void = 0x1 as *mut c_void;
let status = unsafe { query_interface(fixture.this(), bogus, &mut out) };
assert_eq!(status, E_NOINTERFACE);
assert!(out.is_null());
}
#[test]
fn add_ref_and_release_track_the_count() {
let fixture = Fixture::new();
unsafe {
assert_eq!(add_ref(fixture.this()), 2);
assert_eq!(add_ref(fixture.this()), 3);
assert_eq!(release(fixture.this()), 2);
assert_eq!(release(fixture.this()), 1);
}
}
#[test]
fn null_driver_ref_is_handled() {
let null = core::ptr::null_mut::<c_void>();
unsafe {
assert_eq!(add_ref(null), 0);
assert_eq!(release(null), 0);
assert_eq!(
initialize(null.cast(), core::ptr::null_mut()),
OsStatus::BAD_OBJECT.as_i32()
);
let addr = global(PropertySelector::CLASS);
assert_eq!(has_property(null.cast(), 1, 0, &addr), 0);
}
}
#[test]
fn initialize_start_stop_drive_the_lifecycle() {
let fixture = Fixture::new();
unsafe {
assert_eq!(
initialize(fixture.driver_ref, core::ptr::null_mut()),
OsStatus::OK.as_i32()
);
let object = DriverObject::from_ref(fixture.driver_ref).unwrap();
assert!(!object.runtime().state().running);
assert_eq!(start_io(fixture.driver_ref, 2, 0), OsStatus::OK.as_i32());
assert!(object.runtime().state().running);
assert_eq!(stop_io(fixture.driver_ref, 2, 0), OsStatus::OK.as_i32());
assert!(!object.runtime().state().running);
}
}
#[test]
fn initialize_records_the_host_pointer() {
let fixture = Fixture::new();
let fake_host = 0xCAFE_usize as AudioServerPlugInHostRef;
unsafe {
assert_eq!(
initialize(fixture.driver_ref, fake_host),
OsStatus::OK.as_i32()
);
let object = DriverObject::from_ref(fixture.driver_ref).unwrap();
assert_eq!(object.runtime().host(), fake_host);
}
}
#[test]
fn has_property_distinguishes_known_and_unknown() {
let fixture = Fixture::new();
let known = global(PropertySelector::CLASS);
let unknown = global(PropertySelector::new(*b"zzzz"));
unsafe {
assert_eq!(has_property(fixture.driver_ref, 1, 0, &known), 1);
assert_eq!(has_property(fixture.driver_ref, 1, 0, &unknown), 0);
assert_eq!(has_property(fixture.driver_ref, 999, 0, &known), 0);
}
}
#[test]
fn is_property_settable_reports_the_sample_rate_as_writable() {
let fixture = Fixture::new();
let rate = global(PropertySelector::DEVICE_NOMINAL_SAMPLE_RATE);
let uid = global(PropertySelector::DEVICE_UID);
let mut settable: Boolean = 0xFF;
unsafe {
assert_eq!(
is_property_settable(fixture.driver_ref, 2, 0, &rate, &mut settable),
OsStatus::OK.as_i32()
);
assert_eq!(settable, 1);
assert_eq!(
is_property_settable(fixture.driver_ref, 2, 0, &uid, &mut settable),
OsStatus::OK.as_i32()
);
assert_eq!(settable, 0);
}
}
#[test]
fn get_property_data_size_and_data_agree_for_a_scalar() {
let fixture = Fixture::new();
let rate = global(PropertySelector::DEVICE_NOMINAL_SAMPLE_RATE);
let mut size: UInt32 = 0;
let mut buffer = [0u8; 8];
let mut written: UInt32 = 0;
unsafe {
assert_eq!(
get_property_data_size(
fixture.driver_ref,
2,
0,
&rate,
0,
core::ptr::null(),
&mut size,
),
OsStatus::OK.as_i32()
);
assert_eq!(size, 8);
assert_eq!(
get_property_data(
fixture.driver_ref,
2,
0,
&rate,
0,
core::ptr::null(),
buffer.len() as UInt32,
&mut written,
buffer.as_mut_ptr().cast(),
),
OsStatus::OK.as_i32()
);
assert_eq!(written, 8);
}
assert_eq!(f64::from_ne_bytes(buffer), 48_000.0);
}
#[test]
fn get_property_data_reads_an_object_list() {
let fixture = Fixture::new();
let device_list = global(PropertySelector::PLUGIN_DEVICE_LIST);
let mut buffer = [0u8; 4];
let mut written: UInt32 = 0;
unsafe {
assert_eq!(
get_property_data(
fixture.driver_ref,
1,
0,
&device_list,
0,
core::ptr::null(),
buffer.len() as UInt32,
&mut written,
buffer.as_mut_ptr().cast(),
),
OsStatus::OK.as_i32()
);
}
assert_eq!(written, 4);
assert_eq!(u32::from_ne_bytes(buffer), 2);
}
#[test]
fn get_property_data_rejects_an_unknown_property() {
let fixture = Fixture::new();
let unknown = global(PropertySelector::new(*b"zzzz"));
let mut buffer = [0u8; 8];
let mut written: UInt32 = 0;
let status = unsafe {
get_property_data(
fixture.driver_ref,
1,
0,
&unknown,
0,
core::ptr::null(),
buffer.len() as UInt32,
&mut written,
buffer.as_mut_ptr().cast(),
)
};
assert_eq!(status, OsStatus::UNKNOWN_PROPERTY.as_i32());
}
fn name_property_dispatches_to_text(fixture: &Fixture) {
let object = unsafe { DriverObject::from_ref(fixture.driver_ref) }.unwrap();
let runtime = object.runtime();
let value = dispatch::get_property_data(
runtime.objects(),
runtime.info(),
&runtime.state(),
AudioObjectId::PLUGIN,
&crate::property::PropertyAddress::global(PropertySelector::NAME),
);
assert!(matches!(value, Ok(PropertyValue::Text(_))));
}
#[cfg(not(target_os = "macos"))]
#[test]
fn text_properties_report_unspecified_off_macos() {
let fixture = Fixture::new();
let name = global(PropertySelector::NAME);
let mut buffer = [0u8; 8];
let mut written: UInt32 = 0;
let status = unsafe {
get_property_data(
fixture.driver_ref,
1,
0,
&name,
0,
core::ptr::null(),
buffer.len() as UInt32,
&mut written,
buffer.as_mut_ptr().cast(),
)
};
assert_eq!(status, OsStatus::UNSPECIFIED.as_i32());
name_property_dispatches_to_text(&fixture);
}
#[cfg(target_os = "macos")]
#[test]
fn text_properties_marshal_a_cfstring_on_macos() {
let fixture = Fixture::new();
let name = global(PropertySelector::NAME);
let mut cfstr: crate::raw::cf::CFStringRef = core::ptr::null();
let mut written: UInt32 = 0;
let status = unsafe {
get_property_data(
fixture.driver_ref,
1,
0,
&name,
0,
core::ptr::null(),
core::mem::size_of::<crate::raw::cf::CFStringRef>() as UInt32,
&mut written,
core::ptr::addr_of_mut!(cfstr).cast(),
)
};
assert_eq!(status, OsStatus::OK.as_i32());
assert_eq!(
written as usize,
core::mem::size_of::<crate::raw::cf::CFStringRef>()
);
assert!(
!cfstr.is_null(),
"the buffer should hold a live CFStringRef"
);
unsafe { crate::raw::cf::release(cfstr) };
name_property_dispatches_to_text(&fixture);
}
#[test]
fn set_property_data_writes_the_sample_rate() {
let fixture = Fixture::new();
let rate = global(PropertySelector::DEVICE_NOMINAL_SAMPLE_RATE);
let payload = 48_000.0_f64.to_ne_bytes();
let status = unsafe {
set_property_data(
fixture.driver_ref,
2,
0,
&rate,
0,
core::ptr::null(),
payload.len() as UInt32,
payload.as_ptr().cast(),
)
};
assert_eq!(status, OsStatus::OK.as_i32());
let payload = 12_345.0_f64.to_ne_bytes();
let status = unsafe {
set_property_data(
fixture.driver_ref,
2,
0,
&rate,
0,
core::ptr::null(),
payload.len() as UInt32,
payload.as_ptr().cast(),
)
};
assert_eq!(status, OsStatus::UNSUPPORTED_FORMAT.as_i32());
}
#[test]
fn add_and_remove_device_client_succeed() {
let fixture = Fixture::new();
unsafe {
assert_eq!(
add_device_client(fixture.driver_ref, 2, core::ptr::null()),
OsStatus::OK.as_i32()
);
assert_eq!(
remove_device_client(fixture.driver_ref, 2, core::ptr::null()),
OsStatus::OK.as_i32()
);
}
}
#[test]
fn start_io_anchors_the_clock_and_stop_io_halts_it() {
let fixture = Fixture::new();
let object = unsafe { DriverObject::from_ref(fixture.driver_ref) }.unwrap();
assert!(!object.runtime().clock().is_running());
unsafe {
initialize(fixture.driver_ref, core::ptr::null_mut());
assert_eq!(start_io(fixture.driver_ref, 2, 0), OsStatus::OK.as_i32());
}
assert!(object.runtime().clock().is_running());
assert_eq!(object.runtime().clock().seed(), 1);
unsafe {
assert_eq!(stop_io(fixture.driver_ref, 2, 0), OsStatus::OK.as_i32());
}
assert!(!object.runtime().clock().is_running());
}
#[test]
fn get_zero_time_stamp_requires_a_running_device() {
let fixture = Fixture::new();
let mut sample_time = 0.0;
let mut host_time = 0u64;
let mut seed = 0u64;
let status = unsafe {
get_zero_time_stamp(
fixture.driver_ref,
2,
0,
&mut sample_time,
&mut host_time,
&mut seed,
)
};
assert_eq!(status, OsStatus::NOT_RUNNING.as_i32());
}
#[test]
fn get_zero_time_stamp_reports_a_timeline_once_running() {
let fixture = Fixture::new();
unsafe {
initialize(fixture.driver_ref, core::ptr::null_mut());
start_io(fixture.driver_ref, 2, 0);
}
let mut sample_time = -1.0;
let mut host_time = 0u64;
let mut seed = 0u64;
let status = unsafe {
get_zero_time_stamp(
fixture.driver_ref,
2,
0,
&mut sample_time,
&mut host_time,
&mut seed,
)
};
assert_eq!(status, OsStatus::OK.as_i32());
assert!(sample_time >= 0.0);
assert_eq!(seed, 1);
}
#[test]
fn get_zero_time_stamp_tolerates_null_out_pointers() {
let fixture = Fixture::new();
unsafe {
initialize(fixture.driver_ref, core::ptr::null_mut());
start_io(fixture.driver_ref, 2, 0);
assert_eq!(
get_zero_time_stamp(
fixture.driver_ref,
2,
0,
core::ptr::null_mut(),
core::ptr::null_mut(),
core::ptr::null_mut(),
),
OsStatus::OK.as_i32()
);
}
}
#[test]
fn will_do_io_operation_declines_every_operation_for_now() {
let fixture = Fixture::new();
let mut will_do: Boolean = 0xFF;
let mut will_do_in_place: Boolean = 0xFF;
let status = unsafe {
will_do_io_operation(
fixture.driver_ref,
2,
0,
crate::io::IoOperation::PROCESS_OUTPUT.code().as_u32(),
&mut will_do,
&mut will_do_in_place,
)
};
assert_eq!(status, OsStatus::OK.as_i32());
assert_eq!(will_do, 0);
}
#[test]
fn begin_and_end_io_operation_succeed() {
let fixture = Fixture::new();
unsafe {
assert_eq!(
begin_io_operation(fixture.driver_ref, 2, 0, 0, 512, core::ptr::null()),
OsStatus::OK.as_i32()
);
assert_eq!(
end_io_operation(fixture.driver_ref, 2, 0, 0, 512, core::ptr::null()),
OsStatus::OK.as_i32()
);
}
}
#[test]
fn io_timing_entry_points_reject_a_null_driver() {
let null = core::ptr::null_mut::<c_void>().cast();
let mut scratch = 0u64;
let mut scratch_f = 0.0;
let mut flag: Boolean = 0;
unsafe {
assert_eq!(
get_zero_time_stamp(null, 2, 0, &mut scratch_f, &mut scratch, &mut scratch),
OsStatus::BAD_OBJECT.as_i32()
);
assert_eq!(
will_do_io_operation(null, 2, 0, 0, &mut flag, &mut flag),
OsStatus::BAD_OBJECT.as_i32()
);
assert_eq!(
begin_io_operation(null, 2, 0, 0, 0, core::ptr::null()),
OsStatus::BAD_OBJECT.as_i32()
);
assert_eq!(
end_io_operation(null, 2, 0, 0, 0, core::ptr::null()),
OsStatus::BAD_OBJECT.as_i32()
);
assert_eq!(
do_io_operation(
null,
2,
3,
0,
IoOperation::WRITE_MIX.code().as_u32(),
128,
core::ptr::null(),
core::ptr::null_mut(),
core::ptr::null_mut(),
),
OsStatus::BAD_OBJECT.as_i32()
);
}
}
fn cycle_at(t: f64) -> AudioServerPlugInIOCycleInfo {
let mut info = AudioServerPlugInIOCycleInfo::default();
info.mInputTime.mSampleTime = t;
info.mOutputTime.mSampleTime = t;
info
}
#[test]
fn will_do_io_operation_claims_only_the_data_operations() {
let fixture = Fixture::new();
let mut will_do: Boolean = 0xFF;
let mut in_place: Boolean = 0;
unsafe {
for op in [IoOperation::READ_INPUT, IoOperation::WRITE_MIX] {
will_do_io_operation(
fixture.driver_ref,
2,
0,
op.code().as_u32(),
&mut will_do,
&mut in_place,
);
assert_eq!(will_do, 1, "{op:?} should be handled");
}
for op in [
IoOperation::CYCLE,
IoOperation::PROCESS_OUTPUT,
IoOperation::MIX_OUTPUT,
] {
will_do_io_operation(
fixture.driver_ref,
2,
0,
op.code().as_u32(),
&mut will_do,
&mut in_place,
);
assert_eq!(will_do, 0, "{op:?} should be declined");
}
}
}
#[test]
fn do_io_operation_round_trips_audio_through_the_device_ring() {
let fixture = Fixture::new();
unsafe {
initialize(fixture.driver_ref, core::ptr::null_mut());
start_io(fixture.driver_ref, 2, 0);
}
let written = [0.1_f32, -0.2, 0.3, -0.4, 0.5, -0.6, 0.7, -0.8];
let cycle = cycle_at(64.0);
let status = unsafe {
do_io_operation(
fixture.driver_ref,
2,
4, 0,
IoOperation::WRITE_MIX.code().as_u32(),
4, &cycle,
written.as_ptr() as *mut c_void,
core::ptr::null_mut(),
)
};
assert_eq!(status, OsStatus::OK.as_i32());
let mut read = [0.0_f32; 8];
let status = unsafe {
do_io_operation(
fixture.driver_ref,
2,
3, 0,
IoOperation::READ_INPUT.code().as_u32(),
4,
&cycle,
read.as_mut_ptr().cast(),
core::ptr::null_mut(),
)
};
assert_eq!(status, OsStatus::OK.as_i32());
assert_eq!(read, written, "what was written must read back");
}
#[test]
fn do_io_operation_reads_silence_from_an_untouched_ring() {
let fixture = Fixture::new();
unsafe {
initialize(fixture.driver_ref, core::ptr::null_mut());
start_io(fixture.driver_ref, 2, 0);
}
let cycle = cycle_at(1_000.0);
let mut read = [9.0_f32; 8];
let status = unsafe {
do_io_operation(
fixture.driver_ref,
2,
3,
0,
IoOperation::READ_INPUT.code().as_u32(),
4,
&cycle,
read.as_mut_ptr().cast(),
core::ptr::null_mut(),
)
};
assert_eq!(status, OsStatus::OK.as_i32());
assert!(
read.iter().all(|&s| s == 0.0),
"an untouched ring is silent"
);
}
#[test]
fn do_io_operation_declines_an_oversized_cycle() {
let fixture = Fixture::new();
unsafe {
initialize(fixture.driver_ref, core::ptr::null_mut());
start_io(fixture.driver_ref, 2, 0);
}
let cycle = cycle_at(0.0);
let huge_frames = (MAX_CYCLE_SAMPLES / 2 + 1) as UInt32;
let status = unsafe {
do_io_operation(
fixture.driver_ref,
2,
4,
0,
IoOperation::WRITE_MIX.code().as_u32(),
huge_frames,
&cycle,
core::ptr::null_mut(),
core::ptr::null_mut(),
)
};
assert_eq!(status, OsStatus::UNSPECIFIED.as_i32());
}
#[test]
fn do_io_operation_ignores_an_operation_it_does_not_handle() {
let fixture = Fixture::new();
unsafe {
initialize(fixture.driver_ref, core::ptr::null_mut());
start_io(fixture.driver_ref, 2, 0);
}
let cycle = cycle_at(0.0);
let status = unsafe {
do_io_operation(
fixture.driver_ref,
2,
4,
0,
IoOperation::MIX_OUTPUT.code().as_u32(),
4,
&cycle,
core::ptr::null_mut(),
core::ptr::null_mut(),
)
};
assert_eq!(status, OsStatus::OK.as_i32());
}
}