Skip to main content

murmur_core/audio/
activate.rs

1//! Platform-specific audio input device activation.
2//!
3//! On macOS, Bluetooth devices (like AirPods) connect in A2DP mode which
4//! provides high-quality audio *output* but no microphone input.  The mic
5//! requires a switch to the HFP/SCO profile, which macOS normally triggers
6//! when an app selects the device as the system input.
7//!
8//! Higher-level Apple frameworks (`AVAudioSession`, `AVCaptureSession`)
9//! handle this automatically, but the low-level AudioUnit HAL that `cpal`
10//! uses does not always trigger the switch.
11//!
12//! This module provides a best-effort activation hook that re-sets the
13//! default input device via CoreAudio, nudging macOS into establishing the
14//! SCO link.  It is a no-op on non-macOS platforms.
15
16/// Attempt to activate the system default input device for audio capture.
17///
18/// On macOS this re-sets the default input device via CoreAudio to nudge
19/// Bluetooth devices into HFP mode.  On other platforms this is a no-op.
20///
21/// This is a best-effort operation — failures are logged at debug level
22/// and do not propagate errors.
23pub fn prepare_default_input() {
24    #[cfg(target_os = "macos")]
25    macos::activate_default_input();
26}
27
28// ── macOS CoreAudio implementation ──────────────────────────────────────
29
30#[cfg(target_os = "macos")]
31mod macos {
32    use std::os::raw::c_void;
33
34    // CoreAudio HAL types
35    type AudioObjectID = u32;
36    type AudioDeviceID = u32;
37    type OSStatus = i32;
38
39    const K_AUDIO_OBJECT_SYSTEM_OBJECT: AudioObjectID = 1;
40
41    // Property selectors (FourCC encoded)
42    const K_AUDIO_HARDWARE_PROPERTY_DEFAULT_INPUT_DEVICE: u32 = u32::from_be_bytes(*b"dIn ");
43    const K_AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL: u32 = u32::from_be_bytes(*b"glob");
44    const K_AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN: u32 = 0;
45
46    #[repr(C)]
47    struct AudioObjectPropertyAddress {
48        selector: u32,
49        scope: u32,
50        element: u32,
51    }
52
53    #[link(name = "CoreAudio", kind = "framework")]
54    extern "C" {
55        fn AudioObjectGetPropertyData(
56            object_id: AudioObjectID,
57            address: *const AudioObjectPropertyAddress,
58            qualifier_data_size: u32,
59            qualifier_data: *const c_void,
60            data_size: *mut u32,
61            data: *mut c_void,
62        ) -> OSStatus;
63
64        fn AudioObjectSetPropertyData(
65            object_id: AudioObjectID,
66            address: *const AudioObjectPropertyAddress,
67            qualifier_data_size: u32,
68            qualifier_data: *const c_void,
69            data_size: u32,
70            data: *const c_void,
71        ) -> OSStatus;
72    }
73
74    /// Re-set the current default input device via CoreAudio.
75    ///
76    /// Writing the same device ID back to `kAudioHardwarePropertyDefaultInputDevice`
77    /// can trigger macOS to establish the Bluetooth SCO/HFP link if it hasn't
78    /// already.  This mirrors what System Settings does when the user selects
79    /// a Bluetooth input device.
80    pub(super) fn activate_default_input() {
81        let addr = AudioObjectPropertyAddress {
82            selector: K_AUDIO_HARDWARE_PROPERTY_DEFAULT_INPUT_DEVICE,
83            scope: K_AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
84            element: K_AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN,
85        };
86
87        let mut device_id: AudioDeviceID = 0;
88        let mut size = std::mem::size_of::<AudioDeviceID>() as u32;
89
90        let status = unsafe {
91            AudioObjectGetPropertyData(
92                K_AUDIO_OBJECT_SYSTEM_OBJECT,
93                &addr,
94                0,
95                std::ptr::null(),
96                &mut size,
97                &mut device_id as *mut _ as *mut c_void,
98            )
99        };
100
101        if status != 0 {
102            log::debug!("CoreAudio: failed to get default input device (status={status})");
103            return;
104        }
105
106        let status = unsafe {
107            AudioObjectSetPropertyData(
108                K_AUDIO_OBJECT_SYSTEM_OBJECT,
109                &addr,
110                0,
111                std::ptr::null(),
112                std::mem::size_of::<AudioDeviceID>() as u32,
113                &device_id as *const _ as *const c_void,
114            )
115        };
116
117        if status != 0 {
118            log::debug!("CoreAudio: failed to re-set default input device (status={status})");
119        } else {
120            log::info!("CoreAudio: activated default input device (id={device_id})");
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn prepare_default_input_does_not_panic() {
131        // Should be a no-op on non-macOS, best-effort on macOS.
132        prepare_default_input();
133    }
134}