1#[cfg(feature = "audio-device")]
8mod cpal_backend;
9
10#[cfg(feature = "audio-device")]
11pub use cpal_backend::*;
12
13use std::fmt;
14
15#[derive(Debug, Clone)]
17pub struct AudioDeviceInfo {
18 pub name: String,
19 pub device_type: DeviceType,
20 pub sample_rates: Vec<u32>,
21 pub channels: Vec<u16>,
22 pub is_default: bool,
23}
24
25impl fmt::Display for AudioDeviceInfo {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 let default_marker = if self.is_default { " (default)" } else { "" };
28 let rates: Vec<String> = self.sample_rates.iter().map(|r| format!("{}Hz", r)).collect();
29 write!(
30 f,
31 "{}{} [{}] rates=[{}] channels={:?}",
32 self.name,
33 default_marker,
34 self.device_type,
35 rates.join(", "),
36 self.channels,
37 )
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum DeviceType {
44 Input,
45 Output,
46}
47
48impl fmt::Display for DeviceType {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 match self {
51 DeviceType::Input => write!(f, "input"),
52 DeviceType::Output => write!(f, "output"),
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
59pub enum DeviceSelector {
60 Default,
62 ByName(String),
64 ByIndex(usize),
66}
67
68impl DeviceSelector {
69 pub fn from_arg(arg: &str) -> Self {
70 if arg.eq_ignore_ascii_case("default") {
71 DeviceSelector::Default
72 } else if let Ok(idx) = arg.parse::<usize>() {
73 DeviceSelector::ByIndex(idx)
74 } else {
75 DeviceSelector::ByName(arg.to_string())
76 }
77 }
78}
79
80impl fmt::Display for DeviceSelector {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 match self {
83 DeviceSelector::Default => write!(f, "default"),
84 DeviceSelector::ByName(name) => write!(f, "\"{}\"", name),
85 DeviceSelector::ByIndex(idx) => write!(f, "#{}", idx),
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct AudioConfig {
93 pub sample_rate: u32,
94 pub channels: u16,
95 pub frame_size_ms: u32,
96}
97
98impl AudioConfig {
99 pub fn telephony() -> Self {
101 Self {
102 sample_rate: 8000,
103 channels: 1,
104 frame_size_ms: 20,
105 }
106 }
107
108 pub fn samples_per_frame(&self) -> usize {
110 (self.sample_rate as usize * self.frame_size_ms as usize) / 1000
111 }
112}
113
114impl Default for AudioConfig {
115 fn default() -> Self {
116 Self::telephony()
117 }
118}
119
120#[cfg(not(feature = "audio-device"))]
123pub fn list_devices() -> Vec<AudioDeviceInfo> {
124 Vec::new()
125}
126
127#[cfg(not(feature = "audio-device"))]
128pub fn list_input_devices() -> Vec<AudioDeviceInfo> {
129 Vec::new()
130}
131
132#[cfg(not(feature = "audio-device"))]
133pub fn list_output_devices() -> Vec<AudioDeviceInfo> {
134 Vec::new()
135}
136
137#[cfg(not(feature = "audio-device"))]
138pub fn is_audio_available() -> bool {
139 false
140}
141
142#[cfg(not(feature = "audio-device"))]
143pub fn audio_unavailable_reason() -> &'static str {
144 "Compiled without audio-device feature. Rebuild with: cargo build --features audio-device"
145}
146
147pub struct TestToneGenerator {
149 frequency: f64,
150 sample_rate: u32,
151 amplitude: i16,
152 phase: f64,
153}
154
155impl TestToneGenerator {
156 pub fn new(frequency: f64, sample_rate: u32, amplitude: i16) -> Self {
157 Self {
158 frequency,
159 sample_rate,
160 amplitude,
161 phase: 0.0,
162 }
163 }
164
165 pub fn next_frame(&mut self, num_samples: usize) -> Vec<i16> {
167 let mut samples = Vec::with_capacity(num_samples);
168 let phase_increment = 2.0 * std::f64::consts::PI * self.frequency / self.sample_rate as f64;
169
170 for _ in 0..num_samples {
171 let sample = (self.phase.sin() * self.amplitude as f64) as i16;
172 samples.push(sample);
173 self.phase += phase_increment;
174 if self.phase > 2.0 * std::f64::consts::PI {
175 self.phase -= 2.0 * std::f64::consts::PI;
176 }
177 }
178
179 samples
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_device_type_display() {
189 assert_eq!(DeviceType::Input.to_string(), "input");
190 assert_eq!(DeviceType::Output.to_string(), "output");
191 }
192
193 #[test]
194 fn test_device_selector_from_arg() {
195 assert!(matches!(DeviceSelector::from_arg("default"), DeviceSelector::Default));
196 assert!(matches!(DeviceSelector::from_arg("0"), DeviceSelector::ByIndex(0)));
197 assert!(matches!(DeviceSelector::from_arg("2"), DeviceSelector::ByIndex(2)));
198 assert!(matches!(DeviceSelector::from_arg("My Mic"), DeviceSelector::ByName(ref s) if s == "My Mic"));
199 }
200
201 #[test]
202 fn test_device_selector_display() {
203 assert_eq!(DeviceSelector::Default.to_string(), "default");
204 assert_eq!(DeviceSelector::ByIndex(3).to_string(), "#3");
205 assert_eq!(DeviceSelector::ByName("USB Mic".into()).to_string(), "\"USB Mic\"");
206 }
207
208 #[test]
209 fn test_audio_config_telephony() {
210 let cfg = AudioConfig::telephony();
211 assert_eq!(cfg.sample_rate, 8000);
212 assert_eq!(cfg.channels, 1);
213 assert_eq!(cfg.frame_size_ms, 20);
214 assert_eq!(cfg.samples_per_frame(), 160);
215 }
216
217 #[test]
218 fn test_audio_config_default() {
219 let cfg = AudioConfig::default();
220 assert_eq!(cfg.sample_rate, 8000);
221 }
222
223 #[test]
224 fn test_audio_device_info_display() {
225 let info = AudioDeviceInfo {
226 name: "Test Mic".to_string(),
227 device_type: DeviceType::Input,
228 sample_rates: vec![8000, 44100, 48000],
229 channels: vec![1, 2],
230 is_default: true,
231 };
232 let s = info.to_string();
233 assert!(s.contains("Test Mic"));
234 assert!(s.contains("(default)"));
235 assert!(s.contains("input"));
236 assert!(s.contains("8000Hz"));
237 }
238
239 #[test]
240 fn test_audio_device_info_non_default() {
241 let info = AudioDeviceInfo {
242 name: "HDMI Output".to_string(),
243 device_type: DeviceType::Output,
244 sample_rates: vec![48000],
245 channels: vec![2],
246 is_default: false,
247 };
248 let s = info.to_string();
249 assert!(!s.contains("(default)"));
250 assert!(s.contains("output"));
251 }
252
253 #[test]
254 fn test_test_tone_generator() {
255 let mut gen = TestToneGenerator::new(440.0, 8000, 12000);
256 let frame1 = gen.next_frame(160);
257 assert_eq!(frame1.len(), 160);
258 assert!(frame1.iter().any(|&s| s != 0));
259
260 let frame2 = gen.next_frame(160);
261 assert_eq!(frame2.len(), 160);
262 }
265
266 #[test]
267 fn test_test_tone_continuous_phase() {
268 let mut gen = TestToneGenerator::new(400.0, 8000, 16000);
269 for _ in 0..50 {
271 let frame = gen.next_frame(160);
272 let max = frame.iter().map(|s| s.abs()).max().unwrap();
273 assert!(max > 10000, "Tone amplitude should stay consistent");
274 }
275 }
276
277 #[cfg(not(feature = "audio-device"))]
278 #[test]
279 fn test_stub_no_devices() {
280 assert_eq!(list_devices().len(), 0);
281 assert_eq!(list_input_devices().len(), 0);
282 assert_eq!(list_output_devices().len(), 0);
283 assert!(!is_audio_available());
284 assert!(audio_unavailable_reason().contains("audio-device"));
285 }
286}