sys_voice/
lib.rs

1mod backends;
2mod resampler;
3
4use resampler::Resampler;
5use thiserror::Error;
6
7/// Output channel configuration
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
9pub enum Channels {
10    #[default]
11    Mono,
12    Stereo,
13}
14
15#[derive(Debug, Clone)]
16pub struct AecConfig {
17    /// Target sample rate in Hz (typically 48000)
18    pub sample_rate: u32,
19    /// Output channels (stereo = duplicated mono from AEC)
20    pub channels: Channels,
21}
22
23impl Default for AecConfig {
24    fn default() -> Self {
25        Self {
26            sample_rate: 48000,
27            channels: Channels::Mono,
28        }
29    }
30}
31
32#[derive(Debug, Error)]
33pub enum AecError {
34    #[error("audio device unavailable")]
35    DeviceUnavailable,
36
37    #[error("microphone permission denied")]
38    PermissionDenied,
39
40    #[error("AEC not supported on this device")]
41    AecNotSupported,
42
43    #[error("invalid configuration: {0}")]
44    InvalidConfig(String),
45
46    #[error("backend error: {0}")]
47    BackendError(String),
48}
49
50/// Handle for receiving AEC-processed audio samples.
51/// Capture stops automatically when dropped (channel disconnect stops backend).
52pub struct CaptureHandle {
53    receiver: flume::Receiver<Result<Vec<f32>, AecError>>,
54    sample_rate: u32,
55}
56
57impl CaptureHandle {
58    /// Create and start a new AEC capture stream.
59    /// Audio samples are received via the async recv() or blocking recv_blocking() methods.
60    pub fn new(config: AecConfig) -> Result<Self, AecError> {
61        if config.sample_rate == 0 {
62            return Err(AecError::InvalidConfig(
63                "sample_rate must be non-zero".to_string(),
64            ));
65        }
66
67        let (backend_tx, backend_rx) = flume::bounded::<Vec<f32>>(32);
68        let (native_rate, _buffer_size) = backends::create_backend(backend_tx)?;
69
70        let (public_tx, public_rx) = flume::bounded::<Result<Vec<f32>, AecError>>(32);
71        let target_rate = config.sample_rate;
72        let target_channels = config.channels;
73
74        let needs_stereo = target_channels == Channels::Stereo;
75        let needs_resampling = native_rate != target_rate;
76
77        let resampler = if needs_resampling {
78            Some(
79                Resampler::new(native_rate, target_rate)
80                    .map_err(|e| AecError::BackendError(format!("resampler init: {e:?}")))?,
81            )
82        } else {
83            None
84        };
85
86        tokio::spawn(async move {
87            let mut resampler = resampler;
88
89            while let Ok(samples) = backend_rx.recv_async().await {
90                let processed = match process_audio_chunk(samples, &mut resampler, needs_stereo) {
91                    Ok(p) => p,
92                    Err(e) => {
93                        let _ = public_tx.send_async(Err(AecError::BackendError(e))).await;
94                        break;
95                    }
96                };
97                if public_tx.send_async(Ok(processed)).await.is_err() {
98                    break;
99                }
100            }
101        });
102
103        Ok(Self {
104            receiver: public_rx,
105            sample_rate: target_rate,
106        })
107    }
108
109    /// Receive audio samples asynchronously.
110    /// Returns None when the capture stream is closed.
111    pub async fn recv(&self) -> Option<Result<Vec<f32>, AecError>> {
112        self.receiver.recv_async().await.ok()
113    }
114
115    /// Receive audio samples, blocking the current thread.
116    /// Returns None when the capture stream is closed.
117    pub fn recv_blocking(&self) -> Option<Result<Vec<f32>, AecError>> {
118        self.receiver.recv().ok()
119    }
120
121    /// Try to receive audio samples without blocking.
122    /// Returns None if no samples are available or stream is closed.
123    pub fn try_recv(&self) -> Option<Result<Vec<f32>, AecError>> {
124        self.receiver.try_recv().ok()
125    }
126
127    /// Get the actual sample rate being used by the backend.
128    /// May differ from requested rate if resampling is active.
129    pub fn native_sample_rate(&self) -> u32 {
130        self.sample_rate
131    }
132}
133
134// Drop on CaptureHandle drops backend, which stops capture via RAII
135
136fn process_audio_chunk(
137    samples: Vec<f32>,
138    resampler: &mut Option<Resampler>,
139    needs_stereo: bool,
140) -> Result<Vec<f32>, String> {
141    let samples = if let Some(r) = resampler {
142        r.process(&samples)
143            .map_err(|e| format!("resample: {e:?}"))?
144    } else {
145        samples
146    };
147
148    if needs_stereo {
149        Ok(samples.iter().flat_map(|&s| [s, s]).collect())
150    } else {
151        Ok(samples)
152    }
153}