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 streaming audio chunks to the playback buffer.
51/// Drop the handle to signal end of stream.
52pub struct PlaybackStreamHandle {
53    chunk_tx: flume::Sender<Vec<f32>>,
54}
55
56impl PlaybackStreamHandle {
57    /// Send a chunk of audio samples to the playback buffer.
58    pub fn send(&self, chunk: Vec<f32>) -> Result<(), AecError> {
59        self.chunk_tx
60            .send(chunk)
61            .map_err(|_| AecError::BackendError("playback stream closed".to_string()))
62    }
63
64    /// Send a chunk of audio samples asynchronously.
65    pub async fn send_async(&self, chunk: Vec<f32>) -> Result<(), AecError> {
66        self.chunk_tx
67            .send_async(chunk)
68            .await
69            .map_err(|_| AecError::BackendError("playback stream closed".to_string()))
70    }
71}
72
73/// Handle for receiving AEC-processed audio samples.
74/// Capture stops automatically when dropped (channel disconnect stops backend).
75pub struct CaptureHandle {
76    receiver: flume::Receiver<Result<Vec<f32>, AecError>>,
77    backend: backends::BackendHandle,
78    sample_rate: u32,
79}
80
81impl CaptureHandle {
82    /// Create and start a new AEC capture stream.
83    /// Audio samples are received via the async recv() or blocking recv_blocking() methods.
84    pub fn new(config: AecConfig) -> Result<Self, AecError> {
85        if config.sample_rate == 0 {
86            return Err(AecError::InvalidConfig(
87                "sample_rate must be non-zero".to_string(),
88            ));
89        }
90
91        let (backend_tx, backend_rx) = flume::bounded::<Vec<f32>>(32);
92        let (native_rate, _buffer_size, backend_handle) = backends::create_backend(backend_tx)?;
93
94        let (public_tx, public_rx) = flume::bounded::<Result<Vec<f32>, AecError>>(32);
95        let target_rate = config.sample_rate;
96        let target_channels = config.channels;
97
98        let needs_stereo = target_channels == Channels::Stereo;
99        let needs_resampling = native_rate != target_rate;
100
101        let resampler = if needs_resampling {
102            Some(
103                Resampler::new(native_rate, target_rate)
104                    .map_err(|e| AecError::BackendError(format!("resampler init: {e:?}")))?,
105            )
106        } else {
107            None
108        };
109
110        tokio::spawn(async move {
111            let mut resampler = resampler;
112
113            while let Ok(samples) = backend_rx.recv_async().await {
114                let processed = match process_audio_chunk(samples, &mut resampler, needs_stereo) {
115                    Ok(p) => p,
116                    Err(e) => {
117                        let _ = public_tx.send_async(Err(AecError::BackendError(e))).await;
118                        break;
119                    }
120                };
121                if public_tx.send_async(Ok(processed)).await.is_err() {
122                    break;
123                }
124            }
125        });
126
127        Ok(Self {
128            receiver: public_rx,
129            backend: backend_handle,
130            sample_rate: target_rate,
131        })
132    }
133
134    /// Receive audio samples asynchronously.
135    /// Returns None when the capture stream is closed.
136    pub async fn recv(&self) -> Option<Result<Vec<f32>, AecError>> {
137        self.receiver.recv_async().await.ok()
138    }
139
140    /// Receive audio samples, blocking the current thread.
141    /// Returns None when the capture stream is closed.
142    pub fn recv_blocking(&self) -> Option<Result<Vec<f32>, AecError>> {
143        self.receiver.recv().ok()
144    }
145
146    /// Try to receive audio samples without blocking.
147    /// Returns None if no samples are available or stream is closed.
148    pub fn try_recv(&self) -> Option<Result<Vec<f32>, AecError>> {
149        self.receiver.try_recv().ok()
150    }
151
152    /// Get the actual sample rate being used by the backend.
153    /// May differ from requested rate if resampling is active.
154    pub fn native_sample_rate(&self) -> u32 {
155        self.sample_rate
156    }
157
158    /// Play audio through the same engine used for capture.
159    /// This enables AEC to cancel the played audio from the recording.
160    /// Audio is played at the specified sample rate.
161    pub fn play_audio(&self, samples: Vec<f32>, sample_rate: u32) -> Result<(), AecError> {
162        self.backend.play_audio(samples, sample_rate)
163    }
164
165    /// Start a streaming playback session.
166    /// Returns a handle for sending audio chunks incrementally.
167    /// The stream ends when the handle is dropped.
168    pub fn start_playback_stream(
169        &self,
170        sample_rate: u32,
171    ) -> Result<PlaybackStreamHandle, AecError> {
172        let chunk_tx = self.backend.start_playback_stream(sample_rate)?;
173        Ok(PlaybackStreamHandle { chunk_tx })
174    }
175}
176
177// Drop on CaptureHandle drops backend, which stops capture via RAII
178
179fn process_audio_chunk(
180    samples: Vec<f32>,
181    resampler: &mut Option<Resampler>,
182    needs_stereo: bool,
183) -> Result<Vec<f32>, String> {
184    let samples = if let Some(r) = resampler {
185        r.process(&samples)
186            .map_err(|e| format!("resample: {e:?}"))?
187    } else {
188        samples
189    };
190
191    if needs_stereo {
192        Ok(samples.iter().flat_map(|&s| [s, s]).collect())
193    } else {
194        Ok(samples)
195    }
196}