Skip to main content

rtp_engine/
device.rs

1//! Audio device abstraction for capture and playback.
2//!
3//! Provides cross-platform audio I/O using cpal, with automatic resampling
4//! between device rates and codec rates (8kHz for G.711).
5
6use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
7use std::collections::VecDeque;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::{Arc, Mutex};
10
11use crate::error::{Error, Result};
12use crate::resample::{f32_to_i16, i16_to_f32, resample_linear};
13
14/// Audio capture device (microphone).
15pub struct AudioCapture {
16    _stream: cpal::Stream,
17    running: Arc<AtomicBool>,
18    buffer: Arc<Mutex<Vec<f32>>>,
19    device_rate: u32,
20}
21
22impl AudioCapture {
23    /// Start capturing audio from the default input device.
24    ///
25    /// Samples are buffered internally and can be retrieved with `read_samples()`.
26    pub fn start() -> Result<Self> {
27        let host = cpal::default_host();
28        let device = host
29            .default_input_device()
30            .ok_or_else(|| Error::device("No input audio device"))?;
31
32        let config = device
33            .default_input_config()
34            .map_err(|e| Error::device(format!("No default input config: {}", e)))?;
35
36        let device_rate = config.sample_rate();
37        log::info!("Audio capture: device rate = {} Hz", device_rate);
38
39        let stream_config = cpal::StreamConfig {
40            channels: 1,
41            sample_rate: config.sample_rate(),
42            buffer_size: cpal::BufferSize::Default,
43        };
44
45        let running = Arc::new(AtomicBool::new(true));
46        let buffer: Arc<Mutex<Vec<f32>>> = Arc::new(Mutex::new(Vec::with_capacity(8192)));
47
48        let cb_running = running.clone();
49        let cb_buffer = buffer.clone();
50
51        let stream = device
52            .build_input_stream(
53                &stream_config,
54                move |data: &[f32], _: &cpal::InputCallbackInfo| {
55                    if !cb_running.load(Ordering::Relaxed) {
56                        return;
57                    }
58                    if let Ok(mut buf) = cb_buffer.lock() {
59                        buf.extend_from_slice(data);
60                        // Limit buffer size to ~1 second
61                        while buf.len() > device_rate as usize {
62                            buf.drain(..device_rate as usize / 10);
63                        }
64                    }
65                },
66                |err| log::error!("Audio capture error: {}", err),
67                None,
68            )
69            .map_err(|e| Error::device(format!("Failed to build input stream: {}", e)))?;
70
71        stream
72            .play()
73            .map_err(|e| Error::device(format!("Failed to start capture: {}", e)))?;
74
75        Ok(Self {
76            _stream: stream,
77            running,
78            buffer,
79            device_rate,
80        })
81    }
82
83    /// Read samples from the capture buffer, resampled to the target rate.
84    ///
85    /// Returns up to `max_samples` samples at the target sample rate.
86    pub fn read_samples(&self, target_rate: u32, max_samples: usize) -> Vec<i16> {
87        let mut result = Vec::new();
88
89        if let Ok(mut buf) = self.buffer.lock() {
90            if buf.is_empty() {
91                return result;
92            }
93
94            // Calculate how many device samples we need for the requested output
95            let device_samples_needed = ((max_samples as f64)
96                * (self.device_rate as f64 / target_rate as f64))
97                .ceil() as usize;
98            let available = buf.len().min(device_samples_needed);
99
100            if available > 0 {
101                let samples: Vec<f32> = buf.drain(..available).collect();
102                let resampled = resample_linear(&samples, self.device_rate, target_rate);
103                result = f32_to_i16(&resampled);
104            }
105        }
106
107        result
108    }
109
110    /// Get the native device sample rate.
111    pub fn device_rate(&self) -> u32 {
112        self.device_rate
113    }
114
115    /// Stop capturing.
116    pub fn stop(&self) {
117        self.running.store(false, Ordering::Relaxed);
118    }
119}
120
121impl Drop for AudioCapture {
122    fn drop(&mut self) {
123        self.stop();
124    }
125}
126
127/// Audio playback device (speaker).
128pub struct AudioPlayback {
129    _stream: cpal::Stream,
130    running: Arc<AtomicBool>,
131    buffer: Arc<Mutex<VecDeque<f32>>>,
132    device_rate: u32,
133}
134
135impl AudioPlayback {
136    /// Start audio playback to the default output device.
137    ///
138    /// Samples can be written with `write_samples()`.
139    pub fn start() -> Result<Self> {
140        let host = cpal::default_host();
141        let device = host
142            .default_output_device()
143            .ok_or_else(|| Error::device("No output audio device"))?;
144
145        let config = device
146            .default_output_config()
147            .map_err(|e| Error::device(format!("No default output config: {}", e)))?;
148
149        let device_rate = config.sample_rate();
150        log::info!("Audio playback: device rate = {} Hz", device_rate);
151
152        let stream_config = cpal::StreamConfig {
153            channels: 1,
154            sample_rate: config.sample_rate(),
155            buffer_size: cpal::BufferSize::Default,
156        };
157
158        let running = Arc::new(AtomicBool::new(true));
159        let buffer: Arc<Mutex<VecDeque<f32>>> =
160            Arc::new(Mutex::new(VecDeque::with_capacity(device_rate as usize)));
161
162        let cb_buffer = buffer.clone();
163
164        let stream = device
165            .build_output_stream(
166                &stream_config,
167                move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
168                    if let Ok(mut buf) = cb_buffer.lock() {
169                        for sample in data.iter_mut() {
170                            *sample = buf.pop_front().unwrap_or(0.0);
171                        }
172                    } else {
173                        for sample in data.iter_mut() {
174                            *sample = 0.0;
175                        }
176                    }
177                },
178                |err| log::error!("Audio playback error: {}", err),
179                None,
180            )
181            .map_err(|e| Error::device(format!("Failed to build output stream: {}", e)))?;
182
183        stream
184            .play()
185            .map_err(|e| Error::device(format!("Failed to start playback: {}", e)))?;
186
187        Ok(Self {
188            _stream: stream,
189            running,
190            buffer,
191            device_rate,
192        })
193    }
194
195    /// Write samples to the playback buffer.
196    ///
197    /// Samples are resampled from the source rate to the device rate.
198    pub fn write_samples(&self, samples: &[i16], source_rate: u32) {
199        let f32_samples = i16_to_f32(samples);
200        let resampled = resample_linear(&f32_samples, source_rate, self.device_rate);
201
202        if let Ok(mut buf) = self.buffer.lock() {
203            for s in resampled {
204                buf.push_back(s);
205            }
206            // Limit buffer size to ~1 second
207            while buf.len() > self.device_rate as usize {
208                buf.pop_front();
209            }
210        }
211    }
212
213    /// Get the native device sample rate.
214    pub fn device_rate(&self) -> u32 {
215        self.device_rate
216    }
217
218    /// Stop playback.
219    pub fn stop(&self) {
220        self.running.store(false, Ordering::Relaxed);
221    }
222}
223
224impl Drop for AudioPlayback {
225    fn drop(&mut self) {
226        self.stop();
227    }
228}
229
230/// Query available audio devices.
231pub fn list_devices() -> Result<Vec<String>> {
232    let host = cpal::default_host();
233    let mut devices = Vec::new();
234
235    if let Ok(input_devices) = host.input_devices() {
236        for device in input_devices {
237            if let Ok(desc) = device.description() {
238                devices.push(format!("Input: {}", desc.name()));
239            }
240        }
241    }
242
243    if let Ok(output_devices) = host.output_devices() {
244        for device in output_devices {
245            if let Ok(desc) = device.description() {
246                devices.push(format!("Output: {}", desc.name()));
247            }
248        }
249    }
250
251    Ok(devices)
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_list_devices() {
260        // This test may fail in CI without audio devices, but should work locally
261        let result = list_devices();
262        assert!(result.is_ok());
263    }
264}