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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **AudioWorklet**: `BufferSize::Fixed` now sets `renderSizeHint` on the `AudioContext`.
- **CoreAudio**: Timestamps now include device latency and safety offset.
- **CoreAudio**: Poisoned stream mutex in stream functions now propagate panics.
- **CoreAudio**: Physical stream format is now set directly on the hardware device.
- **CoreAudio**: Stream error callback now receives `StreamError::StreamInvalidated` on any sample
rate change on macOS, and on iOS on route changes that require a stream rebuild.
- **CoreAudio**: Stream error callback now receives `StreamError::DeviceNotAvailable` on iOS
when media services are lost.
- **CoreAudio**: User timeouts are now obeyed when building a stream.
- **JACK**: Timestamps now use the precise hardware deadline.
- **JACK**: Buffer size change no longer fires an error callback; internal buffers are resized
without error.
Expand Down
15 changes: 14 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,29 @@ objc2-core-audio-types = { version = "0.3", default-features = false, features =
"CoreAudioBaseTypes",
] }
objc2-core-foundation = { version = "0.3" }
objc2-foundation = { version = "0.3" }
objc2-foundation = { version = "0.3", default-features = false, features = [
"std",
"NSArray",
"NSString",
"NSValue",
] }
objc2 = { version = "0.6" }

[target.'cfg(target_os = "macos")'.dependencies]
jack = { version = "0.13", optional = true }

[target.'cfg(target_os = "ios")'.dependencies]
block2 = "0.6"
objc2-foundation = { version = "0.3", features = [
"block2",
"NSDictionary",
"NSNotification",
"NSOperation",
] }
objc2-avf-audio = { version = "0.3", default-features = false, features = [
"std",
"AVAudioSession",
"AVAudioSessionTypes",
] }

[target.'cfg(target_os = "emscripten")'.dependencies]
Expand Down
54 changes: 39 additions & 15 deletions src/host/coreaudio/ios/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
//! CoreAudio implementation for iOS using AVAudioSession and RemoteIO Audio Units.

use std::ptr::NonNull;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;

use coreaudio::audio_unit::render_callback::data;
use coreaudio::audio_unit::{render_callback, AudioUnit, Element, Scope};
use objc2_audio_toolbox::{kAudioOutputUnitProperty_EnableIO, kAudioUnitProperty_StreamFormat};
use objc2_core_audio_types::AudioBuffer;

use objc2_avf_audio::AVAudioSession;
use objc2_core_audio_types::AudioBuffer;

use super::{asbd_from_config, frames_to_duration, host_time_to_stream_instant};
use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
Expand All @@ -25,10 +27,10 @@ use self::enumerate::{
default_input_device, default_output_device, Devices, SupportedInputConfigs,
SupportedOutputConfigs,
};
use std::ptr::NonNull;
use std::time::Duration;

Comment thread
roderickvd marked this conversation as resolved.
pub mod enumerate;
mod session_event_manager;
use session_event_manager::{ErrorCallbackMutex, SessionEventManager};

// These days the default of iOS is now F32 and no longer I16
const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;
Expand Down Expand Up @@ -169,22 +171,32 @@ impl DeviceTrait for Device {
// Query device buffer size for latency calculation
let device_buffer_frames = Some(get_device_buffer_frames());

let error_callback: ErrorCallbackMutex = Arc::new(Mutex::new(Box::new(error_callback)));
let session_manager = SessionEventManager::new(error_callback.clone());

// Set up input callback
setup_input_callback(
&mut audio_unit,
sample_format,
config.sample_rate,
device_buffer_frames,
data_callback,
error_callback,
move |e| {
if let Ok(mut cb) = error_callback.lock() {
cb(e);
}
},
)?;

audio_unit.start()?;

Ok(Stream::new(StreamInner {
playing: true,
audio_unit,
}))
Ok(Stream::new(
StreamInner {
playing: true,
audio_unit,
},
session_manager,
))
}

/// Create an output stream.
Expand All @@ -206,33 +218,45 @@ impl DeviceTrait for Device {
// Query device buffer size for latency calculation
let device_buffer_frames = Some(get_device_buffer_frames());

let error_callback: ErrorCallbackMutex = Arc::new(Mutex::new(Box::new(error_callback)));
let session_manager = SessionEventManager::new(error_callback.clone());

// Set up output callback
setup_output_callback(
&mut audio_unit,
sample_format,
config.sample_rate,
device_buffer_frames,
data_callback,
error_callback,
move |e| {
if let Ok(mut cb) = error_callback.lock() {
cb(e);
}
},
)?;

audio_unit.start()?;

Ok(Stream::new(StreamInner {
playing: true,
audio_unit,
}))
Ok(Stream::new(
StreamInner {
playing: true,
audio_unit,
},
session_manager,
))
}
}

pub struct Stream {
inner: Mutex<StreamInner>,
_session_manager: SessionEventManager,
}

impl Stream {
fn new(inner: StreamInner) -> Self {
fn new(inner: StreamInner, session_manager: SessionEventManager) -> Self {
Self {
inner: Mutex::new(inner),
_session_manager: session_manager,
}
}
}
Expand Down
116 changes: 116 additions & 0 deletions src/host/coreaudio/ios/session_event_manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//! Monitors AVAudioSession lifecycle events and reports them as stream errors.

use std::ptr::NonNull;
use std::sync::{Arc, Mutex};

use block2::RcBlock;
use objc2::runtime::AnyObject;
use objc2_avf_audio::{
AVAudioSessionMediaServicesWereLostNotification,
AVAudioSessionMediaServicesWereResetNotification, AVAudioSessionRouteChangeNotification,
AVAudioSessionRouteChangeReason, AVAudioSessionRouteChangeReasonKey,
};
use objc2_foundation::{NSNotification, NSNotificationCenter, NSNumber, NSString};

use crate::StreamError;

pub(super) type ErrorCallbackMutex = Arc<Mutex<Box<dyn FnMut(StreamError) + Send>>>;

unsafe fn route_change_error(notification: &NSNotification) -> Option<StreamError> {
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());
match reason {
AVAudioSessionRouteChangeReason::OldDeviceUnavailable
| AVAudioSessionRouteChangeReason::CategoryChange
| AVAudioSessionRouteChangeReason::Override
| AVAudioSessionRouteChangeReason::RouteConfigurationChange => {
Some(StreamError::StreamInvalidated)
}

AVAudioSessionRouteChangeReason::NoSuitableRouteForCategory => {
Some(StreamError::DeviceNotAvailable)
}

_ => None,
}
}

pub(super) struct SessionEventManager {
observers: Vec<
objc2::rc::Retained<objc2::runtime::ProtocolObject<dyn objc2::runtime::NSObjectProtocol>>,
>,
}

// SAFETY: NSNotificationCenter is thread-safe on iOS. The observer tokens stored here are opaque
// handles used only to call removeObserver in Drop; no data is read or written through them.
unsafe impl Send for SessionEventManager {}
unsafe impl Sync for SessionEventManager {}

impl SessionEventManager {
pub(super) fn new(error_callback: ErrorCallbackMutex) -> Self {
let nc = NSNotificationCenter::defaultCenter();
let mut observers = Vec::new();

{
let cb = error_callback.clone();
let block = RcBlock::new(move |notif: NonNull<NSNotification>| {
if let Some(err) = unsafe { route_change_error(notif.as_ref()) } {
if let Ok(mut cb) = cb.lock() {
cb(err);
}
}
});
if let Some(name) = unsafe { AVAudioSessionRouteChangeNotification } {
let observer = unsafe {
nc.addObserverForName_object_queue_usingBlock(Some(name), None, None, &block)
};
observers.push(observer);
}
}

{
let cb = error_callback.clone();
let block = RcBlock::new(move |_: NonNull<NSNotification>| {
if let Ok(mut cb) = cb.lock() {
cb(StreamError::DeviceNotAvailable);
}
});
if let Some(name) = unsafe { AVAudioSessionMediaServicesWereLostNotification } {
let observer = unsafe {
nc.addObserverForName_object_queue_usingBlock(Some(name), None, None, &block)
};
observers.push(observer);
}
}

{
let cb = error_callback.clone();
let block = RcBlock::new(move |_: NonNull<NSNotification>| {
if let Ok(mut cb) = cb.lock() {
cb(StreamError::StreamInvalidated);
}
});
if let Some(name) = unsafe { AVAudioSessionMediaServicesWereResetNotification } {
let observer = unsafe {
nc.addObserverForName_object_queue_usingBlock(Some(name), None, None, &block)
};
observers.push(observer);
}
}

Self { observers }
}
}

impl Drop for SessionEventManager {
fn drop(&mut self) {
let nc = NSNotificationCenter::defaultCenter();
for observer in &self.observers {
unsafe { nc.removeObserver(observer.as_ref()) };
}
}
}
Loading
Loading