use std::cell::RefCell;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::ptr::NonNull;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use block2::RcBlock;
use objc2::rc::Retained;
use objc2::AnyThread;
use objc2_core_audio::{
kAudioAggregateDeviceIsPrivateKey, kAudioAggregateDeviceIsStackedKey,
kAudioAggregateDeviceNameKey, kAudioAggregateDeviceTapAutoStartKey,
kAudioAggregateDeviceTapListKey, kAudioAggregateDeviceUIDKey, kAudioSubTapDriftCompensationKey,
kAudioSubTapUIDKey, AudioDeviceCreateIOProcIDWithBlock, AudioDeviceDestroyIOProcID,
AudioDeviceIOProcID, AudioDeviceStart, AudioDeviceStop, AudioHardwareCreateAggregateDevice,
AudioHardwareCreateProcessTap, AudioHardwareDestroyAggregateDevice,
AudioHardwareDestroyProcessTap, AudioObjectID, CATapDescription,
};
use objc2_core_audio_types::{AudioBufferList, AudioTimeStamp};
use objc2_core_foundation::CFDictionary;
use objc2_foundation::{NSArray, NSDictionary, NSNumber, NSObject, NSString};
use flexaudio_core::backend::RawSink;
use flexaudio_core::types::Error;
use crate::common::{
map_os_status, now_ns, tap_format_is_float, tap_native_format, FALLBACK_FORMAT, NO_ERR,
};
pub(crate) enum TapKind {
IncludeProcesses(Vec<AudioObjectID>),
ExcludeProcesses(Vec<AudioObjectID>),
ExcludeProcessesOnDevice {
ids: Vec<AudioObjectID>,
device_uid: String,
},
}
#[allow(clippy::type_complexity)]
pub(crate) struct TapChain {
aggregate_id: AudioObjectID,
io_proc_id: AudioDeviceIOProcID,
tap_id: AudioObjectID,
stopped: Arc<AtomicBool>,
_block: RcBlock<
dyn Fn(
NonNull<AudioTimeStamp>,
NonNull<AudioBufferList>,
NonNull<AudioTimeStamp>,
NonNull<AudioBufferList>,
NonNull<AudioTimeStamp>,
),
>,
_desc: Retained<CATapDescription>,
}
impl Drop for TapChain {
fn drop(&mut self) {
self.stopped.store(true, Ordering::Release);
unsafe {
if self.io_proc_id.is_some() {
let _ = AudioDeviceStop(self.aggregate_id, self.io_proc_id);
let _ = AudioDeviceDestroyIOProcID(self.aggregate_id, self.io_proc_id);
}
if self.aggregate_id != 0 {
let _ = AudioHardwareDestroyAggregateDevice(self.aggregate_id);
}
if self.tap_id != 0 {
let _ = AudioHardwareDestroyProcessTap(self.tap_id);
}
}
}
}
fn object_ids_to_nsarray(ids: &[AudioObjectID]) -> Retained<NSArray<NSNumber>> {
let numbers: Vec<Retained<NSNumber>> = ids
.iter()
.map(|&id| NSNumber::numberWithUnsignedInt(id))
.collect();
NSArray::from_retained_slice(&numbers)
}
fn cstr_key(key: &std::ffi::CStr) -> Retained<NSString> {
NSString::from_str(key.to_str().unwrap_or(""))
}
pub(crate) unsafe fn build_tap_chain(
kind: TapKind,
name: &str,
sink: RawSink,
) -> Result<TapChain, Error> {
let desc: Retained<CATapDescription> = match &kind {
TapKind::IncludeProcesses(ids) => {
let arr = object_ids_to_nsarray(ids);
CATapDescription::initStereoMixdownOfProcesses(CATapDescription::alloc(), &arr)
}
TapKind::ExcludeProcesses(ids) => {
let arr = object_ids_to_nsarray(ids);
CATapDescription::initStereoGlobalTapButExcludeProcesses(
CATapDescription::alloc(),
&arr,
)
}
TapKind::ExcludeProcessesOnDevice { ids, device_uid } => {
let arr = object_ids_to_nsarray(ids);
let uid = NSString::from_str(device_uid);
CATapDescription::initExcludingProcesses_andDeviceUID_withStream(
CATapDescription::alloc(),
&arr,
&uid,
0,
)
}
};
desc.setName(&NSString::from_str(name));
desc.setPrivate(true);
let uuid_str: Retained<NSString> = desc.UUID().UUIDString();
let mut tap_id: AudioObjectID = 0;
let status = AudioHardwareCreateProcessTap(Some(&desc), &mut tap_id as *mut AudioObjectID);
if status != NO_ERR {
return Err(map_os_status("AudioHardwareCreateProcessTap", status));
}
if tap_id == 0 {
return Err(Error::Backend(
"AudioHardwareCreateProcessTap returned null tap id".into(),
));
}
if std::env::var_os("FLEXAUDIO_DEBUG").is_some() {
match tap_native_format(tap_id) {
Some((rate, ch)) => eprintln!(
"[flexaudio-os-macos] tap ASBD: rate={rate} channels={ch} (fallback would be {FALLBACK_FORMAT:?})"
),
None => eprintln!(
"[flexaudio-os-macos] tap ASBD unavailable; using fallback {FALLBACK_FORMAT:?}"
),
}
}
if let Some(false) = tap_format_is_float(tap_id) {
let _ = unsafe { AudioHardwareDestroyProcessTap(tap_id) };
return Err(Error::Backend(
"tap format is not float (kAudioFormatFlagIsFloat unset); IOProc reads f32".into(),
));
}
let aggregate_id = match create_aggregate_device(name, &uuid_str) {
Ok(id) => id,
Err(e) => {
let _ = AudioHardwareDestroyProcessTap(tap_id);
return Err(e);
}
};
let sink_cell = RefCell::new(sink);
let (native_rate, native_ch) = tap_native_format(tap_id).unwrap_or(FALLBACK_FORMAT);
let max_scratch = ((native_rate as usize / 10).max(1)) * (native_ch as usize).max(1);
let scratch_cell = RefCell::new({
let mut v: Vec<f32> = Vec::new();
v.reserve_exact(max_scratch);
v
});
let stopped = Arc::new(AtomicBool::new(false));
let stopped_for_block = stopped.clone();
let block = RcBlock::new(
move |_in_now: NonNull<AudioTimeStamp>,
in_input: NonNull<AudioBufferList>,
_in_input_time: NonNull<AudioTimeStamp>,
_out: NonNull<AudioBufferList>,
_out_time: NonNull<AudioTimeStamp>| {
let _ = catch_unwind(AssertUnwindSafe(|| {
if stopped_for_block.load(Ordering::Acquire) {
return;
}
if let Ok(mut sink) = sink_cell.try_borrow_mut() {
if let Ok(mut scratch) = scratch_cell.try_borrow_mut() {
unsafe { push_buffer_list(&mut sink, &mut scratch, in_input.as_ptr()) };
}
}
}));
},
);
let mut io_proc_id: AudioDeviceIOProcID = None;
let status = AudioDeviceCreateIOProcIDWithBlock(
NonNull::from(&mut io_proc_id),
aggregate_id,
None,
RcBlock::as_ptr(&block),
);
if status != NO_ERR || io_proc_id.is_none() {
let _ = AudioHardwareDestroyAggregateDevice(aggregate_id);
let _ = AudioHardwareDestroyProcessTap(tap_id);
return Err(map_os_status("AudioDeviceCreateIOProcIDWithBlock", status));
}
let status = AudioDeviceStart(aggregate_id, io_proc_id);
if status != NO_ERR {
let _ = AudioDeviceDestroyIOProcID(aggregate_id, io_proc_id);
let _ = AudioHardwareDestroyAggregateDevice(aggregate_id);
let _ = AudioHardwareDestroyProcessTap(tap_id);
return Err(map_os_status("AudioDeviceStart", status));
}
Ok(TapChain {
aggregate_id,
io_proc_id,
tap_id,
stopped,
_block: block,
_desc: desc,
})
}
fn create_aggregate_device(name: &str, sub_tap_uid: &NSString) -> Result<AudioObjectID, Error> {
let drift_true = NSNumber::numberWithBool(true);
let sub_tap: Retained<NSDictionary<NSString, NSObject>> = NSDictionary::from_slices::<NSString>(
&[
&cstr_key(kAudioSubTapUIDKey),
&cstr_key(kAudioSubTapDriftCompensationKey),
],
&[sub_tap_uid.as_ref(), drift_true.as_ref()],
);
let tap_list: Retained<NSArray<NSObject>> =
NSArray::from_retained_slice(&[Retained::into_super(sub_tap)]);
let agg_uid = NSString::from_str(&new_uuid_string());
let agg_name = NSString::from_str(name);
let is_private = NSNumber::numberWithBool(true);
let is_stacked = NSNumber::numberWithBool(false);
let tap_auto_start = NSNumber::numberWithBool(true);
let keys: [&NSString; 6] = [
&cstr_key(kAudioAggregateDeviceNameKey),
&cstr_key(kAudioAggregateDeviceUIDKey),
&cstr_key(kAudioAggregateDeviceIsPrivateKey),
&cstr_key(kAudioAggregateDeviceIsStackedKey),
&cstr_key(kAudioAggregateDeviceTapAutoStartKey),
&cstr_key(kAudioAggregateDeviceTapListKey),
];
let values: [&NSObject; 6] = [
agg_name.as_ref(),
agg_uid.as_ref(),
is_private.as_ref(),
is_stacked.as_ref(),
tap_auto_start.as_ref(),
tap_list.as_ref(),
];
let dict: Retained<NSDictionary<NSString, NSObject>> =
NSDictionary::from_slices::<NSString>(&keys, &values);
let cf: &CFDictionary = unsafe { &*(Retained::as_ptr(&dict) as *const CFDictionary) };
let mut device_id: AudioObjectID = 0;
let status = unsafe { AudioHardwareCreateAggregateDevice(cf, NonNull::from(&mut device_id)) };
if status != NO_ERR {
return Err(map_os_status("AudioHardwareCreateAggregateDevice", status));
}
if device_id == 0 {
return Err(Error::Backend(
"AudioHardwareCreateAggregateDevice returned null device id".into(),
));
}
Ok(device_id)
}
fn new_uuid_string() -> String {
use objc2_foundation::NSUUID;
NSUUID::new().UUIDString().to_string()
}
unsafe fn push_buffer_list(
sink: &mut RawSink,
scratch: &mut Vec<f32>,
list: *const AudioBufferList,
) {
if list.is_null() {
return;
}
let num_buffers = (*list).mNumberBuffers as usize;
if num_buffers == 0 {
return;
}
log_buffer_shape_once(num_buffers);
let buffers = std::slice::from_raw_parts((*list).mBuffers.as_ptr(), num_buffers);
if num_buffers == 1 {
let buf = &buffers[0];
let n = buf.mDataByteSize as usize / core::mem::size_of::<f32>();
if n == 0 || buf.mData.is_null() {
return;
}
let slice = std::slice::from_raw_parts(buf.mData as *const f32, n);
sink.push(slice, now_ns());
return;
}
let channels = num_buffers;
let mut min_frames = usize::MAX;
for b in buffers.iter() {
if b.mData.is_null() {
return;
}
let frames = b.mDataByteSize as usize / core::mem::size_of::<f32>();
min_frames = min_frames.min(frames);
}
if min_frames == 0 || min_frames == usize::MAX {
return;
}
let total = min_frames * channels;
scratch.resize(total, 0.0);
for (ch, buf) in buffers.iter().enumerate() {
let src = std::slice::from_raw_parts(buf.mData as *const f32, min_frames);
let mut idx = ch;
for &s in src.iter() {
scratch[idx] = s;
idx += channels;
}
}
sink.push(&scratch[..total], now_ns());
}
thread_local! {
static LOGGED_SHAPE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
fn log_buffer_shape_once(num_buffers: usize) {
if std::env::var_os("FLEXAUDIO_DEBUG").is_none() {
return;
}
LOGGED_SHAPE.with(|c| {
if !c.get() {
c.set(true);
let kind = if num_buffers == 1 {
"interleaved"
} else {
"planar"
};
eprintln!(
"[flexaudio-os-macos] IOProc buffer shape: mNumberBuffers={num_buffers} ({kind})"
);
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uuid_string_has_expected_shape() {
let s = new_uuid_string();
assert_eq!(s.len(), 36);
assert_eq!(s.matches('-').count(), 4);
}
#[test]
fn object_ids_array_preserves_count() {
let arr = object_ids_to_nsarray(&[1, 2, 3]);
assert_eq!(arr.count(), 3);
}
}