diff --git a/CHANGELOG.md b/CHANGELOG.md index e0830a23d..36eb36d06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **AudioWorklet**: Fix stale audio output when a data callback wrote a partial buffer. - **CoreAudio**: Default-output streams now report xrun status. - **CoreAudio**: Fix stale audio output when a data callback wrote a partial buffer. +- **iOS**: Streams now resume automatically when an audio session interruption ends. - **JACK**: Streams with more channels than physical system ports no longer fail to build. - **JACK**: Channel enumeration is no longer capped at the physical system port count. - **JACK**: `Device::id` no longer embeds the process ID, so `DeviceId` is now stable across application restarts. diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index dffd433ea..8f8dc0b8c 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -138,6 +138,40 @@ impl Device { } } +#[derive(Clone, Copy, Default, PartialEq, Eq)] +enum PlaybackState { + #[default] + Stopped, + Playing, + Interrupted, +} + +struct StreamInner { + state: PlaybackState, + audio_unit: AudioUnit, +} + +impl StreamInner { + fn stop_for_interruption(&mut self) { + if self.state != PlaybackState::Playing { + return; + } + // Unlike pause(), the OS is the one that actually halted the unit here; this + // call only resyncs AudioUnit's own bookkeeping, so its result carries nothing. + let _ = self.audio_unit.stop(); + self.state = PlaybackState::Interrupted; + } + + fn resume_after_interruption(&mut self) { + if self.state != PlaybackState::Interrupted { + return; + } + if self.audio_unit.start().is_ok() { + self.state = PlaybackState::Playing; + } + } +} + impl DeviceTrait for Device { type SupportedInputConfigs = SupportedInputConfigs; type SupportedOutputConfigs = SupportedOutputConfigs; @@ -190,11 +224,8 @@ impl DeviceTrait for Device { let latency_frames = Arc::new(AtomicUsize::new(input_latency_frames())); let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); - let session_manager = SessionEventManager::new( - error_callback.clone(), - Latch::new(), - Some((latency_frames.clone(), true)), - ); + let session_error_callback = error_callback.clone(); + let session_latency_frames = latency_frames.clone(); // Set up input callback setup_input_callback( @@ -208,13 +239,20 @@ impl DeviceTrait for Device { }, )?; - let stream = Stream::new( - StreamInner { - playing: false, - audio_unit, - }, - session_manager, + let inner = Arc::new(Mutex::new(StreamInner { + state: PlaybackState::default(), + audio_unit, + })); + let session_manager = SessionEventManager::new( + session_error_callback, + Latch::new(), + Some((session_latency_frames, true)), + Arc::downgrade(&inner), ); + let stream = Stream { + inner, + session_manager, + }; stream.signal_ready(); Ok(stream) } @@ -245,11 +283,8 @@ impl DeviceTrait for Device { let latency_frames = Arc::new(AtomicUsize::new(output_latency_frames())); let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); - let session_manager = SessionEventManager::new( - error_callback.clone(), - Latch::new(), - Some((latency_frames.clone(), false)), - ); + let session_error_callback = error_callback.clone(); + let session_latency_frames = latency_frames.clone(); // Set up output callback setup_output_callback( @@ -263,31 +298,31 @@ impl DeviceTrait for Device { }, )?; - let stream = Stream::new( - StreamInner { - playing: false, - audio_unit, - }, - session_manager, + let inner = Arc::new(Mutex::new(StreamInner { + state: PlaybackState::default(), + audio_unit, + })); + let session_manager = SessionEventManager::new( + session_error_callback, + Latch::new(), + Some((session_latency_frames, false)), + Arc::downgrade(&inner), ); + let stream = Stream { + inner, + session_manager, + }; stream.signal_ready(); Ok(stream) } } pub struct Stream { - inner: Mutex, + inner: Arc>, session_manager: SessionEventManager, } impl Stream { - fn new(inner: StreamInner, session_manager: SessionEventManager) -> Self { - Self { - inner: Mutex::new(inner), - session_manager, - } - } - fn signal_ready(&self) { self.session_manager.signal_ready(); } @@ -305,12 +340,12 @@ impl StreamTrait for Stream { let mut stream = self.inner.lock().map_err(|_| { Error::with_message(ErrorKind::StreamInvalidated, "Stream lock poisoned") })?; - if !stream.playing { + if stream.state != PlaybackState::Playing { stream .audio_unit .start() .context("Failed to start audio unit")?; - stream.playing = true; + stream.state = PlaybackState::Playing; } Ok(()) } @@ -319,13 +354,13 @@ impl StreamTrait for Stream { let mut stream = self.inner.lock().map_err(|_| { Error::with_message(ErrorKind::StreamInvalidated, "Stream lock poisoned") })?; - if stream.playing { + if stream.state == PlaybackState::Playing { stream .audio_unit .stop() .context("Failed to stop audio unit")?; - stream.playing = false; } + stream.state = PlaybackState::Stopped; Ok(()) } @@ -339,11 +374,6 @@ impl StreamTrait for Stream { } } -struct StreamInner { - playing: bool, - audio_unit: AudioUnit, -} - fn create_audio_unit() -> Result { AudioUnit::new_uninitialized(coreaudio::audio_unit::IOType::RemoteIO) } diff --git a/src/host/coreaudio/ios/session_event_manager.rs b/src/host/coreaudio/ios/session_event_manager.rs index 5bab1d889..ea5c6f5b5 100644 --- a/src/host/coreaudio/ios/session_event_manager.rs +++ b/src/host/coreaudio/ios/session_event_manager.rs @@ -1,39 +1,53 @@ -//! Monitors AVAudioSession lifecycle events and reports them as stream errors. +//! Monitors AVAudioSession lifecycle events, recovering the stream from an interruption and +//! reporting the rest as stream errors. use std::{ ptr::NonNull, sync::{ atomic::{AtomicUsize, Ordering}, - Arc, + Arc, Mutex, Weak, }, }; use block2::RcBlock; -use objc2::runtime::AnyObject; use objc2_avf_audio::{ - AVAudioSessionMediaServicesWereLostNotification, + AVAudioSession, AVAudioSessionInterruptionNotification, AVAudioSessionInterruptionOptionKey, + AVAudioSessionInterruptionOptions, AVAudioSessionInterruptionType, + AVAudioSessionInterruptionTypeKey, AVAudioSessionMediaServicesWereLostNotification, AVAudioSessionMediaServicesWereResetNotification, AVAudioSessionRouteChangeNotification, AVAudioSessionRouteChangeReason, AVAudioSessionRouteChangeReasonKey, }; use objc2_foundation::{NSNotification, NSNotificationCenter, NSNumber, NSString}; -use super::{input_latency_frames, output_latency_frames}; +use super::{input_latency_frames, output_latency_frames, StreamInner}; use crate::{ host::{emit_error, latch::Latch, ErrorCallbackArc}, Error, ErrorKind, }; +/// Runs `f` against the stream, if it is still alive and its lock is intact. +fn with_stream(stream: &Weak>, f: impl FnOnce(&mut StreamInner)) { + if let Some(inner) = stream.upgrade() { + if let Ok(mut inner) = inner.lock() { + f(&mut inner); + } + } +} + +/// Reads the number stored under `key` in a notification's `userInfo`. +fn user_info_number(notification: &NSNotification, key: Option<&NSString>) -> Option { + let user_info = notification.userInfo()?; + let value = user_info.objectForKey(key?)?; + Some(value.downcast::().ok()?.unsignedIntegerValue()) +} + /// Shared buffer-depth value to refresh on route changes, paired with `is_input` to select the /// input or output latency. `true` means an input stream. type LatencyRefresh = (Arc, bool); -unsafe fn route_change_error(notification: &NSNotification) -> Option { - let user_info = notification.userInfo()?; - let key = AVAudioSessionRouteChangeReasonKey?; - let dict = unsafe { user_info.cast_unchecked::() }; - let value = dict.objectForKey(key)?; - let number = value.downcast_ref::()?; - let reason = AVAudioSessionRouteChangeReason(number.unsignedIntegerValue()); +fn route_change_error(notification: &NSNotification) -> Option { + let key = unsafe { AVAudioSessionRouteChangeReasonKey }; + let reason = AVAudioSessionRouteChangeReason(user_info_number(notification, key)?); match reason { AVAudioSessionRouteChangeReason::OldDeviceUnavailable => Some(Error::with_message( ErrorKind::DeviceChanged, @@ -73,11 +87,49 @@ impl SessionEventManager { error_callback: ErrorCallbackArc, latch: Latch, latency_refresh: Option, + stream: Weak>, ) -> Self { let nc = NSNotificationCenter::defaultCenter(); let mut observers = Vec::new(); let waiter = latch.waiter(); + // The OS stops the unit itself, and the session it stops is inactive on the way back. + { + let w = waiter.clone(); + let stream = stream.clone(); + let block = RcBlock::new(move |notif: NonNull| { + if !w.is_released() { + return; + } + let notif = unsafe { notif.as_ref() }; + let interruption_type_key = unsafe { AVAudioSessionInterruptionTypeKey }; + let Some(kind) = user_info_number(notif, interruption_type_key) else { + return; + }; + if AVAudioSessionInterruptionType(kind) == AVAudioSessionInterruptionType::Began { + with_stream(&stream, StreamInner::stop_for_interruption); + return; + } + let interruption_option_key = unsafe { AVAudioSessionInterruptionOptionKey }; + let options = AVAudioSessionInterruptionOptions( + user_info_number(notif, interruption_option_key).unwrap_or(0), + ); + if !options.contains(AVAudioSessionInterruptionOptions::ShouldResume) { + return; + } + let session = unsafe { AVAudioSession::sharedInstance() }; + if unsafe { session.setActive_error(true) }.is_ok() { + with_stream(&stream, StreamInner::resume_after_interruption); + } + }); + if let Some(name) = unsafe { AVAudioSessionInterruptionNotification } { + let observer = unsafe { + nc.addObserverForName_object_queue_usingBlock(Some(name), None, None, &block) + }; + observers.push(observer); + } + } + { let cb = error_callback.clone(); let w = waiter.clone(); @@ -93,7 +145,8 @@ impl SessionEventManager { }; frames.store(depth, Ordering::Relaxed); } - if let Some(err) = unsafe { route_change_error(notif.as_ref()) } { + let notif = unsafe { notif.as_ref() }; + if let Some(err) = route_change_error(notif) { emit_error(&cb, err); } }