gemini_genai_rs/buffer/
jitter.rs

1//! Adaptive jitter buffer for smooth playback of network audio.
2//!
3//! Network audio arrives in variable-size bursts. The jitter buffer
4//! smooths playback by accumulating a configurable minimum depth
5//! before starting playback, and adjusting depth dynamically based
6//! on measured inter-arrival jitter (EWMA, similar to TCP RTT estimation).
7
8use std::collections::VecDeque;
9use std::time::Instant;
10
11/// Configuration for the jitter buffer.
12#[derive(Debug, Clone)]
13pub struct JitterConfig {
14    /// Sample rate in Hz (e.g., 24000 for Gemini output).
15    pub sample_rate: u32,
16    /// Minimum buffer depth in samples before playback starts.
17    pub min_depth_samples: usize,
18    /// Maximum buffer depth in samples (overflow drops oldest).
19    pub max_depth_samples: usize,
20    /// EWMA smoothing factor for jitter estimation (0.0–1.0).
21    /// Lower = smoother, higher = more responsive.
22    pub jitter_alpha: f64,
23    /// Multiplier for jitter estimate to compute adaptive min depth.
24    pub target_jitter_multiple: f64,
25}
26
27impl Default for JitterConfig {
28    fn default() -> Self {
29        Self {
30            sample_rate: 24000,
31            min_depth_samples: 24000 / 5, // 200ms at 24kHz
32            max_depth_samples: 24000 * 2, // 2 seconds
33            jitter_alpha: 0.125,          // RFC 6298 default
34            target_jitter_multiple: 2.0,
35        }
36    }
37}
38
39impl JitterConfig {
40    /// Create a config for a given sample rate with sensible defaults.
41    pub fn for_sample_rate(sample_rate: u32) -> Self {
42        Self {
43            sample_rate,
44            min_depth_samples: sample_rate as usize / 5,
45            max_depth_samples: sample_rate as usize * 2,
46            ..Default::default()
47        }
48    }
49}
50
51/// Current state of the jitter buffer.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum BufferState {
54    /// Accumulating initial depth before playback.
55    Filling,
56    /// Normal playback — pulling samples for output.
57    Playing,
58    /// Underrun — generating silence while re-filling.
59    Underrun,
60}
61
62/// Adaptive jitter buffer for audio playback.
63pub struct AudioJitterBuffer {
64    config: JitterConfig,
65    queue: VecDeque<i16>,
66    state: BufferState,
67    /// Smoothed jitter estimate in microseconds.
68    jitter_estimate_us: f64,
69    /// Timestamp of last push.
70    last_arrival: Option<Instant>,
71    /// Total underrun events.
72    underrun_count: u64,
73}
74
75impl AudioJitterBuffer {
76    /// Create a new jitter buffer with the given configuration.
77    pub fn new(config: JitterConfig) -> Self {
78        let initial_capacity = config.max_depth_samples;
79        Self {
80            config,
81            queue: VecDeque::with_capacity(initial_capacity),
82            state: BufferState::Filling,
83            jitter_estimate_us: 0.0,
84            last_arrival: None,
85            underrun_count: 0,
86        }
87    }
88
89    /// Current buffer state.
90    pub fn state(&self) -> BufferState {
91        self.state
92    }
93
94    /// Number of samples currently buffered.
95    pub fn depth(&self) -> usize {
96        self.queue.len()
97    }
98
99    /// Depth in milliseconds.
100    pub fn depth_ms(&self) -> f64 {
101        self.queue.len() as f64 / self.config.sample_rate as f64 * 1000.0
102    }
103
104    /// Total underrun events since creation.
105    pub fn underrun_count(&self) -> u64 {
106        self.underrun_count
107    }
108
109    /// Current smoothed jitter estimate in microseconds.
110    pub fn jitter_estimate_us(&self) -> f64 {
111        self.jitter_estimate_us
112    }
113
114    /// Compute the adaptive minimum depth based on measured jitter.
115    fn adaptive_min_depth(&self) -> usize {
116        let jitter_samples = (self.jitter_estimate_us / 1_000_000.0
117            * self.config.sample_rate as f64
118            * self.config.target_jitter_multiple) as usize;
119        jitter_samples.max(self.config.min_depth_samples)
120    }
121
122    /// Push audio samples into the buffer (called when network data arrives).
123    pub fn push(&mut self, samples: &[i16]) {
124        // Update jitter estimate
125        let now = Instant::now();
126        if let Some(last) = self.last_arrival {
127            let interval_us = now.duration_since(last).as_micros() as f64;
128            // EWMA jitter update (RFC 6298 style)
129            let deviation = (interval_us - self.jitter_estimate_us).abs();
130            self.jitter_estimate_us = self.jitter_estimate_us * (1.0 - self.config.jitter_alpha)
131                + deviation * self.config.jitter_alpha;
132        }
133        self.last_arrival = Some(now);
134
135        // Enforce max depth — drop oldest if overflow
136        let total_after = self.queue.len() + samples.len();
137        if total_after > self.config.max_depth_samples {
138            let to_drop = total_after - self.config.max_depth_samples;
139            self.queue.drain(..to_drop.min(self.queue.len()));
140        }
141
142        // Add samples, respecting max depth constraint
143        self.queue.extend(samples.iter());
144        while self.queue.len() > self.config.max_depth_samples {
145            self.queue.pop_front();
146        }
147
148        // State transitions
149        if (self.state == BufferState::Filling || self.state == BufferState::Underrun)
150            && self.queue.len() >= self.adaptive_min_depth()
151        {
152            self.state = BufferState::Playing;
153        }
154    }
155
156    /// Pull audio samples for playback.
157    ///
158    /// Fills `out` with audio data. If the buffer underruns, fills remaining
159    /// slots with silence (zero) for click-free output.
160    ///
161    /// Returns the number of real (non-silence) samples written.
162    pub fn pull(&mut self, out: &mut [i16]) -> usize {
163        match self.state {
164            BufferState::Filling => {
165                // Not ready yet — fill with silence
166                out.fill(0);
167                0
168            }
169            BufferState::Playing | BufferState::Underrun => {
170                let available = self.queue.len().min(out.len());
171                for (i, sample) in self.queue.drain(..available).enumerate() {
172                    out[i] = sample;
173                }
174
175                // Fill remainder with silence if underrun
176                if available < out.len() {
177                    out[available..].fill(0);
178                    if self.state == BufferState::Playing {
179                        self.state = BufferState::Underrun;
180                        self.underrun_count += 1;
181                    }
182                } else if self.state == BufferState::Underrun
183                    && self.queue.len() >= self.adaptive_min_depth()
184                {
185                    self.state = BufferState::Playing;
186                }
187
188                available
189            }
190        }
191    }
192
193    /// Flush the buffer immediately (used for barge-in).
194    ///
195    /// Drops all buffered audio and resets to the Filling state.
196    /// This produces instant silence when the user starts speaking.
197    pub fn flush(&mut self) {
198        self.queue.clear();
199        self.state = BufferState::Filling;
200        self.last_arrival = None;
201    }
202
203    /// Reset the buffer completely, including jitter estimates.
204    pub fn reset(&mut self) {
205        self.flush();
206        self.jitter_estimate_us = 0.0;
207        self.underrun_count = 0;
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    fn make_buffer() -> AudioJitterBuffer {
216        AudioJitterBuffer::new(JitterConfig {
217            sample_rate: 16000,
218            min_depth_samples: 1600, // 100ms
219            max_depth_samples: 16000,
220            jitter_alpha: 0.125,
221            target_jitter_multiple: 2.0,
222        })
223    }
224
225    #[test]
226    fn starts_in_filling_state() {
227        let buf = make_buffer();
228        assert_eq!(buf.state(), BufferState::Filling);
229        assert_eq!(buf.depth(), 0);
230    }
231
232    #[test]
233    fn filling_produces_silence() {
234        let mut buf = make_buffer();
235        buf.push(&vec![42i16; 800]); // < min_depth
236
237        let mut out = [0i16; 160];
238        let real = buf.pull(&mut out);
239        assert_eq!(real, 0);
240        assert!(out.iter().all(|&s| s == 0));
241    }
242
243    #[test]
244    fn transitions_to_playing() {
245        let mut buf = make_buffer();
246        buf.push(&vec![100i16; 1600]); // = min_depth
247
248        assert_eq!(buf.state(), BufferState::Playing);
249
250        let mut out = [0i16; 160];
251        let real = buf.pull(&mut out);
252        assert_eq!(real, 160);
253        assert!(out.iter().all(|&s| s == 100));
254    }
255
256    #[test]
257    fn underrun_fills_silence() {
258        let mut buf = make_buffer();
259        buf.push(&vec![99i16; 1600]);
260        assert_eq!(buf.state(), BufferState::Playing);
261
262        // Drain most of the buffer
263        let mut out = [0i16; 1600];
264        buf.pull(&mut out);
265
266        // Now try to pull more — underrun
267        let mut out2 = [0i16; 160];
268        let real = buf.pull(&mut out2);
269        assert_eq!(real, 0);
270        assert_eq!(buf.state(), BufferState::Underrun);
271        assert_eq!(buf.underrun_count(), 1);
272    }
273
274    #[test]
275    fn flush_clears_and_resets() {
276        let mut buf = make_buffer();
277        buf.push(&vec![42i16; 3200]);
278        assert_eq!(buf.state(), BufferState::Playing);
279
280        buf.flush();
281        assert_eq!(buf.state(), BufferState::Filling);
282        assert_eq!(buf.depth(), 0);
283    }
284
285    #[test]
286    fn overflow_drops_oldest() {
287        let mut buf = AudioJitterBuffer::new(JitterConfig {
288            sample_rate: 16000,
289            min_depth_samples: 100,
290            max_depth_samples: 500,
291            ..Default::default()
292        });
293
294        buf.push(&vec![1i16; 400]);
295        buf.push(&vec![2i16; 200]); // total 600 > max 500 → drop 100 oldest
296
297        assert!(buf.depth() <= 500);
298
299        // The oldest samples (1s) were dropped, we should get some 1s then 2s
300        let mut out = [0i16; 500];
301        buf.pull(&mut out);
302        // Last 200 should be 2s
303        assert!(out[300..].iter().all(|&s| s == 2));
304    }
305
306    #[test]
307    fn depth_ms_calculation() {
308        let mut buf = make_buffer();
309        buf.push(&vec![0i16; 1600]); // 100ms at 16kHz
310        assert!((buf.depth_ms() - 100.0).abs() < 0.01);
311    }
312
313    #[test]
314    fn large_sample_exceeds_max_depth() {
315        let mut buf = AudioJitterBuffer::new(JitterConfig {
316            sample_rate: 16000,
317            min_depth_samples: 100,
318            max_depth_samples: 500,
319            ..Default::default()
320        });
321
322        // Push initial data to fill part of buffer
323        buf.push(&vec![1i16; 200]);
324        assert_eq!(buf.depth(), 200);
325
326        // Push a very large sample that exceeds remaining capacity
327        // With current implementation, this can cause buffer to exceed max_depth
328        buf.push(&vec![2i16; 600]);
329
330        // Buffer should not exceed max_depth_samples
331        assert!(
332            buf.depth() <= 500,
333            "Buffer depth {} exceeded max {} after large push",
334            buf.depth(),
335            500
336        );
337    }
338}