use alloc::boxed::Box;
use alloc::format;
use alloc::string::ToString;
use core::cell::RefCell;
use core::mem::{size_of, zeroed};
use core::ptr::{NonNull, addr_of_mut};
use core::slice;
use anyhow::Context;
use block2::StackBlock;
use dispatch2::{DispatchQueue, DispatchQueueAttr};
use log::info;
use objc2::__framework_prelude::Retained;
use objc2::AnyThread;
use objc2::rc::RetainedFromIterator;
use objc2_avf_audio::{AVAudioFormat, AVAudioPCMBuffer};
use objc2_core_audio::{
AudioDeviceCreateIOProcIDWithBlock, AudioDeviceDestroyIOProcID, AudioDeviceIOBlock,
AudioDeviceIOProcID, AudioDeviceStart, AudioDeviceStop, AudioHardwareCreateProcessTap,
AudioHardwareDestroyAggregateDevice, AudioHardwareDestroyProcessTap, AudioObjectID,
CATapDescription, kAudioAggregateDeviceIsPrivateKey, kAudioAggregateDeviceNameKey,
kAudioAggregateDeviceTapListKey, kAudioAggregateDeviceUIDKey,
kAudioHardwarePropertyTranslatePIDToProcessObject, kAudioObjectSystemObject,
kAudioSubTapUIDKey, kAudioTapPropertyFormat,
};
use objc2_core_audio_types::{AudioBufferList, AudioStreamBasicDescription, AudioTimeStamp};
use objc2_core_foundation::{CFDictionary, CFRetained};
use objc2_foundation::{NSArray, NSNumber, NSUUID};
use uuid::Uuid;
use crate::common::audiofox::AUDIOFOX;
use crate::common::builder::ChunkBuilder;
use crate::common::callback::AudioCallback;
use crate::common::chunk::Chunk;
use crate::common::result::Result;
use crate::common::source::AudioSource;
use crate::platform::mac::wrapper::{DictBuilder, create_aggregate, get_prop_data};
pub struct StreamState {
tap_id: AudioObjectID,
aggregate_device_id: AudioObjectID,
device_proc_id: AudioDeviceIOProcID,
}
pub fn start_stream(
uuid: Uuid,
sources: &[AudioSource],
callback: AudioCallback,
) -> Result<StreamState> {
let audio_object_ids = sources_to_aoids(sources)?;
let (tap_id, tap_uuid) = create_process_tap(uuid, &audio_object_ids)?;
let aggregate_device_id = create_aggregate_device(&tap_uuid.to_string())?;
let audio_format = get_audio_format(tap_id)?;
let builder = RefCell::from(ChunkBuilder::new_u8());
let block = StackBlock::new(
move |_in_now: NonNull<AudioTimeStamp>,
in_input_data: NonNull<AudioBufferList>,
_in_input_time: NonNull<AudioTimeStamp>,
_out_output_data: NonNull<AudioBufferList>,
_in_output_time: NonNull<AudioTimeStamp>| {
if let Some(pcm_buffer) = unsafe {
AVAudioPCMBuffer::initWithPCMFormat_bufferListNoCopy_deallocator(
AVAudioPCMBuffer::alloc(), &audio_format, in_input_data, None,
)
} {
let data = unsafe { pcm_buffer.audioBufferList().as_ref() };
let mut builder = builder.borrow_mut();
for i in 0..data.mNumberBuffers as usize {
let buffer = &data.mBuffers[i];
let sample_slice = unsafe {
slice::from_raw_parts(
buffer.mData as *const u8,
buffer.mDataByteSize as usize,
)
};
if let Some(data) = builder.extend_from_slice(sample_slice) {
callback(Chunk::from(&*builder));
builder.reset_with_overflow(data);
}
}
}
},
);
let queue = DispatchQueue::new(AUDIOFOX, DispatchQueueAttr::concurrent());
let mut device_proc_id: AudioDeviceIOProcID = None;
check_status!(unsafe {
AudioDeviceCreateIOProcIDWithBlock(
NonNull::from(&mut device_proc_id),
aggregate_device_id,
Some(&queue),
AudioDeviceIOBlock::from(&raw const block as *mut _),
)
}).context("Failed to create audio process loop")?;
check_status!(unsafe { AudioDeviceStart(aggregate_device_id, device_proc_id) })
.context("Failed to start audio process loop")?;
info!("stream successfully started");
Ok(StreamState { tap_id, aggregate_device_id, device_proc_id })
}
pub fn stop_stream(state: &StreamState) -> Result<()> {
if state.device_proc_id.is_some() {
check_status!(unsafe { AudioDeviceStop(state.aggregate_device_id, state.device_proc_id)})
.context("Failed to stop the audio process loop")?;
check_status!(unsafe { AudioDeviceDestroyIOProcID(state.aggregate_device_id, state.device_proc_id) })
.context("Failed to destroy audio process loop")?;
}
if state.aggregate_device_id != 0 {
check_status!(unsafe { AudioHardwareDestroyAggregateDevice(state.aggregate_device_id) })
.context("Failed to destroy aggregate device")?;
}
if state.tap_id != 0 {
check_status!(unsafe { AudioHardwareDestroyProcessTap(state.tap_id) })
.context("Failed to destroy process tap")?;
}
Ok(())
}
fn sources_to_aoids(sources: &[AudioSource]) -> Result<Box<[AudioObjectID]>> {
sources.iter().map(|source| {
let mut object_id: AudioObjectID = 0;
get_prop_data(
kAudioObjectSystemObject as AudioObjectID,
&query!(kAudioHardwarePropertyTranslatePIDToProcessObject),
qual!(source.id[0]),
size_of::<AudioObjectID>() as u32,
addr_of_mut!(object_id),
).context("Failed to translate PID to AudioObjectID")?;
Ok(object_id)
}).collect()
}
fn create_process_tap(
uuid: Uuid,
process_object_ids: &[AudioObjectID],
) -> Result<(AudioObjectID, Retained<NSUUID>)> {
unsafe {
let process_nums = process_object_ids.iter().map(|id| NSNumber::new_u32(*id));
let process_array = NSArray::retained_from_iter(process_nums);
let tap_desc = CATapDescription::initStereoMixdownOfProcesses(
CATapDescription::alloc(),
&process_array,
);
let tap_uuid = NSUUID::from_bytes(uuid.into_bytes());
tap_desc.setUUID(&tap_uuid);
let mut tap_id: AudioObjectID = 0;
check_status!(AudioHardwareCreateProcessTap(
Some(&*tap_desc),
&raw mut tap_id
)).context("Failed to create process tap")?;
Ok((tap_id, tap_uuid))
}
}
fn create_aggregate_device(tap_uid: &str) -> Result<AudioObjectID> {
let tap_dict: CFRetained<CFDictionary> = DictBuilder::<1>::new()
.insert(kAudioSubTapUIDKey, tap_uid)
.build()
.context("Failed to create tap dictionary")?;
let aggregate_dict = DictBuilder::<4>::new()
.insert(kAudioAggregateDeviceNameKey, AUDIOFOX)
.insert(kAudioAggregateDeviceUIDKey, &*format!("{AUDIOFOX}-{tap_uid}"))
.insert(kAudioAggregateDeviceTapListKey, tap_dict)
.insert(kAudioAggregateDeviceIsPrivateKey, true)
.build()
.context("Failed to create aggregate dictionary")?;
create_aggregate(&aggregate_dict).context("Failed to create aggregate device")
}
fn get_audio_format(tap_id: AudioObjectID) -> Result<Retained<AVAudioFormat>> {
let mut stream_format: AudioStreamBasicDescription = unsafe { zeroed() };
get_prop_data(
tap_id,
&query!(kAudioTapPropertyFormat),
None,
size_of::<AudioStreamBasicDescription>() as u32,
addr_of_mut!(stream_format),
).context("Failed to get stream format")?;
info!("capturing at rate:{}, channels:{}", stream_format.mSampleRate, stream_format.mChannelsPerFrame);
let av_format = unsafe {
AVAudioFormat::initWithStreamDescription(
AVAudioFormat::alloc(),
NonNull::from(&stream_format),
)
}.context("Failed to create AVAudioFormat")?;
Ok(av_format)
}