#![allow(dead_code)]
#[derive(Clone, Copy, Debug)]
pub struct MsBalance {
pub mid_rms: f64,
pub side_rms: f64,
}
impl MsBalance {
pub fn new(mid_rms: f64, side_rms: f64) -> Self {
Self { mid_rms, side_rms }
}
pub fn ms_ratio(&self) -> f64 {
if self.mid_rms < 1e-15 {
return f64::INFINITY;
}
self.side_rms / self.mid_rms
}
pub fn is_wide(&self) -> bool {
self.side_rms > self.mid_rms
}
pub fn width_percent(&self) -> f64 {
let total = self.mid_rms + self.side_rms;
if total < 1e-15 {
return 0.0;
}
(self.side_rms / total * 100.0).clamp(0.0, 100.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MsChannel {
Mid,
Side,
}
impl MsChannel {
pub fn label(&self) -> &'static str {
match self {
Self::Mid => "Mid",
Self::Side => "Side",
}
}
}
#[derive(Clone, Debug, Default)]
pub struct MsMeter {
mid_sum_sq: f64,
side_sum_sq: f64,
sample_count: usize,
mid_peak: f64,
side_peak: f64,
}
impl MsMeter {
pub fn new() -> Self {
Self::default()
}
pub fn process_stereo_pair(&mut self, left: f64, right: f64) {
let m = (left + right) * 0.5;
let s = (left - right) * 0.5;
self.mid_sum_sq += m * m;
self.side_sum_sq += s * s;
self.sample_count += 1;
let m_abs = m.abs();
let s_abs = s.abs();
if m_abs > self.mid_peak {
self.mid_peak = m_abs;
}
if s_abs > self.side_peak {
self.side_peak = s_abs;
}
}
pub fn process_interleaved(&mut self, samples: &[f64]) {
assert!(
samples.len() % 2 == 0,
"interleaved buffer must have even length"
);
for pair in samples.chunks_exact(2) {
self.process_stereo_pair(pair[0], pair[1]);
}
}
pub fn process_planar(&mut self, left: &[f64], right: &[f64]) {
assert_eq!(
left.len(),
right.len(),
"left and right channels must have equal length"
);
for (&l, &r) in left.iter().zip(right.iter()) {
self.process_stereo_pair(l, r);
}
}
pub fn mid_level(&self) -> f64 {
if self.sample_count == 0 {
return 0.0;
}
(self.mid_sum_sq / self.sample_count as f64).sqrt()
}
pub fn side_level(&self) -> f64 {
if self.sample_count == 0 {
return 0.0;
}
(self.side_sum_sq / self.sample_count as f64).sqrt()
}
pub fn mid_level_dbfs(&self) -> f64 {
let rms = self.mid_level();
if rms < 1e-15 {
f64::NEG_INFINITY
} else {
20.0 * rms.log10()
}
}
pub fn side_level_dbfs(&self) -> f64 {
let rms = self.side_level();
if rms < 1e-15 {
f64::NEG_INFINITY
} else {
20.0 * rms.log10()
}
}
pub fn mid_peak(&self) -> f64 {
self.mid_peak
}
pub fn side_peak(&self) -> f64 {
self.side_peak
}
pub fn ms_ratio(&self) -> f64 {
let mid = self.mid_level();
if mid < 1e-15 {
return f64::INFINITY;
}
self.side_level() / mid
}
pub fn balance(&self) -> MsBalance {
MsBalance::new(self.mid_level(), self.side_level())
}
pub fn sample_count(&self) -> usize {
self.sample_count
}
pub fn reset(&mut self) {
*self = Self::default();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ms_channel_label_mid() {
assert_eq!(MsChannel::Mid.label(), "Mid");
}
#[test]
fn test_ms_channel_label_side() {
assert_eq!(MsChannel::Side.label(), "Side");
}
#[test]
fn test_ms_balance_is_wide_true() {
let b = MsBalance::new(0.2, 0.8);
assert!(b.is_wide());
}
#[test]
fn test_ms_balance_is_wide_false() {
let b = MsBalance::new(0.8, 0.2);
assert!(!b.is_wide());
}
#[test]
fn test_ms_balance_width_percent_mono() {
let b = MsBalance::new(1.0, 0.0);
assert!((b.width_percent() - 0.0).abs() < 1e-9);
}
#[test]
fn test_ms_balance_width_percent_full() {
let b = MsBalance::new(0.0, 1.0);
assert!((b.width_percent() - 100.0).abs() < 1e-9);
}
#[test]
fn test_ms_balance_ms_ratio() {
let b = MsBalance::new(0.5, 1.0);
assert!((b.ms_ratio() - 2.0).abs() < 1e-9);
}
#[test]
fn test_ms_meter_pure_mono_no_side() {
let mut m = MsMeter::new();
for _ in 0..100 {
m.process_stereo_pair(0.5, 0.5);
}
assert!(m.side_level() < 1e-12);
assert!(m.mid_level() > 0.0);
}
#[test]
fn test_ms_meter_pure_side_no_mid() {
let mut m = MsMeter::new();
for _ in 0..100 {
m.process_stereo_pair(0.5, -0.5);
}
assert!(m.mid_level() < 1e-12);
assert!(m.side_level() > 0.0);
}
#[test]
fn test_ms_meter_sample_count() {
let mut m = MsMeter::new();
m.process_stereo_pair(0.1, -0.1);
m.process_stereo_pair(0.2, 0.2);
assert_eq!(m.sample_count(), 2);
}
#[test]
fn test_ms_meter_interleaved() {
let mut m = MsMeter::new();
let samples = vec![0.5_f64, 0.5, 0.3, -0.3]; m.process_interleaved(&samples);
assert_eq!(m.sample_count(), 2);
}
#[test]
fn test_ms_meter_planar() {
let mut m = MsMeter::new();
let left = vec![0.4_f64, 0.6];
let right = vec![0.4_f64, -0.6];
m.process_planar(&left, &right);
assert_eq!(m.sample_count(), 2);
}
#[test]
fn test_ms_meter_reset() {
let mut m = MsMeter::new();
m.process_stereo_pair(0.5, 0.3);
m.reset();
assert_eq!(m.sample_count(), 0);
assert_eq!(m.mid_level(), 0.0);
assert_eq!(m.side_level(), 0.0);
}
#[test]
fn test_mid_level_dbfs_is_negative() {
let mut m = MsMeter::new();
m.process_stereo_pair(0.5, 0.5);
let db = m.mid_level_dbfs();
assert!(db < 0.0, "RMS dBFS should be negative for sub-unity signal");
}
#[test]
fn test_side_level_dbfs_neg_inf_for_zero() {
let m = MsMeter::new();
assert!(m.side_level_dbfs().is_infinite());
}
#[test]
fn test_peak_tracking() {
let mut m = MsMeter::new();
m.process_stereo_pair(0.8, -0.8); m.process_stereo_pair(0.2, 0.2); assert!((m.side_peak() - 0.8).abs() < 1e-9);
assert!((m.mid_peak() - 0.2).abs() < 1e-9);
}
}