#[cfg(feature = "resampling")]
use crate::operations::types::ResamplingQuality;
use crate::operations::types::{NormalizationConfig, NormalizationMethod};
use crate::repr::AudioData;
#[cfg(feature = "resampling")]
use crate::repr::SampleRate;
use crate::{
AudioSampleError, AudioSampleResult, AudioSamples, ConvertTo, LayoutError, ParameterError,
StandardSample,
operations::traits::{AudioProcessing, AudioStatistics},
};
use ndarray::{Array2, Axis};
use non_empty_slice::NonEmptySlice;
use num_traits::FloatConst;
#[inline]
fn fir_valid_convolve<T>(input: &[T], coeffs: &[T], output_len: usize) -> Vec<T>
where
T: StandardSample,
{
let filter_len = coeffs.len();
let mut output = Vec::with_capacity(output_len);
for i in 0..output_len {
let window = &input[i..i + filter_len];
let mut sum = T::zero();
for (s, c) in window.iter().zip(coeffs.iter()) {
sum += *s * *c;
}
output.push(sum);
}
output
}
#[cfg(feature = "transforms")]
const FIR_FFT_MIN_TAPS: usize = 256;
#[cfg(feature = "transforms")]
fn fir_valid_convolve_dispatch<T>(
input: &[T],
coeffs: &[T],
output_len: usize,
) -> AudioSampleResult<Vec<T>>
where
T: StandardSample,
{
if coeffs.len() >= FIR_FFT_MIN_TAPS {
fir_valid_convolve_fft(input, coeffs, output_len)
} else {
Ok(fir_valid_convolve(input, coeffs, output_len))
}
}
#[cfg(not(feature = "transforms"))]
fn fir_valid_convolve_dispatch<T>(
input: &[T],
coeffs: &[T],
output_len: usize,
) -> AudioSampleResult<Vec<T>>
where
T: StandardSample,
{
Ok(fir_valid_convolve(input, coeffs, output_len))
}
#[cfg(feature = "transforms")]
fn fir_valid_convolve_fft<T>(
input: &[T],
coeffs: &[T],
output_len: usize,
) -> AudioSampleResult<Vec<T>>
where
T: StandardSample,
{
use non_empty_slice::NonEmptySlice;
let x: Vec<f64> = input.iter().map(|&s| -> f64 { s.convert_to() }).collect();
let h_rev: Vec<f64> = coeffs
.iter()
.rev()
.map(|&c| -> f64 { c.convert_to() })
.collect();
let x_ne = NonEmptySlice::new(x.as_slice()).ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value("audio", "empty signal"))
})?;
let h_ne = NonEmptySlice::new(h_rev.as_slice()).ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value("filter", "empty filter"))
})?;
let full = spectrograms::fft_convolve(x_ne, h_ne)?.into_vec();
let start = coeffs.len() - 1;
let out: Vec<T> = full[start..start + output_len]
.iter()
.map(|&v| -> T { v.convert_to() })
.collect();
Ok(out)
}
impl<T> AudioProcessing for AudioSamples<'_, T>
where
T: StandardSample,
{
#[inline]
fn normalize_in_place(
&mut self,
config: NormalizationConfig<Self::Sample>,
) -> AudioSampleResult<()> {
match config.method {
NormalizationMethod::MinMax => {
let min = config.min.unwrap_or(T::MIN);
let max = config.max.unwrap_or(T::MAX);
if min >= max {
return Err(AudioSampleError::Parameter(ParameterError::out_of_range(
"normalization_range",
format!("min ({min:?}) >= max ({max:?})"),
format!("{min:?}"),
format!("{max:?}"),
"min value must be less than max value for normalization",
)));
}
let current_min = self.min_sample();
let current_max = self.max_sample();
if current_min == current_max {
let middle = min + (max - min) / Self::Sample::cast_from(2.0f64);
match self.data_mut() {
AudioData::Mono(arr) => arr.fill(middle),
AudioData::Multi(arr) => arr.fill(middle),
}
return Ok(());
}
let current_range = current_max - current_min;
let target_range = max - min;
let scale_factor = target_range / current_range;
match self.data_mut() {
AudioData::Mono(arr) => {
arr.mapv_inplace(|x| min + (x - current_min) * scale_factor);
}
AudioData::Multi(arr) => {
arr.mapv_inplace(|x| min + (x - current_min) * scale_factor);
}
}
}
NormalizationMethod::Peak => {
let target = config.target.unwrap_or({
Self::Sample::MAX
});
let peak: Self::Sample = self.peak();
if peak == Self::Sample::zero() {
return Ok(()); }
let target_f64: f64 = target.convert_to();
let target_peak = target_f64.abs();
let peak_f64: f64 = peak.convert_to();
let scale_factor = target_peak / peak_f64;
let factor_t: T = T::cast_from(scale_factor);
match self.data_mut() {
AudioData::Mono(arr) => arr.mapv_inplace(|x| x * factor_t),
AudioData::Multi(arr) => arr.mapv_inplace(|x| x * factor_t),
}
}
NormalizationMethod::Mean => {
let mean: f64 = self.mean();
match self.data_mut() {
AudioData::Mono(arr) => {
arr.mapv_inplace(|x| {
let x: f64 = x.cast_into();
let diff = x - mean;
Self::Sample::cast_from(diff)
});
}
AudioData::Multi(arr) => {
arr.mapv_inplace(|x| {
let x: f64 = x.cast_into();
let diff = x - mean;
Self::Sample::cast_from(diff)
});
}
}
}
NormalizationMethod::Median => {
let median: f64 = self.midpoint_sample().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::InvalidValue {
parameter: "self".to_string(),
reason: "Self is not mono".to_string(),
})
})?;
match self.data_mut() {
AudioData::Mono(arr) => {
arr.mapv_inplace(|x| {
let x: f64 = x.cast_into();
let diff = x - median;
Self::Sample::cast_from(diff)
});
}
AudioData::Multi(arr) => {
arr.mapv_inplace(|x| {
let x: f64 = x.cast_into();
let diff = x - median;
Self::Sample::cast_from(diff)
});
}
}
}
NormalizationMethod::ZScore => {
let mean: f64 = self.mean();
let std_dev: f64 = self.std_dev();
if std_dev == 0.0f64 {
match self.data_mut() {
AudioData::Mono(arr) => {
arr.mapv_inplace(|x| {
let x: f64 = x.cast_into();
let diff = x - mean;
Self::Sample::cast_from(diff)
});
}
AudioData::Multi(arr) => {
arr.mapv_inplace(|x| {
let x: f64 = x.cast_into();
let diff = x - mean;
Self::Sample::cast_from(diff)
});
}
}
} else {
match self.data_mut() {
AudioData::Mono(arr) => {
arr.mapv_inplace(|x| {
let x: f64 = x.cast_into();
let diff = x - mean;
Self::Sample::cast_from(diff / std_dev)
});
}
AudioData::Multi(arr) => {
arr.mapv_inplace(|x| {
let x: f64 = x.cast_into();
let diff = x - mean;
Self::Sample::cast_from(diff / std_dev)
});
}
}
}
}
}
Ok(())
}
#[inline]
fn scale_in_place(&mut self, factor: f64) {
let factor_t: T = T::cast_from(factor);
match self.data_mut() {
AudioData::Mono(arr) => arr.mapv_inplace(|x| x * factor_t),
AudioData::Multi(arr) => arr.mapv_inplace(|x| x * factor_t),
}
}
#[inline]
fn remove_dc_offset_in_place(&mut self) -> AudioSampleResult<()> {
let mean: f64 = self.mean();
match self.data_mut() {
AudioData::Mono(arr) => {
arr.mapv_inplace(|x| {
let x: f64 = x.cast_into();
let diff = x - mean;
Self::Sample::cast_from(diff)
});
}
AudioData::Multi(arr) => {
arr.mapv_inplace(|x| {
let x: f64 = x.cast_into();
let diff = x - mean;
Self::Sample::cast_from(diff)
});
}
}
Ok(())
}
#[inline]
fn clip_in_place(
&mut self,
min_val: Self::Sample,
max_val: Self::Sample,
) -> AudioSampleResult<()> {
if min_val > max_val {
return Err(AudioSampleError::Parameter(ParameterError::out_of_range(
"clipping_range",
format!("min ({min_val:?}) > max ({max_val:?})"),
format!("{min_val:?}"),
format!("{max_val:?}"),
"min value must be less than or equal to max value for clipping",
)));
}
match self.data_mut() {
AudioData::Mono(arr) => arr.mapv_inplace(|x| x.clamp_to(min_val, max_val)),
AudioData::Multi(arr) => arr.mapv_inplace(|x| x.clamp_to(min_val, max_val)),
}
Ok(())
}
#[inline]
fn apply_window_in_place(
&mut self,
window: &NonEmptySlice<Self::Sample>,
) -> AudioSampleResult<()> {
match self.data_mut() {
AudioData::Mono(arr) => {
if window.len() != arr.len() {
return Err(AudioSampleError::Layout(LayoutError::dimension_mismatch(
format!("window length ({})", window.len()),
format!("audio length ({})", arr.len()),
"apply_window",
)));
}
for (sample, &win_coeff) in arr.iter_mut().zip(window.iter()) {
*sample *= win_coeff;
}
}
AudioData::Multi(arr) => {
let num_samples = arr.ncols();
if window.len() != num_samples {
return Err(AudioSampleError::Layout(LayoutError::dimension_mismatch(
format!("window length ({})", window.len()),
format!("audio length ({num_samples})"),
"apply_window",
)));
}
for mut channel in arr.axis_iter_mut(Axis(0)) {
for (sample, &win_coeff) in channel.iter_mut().zip(window.iter()) {
*sample *= win_coeff;
}
}
}
}
Ok(())
}
#[inline]
fn apply_filter_in_place(
&mut self,
filter_coeffs: &NonEmptySlice<Self::Sample>,
) -> AudioSampleResult<()> {
let coeffs: &[Self::Sample] = filter_coeffs.as_ref();
let filter_len = coeffs.len();
match self.data_mut() {
AudioData::Mono(arr) => {
if arr.len().get() < filter_len {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_length",
"Audio length must be at least as long as filter length",
)));
}
let output_len = arr.len().get() - filter_len + 1;
let owned_fallback;
let input: &[Self::Sample] = match arr.as_slice() {
Some(s) => s,
None => {
owned_fallback = arr.to_vec();
&owned_fallback
}
};
let output = fir_valid_convolve_dispatch(input, coeffs, output_len)?;
*arr = ndarray::Array1::from_vec(output).try_into()?;
}
AudioData::Multi(arr) => {
if arr.ncols().get() < filter_len {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_length",
"Audio length must be at least as long as filter length",
)));
}
let output_len = arr.ncols().get() - filter_len + 1;
let num_channels = arr.nrows().get();
let mut output = Array2::zeros((num_channels, output_len));
for (ch, mut output_channel) in output.axis_iter_mut(Axis(0)).enumerate() {
let row = arr.index_axis(Axis(0), ch);
let owned_fallback;
let input: &[Self::Sample] = match row.as_slice() {
Some(s) => s,
None => {
owned_fallback = row.to_vec();
&owned_fallback
}
};
let filtered = fir_valid_convolve_dispatch(input, coeffs, output_len)?;
for (dst, src) in output_channel.iter_mut().zip(filtered) {
*dst = src;
}
}
*arr = output.try_into()?;
}
}
Ok(())
}
#[inline]
fn mu_compress_in_place(&mut self, mu: Self::Sample) -> AudioSampleResult<()> {
let mu_f64: f64 = mu.convert_to();
let mu_plus_one: f64 = mu_f64 + 1.0;
self.apply_with_error_in_place(|x: Self::Sample| {
let x: f64 = x.convert_to();
let sign = if x >= 0.0 { 1.0 } else { -1.0 };
let abs_x = x.abs();
let compressed = sign * mu_f64.mul_add(abs_x, mu_plus_one.ln()).ln() / mu_plus_one.ln();
Ok(Self::Sample::convert_from(compressed))
})
}
#[inline]
fn mu_expand_in_place(&mut self, mu: Self::Sample) -> AudioSampleResult<()> {
let mu: f64 = mu.convert_to();
let mu_plus_one = mu + 1.0;
self.apply_with_error_in_place(|x: Self::Sample| {
let x_f64: f64 = x.convert_to();
let sign = if x_f64 >= 0.0 { 1.0 } else { -1.0 };
let abs_x = x_f64.abs();
let expanded = sign * (mu_plus_one.powf(abs_x) - 1.0) / mu;
Ok(Self::Sample::convert_from(expanded))
})
}
#[inline]
fn low_pass_filter_in_place(&mut self, cutoff_hz: f64) -> AudioSampleResult<()> {
let sample_rate = self.sample_rate_hz();
let normalized_cutoff = cutoff_hz / sample_rate;
if normalized_cutoff >= 0.5 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"cutoff_hz",
"Cutoff frequency must be less than Nyquist frequency",
)));
}
let alpha = 2.0 * f64::PI() * normalized_cutoff;
let one_minus_alpha = 1.0 - alpha;
match self.data_mut() {
AudioData::Mono(arr) => {
let mut prev_output: f64 = arr[0].convert_to();
for sample in arr.iter_mut() {
let s: f64 = (*sample).convert_to();
let s = alpha * s + one_minus_alpha * prev_output;
prev_output = s;
*sample = s.convert_to();
}
}
AudioData::Multi(arr) => {
for mut channel in arr.axis_iter_mut(Axis(0)) {
let mut prev_output: f64 = channel[0].convert_to();
for sample in &mut channel {
let s: f64 = (*sample).convert_to();
let s = alpha * s + one_minus_alpha * prev_output;
prev_output = s;
*sample = s.convert_to();
}
}
}
}
Ok(())
}
#[inline]
fn high_pass_filter_in_place(&mut self, cutoff_hz: f64) -> AudioSampleResult<()> {
let sample_rate = self.sample_rate_hz();
let normalized_cutoff = cutoff_hz / sample_rate;
if normalized_cutoff >= 0.5 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"cutoff_hz",
"Cutoff frequency must be less than Nyquist frequency",
)));
}
let rc = 1.0 / (2.0 * f64::PI() * cutoff_hz);
let dt = 1.0 / sample_rate;
let alpha = T::cast_from(rc / (rc + dt));
match self.data_mut() {
AudioData::Mono(arr) => {
if arr.len().get() > 1 {
let mut prev_input = arr[0];
let mut prev_output = Self::Sample::zero();
for sample in arr.iter_mut() {
let current = *sample;
*sample = alpha * (prev_output + current - prev_input);
prev_input = current;
prev_output = *sample;
}
}
}
AudioData::Multi(arr) => {
for mut channel in arr.axis_iter_mut(Axis(0)) {
if channel.len() > 1 {
let mut prev_input = channel[0];
let mut prev_output = Self::Sample::zero();
for sample in &mut channel {
let current = *sample;
*sample = alpha * (prev_output + current - prev_input);
prev_input = current;
prev_output = *sample;
}
}
}
}
}
Ok(())
}
#[inline]
fn band_pass_filter_in_place(
&mut self,
low_cutoff_hz: f64,
high_cutoff_hz: f64,
) -> AudioSampleResult<()> {
if low_cutoff_hz >= high_cutoff_hz {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"frequency_range",
"Low frequency must be less than high frequency",
)));
}
self.high_pass_filter_in_place(low_cutoff_hz)?;
self.low_pass_filter_in_place(high_cutoff_hz)
}
#[cfg(feature = "resampling")]
#[inline]
fn resample(
&self,
target_sample_rate: SampleRate,
quality: ResamplingQuality,
) -> AudioSampleResult<AudioSamples<'static, Self::Sample>> {
use crate::resample;
resample::<Self::Sample>(self, target_sample_rate, quality)
}
#[cfg(feature = "resampling")]
#[inline]
fn resample_in_place(
&mut self,
target_sample_rate: SampleRate,
quality: ResamplingQuality,
) -> AudioSampleResult<()> {
*self = self.resample(target_sample_rate, quality)?.into_owned();
Ok(())
}
#[cfg(feature = "resampling")]
#[inline]
fn resample_by_ratio(
&self,
ratio: f64,
quality: ResamplingQuality,
) -> AudioSampleResult<AudioSamples<'static, Self::Sample>> {
use crate::resample_by_ratio;
resample_by_ratio(self, ratio, quality)
}
#[cfg(feature = "resampling")]
#[inline]
fn resample_by_ratio_in_place(
&mut self,
ratio: f64,
quality: ResamplingQuality,
) -> AudioSampleResult<()> {
*self = self.resample_by_ratio(ratio, quality)?.into_owned();
Ok(())
}
}
impl<T> AudioSamples<'_, T>
where
T: StandardSample,
{
#[inline]
pub fn apply_with_error_in_place<F>(&mut self, f: F) -> AudioSampleResult<()>
where
F: Fn(T) -> AudioSampleResult<T>,
{
match self.data_mut() {
AudioData::Mono(arr) => {
for x in arr.iter_mut() {
*x = f(*x)?;
}
}
AudioData::Multi(arr) => {
for x in arr.iter_mut() {
*x = f(*x)?;
}
}
}
Ok(())
}
#[inline]
pub fn try_fold<Acc, F>(&mut self, mut acc: Acc, mut f: F) -> AudioSampleResult<Acc>
where
F: FnMut(&mut Acc, T) -> AudioSampleResult<T>,
{
match self.data_mut() {
AudioData::Mono(arr) => {
for x in arr.iter_mut() {
*x = f(&mut acc, *x)?;
}
Ok(acc)
}
AudioData::Multi(arr) => {
for x in arr.iter_mut() {
*x = f(&mut acc, *x)?;
}
Ok(acc)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sample_rate;
use approx_eq::assert_approx_eq;
use ndarray::array;
use crate::AudioProcessing;
#[cfg(feature = "transforms")]
#[test]
fn apply_filter_fft_matches_direct_long_filter() {
let n = 4000usize;
let m = 512usize; assert!(m >= FIR_FFT_MIN_TAPS);
let input: Vec<f32> = (0..n)
.map(|i| (i as f32 * 0.013).sin() + 0.4 * (i as f32 * 0.07).cos())
.collect();
let coeffs: Vec<f32> = (0..m)
.map(|k| ((k as f32 * 0.05).sin()) / m as f32)
.collect();
let output_len = n - m + 1;
let reference = fir_valid_convolve(&input, &coeffs, output_len);
let audio: AudioSamples<f32> =
AudioSamples::new_mono(ndarray::Array1::from_vec(input), sample_rate!(44100)).unwrap();
let filtered = audio
.apply_filter(NonEmptySlice::new(&coeffs).unwrap())
.unwrap();
assert_eq!(filtered.samples_per_channel().get(), output_len);
for (i, &want) in reference.iter().enumerate() {
let got = filtered[i];
assert!(
(got - want).abs() < 1e-3,
"sample {i}: fft {got} vs direct {want}"
);
}
}
#[test]
fn test_normalize_min_max() {
let data = array![1.0f32, 2.0, 3.0, 4.0, 5.0];
let audio: AudioSamples<f32> = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let audio = audio
.normalize(NormalizationConfig::min_max(-1.0, 1.0))
.unwrap();
assert_approx_eq!(audio.min_sample() as f64, -1.0);
assert_approx_eq!(audio.max_sample() as f64, 1.0);
}
#[test]
fn test_normalize_peak() {
let data = array![-2.0f32, 1.0, 3.0, -1.0];
let audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let audio = audio.normalize(NormalizationConfig::peak(1.0)).unwrap();
assert_approx_eq!(audio.peak() as f64, 1.0);
}
#[test]
fn test_scale() {
let data = array![1.0f32, 2.0, 3.0];
let audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let audio = audio.scale(2.0);
let expected = array![2.0f32, 4.0, 6.0];
match audio.data() {
AudioData::Mono(arr) => {
for (actual, expected) in arr.iter().zip(expected.iter()) {
assert_approx_eq!(*actual as f64, *expected as f64, 1e-6);
}
}
_ => panic!("Expected mono data"),
}
}
#[test]
fn test_remove_dc_offset() {
let data = array![3.0f32, 4.0, 5.0]; let audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let audio = audio.remove_dc_offset().unwrap();
let mean: f64 = audio.mean();
assert_approx_eq!(mean as f64, 0.0, 1e-6);
}
#[test]
fn test_clip() {
let data = array![-3.0f32, -1.0, 0.0, 1.0, 3.0];
let audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let audio = audio.clip(-2.0, 2.0).unwrap();
let expected = array![-2.0f32, -1.0, 0.0, 1.0, 2.0];
match audio.data() {
AudioData::Mono(arr) => {
for (actual, expected) in arr.iter().zip(expected.iter()) {
assert_approx_eq!(*actual as f64, *expected as f64, 1e-6);
}
}
_ => panic!("Expected mono data"),
}
}
#[test]
fn test_apply_window() {
let data = array![1.0f32, 1.0, 1.0, 1.0];
let audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let window = [0.5f32, 1.0, 1.0, 0.5];
let window_slice = NonEmptySlice::new(&window).unwrap();
let audio = audio.apply_window(window_slice).unwrap();
let expected = array![0.5f32, 1.0, 1.0, 0.5];
match audio.data() {
AudioData::Mono(arr) => {
for (actual, expected) in arr.iter().zip(expected.iter()) {
assert_approx_eq!(*actual as f64, *expected as f64, 1e-6);
}
}
_ => panic!("Expected mono data"),
}
}
#[test]
fn test_multi_channel_normalize() {
let data = array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]];
let audio: AudioSamples<f32> =
AudioSamples::new_multi_channel(data, sample_rate!(44100)).unwrap();
let audio = audio
.normalize(NormalizationConfig::min_max(-1.0, 1.0))
.unwrap();
assert_approx_eq!(audio.min_sample() as f64, -1.0);
assert_approx_eq!(audio.max_sample() as f64, 1.0);
}
#[test]
fn test_normalize_zscore() {
let data = array![1.0f32, 2.0, 3.0, 4.0, 5.0];
let audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let audio = audio.normalize(NormalizationConfig::zscore()).unwrap();
let mean: f64 = audio.mean();
let std_dev: f64 = audio.std_dev();
assert_approx_eq!(mean as f64, 0.0, 1e-6);
assert_approx_eq!(std_dev as f64, 1.0, 1e-6);
}
#[test]
fn test_direct_chaining() {
let data = array![1.0f32, 2.0, 3.0, 4.0, 5.0];
let audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let audio = audio.scale(2.0).clip(-5.0, 5.0).unwrap();
let expected = array![2.0f32, 4.0, 5.0, 5.0, 5.0]; let result_data = audio.as_mono().unwrap();
for (actual, expected) in result_data.iter().zip(expected.iter()) {
assert_approx_eq!(*actual as f64, *expected as f64, 1e-6);
}
}
#[test]
fn test_chaining_error_handling() {
let data = array![1.0f32, 2.0, 3.0];
let audio = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let result = audio.normalize(NormalizationConfig::min_max(2.0, 1.0));
assert!(result.is_err());
}
#[test]
fn test_multi_channel_chaining() {
let data = array![[1.0f32, 2.0], [3.0, 4.0]];
let audio = AudioSamples::new_multi_channel(data, sample_rate!(44100)).unwrap();
let expected = array![[0.5f32, 1.0], [1.5, 2.0]];
let audio = audio.scale(0.5);
let result_data = audio.as_multi_channel().unwrap();
for (actual_row, expected_row) in result_data
.axis_iter(ndarray::Axis(0))
.zip(expected.axis_iter(ndarray::Axis(0)))
{
for (actual, expected) in actual_row.iter().zip(expected_row.iter()) {
assert_approx_eq!(*actual as f64, *expected as f64, 1e-6);
}
}
}
fn assert_mono_eq(a: &AudioSamples<f32>, b: &AudioSamples<f32>) {
let a = a.as_mono().unwrap();
let b = b.as_mono().unwrap();
assert_eq!(a.len(), b.len(), "lengths differ");
for (x, y) in a.iter().zip(b.iter()) {
assert_approx_eq!(*x as f64, *y as f64, 1e-6);
}
}
#[test]
fn test_normalize_dual_variant_equivalence() {
let data = array![1.0f32, -3.0, 2.0, -1.0, 0.5];
let original = AudioSamples::new_mono(data, sample_rate!(44100)).unwrap();
let borrowed = original.normalize(NormalizationConfig::peak(1.0)).unwrap();
assert_mono_eq(
&original,
&AudioSamples::new_mono(array![1.0f32, -3.0, 2.0, -1.0, 0.5], sample_rate!(44100))
.unwrap(),
);
let mut mutated = original.clone();
mutated
.normalize_in_place(NormalizationConfig::peak(1.0))
.unwrap();
assert_mono_eq(&borrowed, &mutated);
}
#[test]
fn test_scale_dual_variant_equivalence() {
let original =
AudioSamples::new_mono(array![1.0f32, -2.0, 3.0, -4.0], sample_rate!(44100)).unwrap();
let borrowed = original.scale(0.25);
assert_mono_eq(
&original,
&AudioSamples::new_mono(array![1.0f32, -2.0, 3.0, -4.0], sample_rate!(44100)).unwrap(),
);
let mut mutated = original.clone();
mutated.scale_in_place(0.25);
assert_mono_eq(&borrowed, &mutated);
}
#[test]
fn test_clip_dual_variant_equivalence() {
let original =
AudioSamples::new_mono(array![2.0f32, -3.0, 1.5, -0.5], sample_rate!(44100)).unwrap();
let borrowed = original.clip(-1.0, 1.0).unwrap();
assert_mono_eq(
&original,
&AudioSamples::new_mono(array![2.0f32, -3.0, 1.5, -0.5], sample_rate!(44100)).unwrap(),
);
let mut mutated = original.clone();
mutated.clip_in_place(-1.0, 1.0).unwrap();
assert_mono_eq(&borrowed, &mutated);
}
}