#![allow(dead_code)]
use std::fmt;
#[derive(Debug, PartialEq, Eq)]
pub enum DownmixError {
InvalidDimensions {
expected: (usize, usize),
actual: usize,
},
InputLengthMismatch {
input_len: usize,
in_channels: usize,
},
OutputLengthMismatch {
output_len: usize,
required: usize,
},
}
impl fmt::Display for DownmixError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidDimensions { expected, actual } => write!(
f,
"invalid matrix: expected {} coefficients ({}×{}), got {}",
expected.0 * expected.1,
expected.0,
expected.1,
actual
),
Self::InputLengthMismatch {
input_len,
in_channels,
} => write!(
f,
"input length {input_len} is not a multiple of in_channels {in_channels}"
),
Self::OutputLengthMismatch {
output_len,
required,
} => write!(f, "output buffer length {output_len} < required {required}"),
}
}
}
impl std::error::Error for DownmixError {}
#[derive(Debug, Clone, PartialEq)]
pub struct DownmixMatrix {
out_channels: usize,
in_channels: usize,
coeffs: Vec<f32>,
}
impl DownmixMatrix {
pub fn new(
out_channels: usize,
in_channels: usize,
coeffs: Vec<f32>,
) -> Result<Self, DownmixError> {
let expected = out_channels * in_channels;
if coeffs.len() != expected {
return Err(DownmixError::InvalidDimensions {
expected: (out_channels, in_channels),
actual: coeffs.len(),
});
}
Ok(Self {
out_channels,
in_channels,
coeffs,
})
}
#[must_use]
pub fn output_channels(&self) -> usize {
self.out_channels
}
#[must_use]
pub fn input_channels(&self) -> usize {
self.in_channels
}
#[must_use]
pub fn coeff(&self, o: usize, i: usize) -> Option<f32> {
if o < self.out_channels && i < self.in_channels {
Some(self.coeffs[o * self.in_channels + i])
} else {
None
}
}
pub fn apply(
&self,
input: &[f32],
output: &mut [f32],
sample_count: usize,
) -> Result<(), DownmixError> {
let expected_in = sample_count * self.in_channels;
if input.len() < expected_in
|| (self.in_channels > 0 && input.len() % self.in_channels != 0)
{
return Err(DownmixError::InputLengthMismatch {
input_len: input.len(),
in_channels: self.in_channels,
});
}
let required_out = sample_count * self.out_channels;
if output.len() < required_out {
return Err(DownmixError::OutputLengthMismatch {
output_len: output.len(),
required: required_out,
});
}
for s in 0..sample_count {
let in_base = s * self.in_channels;
let out_base = s * self.out_channels;
for o in 0..self.out_channels {
let mut acc = 0.0f32;
for i in 0..self.in_channels {
acc += self.coeffs[o * self.in_channels + i] * input[in_base + i];
}
output[out_base + o] = acc;
}
}
Ok(())
}
pub fn apply_unchecked(&self, input: &[f32], output: &mut [f32], sample_count: usize) {
self.apply(input, output, sample_count)
.expect("apply_unchecked: dimension mismatch");
}
const INV_SQRT2: f32 = 0.707_106_8;
#[must_use]
pub fn surround51_to_stereo() -> Self {
#[rustfmt::skip]
let coeffs = vec![
1.0, 0.0, Self::INV_SQRT2, 0.0, 0.5, 0.0, 0.0, 1.0, Self::INV_SQRT2, 0.0, 0.0, 0.5, ];
Self {
out_channels: 2,
in_channels: 6,
coeffs,
}
}
#[must_use]
pub fn surround51_to_mono() -> Self {
#[rustfmt::skip]
let coeffs = vec![
0.5, 0.5, Self::INV_SQRT2, 0.0, 0.25, 0.25,
];
Self {
out_channels: 1,
in_channels: 6,
coeffs,
}
}
#[must_use]
pub fn stereo_to_mono() -> Self {
let coeffs = vec![0.5, 0.5];
Self {
out_channels: 1,
in_channels: 2,
coeffs,
}
}
#[must_use]
pub fn mono_to_stereo() -> Self {
let coeffs = vec![
Self::INV_SQRT2, Self::INV_SQRT2, ];
Self {
out_channels: 2,
in_channels: 1,
coeffs,
}
}
#[must_use]
pub fn surround71_to_51() -> Self {
#[rustfmt::skip]
let coeffs = vec![
1.0, 0.0, 0.0, 0.0, 0.0, 0.0, Self::INV_SQRT2, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, Self::INV_SQRT2, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, Self::INV_SQRT2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, Self::INV_SQRT2, ];
Self {
out_channels: 6,
in_channels: 8,
coeffs,
}
}
#[must_use]
pub fn surround71_to_stereo() -> Self {
#[rustfmt::skip]
let coeffs = vec![
1.0, 0.0, Self::INV_SQRT2, 0.0, 0.5, 0.0, Self::INV_SQRT2, 0.0, 0.0, 1.0, Self::INV_SQRT2, 0.0, 0.0, 0.5, 0.0, Self::INV_SQRT2, ];
Self {
out_channels: 2,
in_channels: 8,
coeffs,
}
}
#[must_use]
pub fn atmos514_to_51() -> Self {
#[rustfmt::skip]
let coeffs = vec![
1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.5, ];
Self {
out_channels: 6,
in_channels: 10,
coeffs,
}
}
#[must_use]
pub fn identity(channels: usize) -> Self {
let n = channels;
let mut coeffs = vec![0.0f32; n * n];
for c in 0..n {
coeffs[c * n + c] = 1.0;
}
Self {
out_channels: n,
in_channels: n,
coeffs,
}
}
#[must_use]
pub fn transpose(&self) -> Self {
let mut coeffs = vec![0.0f32; self.in_channels * self.out_channels];
for o in 0..self.out_channels {
for i in 0..self.in_channels {
coeffs[i * self.out_channels + o] = self.coeffs[o * self.in_channels + i];
}
}
Self {
out_channels: self.in_channels,
in_channels: self.out_channels,
coeffs,
}
}
}
impl fmt::Display for DownmixMatrix {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"DownmixMatrix({}×{})",
self.out_channels, self.in_channels
)
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f32 = 1e-5;
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < EPS
}
#[test]
fn test_new_dimension_mismatch() {
let result = DownmixMatrix::new(2, 3, vec![1.0; 7]);
assert!(result.is_err());
match result {
Err(DownmixError::InvalidDimensions { expected, actual }) => {
assert_eq!(expected, (2, 3));
assert_eq!(actual, 7);
}
_ => panic!("expected InvalidDimensions"),
}
}
#[test]
fn test_new_valid() {
let mx = DownmixMatrix::new(2, 3, vec![0.0; 6]).expect("valid");
assert_eq!(mx.input_channels(), 3);
assert_eq!(mx.output_channels(), 2);
}
#[test]
fn test_identity_passthrough() {
let mx = DownmixMatrix::identity(3);
let input = vec![1.0f32, 2.0, 3.0];
let mut output = vec![0.0f32; 3];
mx.apply(&input, &mut output, 1).expect("apply ok");
assert!(approx_eq(output[0], 1.0));
assert!(approx_eq(output[1], 2.0));
assert!(approx_eq(output[2], 3.0));
}
#[test]
fn test_stereo_to_mono() {
let mx = DownmixMatrix::stereo_to_mono();
let input = vec![1.0f32, 1.0];
let mut output = vec![0.0f32; 1];
mx.apply(&input, &mut output, 1).expect("apply ok");
assert!(approx_eq(output[0], 1.0));
}
#[test]
fn test_mono_to_stereo() {
let mx = DownmixMatrix::mono_to_stereo();
let input = vec![1.0f32];
let mut output = vec![0.0f32; 2];
mx.apply(&input, &mut output, 1).expect("apply ok");
let expected = DownmixMatrix::INV_SQRT2;
assert!(approx_eq(output[0], expected));
assert!(approx_eq(output[1], expected));
}
#[test]
fn test_51_to_stereo_dimensions() {
let mx = DownmixMatrix::surround51_to_stereo();
assert_eq!(mx.input_channels(), 6);
assert_eq!(mx.output_channels(), 2);
}
#[test]
fn test_51_to_stereo_pure_front() {
let mx = DownmixMatrix::surround51_to_stereo();
let input = vec![1.0f32, 1.0, 0.0, 0.0, 0.0, 0.0];
let mut output = vec![0.0f32; 2];
mx.apply(&input, &mut output, 1).expect("apply ok");
assert!(approx_eq(output[0], 1.0));
assert!(approx_eq(output[1], 1.0));
}
#[test]
fn test_51_to_stereo_centre_split() {
let mx = DownmixMatrix::surround51_to_stereo();
let input = vec![0.0f32, 0.0, 1.0, 0.0, 0.0, 0.0];
let mut output = vec![0.0f32; 2];
mx.apply(&input, &mut output, 1).expect("apply ok");
assert!(
approx_eq(output[0], DownmixMatrix::INV_SQRT2),
"L = {:.6}",
output[0]
);
assert!(
approx_eq(output[1], DownmixMatrix::INV_SQRT2),
"R = {:.6}",
output[1]
);
}
#[test]
fn test_51_to_mono_dimensions() {
let mx = DownmixMatrix::surround51_to_mono();
assert_eq!(mx.input_channels(), 6);
assert_eq!(mx.output_channels(), 1);
}
#[test]
fn test_51_to_mono_lfe_discarded() {
let mx = DownmixMatrix::surround51_to_mono();
let lfe_coeff = mx.coeff(0, 3).expect("coeff(0,3)");
assert!(approx_eq(lfe_coeff, 0.0));
}
#[test]
fn test_71_to_51_dimensions() {
let mx = DownmixMatrix::surround71_to_51();
assert_eq!(mx.input_channels(), 8);
assert_eq!(mx.output_channels(), 6);
}
#[test]
fn test_71_to_51_fc_passthrough() {
let mx = DownmixMatrix::surround71_to_51();
let fc_coeff = mx.coeff(2, 2).expect("coeff(2,2)");
assert!(approx_eq(fc_coeff, 1.0));
assert!(approx_eq(mx.coeff(0, 2).expect("c(0,2)"), 0.0));
assert!(approx_eq(mx.coeff(1, 2).expect("c(1,2)"), 0.0));
}
#[test]
fn test_71_to_stereo() {
let mx = DownmixMatrix::surround71_to_stereo();
assert_eq!(mx.input_channels(), 8);
assert_eq!(mx.output_channels(), 2);
let sl_to_l = mx.coeff(0, 6).expect("coeff(0,6)");
assert!(approx_eq(sl_to_l, DownmixMatrix::INV_SQRT2));
let sr_to_l = mx.coeff(0, 7).expect("coeff(0,7)");
assert!(approx_eq(sr_to_l, 0.0));
}
#[test]
fn test_atmos514_to_51() {
let mx = DownmixMatrix::atmos514_to_51();
assert_eq!(mx.input_channels(), 10);
assert_eq!(mx.output_channels(), 6);
let tfl_coeff = mx.coeff(0, 6).expect("coeff(0,6)");
assert!(approx_eq(tfl_coeff, 0.5));
}
#[test]
fn test_apply_multiple_samples() {
let mx = DownmixMatrix::stereo_to_mono();
let input = vec![1.0f32, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0];
let mut output = vec![0.0f32; 4];
mx.apply(&input, &mut output, 4).expect("apply ok");
assert!(approx_eq(output[0], 1.0));
assert!(approx_eq(output[1], 2.0));
assert!(approx_eq(output[2], 3.0));
assert!(approx_eq(output[3], 4.0));
}
#[test]
fn test_apply_output_too_small() {
let mx = DownmixMatrix::stereo_to_mono();
let input = vec![1.0f32, 1.0];
let mut output = vec![]; let result = mx.apply(&input, &mut output, 1);
assert!(matches!(
result,
Err(DownmixError::OutputLengthMismatch { .. })
));
}
#[test]
fn test_transpose_stereo_to_mono() {
let mx = DownmixMatrix::stereo_to_mono().transpose();
assert_eq!(mx.input_channels(), 1);
assert_eq!(mx.output_channels(), 2);
}
#[test]
fn test_coeff_out_of_range() {
let mx = DownmixMatrix::stereo_to_mono();
assert!(mx.coeff(0, 0).is_some());
assert!(mx.coeff(1, 0).is_none()); assert!(mx.coeff(0, 2).is_none()); }
#[test]
fn test_display() {
let mx = DownmixMatrix::surround51_to_stereo();
let s = format!("{mx}");
assert!(s.contains("2×6"), "display = {s}");
}
#[test]
fn test_error_display() {
let err = DownmixError::InvalidDimensions {
expected: (2, 3),
actual: 7,
};
let s = format!("{err}");
assert!(s.contains("7"));
}
}