diff --git a/CHANGELOG.md b/CHANGELOG.md index 194a97ce3..8e9ea80f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/host/coreaudio/macos/loopback.rs b/src/host/coreaudio/macos/loopback.rs index 8612755cd..b4f0b9596 100644 --- a/src/host/coreaudio/macos/loopback.rs +++ b/src/host/coreaudio/macos/loopback.rs @@ -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, @@ -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}; @@ -86,6 +89,9 @@ impl LoopbackDevice { pub fn from_device(device: &Device) -> Result { // 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()?; @@ -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 }; @@ -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( @@ -168,14 +180,14 @@ fn to_cfstring(cstr: &'static CStr) -> CFRetained { /// @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, + agg_uid: &str, + agg_name: &str, ) -> CFRetained { let tap_inner = unsafe { let dict = CFMutableDictionary::new( @@ -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()), @@ -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()),