use crate::{
AudioSampleError, AudioSampleResult, AudioSamples, AudioTypeConversion, LayoutError,
ParameterError, traits::StandardSample,
};
use ndarray::{Array1, ArrayView1};
#[inline]
pub fn correlation<T>(a: &AudioSamples<T>, b: &AudioSamples<T>) -> AudioSampleResult<f64>
where
T: StandardSample,
{
if a.num_channels() != b.num_channels() || a.samples_per_channel() != b.samples_per_channel() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_signals",
"Signals must have the same dimensions for correlation",
)));
}
let a_f = a.as_f64();
let b_f = b.as_f64();
match (a_f.as_mono(), b_f.as_mono()) {
(Some(a_mono), Some(b_mono)) => {
let corr = correlation_1d(&a_mono.view(), &b_mono.view())?;
Ok(corr)
}
(Some(_), None) | (None, Some(_)) => {
Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Signals must have the same channel configuration",
)))
}
(None, None) => {
let a_multi = a_f.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Must be multi-channel audio",
))
})?;
let b_multi = b_f.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Must be multi-channel audio",
))
})?;
let mut correlations = Vec::new();
for i in 0..a_multi.nrows().get() {
let a_channel = a_multi.row(i);
let b_channel = b_multi.row(i);
let corr = correlation_1d_slice(
a_channel.as_slice().ok_or_else(|| {
AudioSampleError::Layout(LayoutError::NonContiguous {
operation: "signal processing".to_string(),
layout_type: "non-contiguous multi-channel samples".to_string(),
})
})?,
b_channel.as_slice().ok_or_else(|| {
AudioSampleError::Layout(LayoutError::NonContiguous {
operation: "signal processing".to_string(),
layout_type: "non-contiguous multi-channel samples".to_string(),
})
})?,
)?;
correlations.push(corr);
}
Ok(correlations.iter().fold(0.0, |acc, x| acc + *x) / correlations.len() as f64)
}
}
}
#[inline]
pub fn mse<T>(a: &AudioSamples<T>, b: &AudioSamples<T>) -> AudioSampleResult<f64>
where
T: StandardSample,
{
if a.num_channels() != b.num_channels() || a.samples_per_channel() != b.samples_per_channel() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_signals",
"Signals must have the same dimensions for MSE",
)));
}
let a_f = a.as_f64();
let b_f = b.as_f64();
match (a_f.as_mono(), b_f.as_mono()) {
(Some(a_mono), Some(b_mono)) => mse_1d(&a_mono.view(), &b_mono.view()),
(Some(_), None) | (None, Some(_)) => {
Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Signals must have the same channel configuration",
)))
}
(None, None) => {
let a_multi = a_f.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Must be multi-channel audio",
))
})?;
let b_multi = b_f.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Must be multi-channel audio",
))
})?;
let mut mses = Vec::new();
for i in 0..a_multi.nrows().get() {
let a_channel = a_multi.row(i);
let b_channel = b_multi.row(i);
let mse = mse_1d_slice(
a_channel.as_slice().ok_or_else(|| {
AudioSampleError::Layout(LayoutError::NonContiguous {
operation: "signal processing".to_string(),
layout_type: "non-contiguous multi-channel samples".to_string(),
})
})?,
b_channel.as_slice().ok_or_else(|| {
AudioSampleError::Layout(LayoutError::NonContiguous {
operation: "signal processing".to_string(),
layout_type: "non-contiguous multi-channel samples".to_string(),
})
})?,
)?;
mses.push(mse);
}
Ok(mses.iter().fold(0.0, |acc, x| acc + *x) / mses.len() as f64)
}
}
}
#[inline]
pub fn snr<T>(signal: &AudioSamples<T>, noise: &AudioSamples<T>) -> AudioSampleResult<f64>
where
T: StandardSample,
{
if signal.num_channels() != noise.num_channels()
|| signal.samples_per_channel() != noise.samples_per_channel()
{
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_signals",
"Signal and noise must have the same dimensions for SNR",
)));
}
let signal_f = signal.as_f64();
let noise_f = noise.as_f64();
let signal_power = if let Some(mono) = signal_f.as_mono() {
mono.iter().map(|&x| x * x).fold(0.0, |acc, x| acc + x) / mono.len().get() as f64
} else {
let multi = signal_f.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Must be multi-channel audio",
))
})?;
multi.iter().map(|&x| x * x).fold(0.0, |acc, x| acc + x) / multi.len().get() as f64
};
let noise_power = if let Some(mono) = noise_f.as_mono() {
mono.iter().map(|x| *x * *x).fold(0.0, |acc, x| acc + x) / mono.len().get() as f64
} else {
let multi = noise_f.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Must be multi-channel audio",
))
})?;
multi.iter().map(|&x| x * x).fold(0.0, |acc, x| acc + x) / multi.len().get() as f64
};
if noise_power == 0.0 {
return Ok(f64::INFINITY);
}
let snr_db = 10.0 * (signal_power / noise_power).log10();
Ok(snr_db)
}
#[inline]
pub fn psnr<T>(reference: &AudioSamples<T>, test: &AudioSamples<T>) -> AudioSampleResult<f64>
where
T: StandardSample,
{
if reference.num_channels() != test.num_channels()
|| reference.samples_per_channel() != test.samples_per_channel()
{
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_signals",
"Signals must have the same dimensions for PSNR",
)));
}
let error = mse(reference, test)?;
if error == 0.0 {
return Ok(f64::INFINITY);
}
let ref_f = reference.as_f64();
let peak = if let Some(mono) = ref_f.as_mono() {
mono.iter().fold(0.0_f64, |acc, &x| acc.max(x.abs()))
} else {
let multi = ref_f.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Must be multi-channel audio",
))
})?;
multi.iter().fold(0.0_f64, |acc, &x| acc.max(x.abs()))
};
Ok(20.0 * (peak / error.sqrt()).log10())
}
#[inline]
pub fn segmental_snr<T>(
signal: &AudioSamples<T>,
noise: &AudioSamples<T>,
segment_len: core::num::NonZeroUsize,
) -> AudioSampleResult<f64>
where
T: StandardSample,
{
const SEG_SNR_MIN_DB: f64 = -10.0;
const SEG_SNR_MAX_DB: f64 = 35.0;
if signal.num_channels() != noise.num_channels()
|| signal.samples_per_channel() != noise.samples_per_channel()
{
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_signals",
"Signal and noise must have the same dimensions for segmental SNR",
)));
}
let signal_f = signal.as_f64();
let noise_f = noise.as_f64();
let (sig_sq, noise_sq): (Vec<f64>, Vec<f64>) = match (signal_f.as_mono(), noise_f.as_mono()) {
(Some(s), Some(n)) => (
s.iter().map(|&x| x * x).collect(),
n.iter().map(|&x| x * x).collect(),
),
(Some(_), None) | (None, Some(_)) => {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Signals must have the same channel configuration",
)));
}
(None, None) => {
let s_multi = signal_f.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Must be multi-channel audio",
))
})?;
let n_multi = noise_f.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Must be multi-channel audio",
))
})?;
let sig_sq: Vec<f64> = (0..s_multi.ncols().get())
.map(|i| s_multi.column(i).iter().map(|&x| x * x).sum())
.collect();
let noise_sq: Vec<f64> = (0..n_multi.ncols().get())
.map(|i| n_multi.column(i).iter().map(|&x| x * x).sum())
.collect();
(sig_sq, noise_sq)
}
};
let seg = segment_len.get();
let mut acc = 0.0;
let mut count = 0usize;
for (s_chunk, n_chunk) in sig_sq.chunks(seg).zip(noise_sq.chunks(seg)) {
let sig_energy: f64 = s_chunk.iter().sum();
let noise_energy: f64 = n_chunk.iter().sum();
let snr_db = if noise_energy == 0.0 {
if sig_energy == 0.0 {
SEG_SNR_MIN_DB
} else {
SEG_SNR_MAX_DB
}
} else if sig_energy == 0.0 {
SEG_SNR_MIN_DB
} else {
10.0 * (sig_energy / noise_energy).log10()
};
acc += snr_db.clamp(SEG_SNR_MIN_DB, SEG_SNR_MAX_DB);
count += 1;
}
Ok(acc / count as f64)
}
#[inline]
pub fn align_signals<T>(
reference: &AudioSamples<'_, T>,
signal: &AudioSamples<'_, T>,
) -> AudioSampleResult<(AudioSamples<'static, T>, usize)>
where
T: StandardSample,
{
if reference.num_channels() != signal.num_channels() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_channels",
"Signals must have the same number of channels for alignment",
)));
}
let sample_rate = signal.sample_rate();
let ref_f = reference.as_f64();
let sig_f = signal.as_f64();
let (ref_data, sig_data) = match (ref_f.as_mono(), sig_f.as_mono()) {
(Some(ref_mono), Some(sig_mono)) => (ref_mono.to_vec(), sig_mono.to_vec()),
(None, None) => {
let ref_multi = ref_f.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Must be multi-channel audio",
))
})?;
let sig_multi = sig_f.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Must be multi-channel audio",
))
})?;
let ref_avg: Vec<f64> = (0..ref_multi.ncols().get())
.map(|i| {
ref_multi.column(i).iter().fold(0.0, |acc, x| acc + *x)
/ ref_multi.nrows().get() as f64
})
.collect();
let sig_avg: Vec<f64> = (0..sig_multi.ncols().get())
.map(|i| {
sig_multi.column(i).iter().fold(0.0, |acc, x| acc + *x)
/ sig_multi.nrows().get() as f64
})
.collect();
(ref_avg, sig_avg)
}
_ => {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Signals must have the same channel configuration",
)));
}
};
let max_offset = ref_data.len().min(sig_data.len()) / 2;
let mut best_offset = 0;
let mut best_correlation = f64::NEG_INFINITY;
for offset in 0..max_offset {
let correlation = if offset < sig_data.len() {
let end = (ref_data.len() - offset).min(sig_data.len() - offset);
correlation_1d_slice(
&ref_data[offset..offset + end],
&sig_data[offset..offset + end],
)?
} else {
0.0
};
if correlation > best_correlation {
best_correlation = correlation;
best_offset = offset;
}
}
let aligned_signal = if best_offset > 0 {
if let Some(mono) = signal.as_mono() {
let mut aligned_data = vec![T::default(); best_offset];
aligned_data.extend_from_slice(
&mono.as_slice().ok_or_else(|| {
AudioSampleError::Layout(LayoutError::NonContiguous {
operation: "signal alignment".to_string(),
layout_type: "non-contiguous mono samples".to_string(),
})
})?[..mono.len().get() - best_offset],
);
let aligned_array = Array1::from_vec(aligned_data);
AudioSamples::new_mono(aligned_array, sample_rate)?
} else {
let multi = signal.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Must be multi-channel audio",
))
})?;
let mut aligned_data = Vec::new();
for i in 0..multi.nrows().get() {
let mut row = vec![T::default(); best_offset];
row.extend_from_slice(
&multi.row(i).as_slice().ok_or_else(|| {
AudioSampleError::Layout(LayoutError::NonContiguous {
operation: "signal alignment".to_string(),
layout_type: "non-contiguous multi-channel samples".to_string(),
})
})?[..multi.ncols().get() - best_offset],
);
aligned_data.push(row);
}
let aligned_array = ndarray::Array2::from_shape_vec(
(aligned_data.len(), aligned_data[0].len()),
aligned_data.into_iter().flatten().collect(),
)
.map_err(|e| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"array_shape",
format!("Array shape error: {e}"),
))
})?;
AudioSamples::new_multi_channel(aligned_array, sample_rate)?
}
} else {
signal.clone().into_owned()
};
Ok((aligned_signal, best_offset))
}
#[cfg(feature = "transforms")]
#[inline]
pub fn log_spectral_distance<T>(a: &AudioSamples<T>, b: &AudioSamples<T>) -> AudioSampleResult<f64>
where
T: StandardSample,
{
use crate::operations::AudioTransforms;
const EPS: f64 = 1e-10;
if a.num_channels() != b.num_channels() || a.samples_per_channel() != b.samples_per_channel() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_signals",
"Signals must have the same dimensions for log-spectral distance",
)));
}
let n_fft = a.samples_per_channel();
let spec_a = a.fft(n_fft)?;
let spec_b = b.fft(n_fft)?;
let mut sum_sq = 0.0;
let mut bins = 0usize;
for (ca, cb) in spec_a.rows().into_iter().zip(spec_b.rows()) {
for (za, zb) in ca.iter().zip(cb.iter()) {
let la = 10.0 * (za.norm_sqr() + EPS).log10();
let lb = 10.0 * (zb.norm_sqr() + EPS).log10();
let d = la - lb;
sum_sq += d * d;
bins += 1;
}
}
Ok((sum_sq / bins as f64).sqrt())
}
#[inline]
pub fn correlation_per_channel<T>(
a: &AudioSamples<T>,
b: &AudioSamples<T>,
) -> AudioSampleResult<Vec<f64>>
where
T: StandardSample,
{
per_channel(a, b, "correlation", correlation_1d_slice)
}
#[inline]
pub fn mse_per_channel<T>(a: &AudioSamples<T>, b: &AudioSamples<T>) -> AudioSampleResult<Vec<f64>>
where
T: StandardSample,
{
per_channel(a, b, "MSE", mse_1d_slice)
}
#[inline]
pub fn snr_per_channel<T>(
signal: &AudioSamples<T>,
noise: &AudioSamples<T>,
) -> AudioSampleResult<Vec<f64>>
where
T: StandardSample,
{
per_channel(signal, noise, "SNR", |sig, noise| {
let n = sig.len();
let signal_power = sig.iter().map(|&x| x * x).sum::<f64>() / n as f64;
let noise_power = noise.iter().map(|&x| x * x).sum::<f64>() / n as f64;
if noise_power == 0.0 {
Ok(f64::INFINITY)
} else {
Ok(10.0 * (signal_power / noise_power).log10())
}
})
}
fn per_channel<T, F>(
a: &AudioSamples<T>,
b: &AudioSamples<T>,
metric_name: &str,
metric: F,
) -> AudioSampleResult<Vec<f64>>
where
T: StandardSample,
F: Fn(&[f64], &[f64]) -> AudioSampleResult<f64>,
{
if a.num_channels() != b.num_channels() || a.samples_per_channel() != b.samples_per_channel() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_signals",
format!("Signals must have the same dimensions for {metric_name}"),
)));
}
let a_f = a.as_f64();
let b_f = b.as_f64();
match (a_f.as_mono(), b_f.as_mono()) {
(Some(a_mono), Some(b_mono)) => {
let av = a_mono.as_slice().ok_or_else(|| {
AudioSampleError::Layout(LayoutError::NonContiguous {
operation: "signal processing".to_string(),
layout_type: "non-contiguous mono samples".to_string(),
})
})?;
let bv = b_mono.as_slice().ok_or_else(|| {
AudioSampleError::Layout(LayoutError::NonContiguous {
operation: "signal processing".to_string(),
layout_type: "non-contiguous mono samples".to_string(),
})
})?;
Ok(vec![metric(av, bv)?])
}
(Some(_), None) | (None, Some(_)) => {
Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Signals must have the same channel configuration",
)))
}
(None, None) => {
let a_multi = a_f.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Must be multi-channel audio",
))
})?;
let b_multi = b_f.as_multi_channel().ok_or_else(|| {
AudioSampleError::Parameter(ParameterError::invalid_value(
"audio_format",
"Must be multi-channel audio",
))
})?;
let mut results = Vec::with_capacity(a_multi.nrows().get());
for i in 0..a_multi.nrows().get() {
let a_channel = a_multi.row(i);
let b_channel = b_multi.row(i);
results.push(metric(
a_channel.as_slice().ok_or_else(|| {
AudioSampleError::Layout(LayoutError::NonContiguous {
operation: "signal processing".to_string(),
layout_type: "non-contiguous multi-channel samples".to_string(),
})
})?,
b_channel.as_slice().ok_or_else(|| {
AudioSampleError::Layout(LayoutError::NonContiguous {
operation: "signal processing".to_string(),
layout_type: "non-contiguous multi-channel samples".to_string(),
})
})?,
)?);
}
Ok(results)
}
}
}
fn correlation_1d(a: &ArrayView1<f64>, b: &ArrayView1<f64>) -> AudioSampleResult<f64> {
correlation_1d_slice(
a.as_slice().ok_or_else(|| {
AudioSampleError::Layout(LayoutError::NonContiguous {
operation: "correlation calculation".to_string(),
layout_type: "non-contiguous mono samples".to_string(),
})
})?,
b.as_slice().ok_or_else(|| {
AudioSampleError::Layout(LayoutError::NonContiguous {
operation: "correlation calculation".to_string(),
layout_type: "non-contiguous mono samples".to_string(),
})
})?,
)
}
fn correlation_1d_slice(a: &[f64], b: &[f64]) -> AudioSampleResult<f64> {
if a.len() != b.len() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"array_length",
"Arrays must have the same length for correlation",
)));
}
let n = a.len();
let mean_a = a.iter().fold(0.0, |acc, x| acc + *x) / n as f64;
let mean_b = b.iter().fold(0.0, |acc, x| acc + *x) / n as f64;
let mut num = 0.0;
let mut den_a = 0.0;
let mut den_b = 0.0;
for (&x, &y) in a.iter().zip(b.iter()) {
let dx = x - mean_a;
let dy = y - mean_b;
num += dx * dy;
den_a += dx * dx;
den_b += dy * dy;
}
let denominator = (den_a * den_b).sqrt();
if denominator == 0.0 {
Ok(0.0)
} else {
Ok(num / denominator)
}
}
fn mse_1d(a: &ArrayView1<f64>, b: &ArrayView1<f64>) -> AudioSampleResult<f64> {
if a.len() != b.len() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"array_length",
"Arrays must have the same length for MSE",
)));
}
let n = a.len();
let sum_squared_diff: f64 = a
.iter()
.zip(b.iter())
.map(|(&x, &y)| (x - y) * (x - y))
.fold(0.0, |acc, val| acc + val);
Ok(sum_squared_diff / n as f64)
}
fn mse_1d_slice(a: &[f64], b: &[f64]) -> AudioSampleResult<f64> {
if a.len() != b.len() {
return Err(AudioSampleError::Parameter(ParameterError::invalid_value(
"array_length",
"Arrays must have the same length for MSE",
)));
}
let n = a.len();
let sum_squared_diff: f64 = a
.iter()
.zip(b.iter())
.map(|(&x, &y)| (x - y) * (x - y))
.fold(0.0, |acc, val| acc + val)
/ n as f64;
Ok(sum_squared_diff)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sample_rate;
use approx_eq::assert_approx_eq;
use non_empty_slice::non_empty_vec;
#[test]
fn test_correlation_identical_signals() {
let data: non_empty_slice::NonEmptyVec<f64> = non_empty_vec![1.0f64, 2.0, 3.0, 4.0, 5.0];
let audio1: AudioSamples<'static, f64> =
AudioSamples::from_mono_vec::<f64>(data.clone(), sample_rate!(44100));
let audio2: AudioSamples<'static, f64> =
AudioSamples::from_mono_vec::<f64>(data, sample_rate!(44100));
let corr: f64 = correlation(&audio1, &audio2).unwrap();
assert_approx_eq!(corr, 1.0, 1e-10);
}
#[test]
fn test_correlation_opposite_signals() {
let audio1: AudioSamples<'static, f64> = AudioSamples::from_mono_vec::<f64>(
non_empty_vec![1.0f64, 2.0, 3.0, 4.0, 5.0],
sample_rate!(44100),
);
let audio2 = AudioSamples::from_mono_vec::<f64>(
non_empty_vec![-1.0f64, -2.0, -3.0, -4.0, -5.0],
sample_rate!(44100),
);
let corr: f64 = correlation(&audio1, &audio2).unwrap();
assert_approx_eq!(corr, -1.0, 1e-10);
}
#[test]
fn test_mse_identical_signals() {
let audio1: AudioSamples<'static, f64> = AudioSamples::from_mono_vec::<f64>(
non_empty_vec![1.0f64, 2.0, 3.0, 4.0, 5.0],
sample_rate!(44100),
);
let audio2 = AudioSamples::from_mono_vec::<f64>(
non_empty_vec![1.0f64, 2.0, 3.0, 4.0, 5.0],
sample_rate!(44100),
);
let mse_val = mse(&audio1, &audio2).unwrap();
assert_approx_eq!(mse_val, 0.0_f64, 1e-10);
}
#[test]
fn test_snr_calculation() {
let signal: AudioSamples<'static, f64> = AudioSamples::from_mono_vec::<f64>(
non_empty_vec![1.0f64, 2.0, 3.0, 4.0, 5.0],
sample_rate!(44100),
);
let noise = AudioSamples::from_mono_vec::<f64>(
non_empty_vec![0.1f64, 0.2, 0.1, 0.2, 0.1],
sample_rate!(44100),
);
let snr_val: f64 = snr(&signal, &noise).unwrap();
assert!(snr_val > 0.0_f64); }
#[test]
fn test_align_signals_no_offset() {
let data = non_empty_vec![1.0f64, 2.0, 3.0, 4.0, 5.0];
let reference = AudioSamples::from_mono_vec::<f64>(data.clone(), sample_rate!(44100));
let signal = AudioSamples::from_mono_vec::<f64>(data, sample_rate!(44100));
let (aligned, offset) = align_signals::<f64>(&reference, &signal).unwrap();
assert_eq!(offset, 0);
assert_eq!(aligned.samples_per_channel(), signal.samples_per_channel());
}
#[test]
fn test_psnr_identical_signals_is_infinite() {
let data = non_empty_vec![0.1f64, -0.5, 0.7, -0.2, 1.0];
let reference: AudioSamples<'static, f64> =
AudioSamples::from_mono_vec::<f64>(data.clone(), sample_rate!(44100));
let test = AudioSamples::from_mono_vec::<f64>(data, sample_rate!(44100));
let db = psnr(&reference, &test).unwrap();
assert!(db.is_infinite() && db > 0.0);
}
#[test]
fn test_psnr_known_noise() {
let reference: AudioSamples<'static, f64> = AudioSamples::from_mono_vec::<f64>(
non_empty_vec![1.0f64, 0.5, -0.5, 0.25, -1.0],
sample_rate!(44100),
);
let test = AudioSamples::from_mono_vec::<f64>(
non_empty_vec![1.1f64, 0.6, -0.4, 0.35, -0.9],
sample_rate!(44100),
);
let db = psnr(&reference, &test).unwrap();
assert_approx_eq!(db, 20.0, 1e-9);
}
#[test]
fn test_segmental_snr_scaled_noise() {
let signal: AudioSamples<'static, f64> = AudioSamples::from_mono_vec::<f64>(
non_empty_vec![1.0f64, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
sample_rate!(44100),
);
let noise = AudioSamples::from_mono_vec::<f64>(
non_empty_vec![0.1f64, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1],
sample_rate!(44100),
);
let db = segmental_snr(&signal, &noise, core::num::NonZeroUsize::new(4).unwrap()).unwrap();
assert_approx_eq!(db, 20.0, 1e-9);
}
#[test]
fn test_segmental_snr_clamp_engages_for_silent_segment() {
let signal: AudioSamples<'static, f64> = AudioSamples::from_mono_vec::<f64>(
non_empty_vec![1.0f64, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0],
sample_rate!(44100),
);
let noise = AudioSamples::from_mono_vec::<f64>(
non_empty_vec![0.1f64, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1],
sample_rate!(44100),
);
let db = segmental_snr(&signal, &noise, core::num::NonZeroUsize::new(4).unwrap()).unwrap();
assert_approx_eq!(db, 5.0, 1e-9);
}
#[test]
fn test_per_channel_mono_equals_aggregate() {
let a: AudioSamples<'static, f64> = AudioSamples::from_mono_vec::<f64>(
non_empty_vec![1.0f64, 2.0, 3.0, 4.0, 5.0],
sample_rate!(44100),
);
let b = AudioSamples::from_mono_vec::<f64>(
non_empty_vec![1.0f64, 2.5, 2.5, 4.5, 5.0],
sample_rate!(44100),
);
let corr_pc = correlation_per_channel(&a, &b).unwrap();
let mse_pc = mse_per_channel(&a, &b).unwrap();
let snr_pc = snr_per_channel(&a, &b).unwrap();
assert_eq!(corr_pc.len(), 1);
assert_eq!(mse_pc.len(), 1);
assert_eq!(snr_pc.len(), 1);
assert_approx_eq!(corr_pc[0], correlation(&a, &b).unwrap(), 1e-12);
assert_approx_eq!(mse_pc[0], mse(&a, &b).unwrap(), 1e-12);
assert_approx_eq!(snr_pc[0], snr(&a, &b).unwrap(), 1e-12);
}
#[test]
fn test_per_channel_distinct_values_for_two_channels() {
let a = AudioSamples::new_multi_channel(
ndarray::array![[1.0f64, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0]],
sample_rate!(44100),
)
.unwrap();
let b = AudioSamples::new_multi_channel(
ndarray::array![[1.0f64, 2.0, 3.0, 4.0], [4.0, 3.0, 2.0, 1.0]],
sample_rate!(44100),
)
.unwrap();
let mse_pc = mse_per_channel(&a, &b).unwrap();
let corr_pc = correlation_per_channel(&a, &b).unwrap();
assert_eq!(mse_pc.len(), 2);
assert_eq!(corr_pc.len(), 2);
assert_approx_eq!(mse_pc[0], 0.0, 1e-12);
assert!(mse_pc[1] > 0.0);
assert!((mse_pc[0] - mse_pc[1]).abs() > 1e-6);
assert_approx_eq!(corr_pc[0], 1.0, 1e-12);
assert_approx_eq!(corr_pc[1], -1.0, 1e-12);
}
#[cfg(feature = "transforms")]
#[test]
fn test_log_spectral_distance_identical_and_different() {
use crate::utils::generation::sine_wave;
use std::time::Duration;
let sr = sample_rate!(44100);
let a = sine_wave::<f64>(440.0, Duration::from_millis(50), sr, 1.0);
let a2 = sine_wave::<f64>(440.0, Duration::from_millis(50), sr, 1.0);
let c = sine_wave::<f64>(880.0, Duration::from_millis(50), sr, 1.0);
let lsd_identical = log_spectral_distance(&a, &a2).unwrap();
assert!(lsd_identical < 1e-6, "identical → ~0, got {lsd_identical}");
let lsd_diff = log_spectral_distance(&a, &c).unwrap();
assert!(lsd_diff > 0.0, "different tones → > 0, got {lsd_diff}");
}
}