Skip to main content

screencapturekit/
audio_devices.rs

1//! Audio input device enumeration using `AVFoundation`.
2//!
3//! This module provides access to available microphone devices on macOS.
4
5use std::ffi::c_void;
6
7/// Represents an audio input device (microphone).
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct AudioInputDevice {
10    /// The unique device ID used with `SCStreamConfiguration::with_microphone_capture_device_id`
11    pub id: String,
12    /// Human-readable device name
13    pub name: String,
14    /// Whether this is the system default audio input device
15    pub is_default: bool,
16}
17
18impl AudioInputDevice {
19    /// List all available audio input devices.
20    ///
21    /// The whole list is read from a single device-discovery pass, so a
22    /// microphone plugged in or unplugged during the call cannot produce a
23    /// half-updated result (a stale count paired with fresh names, or an entry
24    /// whose `is_default` disagrees with [`default_device`](Self::default_device)).
25    ///
26    /// # Caching
27    ///
28    /// **Not cached.** Each call walks Apple's audio device list and copies the
29    /// per-device strings across the FFI boundary. The cost is small in
30    /// absolute terms (microseconds) but is **non-zero on every call**. Code
31    /// that repeatedly needs the device list (e.g. inside a UI render loop or
32    /// per-frame decision) should cache the result and re-list only when the
33    /// user signals a possible device change (e.g. on a settings-pane open or
34    /// an `AVAudioRouteChangeNotification`).
35    ///
36    /// # Example
37    ///
38    /// ```no_run
39    /// use screencapturekit::audio_devices::AudioInputDevice;
40    ///
41    /// let devices = AudioInputDevice::list();
42    /// for device in &devices {
43    ///     println!("{}: {} {}", device.id, device.name,
44    ///         if device.is_default { "(default)" } else { "" });
45    /// }
46    /// ```
47    #[must_use]
48    pub fn list() -> Vec<Self> {
49        Snapshot::take().devices()
50    }
51
52    /// Get the default audio input device, if any.
53    ///
54    /// Reads from the same single-pass snapshot as [`list`](Self::list), so the
55    /// returned device is guaranteed to be one of the listed devices rather
56    /// than an entry that appeared or vanished between two queries.
57    ///
58    /// # Example
59    ///
60    /// ```no_run
61    /// use screencapturekit::audio_devices::AudioInputDevice;
62    ///
63    /// if let Some(device) = AudioInputDevice::default_device() {
64    ///     println!("Default microphone: {}", device.name);
65    /// }
66    /// ```
67    #[must_use]
68    pub fn default_device() -> Option<Self> {
69        let snapshot = Snapshot::take();
70        let index = isize::try_from(snapshot.default_index()?).ok()?;
71        snapshot.device_at(index)
72    }
73
74    /// List all devices and identify the default one in a single pass.
75    ///
76    /// Returns the device list plus the index of the default device within it.
77    /// Prefer this over calling [`list`](Self::list) and
78    /// [`default_device`](Self::default_device) separately: it halves the work
79    /// and rules out the two results disagreeing.
80    ///
81    /// # Example
82    ///
83    /// ```no_run
84    /// use screencapturekit::audio_devices::AudioInputDevice;
85    ///
86    /// let (devices, default_index) = AudioInputDevice::list_with_default();
87    /// if let Some(default) = default_index.and_then(|i| devices.get(i)) {
88    ///     println!("Default: {}", default.name);
89    /// }
90    /// ```
91    #[must_use]
92    pub fn list_with_default() -> (Vec<Self>, Option<usize>) {
93        let snapshot = Snapshot::take();
94        let devices = snapshot.devices();
95        // Derive the index from the returned list rather than reusing the
96        // snapshot's index: `devices()` drops entries whose id or name is
97        // unreadable, so the snapshot index addresses the unfiltered array and
98        // would designate the wrong device, or fall outside the list entirely.
99        let default_index = devices.iter().position(|device| device.is_default);
100        (devices, default_index)
101    }
102}
103
104/// RAII wrapper around a Swift-side frozen device list.
105struct Snapshot {
106    ptr: *const c_void,
107}
108
109impl Snapshot {
110    fn take() -> Self {
111        Self {
112            ptr: unsafe { crate::ffi::sc_audio_input_devices_snapshot_create() },
113        }
114    }
115
116    fn count(&self) -> usize {
117        if self.ptr.is_null() {
118            return 0;
119        }
120        let count = unsafe { crate::ffi::sc_audio_input_devices_snapshot_count(self.ptr) };
121        usize::try_from(count).unwrap_or(0)
122    }
123
124    fn default_index(&self) -> Option<usize> {
125        if self.ptr.is_null() {
126            return None;
127        }
128        let index = unsafe { crate::ffi::sc_audio_input_devices_snapshot_default_index(self.ptr) };
129        usize::try_from(index).ok().filter(|i| *i < self.count())
130    }
131
132    fn device_at(&self, index: isize) -> Option<AudioInputDevice> {
133        let id = unsafe {
134            crate::utils::ffi_string::ffi_string_owned(|| {
135                crate::ffi::sc_audio_input_devices_snapshot_id_owned(self.ptr, index)
136            })
137        }?;
138        let name = unsafe {
139            crate::utils::ffi_string::ffi_string_owned(|| {
140                crate::ffi::sc_audio_input_devices_snapshot_name_owned(self.ptr, index)
141            })
142        }?;
143        let is_default =
144            unsafe { crate::ffi::sc_audio_input_devices_snapshot_is_default(self.ptr, index) };
145        Some(AudioInputDevice {
146            id,
147            name,
148            is_default,
149        })
150    }
151
152    fn devices(&self) -> Vec<AudioInputDevice> {
153        let count = self.count();
154        (0..count)
155            .filter_map(|i| isize::try_from(i).ok().and_then(|i| self.device_at(i)))
156            .collect()
157    }
158}
159
160impl Drop for Snapshot {
161    fn drop(&mut self) {
162        if !self.ptr.is_null() {
163            unsafe { crate::ffi::sc_audio_input_devices_snapshot_release(self.ptr) };
164        }
165    }
166}