From 6568d837f93255b7744a71f82e2c850142ce1ce1 Mon Sep 17 00:00:00 2001 From: umerhder Date: Mon, 11 May 2026 01:30:57 -0400 Subject: [PATCH 1/2] fix(coreaudio): scope loopback aggregate IDs per process and instance --- CHANGELOG.md | 1 + src/host/coreaudio/macos/loopback.rs | 26 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 194a97ce3..62a299614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -159,6 +159,7 @@ 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**: 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..ab273d7c6 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( @@ -176,6 +188,8 @@ fn to_cfstring(cstr: &'static CStr) -> CFRetained { /// ``` 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 +235,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()), From b665a5adb154f64fa5f157b0049924201413ea25 Mon Sep 17 00:00:00 2001 From: umerhder Date: Mon, 11 May 2026 01:31:06 -0400 Subject: [PATCH 2/2] fix(coreaudio): enable tap auto-start for loopback aggregate device --- CHANGELOG.md | 1 + src/host/coreaudio/macos/loopback.rs | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62a299614..8e9ea80f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -160,6 +160,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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 ab273d7c6..b4f0b9596 100644 --- a/src/host/coreaudio/macos/loopback.rs +++ b/src/host/coreaudio/macos/loopback.rs @@ -180,9 +180,7 @@ 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, /// }; /// ``` @@ -250,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()),