1pub 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 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}