Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
108 changes: 69 additions & 39 deletions src/host/coreaudio/ios/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -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)
}
Expand Down Expand Up @@ -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(
Expand All @@ -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<StreamInner>,
inner: Arc<Mutex<StreamInner>>,
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();
}
Expand All @@ -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(())
}
Expand All @@ -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(())
}

Expand All @@ -339,11 +374,6 @@ impl StreamTrait for Stream {
}
}

struct StreamInner {
playing: bool,
audio_unit: AudioUnit,
}

fn create_audio_unit() -> Result<AudioUnit, coreaudio::Error> {
AudioUnit::new_uninitialized(coreaudio::audio_unit::IOType::RemoteIO)
}
Expand Down
79 changes: 66 additions & 13 deletions src/host/coreaudio/ios/session_event_manager.rs
Original file line number Diff line number Diff line change
@@ -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<Mutex<StreamInner>>, 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<usize> {
let user_info = notification.userInfo()?;
let value = user_info.objectForKey(key?)?;
Some(value.downcast::<NSNumber>().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<AtomicUsize>, bool);

unsafe fn route_change_error(notification: &NSNotification) -> Option<Error> {
let user_info = notification.userInfo()?;
let key = AVAudioSessionRouteChangeReasonKey?;
let dict = unsafe { user_info.cast_unchecked::<NSString, AnyObject>() };
let value = dict.objectForKey(key)?;
let number = value.downcast_ref::<NSNumber>()?;
let reason = AVAudioSessionRouteChangeReason(number.unsignedIntegerValue());
fn route_change_error(notification: &NSNotification) -> Option<Error> {
let key = unsafe { AVAudioSessionRouteChangeReasonKey };
let reason = AVAudioSessionRouteChangeReason(user_info_number(notification, key)?);
match reason {
AVAudioSessionRouteChangeReason::OldDeviceUnavailable => Some(Error::with_message(
ErrorKind::DeviceChanged,
Expand Down Expand Up @@ -73,11 +87,49 @@ impl SessionEventManager {
error_callback: ErrorCallbackArc,
latch: Latch,
latency_refresh: Option<LatencyRefresh>,
stream: Weak<Mutex<StreamInner>>,
) -> 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<NSNotification>| {
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();
Expand All @@ -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);
}
}
Expand Down
Loading