Skip to content
Open
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
24 changes: 17 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
silently returning an empty list.
- **AAudio**: Bump MSRV to 1.85.
- **AAudio**: Buffers with default sizes are now dynamically tuned.
- **ALSA**: Device disconnection now stops the stream with `StreamError::DeviceNotAvailable` instead of looping.
- **ALSA**: Device disconnection now stops the stream with `StreamError::DeviceNotAvailable`
instead of looping.
- **ALSA**: Polling errors trigger underrun recovery instead of looping.
- **ALSA**: Try to resume from hardware after a system suspend.
- **ALSA**: Loop partial reads and writes to completion.
- **ALSA**: Prevent reentrancy issues with non-reentrant plugins and devices.
- **ASIO**: `Device::driver`, `asio_streams`, and `current_callback_flag` are no longer `pub`.
- **ASIO**: Timestamps now include driver-reported hardware latency.
- **ASIO**: Hardware latency is now re-queried when the driver reports `kAsioLatenciesChanged`.
- **ASIO**: Stream error callback now receives `StreamError::BufferUnderrun` on `kAsioResyncRequest`.
- **ASIO**: Stream error callback now receives `StreamError::StreamInvalidated` when the driver reports a sample
rate change (`sampleRateDidChange`) of 1 Hz or more from the configured rate.
- **ASIO**: Stream error callback now receives `StreamError::BufferUnderrun` on
`kAsioResyncRequest`.
- **ASIO**: Stream error callback now receives `StreamError::StreamInvalidated` when the driver
reports a sample rate change (`sampleRateDidChange`) of 1 Hz or more from the configured rate.
- **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.
- **JACK**: Timestamps now use the precise hardware deadline.
- **JACK**: Buffer size change no longer fires an error callback; internal buffers are resized without error.
- **JACK**: Buffer size change no longer fires an error callback; internal buffers are resized
without error.
- **JACK**: Server shutdown now fires `StreamError::DeviceNotAvailable`.
- **Linux/BSD**: Default host in order from first to last available now is: PipeWire, PulseAudio, ALSA.
- **Linux/BSD**: Default host in order from first to last available now is: PipeWire, PulseAudio,
ALSA.
- **WASAPI**: Timestamps now include hardware pipeline latency.
- **WebAudio**: Bump MSRV to 1.85.
- **WebAudio**: Timestamps now include base and output latency.
Expand Down Expand Up @@ -78,7 +87,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **ASIO**: Fix latency not updating when the driver reports `kAsioLatenciesChanged`.
- **ASIO**: Fix distortion when buggy drivers fire the buffer callback multiple times per cycle.
- **ASIO**: Poisoned error callback mutex no longer silently drops subsequent error notifications.
- **ASIO**: Poisoned stream mutex in the buffer-size change handler no longer silently skips the update.
- **ASIO**: Poisoned stream mutex in the buffer-size change handler no longer silently skips the
update.
- **CoreAudio**: Fix undefined behaviour and silent failure in loopback device creation.
- **Emscripten**: Fix build failure introduced by newer `wasm-bindgen` versions.
- **JACK**: Fix input capture timestamp using callback execution time instead of cycle start.
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;

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