Skip to main content

rtp_core/audio_device/
mod.rs

1//! Audio device abstraction for microphone capture and speaker playback.
2//!
3//! Requires the `audio-device` feature flag (backed by `cpal`).
4//! On headless systems without audio hardware, the module compiles but
5//! device enumeration returns empty lists.
6
7#[cfg(feature = "audio-device")]
8mod cpal_backend;
9
10#[cfg(feature = "audio-device")]
11pub use cpal_backend::*;
12
13use std::fmt;
14
15/// Describes an audio device (input or output).
16#[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/// Type of audio device.
42#[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/// Audio device selection criteria.
58#[derive(Debug, Clone)]
59pub enum DeviceSelector {
60    /// Use the system default device.
61    Default,
62    /// Select by device name (substring match).
63    ByName(String),
64    /// Select by index from the device list.
65    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/// Configuration for audio capture/playback.
91#[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    /// Standard telephony config: 8kHz mono, 20ms frames.
100    pub fn telephony() -> Self {
101        Self {
102            sample_rate: 8000,
103            channels: 1,
104            frame_size_ms: 20,
105        }
106    }
107
108    /// Samples per frame.
109    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/// Stub implementations when `audio-device` feature is not enabled.
121/// These allow the CLI to compile and provide helpful messages.
122#[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
147/// Test tone generator that produces audio frames for device testing.
148pub 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    /// Generate the next frame of audio samples.
166    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        // Phase should continue, so frame2 != frame1 in general
263        // (unless frequency perfectly divides sample rate * frame_size)
264    }
265
266    #[test]
267    fn test_test_tone_continuous_phase() {
268        let mut gen = TestToneGenerator::new(400.0, 8000, 16000);
269        // Generate multiple frames and check they have reasonable amplitude
270        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}