1use std::{error::Error, fmt};
2
3mod dsp;
4mod validate;
5
6use dsp::{measure_true_peak, weight_channels};
7use validate::{validate_normalization, validate_spec};
8
9const LOUDNESS_OFFSET_DB: f64 = -0.691;
10const MOMENTARY_SECONDS: f64 = 0.400;
11const MOMENTARY_STEP_SECONDS: f64 = 0.100;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum LoudnessChannel {
16 Center,
18 Left,
20 Right,
22 LeftSurround,
24 RightSurround,
26 Lfe,
28}
29
30#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct LoudnessLayout {
33 channels: Vec<LoudnessChannel>,
34}
35
36impl LoudnessLayout {
37 pub fn new(channels: Vec<LoudnessChannel>) -> Result<Self, LoudnessError> {
39 if channels.is_empty() || channels.len() > 32 {
40 return Err(LoudnessError::InvalidPolicy {
41 field: "channel layout",
42 reason: "must contain between one and 32 channels",
43 });
44 }
45 Ok(Self { channels })
46 }
47
48 pub fn mono() -> Self {
50 Self {
51 channels: vec![LoudnessChannel::Center],
52 }
53 }
54
55 pub fn stereo() -> Self {
57 Self {
58 channels: vec![LoudnessChannel::Left, LoudnessChannel::Right],
59 }
60 }
61
62 pub fn five_point_one() -> Self {
64 Self {
65 channels: vec![
66 LoudnessChannel::Left,
67 LoudnessChannel::Right,
68 LoudnessChannel::Center,
69 LoudnessChannel::Lfe,
70 LoudnessChannel::LeftSurround,
71 LoudnessChannel::RightSurround,
72 ],
73 }
74 }
75
76 pub fn channels(&self) -> &[LoudnessChannel] {
78 &self.channels
79 }
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub enum FrequencyWeighting {
85 ItuRBs1770K,
87 Flat,
89}
90
91#[derive(Clone, Copy, Debug, PartialEq)]
93pub enum GatingPolicy {
94 EbuR128,
96 AbsoluteRelative {
98 absolute_lufs: f64,
100 relative_lu: f64,
102 },
103 None,
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109pub struct TruePeakPolicy {
110 pub oversample_factor: usize,
112 pub taps: usize,
114 pub max_work: u64,
116}
117
118impl Default for TruePeakPolicy {
119 fn default() -> Self {
120 Self {
121 oversample_factor: 4,
122 taps: 24,
123 max_work: 100_000_000,
124 }
125 }
126}
127
128#[derive(Clone, Debug, PartialEq)]
130pub struct LoudnessSpec {
131 pub sample_rate_hz: u32,
133 pub layout: LoudnessLayout,
135 pub frequency_weighting: FrequencyWeighting,
137 pub gating: GatingPolicy,
139 pub true_peak: TruePeakPolicy,
141 pub max_frames: usize,
143}
144
145impl Default for LoudnessSpec {
146 fn default() -> Self {
147 Self {
148 sample_rate_hz: 48_000,
149 layout: LoudnessLayout::stereo(),
150 frequency_weighting: FrequencyWeighting::ItuRBs1770K,
151 gating: GatingPolicy::EbuR128,
152 true_peak: TruePeakPolicy::default(),
153 max_frames: 16_777_216,
154 }
155 }
156}
157
158#[derive(Clone, Debug, PartialEq)]
160pub struct MomentaryLoudness {
161 pub start_frame: usize,
163 pub mean_square: f64,
165 pub lufs: Option<f64>,
167}
168
169#[derive(Clone, Debug, PartialEq)]
171pub struct TruePeakReport {
172 pub sample_peak: f64,
174 pub true_peak: f64,
176 pub true_peak_dbtp: Option<f64>,
178 pub oversample_factor: usize,
180 pub work_units: u64,
182}
183
184#[derive(Clone, Debug, PartialEq)]
186pub struct LoudnessReport {
187 pub integrated_lufs: Option<f64>,
189 pub absolute_gated_lufs: Option<f64>,
191 pub absolute_gate_lufs: Option<f64>,
193 pub relative_gate_lufs: Option<f64>,
195 pub momentary: Vec<MomentaryLoudness>,
197 pub gated_blocks: usize,
199 pub true_peak: TruePeakReport,
201 pub spec: LoudnessSpec,
203}
204
205#[derive(Clone, Copy, Debug, PartialEq)]
207pub struct NormalizationSpec {
208 pub target_lufs: f64,
210 pub max_true_peak_dbtp: f64,
212 pub max_abs_gain_db: f64,
214}
215
216#[derive(Clone, Debug, PartialEq)]
218pub struct NormalizationReport {
219 pub samples: Vec<f32>,
221 pub input: LoudnessReport,
223 pub output: LoudnessReport,
225 pub requested_gain_db: f64,
227 pub applied_gain_db: f64,
229 pub gain_limited: bool,
231 pub true_peak_ceiling_exceeded: bool,
233 pub clipped_samples: usize,
235}
236
237#[derive(Clone, Debug, PartialEq, Eq)]
239pub enum LoudnessError {
240 InvalidPolicy {
242 field: &'static str,
244 reason: &'static str,
246 },
247 MisalignedInput,
249 NonFiniteSample {
251 index: usize,
253 },
254 FrameLimit {
256 supplied: usize,
258 maximum: usize,
260 },
261 WorkLimit {
263 required: u64,
265 maximum: u64,
267 },
268 UndefinedIntegratedLoudness,
270 SizeOverflow,
272}
273
274impl fmt::Display for LoudnessError {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 match self {
277 Self::InvalidPolicy { field, reason } => {
278 write!(f, "invalid loudness {field}: {reason}")
279 }
280 Self::MisalignedInput => write!(f, "interleaved loudness input ends mid-frame"),
281 Self::NonFiniteSample { index } => write!(f, "loudness sample {index} is not finite"),
282 Self::FrameLimit { supplied, maximum } => {
283 write!(
284 f,
285 "loudness input has {supplied} frames, exceeding {maximum}"
286 )
287 }
288 Self::WorkLimit { required, maximum } => {
289 write!(
290 f,
291 "true-peak interpolation needs {required} work, exceeding {maximum}"
292 )
293 }
294 Self::UndefinedIntegratedLoudness => {
295 write!(f, "integrated loudness is undefined for this signal")
296 }
297 Self::SizeOverflow => write!(f, "loudness size arithmetic overflowed"),
298 }
299 }
300}
301
302impl Error for LoudnessError {}
303
304pub fn measure_loudness(
307 input: &[f32],
308 spec: LoudnessSpec,
309) -> Result<LoudnessReport, LoudnessError> {
310 validate_spec(&spec)?;
311 let channels = spec.layout.channels.len();
312 if !input.len().is_multiple_of(channels) {
313 return Err(LoudnessError::MisalignedInput);
314 }
315 for (index, sample) in input.iter().copied().enumerate() {
316 if !sample.is_finite() {
317 return Err(LoudnessError::NonFiniteSample { index });
318 }
319 }
320 let frames = input.len() / channels;
321 if frames > spec.max_frames {
322 return Err(LoudnessError::FrameLimit {
323 supplied: frames,
324 maximum: spec.max_frames,
325 });
326 }
327 let weighted = weight_channels(input, &spec);
328 let momentary = momentary_blocks(&weighted, &spec)?;
329 let (
330 absolute_gate_lufs,
331 relative_gate_lufs,
332 absolute_gated_lufs,
333 integrated_lufs,
334 gated_blocks,
335 ) = integrate_blocks(&momentary, spec.gating);
336 let true_peak = measure_true_peak(input, &spec)?;
337 Ok(LoudnessReport {
338 integrated_lufs,
339 absolute_gated_lufs,
340 absolute_gate_lufs,
341 relative_gate_lufs,
342 momentary,
343 gated_blocks,
344 true_peak,
345 spec,
346 })
347}
348
349pub fn normalize_loudness(
352 input: &[f32],
353 loudness: LoudnessSpec,
354 normalization: NormalizationSpec,
355) -> Result<NormalizationReport, LoudnessError> {
356 validate_normalization(normalization)?;
357 let before = measure_loudness(input, loudness.clone())?;
358 let integrated = before
359 .integrated_lufs
360 .ok_or(LoudnessError::UndefinedIntegratedLoudness)?;
361 let requested_gain_db = normalization.target_lufs - integrated;
362 let applied_gain_db = requested_gain_db.clamp(
363 -normalization.max_abs_gain_db,
364 normalization.max_abs_gain_db,
365 );
366 let gain_limited = (applied_gain_db - requested_gain_db).abs() > 1e-12;
367 let gain = 10.0f64.powf(applied_gain_db / 20.0);
368 let samples = input
369 .iter()
370 .map(|sample| (f64::from(*sample) * gain) as f32)
371 .collect::<Vec<_>>();
372 let clipped_samples = samples.iter().filter(|sample| sample.abs() > 1.0).count();
373 let output = measure_loudness(&samples, loudness)?;
374 let true_peak_ceiling_exceeded = output
375 .true_peak
376 .true_peak_dbtp
377 .is_some_and(|peak| peak > normalization.max_true_peak_dbtp);
378 Ok(NormalizationReport {
379 samples,
380 input: before,
381 output,
382 requested_gain_db,
383 applied_gain_db,
384 gain_limited,
385 true_peak_ceiling_exceeded,
386 clipped_samples,
387 })
388}
389
390fn momentary_blocks(
391 weighted: &[f64],
392 spec: &LoudnessSpec,
393) -> Result<Vec<MomentaryLoudness>, LoudnessError> {
394 let channels = spec.layout.channels.len();
395 let frames = weighted.len() / channels;
396 let window = (f64::from(spec.sample_rate_hz) * MOMENTARY_SECONDS).round() as usize;
397 let step = (f64::from(spec.sample_rate_hz) * MOMENTARY_STEP_SECONDS).round() as usize;
398 if frames < window {
399 return Ok(Vec::new());
400 }
401 let count = (frames - window) / step + 1;
402 let mut blocks = Vec::with_capacity(count);
403 for block in 0..count {
404 let start = block.checked_mul(step).ok_or(LoudnessError::SizeOverflow)?;
405 let mut energy = 0.0;
406 for (channel, position) in spec.layout.channels.iter().enumerate() {
407 let channel_energy = (start..start + window)
408 .map(|frame| weighted[frame * channels + channel].powi(2))
409 .sum::<f64>()
410 / window as f64;
411 energy += channel_weight(*position) * channel_energy;
412 }
413 blocks.push(MomentaryLoudness {
414 start_frame: start,
415 mean_square: energy,
416 lufs: loudness_level(energy),
417 });
418 }
419 Ok(blocks)
420}
421
422#[allow(clippy::type_complexity)]
423fn integrate_blocks(
424 blocks: &[MomentaryLoudness],
425 policy: GatingPolicy,
426) -> (Option<f64>, Option<f64>, Option<f64>, Option<f64>, usize) {
427 if policy == GatingPolicy::None {
428 let integrated = mean_energy(blocks.iter().map(|block| block.mean_square));
429 return (None, None, integrated, integrated, blocks.len());
430 }
431 let (absolute, relative) = match policy {
432 GatingPolicy::EbuR128 => (-70.0, -10.0),
433 GatingPolicy::AbsoluteRelative {
434 absolute_lufs,
435 relative_lu,
436 } => (absolute_lufs, relative_lu),
437 GatingPolicy::None => unreachable!(),
438 };
439 let absolute_energies = blocks
440 .iter()
441 .filter(|block| block.lufs.is_some_and(|level| level > absolute))
442 .map(|block| block.mean_square)
443 .collect::<Vec<_>>();
444 let absolute_gated = mean_energy(absolute_energies.iter().copied());
445 let relative_gate = absolute_gated.map(|level| level + relative);
446 let final_energies = blocks
447 .iter()
448 .filter(|block| {
449 block.lufs.is_some_and(|level| {
450 level > absolute && relative_gate.is_none_or(|relative| level > relative)
451 })
452 })
453 .map(|block| block.mean_square)
454 .collect::<Vec<_>>();
455 let gated_blocks = final_energies.len();
456 (
457 Some(absolute),
458 relative_gate,
459 absolute_gated,
460 mean_energy(final_energies.into_iter()),
461 gated_blocks,
462 )
463}
464
465fn channel_weight(channel: LoudnessChannel) -> f64 {
466 match channel {
467 LoudnessChannel::LeftSurround | LoudnessChannel::RightSurround => 1.41,
468 LoudnessChannel::Lfe => 0.0,
469 LoudnessChannel::Center | LoudnessChannel::Left | LoudnessChannel::Right => 1.0,
470 }
471}
472
473fn mean_energy(values: impl Iterator<Item = f64>) -> Option<f64> {
474 let (sum, count) = values.fold((0.0, 0usize), |(sum, count), value| {
475 (sum + value, count + 1)
476 });
477 (count > 0)
478 .then(|| sum / count as f64)
479 .and_then(loudness_level)
480}
481
482fn loudness_level(mean_square: f64) -> Option<f64> {
483 (mean_square > 0.0).then(|| LOUDNESS_OFFSET_DB + 10.0 * mean_square.log10())
484}