diff --git a/rhubarb/rhubarb-audio/src/lib.rs b/rhubarb/rhubarb-audio/src/lib.rs index e5382e9..3222e85 100644 --- a/rhubarb/rhubarb-audio/src/lib.rs +++ b/rhubarb/rhubarb-audio/src/lib.rs @@ -14,6 +14,7 @@ mod audio_clip; mod audio_error; +mod memory_audio_clip; mod ogg_audio_clip; mod open_audio_file; mod read_and_seek; @@ -22,6 +23,7 @@ mod wave_audio_clip; pub use audio_clip::{AudioClip, Sample, SampleReader}; pub use audio_error::AudioError; +pub use memory_audio_clip::MemoryAudioClip; pub use ogg_audio_clip::ogg_audio_clip::OggAudioClip; pub use open_audio_file::{open_audio_file, open_audio_file_with_reader}; pub use read_and_seek::ReadAndSeek; diff --git a/rhubarb/rhubarb-audio/src/memory_audio_clip.rs b/rhubarb/rhubarb-audio/src/memory_audio_clip.rs new file mode 100644 index 0000000..276512a --- /dev/null +++ b/rhubarb/rhubarb-audio/src/memory_audio_clip.rs @@ -0,0 +1,202 @@ +use crate::{sample_reader_assertions::SampleReaderAssertions, AudioClip, Sample, SampleReader}; +use derivative::Derivative; +use std::fmt::{self, Formatter}; +use std::sync::Arc; + +/// An audio clip representing audio samples in memory. +/// +/// *Note:* Cloned instances share the same audio buffer, so cloning is cheap. +#[derive(Derivative)] +#[derivative(Clone, Debug)] +pub struct MemoryAudioClip { + #[derivative(Debug(format_with = "format_buffer"))] + buffer: Arc>, + sampling_rate: u32, +} + +impl MemoryAudioClip { + /// Creates a new memory audio clip. + pub fn new(samples: &[Sample], sampling_rate: u32) -> Self { + Self { + buffer: Arc::new(samples.to_vec()), + sampling_rate, + } + } +} + +impl AudioClip for MemoryAudioClip { + fn len(&self) -> u64 { + self.buffer.len() as u64 + } + + fn sampling_rate(&self) -> u32 { + self.sampling_rate + } + + fn create_sample_reader(&self) -> Result, crate::AudioError> { + Ok(Box::new(MemorySampleReader { + buffer: self.buffer.clone(), + position: 0, + })) + } +} + +#[derive(Derivative)] +#[derivative(Debug)] +struct MemorySampleReader { + #[derivative(Debug(format_with = "format_buffer"))] + buffer: Arc>, + position: u64, +} + +impl SampleReader for MemorySampleReader { + fn len(&self) -> u64 { + self.buffer.len() as u64 + } + + fn position(&self) -> u64 { + self.position + } + + fn set_position(&mut self, position: u64) { + self.assert_valid_seek_position(position); + self.position = position; + } + + fn read(&mut self, buffer: &mut [Sample]) -> Result<(), crate::AudioError> { + self.assert_valid_read_size(buffer); + + let start = self.position as usize; + buffer.copy_from_slice(&self.buffer[start..start + buffer.len()]); + self.position += buffer.len() as u64; + Ok(()) + } +} + +fn format_buffer(buffer: &Arc>, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{:?} samples", buffer.len()) +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::*; + use speculoos::prelude::*; + + use crate::{AudioClip, SampleReader}; + + #[fixture] + fn clip() -> MemoryAudioClip { + MemoryAudioClip::new(&[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9], 16000) + } + + #[rstest] + fn supports_debug(clip: MemoryAudioClip) { + assert_that!(format!("{clip:?}")) + .is_equal_to("MemoryAudioClip { buffer: 10 samples, sampling_rate: 16000 }".to_owned()); + } + + #[rstest] + fn provides_length(clip: MemoryAudioClip) { + assert_that!(clip.len()).is_equal_to(10); + } + + #[rstest] + fn provides_sampling_rate(clip: MemoryAudioClip) { + assert_that!(clip.sampling_rate()).is_equal_to(16000); + } + + #[rstest] + fn clone_reuses_buffer(clip: MemoryAudioClip) { + let clone = clip.clone(); + assert_that!(clone.buffer.as_ptr()).is_equal_to(clip.buffer.as_ptr()); + } + + #[rstest] + fn supports_zero_samples() { + let clip = MemoryAudioClip::new(&[], 16000); + assert_that!(clip.len()).is_equal_to(0); + assert_that!(clip.sampling_rate()).is_equal_to(16000); + + let mut sample_reader = clip.create_sample_reader().unwrap(); + let mut buffer = [0.0f32; 0]; + sample_reader.read(&mut buffer).unwrap(); + + sample_reader.set_position(0); + } + + mod sample_reader { + use super::*; + + #[fixture] + fn reader(clip: MemoryAudioClip) -> Box { + clip.create_sample_reader().unwrap() + } + + #[rstest] + fn supports_debug(reader: Box) { + assert_that!(format!("{reader:?}")) + .is_equal_to("MemorySampleReader { buffer: 10 samples, position: 0 }".to_owned()); + } + + #[rstest] + fn provides_length(reader: Box) { + assert_that!(reader.len()).is_equal_to(10); + } + + #[rstest] + fn position_is_initially_0(reader: Box) { + assert_that!(reader.position()).is_equal_to(0); + } + + #[rstest] + fn reads_samples_up_to_the_end(mut reader: Box) { + let mut three_samples = [0f32; 3]; + reader.read(&mut three_samples).unwrap(); + assert_that!(three_samples).is_equal_to([0.0, 0.1, 0.2]); + + reader.read(&mut three_samples).unwrap(); + assert_that!(three_samples).is_equal_to([0.3, 0.4, 0.5]); + + let mut four_samples = vec![0f32; 4]; + reader.read(&mut four_samples).unwrap(); + assert_that!(four_samples).is_equal_to(vec![0.6, 0.7, 0.8, 0.9]); + } + + #[rstest] + fn seeks(mut reader: Box) { + reader.set_position(2); + let mut three_samples = [0f32; 3]; + reader.read(&mut three_samples).unwrap(); + assert_that!(three_samples).is_equal_to([0.2, 0.3, 0.4]); + + reader.set_position(1); + reader.read(&mut three_samples).unwrap(); + assert_that!(three_samples).is_equal_to([0.1, 0.2, 0.3]); + + reader.read(&mut three_samples).unwrap(); + assert_that!(three_samples).is_equal_to([0.4, 0.5, 0.6]); + } + + #[rstest] + fn seeks_up_to_the_end(mut reader: Box) { + reader.set_position(10); + let mut zero_samples = [0f32; 0]; + reader.read(&mut zero_samples).unwrap(); + } + + #[rstest] + #[should_panic(expected = "Attempting to read up to position 11 of 10-frame audio clip.")] + fn reading_beyond_the_end(mut reader: Box) { + reader.set_position(8); + let mut three_samples = [0f32; 3]; + reader.read(&mut three_samples).unwrap(); + } + + #[rstest] + #[should_panic(expected = "Attempting to seek to position 11 of 10-frame audio clip.")] + fn seeking_beyond_the_end(mut reader: Box) { + reader.set_position(11); + } + } +}