#![forbid(unsafe_code)]
const SILENCE_DB: f64 = -144.0;
#[derive(Debug, Clone)]
pub struct BatchMeterConfig {
pub channels: usize,
pub sample_rate: f64,
pub rms_window_ms: f64,
pub peak_hold_ms: f64,
pub overload_threshold_db: f64,
}
impl Default for BatchMeterConfig {
fn default() -> Self {
Self {
channels: 2,
sample_rate: 48_000.0,
rms_window_ms: 300.0,
peak_hold_ms: 2_000.0,
overload_threshold_db: -0.1,
}
}
}
struct ChannelState {
rms_buf: Vec<f64>,
rms_write: usize,
rms_sum: f64,
peak_linear: f64,
peak_hold_db: f64,
peak_hold_remaining: usize,
peak_hold_samples: usize,
true_peak_linear: f64,
overload: bool,
overload_threshold_linear: f64,
}
impl ChannelState {
fn new(rms_window_samples: usize, peak_hold_samples: usize, overload_threshold_db: f64) -> Self {
let overload_threshold_linear = db_to_linear(overload_threshold_db);
Self {
rms_buf: vec![0.0; rms_window_samples.max(1)],
rms_write: 0,
rms_sum: 0.0,
peak_linear: 0.0,
peak_hold_db: SILENCE_DB,
peak_hold_remaining: 0,
peak_hold_samples,
true_peak_linear: 0.0,
overload: false,
overload_threshold_linear,
}
}
fn push_sample(&mut self, sample: f64) {
let abs_sample = sample.abs();
if abs_sample > self.true_peak_linear {
self.true_peak_linear = abs_sample;
}
if abs_sample >= self.overload_threshold_linear {
self.overload = true;
}
if abs_sample > self.peak_linear {
self.peak_linear = abs_sample;
let db = linear_to_db(abs_sample);
self.peak_hold_db = db;
self.peak_hold_remaining = self.peak_hold_samples;
} else if self.peak_hold_remaining > 0 {
self.peak_hold_remaining -= 1;
} else {
self.peak_linear = (self.peak_linear * 0.999).max(abs_sample);
}
let squared = sample * sample;
self.rms_sum -= self.rms_buf[self.rms_write];
self.rms_buf[self.rms_write] = squared;
self.rms_sum = (self.rms_sum + squared).max(0.0); self.rms_write = (self.rms_write + 1) % self.rms_buf.len();
}
fn peak_dbfs(&self) -> f64 {
if self.peak_linear <= 0.0 {
SILENCE_DB
} else {
linear_to_db(self.peak_linear)
}
}
fn rms_dbfs(&self) -> f64 {
let mean_sq = self.rms_sum / self.rms_buf.len() as f64;
if mean_sq <= 0.0 {
SILENCE_DB
} else {
10.0 * mean_sq.log10()
}
}
fn peak_hold_dbfs(&self) -> f64 {
self.peak_hold_db
}
fn true_peak_dbfs(&self) -> f64 {
if self.true_peak_linear <= 0.0 {
SILENCE_DB
} else {
linear_to_db(self.true_peak_linear)
}
}
fn reset(&mut self) {
self.rms_buf.fill(0.0);
self.rms_write = 0;
self.rms_sum = 0.0;
self.peak_linear = 0.0;
self.peak_hold_db = SILENCE_DB;
self.peak_hold_remaining = 0;
self.true_peak_linear = 0.0;
self.overload = false;
}
fn reset_peak_hold(&mut self) {
self.peak_hold_db = SILENCE_DB;
self.peak_hold_remaining = 0;
}
}
#[derive(Debug, Clone)]
pub struct BatchMeterReading {
pub peak_dbfs: Vec<f64>,
pub rms_dbfs: Vec<f64>,
pub peak_hold_dbfs: Vec<f64>,
pub true_peak_dbfs: Vec<f64>,
pub overload: Vec<bool>,
pub max_peak_dbfs: f64,
pub max_rms_dbfs: f64,
pub any_overload: bool,
}
impl BatchMeterReading {
fn from_states(states: &[ChannelState]) -> Self {
let peak_dbfs: Vec<f64> = states.iter().map(ChannelState::peak_dbfs).collect();
let rms_dbfs: Vec<f64> = states.iter().map(ChannelState::rms_dbfs).collect();
let peak_hold_dbfs: Vec<f64> = states.iter().map(ChannelState::peak_hold_dbfs).collect();
let true_peak_dbfs: Vec<f64> = states.iter().map(ChannelState::true_peak_dbfs).collect();
let overload: Vec<bool> = states.iter().map(|s| s.overload).collect();
let max_peak_dbfs = peak_dbfs.iter().copied().fold(SILENCE_DB, f64::max);
let max_rms_dbfs = rms_dbfs.iter().copied().fold(SILENCE_DB, f64::max);
let any_overload = overload.iter().any(|&o| o);
Self {
peak_dbfs,
rms_dbfs,
peak_hold_dbfs,
true_peak_dbfs,
overload,
max_peak_dbfs,
max_rms_dbfs,
any_overload,
}
}
}
pub struct BatchMeterProcessor {
config: BatchMeterConfig,
states: Vec<ChannelState>,
frames_processed: u64,
}
impl BatchMeterProcessor {
#[must_use]
pub fn new(config: BatchMeterConfig) -> Self {
let rms_window_samples =
((config.rms_window_ms * config.sample_rate / 1_000.0).round() as usize).max(1);
let peak_hold_samples =
((config.peak_hold_ms * config.sample_rate / 1_000.0).round() as usize).max(1);
let states = (0..config.channels)
.map(|_| {
ChannelState::new(
rms_window_samples,
peak_hold_samples,
config.overload_threshold_db,
)
})
.collect();
Self {
config,
states,
frames_processed: 0,
}
}
pub fn process_interleaved(&mut self, samples: &[f32]) {
let ch = self.config.channels;
if ch == 0 {
return;
}
let frames = samples.len() / ch;
for frame_idx in 0..frames {
for (ch_idx, state) in self.states.iter_mut().enumerate() {
let sample = samples[frame_idx * ch + ch_idx];
state.push_sample(f64::from(sample));
}
self.frames_processed += 1;
}
}
pub fn process_channel(&mut self, channel: usize, samples: &[f32]) -> Result<(), ()> {
let state = self.states.get_mut(channel).ok_or(())?;
for &s in samples {
state.push_sample(f64::from(s));
}
Ok(())
}
pub fn process_interleaved_f64(&mut self, samples: &[f64]) {
let ch = self.config.channels;
if ch == 0 {
return;
}
let frames = samples.len() / ch;
for frame_idx in 0..frames {
for (ch_idx, state) in self.states.iter_mut().enumerate() {
state.push_sample(samples[frame_idx * ch + ch_idx]);
}
self.frames_processed += 1;
}
}
#[must_use]
pub fn reading(&self) -> BatchMeterReading {
BatchMeterReading::from_states(&self.states)
}
#[must_use]
pub fn peak_dbfs(&self, channel: usize) -> Option<f64> {
self.states.get(channel).map(ChannelState::peak_dbfs)
}
#[must_use]
pub fn rms_dbfs(&self, channel: usize) -> Option<f64> {
self.states.get(channel).map(ChannelState::rms_dbfs)
}
#[must_use]
pub fn true_peak_dbfs(&self, channel: usize) -> Option<f64> {
self.states.get(channel).map(ChannelState::true_peak_dbfs)
}
#[must_use]
pub fn any_overload(&self) -> bool {
self.states.iter().any(|s| s.overload)
}
#[must_use]
pub fn channel_overload(&self, channel: usize) -> bool {
self.states.get(channel).map_or(false, |s| s.overload)
}
pub fn reset(&mut self) {
for state in &mut self.states {
state.reset();
}
self.frames_processed = 0;
}
pub fn reset_peak_holds(&mut self) {
for state in &mut self.states {
state.reset_peak_hold();
}
}
#[must_use]
pub fn frames_processed(&self) -> u64 {
self.frames_processed
}
#[must_use]
pub fn channels(&self) -> usize {
self.config.channels
}
#[must_use]
pub fn sample_rate(&self) -> f64 {
self.config.sample_rate
}
}
#[inline]
fn linear_to_db(linear: f64) -> f64 {
20.0 * linear.log10()
}
#[inline]
fn db_to_linear(db: f64) -> f64 {
10.0_f64.powf(db / 20.0)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_config(channels: usize) -> BatchMeterConfig {
BatchMeterConfig {
channels,
sample_rate: 48_000.0,
rms_window_ms: 10.0, peak_hold_ms: 500.0,
overload_threshold_db: -0.1,
}
}
#[test]
fn test_channel_count() {
let proc = BatchMeterProcessor::new(make_config(4));
assert_eq!(proc.channels(), 4);
}
#[test]
fn test_silence_readings() {
let mut proc = BatchMeterProcessor::new(make_config(2));
let samples = vec![0.0_f32; 2 * 512];
proc.process_interleaved(&samples);
let r = proc.reading();
for &p in &r.peak_dbfs {
assert!(p <= SILENCE_DB + 1.0, "expected silence, got {p}");
}
}
#[test]
fn test_full_scale_peak() {
let mut proc = BatchMeterProcessor::new(make_config(1));
let samples: Vec<f32> = (0..1024)
.map(|i| (i as f32 * 0.01).sin())
.collect();
proc.process_interleaved(&samples);
let peak = proc.peak_dbfs(0).expect("channel exists");
assert!(peak > -3.0, "expected near-full-scale peak, got {peak}");
}
#[test]
fn test_constant_rms() {
let mut proc = BatchMeterProcessor::new(make_config(1));
let window_samples = (10.0_f64 * 48_000.0_f64 / 1_000.0_f64).ceil() as usize;
let samples: Vec<f32> = vec![0.1_f32; window_samples];
proc.process_interleaved(&samples);
let rms = proc.rms_dbfs(0).expect("channel exists");
let expected = 20.0 * (0.1_f64).log10(); assert!(
(rms - expected).abs() < 0.5,
"RMS {rms:.2} not close to expected {expected:.2}"
);
}
#[test]
fn test_overload_detection() {
let mut proc = BatchMeterProcessor::new(make_config(2));
let samples: Vec<f32> = vec![
0.5, 1.0, ];
proc.process_interleaved(&samples);
assert!(!proc.channel_overload(0), "ch0 should not overload");
assert!(proc.channel_overload(1), "ch1 should overload");
assert!(proc.any_overload());
}
#[test]
fn test_reset_clears_state() {
let mut proc = BatchMeterProcessor::new(make_config(2));
let samples = vec![1.0_f32; 4]; proc.process_interleaved(&samples);
proc.reset();
let r = proc.reading();
for &p in &r.peak_dbfs {
assert!(p <= SILENCE_DB + 1.0, "after reset, expected silence, got {p}");
}
assert!(!proc.any_overload(), "overload flag should be cleared after reset");
assert_eq!(proc.frames_processed(), 0);
}
#[test]
fn test_process_channel_planar() {
let mut proc = BatchMeterProcessor::new(make_config(3));
let samples: Vec<f32> = vec![0.8_f32; 256];
proc.process_channel(1, &samples).expect("channel 1 ok");
let p0 = proc.peak_dbfs(0).expect("ch 0");
let p1 = proc.peak_dbfs(1).expect("ch 1");
let p2 = proc.peak_dbfs(2).expect("ch 2");
assert!(p0 <= SILENCE_DB + 1.0, "ch0 should be silent, got {p0}");
assert!(p1 > -5.0, "ch1 should be loud, got {p1}");
assert!(p2 <= SILENCE_DB + 1.0, "ch2 should be silent, got {p2}");
}
#[test]
fn test_out_of_range_channel() {
let proc = BatchMeterProcessor::new(make_config(2));
assert!(proc.peak_dbfs(99).is_none());
assert!(proc.rms_dbfs(99).is_none());
assert!(proc.true_peak_dbfs(99).is_none());
let mut proc = BatchMeterProcessor::new(make_config(2));
assert!(proc.process_channel(99, &[0.0]).is_err());
}
#[test]
fn test_true_peak() {
let mut proc = BatchMeterProcessor::new(make_config(1));
let samples: Vec<f32> = vec![0.3, -0.9, 0.5, -0.2];
proc.process_interleaved(&samples);
let tp = proc.true_peak_dbfs(0).expect("channel exists");
let expected_db = 20.0 * (0.9_f64).log10();
assert!(
(tp - expected_db).abs() < 0.1,
"true peak {tp:.2} should be near {expected_db:.2}"
);
}
#[test]
fn test_frames_processed_counter() {
let mut proc = BatchMeterProcessor::new(make_config(2));
let samples = vec![0.0_f32; 2 * 100]; proc.process_interleaved(&samples);
assert_eq!(proc.frames_processed(), 100);
proc.reset();
assert_eq!(proc.frames_processed(), 0);
}
#[test]
fn test_reading_summary_fields() {
let mut proc = BatchMeterProcessor::new(make_config(4));
let samples: Vec<f32> = vec![0.1, 0.5, 0.9, 0.0];
proc.process_interleaved(&samples);
let r = proc.reading();
assert!(r.max_peak_dbfs > r.peak_dbfs[0], "max should exceed ch0");
assert!(r.any_overload || !r.any_overload); assert!(!r.any_overload, "0.9 is below -0.1 dBFS threshold");
}
#[test]
fn test_reset_peak_holds_preserves_rms() {
let mut proc = BatchMeterProcessor::new(make_config(1));
let window = (10.0 * 48_000.0_f64 / 1_000.0).ceil() as usize;
let samples: Vec<f32> = vec![0.5_f32; window];
proc.process_interleaved(&samples);
let rms_before = proc.rms_dbfs(0).expect("ch 0");
proc.reset_peak_holds();
let rms_after = proc.rms_dbfs(0).expect("ch 0");
assert!(
(rms_before - rms_after).abs() < 0.01,
"RMS should be preserved after reset_peak_holds"
);
}
}