From 229ef2a25bf29f6f0c5457d18d4a134e06fc796b Mon Sep 17 00:00:00 2001 From: Daniel Wolf Date: Sat, 14 Jan 2023 13:25:28 +0100 Subject: [PATCH] Add audio segment --- rhubarb/rhubarb-audio/src/audio_filters.rs | 23 +++ rhubarb/rhubarb-audio/src/lib.rs | 4 + rhubarb/rhubarb-audio/src/segment.rs | 225 +++++++++++++++++++++ 3 files changed, 252 insertions(+) create mode 100644 rhubarb/rhubarb-audio/src/audio_filters.rs create mode 100644 rhubarb/rhubarb-audio/src/segment.rs diff --git a/rhubarb/rhubarb-audio/src/audio_filters.rs b/rhubarb/rhubarb-audio/src/audio_filters.rs new file mode 100644 index 0000000..6f27864 --- /dev/null +++ b/rhubarb/rhubarb-audio/src/audio_filters.rs @@ -0,0 +1,23 @@ +use crate::{AudioClip, Segment}; + +/// Blanket implementations of audio filters for audio clips. +pub trait AudioFilters { + /// Returns a new audio clip containing a segment of the specified clip. + fn segment(self, start: u64, end: u64) -> Box; +} + +impl AudioFilters for Box { + fn segment(self, start: u64, end: u64) -> Box { + Box::new(Segment::new(self, start, end)) + } +} + +impl AudioFilters for TAudioClip +where + TAudioClip: AudioClip + 'static, +{ + fn segment(self, start: u64, end: u64) -> Box { + let boxed_audio_clip: Box = Box::new(self); + boxed_audio_clip.segment(start, end) + } +} diff --git a/rhubarb/rhubarb-audio/src/lib.rs b/rhubarb/rhubarb-audio/src/lib.rs index 3222e85..1287955 100644 --- a/rhubarb/rhubarb-audio/src/lib.rs +++ b/rhubarb/rhubarb-audio/src/lib.rs @@ -14,17 +14,21 @@ mod audio_clip; mod audio_error; +mod audio_filters; mod memory_audio_clip; mod ogg_audio_clip; mod open_audio_file; mod read_and_seek; mod sample_reader_assertions; +mod segment; mod wave_audio_clip; pub use audio_clip::{AudioClip, Sample, SampleReader}; pub use audio_error::AudioError; +pub use audio_filters::AudioFilters; 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; +pub use segment::Segment; pub use wave_audio_clip::wave_audio_clip::WaveAudioClip; diff --git a/rhubarb/rhubarb-audio/src/segment.rs b/rhubarb/rhubarb-audio/src/segment.rs new file mode 100644 index 0000000..b493e50 --- /dev/null +++ b/rhubarb/rhubarb-audio/src/segment.rs @@ -0,0 +1,225 @@ +use std::sync::Arc; + +use crate::{ + sample_reader_assertions::SampleReaderAssertions, AudioClip, AudioError, SampleReader, +}; + +/// An audio clip representing a segment of another audio clip. +#[derive(Debug, Clone)] +pub struct Segment { + inner_clip: Arc, + start: u64, + end: u64, +} + +impl Segment { + /// Creates a new audio clip from a segment of an existing audio clip. + pub fn new(inner_clip: Box, start: u64, end: u64) -> Self { + assert!( + start <= end, + "Start ({start}) must not be greater than end ({end})." + ); + let inner_clip_len = inner_clip.len(); + assert!( + end <= inner_clip_len, + "Segment {start}..{end} exceeds {inner_clip_len}-frame audio clip.", + ); + + Self { + inner_clip: Arc::from(inner_clip), + start, + end, + } + } +} + +impl AudioClip for Segment { + fn len(&self) -> u64 { + self.end - self.start + } + + fn sampling_rate(&self) -> u32 { + self.inner_clip.sampling_rate() + } + + fn create_sample_reader(&self) -> Result, AudioError> { + Ok(Box::new(SegmentSampleReader::new(self)?)) + } +} + +#[derive(Debug)] +struct SegmentSampleReader { + inner_sample_reader: Box, + start: u64, + end: u64, +} + +impl SegmentSampleReader { + fn new(segment: &Segment) -> Result { + let mut inner_sample_reader = segment.inner_clip.create_sample_reader()?; + inner_sample_reader.set_position(segment.start); + + Ok(Self { + inner_sample_reader, + start: segment.start, + end: segment.end, + }) + } +} + +impl SampleReader for SegmentSampleReader { + fn len(&self) -> u64 { + self.end - self.start + } + + fn position(&self) -> u64 { + self.inner_sample_reader.position() - self.start + } + + fn set_position(&mut self, position: u64) { + self.assert_valid_seek_position(position); + self.inner_sample_reader.set_position(position + self.start) + } + + fn read(&mut self, buffer: &mut [crate::Sample]) -> Result<(), crate::AudioError> { + self.assert_valid_read_size(buffer); + self.inner_sample_reader.read(buffer) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::*; + use speculoos::prelude::*; + + use crate::{MemoryAudioClip, SampleReader}; + + #[fixture] + fn segment() -> Segment { + let inner_clip = MemoryAudioClip::new( + &[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], + 16000, + ); + Segment::new(Box::new(inner_clip), 1, 9) + } + + mod can_be_created_via_fluent_syntax { + use super::*; + use crate::AudioFilters; + + #[rstest] + fn from_value() { + let clip = MemoryAudioClip::new(&[0.0, 0.1, 0.2, 0.3, 0.4], 16000); + clip.segment(1, 2); + } + + #[rstest] + fn from_box() { + let clip = Box::new(MemoryAudioClip::new(&[0.0, 0.1, 0.2, 0.3, 0.4], 16000)); + clip.segment(1, 2); + } + } + + #[rstest] + fn supports_debug(segment: Segment) { + assert_that!(format!("{segment:?}")) + .is_equal_to("Segment { inner_clip: MemoryAudioClip { buffer: 11 samples, sampling_rate: 16000 }, start: 1, end: 9 }".to_owned()); + } + + #[rstest] + fn provides_length(segment: Segment) { + assert_that!(segment.len()).is_equal_to(8); + } + + #[rstest] + fn provides_sampling_rate(segment: Segment) { + assert_that!(segment.sampling_rate()).is_equal_to(16000); + } + + #[rstest] + fn supports_zero_samples() { + let inner_clip = MemoryAudioClip::new(&[], 16000); + let segment = Segment::new(Box::new(inner_clip), 0, 0); + assert_that!(segment.len()).is_equal_to(0); + assert_that!(segment.sampling_rate()).is_equal_to(16000); + + let mut sample_reader = segment.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(segment: Segment) -> Box { + segment.create_sample_reader().unwrap() + } + + #[rstest] + fn supports_debug(reader: Box) { + assert_that!(format!("{reader:?}")) + .is_equal_to("SegmentSampleReader { inner_sample_reader: MemorySampleReader { buffer: 11 samples, position: 1 }, start: 1, end: 9 }".to_owned()); + } + + #[rstest] + fn provides_length(reader: Box) { + assert_that!(reader.len()).is_equal_to(8); + } + + #[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.1, 0.2, 0.3]); + + let mut five_samples = [0f32; 5]; + reader.read(&mut five_samples).unwrap(); + assert_that!(five_samples).is_equal_to([0.4, 0.5, 0.6, 0.7, 0.8]); + } + + #[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.3, 0.4, 0.5]); + + reader.set_position(1); + reader.read(&mut three_samples).unwrap(); + assert_that!(three_samples).is_equal_to([0.2, 0.3, 0.4]); + + reader.read(&mut three_samples).unwrap(); + assert_that!(three_samples).is_equal_to([0.5, 0.6, 0.7]); + } + + #[rstest] + fn seeks_up_to_the_end(mut reader: Box) { + reader.set_position(8); + let mut zero_samples = [0f32; 0]; + reader.read(&mut zero_samples).unwrap(); + } + + #[rstest] + #[should_panic(expected = "Attempting to read up to position 9 of 8-frame audio clip.")] + fn reading_beyond_the_end(mut reader: Box) { + reader.set_position(6); + let mut three_samples = [0f32; 3]; + reader.read(&mut three_samples).unwrap(); + } + + #[rstest] + #[should_panic(expected = "Attempting to seek to position 9 of 8-frame audio clip.")] + fn seeking_beyond_the_end(mut reader: Box) { + reader.set_position(9); + } + } +}