use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
const WAVEFORM_LEN: usize = 120;
pub const NUM_BANDS: usize = 16;
pub const BAND_FREQS: [f32; NUM_BANDS] = [
60.0, 80.0, 120.0, 170.0, 250.0, 350.0, 500.0, 700.0, 1000.0, 1400.0, 2000.0, 3000.0, 4000.0,
6000.0, 9000.0, 13000.0,
];
pub struct ChannelMeter {
rms: AtomicUsize,
peak: AtomicUsize,
peak_hold: AtomicUsize,
waveform: Vec<AtomicUsize>,
write_idx: AtomicUsize,
bands: Vec<AtomicUsize>,
sample_rate: AtomicUsize,
clipped: AtomicBool,
}
const SCALE: usize = 1_000_000;
impl ChannelMeter {
pub fn new() -> Self {
let waveform = (0..WAVEFORM_LEN).map(|_| AtomicUsize::new(0)).collect();
let bands = (0..NUM_BANDS).map(|_| AtomicUsize::new(0)).collect();
Self {
rms: AtomicUsize::new(0),
peak: AtomicUsize::new(0),
peak_hold: AtomicUsize::new(0),
waveform,
write_idx: AtomicUsize::new(0),
bands,
sample_rate: AtomicUsize::new(48000),
clipped: AtomicBool::new(false),
}
}
pub fn set_sample_rate(&self, sr: u32) {
self.sample_rate.store(sr as usize, Ordering::Relaxed);
}
pub fn update(&self, samples: &[f32]) {
if samples.is_empty() {
return;
}
let mut sum_sq = 0.0f64;
let mut max_abs = 0.0f32;
for &s in samples {
let abs = s.abs();
sum_sq += (s as f64) * (s as f64);
if abs > max_abs {
max_abs = abs;
}
}
let rms_lin = (sum_sq / samples.len() as f64).sqrt() as f32;
let rms_fixed = lin_to_fixed(rms_lin);
let peak_fixed = lin_to_fixed(max_abs);
self.rms.store(rms_fixed, Ordering::Relaxed);
self.peak.store(peak_fixed, Ordering::Relaxed);
let prev_hold = self.peak_hold.load(Ordering::Relaxed);
if peak_fixed > prev_hold {
self.peak_hold.store(peak_fixed, Ordering::Relaxed);
}
if max_abs > 1.0 {
self.clipped.store(true, Ordering::Relaxed);
}
let n_points = samples.len().clamp(1, 8);
let chunk = samples.len() / n_points;
if chunk == 0 {
return;
}
for i in 0..n_points {
let start = i * chunk;
let end = ((i + 1) * chunk).min(samples.len());
if start >= end {
break;
}
let chunk_max = samples[start..end]
.iter()
.map(|s| s.abs())
.fold(0.0f32, f32::max);
let val = lin_to_fixed(chunk_max);
let idx = self.write_idx.fetch_add(1, Ordering::Relaxed) % WAVEFORM_LEN;
self.waveform[idx].store(val, Ordering::Relaxed);
}
let sr = self.sample_rate.load(Ordering::Relaxed) as f32;
if sr > 0.0 && samples.len() >= 8 {
for (bi, &freq) in BAND_FREQS.iter().enumerate() {
if freq >= sr * 0.5 {
continue;
}
let k = freq / sr * samples.len() as f32;
let w = 2.0 * std::f32::consts::PI * k / samples.len() as f32;
let coeff = 2.0 * w.cos();
let mut s_prev = 0.0f32;
let mut s_prev2 = 0.0f32;
for &s in samples {
let s_cur = s + coeff * s_prev - s_prev2;
s_prev2 = s_prev;
s_prev = s_cur;
}
let mag = ((s_prev * s_prev + s_prev2 * s_prev2 - coeff * s_prev * s_prev2).abs())
/ (samples.len() as f32);
let db = if mag > 1e-10 {
10.0 * mag.log10()
} else {
-60.0
};
let norm = ((db + 60.0) / 60.0).clamp(0.0, 1.0);
self.bands[bi].store(lin_to_fixed(norm), Ordering::Relaxed);
}
}
}
pub fn snapshot(&self) -> MeterSnapshot {
let rms = fixed_to_lin(self.rms.load(Ordering::Relaxed));
let peak = fixed_to_lin(self.peak.load(Ordering::Relaxed));
let peak_hold = fixed_to_lin(self.peak_hold.load(Ordering::Relaxed));
let clipped = self.clipped.load(Ordering::Relaxed);
let write_pos = self.write_idx.load(Ordering::Relaxed);
let mut waveform = Vec::with_capacity(WAVEFORM_LEN);
let filled = write_pos.min(WAVEFORM_LEN);
let start = if write_pos > WAVEFORM_LEN {
write_pos % WAVEFORM_LEN
} else {
0
};
for i in 0..filled {
let idx = (start + i) % WAVEFORM_LEN;
waveform.push(fixed_to_lin(self.waveform[idx].load(Ordering::Relaxed)));
}
MeterSnapshot {
rms,
peak,
peak_hold,
clipped,
waveform,
bands: self
.bands
.iter()
.map(|b| fixed_to_lin(b.load(Ordering::Relaxed)))
.collect(),
}
}
pub fn reset_peak(&self) {
self.peak_hold.store(0, Ordering::Relaxed);
self.clipped.store(false, Ordering::Relaxed);
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct MeterSnapshot {
pub rms: f32,
pub peak: f32,
pub peak_hold: f32,
pub clipped: bool,
pub waveform: Vec<f32>,
pub bands: Vec<f32>,
}
#[inline]
fn lin_to_fixed(x: f32) -> usize {
(x.clamp(0.0, 2.0) * SCALE as f32) as usize
}
#[inline]
fn fixed_to_lin(x: usize) -> f32 {
x as f32 / SCALE as f32
}
pub struct MeterBank {
pub meters: Vec<(String, Arc<ChannelMeter>)>,
}
impl MeterBank {
pub fn for_plan(
plan: &crate::validate::ValidatedConfig,
resolved: &crate::devices::ResolvedAudioDevices,
) -> Self {
use std::collections::BTreeMap;
let mut keys: BTreeMap<String, ()> = BTreeMap::new();
for route in &plan.routes {
for &ch in &route.from_channels {
keys.insert(format!("{}:{}", route.from, ch), ());
}
for &ch in &route.to_channels {
keys.insert(format!("{}:{}", route.to, ch), ());
}
}
for alias in resolved.devices.keys() {
keys.insert(format!("{}:0", alias), ());
}
let meters = keys
.into_keys()
.map(|k| (k, Arc::new(ChannelMeter::new())))
.collect();
Self { meters }
}
pub fn get(&self, device: &str, channel: usize) -> Option<&Arc<ChannelMeter>> {
let key = format!("{}:{}", device, channel);
self.meters.iter().find(|(k, _)| k == &key).map(|(_, m)| m)
}
pub fn reset_all_peaks(&self) {
for (_, m) in &self.meters {
m.reset_peak();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fixed_point_roundtrip() {
for x in [0.0f32, 0.1, 0.5, 0.99, 1.0] {
let fixed = lin_to_fixed(x);
let back = fixed_to_lin(fixed);
assert!(
(back - x).abs() < 0.002,
"roundtrip failed for {x}: got {back}"
);
}
}
#[test]
fn silence_produces_zero() {
let m = ChannelMeter::new();
m.update(&[0.0; 256]);
let s = m.snapshot();
assert!(s.rms < 0.001);
assert!(s.peak < 0.001);
}
#[test]
fn full_scale_produces_unity() {
let m = ChannelMeter::new();
m.update(&[1.0; 256]);
let s = m.snapshot();
assert!((s.peak - 1.0).abs() < 0.002);
assert!((s.rms - 1.0).abs() < 0.002);
}
#[test]
fn clip_detected() {
let m = ChannelMeter::new();
m.update(&[1.5, -1.2, 0.5]);
let s = m.snapshot();
assert!(s.clipped);
}
#[test]
fn waveform_is_populated() {
let m = ChannelMeter::new();
let samples: Vec<f32> = (0..512).map(|i| (i as f32 * 0.01).sin()).collect();
m.update(&samples);
let s = m.snapshot();
assert!(!s.waveform.is_empty());
assert!(s.waveform.iter().any(|&v| v > 0.0));
}
#[test]
fn reset_clears_clip() {
let m = ChannelMeter::new();
m.update(&[1.5]);
assert!(m.snapshot().clipped);
m.reset_peak();
assert!(!m.snapshot().clipped);
assert!(m.snapshot().peak_hold < 0.001);
}
}