Skip to main content

adk_realtime/
audio.rs

1//! Audio format definitions and utilities.
2
3use std::borrow::Cow;
4
5use serde::{Deserialize, Serialize};
6
7/// Audio encoding formats supported by realtime APIs.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
9#[serde(rename_all = "lowercase")]
10pub enum AudioEncoding {
11    /// 16-bit PCM audio (most common).
12    #[serde(rename = "pcm16")]
13    #[default]
14    Pcm16,
15    /// G.711 μ-law encoding.
16    #[serde(rename = "g711_ulaw")]
17    G711Ulaw,
18    /// G.711 A-law encoding.
19    #[serde(rename = "g711_alaw")]
20    G711Alaw,
21}
22
23impl std::fmt::Display for AudioEncoding {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        match self {
26            Self::Pcm16 => write!(f, "pcm16"),
27            Self::G711Ulaw => write!(f, "g711_ulaw"),
28            Self::G711Alaw => write!(f, "g711_alaw"),
29        }
30    }
31}
32
33/// Complete audio format specification.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct AudioFormat {
36    /// Sample rate in Hz (e.g., 24000, 16000, 8000).
37    pub sample_rate: u32,
38    /// Number of audio channels (1 = mono, 2 = stereo).
39    pub channels: u8,
40    /// Bits per sample.
41    pub bits_per_sample: u8,
42    /// Audio encoding format.
43    pub encoding: AudioEncoding,
44}
45
46impl Default for AudioFormat {
47    fn default() -> Self {
48        Self::pcm16_24khz()
49    }
50}
51
52impl AudioFormat {
53    /// Create a new audio format specification.
54    pub fn new(
55        sample_rate: u32,
56        channels: u8,
57        bits_per_sample: u8,
58        encoding: AudioEncoding,
59    ) -> Self {
60        Self { sample_rate, channels, bits_per_sample, encoding }
61    }
62
63    /// Standard PCM16 format at 24kHz (OpenAI default).
64    pub fn pcm16_24khz() -> Self {
65        Self {
66            sample_rate: 24000,
67            channels: 1,
68            bits_per_sample: 16,
69            encoding: AudioEncoding::Pcm16,
70        }
71    }
72
73    /// PCM16 format at 16kHz (Gemini input default).
74    pub fn pcm16_16khz() -> Self {
75        Self {
76            sample_rate: 16000,
77            channels: 1,
78            bits_per_sample: 16,
79            encoding: AudioEncoding::Pcm16,
80        }
81    }
82
83    /// G.711 μ-law format at 8kHz (telephony standard).
84    pub fn g711_ulaw() -> Self {
85        Self {
86            sample_rate: 8000,
87            channels: 1,
88            bits_per_sample: 8,
89            encoding: AudioEncoding::G711Ulaw,
90        }
91    }
92
93    /// G.711 A-law format at 8kHz (telephony standard).
94    pub fn g711_alaw() -> Self {
95        Self {
96            sample_rate: 8000,
97            channels: 1,
98            bits_per_sample: 8,
99            encoding: AudioEncoding::G711Alaw,
100        }
101    }
102
103    /// Calculate bytes per second for this format.
104    pub fn bytes_per_second(&self) -> u32 {
105        self.sample_rate * self.channels as u32 * (self.bits_per_sample / 8) as u32
106    }
107
108    /// Calculate duration in milliseconds for a given number of bytes.
109    pub fn duration_ms(&self, bytes: usize) -> f64 {
110        let bytes_per_ms = self.bytes_per_second() as f64 / 1000.0;
111        bytes as f64 / bytes_per_ms
112    }
113}
114
115/// Audio chunk with format information.
116#[derive(Debug, Clone)]
117pub struct AudioChunk {
118    /// Raw audio data.
119    pub data: Vec<u8>,
120    /// Audio format of this chunk.
121    pub format: AudioFormat,
122}
123
124impl AudioChunk {
125    /// Create a new audio chunk.
126    pub fn new(data: Vec<u8>, format: AudioFormat) -> Self {
127        Self { data, format }
128    }
129
130    /// Create a PCM16 24kHz audio chunk (OpenAI format).
131    pub fn pcm16_24khz(data: Vec<u8>) -> Self {
132        Self::new(data, AudioFormat::pcm16_24khz())
133    }
134
135    /// Create a PCM16 16kHz audio chunk (Gemini input format).
136    pub fn pcm16_16khz(data: Vec<u8>) -> Self {
137        Self::new(data, AudioFormat::pcm16_16khz())
138    }
139
140    /// Get duration of this audio chunk in milliseconds.
141    pub fn duration_ms(&self) -> f64 {
142        self.format.duration_ms(self.data.len())
143    }
144
145    /// Encode audio data as base64.
146    pub fn to_base64(&self) -> String {
147        use base64::Engine;
148        base64::engine::general_purpose::STANDARD.encode(&self.data)
149    }
150
151    /// Decode audio data from base64.
152    pub fn from_base64(encoded: &str, format: AudioFormat) -> Result<Self, base64::DecodeError> {
153        use base64::Engine;
154        let data = base64::engine::general_purpose::STANDARD.decode(encoded)?;
155        Ok(Self::new(data, format))
156    }
157
158    /// Create an `AudioChunk` from i16 samples (converts to PCM16 little-endian bytes).
159    ///
160    /// This is useful when working with audio APIs (like LiveKit) that provide
161    /// samples as `i16` slices rather than raw byte buffers.
162    ///
163    /// On little-endian hosts the samples are reinterpreted and copied in one
164    /// vectorized `memcpy` instead of one `to_le_bytes` call per sample. On
165    /// big-endian hosts each sample is byte-swapped individually, so the emitted
166    /// bytes are little-endian PCM16 on every target.
167    ///
168    /// # Example
169    ///
170    /// ```
171    /// use adk_realtime::audio::{AudioChunk, AudioFormat};
172    ///
173    /// let chunk = AudioChunk::from_i16_samples(&[1, -1], AudioFormat::pcm16_24khz());
174    /// assert_eq!(chunk.data, vec![0x01, 0x00, 0xff, 0xff]);
175    /// ```
176    pub fn from_i16_samples(samples: &[i16], format: AudioFormat) -> Self {
177        // Narrowing the element type to `u8` can never fail an alignment check, so
178        // this is a single bulk copy of the already little-endian sample bytes.
179        #[cfg(target_endian = "little")]
180        let data = bytemuck::cast_slice::<i16, u8>(samples).to_vec();
181
182        #[cfg(target_endian = "big")]
183        let data = {
184            let mut data = Vec::with_capacity(samples.len() * size_of::<i16>());
185            for sample in samples {
186                data.extend_from_slice(&sample.to_le_bytes());
187            }
188            data
189        };
190
191        Self::new(data, format)
192    }
193
194    /// Interpret the audio data as i16 samples (assuming PCM16 little-endian).
195    ///
196    /// When the buffer is suitably aligned on a little-endian host the samples are
197    /// borrowed directly from `self.data` and no copy is made ([`Cow::Borrowed`]).
198    /// Otherwise — a big-endian host, or a misaligned buffer — the samples are
199    /// decoded into a freshly allocated `Vec` ([`Cow::Owned`]).
200    ///
201    /// # Errors
202    ///
203    /// Returns an error string if the data length is not even (not valid PCM16).
204    ///
205    /// # Example
206    ///
207    /// ```
208    /// use adk_realtime::audio::{AudioChunk, AudioFormat};
209    ///
210    /// let chunk = AudioChunk::pcm16_24khz(vec![0x01, 0x00, 0xff, 0xff]);
211    /// let samples = chunk.to_i16_samples().unwrap();
212    /// assert_eq!(samples.as_ref(), &[1, -1]);
213    ///
214    /// // An odd byte count cannot be valid PCM16.
215    /// assert!(AudioChunk::pcm16_24khz(vec![0x01]).to_i16_samples().is_err());
216    /// ```
217    pub fn to_i16_samples(&self) -> Result<Cow<'_, [i16]>, String> {
218        if !self.data.len().is_multiple_of(size_of::<i16>()) {
219            return Err(format!(
220                "Invalid data length for PCM16: {} (must be even)",
221                self.data.len()
222            ));
223        }
224        Ok(decode_pcm16_le(&self.data))
225    }
226}
227
228/// Decode little-endian PCM16 bytes into i16 samples, borrowing when possible.
229///
230/// Mirrors the idiom used by the LiveKit audio handler: on a little-endian host an
231/// aligned buffer is reinterpreted in place, which is free. The `as_chunks`
232/// fallback covers both big-endian hosts (where the bytes need swapping) and
233/// misaligned buffers (where `i16` cannot be read directly).
234fn decode_pcm16_le(audio: &[u8]) -> Cow<'_, [i16]> {
235    debug_assert!(audio.len().is_multiple_of(size_of::<i16>()));
236
237    #[cfg(target_endian = "little")]
238    if let Ok(aligned_slice) = bytemuck::try_cast_slice::<u8, i16>(audio) {
239        return Cow::Borrowed(aligned_slice);
240    }
241
242    Cow::Owned(
243        audio
244            .as_chunks::<{ size_of::<i16>() }>()
245            .0
246            .iter()
247            .map(|chunk| i16::from_le_bytes([chunk[0], chunk[1]]))
248            .collect(),
249    )
250}
251
252/// Buffers audio samples until a target duration is reached.
253///
254/// Smart buffering (e.g., 40-80ms) is essential for AI voice services to:
255/// 1. **Reduce Network Overhead**: Aggregating small frames into larger chunks
256///    drastically reduces packet rate, lowering CPU usage and bandwidth overhead.
257/// 2. **Improve Model Performance**: Provides sufficient context for Voice Activity
258///    Detection (VAD) to distinguish speech from noise.
259/// 3. **Resist Jitter**: Smooths out network jitter common in mobile networks.
260/// 4. **Latency Trade-off**: Maintains a real-time feel while gaining stability.
261#[derive(Debug, Clone)]
262pub struct SmartAudioBuffer {
263    buffer: Vec<i16>,
264    sample_rate: u32,
265    target_duration_ms: u32,
266}
267
268impl SmartAudioBuffer {
269    /// Create a new smart audio buffer.
270    pub fn new(sample_rate: u32, target_duration_ms: u32) -> Self {
271        Self { buffer: Vec::new(), sample_rate, target_duration_ms }
272    }
273
274    /// Push new samples into the buffer.
275    pub fn push(&mut self, samples: &[i16]) {
276        self.buffer.extend_from_slice(samples);
277    }
278
279    fn should_flush(&self) -> bool {
280        let duration_ms = (self.buffer.len() as f64 / self.sample_rate as f64) * 1000.0;
281
282        duration_ms >= self.target_duration_ms as f64
283    }
284
285    /// Flush the buffer if the target duration has been reached.
286    pub fn flush(&mut self) -> Option<Vec<i16>> {
287        if self.should_flush() { Some(std::mem::take(&mut self.buffer)) } else { None }
288    }
289
290    /// Flush any remaining samples in the buffer.
291    pub fn flush_remaining(&mut self) -> Option<Vec<i16>> {
292        if self.buffer.is_empty() { None } else { Some(std::mem::take(&mut self.buffer)) }
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn test_smart_audio_buffer_flush_threshold() {
302        let sample_rate = 1000;
303        let target_ms = 100;
304        // 1000 samples/sec -> 1 sample = 1ms.
305        // target 100ms -> 100 samples.
306
307        let mut buffer = SmartAudioBuffer::new(sample_rate, target_ms);
308
309        // Push 50 samples (50ms)
310        buffer.push(&[0; 50]);
311        assert!(buffer.flush().is_none());
312
313        // Push 49 samples (total 99ms)
314        buffer.push(&[0; 49]);
315        assert!(buffer.flush().is_none());
316
317        // Push 1 sample (total 100ms)
318        buffer.push(&[0; 1]);
319        let flushed = buffer.flush();
320        assert!(flushed.is_some());
321        assert_eq!(flushed.unwrap().len(), 100);
322        assert!(buffer.buffer.is_empty());
323    }
324
325    #[test]
326    fn test_smart_audio_buffer_flush_remaining() {
327        let sample_rate = 1000;
328        let target_ms = 100;
329        let mut buffer = SmartAudioBuffer::new(sample_rate, target_ms);
330
331        buffer.push(&[0; 50]);
332        assert!(buffer.flush().is_none());
333
334        let remaining = buffer.flush_remaining();
335        assert!(remaining.is_some());
336        assert_eq!(remaining.unwrap().len(), 50);
337        assert!(buffer.buffer.is_empty());
338    }
339
340    #[test]
341    fn test_smart_audio_buffer_empty_flush() {
342        let mut buffer = SmartAudioBuffer::new(1000, 100);
343        assert!(buffer.flush().is_none());
344        assert!(buffer.flush_remaining().is_none());
345    }
346
347    #[test]
348    fn test_audio_format_bytes_per_second() {
349        let pcm16_24k = AudioFormat::pcm16_24khz();
350        assert_eq!(pcm16_24k.bytes_per_second(), 48000); // 24000 * 1 * 2
351
352        let pcm16_16k = AudioFormat::pcm16_16khz();
353        assert_eq!(pcm16_16k.bytes_per_second(), 32000); // 16000 * 1 * 2
354    }
355
356    #[test]
357    fn test_audio_format_duration() {
358        let format = AudioFormat::pcm16_24khz();
359        // 48000 bytes = 1 second
360        let duration = format.duration_ms(48000);
361        assert!((duration - 1000.0).abs() < 0.001);
362    }
363
364    #[test]
365    fn test_audio_chunk_base64() {
366        let original = AudioChunk::pcm16_24khz(vec![0, 1, 2, 3, 4, 5]);
367        let encoded = original.to_base64();
368        let decoded = AudioChunk::from_base64(&encoded, AudioFormat::pcm16_24khz()).unwrap();
369        assert_eq!(original.data, decoded.data);
370    }
371
372    #[test]
373    fn test_i16_samples_roundtrip() {
374        let samples: Vec<i16> = vec![0, 1, -1, 32767, -32768, 1000, -1000];
375        let chunk = AudioChunk::from_i16_samples(&samples, AudioFormat::pcm16_24khz());
376        let recovered = chunk.to_i16_samples().unwrap();
377        assert_eq!(samples.as_slice(), recovered.as_ref());
378    }
379
380    #[test]
381    fn test_from_i16_samples_emits_little_endian_bytes() {
382        let chunk = AudioChunk::from_i16_samples(&[1, -1, 256], AudioFormat::pcm16_24khz());
383        assert_eq!(chunk.data, vec![0x01, 0x00, 0xff, 0xff, 0x00, 0x01]);
384    }
385
386    #[test]
387    fn test_i16_samples_empty() {
388        let chunk = AudioChunk::from_i16_samples(&[], AudioFormat::pcm16_24khz());
389        assert!(chunk.data.is_empty());
390        assert!(chunk.to_i16_samples().unwrap().is_empty());
391    }
392
393    #[test]
394    fn test_i16_samples_odd_bytes_error() {
395        let chunk = AudioChunk::pcm16_24khz(vec![0, 1, 2]); // 3 bytes = invalid PCM16
396        assert_eq!(
397            chunk.to_i16_samples().unwrap_err(),
398            "Invalid data length for PCM16: 3 (must be even)"
399        );
400    }
401
402    #[test]
403    #[cfg(target_endian = "little")]
404    fn test_to_i16_samples_borrows_aligned_buffer() {
405        // `AudioChunk::data` is an owned `Vec<u8>`, so its allocation is always
406        // suitably aligned for `i16` and the borrowed fast path applies.
407        let chunk = AudioChunk::from_i16_samples(&[1, -1, 256], AudioFormat::pcm16_24khz());
408        let samples = chunk.to_i16_samples().unwrap();
409        assert!(matches!(samples, Cow::Borrowed(_)));
410        assert_eq!(samples.as_ref(), &[1, -1, 256]);
411    }
412
413    #[test]
414    fn test_decode_pcm16_le_misaligned_buffer_is_owned() {
415        // Build an aligned `[i16]`, view it as bytes, then take an odd-offset
416        // sub-slice so the buffer cannot be reinterpreted as `i16` in place.
417        let aligned_words = [
418            i16::from_ne_bytes([0x00, 0x01]),
419            i16::from_ne_bytes([0x02, 0x03]),
420            i16::from_ne_bytes([0x04, 0x00]),
421        ];
422        let aligned_bytes: &[u8] = bytemuck::cast_slice(&aligned_words);
423        let misaligned = &aligned_bytes[1..5];
424
425        let samples = decode_pcm16_le(misaligned);
426        assert!(matches!(samples, Cow::Owned(_)));
427        assert_eq!(samples.as_ref(), &[0x0201, 0x0403]);
428    }
429
430    #[test]
431    fn test_decode_pcm16_le_empty_input() {
432        let samples = decode_pcm16_le(&[]);
433        assert!(samples.is_empty());
434    }
435}