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 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#[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}