use crate::toc::Bandwidth;
use crate::Error;
pub fn interp_phase_samples(bandwidth: Bandwidth) -> Result<usize, Error> {
Ok(match bandwidth {
Bandwidth::Nb => 64,
Bandwidth::Mb => 96,
Bandwidth::Wb => 128,
_ => return Err(Error::MalformedPacket),
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct StereoWeightsQ13 {
pub w0_q13: i32,
pub w1_q13: i32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StereoUnmixState {
mid_hist: [f32; 2],
side_hist: f32,
prev_weights: StereoWeightsQ13,
}
impl Default for StereoUnmixState {
fn default() -> Self {
Self::new()
}
}
impl StereoUnmixState {
pub fn new() -> Self {
StereoUnmixState {
mid_hist: [0.0; 2],
side_hist: 0.0,
prev_weights: StereoWeightsQ13::default(),
}
}
pub fn reset(&mut self) {
*self = Self::new();
}
pub fn prev_weights(&self) -> StereoWeightsQ13 {
self.prev_weights
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StereoFrame {
pub left: Vec<f32>,
pub right: Vec<f32>,
}
pub fn stereo_ms_to_lr(
bandwidth: Bandwidth,
mid: &[f32],
side: Option<&[f32]>,
weights: StereoWeightsQ13,
state: &mut StereoUnmixState,
) -> Result<StereoFrame, Error> {
let n2 = mid.len();
if n2 == 0 {
return Err(Error::MalformedPacket);
}
if let Some(s) = side {
if s.len() != n2 {
return Err(Error::MalformedPacket);
}
}
let n1 = interp_phase_samples(bandwidth)?;
let prev = state.prev_weights;
let w0_q13 = weights.w0_q13 as f32;
let w1_q13 = weights.w1_q13 as f32;
let prev_w0_q13 = prev.w0_q13 as f32;
let prev_w1_q13 = prev.w1_q13 as f32;
let n1_f = n1 as f32;
let w0_base = prev_w0_q13 / 8192.0;
let w1_base = prev_w1_q13 / 8192.0;
let w0_step = (w0_q13 - prev_w0_q13) / (8192.0 * n1_f);
let w1_step = (w1_q13 - prev_w1_q13) / (8192.0 * n1_f);
let mut left = vec![0.0f32; n2];
let mut right = vec![0.0f32; n2];
let mid_m2 = state.mid_hist[0]; let mid_m1 = state.mid_hist[1]; let side_m1 = if side.is_some() {
state.side_hist
} else {
0.0
};
for i in 0..n2 {
let ramp = (i.min(n1)) as f32;
let w0 = w0_base + ramp * w0_step;
let w1 = w1_base + ramp * w1_step;
let m_i = mid[i];
let m_i1 = if i >= 1 { mid[i - 1] } else { mid_m1 };
let m_i2 = match i {
0 => mid_m2,
1 => mid_m1,
_ => mid[i - 2],
};
let s_i1 = match side {
Some(s) if i >= 1 => s[i - 1],
Some(_) => side_m1,
None => 0.0,
};
let p0 = (m_i2 + 2.0 * m_i1 + m_i) / 4.0;
let l = (1.0 + w1) * m_i1 + s_i1 + w0 * p0;
let r = (1.0 - w1) * m_i1 - s_i1 - w0 * p0;
left[i] = l.clamp(-1.0, 1.0);
right[i] = r.clamp(-1.0, 1.0);
}
state.mid_hist = if n2 >= 2 {
[mid[n2 - 2], mid[n2 - 1]]
} else {
[mid_m1, mid[n2 - 1]]
};
state.side_hist = match side {
Some(s) => s[n2 - 1],
None => 0.0,
};
state.prev_weights = weights;
Ok(StereoFrame { left, right })
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) {
assert!(
(a - b).abs() < 1e-5,
"expected {b}, got {a} (delta {})",
(a - b).abs()
);
}
#[test]
fn interp_phase_table() {
assert_eq!(interp_phase_samples(Bandwidth::Nb).unwrap(), 64);
assert_eq!(interp_phase_samples(Bandwidth::Mb).unwrap(), 96);
assert_eq!(interp_phase_samples(Bandwidth::Wb).unwrap(), 128);
assert!(interp_phase_samples(Bandwidth::Swb).is_err());
assert!(interp_phase_samples(Bandwidth::Fb).is_err());
}
#[test]
fn state_starts_and_resets_zero() {
let mut s = StereoUnmixState::new();
assert_eq!(s.mid_hist, [0.0, 0.0]);
assert_eq!(s.side_hist, 0.0);
assert_eq!(s.prev_weights, StereoWeightsQ13::default());
s.mid_hist = [0.3, -0.2];
s.side_hist = 0.1;
s.prev_weights = StereoWeightsQ13 {
w0_q13: 5,
w1_q13: 7,
};
s.reset();
assert_eq!(s, StereoUnmixState::new());
}
#[test]
fn rejects_empty_and_mismatched() {
let mut s = StereoUnmixState::new();
assert!(stereo_ms_to_lr(
Bandwidth::Wb,
&[],
None,
StereoWeightsQ13::default(),
&mut s
)
.is_err());
let mid = vec![0.0f32; 80];
let side = vec![0.0f32; 79];
assert!(stereo_ms_to_lr(
Bandwidth::Wb,
&mid,
Some(&side),
StereoWeightsQ13::default(),
&mut s
)
.is_err());
}
#[test]
fn zero_weights_no_side_is_delayed_mono() {
let mut s = StereoUnmixState::new();
let mid: Vec<f32> = vec![0.1, 0.2, 0.3, 0.4];
let out = stereo_ms_to_lr(
Bandwidth::Wb,
&mid,
None,
StereoWeightsQ13::default(),
&mut s,
)
.unwrap();
let expect = [0.0, 0.1, 0.2, 0.3];
for (i, &e) in expect.iter().enumerate() {
approx(out.left[i], e);
approx(out.right[i], e);
}
assert_eq!(out.left, out.right);
}
#[test]
fn known_midside_reconstruction_constant_weights() {
let w = StereoWeightsQ13 {
w0_q13: 4096, w1_q13: 8192, };
let mut s = StereoUnmixState::new();
s.prev_weights = w;
let mid = vec![0.4f32, -0.2, 0.1, 0.3, -0.1];
let side = vec![0.05f32, 0.0, -0.1, 0.2, 0.1];
let out = stereo_ms_to_lr(Bandwidth::Wb, &mid, Some(&side), w, &mut s).unwrap();
let w0 = 0.5f32;
let w1 = 1.0f32;
let mut mhist = [0.0f32, 0.0]; let mut shist = 0.0f32; for i in 0..mid.len() {
let m_i = mid[i];
let m_i1 = mhist[1];
let m_i2 = mhist[0];
let s_i1 = shist;
let p0 = (m_i2 + 2.0 * m_i1 + m_i) / 4.0;
let l = ((1.0 + w1) * m_i1 + s_i1 + w0 * p0).clamp(-1.0, 1.0);
let r = ((1.0 - w1) * m_i1 - s_i1 - w0 * p0).clamp(-1.0, 1.0);
approx(out.left[i], l);
approx(out.right[i], r);
mhist = [m_i1, m_i];
shist = side[i];
}
}
#[test]
fn phase1_ramp_endpoints() {
let n1 = 64usize;
let n2 = n1 + 4;
let w_cur = StereoWeightsQ13 {
w0_q13: 0,
w1_q13: 8192,
}; let mut s = StereoUnmixState::new();
s.prev_weights = StereoWeightsQ13 {
w0_q13: 0,
w1_q13: 0,
};
let m = 0.4f32;
let mid = vec![m; n2];
let out = stereo_ms_to_lr(Bandwidth::Nb, &mid, None, w_cur, &mut s).unwrap();
approx(out.left[1], (1.0 + 1.0 / 64.0) * m);
approx(out.left[n1], 2.0 * m);
approx(out.left[n1 + 1], 2.0 * m);
approx(out.right[1], (1.0 - 1.0 / 64.0) * m);
approx(out.right[n1 + 1], 0.0);
}
#[test]
fn history_carries_across_frames() {
let w = StereoWeightsQ13 {
w0_q13: 0,
w1_q13: 0,
}; let mut s = StereoUnmixState::new();
s.prev_weights = w;
let frame1 = vec![0.1f32, 0.2, 0.3, 0.4];
let _ = stereo_ms_to_lr(Bandwidth::Wb, &frame1, None, w, &mut s).unwrap();
assert_eq!(s.mid_hist, [0.3, 0.4]);
let frame2 = vec![0.5f32, 0.6, 0.7, 0.8];
let out2 = stereo_ms_to_lr(Bandwidth::Wb, &frame2, None, w, &mut s).unwrap();
approx(out2.left[0], 0.4);
approx(out2.left[1], 0.5);
}
#[test]
fn side_history_carries_across_frames() {
let w = StereoWeightsQ13 {
w0_q13: 0,
w1_q13: 0,
};
let mut s = StereoUnmixState::new();
s.prev_weights = w;
let mid1 = vec![0.0f32; 4];
let side1 = vec![0.1f32, 0.2, 0.3, 0.4];
let _ = stereo_ms_to_lr(Bandwidth::Wb, &mid1, Some(&side1), w, &mut s).unwrap();
assert_eq!(s.side_hist, 0.4);
let mid2 = vec![0.0f32; 4];
let side2 = vec![0.5f32, 0.6, 0.7, 0.8];
let out2 = stereo_ms_to_lr(Bandwidth::Wb, &mid2, Some(&side2), w, &mut s).unwrap();
approx(out2.left[0], 0.4);
approx(out2.right[0], -0.4);
approx(out2.left[1], 0.5);
approx(out2.right[1], -0.5);
}
#[test]
fn output_is_clamped() {
let w = StereoWeightsQ13 {
w0_q13: 8192 * 4, w1_q13: 8192 * 4, };
let mut s = StereoUnmixState::new();
s.prev_weights = w;
let mid = vec![1.0f32; 8];
let side = vec![1.0f32; 8];
let out = stereo_ms_to_lr(Bandwidth::Wb, &mid, Some(&side), w, &mut s).unwrap();
for i in 0..8 {
assert!((-1.0..=1.0).contains(&out.left[i]), "left {}", out.left[i]);
assert!(
(-1.0..=1.0).contains(&out.right[i]),
"right {}",
out.right[i]
);
}
}
}