use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
use objc::runtime::Object;
#[allow(unused_imports)]
use objc::{msg_send, sel, sel_impl};
use tracing::debug;
use super::devices::check_microphone_permission;
use super::ffi::{ns_string_to_rust, objc_class, release_objc_object};
use super::{AudioData, AudioError, CHANNELS, MAX_CAPTURE_SECS, SAMPLE_RATE};
pub(super) struct CaptureState {
pub(super) samples: Vec<i16>,
pub(super) done: bool,
}
pub fn validate_duration(duration_secs: f32) -> Result<(), AudioError> {
if duration_secs > MAX_CAPTURE_SECS {
Err(AudioError::DurationExceeded {
requested: duration_secs,
max: MAX_CAPTURE_SECS,
})
} else {
Ok(())
}
}
pub fn capture_microphone(duration_secs: f32) -> Result<AudioData, AudioError> {
validate_duration(duration_secs)?;
check_microphone_permission()?;
debug!(duration = duration_secs, "capturing microphone audio");
capture_via_av_audio_engine(duration_secs)
}
pub fn capture_system_audio(duration_secs: f32) -> Result<AudioData, AudioError> {
validate_duration(duration_secs)?;
check_microphone_permission()?;
debug!(duration = duration_secs, "capturing system audio output");
capture_via_av_audio_engine(duration_secs)
}
fn capture_via_av_audio_engine(duration_secs: f32) -> Result<AudioData, AudioError> {
let state = Arc::new((
Mutex::new(CaptureState {
samples: Vec::new(),
done: false,
}),
Condvar::new(),
));
let deadline = Instant::now() + Duration::from_secs_f32(duration_secs);
let engine = create_av_audio_engine()
.ok_or_else(|| AudioError::Framework("Failed to create AVAudioEngine".to_string()))?;
let native_rate = query_input_sample_rate(engine);
let state_clone = Arc::clone(&state);
install_input_tap(
engine,
native_rate,
CHANNELS,
move |pcm_samples: &[f32]| {
let (lock, _cvar) = &*state_clone;
if let Ok(mut guard) = lock.lock() {
if !guard.done {
for &s in pcm_samples {
#[allow(clippy::cast_possible_truncation)]
guard.samples.push((s.clamp(-1.0, 1.0) * 32767.0) as i16);
}
}
}
},
)
.map_err(AudioError::Framework)?;
start_av_audio_engine(engine).map_err(AudioError::Framework)?;
let remaining = deadline.saturating_duration_since(Instant::now());
std::thread::sleep(remaining);
stop_av_audio_engine(engine);
release_objc_object(engine);
let (lock, _) = &*state;
let mut guard = lock
.lock()
.map_err(|_| AudioError::Framework("Lock poisoned".to_string()))?;
guard.done = true;
let samples_i16 = std::mem::take(&mut guard.samples);
let samples_f32: Vec<f32> = samples_i16
.iter()
.map(|&s| f32::from(s) / 32767.0)
.collect();
#[allow(clippy::cast_precision_loss)]
let actual_duration = samples_f32.len() as f32 / native_rate as f32;
Ok(AudioData {
samples: samples_f32,
sample_rate: native_rate,
channels: CHANNELS,
duration_secs: actual_duration.min(duration_secs),
})
}
fn query_input_sample_rate(engine: *mut Object) -> u32 {
let input_node: *mut Object = unsafe { msg_send![engine, inputNode] };
if input_node.is_null() {
return SAMPLE_RATE;
}
let format: *mut Object = unsafe { msg_send![input_node, outputFormatForBus: 0u32] };
if format.is_null() {
return SAMPLE_RATE;
}
let rate: f64 = unsafe { msg_send![format, sampleRate] };
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let rate_u32 = rate as u32;
if rate_u32 == 0 {
SAMPLE_RATE
} else {
rate_u32
}
}
fn create_av_audio_engine() -> Option<*mut Object> {
let cls = objc_class("AVAudioEngine");
if cls.is_null() {
return None;
}
let engine: *mut Object = unsafe { msg_send![cls, new] };
if engine.is_null() {
None
} else {
Some(engine)
}
}
fn install_input_tap(
engine: *mut Object,
_sample_rate: u32,
_channels: u16,
callback: impl Fn(&[f32]) + Send + 'static,
) -> Result<(), String> {
let input_node: *mut Object = unsafe { msg_send![engine, inputNode] };
if input_node.is_null() {
return Err("AVAudioEngine.inputNode is nil".to_string());
}
let cb = Arc::new(Mutex::new(callback));
let tap_block = block::ConcreteBlock::new(move |buffer: *mut Object, _time: *mut Object| {
if buffer.is_null() {
return;
}
let float_channels: *mut *mut f32 = unsafe { msg_send![buffer, floatChannelData] };
if float_channels.is_null() {
return;
}
let frame_count: u32 = unsafe { msg_send![buffer, frameLength] };
let samples = unsafe { std::slice::from_raw_parts(*float_channels, frame_count as usize) };
if let Ok(f) = cb.lock() {
f(samples);
}
})
.copy();
unsafe {
let _: () = msg_send![input_node,
installTapOnBus: 0u32
bufferSize: 4096u32
format: std::ptr::null_mut::<Object>()
block: &*tap_block
];
}
Ok(())
}
fn start_av_audio_engine(engine: *mut Object) -> Result<(), String> {
let mut error: *mut Object = std::ptr::null_mut();
let ok: bool = unsafe { msg_send![engine, startAndReturnError: &mut error] };
if ok {
return Ok(());
}
let msg = if error.is_null() {
"AVAudioEngine start failed (unknown error)".to_string()
} else {
let desc: *mut Object = unsafe { msg_send![error, localizedDescription] };
ns_string_to_rust(desc)
};
Err(msg)
}
fn stop_av_audio_engine(engine: *mut Object) {
if engine.is_null() {
return;
}
let input_node: *mut Object = unsafe { msg_send![engine, inputNode] };
if !input_node.is_null() {
unsafe {
let _: () = msg_send![input_node, removeTapOnBus: 0u32];
}
}
unsafe {
let _: () = msg_send![engine, stop];
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_duration_accepts_minimum() {
assert!(validate_duration(0.1).is_ok());
}
#[test]
fn validate_duration_accepts_max() {
assert!(validate_duration(MAX_CAPTURE_SECS).is_ok());
}
#[test]
fn validate_duration_rejects_over_max() {
let result = validate_duration(30.1);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.code(), "duration_exceeded");
}
#[test]
fn validate_duration_rejects_large_value() {
let err = validate_duration(3600.0).unwrap_err();
assert_eq!(err.code(), "duration_exceeded");
}
}