use super::*;
use crate::Dtype;
const WIN_TOL: f32 = 1e-6;
fn to_vec(a: &Array) -> Vec<f32> {
a.try_clone().unwrap().to_vec::<f32>().unwrap()
}
#[test]
fn hamming_matches_closed_form_n5() {
let v = to_vec(&hamming_window(5).unwrap());
let expected = [0.08_f32, 0.54, 1.0, 0.54, 0.08];
for (i, (g, e)) in v.iter().zip(expected.iter()).enumerate() {
assert!((g - e).abs() < WIN_TOL, "hamming[{i}]: got {g}, want {e}");
}
}
#[test]
fn hamming_endpoints_are_0_08() {
let v = to_vec(&hamming_window(8).unwrap());
assert!((v[0] - 0.08).abs() < WIN_TOL, "first: {}", v[0]);
assert!((v[7] - 0.08).abs() < WIN_TOL, "last: {}", v[7]);
}
#[test]
fn blackman_matches_closed_form_n5() {
let v = to_vec(&blackman_window(5).unwrap());
let expected = [0.0_f32, 0.34, 1.0, 0.34, 0.0];
for (i, (g, e)) in v.iter().zip(expected.iter()).enumerate() {
assert!((g - e).abs() < WIN_TOL, "blackman[{i}]: got {g}, want {e}");
}
}
#[test]
fn bartlett_matches_closed_form_n5_and_n4() {
let v5 = to_vec(&bartlett_window(5).unwrap());
let e5 = [0.0_f32, 0.5, 1.0, 0.5, 0.0];
for (i, (g, e)) in v5.iter().zip(e5.iter()).enumerate() {
assert!((g - e).abs() < WIN_TOL, "bartlett5[{i}]: got {g}, want {e}");
}
let v4 = to_vec(&bartlett_window(4).unwrap());
let e4 = [0.0_f32, 2.0 / 3.0, 2.0 / 3.0, 0.0];
for (i, (g, e)) in v4.iter().zip(e4.iter()).enumerate() {
assert!((g - e).abs() < WIN_TOL, "bartlett4[{i}]: got {g}, want {e}");
}
}
#[test]
fn windows_reject_n_lt_2() {
for r in [
hann_window(0),
hann_window(1),
hamming_window(0),
hamming_window(1),
blackman_window(0),
blackman_window(1),
bartlett_window(0),
bartlett_window(1),
] {
assert!(matches!(r, Err(Error::OutOfRange(_))));
}
for name in ["hann", "hanning", "hamming", "blackman", "bartlett"] {
let r = window_from_name(name, 1);
assert!(
matches!(r, Err(Error::OutOfRange(_))),
"window_from_name({name:?}, 1) must reject n<2, got {r:?}"
);
}
}
#[test]
fn window_from_name_dispatches_case_insensitively() {
let hann = to_vec(&window_from_name("HaNn", 8).unwrap());
assert!(hann[0].abs() < WIN_TOL && hann[7].abs() < WIN_TOL);
let hanning = to_vec(&window_from_name("hanning", 8).unwrap());
assert_eq!(hann, hanning, "hann and hanning must be identical");
let hamming = to_vec(&window_from_name("HAMMING", 8).unwrap());
assert!((hamming[0] - 0.08).abs() < WIN_TOL);
let bartlett = to_vec(&window_from_name("Bartlett", 5).unwrap());
assert!((bartlett[2] - 1.0).abs() < WIN_TOL);
}
#[test]
fn window_from_name_rejects_unknown() {
assert!(matches!(
window_from_name("kaiser", 8),
Err(Error::UnknownEnumValue(_))
));
}
fn signal_16() -> [f32; 16] {
[
0.1, 0.5, -0.3, 0.8, -0.2, 0.6, 0.0, -0.7, 0.4, 0.9, -0.5, 0.2, 0.3, -0.1, 0.7, -0.4,
]
}
fn signal_19() -> [f32; 19] {
[
0.1, 0.5, -0.3, 0.8, -0.2, 0.6, 0.0, -0.7, 0.4, 0.9, -0.5, 0.2, 0.3, -0.1, 0.7, -0.4, 0.55,
0.66, -0.77,
]
}
fn assert_roundtrips_all_samples(
signal: &[f32],
n_fft: usize,
win_length: usize,
hop: usize,
window_pad: WindowPad,
len_override: Option<usize>,
) {
let x = Array::from_slice::<f32>(signal, &[signal.len() as i32]).unwrap();
let spec = stft(&x, n_fft, hop, Some(win_length), window_pad).unwrap();
let rec = istft(&spec, len_override).unwrap();
let r = to_vec(&rec);
let expected_len = len_override.unwrap_or(signal.len());
assert_eq!(
r.len(),
expected_len,
"round-trip length mismatch (n_fft={n_fft} win={win_length} hop={hop} {window_pad:?})"
);
for (i, (g, e)) in r.iter().zip(signal.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-5,
"roundtrip[{i}] (n_fft={n_fft} win={win_length} hop={hop} {window_pad:?}): \
got {g}, want {e} (diff {})",
(g - e).abs()
);
}
}
#[test]
fn istft_win_eq_nfft_both_modes_identical_all_samples() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
let spec_c = stft(&x, 8, 4, Some(8), WindowPad::Center).unwrap();
let spec_r = stft(&x, 8, 4, Some(8), WindowPad::Right).unwrap();
assert_eq!(spec_c.data_ref().shape(), vec![5, 5]); assert_eq!(spec_c.n_fft(), 8);
assert_eq!(spec_c.win_length(), 8);
assert_eq!(spec_c.hop_length(), 4);
assert_eq!(spec_c.window_pad(), WindowPad::Center);
assert!(spec_c.center());
for (c, r) in to_vec(&spec_c.data_ref().abs().unwrap())
.iter()
.zip(to_vec(&spec_r.data_ref().abs().unwrap()).iter())
{
assert!(
(c - r).abs() < 1e-6,
"win==nfft: spectra must match across modes"
);
}
for mode in [WindowPad::Center, WindowPad::Right] {
assert_roundtrips_all_samples(&buf, 8, 8, 4, mode, None);
assert_roundtrips_all_samples(&buf, 8, 8, 4, mode, Some(16));
}
}
#[test]
fn istft_win_eq_nfft_non_hop_aligned_all_samples() {
for &len in &[17usize, 19usize] {
let full = signal_19();
let buf = &full[..len];
for mode in [WindowPad::Center, WindowPad::Right] {
assert_roundtrips_all_samples(buf, 8, 8, 4, mode, Some(len));
}
}
}
#[test]
fn istft_center_short_window_all_samples() {
let buf = signal_16();
for &win in &[8usize, 12usize] {
assert_roundtrips_all_samples(&buf, 16, win, 4, WindowPad::Center, None);
}
}
#[test]
fn istft_right_short_window_rejected() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
for &win in &[8usize, 12usize] {
let spec = stft(&x, 16, 4, Some(win), WindowPad::Right).unwrap();
assert_eq!(spec.data_ref().shape(), vec![5, 9]); assert_eq!(spec.window_pad(), WindowPad::Right);
assert_eq!(spec.win_length(), win);
for len in [None, Some(16usize)] {
let res = istft(&spec, len);
assert!(
matches!(res, Err(Error::OutOfRange(_))),
"Right + win={win} < n_fft=16 (length={len:?}) must be rejected up front \
(covered-but-wrong for win=12; the coverage guard does NOT catch it), \
got {res:?}"
);
}
}
for &win in &[8usize, 12usize] {
assert_roundtrips_all_samples(&buf, 16, win, 4, WindowPad::Center, None);
}
}
#[test]
fn istft_center_length_removes_pad_before_truncating() {
let buf = signal_16();
assert_roundtrips_all_samples(&buf, 8, 8, 4, WindowPad::Center, Some(10));
}
#[test]
fn istft_center_false_uncovered_edge_errors() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
let spec = stft(&x, 8, 4, Some(8), WindowPad::Center).unwrap();
let spec_no_center = Spectrum::from_parts(
spec.data_ref().try_clone().unwrap(),
8, 4, 8, WindowPad::Center,
false, )
.unwrap();
for len in [None, Some(10usize)] {
let res = istft(&spec_no_center, len);
assert!(
matches!(res, Err(Error::OutOfRange(_))),
"center=false head (length={len:?}) includes the zero-coverage OLA \
index 0 and must hit the coverage guard, got {res:?}"
);
}
}
#[test]
fn stft_rejects_odd_n_fft() {
let buf = signal_19();
let x = Array::from_slice::<f32>(&buf, &[buf.len() as i32]).unwrap();
for n_fft in [9usize, 15] {
let res = stft(&x, n_fft, 4, None, WindowPad::Center);
assert!(
matches!(res, Err(Error::OutOfRange(_))),
"odd n_fft={n_fft} must be rejected up front, got {res:?}"
);
}
assert!(stft(&x, 8, 4, None, WindowPad::Center).is_ok());
}
#[test]
fn istft_rejects_length_out_of_range() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
let spec = stft(&x, 8, 4, Some(8), WindowPad::Center).unwrap();
assert!(matches!(
istft(&spec, Some(1000)),
Err(Error::OutOfRange(_))
));
}
#[test]
fn spectrum_from_parts_rejects_inconsistent_metadata() {
let valid = Array::zeros::<f32>(&[5i32, 5i32])
.unwrap()
.astype(Dtype::Complex64)
.unwrap();
assert!(
Spectrum::from_parts(valid.try_clone().unwrap(), 8, 4, 8, WindowPad::Center, true).is_ok()
);
assert!(matches!(
Spectrum::from_parts(valid.try_clone().unwrap(), 9, 4, 8, WindowPad::Center, true),
Err(Error::OutOfRange(_))
));
assert!(matches!(
Spectrum::from_parts(
valid.try_clone().unwrap(),
16,
4,
8,
WindowPad::Center,
true
),
Err(Error::LengthMismatch(_))
));
assert!(matches!(
Spectrum::from_parts(
valid.try_clone().unwrap(),
8,
4,
16,
WindowPad::Center,
true
),
Err(Error::OutOfRange(_))
));
assert!(matches!(
Spectrum::from_parts(valid.try_clone().unwrap(), 8, 0, 8, WindowPad::Center, true),
Err(Error::InvariantViolation(_))
));
assert!(matches!(
Spectrum::from_parts(valid.try_clone().unwrap(), 8, 4, 0, WindowPad::Center, true),
Err(Error::InvariantViolation(_))
));
assert!(matches!(
Spectrum::from_parts(valid.try_clone().unwrap(), 0, 4, 0, WindowPad::Center, true),
Err(Error::InvariantViolation(_))
));
let one_d = Array::from_slice::<f32>(&[1.0, 2.0, 3.0], &[3i32])
.unwrap()
.astype(Dtype::Complex64)
.unwrap();
assert!(matches!(
Spectrum::from_parts(one_d, 8, 4, 8, WindowPad::Center, true),
Err(Error::RankMismatch(_))
));
let real_data = Array::zeros::<f32>(&[5i32, 5i32]).unwrap();
assert!(matches!(
Spectrum::from_parts(real_data, 8, 4, 8, WindowPad::Center, true),
Err(Error::DtypeMismatch(_))
));
}
#[test]
fn spectrum_from_parts_then_istft_round_trips() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
let stft_spec = stft(&x, 8, 4, Some(8), WindowPad::Center).unwrap();
let external = Spectrum::from_parts(
stft_spec.data_ref().try_clone().unwrap(),
8,
4,
8,
WindowPad::Center,
true,
)
.unwrap();
let rec = istft(&external, Some(16)).unwrap();
let r = to_vec(&rec);
assert_eq!(r.len(), 16);
for (i, (g, e)) in r.iter().zip(buf.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-5,
"from_parts round-trip[{i}]: got {g}, want {e}"
);
}
}
#[test]
fn istft_rejects_pathological_scatter_work_before_window_alloc() {
let n_freqs: i32 = 9 * 1024 * 1024 + 1;
let num_frames: i32 = 4;
let n_fft = (n_freqs as usize - 1) * 2; let data = Array::zeros::<f32>(&[num_frames, n_freqs])
.unwrap()
.astype(crate::Dtype::Complex64)
.unwrap();
let spec = Spectrum::from_parts(
data,
n_fft,
2, n_fft, WindowPad::Center,
true,
)
.unwrap();
let res = istft(&spec, None);
assert!(
matches!(res, Err(Error::CapExceeded(_))),
"pathological num_frames*n_fft must be rejected by the MAX_OLA_WORK cap \
before the frame_window allocation"
);
}
#[test]
fn stft_rejects_pathological_work_before_alloc() {
let n_samples = 64 * 1024 * 1024i32;
let lazy = Array::zeros::<f32>(&[n_samples]).unwrap();
let res = stft(&lazy, 1024, 1, None, WindowPad::Right);
assert!(
matches!(res, Err(Error::CapExceeded(_))),
"pathological lazy huge-shape stft input (num_frames * n_fft) must be \
rejected by the MAX_STFT_WORK cap before any framing/FFT allocation, got {res:?}"
);
}
#[test]
fn stft_rejects_oversized_input_before_reflect_pad_large_hop() {
let n_samples = (crate::audio::io::MAX_DECODED_SAMPLES + 16) as i32;
let lazy = Array::zeros::<f32>(&[n_samples]).unwrap();
let large_hop = crate::audio::io::MAX_DECODED_SAMPLES;
let res = stft(&lazy, 16, large_hop, None, WindowPad::Right);
assert!(
matches!(res, Err(Error::CapExceeded(_))),
"oversized lazy stft input with a large hop (work cap would pass) must be \
rejected by the input/padded-length cap before the reflect pad, got {res:?}"
);
}
#[test]
fn mel_spectrogram_short_window_uses_right_pad_unchanged() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
let n_fft = 16usize;
let win = 8usize;
let hop = 4usize;
let n_mels = 6usize;
let sr = 16_000u32;
let got = to_vec(&mel_spectrogram(&x, n_fft, hop, Some(win), n_mels, sr, 0.0, None).unwrap());
let expected_mel = {
let spec = stft(&x, n_fft, hop, Some(win), WindowPad::Right).unwrap();
let power = spec.data_ref().abs().unwrap().square().unwrap();
let bank = mel_filter_bank(n_mels, n_fft, sr, 0.0, None).unwrap();
let power_t = power.transpose().unwrap();
to_vec(&ops::linalg_basic::matmul(&bank, &power_t).unwrap())
};
assert_eq!(got.len(), expected_mel.len(), "mel length mismatch");
for (i, (g, e)) in got.iter().zip(expected_mel.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-5,
"mel_spectrogram[{i}] must match the Right-padded reference: got {g}, want {e}"
);
}
let center_mel = {
let spec = stft(&x, n_fft, hop, Some(win), WindowPad::Center).unwrap();
let power = spec.data_ref().abs().unwrap().square().unwrap();
let bank = mel_filter_bank(n_mels, n_fft, sr, 0.0, None).unwrap();
let power_t = power.transpose().unwrap();
to_vec(&ops::linalg_basic::matmul(&bank, &power_t).unwrap())
};
let max_diff = got
.iter()
.zip(center_mel.iter())
.map(|(r, c)| (r - c).abs())
.fold(0.0_f32, f32::max);
assert!(
max_diff > 1e-4,
"Right- and Center-padded short-window mel must DIFFER (else the pad pin \
is vacuous); max diff was {max_diff}"
);
}
#[test]
fn lfilter_single_pole_iir_impulse_response() {
let b: [f64; 1] = [0.5];
let a: [f64; 2] = [1.0, -0.5];
let x_buf: [f32; 5] = [1.0, 0.0, 0.0, 0.0, 0.0];
let x = Array::from_slice::<f32>(&x_buf, &[5i32]).unwrap();
let y = to_vec(&lfilter(&b, &a, &x).unwrap());
let expected = [0.5_f32, 0.25, 0.125, 0.0625, 0.03125];
assert_eq!(y.len(), expected.len());
for (i, (g, e)) in y.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-7,
"lfilter[{i}]: got {g}, want {e} (diff {})",
(g - e).abs()
);
}
}
#[test]
fn lfilter_single_pole_iir_step_response() {
let b: [f64; 1] = [0.5];
let a: [f64; 2] = [1.0, -0.5];
let x_buf: [f32; 5] = [1.0, 1.0, 1.0, 1.0, 1.0];
let x = Array::from_slice::<f32>(&x_buf, &[5i32]).unwrap();
let y = to_vec(&lfilter(&b, &a, &x).unwrap());
let expected = [0.5_f32, 0.75, 0.875, 0.9375, 0.96875];
assert_eq!(y.len(), expected.len());
for (i, (g, e)) in y.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-7,
"lfilter step[{i}]: got {g}, want {e} (diff {})",
(g - e).abs()
);
}
}
#[test]
fn lfilter_fir_no_state_doubles() {
let b: [f64; 1] = [2.0];
let a: [f64; 1] = [1.0];
let x_buf: [f32; 4] = [0.1, -0.5, 0.7, 1.0];
let x = Array::from_slice::<f32>(&x_buf, &[4i32]).unwrap();
let y = to_vec(&lfilter(&b, &a, &x).unwrap());
let expected = [0.2_f32, -1.0, 1.4, 2.0];
for (i, (g, e)) in y.iter().zip(expected.iter()).enumerate() {
assert!((g - e).abs() < 1e-6, "fir[{i}]: got {g}, want {e}");
}
}
#[test]
fn lfilter_normalizes_by_leading_a() {
let b: [f64; 1] = [1.0];
let a: [f64; 1] = [2.0];
let x_buf: [f32; 3] = [4.0, 8.0, -2.0];
let x = Array::from_slice::<f32>(&x_buf, &[3i32]).unwrap();
let y = to_vec(&lfilter(&b, &a, &x).unwrap());
let expected = [2.0_f32, 4.0, -1.0];
for (i, (g, e)) in y.iter().zip(expected.iter()).enumerate() {
assert!((g - e).abs() < 1e-6, "norm[{i}]: got {g}, want {e}");
}
}
#[test]
fn lfilter_biquad_hand_traced_impulse() {
let b: [f64; 2] = [0.25, 0.5];
let a: [f64; 3] = [1.0, -0.3, 0.1];
let x_buf: [f32; 4] = [1.0, 0.0, 0.0, 0.0];
let x = Array::from_slice::<f32>(&x_buf, &[4i32]).unwrap();
let y = to_vec(&lfilter(&b, &a, &x).unwrap());
let expected = [0.25_f32, 0.575, 0.1475, -0.01325];
for (i, (g, e)) in y.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-6,
"biquad[{i}]: got {g}, want {e} (diff {})",
(g - e).abs()
);
}
}
#[test]
fn lfilter_empty_b_returns_zeros() {
let b: [f64; 0] = [];
let a: [f64; 1] = [1.0];
let x_buf: [f32; 4] = [1.0, 2.0, 3.0, 4.0];
let x = Array::from_slice::<f32>(&x_buf, &[4i32]).unwrap();
let y = to_vec(&lfilter(&b, &a, &x).unwrap());
assert_eq!(y, vec![0.0_f32; 4]);
}
#[test]
fn lfilter_rejects_invalid_inputs() {
let x = Array::from_slice::<f32>(&[1.0_f32, 2.0], &[2i32]).unwrap();
assert!(matches!(
lfilter(&[1.0_f64], &[], &x),
Err(Error::EmptyInput(_))
));
assert!(matches!(
lfilter(&[1.0_f64], &[0.0_f64, 1.0], &x),
Err(Error::InvariantViolation(_))
));
let x_2d = Array::from_slice::<f32>(&[1.0_f32, 2.0, 3.0, 4.0], &[2i32, 2i32]).unwrap();
assert!(matches!(
lfilter(&[0.5_f64], &[1.0_f64, -0.5], &x_2d),
Err(Error::RankMismatch(_))
));
}
#[test]
fn lfilter_rejects_lazy_oversized_input_without_allocating() {
let lazy_huge =
Array::zeros::<f32>(&[(MAX_LFILTER_SAMPLES + 1) as i32]).expect("lazy zeros must succeed");
let res = lfilter(&[0.5_f64], &[1.0_f64, -0.5], &lazy_huge);
assert!(
matches!(res, Err(Error::CapExceeded(_))),
"lfilter must reject a lazy ({} samples) input via the up-front \
sample cap, BEFORE the to_vec materializes f32 / promotes to f64 \
(got {res:?})",
MAX_LFILTER_SAMPLES + 1
);
}
#[test]
fn lfilter_f64_in_place_matches_out_of_place() {
let b: [f64; 2] = [0.25, 0.5];
let a: [f64; 3] = [1.0, -0.3, 0.1];
let x_in: [f64; 4] = [1.0, 0.0, 0.0, 0.0];
let y_out = lfilter_f64(&b, &a, &x_in).expect("out-of-place must succeed");
let mut x_buf = x_in;
lfilter_f64_in_place(&b, &a, &mut x_buf).expect("in-place must succeed");
assert_eq!(
x_buf.len(),
y_out.len(),
"in-place output length must match out-of-place"
);
for (i, (g, e)) in x_buf.iter().zip(y_out.iter()).enumerate() {
assert_eq!(
g, e,
"in-place[{i}] = {g} must equal out-of-place[{i}] = {e} \
(bit-identical f64)"
);
}
}
#[test]
fn lfilter_f64_in_place_state_len_zero_doubles() {
let b: [f64; 1] = [2.0];
let a: [f64; 1] = [1.0];
let x_in: [f64; 4] = [0.1, -0.5, 0.7, 1.0];
let y_out = lfilter_f64(&b, &a, &x_in).expect("out-of-place must succeed");
let mut x_buf = x_in;
lfilter_f64_in_place(&b, &a, &mut x_buf).expect("in-place must succeed");
for (i, (g, e)) in x_buf.iter().zip(y_out.iter()).enumerate() {
assert_eq!(g, e, "in-place fir[{i}] = {g} must equal out-of-place {e}");
}
}
#[test]
fn lfilter_f64_in_place_empty_b_zeros_buffer() {
let b: [f64; 0] = [];
let a: [f64; 1] = [1.0];
let mut x_buf: [f64; 4] = [1.0, 2.0, 3.0, 4.0];
lfilter_f64_in_place(&b, &a, &mut x_buf).expect("empty-b must succeed");
for (i, &v) in x_buf.iter().enumerate() {
assert_eq!(v, 0.0, "empty-b in-place must zero x_buf[{i}], got {v}");
}
}
#[test]
fn lfilter_f64_in_place_rejects_invalid_inputs() {
let mut x_buf: [f64; 2] = [1.0, 2.0];
assert!(matches!(
lfilter_f64_in_place(&[1.0_f64], &[], &mut x_buf),
Err(Error::EmptyInput(_))
));
assert!(matches!(
lfilter_f64_in_place(&[1.0_f64], &[0.0_f64, 1.0], &mut x_buf),
Err(Error::InvariantViolation(_))
));
}
fn sine_mono(freq: f64, amp: f32, rate: u32, seconds: f64) -> Array {
let n = (seconds * f64::from(rate)) as usize;
let mut buf: Vec<f32> = Vec::with_capacity(n);
let two_pi_freq = 2.0 * std::f64::consts::PI * freq;
let rate_f64 = f64::from(rate);
for i in 0..n {
let t = i as f64 / rate_f64;
buf.push(amp * (two_pi_freq * t).sin() as f32);
}
Array::from_slice::<f32>(&buf, &[n as i32]).unwrap()
}
#[test]
fn integrated_loudness_sine_produces_finite_lufs() {
let x = sine_mono(1000.0, 0.5, 48_000, 3.0);
let lufs = integrated_loudness(&x, 48_000, 0.4, 0.75).unwrap();
assert!(
lufs.is_finite(),
"integrated_loudness on a 1 kHz sine must be finite, got {lufs}"
);
assert!(
lufs > BS1770_ABSOLUTE_THRESHOLD_LUFS,
"1 kHz sine at amp=0.5 should be well above -70 LUFS (got {lufs})"
);
}
#[test]
fn integrated_loudness_scales_with_amplitude_squared() {
let rate = 48_000u32;
let x_lo = sine_mono(1000.0, 0.25, rate, 3.0);
let x_hi = sine_mono(1000.0, 0.5, rate, 3.0); let l_lo = integrated_loudness(&x_lo, rate, 0.4, 0.75).unwrap();
let l_hi = integrated_loudness(&x_hi, rate, 0.4, 0.75).unwrap();
let delta = l_hi - l_lo;
assert!(
(delta - 6.0206).abs() < 0.05,
"doubling amplitude (+6 dB) should add ~6 LU (got {delta} = {l_hi} - {l_lo})"
);
}
#[test]
fn normalize_loudness_round_trip_matches_target() {
let rate = 48_000u32;
let x = sine_mono(1000.0, 0.5, rate, 3.0);
let lufs_before = integrated_loudness(&x, rate, 0.4, 0.75).unwrap();
let target = -23.0_f64;
let normalized = normalize_loudness(&x, lufs_before, target).unwrap();
let lufs_after = integrated_loudness(&normalized, rate, 0.4, 0.75).unwrap();
assert!(
(lufs_after - target).abs() < 0.01,
"normalize_loudness round-trip should hit target ±0.01 LUFS, \
got {lufs_after} (target {target}, before {lufs_before})"
);
}
#[test]
fn integrated_loudness_silence_returns_neg_inf() {
let rate = 48_000u32;
let n = (3.0 * f64::from(rate)) as usize;
let zeros = vec![0.0_f32; n];
let x = Array::from_slice::<f32>(&zeros, &[n as i32]).unwrap();
let lufs = integrated_loudness(&x, rate, 0.4, 0.75).unwrap();
assert!(
lufs == f64::NEG_INFINITY,
"silence should return -inf LUFS (got {lufs})"
);
}
#[test]
fn integrated_loudness_stereo_accepts_2d_and_adds_3lu() {
let rate = 48_000u32;
let mono = sine_mono(1000.0, 0.5, rate, 3.0);
let mono_buf = mono.try_clone().unwrap().to_vec::<f32>().unwrap();
let n = mono_buf.len();
let mut stereo_buf: Vec<f32> = Vec::with_capacity(n * 2);
for &s in &mono_buf {
stereo_buf.push(s);
stereo_buf.push(s);
}
let stereo = Array::from_slice::<f32>(&stereo_buf, &[n as i32, 2i32]).unwrap();
let lufs_mono = integrated_loudness(&mono, rate, 0.4, 0.75).unwrap();
let lufs_stereo = integrated_loudness(&stereo, rate, 0.4, 0.75).unwrap();
let delta = lufs_stereo - lufs_mono;
assert!(
(delta - 3.0103).abs() < 0.05,
"duplicating a mono signal to stereo (same content, gains [1, 1]) \
should add ~3 LU (got delta {delta} = {lufs_stereo} - {lufs_mono})"
);
}
#[test]
fn integrated_loudness_block_count_uses_round_ties_even() {
let rate = 48_000u32;
let n = 31_200usize;
debug_assert_eq!(n, (0.65_f64 * f64::from(rate)) as usize);
let loud_start = 28_800usize;
let mut buf: Vec<f32> = vec![0.0_f32; n];
let two_pi_freq = 2.0 * std::f64::consts::PI * 1000.0;
let rate_f64 = f64::from(rate);
for (i, s) in buf.iter_mut().enumerate().skip(loud_start) {
let t = i as f64 / rate_f64;
*s = 0.5_f32 * (two_pi_freq * t).sin() as f32;
}
let x = Array::from_slice::<f32>(&buf, &[n as i32]).unwrap();
let lufs = integrated_loudness(&x, rate, 0.4, 0.75).unwrap();
assert!(
lufs == f64::NEG_INFINITY,
"0.65 s clip @ 48 kHz must yield 3 blocks (round-ties-even); a silent \
signal with a loud tail only in the would-be 4th block must return \
-inf LUFS. Got {lufs} — a finite value means the block count \
regressed to 4 (f64::round instead of round_ties_even)"
);
}
#[test]
fn integrated_loudness_rejects_too_short_input() {
let rate = 48_000u32;
let x = sine_mono(1000.0, 0.5, rate, 0.1);
let res = integrated_loudness(&x, rate, 0.4, 0.75);
assert!(
matches!(res, Err(Error::OutOfRange(_))),
"audio shorter than block_size * rate must be rejected (got {res:?})"
);
}
#[test]
fn integrated_loudness_rejects_more_than_five_channels() {
let rate = 48_000u32;
let n = 24_000usize; let buf = vec![0.0_f32; n * 6];
let x = Array::from_slice::<f32>(&buf, &[n as i32, 6i32]).unwrap();
let res = integrated_loudness(&x, rate, 0.4, 0.75);
let Err(Error::OutOfRange(payload)) = &res else {
panic!("audio with >5 channels must be rejected with OutOfRange (got {res:?})");
};
assert_eq!(payload.value(), "6");
}
#[test]
fn integrated_loudness_rejects_3d_input() {
let rate = 48_000u32;
let buf = vec![0.0_f32; 24_000];
let x = Array::from_slice::<f32>(&buf, &[100i32, 60i32, 4i32]).unwrap();
let res = integrated_loudness(&x, rate, 0.4, 0.75);
assert!(
matches!(res, Err(Error::RankMismatch(_))),
"3-D input must be rejected (got {res:?})"
);
}
#[test]
fn integrated_loudness_rejects_invalid_block_params() {
let rate = 48_000u32;
let x = sine_mono(1000.0, 0.5, rate, 3.0);
assert!(matches!(
integrated_loudness(&x, rate, 0.4, 1.0),
Err(Error::OutOfRange(_))
));
assert!(matches!(
integrated_loudness(&x, rate, 0.4, -0.1),
Err(Error::OutOfRange(_))
));
assert!(matches!(
integrated_loudness(&x, rate, 0.0, 0.75),
Err(Error::OutOfRange(_))
));
assert!(matches!(
integrated_loudness(&x, 0, 0.4, 0.75),
Err(Error::OutOfRange(_))
));
}
#[test]
fn integrated_loudness_rejects_oversized_total_elements() {
let rate = 48_000u32;
let lazy_mono =
Array::zeros::<f32>(&[(crate::audio::io::MAX_DECODED_SAMPLES + 1) as i32]).unwrap();
let res_mono = integrated_loudness(&lazy_mono, rate, 0.4, 0.75);
assert!(
matches!(res_mono, Err(Error::CapExceeded(_))),
"1-D input above the per-channel cap must be rejected (got {res_mono:?})"
);
let n_per_chan = crate::audio::io::MAX_DECODED_SAMPLES / 5 + 1;
let lazy_5ch = Array::zeros::<f32>(&[n_per_chan as i32, 5i32]).unwrap();
let res_5ch = integrated_loudness(&lazy_5ch, rate, 0.4, 0.75);
assert!(
matches!(res_5ch, Err(Error::CapExceeded(_))),
"2-D input where per-channel count fits but total elements does not \
must be rejected by the TOTAL-elements cap (got {res_5ch:?})"
);
}
#[test]
fn integrated_loudness_rejects_pathological_overlap() {
let rate = 48_000u32;
let x = sine_mono(1000.0, 0.5, rate, 3.0);
let res = integrated_loudness(&x, rate, 0.4, 0.999_999_999_999);
assert!(
matches!(res, Err(Error::CapExceeded(_))),
"pathological overlap close to 1 must be rejected by the byte/work \
caps BEFORE any num_blocks-scaled allocation (got {res:?})"
);
let mono_buf = x.try_clone().unwrap().to_vec::<f32>().unwrap();
let n = mono_buf.len();
let mut stereo_buf: Vec<f32> = Vec::with_capacity(n * 2);
for &s in &mono_buf {
stereo_buf.push(s);
stereo_buf.push(s);
}
let stereo = Array::from_slice::<f32>(&stereo_buf, &[n as i32, 2i32]).unwrap();
let res_stereo = integrated_loudness(&stereo, rate, 0.4, 0.999_999_999_999);
assert!(
matches!(res_stereo, Err(Error::CapExceeded(_))),
"pathological-overlap stereo (byte cap factor n_channels) must be \
rejected (got {res_stereo:?})"
);
}
#[test]
fn integrated_loudness_rejects_overlap_just_below_old_element_cap() {
let rate = 48_000u32;
let x = sine_mono(1000.0, 0.5, rate, 3.0);
let res = integrated_loudness(&x, rate, 0.4, 0.999_999_90);
assert!(
matches!(res, Err(Error::CapExceeded(_))),
"overlap=0.99999990 (under old elements-only cap; over new byte/work \
caps) must be rejected BEFORE allocation (got {res:?})"
);
}
#[test]
fn integrated_loudness_rejects_work_cap_only_when_n_channels_counted() {
let rate = 16_000u32;
let n_samples = 161usize;
let n_channels = 5usize;
let buf = vec![0.0_f32; n_samples * n_channels];
let x = Array::from_slice::<f32>(&buf, &[n_samples as i32, n_channels as i32]).unwrap();
let res = integrated_loudness(&x, rate, 0.01, 0.999_999_987_5);
let Err(Error::CapExceeded(payload)) = &res else {
panic!(
"5-channel input with num_blocks * block_samples just under the cap \
but num_blocks * block_samples * n_channels above must be REJECTED \
by the n_channels-aware work cap (got {res:?})"
);
};
assert!(
payload.context().contains("total sample-visit work")
&& payload.context().contains("n_channels"),
"rejection must come from the n_channels-aware work cap (got: {})",
payload.context()
);
}
#[test]
fn integrated_loudness_one_khz_sine_matches_theoretical() {
let rate = 48_000u32;
let amp = 0.5_f32;
let x = sine_mono(1000.0, amp, rate, 3.0);
let lufs = integrated_loudness(&x, rate, 0.4, 0.75).unwrap();
let theoretical = -9.0656046890608_f64;
assert!(
(lufs - theoretical).abs() < 0.05,
"1 kHz sine @ amp=0.5 should be within ±0.05 LUFS of theoretical \
{theoretical} (got {lufs}, diff {})",
(lufs - theoretical).abs()
);
let normalized = normalize_loudness(&x, lufs, -23.0).unwrap();
let lufs_after = integrated_loudness(&normalized, rate, 0.4, 0.75).unwrap();
assert!(
(lufs_after - (-23.0)).abs() < 0.005,
"f64-end-to-end K-weighting must yield a tighter round-trip (±0.005 \
LUFS), got {lufs_after} (target -23.0, before {lufs})"
);
}
#[test]
fn normalize_loudness_rejects_non_finite_params() {
let rate = 48_000u32;
let x = sine_mono(1000.0, 0.5, rate, 1.0);
assert!(matches!(
normalize_loudness(&x, f64::NAN, -23.0),
Err(Error::OutOfRange(_))
));
assert!(matches!(
normalize_loudness(&x, -10.0, f64::INFINITY),
Err(Error::OutOfRange(_))
));
assert!(matches!(
normalize_loudness(&x, f64::NEG_INFINITY, -23.0),
Err(Error::OutOfRange(_))
));
}
#[test]
fn normalize_loudness_identity_when_target_eq_input() {
let rate = 48_000u32;
let x = sine_mono(1000.0, 0.5, rate, 1.0);
let original = x.try_clone().unwrap().to_vec::<f32>().unwrap();
let y = normalize_loudness(&x, -10.0, -10.0).unwrap();
let result = y.try_clone().unwrap().to_vec::<f32>().unwrap();
assert_eq!(result.len(), original.len());
for (i, (g, e)) in result.iter().zip(original.iter()).enumerate() {
assert!((g - e).abs() < 1e-7, "identity[{i}]: got {g}, want {e}");
}
}
#[test]
fn biquad_coefficients_normalize_a0_to_one() {
let (_b, a) = bs1770_biquad_coefficients(
4.0,
1.0 / std::f64::consts::SQRT_2,
1500.0,
48_000.0,
BiquadKind::HighShelf,
);
assert!(
(a[0] - 1.0).abs() < 1e-15,
"high-shelf a[0] must normalize to 1.0, got {}",
a[0]
);
let (_b, a) = bs1770_biquad_coefficients(0.0, 0.5, 38.0, 48_000.0, BiquadKind::HighPass);
assert!(
(a[0] - 1.0).abs() < 1e-15,
"high-pass a[0] must normalize to 1.0, got {}",
a[0]
);
}
#[test]
fn istft_cache_matches_free_istft_win_eq_nfft() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
for mode in [WindowPad::Center, WindowPad::Right] {
let spec = stft(&x, 8, 4, Some(8), mode).unwrap();
let one_shot = to_vec(&istft(&spec, None).unwrap());
let mut cache = ISTFTCache::new();
let cached = to_vec(&cache.istft(&spec, None).unwrap());
assert_eq!(one_shot.len(), cached.len(), "length mismatch ({mode:?})");
for (i, (a, b)) in one_shot.iter().zip(cached.iter()).enumerate() {
assert!(
(a - b).abs() < 1e-6,
"ISTFTCache vs istft[{i}] ({mode:?}): {a} vs {b}"
);
}
assert_eq!(cache.len(), 2, "expected 2 cached buffers after one call");
}
}
#[test]
fn istft_cache_center_short_window_round_trips() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
let spec = stft(&x, 8, 2, Some(4), WindowPad::Center).unwrap();
let mut cache = ISTFTCache::new();
let rec = to_vec(&cache.istft(&spec, Some(16)).unwrap());
assert_eq!(rec.len(), 16);
for (i, (g, e)) in rec.iter().zip(buf.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-5,
"ISTFTCache short-window round-trip[{i}]: got {g}, want {e}"
);
}
}
#[test]
fn istft_cache_reuses_buffers_across_same_geometry_spectra() {
let buf_a = signal_16();
let mut buf_b = signal_16();
buf_b.reverse(); let xa = Array::from_slice::<f32>(&buf_a, &[16i32]).unwrap();
let xb = Array::from_slice::<f32>(&buf_b, &[16i32]).unwrap();
let spec_a = stft(&xa, 8, 4, Some(8), WindowPad::Center).unwrap();
let spec_b = stft(&xb, 8, 4, Some(8), WindowPad::Center).unwrap();
let mut cache = ISTFTCache::new();
let ca = to_vec(&cache.istft(&spec_a, None).unwrap());
assert_eq!(cache.len(), 2, "first call should populate 2 buffers");
let cb = to_vec(&cache.istft(&spec_b, None).unwrap());
assert_eq!(
cache.len(),
2,
"same-geometry second call must REUSE buffers (no new entries)"
);
let fa = to_vec(&istft(&spec_a, None).unwrap());
let fb = to_vec(&istft(&spec_b, None).unwrap());
for (i, (g, e)) in ca.iter().zip(fa.iter()).enumerate() {
assert!((g - e).abs() < 1e-6, "cache A[{i}]: {g} vs {e}");
}
for (i, (g, e)) in cb.iter().zip(fb.iter()).enumerate() {
assert!((g - e).abs() < 1e-6, "cache B[{i}]: {g} vs {e}");
}
}
#[test]
fn istft_cache_clear_empties_and_rejects_right_short_window() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
let spec = stft(&x, 8, 4, Some(8), WindowPad::Center).unwrap();
let mut cache = ISTFTCache::new();
assert!(cache.is_empty());
let _ = cache.istft(&spec, None).unwrap();
assert!(!cache.is_empty());
cache.clear();
assert!(cache.is_empty(), "clear() must drop all cached buffers");
let spec_short = stft(&x, 8, 2, Some(4), WindowPad::Right).unwrap();
let mut cache2 = ISTFTCache::new();
assert!(matches!(
cache2.istft(&spec_short, None),
Err(Error::OutOfRange(_))
));
}
#[test]
fn istft_cache_center_zero_coverage_tail_rejects_like_free_istft() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
let spec = stft(&x, 8, 4, Some(8), WindowPad::Center).unwrap();
assert_eq!(spec.num_frames(), 5);
let t = (spec.num_frames() - 1) * spec.hop_length() + spec.n_fft();
assert_eq!(t, 24);
let pad = spec.n_fft() / 2;
let tail_len = t - pad; let free_tail = istft(&spec, Some(tail_len));
assert!(
matches!(free_tail, Err(Error::OutOfRange(_))),
"free istft must reject the zero-coverage tail, got {free_tail:?}"
);
let mut cache = ISTFTCache::new();
let cached_tail = cache.istft(&spec, Some(tail_len));
assert!(
matches!(cached_tail, Err(Error::OutOfRange(_))),
"ISTFTCache must reject the zero-coverage tail IDENTICALLY to free istft \
(not divide by a floor + emit corrupt audio), got {cached_tail:?}"
);
let free_ok = to_vec(&istft(&spec, None).unwrap());
let mut cache_ok = ISTFTCache::new();
let cached_ok = to_vec(&cache_ok.istft(&spec, None).unwrap());
assert_eq!(free_ok.len(), cached_ok.len(), "covered-length mismatch");
for (i, (a, b)) in free_ok.iter().zip(cached_ok.iter()).enumerate() {
assert!(
(a - b).abs() < 1e-6,
"covered ISTFTCache vs istft[{i}]: {a} vs {b}"
);
}
let warm_reject = cache.istft(&spec, Some(tail_len));
assert!(
matches!(warm_reject, Err(Error::OutOfRange(_))),
"warm-cache tail request must STILL reject (guard is per-call), got {warm_reject:?}"
);
}
#[test]
fn istft_cache_center_false_uncovered_head_rejects_like_free_istft() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
let spec = stft(&x, 8, 4, Some(8), WindowPad::Center).unwrap();
let spec_no_center = Spectrum::from_parts(
spec.data_ref().try_clone().unwrap(),
8,
4,
8,
WindowPad::Center,
false, )
.unwrap();
for len in [None, Some(10usize)] {
let free_res = istft(&spec_no_center, len);
assert!(
matches!(free_res, Err(Error::OutOfRange(_))),
"free istft center=false head (len={len:?}) must reject, got {free_res:?}"
);
let mut cache = ISTFTCache::new();
let cached_res = cache.istft(&spec_no_center, len);
assert!(
matches!(cached_res, Err(Error::OutOfRange(_))),
"ISTFTCache center=false head (len={len:?}) must reject IDENTICALLY to \
free istft, got {cached_res:?}"
);
}
let spec_cov = stft(&x, 8, 2, Some(8), WindowPad::Center).unwrap();
let cov = Spectrum::from_parts(
spec_cov.data_ref().try_clone().unwrap(),
8,
2,
8,
WindowPad::Center,
false,
)
.unwrap();
for len in [Some(6usize), Some(8usize), Some(12usize), None] {
let free_res = istft(&cov, len);
let mut cache = ISTFTCache::new();
let cached_res = cache.istft(&cov, len);
match (free_res, cached_res) {
(Ok(f), Ok(c)) => {
let fv = to_vec(&f);
let cv = to_vec(&c);
assert_eq!(fv.len(), cv.len(), "len={len:?} length mismatch");
for (i, (a, b)) in fv.iter().zip(cv.iter()).enumerate() {
assert!(
(a - b).abs() < 1e-6,
"center=false covered ISTFTCache vs istft[{i}] (len={len:?}): {a} vs {b}"
);
}
}
(Err(_), Err(_)) => { }
(f, c) => panic!("center=false len={len:?}: free and cache DISAGREE: {f:?} vs {c:?}"),
}
}
}
#[test]
fn normalize_peak_brings_peak_to_target_dbfs() {
let data = Array::from_slice::<f32>(&[0.5, -0.25, 0.1], &[3]).unwrap();
let out0 = normalize_peak(&data, 0.0).unwrap();
let v0 = to_vec(&out0);
let peak0 = v0.iter().map(|x| x.abs()).fold(0.0_f32, f32::max);
assert!(
(peak0 - 1.0).abs() < 1e-6,
"0 dBFS peak: got {peak0}, want 1.0"
);
for (g, e) in v0.iter().zip([1.0_f32, -0.5, 0.2].iter()) {
assert!((g - e).abs() < 1e-6, "0 dBFS value: got {g}, want {e}");
}
let out6 = normalize_peak(&data, -6.0).unwrap();
let v6 = to_vec(&out6);
let peak6 = v6.iter().map(|x| x.abs()).fold(0.0_f32, f32::max);
let want6 = 10.0_f32.powf(-6.0 / 20.0);
assert!(
(peak6 - want6).abs() < 1e-5,
"-6 dBFS peak: got {peak6}, want {want6}"
);
}
#[test]
fn normalize_peak_2d_input_uses_global_peak() {
let data = Array::from_slice::<f32>(&[0.2, -0.8, 0.4, 0.1], &[2, 2]).unwrap();
let out = normalize_peak(&data, 0.0).unwrap();
assert_eq!(out.shape(), vec![2, 2], "shape must be preserved");
let v = to_vec(&out);
let peak = v.iter().map(|x| x.abs()).fold(0.0_f32, f32::max);
assert!(
(peak - 1.0).abs() < 1e-6,
"global peak should hit 1.0, got {peak}"
);
}
#[test]
fn normalize_peak_rejects_silence_and_nonfinite() {
let silence = Array::from_slice::<f32>(&[0.0, 0.0, 0.0], &[3]).unwrap();
assert!(matches!(
normalize_peak(&silence, 0.0),
Err(Error::OutOfRange(_))
));
let data = Array::from_slice::<f32>(&[0.5, 0.1], &[2]).unwrap();
assert!(matches!(
normalize_peak(&data, f64::NAN),
Err(Error::OutOfRange(_))
));
assert!(matches!(
normalize_peak(&data, f64::INFINITY),
Err(Error::OutOfRange(_))
));
}
#[test]
fn normalize_peak_rejects_overflowing_gain_from_finite_input() {
let data = Array::from_slice::<f32>(&[0.5, -0.25, 0.1], &[3]).unwrap();
assert!(
matches!(normalize_peak(&data, 1e30), Err(Error::NonFiniteScalar(_))),
"huge finite target_peak_db must be rejected (target_linear overflows f32)"
);
let tiny = f32::from_bits(1); assert!(
tiny > 0.0 && tiny.is_finite(),
"tiny must be a finite nonzero"
);
let subnormal_peak = Array::from_slice::<f32>(&[tiny, 0.0, -tiny], &[3]).unwrap();
assert!(
matches!(
normalize_peak(&subnormal_peak, 0.0),
Err(Error::NonFiniteScalar(_))
),
"subnormal nonzero peak that overflows the gain must be rejected"
);
let ok = normalize_peak(&data, -3.0).unwrap();
for v in to_vec(&ok) {
assert!(
v.is_finite(),
"normal target must keep samples finite, got {v}"
);
}
}
#[test]
fn mel_filter_bank_cached_matches_uncached() {
clear_mel_filter_cache();
let plain = mel_filter_bank(80, 400, 16_000, 0.0, None).unwrap();
let cached = mel_filter_bank_cached(80, 400, 16_000, 0.0, None).unwrap();
let p = to_vec(&plain);
let c = to_vec(&cached);
assert_eq!(p, c, "cached mel bank must match uncached value-for-value");
clear_mel_filter_cache();
}
#[test]
fn mel_filter_bank_cached_hit_returns_same_values() {
clear_mel_filter_cache();
let first = mel_filter_bank_cached(80, 400, 16_000, 0.0, None).unwrap();
let second = mel_filter_bank_cached(80, 400, 16_000, 0.0, None).unwrap();
assert_eq!(to_vec(&first), to_vec(&second));
clear_mel_filter_cache();
}
#[test]
fn mel_filter_bank_cached_distinguishes_keys() {
clear_mel_filter_cache();
let a = mel_filter_bank_cached(80, 400, 16_000, 0.0, None).unwrap();
let b = mel_filter_bank_cached(80, 400, 22_050, 0.0, None).unwrap();
let c = mel_filter_bank_cached(40, 400, 16_000, 0.0, None).unwrap();
let d = mel_filter_bank_cached(80, 512, 16_000, 0.0, None).unwrap();
let e = mel_filter_bank_cached(80, 400, 16_000, 80.0, None).unwrap();
let f = mel_filter_bank_cached(80, 400, 16_000, 0.0, Some(7_500.0)).unwrap();
let av = to_vec(&a);
for (name, other) in [
("sample_rate", &b),
("n_mels", &c),
("n_fft", &d),
("f_min", &e),
("f_max", &f),
] {
let ov = to_vec(other);
assert_ne!(av, ov, "{name} key collapsed into same cache entry");
}
clear_mel_filter_cache();
}
#[test]
fn mel_filter_bank_cached_evicts_lru_at_cap() {
clear_mel_filter_cache();
let cap = super::MEL_FILTER_CACHE_CAP;
let mut first_bank: Option<Vec<f32>> = None;
for i in 0..(cap + 1) {
let sr = 16_000u32 + (i as u32) * 1_000;
let bank = mel_filter_bank_cached(40, 400, sr, 0.0, None).unwrap();
if i == 0 {
first_bank = Some(to_vec(&bank));
}
}
let refetched = mel_filter_bank_cached(40, 400, 16_000, 0.0, None).unwrap();
assert_eq!(
to_vec(&refetched),
first_bank.unwrap(),
"evicted key must rebuild value-equal bank on re-request"
);
clear_mel_filter_cache();
}
#[test]
fn mel_filter_bank_cached_propagates_errors() {
clear_mel_filter_cache();
assert!(matches!(
mel_filter_bank_cached(80, 0, 16_000, 0.0, None),
Err(Error::InvariantViolation(_))
));
let ok = mel_filter_bank_cached(80, 400, 16_000, 0.0, None);
assert!(ok.is_ok());
clear_mel_filter_cache();
}
#[test]
fn mel_filter_bank_cached_precise_matches_uncached_precise() {
clear_mel_filter_cache();
let plain = mel_filter_bank_with(80, 400, 16_000, 0.0, None, MelPrecision::Precise).unwrap();
let cached =
mel_filter_bank_cached_with(80, 400, 16_000, 0.0, None, MelPrecision::Precise).unwrap();
assert_eq!(
to_vec(&plain),
to_vec(&cached),
"cached precise bank must match the uncached precise bank value-for-value"
);
clear_mel_filter_cache();
}
#[test]
fn mel_filter_bank_cached_precision_does_not_collide() {
clear_mel_filter_cache();
let std_bank =
mel_filter_bank_cached_with(80, 400, 16_000, 0.0, None, MelPrecision::Standard).unwrap();
let precise =
mel_filter_bank_cached_with(80, 400, 16_000, 0.0, None, MelPrecision::Precise).unwrap();
let len = MEL_FILTER_CACHE.with(|cell| cell.borrow().len());
assert_eq!(
len, 2,
"standard and precise banks for identical params must occupy distinct cache slots"
);
assert_ne!(
to_vec(&std_bank),
to_vec(&precise),
"precise cache hit must not alias the standard bank"
);
let std_again =
mel_filter_bank_cached_with(80, 400, 16_000, 0.0, None, MelPrecision::Standard).unwrap();
let precise_again =
mel_filter_bank_cached_with(80, 400, 16_000, 0.0, None, MelPrecision::Precise).unwrap();
assert_eq!(to_vec(&std_bank), to_vec(&std_again));
assert_eq!(to_vec(&precise), to_vec(&precise_again));
let len2 = MEL_FILTER_CACHE.with(|cell| cell.borrow().len());
assert_eq!(len2, 2, "repeat fetches must hit, not insert new entries");
clear_mel_filter_cache();
}
#[test]
fn mel_filter_cache_key_distinguishes_precision() {
let std_key = MelFilterCacheKey::new(80, 400, 16_000, 0.0, None, MelPrecision::Standard);
let precise_key = MelFilterCacheKey::new(80, 400, 16_000, 0.0, None, MelPrecision::Precise);
assert_ne!(
std_key, precise_key,
"cache keys differing only in precision must be unequal"
);
}
#[test]
fn mel_filter_bank_cached_shorthand_is_standard() {
clear_mel_filter_cache();
let shorthand = mel_filter_bank_cached(80, 400, 16_000, 0.0, None).unwrap();
let with_std =
mel_filter_bank_cached_with(80, 400, 16_000, 0.0, None, MelPrecision::Standard).unwrap();
assert_eq!(to_vec(&shorthand), to_vec(&with_std));
let len = MEL_FILTER_CACHE.with(|cell| cell.borrow().len());
assert_eq!(len, 1, "shorthand and Standard must share one cache slot");
clear_mel_filter_cache();
}
#[test]
fn dsp_named_constants_match_mlx_audio_literals() {
assert_eq!(super::MEL_HZ_DIV, 2595.0_f32);
assert_eq!(super::MEL_HZ_BREAK, 700.0_f32);
assert_eq!(super::LOG_FLOOR_WHISPER, 1e-10_f32);
assert_eq!(super::LOG_FLOOR_KALDI, 1e-8_f32);
assert_eq!(super::BS1770_LOUDNESS_OFFSET_LUFS, -0.691_f64);
}
#[test]
fn log_floor_variants_resolve_named_constants() {
assert_eq!(LogFloor::Whisper.value(), super::LOG_FLOOR_WHISPER);
assert_eq!(LogFloor::Kaldi.value(), super::LOG_FLOOR_KALDI);
assert_eq!(LogFloor::Custom(1e-6).value(), 1e-6);
assert_eq!(LogFloor::Custom(f32::NAN).value(), f32::MIN_POSITIVE);
assert_eq!(LogFloor::Custom(-1.0).value(), f32::MIN_POSITIVE);
assert_eq!(LogFloor::Custom(0.0).value(), f32::MIN_POSITIVE);
}
#[test]
fn mel_precision_surface() {
assert_eq!(MelPrecision::default(), MelPrecision::Standard);
assert_eq!(MelPrecision::Standard.as_str(), "standard");
assert_eq!(MelPrecision::Precise.as_str(), "precise");
assert_eq!(MelPrecision::Precise.to_string(), "precise");
assert!(MelPrecision::Standard.is_standard());
assert!(MelPrecision::Precise.is_precise());
assert!(!MelPrecision::Standard.is_precise());
}
#[test]
fn stft_config_default_matches_mlx_audio_defaults() {
let cfg = StftConfig::default();
assert!(cfg.center());
assert_eq!(cfg.pad_mode(), PadMode::Reflect);
}
#[test]
fn stft_with_config_default_matches_bare_stft() {
let n = 256usize;
let samples: Vec<f32> = (0..n).map(|i| (i as f32 * 0.01).sin()).collect();
let arr = Array::from_slice::<f32>(&samples, &[n as i32]).unwrap();
let bare = stft(&arr, 64, 16, None, WindowPad::Right).unwrap();
let with_cfg =
stft_with_config(&arr, 64, 16, None, WindowPad::Right, &StftConfig::default()).unwrap();
assert_eq!(bare.n_fft(), with_cfg.n_fft());
assert_eq!(bare.hop_length(), with_cfg.hop_length());
assert_eq!(bare.win_length(), with_cfg.win_length());
assert_eq!(bare.window_pad(), with_cfg.window_pad());
assert_eq!(bare.center(), with_cfg.center());
assert_eq!(bare.num_frames(), with_cfg.num_frames());
assert_eq!(bare.n_freqs(), with_cfg.n_freqs());
}
#[test]
fn stft_aligned_carries_center_false() {
let n = 256usize;
let samples: Vec<f32> = (0..n).map(|i| (i as f32 * 0.02).cos()).collect();
let arr = Array::from_slice::<f32>(&samples, &[n as i32]).unwrap();
let aligned = stft_aligned(&arr, 64, 16, None, WindowPad::Right).unwrap();
let centered = stft(&arr, 64, 16, None, WindowPad::Right).unwrap();
assert!(!aligned.center(), "stft_aligned must carry center == false");
assert!(centered.center(), "stft must carry center == true");
assert!(
centered.num_frames() > aligned.num_frames(),
"centered={} aligned={}",
centered.num_frames(),
aligned.num_frames()
);
}
#[test]
fn stft_aligned_rejects_too_short_input() {
let samples: Vec<f32> = (0..32).map(|i| i as f32).collect();
let arr = Array::from_slice::<f32>(&samples, &[32]).unwrap();
assert!(matches!(
stft_aligned(&arr, 64, 16, None, WindowPad::Right),
Err(Error::OutOfRange(_))
));
}
#[test]
fn reflect_pad_1d_zero_padding_returns_unchanged() {
let samples: Vec<f32> = (0..16).map(|i| i as f32).collect();
let arr = Array::from_slice::<f32>(&samples, &[16]).unwrap();
let padded = reflect_pad_1d(&arr, 0).unwrap();
assert_eq!(to_vec(&padded), samples);
}
#[test]
fn reflect_pad_1d_matches_python_reference_construction() {
let samples: Vec<f32> = (0..8).map(|i| i as f32).collect();
let arr = Array::from_slice::<f32>(&samples, &[8]).unwrap();
let padded = reflect_pad_1d(&arr, 3).unwrap();
let v = to_vec(&padded);
let expected: Vec<f32> = vec![
3.0, 2.0, 1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 6.0, 5.0, 4.0, ];
assert_eq!(v, expected);
}
#[test]
fn window_pad_as_str_and_display_both_variants() {
assert_eq!(WindowPad::Right.as_str(), "right");
assert_eq!(WindowPad::Center.as_str(), "center");
assert_eq!(WindowPad::Right.to_string(), "right");
assert_eq!(WindowPad::Center.to_string(), "center");
assert!(WindowPad::Right.is_right());
assert!(WindowPad::Center.is_center());
assert!(!WindowPad::Right.is_center());
assert_eq!(WindowPad::default(), WindowPad::Right);
}
#[test]
fn pad_mode_as_str_and_display() {
assert_eq!(PadMode::Reflect.as_str(), "reflect");
assert_eq!(PadMode::Reflect.to_string(), "reflect");
assert!(PadMode::Reflect.is_reflect());
assert_eq!(PadMode::default(), PadMode::Reflect);
}
#[test]
fn spectrum_from_parts_rejects_zero_frames() {
let empty = Array::zeros::<f32>(&[0i32, 5i32])
.unwrap()
.astype(Dtype::Complex64)
.unwrap();
let res = Spectrum::from_parts(empty, 8, 4, 8, WindowPad::Center, true);
assert!(
matches!(res, Err(Error::InvariantViolation(_))),
"zero-frame spectrum must be rejected by the num_frames invariant, got {res:?}"
);
}
#[test]
fn place_window_rejects_bad_shape_len_and_win_gt_nfft() {
let w_2d = Array::from_slice::<f32>(&[1.0_f32, 2.0, 3.0, 4.0], &[2i32, 2i32]).unwrap();
assert!(matches!(
place_window("test", &w_2d, 4, 8, WindowPad::Right),
Err(Error::RankMismatch(_))
));
let w_short = Array::from_slice::<f32>(&[1.0_f32, 2.0, 3.0], &[3i32]).unwrap();
let res = place_window("test", &w_short, 5, 8, WindowPad::Right);
let Err(Error::LengthMismatch(payload)) = &res else {
panic!("length disagreement must be LengthMismatch, got {res:?}");
};
assert_eq!(payload.expected(), 5);
assert_eq!(payload.actual(), 3);
let w_ok = Array::from_slice::<f32>(&[1.0_f32; 8], &[8i32]).unwrap();
assert!(matches!(
place_window("test", &w_ok, 8, 4, WindowPad::Right),
Err(Error::OutOfRange(_))
));
}
#[test]
fn place_window_pads_right_and_center_closed_form() {
let w = Array::from_slice::<f32>(&[1.0_f32, 2.0], &[2i32]).unwrap();
let full = place_window("t", &w, 2, 2, WindowPad::Right).unwrap();
assert_eq!(to_vec(&full), vec![1.0, 2.0]);
let right = place_window("t", &w, 2, 4, WindowPad::Right).unwrap();
assert_eq!(to_vec(&right), vec![1.0, 2.0, 0.0, 0.0]);
let center = place_window("t", &w, 2, 4, WindowPad::Center).unwrap();
assert_eq!(to_vec(¢er), vec![0.0, 1.0, 2.0, 0.0]);
}
#[test]
fn frame_window_center_padded_closed_form() {
let fw = frame_window(4, 6, WindowPad::Center).unwrap();
let v = to_vec(&fw);
let expected = [0.0_f32, 0.0, 0.75, 0.75, 0.0, 0.0];
assert_eq!(v.len(), 6);
for (i, (g, e)) in v.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < WIN_TOL,
"frame_window[{i}]: got {g}, want {e}"
);
}
}
#[test]
fn windows_reject_n_above_decoded_cap() {
let over = crate::audio::io::MAX_DECODED_SAMPLES + 1;
for r in [
hann_window(over),
hamming_window(over),
blackman_window(over),
bartlett_window(over),
] {
let Err(Error::CapExceeded(payload)) = &r else {
panic!("n above the decoded-sample cap must be CapExceeded, got {r:?}");
};
assert_eq!(payload.observed(), over as u64);
assert_eq!(payload.cap(), crate::audio::io::MAX_DECODED_SAMPLES as u64);
}
}
#[test]
fn reflect_pad_1d_rejects_non_1d() {
let arr_2d = Array::from_slice::<f32>(&[1.0_f32, 2.0, 3.0, 4.0], &[2i32, 2i32]).unwrap();
assert!(matches!(
reflect_pad_1d(&arr_2d, 1),
Err(Error::RankMismatch(_))
));
}
#[test]
fn reflect_pad_1d_rejects_padding_too_large() {
let arr = Array::from_slice::<f32>(&[1.0_f32, 2.0, 3.0], &[3i32]).unwrap();
assert!(matches!(reflect_pad_1d(&arr, 3), Err(Error::OutOfRange(_))));
}
#[test]
fn stft_rejects_zero_win_length_and_non_1d_input() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
assert!(matches!(
stft(&x, 8, 4, Some(0), WindowPad::Right),
Err(Error::InvariantViolation(_))
));
let x_2d = Array::from_slice::<f32>(&buf, &[4i32, 4i32]).unwrap();
assert!(matches!(
stft(&x_2d, 8, 4, None, WindowPad::Right),
Err(Error::RankMismatch(_))
));
assert!(matches!(
stft(&x, 8, 0, None, WindowPad::Right),
Err(Error::InvariantViolation(_))
));
assert!(matches!(
stft(&x, 0, 4, None, WindowPad::Right),
Err(Error::InvariantViolation(_))
));
}
#[test]
fn stft_rejects_input_too_short_for_one_frame() {
let tiny = Array::from_slice::<f32>(&[0.1_f32, 0.2, 0.3], &[3i32]).unwrap();
assert!(matches!(
stft(&tiny, 64, 16, None, WindowPad::Right),
Err(Error::OutOfRange(_))
));
}
#[test]
fn istft_center_false_length_gt_t_rejected() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
let spec = stft(&x, 8, 4, Some(8), WindowPad::Center).unwrap();
let spec_no_center = Spectrum::from_parts(
spec.data_ref().try_clone().unwrap(),
8,
4,
8,
WindowPad::Center,
false,
)
.unwrap();
assert!(matches!(
istft(&spec_no_center, Some(1000)),
Err(Error::OutOfRange(_))
));
}
#[test]
fn istft_cache_rejects_length_out_of_range_both_center_modes() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
let spec = stft(&x, 8, 4, Some(8), WindowPad::Center).unwrap();
let mut cache = ISTFTCache::new();
assert!(matches!(
cache.istft(&spec, Some(1000)),
Err(Error::OutOfRange(_))
));
let spec_no_center = Spectrum::from_parts(
spec.data_ref().try_clone().unwrap(),
8,
4,
8,
WindowPad::Center,
false,
)
.unwrap();
let mut cache2 = ISTFTCache::new();
assert!(matches!(
cache2.istft(&spec_no_center, Some(1000)),
Err(Error::OutOfRange(_))
));
}
#[test]
fn istft_cache_default_is_empty() {
let cache = ISTFTCache::default();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
}
#[test]
fn mel_filter_bank_rejects_invalid_params() {
assert!(matches!(
mel_filter_bank(0, 400, 16_000, 0.0, None),
Err(Error::InvariantViolation(_))
));
assert!(matches!(
mel_filter_bank(80, 400, 0, 0.0, None),
Err(Error::InvariantViolation(_))
));
assert!(matches!(
mel_filter_bank(80, 400, 16_000, -1.0, None),
Err(Error::OutOfRange(_))
));
assert!(matches!(
mel_filter_bank(80, 400, 16_000, 1000.0, Some(1000.0)),
Err(Error::OutOfRange(_))
));
}
#[test]
fn mel_filter_bank_single_triangle_closed_form() {
let hz_to_mel = |hz: f64| 2595.0 * (1.0 + hz / 700.0).log10();
let mel_to_hz = |mel: f64| 700.0 * (10f64.powf(mel / 2595.0) - 1.0);
let m_max = hz_to_mel(4.0);
let fc = mel_to_hz(m_max / 2.0); let mid = (2.0 / fc).min(2.0 / (4.0 - fc)); let expected = [0.0_f32, mid as f32, 0.0_f32];
let bank = mel_filter_bank(1, 4, 8, 0.0, None).unwrap();
assert_eq!(
bank.shape(),
vec![1, 3],
"bank shape must be (n_mels, n_freqs)"
);
let v = to_vec(&bank);
for (i, (g, e)) in v.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 3e-5,
"mel_filter_bank[{i}]: got {g}, want {e} (fc={fc})"
);
}
}
#[test]
fn mel_filter_bank_precise_single_triangle_closed_form() {
let hz_to_mel = |hz: f64| 2595.0 * (1.0 + hz / 700.0).log10();
let mel_to_hz = |mel: f64| 700.0 * (10f64.powf(mel / 2595.0) - 1.0);
let m_max = hz_to_mel(4.0);
let fc = mel_to_hz(m_max / 2.0);
let mid = (2.0 / fc).min(2.0 / (4.0 - fc));
let expected = [0.0_f32, mid as f32, 0.0_f32];
let bank = mel_filter_bank_with(1, 4, 8, 0.0, None, MelPrecision::Precise).unwrap();
assert_eq!(bank.shape(), vec![1, 3]);
let v = to_vec(&bank);
for (i, (g, e)) in v.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-5,
"precise mel_filter_bank[{i}]: got {g}, want {e}"
);
}
}
#[test]
fn hz_mel_round_trip_and_known_value() {
let mel_1000_ref = 2595.0_f32 * (1.0_f32 + 1000.0 / 700.0).log10();
let mel_1000 = hz_to_mel(1000.0);
assert!(
(mel_1000 - mel_1000_ref).abs() < 1e-2,
"hz_to_mel(1000) = {mel_1000}, ref {mel_1000_ref}"
);
let back = mel_to_hz(hz_to_mel(1000.0));
assert!((back - 1000.0).abs() < 0.1, "hz->mel->hz: got {back}");
let back_f64 = mel_to_hz_f64(hz_to_mel_f64(1000.0));
assert!(
(back_f64 - 1000.0).abs() < 1e-6,
"f64 hz->mel->hz: got {back_f64}"
);
}
#[test]
fn log_mel_spectrogram_applies_floor_on_silence() {
let zeros = vec![0.0_f32; 64];
let x = Array::from_slice::<f32>(&zeros, &[64i32]).unwrap();
let n_fft = 16usize;
let hop = 8usize;
let n_mels = 4usize;
let sr = 16_000u32;
let lm = to_vec(&log_mel_spectrogram(&x, n_fft, hop, None, n_mels, sr, 0.0, None).unwrap());
let want_default = (1e-10_f32).ln();
assert!(!lm.is_empty(), "log-mel must be non-empty");
for (i, &g) in lm.iter().enumerate() {
assert!(
(g - want_default).abs() < 1e-3,
"log_mel[{i}] on silence must equal log(1e-10) = {want_default}, got {g}"
);
}
let custom = LogFloor::Custom(1e-3);
let lm_c =
to_vec(&log_mel_spectrogram_with(&x, n_fft, hop, None, n_mels, sr, 0.0, None, custom).unwrap());
let want_custom = (1e-3_f32).ln();
for (i, &g) in lm_c.iter().enumerate() {
assert!(
(g - want_custom).abs() < 1e-4,
"log_mel_with(Custom(1e-3))[{i}] on silence must equal log(1e-3) = {want_custom}, got {g}"
);
}
}
#[test]
fn biquad_kind_as_str_both_variants() {
assert_eq!(BiquadKind::HighShelf.as_str(), "high_shelf");
assert_eq!(BiquadKind::HighPass.as_str(), "high_pass");
assert!(BiquadKind::HighShelf.is_high_shelf());
assert!(BiquadKind::HighPass.is_high_pass());
}
#[test]
fn integrated_loudness_rejects_zero_channels() {
let rate = 48_000u32;
let x = Array::zeros::<f32>(&[24_000i32, 0i32]).unwrap();
assert!(matches!(
integrated_loudness(&x, rate, 0.4, 0.75),
Err(Error::EmptyInput(_))
));
}
#[test]
fn integrated_loudness_rejects_sub_sample_block() {
let rate = 48_000u32;
let x = sine_mono(1000.0, 0.5, rate, 1.0);
let res = integrated_loudness(&x, rate, 1e-9, 0.75);
assert!(
matches!(res, Err(Error::OutOfRange(_))),
"a block smaller than one sample must be rejected, got {res:?}"
);
}
#[test]
fn normalize_peak_rejects_empty_input() {
let empty = Array::zeros::<f32>(&[0i32]).unwrap();
assert!(matches!(
normalize_peak(&empty, 0.0),
Err(Error::EmptyInput(_))
));
}
#[test]
fn normalize_peak_rejects_non_finite_input_sample() {
let with_inf = Array::from_slice::<f32>(&[0.5_f32, f32::INFINITY, -0.25], &[3i32]).unwrap();
assert!(
matches!(
normalize_peak(&with_inf, 0.0),
Err(Error::NonFiniteScalar(_))
),
"an infinite input sample must be rejected via the current-peak finiteness guard"
);
let with_nan = Array::from_slice::<f32>(&[0.5_f32, f32::NAN, -0.25], &[3i32]).unwrap();
assert!(matches!(
normalize_peak(&with_nan, 0.0),
Err(Error::NonFiniteScalar(_))
));
}
#[test]
fn stft_aligned_single_frame_dc_matches_closed_form_dft() {
let dc = [1.0_f32, 1.0, 1.0, 1.0];
let x = Array::from_slice::<f32>(&dc, &[4i32]).unwrap();
let spec = stft_aligned(&x, 4, 4, Some(4), WindowPad::Right).unwrap();
assert!(!spec.center(), "stft_aligned must carry center == false");
assert_eq!(spec.num_frames(), 1, "n_fft samples, hop=n_fft ⇒ one frame");
assert_eq!(
spec.data_ref().shape(),
vec![1, 3],
"(num_frames, n_fft/2+1)"
);
let mag = to_vec(&spec.data_ref().abs().unwrap());
let expected = [1.5_f32, 0.75 * std::f32::consts::SQRT_2, 0.0];
assert_eq!(mag.len(), 3);
for (i, (g, e)) in mag.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-5,
"aligned DC |bin{i}|: got {g}, want {e} (diff {})",
(g - e).abs()
);
}
}
#[test]
fn stft_aligned_data_differs_from_centered_stft() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
let aligned = stft_aligned(&x, 8, 4, Some(8), WindowPad::Right).unwrap();
let centered = stft(&x, 8, 4, Some(8), WindowPad::Right).unwrap();
let a = to_vec(&aligned.data_ref().abs().unwrap());
let c = to_vec(¢ered.data_ref().abs().unwrap());
let n_freqs = aligned.n_freqs();
assert_eq!(n_freqs, 5);
let max_diff = a[..n_freqs]
.iter()
.zip(c[..n_freqs].iter())
.map(|(g, e)| (g - e).abs())
.fold(0.0_f32, f32::max);
assert!(
max_diff > 1e-4,
"aligned frame-0 spectrum must differ from the centered (reflect-padded) \
frame-0 spectrum (max diff was {max_diff})"
);
}
#[test]
fn istft_on_stft_aligned_rejects_zero_coverage_head() {
let buf = signal_16();
let x = Array::from_slice::<f32>(&buf, &[16i32]).unwrap();
let aligned = stft_aligned(&x, 8, 4, Some(8), WindowPad::Right).unwrap();
assert!(!aligned.center());
for len in [None, Some(6usize)] {
let res = istft(&aligned, len);
assert!(
matches!(res, Err(Error::OutOfRange(_))),
"center=false istft (length={len:?}) on real stft_aligned data includes \
the zero-coverage OLA index 0 and must hit the coverage guard, got {res:?}"
);
}
}
#[test]
fn istft_single_centered_frame_empty_region_is_zero_length() {
let sig = [0.2_f32, -0.4, 0.6, -0.1, 0.3];
let x = Array::from_slice::<f32>(&sig, &[5i32]).unwrap();
let spec = stft(&x, 8, 6, Some(8), WindowPad::Center).unwrap();
assert_eq!(spec.num_frames(), 1, "this geometry must produce ONE frame");
let rec = istft(&spec, None).unwrap();
assert_eq!(
rec.size(),
0,
"single centered frame ⇒ empty centered region"
);
assert_eq!(
rec.shape(),
vec![0],
"empty region must be a 0-length 1-D array"
);
let rec2 = istft(&spec, Some(2)).unwrap();
assert_eq!(
rec2.size(),
2,
"explicit length=2 on a single frame ⇒ 2 samples"
);
}