Added audio_chunk
This commit is contained in:
344
src/audio_chunk.rs
Normal file
344
src/audio_chunk.rs
Normal file
@@ -0,0 +1,344 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::*;
|
||||
|
||||
/// A chunk of audio data that can be linked to form longer recordings.
|
||||
/// Designed for lock-free sharing between real-time and I/O threads.
|
||||
#[derive(Debug)]
|
||||
pub struct AudioChunk {
|
||||
/// Fixed-size audio buffer
|
||||
pub samples: Box<[f32]>,
|
||||
|
||||
/// Number of valid samples (≤ samples.len())
|
||||
pub sample_count: usize,
|
||||
|
||||
/// Next chunk in linked list
|
||||
pub next: Option<Arc<AudioChunk>>,
|
||||
}
|
||||
|
||||
impl AudioChunk {
|
||||
/// Create a new empty chunk with specified capacity (for buffer pool)
|
||||
pub fn allocate(size: usize) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
samples: vec![0.0; size].into_boxed_slice(),
|
||||
sample_count: 0,
|
||||
next: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Consolidate a linked list of chunks into a single optimized chunk
|
||||
pub fn consolidate(this: &Arc<AudioChunk>) -> Arc<Self> {
|
||||
// Calculate total sample count
|
||||
let mut total_samples = 0;
|
||||
let mut current = Some(this.clone());
|
||||
while let Some(chunk) = current {
|
||||
total_samples += chunk.sample_count;
|
||||
current = chunk.next.clone();
|
||||
}
|
||||
|
||||
// Create consolidated buffer
|
||||
let mut consolidated_samples = Vec::with_capacity(total_samples);
|
||||
let mut current = Some(this.clone());
|
||||
while let Some(chunk) = current {
|
||||
consolidated_samples.extend_from_slice(&chunk.samples[..chunk.sample_count]);
|
||||
current = chunk.next.clone();
|
||||
}
|
||||
|
||||
Arc::new(Self {
|
||||
samples: consolidated_samples.into_boxed_slice(),
|
||||
sample_count: total_samples,
|
||||
next: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Write samples into this chunk and additional chunks as needed.
|
||||
/// Uses the factory to get pre-allocated chunks when current chunk fills up.
|
||||
pub fn write_samples<F>(this: &mut Arc<Self>, samples: &[f32], chunk_factory: F) -> Result<(), LooperError>
|
||||
where
|
||||
F: Fn() -> Result<Arc<AudioChunk>, LooperError>
|
||||
{
|
||||
let sample_count = samples.len();
|
||||
|
||||
if sample_count > 0 {
|
||||
let available_space = this.samples.len();
|
||||
let samples_to_write = sample_count.min(available_space);
|
||||
|
||||
// Get mutable access to write samples
|
||||
let chunk_mut = Arc::get_mut(this).ok_or(LooperError::ChunkOwnership)?;
|
||||
|
||||
// Write what fits
|
||||
if let Some(dest) = chunk_mut.samples.get_mut(..samples_to_write) {
|
||||
dest.copy_from_slice(&samples[..samples_to_write]);
|
||||
chunk_mut.sample_count = samples_to_write;
|
||||
}
|
||||
|
||||
// Handle remaining samples if any
|
||||
if samples_to_write < sample_count {
|
||||
// Need a new chunk for remaining samples
|
||||
let mut new_chunk = chunk_factory()?;
|
||||
let remaining_samples = &samples[samples_to_write..];
|
||||
|
||||
// Recurse on the new chunk
|
||||
Self::write_samples(&mut new_chunk, remaining_samples, chunk_factory)?;
|
||||
|
||||
// Link the new chunk
|
||||
chunk_mut.next = Some(new_chunk);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_allocate_creates_empty_chunk() {
|
||||
let chunk = AudioChunk::allocate(1024);
|
||||
|
||||
assert_eq!(chunk.samples.len(), 1024);
|
||||
assert_eq!(chunk.sample_count, 0);
|
||||
assert!(chunk.next.is_none());
|
||||
|
||||
// All samples should be initialized to 0.0
|
||||
assert!(chunk.samples.iter().all(|&x| x == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_consolidate_single_chunk() {
|
||||
let mut chunk = AudioChunk::allocate(10);
|
||||
|
||||
// Write some sample data
|
||||
let samples = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
let factory = || panic!("Factory should not be called");
|
||||
let result = AudioChunk::write_samples(&mut chunk, &samples, factory);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let consolidated = AudioChunk::consolidate(&chunk);
|
||||
|
||||
assert_eq!(consolidated.samples.len(), 5);
|
||||
assert_eq!(consolidated.sample_count, 5);
|
||||
assert!(consolidated.next.is_none());
|
||||
assert_eq!(consolidated.samples[0], 1.0);
|
||||
assert_eq!(consolidated.samples[4], 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_consolidate_linked_chunks() {
|
||||
// Create first chunk
|
||||
let chunk1 = Arc::new(AudioChunk {
|
||||
samples: vec![1.0, 2.0, 3.0].into_boxed_slice(),
|
||||
sample_count: 3,
|
||||
next: None,
|
||||
});
|
||||
|
||||
// Create second chunk
|
||||
let chunk2 = Arc::new(AudioChunk {
|
||||
samples: vec![4.0, 5.0].into_boxed_slice(),
|
||||
sample_count: 2,
|
||||
next: None,
|
||||
});
|
||||
|
||||
// Create third chunk
|
||||
let chunk3 = Arc::new(AudioChunk {
|
||||
samples: vec![6.0, 7.0, 8.0, 9.0].into_boxed_slice(),
|
||||
sample_count: 4,
|
||||
next: None,
|
||||
});
|
||||
|
||||
// Link them together
|
||||
let chunk1 = Arc::new(AudioChunk {
|
||||
samples: chunk1.samples.clone(),
|
||||
sample_count: chunk1.sample_count,
|
||||
next: Some(Arc::new(AudioChunk {
|
||||
samples: chunk2.samples.clone(),
|
||||
sample_count: chunk2.sample_count,
|
||||
next: Some(chunk3),
|
||||
})),
|
||||
});
|
||||
|
||||
let consolidated = AudioChunk::consolidate(&chunk1);
|
||||
|
||||
assert_eq!(consolidated.samples.len(), 9);
|
||||
assert_eq!(consolidated.sample_count, 9);
|
||||
assert!(consolidated.next.is_none());
|
||||
|
||||
let expected = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
|
||||
assert_eq!(consolidated.samples.as_ref(), expected.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_consolidate_partial_chunks() {
|
||||
// Test chunks where sample_count < samples.len()
|
||||
let chunk1 = Arc::new(AudioChunk {
|
||||
samples: vec![1.0, 2.0, 3.0, 0.0, 0.0].into_boxed_slice(),
|
||||
sample_count: 3, // Only first 3 samples are valid
|
||||
next: None,
|
||||
});
|
||||
|
||||
let chunk2 = Arc::new(AudioChunk {
|
||||
samples: vec![4.0, 5.0, 0.0].into_boxed_slice(),
|
||||
sample_count: 2, // Only first 2 samples are valid
|
||||
next: None,
|
||||
});
|
||||
|
||||
let chunk1 = Arc::new(AudioChunk {
|
||||
samples: chunk1.samples.clone(),
|
||||
sample_count: chunk1.sample_count,
|
||||
next: Some(chunk2),
|
||||
});
|
||||
|
||||
let consolidated = AudioChunk::consolidate(&chunk1);
|
||||
|
||||
assert_eq!(consolidated.samples.len(), 5);
|
||||
assert_eq!(consolidated.sample_count, 5);
|
||||
|
||||
let expected = vec![1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
assert_eq!(consolidated.samples.as_ref(), expected.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_samples_empty() {
|
||||
let mut chunk = AudioChunk::allocate(5);
|
||||
|
||||
let factory = || panic!("Factory should not be called for empty samples");
|
||||
let result = AudioChunk::write_samples(&mut chunk, &[], factory);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(chunk.sample_count, 0);
|
||||
assert!(chunk.next.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_samples_fits_in_one_chunk() {
|
||||
let mut chunk = AudioChunk::allocate(5);
|
||||
|
||||
let factory = || panic!("Factory should not be called when samples fit");
|
||||
let samples = vec![1.0, 2.0, 3.0];
|
||||
let result = AudioChunk::write_samples(&mut chunk, &samples, factory);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(chunk.sample_count, 3);
|
||||
assert_eq!(chunk.samples[0], 1.0);
|
||||
assert_eq!(chunk.samples[1], 2.0);
|
||||
assert_eq!(chunk.samples[2], 3.0);
|
||||
assert!(chunk.next.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_samples_exactly_fills_chunk() {
|
||||
let mut chunk = AudioChunk::allocate(3);
|
||||
|
||||
let factory = || panic!("Factory should not be called when samples exactly fit");
|
||||
let samples = vec![1.0, 2.0, 3.0];
|
||||
let result = AudioChunk::write_samples(&mut chunk, &samples, factory);
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(chunk.sample_count, 3);
|
||||
assert_eq!(chunk.samples[0], 1.0);
|
||||
assert_eq!(chunk.samples[1], 2.0);
|
||||
assert_eq!(chunk.samples[2], 3.0);
|
||||
assert!(chunk.next.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_samples_requires_new_chunk() {
|
||||
let mut chunk = AudioChunk::allocate(2);
|
||||
|
||||
use std::cell::RefCell;
|
||||
let available_chunks = RefCell::new(vec![AudioChunk::allocate(3)]);
|
||||
let factory = || available_chunks.borrow_mut().pop().ok_or(LooperError::ChunkAllocation);
|
||||
|
||||
let samples = vec![1.0, 2.0, 3.0, 4.0]; // 4 samples, first chunk holds 2
|
||||
let result = AudioChunk::write_samples(&mut chunk, &samples, factory);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
// First chunk: 2 samples
|
||||
assert_eq!(chunk.sample_count, 2);
|
||||
assert_eq!(chunk.samples[0], 1.0);
|
||||
assert_eq!(chunk.samples[1], 2.0);
|
||||
|
||||
// Second chunk: remaining 2 samples
|
||||
let chunk2 = chunk.next.as_ref().unwrap();
|
||||
assert_eq!(chunk2.sample_count, 2);
|
||||
assert_eq!(chunk2.samples[0], 3.0);
|
||||
assert_eq!(chunk2.samples[1], 4.0);
|
||||
assert!(chunk2.next.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_samples_multiple_chunks_cascade() {
|
||||
let mut chunk = AudioChunk::allocate(2);
|
||||
|
||||
use std::cell::RefCell;
|
||||
let available_chunks = RefCell::new(vec![
|
||||
AudioChunk::allocate(2),
|
||||
AudioChunk::allocate(2),
|
||||
]);
|
||||
let factory = || available_chunks.borrow_mut().pop().ok_or(LooperError::ChunkAllocation);
|
||||
|
||||
let samples = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; // 6 samples, each chunk holds 2
|
||||
let result = AudioChunk::write_samples(&mut chunk, &samples, factory);
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
// First chunk
|
||||
assert_eq!(chunk.sample_count, 2);
|
||||
assert_eq!(chunk.samples[0], 1.0);
|
||||
assert_eq!(chunk.samples[1], 2.0);
|
||||
|
||||
// Second chunk
|
||||
let chunk2 = chunk.next.as_ref().unwrap();
|
||||
assert_eq!(chunk2.sample_count, 2);
|
||||
assert_eq!(chunk2.samples[0], 3.0);
|
||||
assert_eq!(chunk2.samples[1], 4.0);
|
||||
|
||||
// Third chunk
|
||||
let chunk3 = chunk2.next.as_ref().unwrap();
|
||||
assert_eq!(chunk3.sample_count, 2);
|
||||
assert_eq!(chunk3.samples[0], 5.0);
|
||||
assert_eq!(chunk3.samples[1], 6.0);
|
||||
assert!(chunk3.next.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_samples_factory_failure() {
|
||||
let mut chunk = AudioChunk::allocate(2);
|
||||
|
||||
let factory = || Err(LooperError::ChunkAllocation);
|
||||
let samples = vec![1.0, 2.0, 3.0]; // 3 samples, chunk only holds 2
|
||||
let result = AudioChunk::write_samples(&mut chunk, &samples, factory);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), LooperError::ChunkAllocation));
|
||||
|
||||
// First chunk should have been written before factory failure
|
||||
assert_eq!(chunk.sample_count, 2);
|
||||
assert_eq!(chunk.samples[0], 1.0);
|
||||
assert_eq!(chunk.samples[1], 2.0);
|
||||
assert!(chunk.next.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_samples_ownership_failure() {
|
||||
let chunk = AudioChunk::allocate(3);
|
||||
let chunk_clone = chunk.clone(); // Create another reference
|
||||
let mut chunk_original = chunk;
|
||||
|
||||
let factory = || panic!("Factory should not be called");
|
||||
let samples = vec![1.0, 2.0];
|
||||
let result = AudioChunk::write_samples(&mut chunk_original, &samples, factory);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result.unwrap_err(), LooperError::ChunkOwnership));
|
||||
|
||||
// Chunk should be unchanged
|
||||
assert_eq!(chunk_original.sample_count, 0);
|
||||
|
||||
// Clean up the clone reference to avoid unused variable warning
|
||||
drop(chunk_clone);
|
||||
}
|
||||
}
|
||||
8
src/looper_error.rs
Normal file
8
src/looper_error.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum LooperError {
|
||||
#[error("Failed to allocate new audio chunk")]
|
||||
ChunkAllocation,
|
||||
|
||||
#[error("Cannot modify chunk with multiple references")]
|
||||
ChunkOwnership,
|
||||
}
|
||||
20
src/main.rs
20
src/main.rs
@@ -1,3 +1,21 @@
|
||||
mod audio_chunk;
|
||||
mod looper_error;
|
||||
|
||||
use audio_chunk::AudioChunk;
|
||||
use looper_error::LooperError;
|
||||
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
let mut chunk = AudioChunk::allocate(2);
|
||||
|
||||
use std::cell::RefCell;
|
||||
let available_chunks = RefCell::new(vec![
|
||||
AudioChunk::allocate(2),
|
||||
AudioChunk::allocate(2),
|
||||
]);
|
||||
let factory = || available_chunks.borrow_mut().pop().ok_or(LooperError::ChunkAllocation);
|
||||
|
||||
let samples = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; // 6 samples, each chunk holds 2
|
||||
AudioChunk::write_samples(&mut chunk, &samples, factory).unwrap();
|
||||
AudioChunk::consolidate(&chunk);
|
||||
println!("Audio chunk allocated with size: {}", chunk.samples.len());
|
||||
}
|
||||
Reference in New Issue
Block a user