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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **CoreAudio**: Fix default output streams silently stopping when the system default output
device is unplugged; they now reroute automatically or report `ErrorKind::DeviceNotAvailable`.
- **CoreAudio**: Fix undefined behaviour and silent failure in loopback device creation.
- **CoreAudio**: Fix loopback aggregate device UID collisions between concurrent instances and after crashes.
- **CoreAudio**: Fix loopback capture returning silence due to disabled tap auto-start.
- **CoreAudio**: Poisoned stream locks now return `ErrorKind::StreamInvalidated` instead of
panicking.
- **CoreAudio**: Fix crashes on certain drivers due to early initialization.
Expand Down
32 changes: 21 additions & 11 deletions src/host/coreaudio/macos/loopback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ use std::{
ffi::{c_void, CStr},
mem::MaybeUninit,
ptr::NonNull,
sync::atomic::{AtomicU32, Ordering},
};

static AGGREGATE_INSTANCE_COUNTER: AtomicU32 = AtomicU32::new(0);

use objc2::{rc::Retained, AnyThread};
use objc2_core_audio::{
kAudioAggregateDeviceNameKey, kAudioAggregateDeviceTapAutoStartKey,
Expand All @@ -22,7 +25,7 @@ use objc2_core_foundation::{
kCFTypeDictionaryValueCallBacks, CFArray, CFDictionary, CFMutableDictionary, CFRetained,
CFString, CFStringCreateWithCString,
};
use objc2_foundation::{ns_string, NSArray, NSNumber, NSString};
use objc2_foundation::{NSArray, NSNumber, NSString};

use super::device::Device;
use crate::{host::coreaudio::check_os_status, Error, ErrorKind};
Expand Down Expand Up @@ -86,6 +89,9 @@ impl LoopbackDevice {
pub fn from_device(device: &Device) -> Result<Self, Error> {
// 1 - Create tap

let pid = std::process::id();
let instance = AGGREGATE_INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed);

// Empty list of processes as we want to record all processes
let processes = NSArray::new();
let device_uid = device.uid()?;
Expand All @@ -99,7 +105,9 @@ impl LoopbackDevice {
};
unsafe {
tap_desc.setMuteBehavior(CATapMuteBehavior::Unmuted); // captured audio still goes to speakers
tap_desc.setName(ns_string!("cpal output recorder"));
tap_desc.setName(&NSString::from_str(&format!(
"cpal output recorder {pid}.{instance}"
)));
tap_desc.setPrivate(true); // the Aggregate Device would be private
tap_desc.setExclusive(true); // the process list means exclude them
};
Expand All @@ -114,7 +122,11 @@ impl LoopbackDevice {
let tap_uid = unsafe { tap_desc.UUID().UUIDString() };

// 2 - Create aggregate device
let aggregate_device_properties = create_audio_aggregate_device_properties(tap_uid);
let aggregate_device_properties = create_audio_aggregate_device_properties(
tap_uid,
&format!("com.cpal.LoopbackRecordAggregateDevice.{pid}.{instance}"),
&format!("Cpal loopback aggregate {pid}.{instance}"),
);
let mut aggregate_device_id: AudioObjectID = 0;
let status = unsafe {
AudioHardwareCreateAggregateDevice(
Expand Down Expand Up @@ -168,14 +180,14 @@ fn to_cfstring(cstr: &'static CStr) -> CFRetained<CFString> {
/// @kAudioAggregateDeviceUIDKey :
/// @"com.josephlyncheski.MiniMetersAggregateDevice",
/// @kAudioAggregateDeviceTapListKey : taps,
/// @kAudioAggregateDeviceTapAutoStartKey : @NO,
/// // If we set this to NO then I believe we need to make the Tap public as
/// // well.
/// @kAudioAggregateDeviceTapAutoStartKey : @YES,
/// @kAudioAggregateDeviceIsPrivateKey : @YES,
/// };
/// ```
pub fn create_audio_aggregate_device_properties(
tap_uid: Retained<NSString>,
agg_uid: &str,
agg_name: &str,
) -> CFRetained<CFDictionary> {
let tap_inner = unsafe {
let dict = CFMutableDictionary::new(
Expand Down Expand Up @@ -221,14 +233,12 @@ pub fn create_audio_aggregate_device_properties(
CFMutableDictionary::set_value(
Some(dict.as_ref()),
&*to_cfstring(kAudioAggregateDeviceNameKey) as *const _ as *const c_void,
&*CFString::from_str("Cpal loopback record aggregate device") as *const _
as *const c_void,
&*CFString::from_str(agg_name) as *const _ as *const c_void,
);
CFMutableDictionary::set_value(
Some(dict.as_ref()),
&*to_cfstring(kAudioAggregateDeviceUIDKey) as *const _ as *const c_void,
&*CFString::from_str("com.cpal.LoopbackRecordAggregateDevice") as *const _
as *const c_void,
&*CFString::from_str(agg_uid) as *const _ as *const c_void,
);
CFMutableDictionary::set_value(
Some(dict.as_ref()),
Expand All @@ -238,7 +248,7 @@ pub fn create_audio_aggregate_device_properties(
CFMutableDictionary::set_value(
Some(dict.as_ref()),
&*to_cfstring(kAudioAggregateDeviceTapAutoStartKey) as *const _ as *const c_void,
&*NSNumber::initWithBool(NSNumber::alloc(), false) as *const _ as *const c_void,
&*NSNumber::initWithBool(NSNumber::alloc(), true) as *const _ as *const c_void,
);
CFMutableDictionary::set_value(
Some(dict.as_ref()),
Expand Down
Loading