#![allow(unsafe_code)]
use std::collections::VecDeque;
use std::ptr::NonNull;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Arc, Mutex};
use crate::audio::{AudioCapture, AudioCaptureConfig};
use crate::{CaptureError, Select};
use block2::RcBlock;
use mediaway_common::{AudioFrame, Bytes, CodecKind, Rational, SampleFormat, StreamInfo};
use objc2::rc::Retained;
use objc2_avf_audio::{AVAudioEngine, AVAudioPCMBuffer, AVAudioTime};
const TAP_BUFFER_FRAMES: u32 = 4096;
struct SharedQueue {
frames: Mutex<VecDeque<AudioFrame>>,
}
const PCM_QUEUE_CAP: usize = 64;
struct MicSession {
stream_info: StreamInfo,
queue: Arc<SharedQueue>,
engine: Retained<AVAudioEngine>,
_tap_block: RcBlock<dyn Fn(NonNull<AVAudioPCMBuffer>, NonNull<AVAudioTime>)>,
}
pub struct AppleMicrophoneCapture {
inner: Option<MicSession>,
}
impl AppleMicrophoneCapture {
pub fn open(config: &AudioCaptureConfig) -> Result<Self, CaptureError> {
if config.select != Select::Default {
return Err(CaptureError::Unsupported);
}
if config.sample_format != SampleFormat::F32 {
return Err(CaptureError::Unsupported);
}
if config.time_base.den == 0 {
return Err(CaptureError::InvalidInput);
}
let engine = unsafe { AVAudioEngine::new() };
let input = unsafe { engine.inputNode() };
let format = unsafe { input.outputFormatForBus(0) };
let (sample_rate, channels) = unsafe { (format.sampleRate(), format.channelCount()) };
if sample_rate <= 0.0 || channels == 0 {
return Err(CaptureError::Backend);
}
let channels_usize = channels as usize;
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "sample_rate > 0.0 checked above; real sample rates are small positive integers"
)]
let sample_rate_u32 = sample_rate as u32;
let queue = Arc::new(SharedQueue {
frames: Mutex::new(VecDeque::new()),
});
let queue_tap = Arc::clone(&queue);
let next_pts = Arc::new(AtomicI64::new(0));
let tap_block: RcBlock<dyn Fn(NonNull<AVAudioPCMBuffer>, NonNull<AVAudioTime>)> =
RcBlock::new(
move |buf: NonNull<AVAudioPCMBuffer>, _when: NonNull<AVAudioTime>| {
let buf = unsafe { buf.as_ref() };
let frame_length = unsafe { buf.frameLength() } as usize;
if frame_length == 0 {
return;
}
let channel_ptrs = unsafe { buf.floatChannelData() };
if channel_ptrs.is_null() {
return;
}
let interleaved =
unsafe { interleave_pcm_f32(channel_ptrs, channels_usize, frame_length) };
let pts = next_pts
.fetch_add(i64::try_from(frame_length).unwrap_or(0), Ordering::Relaxed);
push_frame(
&queue_tap,
sample_rate_u32,
channels_usize,
pts,
&interleaved,
);
},
);
unsafe {
input.installTapOnBus_bufferSize_format_block(
0,
TAP_BUFFER_FRAMES,
None,
RcBlock::as_ptr(&tap_block),
);
}
unsafe { engine.startAndReturnError() }.map_err(|_| CaptureError::Backend)?;
let info = StreamInfo::Audio {
id: 0,
codec: CodecKind::RawAudio,
time_base: config.time_base,
sample_rate: sample_rate_u32,
channels: u16::try_from(channels).unwrap_or(0),
extra_data: Bytes::new(),
};
Ok(Self {
inner: Some(MicSession {
stream_info: info,
queue,
engine,
_tap_block: tap_block,
}),
})
}
}
impl AudioCapture for AppleMicrophoneCapture {
fn stream_info(&self) -> &StreamInfo {
#[allow(
clippy::option_if_let_else,
reason = "map_or_else forces 'static vs 'self lifetime clash"
)]
if let Some(s) = self.inner.as_ref() {
&s.stream_info
} else {
closed_audio_info()
}
}
fn poll_frame(&mut self) -> Result<Option<AudioFrame>, CaptureError> {
let Some(session) = self.inner.as_ref() else {
return Err(CaptureError::Closed);
};
let mut q = session
.queue
.frames
.lock()
.map_err(|_| CaptureError::Backend)?;
Ok(q.pop_front())
}
fn close(&mut self) -> Result<(), CaptureError> {
let Some(session) = self.inner.take() else {
return Ok(());
};
unsafe { session.engine.stop() };
Ok(())
}
}
impl Drop for AppleMicrophoneCapture {
fn drop(&mut self) {
let _ = self.close();
}
}
fn closed_audio_info() -> &'static StreamInfo {
use std::sync::OnceLock;
static INFO: OnceLock<StreamInfo> = OnceLock::new();
INFO.get_or_init(|| StreamInfo::Audio {
id: 0,
codec: CodecKind::RawAudio,
time_base: Rational::new(1, 48_000),
sample_rate: 0,
channels: 0,
extra_data: Bytes::new(),
})
}
unsafe fn interleave_pcm_f32(
channel_ptrs: *mut NonNull<f32>,
channels: usize,
frame_length: usize,
) -> Vec<f32> {
let mut out = vec![0f32; frame_length * channels];
for ch in 0..channels {
let channel_ptr = unsafe { *channel_ptrs.add(ch) };
let channel_slice =
unsafe { std::slice::from_raw_parts(channel_ptr.as_ptr(), frame_length) };
for (frame, &sample) in channel_slice.iter().enumerate() {
out[frame * channels + ch] = sample;
}
}
out
}
fn push_frame(queue: &SharedQueue, sample_rate: u32, channels: usize, pts: i64, samples: &[f32]) {
let frame = AudioFrame {
pts,
duration: u64::try_from(samples.len() / channels.max(1)).unwrap_or(0),
sample_rate,
channels: u16::try_from(channels).unwrap_or(0),
format: SampleFormat::F32,
data: bytes_from_f32_vec(samples),
};
if let Ok(mut q) = queue.frames.lock() {
if q.len() >= PCM_QUEUE_CAP {
let _ = q.pop_front();
}
q.push_back(frame);
}
}
fn bytes_from_f32_vec(samples: &[f32]) -> Bytes {
let bytes = unsafe {
std::slice::from_raw_parts(
samples.as_ptr().cast::<u8>(),
std::mem::size_of_val(samples),
)
};
Bytes::copy_from_slice(bytes)
}
#[cfg(test)]
#[path = "mic_tests.rs"]
mod tests;