hydra

Terminal replacement for Loopback — virtual audio devices and routing on macOS, from a ratatui TUI.
Log | Files | Refs | README | LICENSE

mod.rs (4966B)


      1 //! Thin, safe-ish wrappers over the CoreAudio HAL C API.
      2 //!
      3 //! We lean on `coreaudio-sys` for the stable base types/functions and `core-foundation`
      4 //! for CFString/CFDictionary, but spell out the property selectors ourselves as
      5 //! four-char-codes — that keeps us independent of whatever the pinned SDK happens to
      6 //! export, which matters for the macOS 14.4+ process-tap selectors.
      7 
      8 pub mod hal;
      9 pub mod process;
     10 pub mod shim;
     11 
     12 use anyhow::{bail, Result};
     13 use coreaudio_sys as sys;
     14 use std::mem;
     15 use std::ptr;
     16 
     17 pub use sys::AudioObjectID;
     18 
     19 /// The well-known system object that owns the device/process lists.
     20 pub const SYSTEM_OBJECT: AudioObjectID = sys::kAudioObjectSystemObject as AudioObjectID;
     21 
     22 /// Build a four-char-code constant the way CoreAudio defines its selectors.
     23 pub const fn fourcc(s: &[u8; 4]) -> u32 {
     24     ((s[0] as u32) << 24) | ((s[1] as u32) << 16) | ((s[2] as u32) << 8) | (s[3] as u32)
     25 }
     26 
     27 /// Property selectors (verified four-char-codes from `AudioHardware.h`).
     28 pub mod sel {
     29     use super::fourcc;
     30     pub const DEVICES: u32 = fourcc(b"dev#");
     31     pub const DEFAULT_OUTPUT: u32 = fourcc(b"dOut");
     32     pub const DEFAULT_INPUT: u32 = fourcc(b"dIn ");
     33     pub const TRANSLATE_UID_TO_DEVICE: u32 = fourcc(b"uidd");
     34     pub const DEVICE_UID: u32 = fourcc(b"uid ");
     35     pub const NAME: u32 = fourcc(b"lnam");
     36     pub const STREAM_CONFIG: u32 = fourcc(b"slay");
     37     pub const NOMINAL_SAMPLE_RATE: u32 = fourcc(b"nsrt");
     38     // Process taps (macOS 14.4+).
     39     pub const PROCESS_LIST: u32 = fourcc(b"prs#");
     40     pub const TRANSLATE_PID_TO_PROCESS: u32 = fourcc(b"id2p");
     41     pub const PROCESS_PID: u32 = fourcc(b"ppid");
     42     pub const PROCESS_BUNDLE_ID: u32 = fourcc(b"pbid");
     43 }
     44 
     45 /// Property scopes.
     46 pub mod scope {
     47     use super::fourcc;
     48     pub const GLOBAL: u32 = fourcc(b"glob");
     49     pub const INPUT: u32 = fourcc(b"inpt");
     50     pub const OUTPUT: u32 = fourcc(b"outp");
     51 }
     52 
     53 /// The "main"/master element.
     54 pub const ELEMENT_MAIN: u32 = 0;
     55 
     56 /// Construct a property address (the most common shape: global scope, main element).
     57 pub fn addr(selector: u32, scope: u32, element: u32) -> sys::AudioObjectPropertyAddress {
     58     sys::AudioObjectPropertyAddress { mSelector: selector, mScope: scope, mElement: element }
     59 }
     60 
     61 /// Byte size CoreAudio reports for a property.
     62 ///
     63 /// # Safety
     64 /// `obj` must be a valid AudioObjectID and `a` a valid address.
     65 pub unsafe fn data_size(obj: AudioObjectID, a: &sys::AudioObjectPropertyAddress) -> Result<usize> {
     66     let mut size: u32 = 0;
     67     let st = sys::AudioObjectGetPropertyDataSize(obj, a, 0, ptr::null(), &mut size);
     68     if st != 0 {
     69         bail!("AudioObjectGetPropertyDataSize(sel={:#x}) -> OSStatus {st}", a.mSelector);
     70     }
     71     Ok(size as usize)
     72 }
     73 
     74 /// Read a property whose value is an array of POD `T` (e.g. a list of object IDs).
     75 ///
     76 /// # Safety
     77 /// `T` must be a plain-old-data type matching the property's element layout.
     78 pub unsafe fn get_array<T: Copy>(obj: AudioObjectID, a: &sys::AudioObjectPropertyAddress) -> Result<Vec<T>> {
     79     let size = data_size(obj, a)?;
     80     let count = size / mem::size_of::<T>();
     81     let mut buf: Vec<T> = Vec::with_capacity(count);
     82     let mut io = size as u32;
     83     let st = sys::AudioObjectGetPropertyData(obj, a, 0, ptr::null(), &mut io, buf.as_mut_ptr() as *mut _);
     84     if st != 0 {
     85         bail!("AudioObjectGetPropertyData(sel={:#x}) -> OSStatus {st}", a.mSelector);
     86     }
     87     buf.set_len(io as usize / mem::size_of::<T>());
     88     Ok(buf)
     89 }
     90 
     91 /// Read a fixed-size scalar property (e.g. a `u32`, `f64`, or an `AudioObjectID`).
     92 ///
     93 /// # Safety
     94 /// `T` must match the property's exact value layout.
     95 pub unsafe fn get_scalar<T: Copy>(obj: AudioObjectID, a: &sys::AudioObjectPropertyAddress) -> Result<T> {
     96     let mut val: mem::MaybeUninit<T> = mem::MaybeUninit::zeroed();
     97     let mut size = mem::size_of::<T>() as u32;
     98     let st = sys::AudioObjectGetPropertyData(obj, a, 0, ptr::null(), &mut size, val.as_mut_ptr() as *mut _);
     99     if st != 0 {
    100         bail!("AudioObjectGetPropertyData(sel={:#x}) -> OSStatus {st}", a.mSelector);
    101     }
    102     Ok(val.assume_init())
    103 }
    104 
    105 /// Read a CFString property and return it as an owned Rust `String`.
    106 ///
    107 /// # Safety
    108 /// `a` must address a property whose value is a `CFStringRef`.
    109 pub unsafe fn get_cfstring(obj: AudioObjectID, a: &sys::AudioObjectPropertyAddress) -> Result<String> {
    110     use core_foundation::base::TCFType;
    111     use core_foundation::string::CFString;
    112     use core_foundation_sys::string::CFStringRef;
    113 
    114     let mut s: CFStringRef = ptr::null();
    115     let mut size = mem::size_of::<CFStringRef>() as u32;
    116     let st = sys::AudioObjectGetPropertyData(obj, a, 0, ptr::null(), &mut size, &mut s as *mut _ as *mut _);
    117     if st != 0 || s.is_null() {
    118         bail!("AudioObjectGetPropertyData(cfstring, sel={:#x}) -> OSStatus {st}", a.mSelector);
    119     }
    120     // CoreAudio follows the create rule for returned CFStrings: we own this reference.
    121     let cf = CFString::wrap_under_create_rule(s);
    122     Ok(cf.to_string())
    123 }