use std::num::NonZeroUsize;
use std::time::Duration;
#[cfg(feature = "random-generation")]
use crate::brown_noise;
use crate::operations::types::{FadeCurve, NoiseColor, PadSide};
#[cfg(all(feature = "random-generation", feature = "iir-filtering"))]
use crate::operations::types::{PerturbationConfig, PerturbationMethod};
use crate::repr::{AudioData, ChannelCount};
use crate::utils::{samples_to_seconds, seconds_to_samples};
use crate::{
AudioEditing, AudioSampleError, AudioSampleResult, AudioSamples, AudioStatistics,
AudioTypeConversion, ConvertTo, LayoutError, ParameterError, StandardSample,
};
#[cfg(feature = "random-generation")]
use crate::{pink_noise, white_noise};
use ndarray::{Array1, Array2, Axis, s};
use non_empty_slice::{NonEmptySlice, NonEmptyVec};
#[cfg(feature = "random-generation")]
use rand::Rng;
fn validate_time_bounds(start: f64, end: f64, duration: f64) -> AudioSampleResult<()> {
if start < 0.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
format!("Start time cannot be negative: {start}"),
)));
}
if end <= start {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
format!("End time ({end}) must be greater than start time ({start})"),
)));
}
if end > duration {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
format!("End time ({end}) exceeds audio duration ({duration})"),
)));
}
Ok(())
}
#[inline(always)]
fn apply_gain_to_slice<T, F>(slice: &mut [T], step: f64, start: f64, gain_fn: F)
where
T: StandardSample,
F: Fn(f64) -> f64,
{
for (i, x) in slice.iter_mut().enumerate() {
*x *= T::cast_from(gain_fn(start + i as f64 * step));
}
}
impl<T> AudioEditing for AudioSamples<'_, T>
where
T: StandardSample,
Self: AudioTypeConversion<Sample = T>,
{
fn reverse<'b>(&self) -> AudioSamples<'b, T> {
match &self.data {
AudioData::Mono(arr) => {
let reversed = arr.slice(s![..;-1]).to_owned();
AudioSamples::new_mono(reversed, self.sample_rate())
.unwrap_or_else(|_| unreachable!("self was valid, therefore reversed is valid"))
}
AudioData::Multi(arr) => {
let reversed = arr.slice(s![.., ..;-1]).to_owned();
AudioSamples::new_multi_channel(reversed, self.sample_rate())
.unwrap_or_else(|_| unreachable!("self was valid, therefore reversed is valid"))
}
}
}
fn reverse_in_place(&mut self) -> AudioSampleResult<()> {
match &mut self.data {
AudioData::Mono(arr) => {
arr.as_slice_mut().reverse();
}
AudioData::Multi(arr) => {
for mut channel in arr.axis_iter_mut(Axis(0)) {
channel
.as_slice_mut()
.ok_or_else(|| {
AudioSampleError::Layout(LayoutError::NonContiguous {
operation: "array access".to_string(),
layout_type: "Multi-channel samples must be contiguous".to_string(),
})
})?
.reverse();
}
}
}
Ok(())
}
fn trim<'b>(
&self,
start_seconds: f64,
end_seconds: f64,
) -> AudioSampleResult<AudioSamples<'b, T>> {
let duration: f64 = self.duration_seconds();
validate_time_bounds(start_seconds, end_seconds, duration)?;
let start_sample = seconds_to_samples(start_seconds, self.sample_rate());
let end_sample = seconds_to_samples(end_seconds, self.sample_rate());
match &self.data {
AudioData::Mono(arr) => {
if end_sample > arr.len().get() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
format!(
"End sample {} exceeds audio length {}",
end_sample,
arr.len().get()
),
)));
}
let trimmed = arr.slice(s![start_sample..end_sample]).to_owned();
AudioSamples::new_mono(trimmed, self.sample_rate())
}
AudioData::Multi(arr) => {
if end_sample > arr.ncols().get() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
format!(
"End sample {} exceeds audio length {}",
end_sample,
arr.ncols().get()
),
)));
}
let trimmed = arr.slice(s![.., start_sample..end_sample]).to_owned();
AudioSamples::new_multi_channel(trimmed, self.sample_rate())
}
}
}
fn pad<'b>(
&self,
pad_start_seconds: f64,
pad_end_seconds: f64,
pad_value: T,
) -> AudioSampleResult<AudioSamples<'b, T>> {
if pad_start_seconds < 0.0 || pad_end_seconds < 0.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"padding_durations",
"Padding durations cannot be negative",
)));
}
let start_samples = seconds_to_samples(pad_start_seconds, self.sample_rate());
let end_samples = seconds_to_samples(pad_end_seconds, self.sample_rate());
match &self.data {
AudioData::Mono(arr) => {
let n = arr.len().get();
let total = start_samples + n + end_samples;
let mut v: Vec<T> = Vec::with_capacity(total);
v.resize(start_samples, pad_value);
match arr.as_slice() {
Some(src) => v.extend_from_slice(src),
None => v.extend(arr.iter().copied()),
}
v.resize(total, pad_value);
AudioSamples::new_mono(Array1::from(v), self.sample_rate())
}
AudioData::Multi(arr) => {
let total_length = start_samples + arr.ncols().get() + end_samples;
let mut padded = Array2::from_elem((arr.nrows().get(), total_length), pad_value);
padded
.slice_mut(s![.., start_samples..start_samples + arr.ncols().get()])
.assign(&arr.view());
AudioSamples::new_multi_channel(padded, self.sample_rate())
}
}
}
fn pad_samples_right<'b>(
&self,
target_num_samples: usize,
pad_value: T,
) -> AudioSampleResult<AudioSamples<'b, T>> {
let current_num_samples = self.samples_per_channel().get();
if target_num_samples <= current_num_samples {
return Ok(self.clone().into_owned());
}
let target_num_samples_seconds: f64 =
samples_to_seconds(target_num_samples, self.sample_rate.get());
self.pad_to_duration(target_num_samples_seconds, pad_value, PadSide::Right)
}
fn pad_to_duration<'b>(
&self,
target_duration_seconds: f64,
pad_value: T,
pad_side: PadSide,
) -> AudioSampleResult<AudioSamples<'b, T>> {
let current_duration = self.duration_seconds();
if target_duration_seconds <= current_duration {
return Ok(self.clone().into_owned());
}
let total_target_samples =
seconds_to_samples(target_duration_seconds, self.sample_rate.get());
let current_samples = self.samples_per_channel().get();
let total_padding_samples = total_target_samples - current_samples;
let (pad_start_samples, pad_end_samples) = match pad_side {
PadSide::Left => (total_padding_samples, 0),
PadSide::Right => (0, total_padding_samples),
};
let padded = self.pad(
pad_start_samples as f64 / f64::from(self.sample_rate.get()),
pad_end_samples as f64 / f64::from(self.sample_rate.get()),
pad_value,
)?;
Ok(padded)
}
fn split(
&self,
segment_duration_seconds: f64,
) -> AudioSampleResult<Vec<AudioSamples<'static, T>>> {
if segment_duration_seconds <= 0.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Segment duration must be positive",
)));
}
let segment_samples = seconds_to_samples(segment_duration_seconds, self.sample_rate.get());
let total_samples = self.samples_per_channel().get();
if segment_samples > total_samples {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Segment duration exceeds audio length",
)));
}
let mut segments = Vec::new();
let mut start = 0;
while start < total_samples {
let end = (start + segment_samples).min(total_samples);
match &self.data {
AudioData::Mono(arr) => {
let segment = arr.slice(s![start..end]).to_owned();
segments.push(AudioSamples::new_mono(segment, self.sample_rate())?);
}
AudioData::Multi(arr) => {
let segment = arr.slice(s![.., start..end]).to_owned();
segments.push(AudioSamples::new_multi_channel(
segment,
self.sample_rate(),
)?);
}
}
start += segment_samples;
}
Ok(segments)
}
fn concatenate<'b>(
segments: &'b NonEmptySlice<AudioSamples<'b, T>>,
) -> AudioSampleResult<AudioSamples<'b, T>> {
let first_sample_rate = segments[0].sample_rate();
let first_num_channels = segments[0].num_channels();
for segment in segments.iter().skip(1) {
if segment.sample_rate() != first_sample_rate {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"All segments must have the same sample rate",
)));
}
if segment.num_channels() != first_num_channels {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"All segments must have the same number of channels",
)));
}
}
let first_sample_rate = segments[0].sample_rate();
let first_is_mono = segments[0].is_mono();
if first_is_mono {
let mut all_samples = Vec::new();
for segment in segments {
let owned_segment = segment.clone().into_owned();
let segment = owned_segment.as_mono().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"input",
"Expected mono audio data",
))
})?;
all_samples.extend_from_slice(segment.as_slice().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"input",
"Mono samples must be contiguous",
))
})?);
}
let concatenated = Array1::from_vec(all_samples);
AudioSamples::new_mono(concatenated, first_sample_rate)
} else {
let num_channels = segments[0].num_channels().get();
let mut total_samples = 0;
for segment in segments {
total_samples += segment.samples_per_channel().get();
}
let mut concatenated_data: Vec<T> =
Vec::with_capacity(num_channels as usize * total_samples);
for channel_idx in 0..num_channels as usize {
for segment in segments {
let owned_segment = segment.clone().into_owned();
let segment_multi = owned_segment.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"input",
"Expected multi-channel audio data",
))
})?;
let channel_data = segment_multi.row(channel_idx);
concatenated_data.extend_from_slice(channel_data.as_slice().ok_or_else(
|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"input",
"Multi-channel samples must be contiguous",
))
},
)?);
}
}
let concatenated =
Array2::from_shape_vec((num_channels as usize, total_samples), concatenated_data)?;
AudioSamples::new_multi_channel(concatenated, first_sample_rate)
}
}
fn stack(sources: &NonEmptySlice<Self>) -> AudioSampleResult<AudioSamples<'static, T>> {
if sources.len() == NonZeroUsize::new(1).expect("1 is non-zero") {
return Ok(sources[0].clone().into_owned());
}
let first: &AudioSamples<T> = &sources[0];
if first.is_multi_channel() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Stacking is only supported for mono sources",
)));
}
for (idx, source) in sources.iter().enumerate().skip(1) {
if source.is_multi_channel() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
format!(
"Stacking is only supported for mono sources. Audio at index {idx} is not mono"
),
)));
}
if source.sample_rate() != first.sample_rate() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"All sources must have the same sample rate",
)));
}
if source.samples_per_channel() != first.samples_per_channel() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"All sources must have the same length",
)));
}
}
let num_sources = sources.len();
let num_samples = first.samples_per_channel();
let mut stacked = Array2::zeros((num_sources.get(), num_samples.get()));
for (i, source) in sources.iter().enumerate() {
if let AudioData::Mono(arr) = &source.data {
stacked.slice_mut(s![i, ..]).assign(&arr.view());
}
}
AudioSamples::new_multi_channel(stacked, first.sample_rate())
}
fn mix(
sources: &NonEmptySlice<Self>,
weights: Option<&NonEmptySlice<f64>>,
) -> AudioSampleResult<AudioSamples<'static, T>> {
let first: &AudioSamples<T> = &sources[0];
for source in sources.iter().skip(1) {
if source.sample_rate() != first.sample_rate() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"All sources must have the same sample rate",
)));
}
if source.num_channels() != first.num_channels() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"All sources must have the same number of channels",
)));
}
if source.samples_per_channel() != first.samples_per_channel() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"All sources must have the same length",
)));
}
}
let mix_weights = if let Some(w) = weights {
if w.len() != sources.len() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Number of weights must match number of sources",
)));
}
w.as_slice()
} else {
&vec![1.0 / sources.len().get() as f64; sources.len().get()]
};
match &first.data {
AudioData::Mono(_) => {
let mut result = first.clone();
if let AudioData::Mono(result_arr) = &mut result.data {
if let AudioData::Mono(_first_arr) = &first.data {
let weight: T = mix_weights[0].convert_to();
result_arr.mapv_inplace(|x| x * weight);
}
for (i, source) in sources.iter().skip(1).enumerate() {
if let AudioData::Mono(source_arr) = &source.data {
let weight: T = mix_weights[i + 1].convert_to();
for (r, s) in result_arr.iter_mut().zip(source_arr.iter()) {
*r += *s * weight;
}
}
}
}
Ok(result.into_owned())
}
AudioData::Multi(_) => {
let mut result = first.clone();
if let AudioData::Multi(result_arr) = &mut result.data {
if let AudioData::Multi(_first_arr) = &first.data {
let weight: T = mix_weights[0].convert_to();
result_arr.mapv_inplace(|x| x * weight);
}
for (i, source) in sources.iter().skip(1).enumerate() {
if let AudioData::Multi(source_arr) = &source.data {
let weight: T = mix_weights[i + 1].convert_to();
for (r, s) in result_arr.iter_mut().zip(source_arr.iter()) {
*r += *s * weight;
}
}
}
}
Ok(result.into_owned())
}
}
}
fn fade_in(&mut self, duration_seconds: f64, curve: FadeCurve) -> AudioSampleResult<()> {
if duration_seconds <= 0.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Fade duration must be positive",
)));
}
let fade_samples = seconds_to_samples(duration_seconds, self.sample_rate());
let total_samples = self.samples_per_channel().get();
let actual_fade_samples = fade_samples.min(total_samples);
let n = actual_fade_samples;
let step = 1.0_f64 / n as f64;
macro_rules! apply_fade_in {
($gain:expr) => {{
match &mut self.data {
AudioData::Mono(arr) => {
apply_gain_to_slice(&mut arr.as_slice_mut()[..n], step, 0.0, $gain);
}
AudioData::Multi(arr) => {
for mut ch in arr.axis_iter_mut(Axis(0)) {
let s = ch
.as_slice_mut()
.expect("fade_in: channel row must be contiguous");
apply_gain_to_slice(&mut s[..n], step, 0.0, $gain);
}
}
}
}};
}
match curve {
FadeCurve::Linear => apply_fade_in!(|p: f64| p),
FadeCurve::Exponential => apply_fade_in!(|p: f64| p * p),
FadeCurve::Logarithmic => apply_fade_in!(|p: f64| {
if p <= 0.0 {
0.0
} else {
p.ln_1p() / 2.0f64.ln()
}
}),
FadeCurve::SmoothStep => apply_fade_in!(|p: f64| p * p * 2.0f64.mul_add(-p, 3.0)),
}
Ok(())
}
fn fade_out(&mut self, duration_seconds: f64, curve: FadeCurve) -> AudioSampleResult<()> {
if duration_seconds <= 0.0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Fade duration must be positive",
)));
}
let fade_samples = seconds_to_samples(duration_seconds, self.sample_rate());
let total_samples = self.samples_per_channel().get();
let actual_fade_samples = fade_samples.min(total_samples);
let start_sample = total_samples - actual_fade_samples;
let n = actual_fade_samples;
let start = start_sample;
let step = 1.0_f64 / n as f64;
macro_rules! apply_fade_out {
($gain:expr) => {{
match &mut self.data {
AudioData::Mono(arr) => {
apply_gain_to_slice(
&mut arr.as_slice_mut()[start..start + n],
-step,
1.0,
$gain,
);
}
AudioData::Multi(arr) => {
for mut ch in arr.axis_iter_mut(Axis(0)) {
let s = ch
.as_slice_mut()
.expect("fade_out: channel row must be contiguous");
apply_gain_to_slice(&mut s[start..start + n], -step, 1.0, $gain);
}
}
}
}};
}
match curve {
FadeCurve::Linear => apply_fade_out!(|p: f64| p),
FadeCurve::Exponential => apply_fade_out!(|p: f64| p * p),
FadeCurve::Logarithmic => apply_fade_out!(|p: f64| {
if p <= 0.0 {
0.0
} else {
p.ln_1p() / 2.0f64.ln()
}
}),
FadeCurve::SmoothStep => apply_fade_out!(|p: f64| p * p * 2.0f64.mul_add(-p, 3.0)),
}
Ok(())
}
fn repeat(&self, count: usize) -> AudioSampleResult<AudioSamples<'static, T>> {
if count == 0 {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"Repeat count must be greater than 0",
)));
}
if count == 1 {
return Ok(self.clone().into_owned());
}
match &self.data {
AudioData::Mono(arr) => {
let mut repeated = Array1::zeros(arr.len().get() * count);
for i in 0..count {
let start = i * arr.len().get();
let end = start + arr.len().get();
repeated.slice_mut(s![start..end]).assign(&arr.view());
}
AudioSamples::new_mono(repeated, self.sample_rate())
}
AudioData::Multi(arr) => {
let mut repeated = Array2::zeros((arr.nrows().get(), arr.ncols().get() * count));
for i in 0..count {
let start = i * arr.ncols().get();
let end = start + arr.ncols().get();
repeated.slice_mut(s![.., start..end]).assign(&arr.view());
}
AudioSamples::new_multi_channel(repeated, self.sample_rate())
}
}
}
fn trim_silence(&self, threshold_db: f64) -> AudioSampleResult<AudioSamples<'static, T>> {
let threshold_lin: f64 = 10.0f64.powf(threshold_db / 20.0);
match &self.data {
AudioData::Mono(arr) => {
let mut start = 0usize;
let mut found_start = false;
for (idx, sample) in arr.iter().enumerate() {
let value: f64 = (*sample).convert_to();
if value.abs() > threshold_lin {
start = idx;
found_start = true;
break;
}
}
if !found_start {
return Ok(AudioSamples::zeros_mono(arr.len(), self.sample_rate()));
}
let mut end = arr.len().get() - 1;
for (idx, sample) in arr.iter().enumerate().rev() {
let value: f64 = (*sample).convert_to();
if value.abs() > threshold_lin {
end = idx;
break;
}
}
AudioSamples::new_mono(arr.slice(s![start..=end]).to_owned(), self.sample_rate())
}
AudioData::Multi(arr) => {
let n_frames = arr.ncols().get();
let mut start = 0usize;
let mut found_start = false;
for idx in 0..n_frames {
let col = arr.column(idx);
let is_silent = col.iter().all(|&x| {
let value: f64 = x.convert_to();
value <= threshold_lin
});
if !is_silent {
start = idx;
found_start = true;
break;
}
}
if !found_start {
return Ok(AudioSamples::zeros_multi(
ChannelCount::new(arr.nrows().get() as u32).expect("Guaranteed non-zero"),
arr.len(),
self.sample_rate(),
));
}
let mut end = n_frames - 1;
for idx in (0..n_frames).rev() {
let col = arr.column(idx);
let is_silent = col.iter().all(|&x| {
let value: f64 = x.convert_to();
value <= threshold_lin
});
if !is_silent {
end = idx;
break;
}
}
AudioSamples::new_multi_channel(
arr.slice(s![.., start..=end]).to_owned(),
self.sample_rate(),
)
}
}
}
#[cfg(all(feature = "random-generation", feature = "iir-filtering"))]
fn perturb<'b>(&self, config: &PerturbationConfig) -> AudioSampleResult<AudioSamples<'b, T>> {
let mut owned = self.clone();
owned.perturb_in_place(config)?;
Ok(owned.into_owned())
}
#[cfg(all(feature = "random-generation", feature = "iir-filtering"))]
fn perturb_in_place(&mut self, config: &PerturbationConfig) -> AudioSampleResult<()> {
config.validate(f64::from(self.sample_rate.get()))?;
if let Some(seed) = config.seed {
use rand::{SeedableRng, rngs::StdRng};
let mut rng = StdRng::seed_from_u64(seed);
apply_perturbation_with_rng(self, &config.method, &mut rng)
} else {
let mut rng = rand::rng();
apply_perturbation_with_rng(self, &config.method, &mut rng)
}
}
fn trim_all_silence(
&self,
threshold_db: f64,
min_silence_duration_seconds: f64,
) -> AudioSampleResult<AudioSamples<'static, Self::Sample>> {
let threshold_lin: f64 = 10.0f64.powf(threshold_db / 20.0);
let sr = self.sample_rate().get();
let min_silence_samples = (min_silence_duration_seconds * f64::from(sr)).round() as usize;
match &self.data {
AudioData::Mono(arr) => {
let mut segments: Vec<(usize, usize)> = Vec::new();
let mut in_silence = true;
let mut silence_start = 0usize;
let mut segment_start = 0usize;
for (i, sample) in arr.iter().enumerate() {
let value: f64 = (*sample).convert_to();
let is_silent = value.abs() <= threshold_lin;
if in_silence && !is_silent {
segment_start = i;
in_silence = false;
} else if !in_silence && is_silent {
silence_start = i;
in_silence = true;
} else if in_silence {
if i - silence_start >= min_silence_samples && segment_start < silence_start
{
segments.push((segment_start, silence_start));
segment_start = silence_start;
}
}
}
if !in_silence && segment_start < arr.len().get() {
segments.push((segment_start, arr.len().get()));
}
let total_len: usize = segments.iter().map(|(s, e)| e - s).sum();
let mut result = Array1::<T>::zeros(total_len);
let mut offset = 0usize;
for (s, e) in segments {
let len = e - s;
result
.slice_mut(s![offset..offset + len])
.assign(&arr.slice(s![s..e]));
offset += len;
}
AudioSamples::new_mono(result, self.sample_rate())
}
AudioData::Multi(arr) => {
let n_channels = arr.nrows().get();
let n_frames = arr.ncols().get();
let mut segments: Vec<(usize, usize)> = Vec::new();
let mut in_silence = true;
let mut silence_start = 0usize;
let mut segment_start = 0usize;
for i in 0..n_frames {
let is_silent = arr.column(i).iter().all(|&x| {
let value: f64 = x.convert_to();
value <= threshold_lin
});
if in_silence && !is_silent {
segment_start = i;
in_silence = false;
} else if !in_silence && is_silent {
silence_start = i;
in_silence = true;
} else if in_silence
&& i - silence_start >= min_silence_samples
&& segment_start < silence_start
{
segments.push((segment_start, silence_start));
segment_start = silence_start;
}
}
if !in_silence && segment_start < n_frames {
segments.push((segment_start, n_frames));
}
let total_len: usize = segments.iter().map(|(s, e)| e - s).sum();
let mut result = ndarray::Array2::<T>::zeros((n_channels, total_len));
let mut offset = 0usize;
for (s, e) in segments {
let len = e - s;
result
.slice_mut(s![.., offset..offset + len])
.assign(&arr.slice(s![.., s..e]));
offset += len;
}
AudioSamples::new_multi_channel(result, self.sample_rate())
}
}
}
fn concatenate_owned<'b>(
segments: NonEmptyVec<AudioSamples<'_, T>>,
) -> AudioSampleResult<AudioSamples<'b, T>>
where
Self: Sized,
{
let first_sample_rate = segments[0].sample_rate();
let first_num_channels = segments[0].num_channels();
for segment in segments.iter().skip(1) {
if segment.sample_rate() != first_sample_rate {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"All segments must have the same sample rate",
)));
}
if segment.num_channels() != first_num_channels {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
"All segments must have the same number of channels",
)));
}
}
let first_sample_rate = segments[0].sample_rate();
let first_is_mono = segments[0].is_mono();
if first_is_mono {
let mut all_samples = Vec::new();
for segment in &segments {
let owned_segment = segment.clone().into_owned();
let segment = owned_segment.as_mono().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"input",
"Expected mono audio data",
))
})?;
all_samples.extend_from_slice(segment.as_slice().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"input",
"Mono samples must be contiguous",
))
})?);
}
let concatenated = Array1::from_vec(all_samples);
AudioSamples::new_mono(concatenated, first_sample_rate)
} else {
let num_channels = segments[0].num_channels();
let mut total_samples = 0;
for segment in &segments {
total_samples += segment.samples_per_channel().get();
}
let mut concatenated_data: Vec<T> =
Vec::with_capacity(num_channels.get() as usize * total_samples);
for channel_idx in 0..num_channels.get() as usize {
for segment in &segments {
let owned_segment = segment.clone().into_owned();
let segment_multi = owned_segment.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"input",
"Expected multi-channel audio data",
))
})?;
let channel_data = segment_multi.row(channel_idx);
concatenated_data.extend_from_slice(channel_data.as_slice().ok_or_else(
|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"input",
"Multi-channel samples must be contiguous",
))
},
)?);
}
}
let concatenated = Array2::from_shape_vec(
(num_channels.get() as usize, total_samples),
concatenated_data,
)
.map_err(|e| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"parameter",
format!("Array shape error: {e}"),
))
})?;
AudioSamples::new_multi_channel(concatenated, first_sample_rate)
}
}
}
#[cfg(all(feature = "random-generation", feature = "iir-filtering"))]
fn apply_perturbation_with_rng<T, R>(
audio: &mut AudioSamples<T>,
method: &PerturbationMethod,
rng: &mut R,
) -> AudioSampleResult<()>
where
R: Rng,
T: StandardSample,
{
match method {
PerturbationMethod::GaussianNoise {
target_snr_db,
noise_color,
} => apply_gaussian_noise_(audio, *target_snr_db, *noise_color, rng),
PerturbationMethod::RandomGain {
min_gain_db,
max_gain_db,
} => {
let _: () = apply_random_gain_(audio, *min_gain_db, *max_gain_db, rng);
Ok(())
}
PerturbationMethod::HighPassFilter {
cutoff_hz,
slope_db_per_octave,
} => {
use crate::operations::{AudioIirFiltering, types::IirFilterDesign};
let order = slope_db_per_octave.as_ref().map_or_else(
|| nzu!(2),
|slope| unsafe {
NonZeroUsize::new_unchecked(((*slope / 6.02).ceil() as usize).max(1))
},
);
let butterworth_filter = IirFilterDesign::butterworth_highpass(order, *cutoff_hz);
audio.apply_iir_filter(&butterworth_filter)
}
PerturbationMethod::LowPassFilter {
cutoff_hz,
slope_db_per_octave,
} => {
use crate::operations::{AudioIirFiltering, types::IirFilterDesign};
let order = slope_db_per_octave.as_ref().map_or_else(
|| nzu!(2),
|slope| unsafe {
NonZeroUsize::new_unchecked(((*slope / 6.02).ceil() as usize).max(1))
},
);
let butterworth_filter = IirFilterDesign::butterworth_lowpass(order, *cutoff_hz);
audio.apply_iir_filter(&butterworth_filter)
}
#[cfg(all(feature = "transforms", feature = "channels"))]
PerturbationMethod::PitchShift {
semitones,
preserve_formants,
} => apply_pitch_shift_(audio, *semitones, *preserve_formants),
#[cfg(not(all(feature = "transforms", feature = "channels")))]
PerturbationMethod::PitchShift { .. } => Err(crate::AudioSampleError::unsupported(
"PitchShift requires the `transforms` and `channels` features",
)),
}
}
#[cfg(feature = "random-generation")]
pub fn apply_gaussian_noise_<T, R>(
audio: &mut AudioSamples<T>,
target_snr_db: f64,
noise_color: NoiseColor,
rng: &mut R,
) -> AudioSampleResult<()>
where
R: Rng,
T: StandardSample,
{
let signal_rms: f64 = audio.rms();
let target_noise_rms = signal_rms / 10.0f64.powf(target_snr_db / 20.0);
let duration = audio.duration_seconds();
let duration: Duration = Duration::from_secs_f64(duration);
let noise_audio: AudioSamples<T> = match noise_color {
NoiseColor::White => {
let mut noise = white_noise(duration, audio.sample_rate, target_noise_rms, None);
apply_custom_noise_to_audio(&mut noise, target_noise_rms, rng);
noise
}
NoiseColor::Pink => {
let mut noise = pink_noise(duration, audio.sample_rate, target_noise_rms, None);
apply_custom_noise_to_audio(&mut noise, target_noise_rms, rng);
noise
}
NoiseColor::Brown => match &audio.data {
AudioData::Mono(_) => {
brown_noise(duration, audio.sample_rate, 0.02, target_noise_rms, None)
}
AudioData::Multi(arr) => {
let mut noise_arrays = Vec::new();
for _ in 0..arr.nrows().get() {
let noise =
brown_noise(duration, audio.sample_rate, 0.02, target_noise_rms, None);
noise_arrays.push(noise);
}
let noise_arrays = unsafe { NonEmptyVec::new_unchecked(noise_arrays) };
AudioSamples::stack(&noise_arrays)?
}
},
};
match (&mut audio.data, &noise_audio.data) {
(AudioData::Mono(signal), AudioData::Mono(noise)) => {
for (s, n) in signal.iter_mut().zip(noise.iter()) {
*s += *n;
}
}
(AudioData::Multi(signal), AudioData::Multi(noise)) => {
for (s, n) in signal.iter_mut().zip(noise.iter()) {
*s += *n;
}
}
_ => {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"input",
"Channel mismatch between signal and noise",
)));
}
}
Ok(())
}
#[cfg(feature = "random-generation")]
fn apply_custom_noise_to_audio<T, R>(noise: &mut AudioSamples<T>, amplitude: f64, rng: &mut R)
where
R: Rng,
T: StandardSample,
{
use rand::RngExt;
match &mut noise.data {
AudioData::Mono(arr) => {
for sample in arr.iter_mut() {
let random_value: f64 = (rng.random_range(0.0..1.0) - 0.5) * 2.0;
let random_value: f64 = random_value;
*sample = (amplitude * random_value).convert_to();
}
}
AudioData::Multi(arr) => {
for sample in arr.iter_mut() {
let random_value = (rng.random_range(0.0..1.0) - 0.5) * 2.0;
let random_value: f64 = random_value;
*sample = (amplitude * random_value).convert_to();
}
}
}
}
#[cfg(feature = "random-generation")]
pub fn apply_random_gain_<T, R>(
audio: &mut AudioSamples<T>,
min_gain_db: f64,
max_gain_db: f64,
rng: &mut R,
) where
T: StandardSample,
R: Rng,
{
use rand::RngExt;
let gain_db = rng.random_range(min_gain_db..=max_gain_db);
let gain_linear: f64 = 10.0f64.powf(gain_db / 20.0);
match &mut audio.data {
AudioData::Mono(arr) => {
for sample in arr.iter_mut() {
let sample_f: f64 = (*sample).convert_to();
*sample = (sample_f * gain_linear).convert_to();
}
}
AudioData::Multi(arr) => {
for sample in arr.iter_mut() {
let sample_f: f64 = (*sample).convert_to();
*sample = (sample_f * gain_linear).convert_to();
}
}
}
}
#[cfg(all(feature = "transforms", feature = "channels"))]
pub fn apply_pitch_shift_<T>(
audio: &mut AudioSamples<T>,
semitones: f64,
preserve_formants: bool,
) -> AudioSampleResult<()>
where
T: StandardSample,
{
use crate::operations::traits::AudioChannelOps;
use spectrograms::WindowType;
if semitones.abs() < 1e-6 {
return Ok(()); }
let pitch_ratio = (semitones / 12.0).exp2();
let n_fft = nzu!(2048);
let hop_size = nzu!(512);
let window = WindowType::Hanning;
let audio_owned = audio.clone().into_owned();
let shifted: AudioSamples<'static, T> = match &audio_owned.data {
AudioData::Mono(_) => {
phase_vocoder_pitch_shift(
&audio_owned,
pitch_ratio,
n_fft,
hop_size,
window,
preserve_formants,
)?
}
AudioData::Multi(_) => {
let num_channels = audio_owned.num_channels().get() as usize;
let mut shifted_channels = Vec::with_capacity(num_channels);
for ch_idx in 0..num_channels {
let channel_audio = audio_owned.extract_channel(ch_idx as u32)?;
let shifted_channel = phase_vocoder_pitch_shift(
&channel_audio,
pitch_ratio,
n_fft,
hop_size,
window.clone(),
preserve_formants,
)?;
shifted_channels.push(shifted_channel);
}
let min_len = shifted_channels
.iter()
.map(|ch| ch.samples_per_channel().get())
.min()
.unwrap_or(0);
if min_len == 0 {
return Err(AudioSampleError::Processing(
"Pitch-shifted channels have zero length".into(),
));
}
let trimmed_channels: Vec<_> = shifted_channels
.into_iter()
.map(|ch| ch.trim(0.0, min_len as f64 / f64::from(ch.sample_rate().get())))
.collect::<Result<Vec<_>, _>>()?;
let non_empty_channels = NonEmptyVec::new(trimmed_channels).map_err(|_| {
AudioSampleError::Processing("Failed to create non-empty channel vector".into())
})?;
AudioSamples::stack(&non_empty_channels)?
}
};
match shifted.data {
AudioData::Mono(arr) => {
let vec_data: Vec<T> = arr
.as_slice()
.ok_or_else(|| {
AudioSampleError::Processing("Failed to get slice from mono data".into())
})?
.to_vec();
let non_empty_vec = NonEmptyVec::new(vec_data).map_err(|_| {
AudioSampleError::Processing(
"Failed to create non-empty vector from shifted data".into(),
)
})?;
audio.replace_with_vec(&non_empty_vec)?;
}
AudioData::Multi(arr) => {
let num_channels = arr.shape()[0];
let samples_per_channel = arr.shape()[1];
let mut interleaved = Vec::with_capacity(num_channels * samples_per_channel);
for sample_idx in 0..samples_per_channel {
for ch_idx in 0..num_channels {
interleaved.push(arr[[ch_idx, sample_idx]]);
}
}
let non_empty_interleaved = NonEmptyVec::new(interleaved).map_err(|_| {
AudioSampleError::Processing("Failed to create non-empty interleaved vector".into())
})?;
let expected_channels =
std::num::NonZeroU32::new(num_channels as u32).ok_or_else(|| {
AudioSampleError::Processing("Invalid channel count (zero)".into())
})?;
audio.replace_with_vec_channels(&non_empty_interleaved, expected_channels)?;
}
}
Ok(())
}
#[cfg(all(feature = "transforms", feature = "channels"))]
fn extract_spectral_envelope(frame: &[num_complex::Complex<f64>], num_bins: usize) -> Vec<f64> {
let magnitudes: Vec<f64> = frame.iter().map(|c| c.norm()).collect();
let window_size = (num_bins / 50).clamp(10, 40);
let mut envelope = vec![0.0; num_bins];
for (i, env_val) in envelope.iter_mut().enumerate().take(num_bins) {
let start = i.saturating_sub(window_size / 2);
let end = (i + window_size / 2 + 1).min(num_bins);
let sum: f64 = magnitudes[start..end].iter().sum();
let count = (end - start) as f64;
*env_val = sum / count;
}
for val in &mut envelope {
*val = val.max(1e-10);
}
envelope
}
#[cfg(all(feature = "transforms", feature = "channels"))]
fn phase_vocoder_pitch_shift<T>(
audio: &AudioSamples<T>,
pitch_ratio: f64,
n_fft: NonZeroUsize,
hop_size: NonZeroUsize,
window: spectrograms::WindowType,
preserve_formants: bool,
) -> AudioSampleResult<AudioSamples<'static, T>>
where
T: StandardSample,
{
use crate::operations::traits::AudioTransforms;
use num_complex::Complex;
use num_traits::FloatConst;
use spectrograms::StftParams;
let params = StftParams::new(n_fft, hop_size, window, true)
.map_err(|e| AudioSampleError::Processing(format!("STFT parameter error: {e}").into()))?;
let mut stft_result = audio.stft(¶ms)?;
let num_bins = stft_result.data.nrows();
let num_frames = stft_result.data.ncols();
let hop_size_f64 = hop_size.get() as f64;
let expected_phase_advance = 2.0 * f64::PI() * hop_size_f64 / n_fft.get() as f64;
let mut output_stft = ndarray::Array2::<Complex<f64>>::zeros((num_bins, num_frames));
let mut prev_phase = vec![0.0_f64; num_bins];
let mut prev_output_phase = vec![0.0_f64; num_bins];
for frame_idx in 0..num_frames {
let envelope = if preserve_formants {
extract_spectral_envelope(&stft_result.data.column(frame_idx).to_vec(), num_bins)
} else {
vec![1.0; num_bins] };
for bin_idx in 0..num_bins {
let complex_val = stft_result.data[[bin_idx, frame_idx]];
let magnitude = complex_val.norm();
let phase = complex_val.arg();
let detail_magnitude = if preserve_formants && envelope[bin_idx] > 1e-10 {
magnitude / envelope[bin_idx]
} else {
magnitude
};
let phase_diff = phase - prev_phase[bin_idx];
let phase_diff_wrapped = ((phase_diff + f64::PI()) % (2.0 * f64::PI())) - f64::PI();
let inst_freq_offset = phase_diff_wrapped / expected_phase_advance;
let true_freq_bin = bin_idx as f64 + inst_freq_offset;
let output_bin_f64 = true_freq_bin * pitch_ratio;
let output_bin = output_bin_f64.round() as usize;
if output_bin < num_bins {
let phase_advance = expected_phase_advance * output_bin_f64;
let output_phase = prev_output_phase[output_bin] + phase_advance;
let output_magnitude = if preserve_formants {
let envelope_idx = output_bin.min(num_bins - 1);
detail_magnitude * envelope[envelope_idx]
} else {
detail_magnitude
};
output_stft[[output_bin, frame_idx]] +=
Complex::from_polar(output_magnitude, output_phase);
prev_output_phase[output_bin] = output_phase;
}
prev_phase[bin_idx] = phase;
}
let nyquist_idx = num_bins - 1;
let nyquist_val = output_stft[[nyquist_idx, frame_idx]];
output_stft[[nyquist_idx, frame_idx]] = Complex::new(nyquist_val.re, 0.0);
let dc_val = output_stft[[0, frame_idx]];
output_stft[[0, frame_idx]] = Complex::new(dc_val.re, 0.0);
}
stft_result.data = output_stft;
AudioSamples::<T>::istft(stft_result)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::AudioSamples;
use crate::sample_rate;
use ndarray::Array1;
#[test]
fn test_reverse_mono_audio() {
let samples = Array1::from(vec![1.0f32, 2.0, 3.0, 4.0, 5.0]);
let audio = AudioSamples::new_mono(samples.into(), sample_rate!(44100)).unwrap();
let reversed = audio.reverse();
if let AudioData::Mono(arr) = &reversed.data {
let expected = vec![5.0, 4.0, 3.0, 2.0, 1.0];
let actual: Vec<f32> = arr.iter().cloned().collect();
assert_eq!(actual, expected);
} else {
panic!("Expected mono data");
}
}
#[test]
fn test_trim_with_time_bounds() {
let samples = Array1::from(vec![1.0f32; 44100]); let audio = AudioSamples::new_mono(samples.into(), sample_rate!(44100)).unwrap();
let trimmed = audio.trim(0.25, 0.75).unwrap();
assert_eq!(trimmed.samples_per_channel().get(), 22050);
}
#[test]
fn test_pad_with_silence() {
let samples = Array1::from(vec![1.0f32; 1000]);
let audio = AudioSamples::new_mono(samples.into(), sample_rate!(44100)).unwrap();
let padded = audio.pad(0.1, 0.1, 0.0).unwrap();
assert_eq!(padded.samples_per_channel().get(), 1000 + 4410 + 4410);
if let AudioData::Mono(arr) = &padded.data {
assert_eq!(arr[0], 0.0);
assert_eq!(arr[arr.len().get() - 1], 0.0);
assert_eq!(arr[4410], 1.0);
}
}
#[test]
fn test_split_into_segments() {
let samples = Array1::from(vec![1.0f32; 8820]); let audio = AudioSamples::new_mono(samples.into(), sample_rate!(44100)).unwrap();
let segments = audio.split(0.05).unwrap();
assert_eq!(segments.len(), 4); assert_eq!(
segments[0].samples_per_channel(),
NonZeroUsize::new(2205).unwrap()
); }
#[test]
fn test_concatenate_segments() {
let samples1 = Array1::from(vec![1.0f32; 1000]);
let samples2 = Array1::from(vec![2.0f32; 1000]);
let audio1 = AudioSamples::new_mono(samples1.into(), sample_rate!(44100)).unwrap();
let audio2 = AudioSamples::new_mono(samples2.into(), sample_rate!(44100)).unwrap();
let audio = vec![audio1, audio2];
let audio = NonEmptyVec::new(audio).unwrap();
let concatenated = AudioSamples::concatenate(&audio).unwrap();
assert_eq!(concatenated.samples_per_channel().get(), 2000);
if let AudioData::Mono(arr) = &concatenated.data {
assert_eq!(arr[500], 1.0); assert_eq!(arr[1500], 2.0); }
}
#[test]
fn test_mix_two_sources() {
let samples1 = Array1::from(vec![1.0f32; 1000]);
let samples2 = Array1::from(vec![2.0f32; 1000]);
let audio1 = AudioSamples::new_mono(samples1.into(), sample_rate!(44100)).unwrap();
let audio2 = AudioSamples::new_mono(samples2.into(), sample_rate!(44100)).unwrap();
let v = NonEmptyVec::new(vec![audio1, audio2]).unwrap();
let mixed = AudioSamples::mix(&v, None).unwrap();
if let AudioData::Mono(arr) = &mixed.data {
assert!((arr[0] - 1.5).abs() < 1e-6);
}
}
#[test]
fn test_fade_operations() {
let samples = Array1::from(vec![1.0f32; 1000]);
let mut audio = AudioSamples::new_mono(samples.into(), sample_rate!(44100)).unwrap();
audio.fade_in(0.01, FadeCurve::Linear).unwrap();
if let AudioData::Mono(arr) = &audio.data {
assert_eq!(arr[0], 0.0); assert!(arr[220] > 0.0 && arr[220] < 1.0); assert!(arr[440] > 0.99); }
let samples = Array1::from(vec![1.0f32; 1000]);
let mut audio = AudioSamples::new_mono(samples.into(), sample_rate!(44100)).unwrap();
audio.fade_out(0.01, FadeCurve::Linear).unwrap();
if let AudioData::Mono(arr) = &audio.data {
assert!(arr[arr.len().get() - 1] < 0.01); let fade_start = arr.len().get() - 441;
assert!(arr[fade_start + 220] > 0.0 && arr[fade_start + 220] < 1.0);
}
}
#[test]
fn test_repeat_audio() {
let samples = Array1::from(vec![1.0f32, 2.0]);
let audio = AudioSamples::new_mono(samples.into(), sample_rate!(44100)).unwrap();
let repeated = audio.repeat(3).unwrap();
assert_eq!(
repeated.samples_per_channel(),
NonZeroUsize::new(6).unwrap()
);
if let AudioData::Mono(arr) = &repeated.data {
let expected = vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0];
assert_eq!(arr.as_slice().unwrap(), &expected);
}
}
#[test]
fn test_trim_silence() {
let mut samples = vec![0.0f32; 1000];
for i in 400..600 {
samples[i] = 1.0;
}
let audio =
AudioSamples::new_mono(Array1::from(samples).into(), sample_rate!(44100)).unwrap();
let trimmed = audio.trim_silence(-10.0).unwrap();
assert_eq!(
trimmed.samples_per_channel(),
NonZeroUsize::new(200).unwrap()
);
if let AudioData::Mono(arr) = &trimmed.data {
assert!(arr.iter().all(|&x| x == 1.0));
}
}
#[test]
fn test_multi_source_mixing_with_weights() {
let samples1 = Array1::from(vec![1.0f32; 100]);
let samples2 = Array1::from(vec![2.0f32; 100]);
let samples3 = Array1::from(vec![3.0f32; 100]);
let audio1 = AudioSamples::new_mono(samples1.into(), sample_rate!(44100)).unwrap();
let audio2 = AudioSamples::new_mono(samples2.into(), sample_rate!(44100)).unwrap();
let audio3 = AudioSamples::new_mono(samples3.into(), sample_rate!(44100)).unwrap();
let weights = NonEmptyVec::new(vec![0.5, 0.3, 0.2]).unwrap();
let v = NonEmptyVec::new(vec![audio1, audio2, audio3]).unwrap();
let mixed = AudioSamples::mix(&v, Some(&weights)).unwrap();
if let AudioData::Mono(arr) = &mixed.data {
assert!((arr[0] - 1.7).abs() < 1e-6);
}
}
#[cfg(all(feature = "random-generation", feature = "iir-filtering"))]
#[test]
fn test_perturbation_gaussian_noise() {
use crate::operations::types::*;
let samples = Array1::from(vec![1.0f32; 1000]);
let audio = AudioSamples::new_mono(samples.into(), sample_rate!(44100)).unwrap();
let config = PerturbationConfig::with_seed(
PerturbationMethod::gaussian_noise(20.0, NoiseColor::White),
12345,
);
let noisy_audio = audio.perturb(&config).unwrap();
if let (AudioData::Mono(original), AudioData::Mono(noisy)) =
(&audio.data, &noisy_audio.data)
{
assert_ne!(original[0], noisy[0]);
assert_eq!(original.len(), noisy.len());
}
}
#[cfg(all(feature = "random-generation", feature = "iir-filtering"))]
#[test]
fn test_perturbation_random_gain() {
use crate::operations::types::*;
let samples = Array1::from(vec![1.0f32; 100]);
let mut audio = AudioSamples::new_mono(samples.into(), sample_rate!(44100)).unwrap();
let config =
PerturbationConfig::with_seed(PerturbationMethod::random_gain(-3.0, 3.0), 54321);
let original_sample = if let AudioData::Mono(arr) = &audio.data {
arr[0]
} else {
panic!("Expected mono audio");
};
audio.perturb_in_place(&config).unwrap();
let gained_sample = if let AudioData::Mono(arr) = &audio.data {
arr[0]
} else {
panic!("Expected mono audio");
};
assert_ne!(original_sample, gained_sample);
}
#[cfg(all(feature = "random-generation", feature = "iir-filtering"))]
#[test]
fn test_perturbation_high_pass_filter() {
use crate::operations::types::*;
let samples = Array1::from(vec![1.0f32; 100]);
let mut audio = AudioSamples::new_mono(samples.into(), sample_rate!(44100)).unwrap();
let config = PerturbationConfig::new(PerturbationMethod::high_pass_filter(80.0));
audio.perturb_in_place(&config).unwrap();
if let AudioData::Mono(arr) = &audio.data {
assert_eq!(arr.len(), NonZeroUsize::new(100).unwrap());
}
}
#[cfg(all(feature = "random-generation", feature = "iir-filtering"))]
#[test]
fn test_perturbation_deterministic() {
let samples = Array1::from(vec![1.0f32; 100]);
let audio = AudioSamples::new_mono(samples.into(), sample_rate!(44100)).unwrap();
let config = PerturbationConfig::with_seed(PerturbationMethod::random_gain(-1.0, 1.0), 42);
let result1 = audio.perturb(&config).unwrap();
let result2 = audio.perturb(&config).unwrap();
if let (AudioData::Mono(arr1), AudioData::Mono(arr2)) = (&result1.data, &result2.data) {
for (a, b) in arr1.iter().zip(arr2.iter()) {
assert_eq!(a, b);
}
}
}
#[cfg(all(feature = "random-generation", feature = "iir-filtering"))]
#[test]
fn test_perturbation_validation() {
let samples = Array1::from(vec![1.0f32; 100]);
let mut audio = AudioSamples::new_mono(samples.into(), sample_rate!(44100)).unwrap();
let invalid_config = PerturbationConfig::new(
PerturbationMethod::high_pass_filter(50000.0), );
let result = audio.perturb_in_place(&invalid_config);
assert!(result.is_err());
}
#[cfg(all(feature = "transforms", feature = "channels"))]
#[test]
fn test_pitch_shift_up_mono() {
use crate::sine_wave;
use std::time::Duration;
let audio = sine_wave::<f32>(440.0, Duration::from_millis(200), sample_rate!(44100), 0.5);
let original_len = audio.samples_per_channel().get();
let mut shifted = audio.clone();
let result = super::apply_pitch_shift_(&mut shifted, 12.0, false);
assert!(
result.is_ok(),
"Pitch shift up should succeed: {:?}",
result.err()
);
let len_diff = (shifted.samples_per_channel().get() as isize - original_len as isize).abs();
assert!(
len_diff < 200,
"Duration should be approximately preserved (diff: {} samples)",
len_diff
);
}
#[cfg(all(feature = "transforms", feature = "channels"))]
#[test]
fn test_pitch_shift_down_mono() {
use crate::sine_wave;
use std::time::Duration;
let audio = sine_wave::<f32>(880.0, Duration::from_millis(200), sample_rate!(44100), 0.5);
let original_len = audio.samples_per_channel().get();
let mut shifted = audio.clone();
let result = super::apply_pitch_shift_(&mut shifted, -12.0, false);
assert!(result.is_ok(), "Pitch shift down should succeed");
let len_diff = (shifted.samples_per_channel().get() as isize - original_len as isize).abs();
assert!(
len_diff < 200,
"Duration should be approximately preserved (diff: {} samples)",
len_diff
);
}
#[cfg(all(feature = "transforms", feature = "channels"))]
#[test]
fn test_pitch_shift_no_change() {
use crate::sine_wave;
use std::time::Duration;
let audio = sine_wave::<f32>(440.0, Duration::from_millis(100), sample_rate!(44100), 0.5);
let original_data: Vec<f32> = audio.as_slice().unwrap().to_vec();
let mut shifted = audio.clone();
let result = super::apply_pitch_shift_(&mut shifted, 0.0, false);
assert!(result.is_ok(), "Zero semitone shift should succeed");
let shifted_data: Vec<f32> = shifted.as_slice().unwrap().to_vec();
assert_eq!(
shifted_data, original_data,
"Zero shift should not modify audio"
);
}
#[cfg(all(feature = "transforms", feature = "channels"))]
#[test]
#[ignore] fn test_pitch_shift_multi_channel() {
use crate::sine_wave;
use non_empty_slice::NonEmptyVec;
use std::time::Duration;
let left_data =
sine_wave::<f32>(440.0, Duration::from_millis(200), sample_rate!(44100), 0.5);
let right_data =
sine_wave::<f32>(440.0, Duration::from_millis(200), sample_rate!(44100), 0.5);
let left_vec = NonEmptyVec::new(left_data.as_slice().unwrap().to_vec()).unwrap();
let right_vec = NonEmptyVec::new(right_data.as_slice().unwrap().to_vec()).unwrap();
let channels = NonEmptyVec::new(vec![left_vec, right_vec]).unwrap();
let audio: super::AudioSamples<'static, f32> =
super::AudioSamples::from_channels(channels, sample_rate!(44100)).unwrap();
assert_eq!(audio.num_channels().get(), 2);
let original_len = audio.samples_per_channel().get();
let mut shifted = audio.clone();
let result = super::apply_pitch_shift_(&mut shifted, 5.0, false);
assert!(
result.is_ok(),
"Multi-channel pitch shift should succeed: {:?}",
result.err()
);
assert_eq!(
shifted.num_channels().get(),
2,
"Channel count should be preserved"
);
let len_diff = (shifted.samples_per_channel().get() as isize - original_len as isize).abs();
assert!(
len_diff < 200,
"Duration should be approximately preserved (diff: {} samples)",
len_diff
);
}
#[cfg(all(feature = "transforms", feature = "channels"))]
#[test]
fn test_pitch_shift_small_shift() {
use crate::sine_wave;
use std::time::Duration;
let audio = sine_wave::<f32>(440.0, Duration::from_millis(150), sample_rate!(44100), 0.5);
let mut shifted = audio.clone();
let result = super::apply_pitch_shift_(&mut shifted, 0.5, false);
assert!(result.is_ok(), "Small pitch shift should succeed");
}
#[cfg(all(feature = "transforms", feature = "channels"))]
#[test]
fn test_pitch_shift_large_shift() {
use crate::sine_wave;
use std::time::Duration;
let audio = sine_wave::<f32>(440.0, Duration::from_millis(150), sample_rate!(44100), 0.5);
let mut shifted = audio.clone();
let result = super::apply_pitch_shift_(&mut shifted, 24.0, false);
assert!(
result.is_ok(),
"Large pitch shift should succeed even if quality degrades"
);
}
#[cfg(all(feature = "transforms", feature = "channels"))]
#[test]
fn test_pitch_shift_preserves_sample_rate() {
use crate::sine_wave;
use std::time::Duration;
let audio = sine_wave::<f32>(440.0, Duration::from_millis(100), sample_rate!(48000), 0.5);
let original_sr = audio.sample_rate();
let mut shifted = audio.clone();
let result = super::apply_pitch_shift_(&mut shifted, 7.0, false);
assert!(result.is_ok());
assert_eq!(
shifted.sample_rate(),
original_sr,
"Sample rate should be preserved"
);
}
#[cfg(all(feature = "transforms", feature = "channels"))]
#[test]
fn test_pitch_shift_short_audio() {
use crate::sine_wave;
use std::time::Duration;
let audio = sine_wave::<f32>(440.0, Duration::from_millis(50), sample_rate!(44100), 0.5);
let mut shifted = audio.clone();
let result = super::apply_pitch_shift_(&mut shifted, 3.0, false);
assert!(result.is_ok(), "Pitch shift should handle short audio");
}
#[cfg(all(feature = "transforms", feature = "channels"))]
#[test]
fn test_pitch_shift_with_formant_preservation() {
use crate::sine_wave;
use std::time::Duration;
let audio = sine_wave::<f32>(440.0, Duration::from_millis(200), sample_rate!(44100), 0.5);
let original_len = audio.samples_per_channel().get();
let mut shifted = audio.clone();
let result = super::apply_pitch_shift_(&mut shifted, 12.0, true);
assert!(
result.is_ok(),
"Pitch shift with formant preservation should succeed"
);
let len_diff = (shifted.samples_per_channel().get() as isize - original_len as isize).abs();
assert!(
len_diff < 200,
"Duration should be approximately preserved (diff: {} samples)",
len_diff
);
}
}