use crate::analyzer::AnalyzerBuilder;
use crate::channel::Channel;
use crate::error::Error;
use crate::mode::Mode;
use crate::report::Report;
#[must_use]
pub fn gain_to_target(report: &Report, target_lufs: f64) -> Option<f64> {
let i = report.integrated_lufs()?;
let delta_db = target_lufs - i;
Some(libm::pow(10.0, delta_db / 20.0))
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NormalizeReport {
pub measured_integrated_lufs: Option<f64>,
pub measured_true_peak_dbtp: Option<f64>,
pub target_lufs: f64,
pub true_peak_ceiling_dbtp: Option<f64>,
pub applied_gain_db: f64,
pub limited_by_true_peak: bool,
}
#[derive(Debug, Clone)]
pub struct Normalizer<'a> {
sample_rate: u32,
channels: &'a [Channel],
target_lufs: f64,
true_peak_ceiling_dbtp: Option<f64>,
}
impl<'a> Normalizer<'a> {
#[must_use]
pub fn new(sample_rate: u32, channels: &'a [Channel]) -> Self {
Self {
sample_rate,
channels,
target_lufs: -23.0,
true_peak_ceiling_dbtp: None,
}
}
#[must_use]
pub fn target_lufs(mut self, lufs: f64) -> Self {
self.target_lufs = lufs;
self
}
#[must_use]
pub fn true_peak_ceiling_dbtp(mut self, dbtp: f64) -> Self {
self.true_peak_ceiling_dbtp = Some(dbtp);
self
}
pub fn normalize_in_place(&self, samples: &mut [f32]) -> Result<NormalizeReport, Error> {
let mut a = AnalyzerBuilder::new()
.sample_rate(self.sample_rate)
.channels(self.channels)
.modes(Mode::Integrated | Mode::TruePeak)
.build()?;
a.push_interleaved::<f32>(samples)?;
let report = a.finalize();
let measured_i = report.integrated_lufs();
let measured_tp = report.true_peak_dbtp();
let mut gain_db = match measured_i {
Some(i) => self.target_lufs - i,
None => 0.0,
};
let mut limited_by_true_peak = false;
if let (Some(ceiling), Some(tp)) = (self.true_peak_ceiling_dbtp, measured_tp) {
let max_gain = ceiling - tp;
if gain_db > max_gain {
gain_db = max_gain;
limited_by_true_peak = true;
}
}
let gain_lin = libm::pow(10.0, gain_db / 20.0) as f32;
for s in samples.iter_mut() {
*s *= gain_lin;
}
Ok(NormalizeReport {
measured_integrated_lufs: measured_i,
measured_true_peak_dbtp: measured_tp,
target_lufs: self.target_lufs,
true_peak_ceiling_dbtp: self.true_peak_ceiling_dbtp,
applied_gain_db: gain_db,
limited_by_true_peak,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AnalyzerBuilder;
fn synth_stereo(amp: f32, secs: f32, fs: u32) -> Vec<f32> {
let n = (fs as f32 * secs) as usize;
let omega = 2.0 * std::f32::consts::PI * 1000.0 / fs as f32;
let mut v = Vec::with_capacity(n * 2);
for i in 0..n {
let s = amp * (omega * i as f32).sin();
v.push(s);
v.push(s);
}
v
}
#[test]
fn gain_to_target_zero_for_self() {
let mut a = AnalyzerBuilder::new()
.sample_rate(48_000)
.channels(&[Channel::Left, Channel::Right])
.modes(Mode::Integrated)
.build()
.unwrap();
let s = synth_stereo(0.05, 5.0, 48_000); a.push_interleaved::<f32>(&s).unwrap();
let r = a.finalize();
let measured = r.integrated_lufs().unwrap();
let g = gain_to_target(&r, measured).unwrap();
assert!((g - 1.0).abs() < 1e-9, "gain to self = {g}");
}
#[test]
fn normalise_brings_loudness_to_target() {
let mut signal = synth_stereo(0.05, 5.0, 48_000);
let n = Normalizer::new(48_000, &[Channel::Left, Channel::Right]).target_lufs(-23.0);
let r = n.normalize_in_place(&mut signal).unwrap();
let mut a = AnalyzerBuilder::new()
.sample_rate(48_000)
.channels(&[Channel::Left, Channel::Right])
.modes(Mode::Integrated)
.build()
.unwrap();
a.push_interleaved::<f32>(&signal).unwrap();
let after = a.finalize().integrated_lufs().unwrap();
assert!(
(after - (-23.0)).abs() <= 0.1,
"after-norm I = {after}, expected ≈ -23"
);
assert!(r.applied_gain_db > 0.0);
}
#[test]
fn true_peak_ceiling_attenuates_gain() {
let mut signal = synth_stereo(0.05, 5.0, 48_000);
let n = Normalizer::new(48_000, &[Channel::Left, Channel::Right])
.target_lufs(-1.0) .true_peak_ceiling_dbtp(-1.0);
let r = n.normalize_in_place(&mut signal).unwrap();
assert!(
r.limited_by_true_peak,
"expected the gain to be limited by the TP ceiling"
);
let mut a = AnalyzerBuilder::new()
.sample_rate(48_000)
.channels(&[Channel::Left, Channel::Right])
.modes(Mode::TruePeak)
.build()
.unwrap();
a.push_interleaved::<f32>(&signal).unwrap();
let tp = a.finalize().true_peak_dbtp().unwrap();
assert!(tp <= -1.0 + 0.5, "tp after cap = {tp}, expected ≤ -1 + ε");
}
}