diff --git a/rhubarb/Cargo.lock b/rhubarb/Cargo.lock index ba8076f..249ea93 100644 --- a/rhubarb/Cargo.lock +++ b/rhubarb/Cargo.lock @@ -326,6 +326,7 @@ dependencies = [ "derivative", "dyn-clone", "log", + "num", "once_cell", "rstest", "rstest_reuse", diff --git a/rhubarb/rhubarb-audio/Cargo.toml b/rhubarb/rhubarb-audio/Cargo.toml index 52a5fff..d79b467 100644 --- a/rhubarb/rhubarb-audio/Cargo.toml +++ b/rhubarb/rhubarb-audio/Cargo.toml @@ -13,6 +13,7 @@ once_cell = "1.17.0" [dev-dependencies] assert_matches = "1.5.0" demonstrate = "0.4.5" +num = "0.4.0" rstest = "0.15.0" rstest_reuse = "0.4.0" speculoos = "0.10.0" diff --git a/rhubarb/rhubarb-audio/src/resampled_audio_clip/resampled_audio_clip.rs b/rhubarb/rhubarb-audio/src/resampled_audio_clip/resampled_audio_clip.rs index 39d2662..17ca752 100644 --- a/rhubarb/rhubarb-audio/src/resampled_audio_clip/resampled_audio_clip.rs +++ b/rhubarb/rhubarb-audio/src/resampled_audio_clip/resampled_audio_clip.rs @@ -173,3 +173,150 @@ unsafe extern "C" fn read(callback_data: *mut c_void, buffer: *mut *const Sample *buffer = sample_buffer.as_ptr(); sample_buffer.len() as c_long } + +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::{MemoryAudioClip, SampleReader}; +// use rstest::*; +// use speculoos::prelude::*; + +// /// A sine wave +// fn sine_440_hz(t: f64) -> Sample { +// let f = 440.0; +// f64::sin(t * f * 2.0 * std::f64::consts::PI) as f32 +// } + +// // A half-second clip of a 440 Hz sine wave sampled at 48 kHz +// #[fixture] +// fn original_clip() -> MemoryAudioClip { +// let samples: Vec = (0..48000 / 2) +// .map(|i| sine_440_hz(i as f64 / 48000.0)) +// .collect(); +// MemoryAudioClip::new(&samples, 48000) +// } + +// // The original clip resampled to 44.1 kHz +// #[fixture] +// fn resampled_clip(original_clip: MemoryAudioClip) -> ResampledAudioClip { +// ResampledAudioClip::new(Box::new(original_clip), 44100) +// } + +// mod can_be_created_via_fluent_syntax { +// use super::*; +// use crate::AudioFilters; + +// #[rstest] +// fn from_value(original_clip: MemoryAudioClip) { +// original_clip.resampled(44100); +// } + +// #[rstest] +// fn from_box(original_clip: MemoryAudioClip) { +// let boxed_clip = Box::new(original_clip); +// boxed_clip.resampled(44100); +// } +// } + +// #[rstest] +// fn supports_debug(resampled_clip: ResampledAudioClip) { +// assert_that!(format!("{resampled_clip:?}")) +// .is_equal_to("ResampledAudioClip { inner_clip: MemoryAudioClip { buffer: 11 samples, sampling_rate: 16000 }, start: 1, end: 9 }".to_owned()); +// } + +// #[rstest] +// fn provides_length(resampled_clip: ResampledAudioClip) { +// assert_that!(resampled_clip.len()).is_equal_to(22050); +// } + +// #[rstest] +// fn provides_sampling_rate(resampled_clip: ResampledAudioClip) { +// assert_that!(resampled_clip.sampling_rate()).is_equal_to(44100); +// } + +// #[rstest] +// fn supports_zero_samples() { +// let inner_clip = MemoryAudioClip::new(&[], 22025); +// let resampled_clip = ResampledAudioClip::new(Box::new(inner_clip), 16000); +// assert_that!(resampled_clip.len()).is_equal_to(0); +// assert_that!(resampled_clip.sampling_rate()).is_equal_to(16000); + +// let mut sample_reader = resampled_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(resampled_clip: ResampledAudioClip) -> Box { +// resampled_clip.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(22050); +// } + +// #[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); +// } +// } +// } diff --git a/rhubarb/rhubarb-audio/tests/open_audio_file_test.rs b/rhubarb/rhubarb-audio/tests/open_audio_file_test.rs index 25956be..276392c 100644 --- a/rhubarb/rhubarb-audio/tests/open_audio_file_test.rs +++ b/rhubarb/rhubarb-audio/tests/open_audio_file_test.rs @@ -1,6 +1,10 @@ +use num::{Float, FromPrimitive}; use rstest::*; use rstest_reuse::{self, *}; use speculoos::prelude::*; +use speculoos::{AssertionFailure, Spec}; +use std::fmt::Debug; +use std::iter::Sum; use std::{ cell::RefCell, fs::File, @@ -128,12 +132,14 @@ mod open_audio_file { let mut buffer = [0.0f32; 48 * 2]; sample_reader.read(&mut buffer).unwrap(); - for (i, sample) in buffer.iter().enumerate() { - let expected = signal_fn(i as f64 / sampling_rate as f64); - assert_that!(*sample) - .named(&i.to_string()) - .is_close_to(expected, tolerance); - } + assert_that!(buffer.to_vec()).is_close_to(&[], Tolerance::MaxError(tolerance)); + + // for (i, sample) in buffer.iter().enumerate() { + // let expected = signal_fn(i as f64 / sampling_rate as f64); + // assert_that!(*sample) + // .named(&i.to_string()) + // .is_close_to(expected, tolerance); + // } } #[apply(supported_audio_files)] @@ -325,6 +331,51 @@ mod open_audio_file { } } +trait FloatVecAssertions { + fn is_close_to(&mut self, expected: &[T], tolerance: Tolerance); +} + +enum Tolerance { + MaxError(T), + MaxRmsError(T), +} + +impl<'a, T: Float + Sum + FromPrimitive + Debug> FloatVecAssertions for Spec<'a, Vec> { + fn is_close_to(&mut self, expected: &[T], tolerance: Tolerance) { + self.has_length(expected.len()); + + let pairs = expected.iter().zip(self.subject.iter()); + match tolerance { + Tolerance::MaxError(max_error) => { + for (i, (expected_item, actual_item)) in pairs.enumerate() { + let error = (*expected_item - *actual_item).abs(); + if error > max_error { + AssertionFailure::from_spec(self) + .with_expected(format!( + "vec[{i}] = <{expected_item:?}> ± {max_error:?}" + )) + .with_actual(format!("<{actual_item:?}>")) + .fail(); + } + } + } + Tolerance::MaxRmsError(max_rmse) => { + let len = self.subject.len(); + let mse: T = + pairs.map(|(a, b)| (*a - *b).powi(2)).sum::() / T::from_usize(len).unwrap(); + let rmse = mse.sqrt(); + + if rmse > max_rmse { + AssertionFailure::from_spec(self) + .with_expected(format!("maximum RMS error <{max_rmse:?}>")) + .with_actual(format!("<{rmse:?}>")) + .fail(); + } + } + } + } +} + struct MockFile { pub file: File, pub next_error_kind: Rc>>,