use crate::error::Result;
pub trait AudioChunker {
fn push_samples(&mut self, samples: &[f32]) -> Result<Vec<Vec<f32>>>;
fn drain_residual(&mut self) -> Vec<f32>;
fn reset(&mut self);
}
#[derive(Debug, Clone)]
pub struct FixedSizeAudioChunker {
chunk_size: usize,
buffer: Vec<f32>,
}
impl FixedSizeAudioChunker {
#[must_use]
pub fn new(chunk_size: usize) -> Self {
assert!(
chunk_size > 0,
"FixedSizeAudioChunker requires chunk_size > 0 (got 0)"
);
Self {
chunk_size,
buffer: Vec::new(),
}
}
#[inline(always)]
#[must_use]
pub fn chunk_size(&self) -> usize {
self.chunk_size
}
#[inline(always)]
#[must_use]
pub fn buffered_len(&self) -> usize {
self.buffer.len()
}
}
impl AudioChunker for FixedSizeAudioChunker {
fn push_samples(&mut self, samples: &[f32]) -> Result<Vec<Vec<f32>>> {
if samples.is_empty() {
return Ok(Vec::new());
}
self.buffer.extend_from_slice(samples);
let n_chunks = self.buffer.len() / self.chunk_size;
if n_chunks == 0 {
return Ok(Vec::new());
}
let mut chunks = Vec::with_capacity(n_chunks);
let consumed = n_chunks * self.chunk_size;
{
let mut drain = self.buffer.drain(..consumed);
for _ in 0..n_chunks {
let chunk: Vec<f32> = (&mut drain).take(self.chunk_size).collect();
chunks.push(chunk);
}
}
Ok(chunks)
}
fn drain_residual(&mut self) -> Vec<f32> {
std::mem::take(&mut self.buffer)
}
fn reset(&mut self) {
self.buffer.clear();
}
}
#[derive(Debug, Clone)]
pub struct PreRollBuffer {
max_samples: usize,
buffer: Vec<f32>,
}
impl PreRollBuffer {
#[must_use]
pub const fn new(max_samples: usize) -> Self {
Self {
max_samples,
buffer: Vec::new(),
}
}
pub fn append(&mut self, samples: &[f32]) {
if self.max_samples == 0 || samples.is_empty() {
return;
}
self.buffer.extend_from_slice(samples);
if self.buffer.len() > self.max_samples {
let excess = self.buffer.len() - self.max_samples;
self.buffer.drain(..excess);
}
}
#[must_use]
pub fn snapshot(&self) -> Vec<f32> {
self.buffer.clone()
}
pub fn clear(&mut self) {
self.buffer.clear();
}
#[inline(always)]
#[must_use]
pub fn len(&self) -> usize {
self.buffer.len()
}
#[inline(always)]
#[must_use]
pub fn is_empty(&self) -> bool {
self.buffer.is_empty()
}
#[inline(always)]
#[must_use]
pub fn max_samples(&self) -> usize {
self.max_samples
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fixed_size_chunker_emits_aligned_chunks() {
let chunk_size = (16_000 * 20) / 1_000;
assert_eq!(chunk_size, 320);
let mut chunker = FixedSizeAudioChunker::new(chunk_size);
let samples: Vec<f32> = (0..1000).map(|i| i as f32).collect();
let chunks = chunker.push_samples(&samples).unwrap();
assert_eq!(chunks.len(), 3);
for c in &chunks {
assert_eq!(c.len(), 320);
}
assert_eq!(chunker.buffered_len(), 40);
assert_eq!(chunks[0].first().copied(), Some(0.0));
assert_eq!(chunks[0].last().copied(), Some(319.0));
assert_eq!(chunks[1].first().copied(), Some(320.0));
assert_eq!(chunks[2].last().copied(), Some(959.0));
}
#[test]
fn fixed_size_chunker_empty_push_is_noop() {
let mut chunker = FixedSizeAudioChunker::new(512);
let chunks = chunker.push_samples(&[]).unwrap();
assert!(chunks.is_empty());
assert_eq!(chunker.buffered_len(), 0);
}
#[test]
fn fixed_size_chunker_accumulates_across_pushes() {
let mut chunker = FixedSizeAudioChunker::new(100);
for _ in 0..3 {
let chunks = chunker.push_samples(&[1.0; 30]).unwrap();
assert!(chunks.is_empty());
}
assert_eq!(chunker.buffered_len(), 90);
let chunks = chunker.push_samples(&[2.0; 20]).unwrap();
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].len(), 100);
assert_eq!(chunker.buffered_len(), 10);
}
#[test]
fn fixed_size_chunker_reset_drops_tail() {
let mut chunker = FixedSizeAudioChunker::new(512);
chunker.push_samples(&vec![0.0; 200]).unwrap();
assert_eq!(chunker.buffered_len(), 200);
chunker.reset();
assert_eq!(chunker.buffered_len(), 0);
}
#[test]
fn fixed_size_chunker_drain_residual_returns_and_clears_tail() {
let mut chunker = FixedSizeAudioChunker::new(512);
let _ = chunker.push_samples(&[1.0_f32; 200]).unwrap();
assert_eq!(chunker.buffered_len(), 200);
let drained = chunker.drain_residual();
assert_eq!(drained.len(), 200);
assert!(drained.iter().all(|&s| s == 1.0));
assert_eq!(chunker.buffered_len(), 0);
let drained2 = chunker.drain_residual();
assert!(drained2.is_empty());
}
#[test]
fn fixed_size_chunker_drain_residual_empty_when_aligned() {
let mut chunker = FixedSizeAudioChunker::new(100);
let _ = chunker.push_samples(&[0.0_f32; 200]).unwrap();
assert_eq!(chunker.buffered_len(), 0);
let drained = chunker.drain_residual();
assert!(drained.is_empty());
}
#[test]
#[should_panic(expected = "chunk_size > 0")]
fn fixed_size_chunker_zero_size_panics() {
let _ = FixedSizeAudioChunker::new(0);
}
#[test]
fn preroll_trims_to_capacity() {
let mut preroll = PreRollBuffer::new(8);
preroll.append(&[1.0, 2.0, 3.0]);
preroll.append(&[4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
assert_eq!(preroll.len(), 8);
assert_eq!(
preroll.snapshot(),
vec![3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
);
}
#[test]
fn preroll_clear_empties() {
let mut preroll = PreRollBuffer::new(8);
preroll.append(&[1.0, 2.0, 3.0]);
assert!(!preroll.is_empty());
preroll.clear();
assert!(preroll.is_empty());
}
#[test]
fn preroll_zero_capacity_is_noop() {
let mut preroll = PreRollBuffer::new(0);
preroll.append(&[1.0, 2.0, 3.0]);
assert!(preroll.is_empty());
assert_eq!(preroll.snapshot(), Vec::<f32>::new());
}
}