#![allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub struct MidSide {
pub mid: f64,
pub side: f64,
}
impl MidSide {
pub fn new(mid: f64, side: f64) -> Self {
Self { mid, side }
}
pub fn encode(left: f64, right: f64) -> Self {
Self {
mid: (left + right) * 0.5,
side: (left - right) * 0.5,
}
}
pub fn decode(&self) -> (f64, f64) {
let left = self.mid + self.side;
let right = self.mid - self.side;
(left, right)
}
}
#[derive(Debug, Clone)]
pub struct StereoWidenerConfig {
pub width: f64,
pub sample_rate: f64,
pub haas_delay_ms: f64,
pub balance: f64,
pub bass_mono_freq: f64,
}
impl Default for StereoWidenerConfig {
fn default() -> Self {
Self {
width: 1.0,
sample_rate: 48000.0,
haas_delay_ms: 0.0,
balance: 0.0,
bass_mono_freq: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct StereoWidener {
config: StereoWidenerConfig,
delay_buffer: Vec<f64>,
delay_pos: usize,
delay_samples: usize,
lp_state_l: f64,
lp_state_r: f64,
lp_coeff: f64,
}
impl StereoWidener {
#[allow(clippy::cast_precision_loss)]
pub fn new(config: StereoWidenerConfig) -> Self {
let delay_samples = (config.haas_delay_ms * config.sample_rate / 1000.0) as usize;
let delay_buffer = vec![0.0; delay_samples.max(1)];
let lp_coeff = if config.bass_mono_freq > 0.0 && config.sample_rate > 0.0 {
let rc = 1.0 / (2.0 * std::f64::consts::PI * config.bass_mono_freq);
let dt = 1.0 / config.sample_rate;
dt / (rc + dt)
} else {
0.0
};
Self {
config,
delay_buffer,
delay_pos: 0,
delay_samples,
lp_state_l: 0.0,
lp_state_r: 0.0,
lp_coeff,
}
}
pub fn width(&self) -> f64 {
self.config.width
}
pub fn set_width(&mut self, width: f64) {
self.config.width = width;
}
pub fn set_balance(&mut self, balance: f64) {
self.config.balance = balance.clamp(-1.0, 1.0);
}
pub fn reset(&mut self) {
for s in &mut self.delay_buffer {
*s = 0.0;
}
self.delay_pos = 0;
self.lp_state_l = 0.0;
self.lp_state_r = 0.0;
}
pub fn process_interleaved(&mut self, samples: &mut [f64]) {
let frame_count = samples.len() / 2;
for i in 0..frame_count {
let l = samples[i * 2];
let r = samples[i * 2 + 1];
let (out_l, out_r) = self.process_sample(l, r);
samples[i * 2] = out_l;
samples[i * 2 + 1] = out_r;
}
}
pub fn process_sample(&mut self, left: f64, right: f64) -> (f64, f64) {
let mut l = left;
let mut r = right;
let ms = MidSide::encode(l, r);
let adjusted = MidSide::new(ms.mid, ms.side * self.config.width);
let (wl, wr) = adjusted.decode();
l = wl;
r = wr;
if self.delay_samples > 0 {
let delayed = self.delay_buffer[self.delay_pos];
self.delay_buffer[self.delay_pos] = r;
self.delay_pos = (self.delay_pos + 1) % self.delay_buffer.len();
r = delayed;
}
if self.lp_coeff > 0.0 {
self.lp_state_l += self.lp_coeff * (l - self.lp_state_l);
self.lp_state_r += self.lp_coeff * (r - self.lp_state_r);
let bass_mono = (self.lp_state_l + self.lp_state_r) * 0.5;
let hi_l = l - self.lp_state_l;
let hi_r = r - self.lp_state_r;
l = bass_mono + hi_l;
r = bass_mono + hi_r;
}
if self.config.balance != 0.0 {
let (bl, br) = apply_balance(l, r, self.config.balance);
l = bl;
r = br;
}
(l, r)
}
pub fn process_split(&mut self, left: &mut [f64], right: &mut [f64]) {
let len = left.len().min(right.len());
for i in 0..len {
let (ol, or) = self.process_sample(left[i], right[i]);
left[i] = ol;
right[i] = or;
}
}
}
pub fn apply_balance(left: f64, right: f64, balance: f64) -> (f64, f64) {
let balance = balance.clamp(-1.0, 1.0);
if balance <= 0.0 {
let gain_r = 1.0 + balance;
(left, right * gain_r)
} else {
let gain_l = 1.0 - balance;
(left * gain_l, right)
}
}
#[allow(clippy::cast_precision_loss)]
pub fn stereo_correlation(interleaved: &[f64]) -> f64 {
let frame_count = interleaved.len() / 2;
if frame_count == 0 {
return 0.0;
}
let mut sum_lr = 0.0;
let mut sum_ll = 0.0;
let mut sum_rr = 0.0;
for i in 0..frame_count {
let l = interleaved[i * 2];
let r = interleaved[i * 2 + 1];
sum_lr += l * r;
sum_ll += l * l;
sum_rr += r * r;
}
let denom = (sum_ll * sum_rr).sqrt();
if denom > 0.0 {
sum_lr / denom
} else {
0.0
}
}
pub fn interleaved_to_mid_side(samples: &mut [f64]) {
let frame_count = samples.len() / 2;
for i in 0..frame_count {
let l = samples[i * 2];
let r = samples[i * 2 + 1];
samples[i * 2] = (l + r) * 0.5;
samples[i * 2 + 1] = (l - r) * 0.5;
}
}
pub fn mid_side_to_interleaved(samples: &mut [f64]) {
let frame_count = samples.len() / 2;
for i in 0..frame_count {
let m = samples[i * 2];
let s = samples[i * 2 + 1];
samples[i * 2] = m + s;
samples[i * 2 + 1] = m - s;
}
}
pub fn sum_to_mono(samples: &mut [f64]) {
let frame_count = samples.len() / 2;
for i in 0..frame_count {
let mono = (samples[i * 2] + samples[i * 2 + 1]) * 0.5;
samples[i * 2] = mono;
samples[i * 2 + 1] = mono;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mid_side_encode_decode_roundtrip() {
let ms = MidSide::encode(0.8, 0.2);
let (l, r) = ms.decode();
assert!((l - 0.8).abs() < 1e-10);
assert!((r - 0.2).abs() < 1e-10);
}
#[test]
fn test_mid_side_mono_signal() {
let ms = MidSide::encode(0.5, 0.5);
assert!((ms.mid - 0.5).abs() < 1e-10);
assert!(ms.side.abs() < 1e-10);
}
#[test]
fn test_mid_side_pure_side() {
let ms = MidSide::encode(0.5, -0.5);
assert!(ms.mid.abs() < 1e-10);
assert!((ms.side - 0.5).abs() < 1e-10);
}
#[test]
fn test_default_config() {
let cfg = StereoWidenerConfig::default();
assert!((cfg.width - 1.0).abs() < 1e-10);
assert!((cfg.balance - 0.0).abs() < 1e-10);
}
#[test]
fn test_widener_unity_passthrough() {
let config = StereoWidenerConfig {
width: 1.0,
haas_delay_ms: 0.0,
balance: 0.0,
bass_mono_freq: 0.0,
sample_rate: 48000.0,
};
let mut widener = StereoWidener::new(config);
let (l, r) = widener.process_sample(0.7, 0.3);
assert!((l - 0.7).abs() < 1e-10);
assert!((r - 0.3).abs() < 1e-10);
}
#[test]
fn test_widener_mono_collapse() {
let config = StereoWidenerConfig {
width: 0.0,
haas_delay_ms: 0.0,
balance: 0.0,
bass_mono_freq: 0.0,
sample_rate: 48000.0,
};
let mut widener = StereoWidener::new(config);
let (l, r) = widener.process_sample(1.0, 0.0);
assert!((l - r).abs() < 1e-10);
}
#[test]
fn test_widener_double_width() {
let config = StereoWidenerConfig {
width: 2.0,
haas_delay_ms: 0.0,
balance: 0.0,
bass_mono_freq: 0.0,
sample_rate: 48000.0,
};
let mut widener = StereoWidener::new(config);
let (l, r) = widener.process_sample(0.8, 0.2);
assert!((l - 1.1).abs() < 1e-10);
assert!((r - (-0.1)).abs() < 1e-10);
}
#[test]
fn test_widener_reset() {
let config = StereoWidenerConfig::default();
let mut widener = StereoWidener::new(config);
let _ = widener.process_sample(1.0, 1.0);
widener.reset();
assert!((widener.lp_state_l - 0.0).abs() < 1e-10);
}
#[test]
fn test_apply_balance_center() {
let (l, r) = apply_balance(1.0, 1.0, 0.0);
assert!((l - 1.0).abs() < 1e-10);
assert!((r - 1.0).abs() < 1e-10);
}
#[test]
fn test_apply_balance_full_left() {
let (l, r) = apply_balance(1.0, 1.0, -1.0);
assert!((l - 1.0).abs() < 1e-10);
assert!((r - 0.0).abs() < 1e-10);
}
#[test]
fn test_stereo_correlation_mono() {
let samples = vec![1.0, 1.0, -1.0, -1.0, 0.5, 0.5];
let corr = stereo_correlation(&samples);
assert!((corr - 1.0).abs() < 1e-10);
}
#[test]
fn test_interleaved_ms_roundtrip() {
let mut samples = vec![0.8, 0.2, -0.5, 0.5];
let original = samples.clone();
interleaved_to_mid_side(&mut samples);
mid_side_to_interleaved(&mut samples);
for (a, b) in samples.iter().zip(original.iter()) {
assert!((a - b).abs() < 1e-10);
}
}
#[test]
fn test_sum_to_mono() {
let mut samples = vec![0.8, 0.2, 0.6, 0.4];
sum_to_mono(&mut samples);
assert!((samples[0] - 0.5).abs() < 1e-10);
assert!((samples[1] - 0.5).abs() < 1e-10);
assert!((samples[2] - 0.5).abs() < 1e-10);
assert!((samples[3] - 0.5).abs() < 1e-10);
}
#[test]
fn test_process_interleaved() {
let config = StereoWidenerConfig {
width: 1.0,
haas_delay_ms: 0.0,
balance: 0.0,
bass_mono_freq: 0.0,
sample_rate: 48000.0,
};
let mut widener = StereoWidener::new(config);
let mut samples = vec![0.5, 0.5, -0.5, -0.5];
widener.process_interleaved(&mut samples);
assert!((samples[0] - 0.5).abs() < 1e-10);
assert!((samples[1] - 0.5).abs() < 1e-10);
}
}