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                let name = name.trim().to_string();
64                if !name.is_empty() && !devices.contains(&name) {
65                    devices.push(name);
66                }
67            }
68        }
69
70        if !devices.is_empty() {
71            Some(format!("Windows Audio ({})", devices.join(", ")))
72        } else {
73            Some("Windows Audio".to_string())
74        }
75    }
76
77    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
78    {
79        let _ = sys;
80        None
81    }
82}
83
84#[allow(dead_code)]
85pub fn parse_asound_cards(content: &str, asound_dir: &str) -> Vec<String> {
86    let mut devices = Vec::new();
87    if let Ok(entries) = std::fs::read_dir(asound_dir) {
88        for entry in entries.filter_map(|e| e.ok()) {
89            let path = entry.path();
90            if path.is_dir() {
91                let name = entry.file_name().to_string_lossy().to_string();
92                if name.starts_with("card") {
93                    if let Ok(sub_entries) = std::fs::read_dir(&path) {
94                        for sub_entry in sub_entries.filter_map(|se| se.ok()) {
95                            let sub_path = sub_entry.path();
96                            let sub_name = sub_entry.file_name().to_string_lossy().to_string();
97                            if sub_name.starts_with("codec#") {
98                                if let Ok(codec_content) = std::fs::read_to_string(&sub_path) {
99                                    for line in codec_content.lines() {
100                                        if let Some(stripped) = line.strip_prefix("Codec: ") {
101                                            let codec_name = stripped.trim().to_string();
102                                            if !codec_name.is_empty()
103                                                && !devices.contains(&codec_name)
104                                            {
105                                                devices.push(codec_name);
106                                            }
107                                        }
108                                    }
109                                }
110                            }
111                        }
112                    }
113                }
114            }
115        }
116    }
117
118    if devices.is_empty() {
119        for line in content.lines() {
120            if let Some(idx) = line.find("]: ") {
121                let desc = line[idx + 3..].trim();
122                let device_name = if let Some(dash_idx) = desc.find(" - ") {
123                    desc[dash_idx + 3..].trim()
124                } else {
125                    desc
126                };
127                if !device_name.is_empty() && !devices.contains(&device_name.to_string()) {
128                    devices.push(device_name.to_string());
129                }
130            }
131        }
132    }
133    devices
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_parse_asound_cards() {
142        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";
143        let parsed = parse_asound_cards(sample, "/nonexistent");
144        assert_eq!(
145            parsed,
146            vec![
147                "HDA Intel PCH".to_string(),
148                "HDA NVIDIA HDMI".to_string(),
149                "sof-hda-dsp".to_string()
150            ]
151        );
152    }
153}