#![forbid(unsafe_code)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_lossless)]
use std::f64::consts::PI;
#[derive(Clone, Debug)]
pub struct MdctTwiddles {
pub n: usize,
pub cos_table: Vec<f64>,
pub sin_table: Vec<f64>,
pub window: Vec<f64>,
}
impl MdctTwiddles {
#[must_use]
pub fn new(n: usize) -> Self {
assert!(
n >= 4 && n.is_power_of_two(),
"MDCT block size must be a power-of-two >= 4"
);
let m = n / 2;
let mut cos_table = Vec::with_capacity(m);
let mut sin_table = Vec::with_capacity(m);
let mut window = Vec::with_capacity(n);
for k in 0..m {
let angle = PI * (2.0 * k as f64 + 1.0) / (4.0 * m as f64);
cos_table.push(angle.cos());
sin_table.push(angle.sin());
}
for i in 0..n {
let arg = PI * (i as f64 + 0.5) / n as f64;
let inner = (PI / 2.0) * arg.sin().powi(2);
window.push(inner.sin());
}
Self {
n,
cos_table,
sin_table,
window,
}
}
pub fn apply_window(&self, samples: &mut [f64]) {
assert_eq!(samples.len(), self.n);
for (s, &w) in samples.iter_mut().zip(self.window.iter()) {
*s *= w;
}
}
#[must_use]
pub fn forward(&self, windowed: &[f64]) -> Vec<f64> {
let m = self.n / 2;
assert_eq!(windowed.len(), self.n);
let mut out = Vec::with_capacity(m);
for k in 0..m {
let mut sum = 0.0f64;
for n in 0..self.n {
let angle =
PI / self.n as f64 * (n as f64 + 0.5 + self.n as f64 / 2.0) * (k as f64 + 0.5);
sum += windowed[n] * angle.cos();
}
out.push(sum * 2.0 / self.n as f64);
}
out
}
#[must_use]
pub fn inverse(&self, coeffs: &[f64]) -> Vec<f64> {
let m = self.n / 2;
assert_eq!(coeffs.len(), m);
let mut out = vec![0.0f64; self.n];
for n in 0..self.n {
let mut sum = 0.0f64;
for k in 0..m {
let angle =
PI / self.n as f64 * (n as f64 + 0.5 + self.n as f64 / 2.0) * (k as f64 + 0.5);
sum += coeffs[k] * angle.cos();
}
out[n] = sum;
}
out
}
pub fn apply_synthesis_window(&self, samples: &mut [f64]) {
self.apply_window(samples);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mdct_twiddles_n8() {
let tw = MdctTwiddles::new(8);
assert_eq!(tw.n, 8);
assert_eq!(tw.cos_table.len(), 4);
assert_eq!(tw.window.len(), 8);
}
#[test]
fn test_mdct_window_values_positive() {
let tw = MdctTwiddles::new(16);
for &w in &tw.window {
assert!(w >= 0.0 && w <= 1.0, "window value out of [0,1]: {w}");
}
}
#[test]
fn test_mdct_forward_zero_input() {
let tw = MdctTwiddles::new(16);
let input = vec![0.0f64; 16];
let out = tw.forward(&input);
for &v in &out {
assert!(v.abs() < 1e-12, "Expected ~0, got {v}");
}
}
#[test]
fn test_mdct_forward_inverse_roundtrip() {
let n = 32;
let tw = MdctTwiddles::new(n);
let input: Vec<f64> = (0..n).map(|i| (i as f64 * 0.1).sin()).collect();
let coeffs = tw.forward(&input);
let recovered = tw.inverse(&coeffs);
let coeff_energy: f64 = coeffs.iter().map(|&v| v * v).sum();
assert!(
coeff_energy > 1e-6,
"Forward MDCT should produce non-zero coefficients"
);
let rec_energy: f64 = recovered.iter().map(|&v| v * v).sum();
assert!(
rec_energy > 1e-6,
"Inverse MDCT should produce non-zero output"
);
}
#[test]
fn test_mdct_apply_window_reduces_edges() {
let tw = MdctTwiddles::new(16);
let mut samples = vec![1.0f64; 16];
tw.apply_window(&mut samples);
assert!(
samples[0].abs() < 0.1,
"Window should be near zero at left edge"
);
assert!(
samples[15].abs() < 0.1,
"Window should be near zero at right edge"
);
}
#[test]
#[should_panic(expected = "power-of-two")]
fn test_mdct_non_power_of_two_panics() {
let _ = MdctTwiddles::new(10);
}
}