pub fn to_mono(channels: &[Vec<f64>]) -> Vec<f64> {
if channels.is_empty() {
return Vec::new();
}
if channels.len() == 1 {
return channels[0].clone();
}
let n_samples = channels[0].len();
let n_channels = channels.len() as f64;
(0..n_samples)
.map(|i| channels.iter().map(|ch| ch[i]).sum::<f64>() / n_channels)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
fn assert_approx_eq(actual: &[f64], expected: &[f64], tol: f64, name: &str) {
assert_eq!(
actual.len(),
expected.len(),
"{name}: length mismatch: got {} expected {}",
actual.len(),
expected.len()
);
for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
let diff = (a - e).abs();
assert!(
diff < tol,
"{name}[{i}]: got {a}, expected {e}, diff {diff} > tol {tol}"
);
}
}
#[derive(Deserialize)]
struct MonoTestCase {
name: String,
channels: Vec<Vec<f64>>,
expected: Vec<f64>,
}
fn load_references() -> Vec<MonoTestCase> {
let data = include_str!("tests/references/mono.json");
serde_json::from_str(data).expect("Failed to parse mono.json")
}
#[test]
fn test_to_mono_matches_librosa() {
for case in load_references() {
let result = to_mono(&case.channels);
assert_approx_eq(&result, &case.expected, 1e-12, &case.name);
}
}
}