Skip to main content

retch_sysinfo/
audio.rs

1// SPDX-FileCopyrightText: 2026 Ken Tobias
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4//! Audio server and device detection.
5
6/// Detects the active audio server and hardware sound cards.
7pub fn detect_audio(sys: &sysinfo::System) -> Option<String> {
8    #[cfg(target_os = "linux")]
9    {
10        let mut server = None;
11        for process in sys.processes().values() {
12            let name = process.name().to_string_lossy().to_lowercase();
13            if name.contains("pipewire") {
14                server = Some("PipeWire");
15                break;
16            } else if name.contains("pulseaudio") {
17                server = Some("PulseAudio");
18            }
19        }
20        let server_str = server.unwrap_or("ALSA");
21
22        let mut devices = Vec::new();
23        if let Ok(content) = std::fs::read_to_string("/proc/asound/cards") {
24            devices = parse_asound_cards(&content, "/proc/asound");
25        }
26
27        if !devices.is_empty() {
28            Some(format!("{} ({})", server_str, devices.join(", ")))
29        } else {
30            Some(server_str.to_string())
31        }
32    }
33
34    #[cfg(target_os = "macos")]
35    {
36        let _ = sys;
37        let devices = crate::macos_ffi::get_audio_device_names();
38        if !devices.is_empty() {
39            Some(format!("CoreAudio ({})", devices.join(", ")))
40        } else {
41            Some("CoreAudio".to_string())
42        }
43    }
44
45    #[cfg(target_os = "windows")]
46    {
47        let _ = sys;
48        // Read sound device names from the media device class in the registry.
49        // HKLM\SYSTEM\CurrentControlSet\Control\Class\{4d36e96c-e325-11ce-bfc1-08002be10318}\<NNNN>
50        use crate::win_reg;
51        const MEDIA_CLASS: &str =
52            "SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e96c-e325-11ce-bfc1-08002be10318}";
53
54        let mut devices = Vec::new();
55        for subkey_name in win_reg::enum_reg_subkeys(win_reg::HKEY_LOCAL_MACHINE, MEDIA_CLASS) {
56            if subkey_name.eq_ignore_ascii_case("Properties") {
57                continue;
58            }
59            let subkey = format!("{}\\{}", MEDIA_CLASS, subkey_name);
60            if let Some(name) =
61                win_reg::get_reg_string(win_reg::HKEY_LOCAL_MACHINE, &subkey, "DriverDesc")
62            {
63                if let Some(clean_name) = normalize_win_audio_device(name.trim()) {
64                    if !devices.contains(&clean_name) {
65                        devices.push(clean_name);
66                    }
67                }
68            }
69        }
70
71        if !devices.is_empty() {
72            Some(format!("Windows Audio ({})", devices.join(", ")))
73        } else {
74            Some("Windows Audio".to_string())
75        }
76    }
77
78    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
79    {
80        let _ = sys;
81        None
82    }
83}
84
85#[allow(dead_code)]
86pub fn parse_asound_cards(content: &str, asound_dir: &str) -> Vec<String> {
87    let mut devices = Vec::new();
88    if let Ok(entries) = std::fs::read_dir(asound_dir) {
89        for entry in entries.filter_map(|e| e.ok()) {
90            let path = entry.path();
91            if path.is_dir() {
92                let name = entry.file_name().to_string_lossy().to_string();
93                if name.starts_with("card") {
94                    if let Ok(sub_entries) = std::fs::read_dir(&path) {
95                        for sub_entry in sub_entries.filter_map(|se| se.ok()) {
96                            let sub_path = sub_entry.path();
97                            let sub_name = sub_entry.file_name().to_string_lossy().to_string();
98                            if sub_name.starts_with("codec#") {
99                                if let Ok(codec_content) = std::fs::read_to_string(&sub_path) {
100                                    for line in codec_content.lines() {
101                                        if let Some(stripped) = line.strip_prefix("Codec: ") {
102                                            let codec_name = stripped.trim().to_string();
103                                            if !codec_name.is_empty()
104                                                && !devices.contains(&codec_name)
105                                            {
106                                                devices.push(codec_name);
107                                            }
108                                        }
109                                    }
110                                }
111                            }
112                        }
113                    }
114                }
115            }
116        }
117    }
118
119    if devices.is_empty() {
120        for line in content.lines() {
121            if let Some(idx) = line.find("]: ") {
122                let desc = line[idx + 3..].trim();
123                let device_name = if let Some(dash_idx) = desc.find(" - ") {
124                    desc[dash_idx + 3..].trim()
125                } else {
126                    desc
127                };
128                if !device_name.is_empty() && !devices.contains(&device_name.to_string()) {
129                    devices.push(device_name.to_string());
130                }
131            }
132        }
133    }
134    devices
135}
136
137/// Filter out synthetic software proxy audio drivers and normalize root hardware controller names on Windows.
138#[allow(dead_code)]
139pub fn normalize_win_audio_device(name: &str) -> Option<String> {
140    let lower = name.to_lowercase();
141    if lower.is_empty()
142        || lower.starts_with("microsoft ")
143        || lower.contains("trusted audio")
144        || lower.contains("a2dp")
145        || lower.contains("render audio")
146        || lower.contains("capture audio")
147        || lower.contains("uaj ")
148        || lower.contains("speaker device")
149        || lower.contains("microphone device")
150    {
151        return None;
152    }
153    if lower.contains("soundwire") {
154        return Some("AMD SoundWire Audio".to_string());
155    }
156    if lower.contains("amd high definition audio") || lower == "amd audio device" {
157        return Some("AMD High Definition Audio".to_string());
158    }
159    if lower.contains("realtek") {
160        return Some("Realtek High Definition Audio".to_string());
161    }
162    if lower.contains("nvidia") {
163        return Some("NVIDIA High Definition Audio".to_string());
164    }
165    if lower.contains("intel") {
166        return Some("Intel Smart Sound Technology".to_string());
167    }
168    if lower.contains("streaming") {
169        return None;
170    }
171    Some(name.to_string())
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_parse_asound_cards() {
180        let sample = " 0 [PCH            ]: HDA-Intel - HDA Intel PCH\n 1 [NVidia         ]: HDA-Intel - HDA NVIDIA HDMI\n 2 [sofhdadsp      ]: sof-hda-dsp - sof-hda-dsp\n                      DellInc.-Inspiron1676302_in_1-0DR8JD\n";
181        let parsed = parse_asound_cards(sample, "/nonexistent");
182        assert_eq!(
183            parsed,
184            vec![
185                "HDA Intel PCH".to_string(),
186                "HDA NVIDIA HDMI".to_string(),
187                "sof-hda-dsp".to_string()
188            ]
189        );
190    }
191
192    #[test]
193    fn test_normalize_win_audio_device_filters_synthetic_and_normalizes() {
194        assert_eq!(
195            normalize_win_audio_device("Microsoft Streaming Service Proxy"),
196            None
197        );
198        assert_eq!(
199            normalize_win_audio_device("Microsoft Bluetooth A2dp Source"),
200            None
201        );
202        assert_eq!(
203            normalize_win_audio_device("AMD SoundWire Audio Streaming Speaker Device"),
204            None
205        );
206        assert_eq!(
207            normalize_win_audio_device("AMD SoundWire Audio Streaming Device"),
208            Some("AMD SoundWire Audio".to_string())
209        );
210        assert_eq!(
211            normalize_win_audio_device("USB Audio Device"),
212            Some("USB Audio Device".to_string())
213        );
214        assert_eq!(
215            normalize_win_audio_device("AMD High Definition Audio Device"),
216            Some("AMD High Definition Audio".to_string())
217        );
218    }
219}