use std::sync::Arc;
use std::sync::{
Mutex,
atomic::{AtomicBool, AtomicU8, Ordering},
};
use block2::RcBlock;
use objc2::rc::Retained;
use objc2::runtime::Bool;
use objc2_avf_audio::{AVAudioApplication, AVAudioApplicationRecordPermission};
use objc2_avf_audio::{AVAudioSession, AVAudioSessionCategory};
use objc2_foundation::NSError;
use oxisound_core::{OxiSoundError, SessionCategory};
fn category_to_av(category: SessionCategory) -> Option<&'static AVAudioSessionCategory> {
unsafe {
match category {
SessionCategory::Playback => objc2_avf_audio::AVAudioSessionCategoryPlayback,
SessionCategory::Record => objc2_avf_audio::AVAudioSessionCategoryRecord,
SessionCategory::PlayAndRecord => objc2_avf_audio::AVAudioSessionCategoryPlayAndRecord,
SessionCategory::Ambient => objc2_avf_audio::AVAudioSessionCategoryAmbient,
SessionCategory::SoloAmbient => objc2_avf_audio::AVAudioSessionCategorySoloAmbient,
}
}
}
pub(crate) fn configure_session(category: SessionCategory) -> Result<(), OxiSoundError> {
static LOCK: Mutex<()> = Mutex::new(());
let _guard = LOCK.lock().unwrap_or_else(|e| e.into_inner());
let av_cat = category_to_av(category).ok_or_else(|| {
OxiSoundError::UnsupportedConfig(format!(
"AVAudioSessionCategory constant unavailable for {category:?}"
))
})?;
let session: Retained<AVAudioSession> = unsafe { AVAudioSession::sharedInstance() };
let result: Result<(), Retained<NSError>> = unsafe { session.setCategory_error(av_cat) };
result.map_err(|ns_err| {
let desc = ns_err.localizedDescription();
OxiSoundError::FormatMismatch(format!("AVAudioSession setCategory failed: {desc}"))
})
}
pub(crate) fn request_microphone_permission() -> Result<bool, OxiSoundError> {
let app: Retained<AVAudioApplication> = unsafe { AVAudioApplication::sharedInstance() };
let permission: AVAudioApplicationRecordPermission = unsafe { app.recordPermission() };
if permission == AVAudioApplicationRecordPermission::Granted {
return Ok(true);
}
if permission == AVAudioApplicationRecordPermission::Denied {
return Ok(false);
}
let result = Arc::new(AtomicU8::new(0));
let done = Arc::new(AtomicBool::new(false));
let result_clone = Arc::clone(&result);
let done_clone = Arc::clone(&done);
let block = RcBlock::new(move |granted: Bool| {
result_clone.store(if granted.as_bool() { 1 } else { 2 }, Ordering::Release);
done_clone.store(true, Ordering::Release);
});
unsafe {
AVAudioApplication::requestRecordPermissionWithCompletionHandler(&block);
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
while !done.load(Ordering::Acquire) {
if std::time::Instant::now() >= deadline {
return Err(OxiSoundError::Timeout(
"microphone permission request timed out after 30 seconds".into(),
));
}
std::hint::spin_loop();
}
Ok(result.load(Ordering::Acquire) == 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn category_mapping_is_exhaustive() {
let categories = [
SessionCategory::Playback,
SessionCategory::Record,
SessionCategory::PlayAndRecord,
SessionCategory::Ambient,
SessionCategory::SoloAmbient,
];
for cat in categories {
let result = category_to_av(cat);
assert!(
result.is_some(),
"category_to_av returned None for {cat:?} — SDK too old?"
);
}
}
#[test]
#[cfg(target_os = "macos")]
fn configure_session_playback_no_panic() {
let result = configure_session(SessionCategory::Playback);
match result {
Ok(()) => {}
Err(OxiSoundError::FormatMismatch(_)) => {}
Err(OxiSoundError::UnsupportedConfig(_)) => {}
Err(e) => panic!("unexpected error: {e:?}"),
}
}
}