nandi/jolt-nativepublic Fork 0
cfd3e3677bed92e80cfe447469a249cfdc0b1401
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

loopback.rs · 250 lines · 8.8 KBRust Blame HistoryRaw
Lift freeq's AV media plane out of sleek 90f8b89 nandi 19d ago1//! Manages loopback recording (recording system audio output)
2
3use super::device::Device;
4use crate::{host::coreaudio::check_os_status, BackendSpecificError, BuildStreamError};
5use objc2::{rc::Retained, AnyThread};
6use objc2_core_audio::{
7 kAudioAggregateDeviceNameKey, kAudioAggregateDeviceTapAutoStartKey,
8 kAudioAggregateDeviceTapListKey, kAudioAggregateDeviceUIDKey, kAudioDevicePropertyDeviceUID,
9 kAudioEndPointDeviceIsPrivateKey, kAudioObjectPropertyElementMain,
10 kAudioObjectPropertyScopeGlobal, kAudioSubTapDriftCompensationKey, kAudioSubTapUIDKey,
11 AudioHardwareCreateAggregateDevice, AudioHardwareCreateProcessTap,
12 AudioHardwareDestroyAggregateDevice, AudioHardwareDestroyProcessTap,
13 AudioObjectGetPropertyData, AudioObjectID, AudioObjectPropertyAddress, CATapDescription,
14 CATapMuteBehavior,
15};
16use objc2_core_foundation::{
17 kCFAllocatorDefault, kCFTypeArrayCallBacks, kCFTypeDictionaryKeyCallBacks,
18 kCFTypeDictionaryValueCallBacks, CFArray, CFDictionary, CFMutableDictionary, CFRetained,
19 CFString, CFStringCreateWithCString,
20};
21use objc2_foundation::{ns_string, NSArray, NSNumber, NSString};
22use std::{
23 ffi::{c_void, CStr},
24 mem::MaybeUninit,
25 ptr::NonNull,
26};
27type CFStringRef = *mut std::os::raw::c_void;
28
29impl Device {
30 fn uid(&self) -> Result<Retained<NSString>, BackendSpecificError> {
31 let mut cfstring: CFStringRef = std::ptr::null_mut();
32 let mut size = std::mem::size_of::<CFStringRef>() as u32;
33
34 let property = AudioObjectPropertyAddress {
35 mSelector: kAudioDevicePropertyDeviceUID,
36 mScope: kAudioObjectPropertyScopeGlobal,
37 mElement: kAudioObjectPropertyElementMain,
38 };
39
40 let status = unsafe {
41 AudioObjectGetPropertyData(
42 self.audio_device_id,
43 NonNull::from(&property),
44 0,
45 std::ptr::null(),
46 NonNull::from(&mut size),
47 NonNull::from(&mut cfstring).cast(),
48 )
49 };
50 check_os_status(status)?;
51
52 if cfstring.is_null() {
53 return Err(BackendSpecificError {
54 description: "Device uid is null".to_string(),
55 });
56 }
57
58 let ns_string: Retained<NSString> = unsafe {
59 // unwrap cause cfstring!=null as checked before
60 Retained::retain(cfstring as *mut NSString).unwrap()
61 };
62
63 Ok(ns_string)
64 }
65}
66
67/// An aggregate device with tap for recording system output.
68///
69/// Its main difference with [`Device`] is that it's destroyed when dropped.
70///
71/// It also doesn't implement the [`DeviceTrait`] as users shouldn't be using it. Its
72/// main purpose is to destroy the created aggregate device when loopback recording
73/// is done.
74#[derive(PartialEq, Eq)]
75pub struct LoopbackDevice {
76 pub tap_id: AudioObjectID,
77 pub aggregate_device: Device,
78}
79
80impl LoopbackDevice {
81 /// Create a [`LoopbackDevice`] that records the sound
82 /// output of `device`.
83 pub fn from_device(device: &Device) -> Result<Self, BuildStreamError> {
84 // 1 - Create tap
85
86 // Empty list of processes as we want to record all processes
87 let processes = NSArray::new();
88 let device_uid = device.uid()?;
89 let tap_desc = unsafe {
90 CATapDescription::initWithProcesses_andDeviceUID_withStream(
91 CATapDescription::alloc(),
92 &processes,
93 device_uid.as_ref(),
94 0,
95 )
96 };
97 unsafe {
98 tap_desc.setMuteBehavior(CATapMuteBehavior::Unmuted); // captured audio still goes to speakers
99 tap_desc.setName(ns_string!("cpal output recorder"));
100 tap_desc.setPrivate(true); // the Aggregate Device would be private
101 tap_desc.setExclusive(true); // the process list means exclude them
102 };
103
104 let mut tap_obj_id: MaybeUninit<AudioObjectID> = MaybeUninit::uninit();
105 let tap_obj_id = unsafe {
106 let status =
107 AudioHardwareCreateProcessTap(Some(tap_desc.as_ref()), tap_obj_id.as_mut_ptr());
108 check_os_status(status)?;
109 tap_obj_id.assume_init()
110 };
111 let tap_uid = unsafe { tap_desc.UUID().UUIDString() };
112
113 // 2 - Create aggregate device
114 let aggregate_device_properties = create_audio_aggregate_device_properties(tap_uid);
115 let mut aggregate_device_id: AudioObjectID = 0;
116 let status = unsafe {
117 AudioHardwareCreateAggregateDevice(
118 aggregate_device_properties.as_ref(),
119 NonNull::from(&mut aggregate_device_id),
120 )
121 };
122 check_os_status(status)?;
123
124 Ok(Self {
125 tap_id: tap_obj_id,
126 aggregate_device: Device::new(aggregate_device_id),
127 })
128 }
129}
130
131impl Drop for LoopbackDevice {
132 fn drop(&mut self) {
133 unsafe {
134 // We don't check status to avoid panic during `drop`
135 let _status =
136 AudioHardwareDestroyAggregateDevice(self.aggregate_device.audio_device_id);
137 let _status = AudioHardwareDestroyProcessTap(self.tap_id);
138 }
139 }
140}
141
142fn to_cfstring(cstr: &'static CStr) -> CFRetained<CFString> {
143 unsafe {
144 CFStringCreateWithCString(
145 kCFAllocatorDefault,
146 cstr.as_ptr(),
147 0x08000100, /* UTF8 */
148 )
149 }
150 .unwrap()
151}
152
153/// Rust reimplementation of the following:
154/// ```c
155/// tap_uid = [[tap_description UUID] UUIDString];
156/// taps = @[
157/// @{
158/// @kAudioSubTapUIDKey : (NSString*)tap_uid,
159/// @kAudioSubTapDriftCompensationKey : @YES,
160/// },
161/// ];
162///
163/// aggregate_device_properties = @{
164/// @kAudioAggregateDeviceNameKey : @"MiniMetersAggregateDevice",
165/// @kAudioAggregateDeviceUIDKey :
166/// @"com.josephlyncheski.MiniMetersAggregateDevice",
167/// @kAudioAggregateDeviceTapListKey : taps,
168/// @kAudioAggregateDeviceTapAutoStartKey : @NO,
169/// // If we set this to NO then I believe we need to make the Tap public as
170/// // well.
171/// @kAudioAggregateDeviceIsPrivateKey : @YES,
172/// };
173/// ```
174pub fn create_audio_aggregate_device_properties(
175 tap_uid: Retained<NSString>,
176) -> CFRetained<CFDictionary> {
177 let tap_inner = unsafe {
178 let dict = CFMutableDictionary::new(
179 kCFAllocatorDefault,
180 2,
181 &kCFTypeDictionaryKeyCallBacks,
182 &kCFTypeDictionaryValueCallBacks,
183 )
184 .unwrap();
185
186 CFMutableDictionary::set_value(
187 Some(dict.as_ref()),
188 &*to_cfstring(kAudioSubTapUIDKey) as *const _ as *const c_void,
189 &*tap_uid as *const _ as *const c_void,
190 );
191 CFMutableDictionary::set_value(
192 Some(dict.as_ref()),
193 &*to_cfstring(kAudioSubTapDriftCompensationKey) as *const _ as *const c_void,
194 &*NSNumber::initWithBool(NSNumber::alloc(), true) as *const _ as *const c_void,
195 );
196
197 dict
198 };
199 let _taps_list = [tap_inner];
200 let taps = unsafe {
201 CFArray::new(
202 kCFAllocatorDefault,
203 _taps_list.as_ptr() as *mut *const c_void,
204 _taps_list.len() as _,
205 &kCFTypeArrayCallBacks,
206 )
207 .unwrap()
208 };
209 let aggregate_dev_properties = unsafe {
210 let dict = CFMutableDictionary::new(
211 kCFAllocatorDefault,
212 5,
213 &kCFTypeDictionaryKeyCallBacks,
214 &kCFTypeDictionaryValueCallBacks,
215 )
216 .unwrap();
217
218 CFMutableDictionary::set_value(
219 Some(dict.as_ref()),
220 &*to_cfstring(kAudioAggregateDeviceNameKey) as *const _ as *const c_void,
221 &*CFString::from_str("Cpal loopback record aggregate device") as *const _
222 as *const c_void,
223 );
224 CFMutableDictionary::set_value(
225 Some(dict.as_ref()),
226 &*to_cfstring(kAudioAggregateDeviceUIDKey) as *const _ as *const c_void,
227 &*CFString::from_str("com.cpal.LoopbackRecordAggregateDevice") as *const _
228 as *const c_void,
229 );
230 CFMutableDictionary::set_value(
231 Some(dict.as_ref()),
232 &*to_cfstring(kAudioAggregateDeviceTapListKey) as *const _ as *const c_void,
233 &*taps as *const _ as *const c_void,
234 );
235 CFMutableDictionary::set_value(
236 Some(dict.as_ref()),
237 &*to_cfstring(kAudioAggregateDeviceTapAutoStartKey) as *const _ as *const c_void,
238 &*NSNumber::initWithBool(NSNumber::alloc(), false) as *const _ as *const c_void,
239 );
240 CFMutableDictionary::set_value(
241 Some(dict.as_ref()),
242 &*to_cfstring(kAudioEndPointDeviceIsPrivateKey) as *const _ as *const c_void,
243 &*NSNumber::initWithBool(NSNumber::alloc(), true) as *const _ as *const c_void,
244 );
245
246 CFRetained::cast_unchecked::<CFDictionary>(dict)
247 };
248
249 aggregate_dev_properties
250}