Skip to main content

maolan_engine/
loudness.rs

1use ebur128::{EbuR128, Mode as LoudnessMode};
2
3/// Real-time EBU R128 loudness values (LUFS).
4#[derive(Clone, Copy, Debug, Default, PartialEq)]
5pub struct LoudnessValues {
6    /// Momentary loudness (400 ms window).
7    pub momentary: f32,
8    /// Short-term loudness (3 s window).
9    pub short_term: f32,
10    /// Integrated loudness since the meter was created or reset.
11    pub integrated: f32,
12}
13
14impl LoudnessValues {
15    /// Value used when no loudness information is available (silence / no signal).
16    pub const SILENCE: f32 = -90.0;
17
18    /// Return a new `LoudnessValues` with all fields set to the silence sentinel.
19    pub fn silence() -> Self {
20        Self {
21            momentary: Self::SILENCE,
22            short_term: Self::SILENCE,
23            integrated: Self::SILENCE,
24        }
25    }
26
27    /// Clamp non-finite values to the silence sentinel.
28    pub fn sanitized(self) -> Self {
29        Self {
30            momentary: sanitize_lufs(self.momentary),
31            short_term: sanitize_lufs(self.short_term),
32            integrated: sanitize_lufs(self.integrated),
33        }
34    }
35}
36
37fn sanitize_lufs(value: f32) -> f32 {
38    if value.is_finite() {
39        value
40    } else {
41        LoudnessValues::SILENCE
42    }
43}
44
45/// Real-time EBU R128 loudness meter.
46///
47/// Wraps `ebur128` and exposes only the momentary / short-term / integrated
48/// values needed for the master strip readout.
49pub struct LoudnessMeter {
50    meter: EbuR128,
51    channels: usize,
52    sample_rate: u32,
53}
54
55impl LoudnessMeter {
56    /// Create a new meter for the given channel count and sample rate.
57    pub fn new(channels: usize, sample_rate: u32) -> Result<Self, ebur128::Error> {
58        let meter = EbuR128::new(
59            channels as u32,
60            sample_rate,
61            LoudnessMode::M | LoudnessMode::S | LoudnessMode::I,
62        )?;
63        Ok(Self {
64            meter,
65            channels,
66            sample_rate,
67        })
68    }
69
70    pub fn channels(&self) -> usize {
71        self.channels
72    }
73
74    pub fn sample_rate(&self) -> u32 {
75        self.sample_rate
76    }
77
78    /// Feed a block of interleaved floating-point samples into the meter.
79    /// This should be called every audio cycle so integrated loudness remains
80    /// accurate; call [`Self::values`] only when the readout is actually needed
81    /// (e.g. at the same rate as the VU meter snapshot).
82    pub fn feed_interleaved(&mut self, samples: &[f32]) {
83        if samples.is_empty() {
84            return;
85        }
86        if let Err(e) = self.meter.add_frames_f32(samples) {
87            tracing::warn!("EBU R128 loudness analysis failed: {}", e);
88        }
89    }
90
91    /// Return the current momentary / short-term / integrated loudness values.
92    pub fn values(&self) -> LoudnessValues {
93        let momentary = self.meter.loudness_momentary().unwrap_or(f64::NEG_INFINITY) as f32;
94        let short_term = self.meter.loudness_shortterm().unwrap_or(f64::NEG_INFINITY) as f32;
95        let integrated = self.meter.loudness_global().unwrap_or(f64::NEG_INFINITY) as f32;
96
97        LoudnessValues {
98            momentary,
99            short_term,
100            integrated,
101        }
102        .sanitized()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn silence_returns_silence_values() {
112        let mut meter = LoudnessMeter::new(2, 48_000).unwrap();
113        meter.feed_interleaved(&[0.0_f32; 48_000 * 2]);
114        assert_eq!(meter.values(), LoudnessValues::silence());
115    }
116
117    #[test]
118    fn sine_wave_produces_finite_lufs() {
119        let sample_rate = 48_000;
120        let duration = 3 * sample_rate;
121        let frequency = 1000.0;
122        let samples: Vec<f32> = (0..duration)
123            .flat_map(|i| {
124                let phase = 2.0 * std::f32::consts::PI * frequency * i as f32 / sample_rate as f32;
125                let sample = phase.sin() * 0.1;
126                [sample, sample]
127            })
128            .collect();
129
130        let mut meter = LoudnessMeter::new(2, sample_rate).unwrap();
131        meter.feed_interleaved(&samples);
132        let values = meter.values();
133
134        assert!(values.momentary.is_finite());
135        assert!(values.short_term.is_finite());
136        assert!(values.integrated.is_finite());
137        assert!(values.integrated > -90.0);
138    }
139
140    #[test]
141    fn integrated_changes_after_multiple_blocks() {
142        let sample_rate = 48_000;
143        let block: Vec<f32> = (0..sample_rate)
144            .flat_map(|i| {
145                let phase = 2.0 * std::f32::consts::PI * 1000.0 * i as f32 / sample_rate as f32;
146                let sample = phase.sin() * 0.05;
147                [sample, sample]
148            })
149            .collect();
150
151        let mut meter = LoudnessMeter::new(2, sample_rate).unwrap();
152        meter.feed_interleaved(&block);
153        let first = meter.values();
154        meter.feed_interleaved(&block);
155        let second = meter.values();
156
157        assert_ne!(first.integrated, second.integrated);
158    }
159}