#![allow(dead_code)]
#[derive(Clone, Debug)]
pub struct LoudnessReading {
pub lufs: f64,
pub sample_index: u64,
}
impl LoudnessReading {
pub fn new(lufs: f64, sample_index: u64) -> Self {
Self { lufs, sample_index }
}
pub fn is_loud(&self, threshold_lufs: f64) -> bool {
self.lufs > threshold_lufs
}
}
#[derive(Debug)]
pub struct LoudnessHistory {
readings: Vec<LoudnessReading>,
capacity: usize,
}
impl LoudnessHistory {
pub fn with_capacity(capacity: usize) -> Self {
Self {
readings: Vec::with_capacity(capacity),
capacity: capacity.max(1),
}
}
pub fn push(&mut self, reading: LoudnessReading) {
if self.readings.len() >= self.capacity {
self.readings.remove(0);
}
self.readings.push(reading);
}
pub fn push_value(&mut self, lufs: f64, sample_index: u64) {
self.push(LoudnessReading::new(lufs, sample_index));
}
pub fn window_average(&self) -> Option<f64> {
if self.readings.is_empty() {
return None;
}
let sum: f64 = self.readings.iter().map(|r| r.lufs).sum();
Some(sum / self.readings.len() as f64)
}
pub fn peak(&self) -> Option<f64> {
self.readings.iter().map(|r| r.lufs).reduce(f64::max)
}
pub fn trough(&self) -> Option<f64> {
self.readings.iter().map(|r| r.lufs).reduce(f64::min)
}
pub fn count(&self) -> usize {
self.readings.len()
}
pub fn is_empty(&self) -> bool {
self.readings.is_empty()
}
pub fn readings(&self) -> &[LoudnessReading] {
&self.readings
}
pub fn clear(&mut self) {
self.readings.clear();
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TrendDirection {
Rising,
Falling,
Stable,
}
#[derive(Debug)]
pub struct LoudnessTrend {
pub sensitivity_lu: f64,
}
impl LoudnessTrend {
pub fn new(sensitivity_lu: f64) -> Self {
Self { sensitivity_lu }
}
pub fn detect_trend(&self, history: &LoudnessHistory) -> Option<TrendDirection> {
let readings = history.readings();
if readings.len() < 2 {
return None;
}
let mid = readings.len() / 2;
let first_half: f64 = readings[..mid].iter().map(|r| r.lufs).sum::<f64>() / mid as f64;
let second_half: f64 =
readings[mid..].iter().map(|r| r.lufs).sum::<f64>() / (readings.len() - mid) as f64;
let delta = second_half - first_half;
if delta > self.sensitivity_lu {
Some(TrendDirection::Rising)
} else if delta < -self.sensitivity_lu {
Some(TrendDirection::Falling)
} else {
Some(TrendDirection::Stable)
}
}
pub fn detect(&self, history: &LoudnessHistory) -> Option<TrendDirection> {
self.detect_trend(history)
}
}
impl Default for LoudnessTrend {
fn default() -> Self {
Self::new(1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reading_is_loud_above_threshold() {
let r = LoudnessReading::new(-12.0, 0);
assert!(r.is_loud(-14.0));
}
#[test]
fn reading_not_loud_below_threshold() {
let r = LoudnessReading::new(-20.0, 0);
assert!(!r.is_loud(-14.0));
}
#[test]
fn reading_not_loud_exactly_at_threshold() {
let r = LoudnessReading::new(-14.0, 0);
assert!(!r.is_loud(-14.0));
}
#[test]
fn history_starts_empty() {
let h = LoudnessHistory::with_capacity(5);
assert!(h.is_empty());
assert_eq!(h.count(), 0);
}
#[test]
fn history_push_stores_reading() {
let mut h = LoudnessHistory::with_capacity(5);
h.push_value(-23.0, 0);
assert_eq!(h.count(), 1);
assert!((h.readings()[0].lufs - (-23.0)).abs() < 1e-9);
}
#[test]
fn history_window_average_correct() {
let mut h = LoudnessHistory::with_capacity(3);
h.push_value(-20.0, 0);
h.push_value(-22.0, 1);
h.push_value(-24.0, 2);
let avg = h.window_average().expect("avg should be valid");
assert!((avg - (-22.0)).abs() < 1e-9);
}
#[test]
fn history_window_average_none_when_empty() {
let h = LoudnessHistory::with_capacity(5);
assert!(h.window_average().is_none());
}
#[test]
fn history_peak_returns_loudest_value() {
let mut h = LoudnessHistory::with_capacity(5);
h.push_value(-23.0, 0);
h.push_value(-10.0, 1);
h.push_value(-30.0, 2);
assert!((h.peak().expect("peak should succeed") - (-10.0)).abs() < 1e-9);
}
#[test]
fn history_trough_returns_quietest_value() {
let mut h = LoudnessHistory::with_capacity(5);
h.push_value(-23.0, 0);
h.push_value(-10.0, 1);
h.push_value(-40.0, 2);
assert!((h.trough().expect("trough should succeed") - (-40.0)).abs() < 1e-9);
}
#[test]
fn history_evicts_oldest_when_full() {
let mut h = LoudnessHistory::with_capacity(2);
h.push_value(-10.0, 0);
h.push_value(-20.0, 1);
h.push_value(-30.0, 2); assert_eq!(h.count(), 2);
assert!((h.readings()[0].lufs - (-20.0)).abs() < 1e-9);
}
#[test]
fn history_clear_empties_storage() {
let mut h = LoudnessHistory::with_capacity(4);
h.push_value(-23.0, 0);
h.clear();
assert!(h.is_empty());
}
#[test]
fn trend_none_for_single_reading() {
let mut h = LoudnessHistory::with_capacity(5);
h.push_value(-23.0, 0);
let t = LoudnessTrend::default();
assert!(t.detect_trend(&h).is_none());
}
#[test]
fn trend_rising_detected() {
let mut h = LoudnessHistory::with_capacity(6);
for i in 0..6u64 {
h.push_value(-30.0 + i as f64 * 3.0, i);
}
let t = LoudnessTrend::new(1.0);
assert_eq!(t.detect_trend(&h), Some(TrendDirection::Rising));
}
#[test]
fn trend_falling_detected() {
let mut h = LoudnessHistory::with_capacity(6);
for i in 0..6u64 {
h.push_value(-10.0 - i as f64 * 3.0, i);
}
let t = LoudnessTrend::new(1.0);
assert_eq!(t.detect_trend(&h), Some(TrendDirection::Falling));
}
#[test]
fn trend_stable_when_flat() {
let mut h = LoudnessHistory::with_capacity(4);
h.push_value(-23.0, 0);
h.push_value(-23.1, 1);
h.push_value(-22.9, 2);
h.push_value(-23.0, 3);
let t = LoudnessTrend::new(1.0);
assert_eq!(t.detect_trend(&h), Some(TrendDirection::Stable));
}
#[test]
fn trend_detect_alias_works() {
let h = LoudnessHistory::with_capacity(2);
let t = LoudnessTrend::default();
assert!(t.detect(&h).is_none());
}
}