1use std::collections::{HashMap, HashSet};
4
5use cpal::traits::HostTrait;
6use serde::Serialize;
7
8use crate::devices::{collect_devices, max_channels, preferred_channels};
9
10#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
11#[serde(rename_all = "camelCase")]
12pub struct AudioDeviceInfo {
13 pub name: String,
14 pub max_input_channels: u16,
15 pub max_output_channels: u16,
16 pub preferred_input_channels: u16,
17 pub preferred_output_channels: u16,
18 pub is_default_input: bool,
19 pub is_default_output: bool,
20}
21
22#[derive(Debug, Clone, Serialize, PartialEq, Eq, Default)]
23pub struct DevicesResponse {
24 pub inputs: Vec<AudioDeviceInfo>,
25 pub outputs: Vec<AudioDeviceInfo>,
26 pub all: Vec<AudioDeviceInfo>,
27}
28
29pub fn list_audio_devices() -> anyhow::Result<DevicesResponse> {
31 let host = cpal::default_host();
32 let default_input_name = host.default_input_device().map(|d| d.to_string());
33 let default_output_name = host.default_output_device().map(|d| d.to_string());
34
35 let input_devices = collect_devices(&host, true)?;
36 let output_devices = collect_devices(&host, false)?;
37
38 let mut names = Vec::new();
39 let mut seen = HashSet::new();
40 for d in input_devices.iter().chain(output_devices.iter()) {
41 let name = d.to_string();
42 if seen.insert(name.clone()) {
43 names.push(name);
44 }
45 }
46
47 let input_by_name: HashMap<String, _> = input_devices
48 .iter()
49 .map(|d| (d.to_string(), d.clone()))
50 .collect();
51 let output_by_name: HashMap<String, _> = output_devices
52 .iter()
53 .map(|d| (d.to_string(), d.clone()))
54 .collect();
55
56 let mut all = Vec::new();
57 for name in names {
58 let input = input_by_name.get(&name);
59 let output = output_by_name.get(&name);
60 all.push(AudioDeviceInfo {
61 max_input_channels: input.and_then(|d| max_channels(d, true)).unwrap_or(0),
62 max_output_channels: output.and_then(|d| max_channels(d, false)).unwrap_or(0),
63 preferred_input_channels: input.map(|d| preferred_channels(d, true)).unwrap_or(0),
64 preferred_output_channels: output.map(|d| preferred_channels(d, false)).unwrap_or(0),
65 is_default_input: default_input_name.as_deref() == Some(name.as_str()),
66 is_default_output: default_output_name.as_deref() == Some(name.as_str()),
67 name,
68 });
69 }
70
71 all.sort_by(|a, b| a.name.cmp(&b.name));
72 let inputs = all
73 .iter()
74 .filter(|d| d.max_input_channels > 0)
75 .cloned()
76 .collect();
77 let outputs = all
78 .iter()
79 .filter(|d| d.max_output_channels > 0)
80 .cloned()
81 .collect();
82
83 Ok(DevicesResponse {
84 inputs,
85 outputs,
86 all,
87 })
88}
89
90fn device_fingerprint(response: &DevicesResponse) -> String {
94 let mut entries: Vec<(&str, u16, u16, bool, bool)> = response
95 .all
96 .iter()
97 .map(|d| {
98 (
99 d.name.as_str(),
100 d.max_input_channels,
101 d.max_output_channels,
102 d.is_default_input,
103 d.is_default_output,
104 )
105 })
106 .collect();
107 entries.sort_by(|a, b| a.0.cmp(b.0));
108 entries
109 .into_iter()
110 .map(|(n, i, o, di, do_)| format!("{n}:{i}/{o}/{di}/{do_}"))
111 .collect::<Vec<_>>()
112 .join(";")
113}
114
115pub fn device_diff(prev: &DevicesResponse, curr: &DevicesResponse) -> Vec<String> {
118 if device_fingerprint(prev) == device_fingerprint(curr) {
119 return Vec::new();
120 }
121
122 let prev_map: HashMap<&str, &AudioDeviceInfo> =
123 prev.all.iter().map(|d| (d.name.as_str(), d)).collect();
124 let curr_map: HashMap<&str, &AudioDeviceInfo> =
125 curr.all.iter().map(|d| (d.name.as_str(), d)).collect();
126
127 let prev_names: HashSet<&str> = prev_map.keys().copied().collect();
128 let curr_names: HashSet<&str> = curr_map.keys().copied().collect();
129
130 let mut events = Vec::new();
131
132 let mut added: Vec<&str> = curr_names.difference(&prev_names).copied().collect();
134 added.sort_unstable();
135 for name in added {
136 let d = &curr_map[name];
137 events.push(format!(
138 "{name} connected (in:{}, out:{})",
139 d.max_input_channels, d.max_output_channels
140 ));
141 }
142
143 let mut removed: Vec<&str> = prev_names.difference(&curr_names).copied().collect();
145 removed.sort_unstable();
146 for name in removed {
147 events.push(format!("{name} disconnected"));
148 }
149
150 let mut changed: Vec<&str> = curr_names.intersection(&prev_names).copied().collect();
152 changed.sort_unstable();
153 for name in changed {
154 let p = &prev_map[name];
155 let c = &curr_map[name];
156 if p.max_input_channels != c.max_input_channels
157 || p.max_output_channels != c.max_output_channels
158 {
159 events.push(format!(
160 "{name} channels changed (in:{}→{}, out:{}→{})",
161 p.max_input_channels,
162 c.max_input_channels,
163 p.max_output_channels,
164 c.max_output_channels
165 ));
166 }
167 if p.is_default_input != c.is_default_input && c.is_default_input {
168 events.push(format!("{name} became default input"));
169 }
170 if p.is_default_output != c.is_default_output && c.is_default_output {
171 events.push(format!("{name} became default output"));
172 }
173 }
174
175 events
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 fn dev(name: &str, i: u16, o: u16) -> AudioDeviceInfo {
183 AudioDeviceInfo {
184 name: name.to_string(),
185 max_input_channels: i,
186 max_output_channels: o,
187 preferred_input_channels: i.min(2),
188 preferred_output_channels: o.min(2),
189 is_default_input: false,
190 is_default_output: false,
191 }
192 }
193
194 fn resp(devs: &[AudioDeviceInfo]) -> DevicesResponse {
195 let all = devs.to_vec();
196 let inputs = all
197 .iter()
198 .filter(|d| d.max_input_channels > 0)
199 .cloned()
200 .collect();
201 let outputs = all
202 .iter()
203 .filter(|d| d.max_output_channels > 0)
204 .cloned()
205 .collect();
206 DevicesResponse {
207 inputs,
208 outputs,
209 all,
210 }
211 }
212
213 #[test]
214 fn no_change_returns_empty() {
215 let r = resp(&[dev("A", 2, 0), dev("B", 0, 2)]);
216 assert!(device_diff(&r, &r).is_empty());
217 }
218
219 #[test]
220 fn device_added() {
221 let prev = resp(&[dev("A", 2, 0)]);
222 let curr = resp(&[dev("A", 2, 0), dev("B", 0, 2)]);
223 let diff = device_diff(&prev, &curr);
224 assert_eq!(diff.len(), 1);
225 assert!(diff[0].contains("B connected"));
226 }
227
228 #[test]
229 fn device_removed() {
230 let prev = resp(&[dev("A", 2, 0), dev("B", 0, 2)]);
231 let curr = resp(&[dev("A", 2, 0)]);
232 let diff = device_diff(&prev, &curr);
233 assert_eq!(diff.len(), 1);
234 assert!(diff[0].contains("B disconnected"));
235 }
236
237 #[test]
238 fn channel_count_changed() {
239 let prev = resp(&[dev("A", 2, 2)]);
240 let curr = resp(&[dev("A", 4, 2)]);
241 let diff = device_diff(&prev, &curr);
242 assert_eq!(diff.len(), 1);
243 assert!(diff[0].contains("channels changed"));
244 }
245
246 #[test]
247 fn fingerprint_order_independent() {
248 let r1 = resp(&[dev("A", 2, 0), dev("B", 0, 2)]);
249 let r2 = resp(&[dev("B", 0, 2), dev("A", 2, 0)]);
250 assert_eq!(device_fingerprint(&r1), device_fingerprint(&r2));
251 }
252}