hydra

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

hal.rs (2836B)


      1 //! Device enumeration. Read-only HAL queries — no special entitlement or TCC consent
      2 //! required, so this works the moment the daemon starts.
      3 
      4 use anyhow::Result;
      5 use hydra_ipc::AudioDevice;
      6 
      7 use super::{addr, get_array, get_cfstring, get_scalar, scope, sel, AudioObjectID, ELEMENT_MAIN, SYSTEM_OBJECT};
      8 
      9 /// The system's current default output device, or `Ok(0)` if none is set.
     10 pub fn default_output_device() -> AudioObjectID {
     11     unsafe { get_scalar(SYSTEM_OBJECT, &addr(sel::DEFAULT_OUTPUT, scope::GLOBAL, ELEMENT_MAIN)).unwrap_or(0) }
     12 }
     13 
     14 /// Enumerate every audio device CoreAudio knows about.
     15 pub fn list_devices() -> Result<Vec<AudioDevice>> {
     16     let default_out = default_output_device();
     17     let ids: Vec<AudioObjectID> =
     18         unsafe { get_array(SYSTEM_OBJECT, &addr(sel::DEVICES, scope::GLOBAL, ELEMENT_MAIN))? };
     19 
     20     let mut devices = Vec::with_capacity(ids.len());
     21     for id in ids {
     22         let name = device_name(id);
     23         let uid = device_uid(id);
     24         devices.push(AudioDevice {
     25             uid,
     26             name,
     27             input_channels: channel_count(id, scope::INPUT),
     28             output_channels: channel_count(id, scope::OUTPUT),
     29             is_default_output: id == default_out,
     30         });
     31     }
     32     Ok(devices)
     33 }
     34 
     35 /// A device's human-readable name (falls back to a placeholder).
     36 pub fn device_name(id: AudioObjectID) -> String {
     37     unsafe { get_cfstring(id, &addr(sel::NAME, scope::GLOBAL, ELEMENT_MAIN)) }
     38         .unwrap_or_else(|_| format!("device {id}"))
     39 }
     40 
     41 /// A device's persistent UID (stable across reboots; used to re-resolve devices).
     42 pub fn device_uid(id: AudioObjectID) -> String {
     43     unsafe { get_cfstring(id, &addr(sel::DEVICE_UID, scope::GLOBAL, ELEMENT_MAIN)) }.unwrap_or_default()
     44 }
     45 
     46 /// Total channels a device exposes in the given scope (`scope::INPUT` / `scope::OUTPUT`),
     47 /// summed across all its streams. Returns 0 if the device has none.
     48 pub fn channel_count(id: AudioObjectID, scope: u32) -> u32 {
     49     use std::ptr;
     50     let a = addr(sel::STREAM_CONFIG, scope, ELEMENT_MAIN);
     51     unsafe {
     52         let size = match super::data_size(id, &a) {
     53             Ok(s) if s > 0 => s,
     54             _ => return 0,
     55         };
     56         let mut buf = vec![0u8; size];
     57         let mut io = size as u32;
     58         let st = coreaudio_sys::AudioObjectGetPropertyData(
     59             id,
     60             &a,
     61             0,
     62             ptr::null(),
     63             &mut io,
     64             buf.as_mut_ptr() as *mut _,
     65         );
     66         if st != 0 {
     67             return 0;
     68         }
     69         // Reinterpret the bytes as an AudioBufferList and sum each buffer's channels.
     70         let abl = buf.as_ptr() as *const coreaudio_sys::AudioBufferList;
     71         let n = (*abl).mNumberBuffers as usize;
     72         let first = (*abl).mBuffers.as_ptr();
     73         (0..n).map(|i| (*first.add(i)).mNumberChannels).sum()
     74     }
     75 }