diff --git a/CHANGELOG.md b/CHANGELOG.md index e0830a23d..a1bc8bfb5 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 stopped by an audio session interruption now resume when it ends, instead of staying silent until rebuilt. - **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..7164a5b4b 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -190,11 +190,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 +205,21 @@ impl DeviceTrait for Device { }, )?; - let stream = Stream::new( - StreamInner { - playing: false, - audio_unit, - }, - session_manager, + let inner = Arc::new(Mutex::new(StreamInner { + playing: false, + interrupted: false, + 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 +250,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 +265,32 @@ impl DeviceTrait for Device { }, )?; - let stream = Stream::new( - StreamInner { - playing: false, - audio_unit, - }, - session_manager, + let inner = Arc::new(Mutex::new(StreamInner { + playing: false, + interrupted: false, + 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(); } @@ -341,9 +344,33 @@ impl StreamTrait for Stream { struct StreamInner { playing: bool, + /// Set while an interruption is what stopped the unit, so only those resume on its end. + interrupted: bool, audio_unit: AudioUnit, } +impl StreamInner { + /// The OS already stopped the unit; resync `playing` so a later `play` actually starts it. + fn stop_for_interruption(&mut self) { + if !self.playing { + return; + } + let _ = self.audio_unit.stop(); + self.playing = false; + self.interrupted = true; + } + + /// Only resumes what the interruption stopped; a stream the caller had paused stays paused. + fn resume_after_interruption(&mut self) { + if !std::mem::take(&mut self.interrupted) { + return; + } + if self.audio_unit.start().is_ok() { + self.playing = true; + } + } +} + 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..c61f79b40 100644 --- a/src/host/coreaudio/ios/session_event_manager.rs +++ b/src/host/coreaudio/ios/session_event_manager.rs @@ -1,39 +1,54 @@ -//! 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`. +unsafe 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()); + let reason = AVAudioSessionRouteChangeReason(unsafe { + user_info_number(notification, AVAudioSessionRouteChangeReasonKey)? + }); match reason { AVAudioSessionRouteChangeReason::OldDeviceUnavailable => Some(Error::with_message( ErrorKind::DeviceChanged, @@ -73,11 +88,50 @@ 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 Some(kind) = + (unsafe { user_info_number(notif, AVAudioSessionInterruptionTypeKey) }) + else { + return; + }; + if AVAudioSessionInterruptionType(kind) == AVAudioSessionInterruptionType::Began { + with_stream(&stream, StreamInner::stop_for_interruption); + return; + } + let options = AVAudioSessionInterruptionOptions( + unsafe { user_info_number(notif, AVAudioSessionInterruptionOptionKey) } + .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();