#![forbid(unsafe_code)]
#![allow(clippy::cast_lossless)]
#![allow(clippy::too_many_arguments)]
use super::peak::TruePeakDetector;
use super::r128::R128Meter;
use crate::frame::AudioFrame;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum NormalizationMode {
LinearGain,
#[default]
LimitedGain,
DynamicCompression,
Full,
}
#[derive(Clone, Debug)]
pub struct NormalizationConfig {
pub target_lufs: f64,
pub max_true_peak_dbtp: f64,
pub mode: NormalizationMode,
pub enable_compression: bool,
pub compression_threshold_lu: f64,
pub compression_ratio: f64,
pub enable_limiting: bool,
pub limiter_attack_ms: f64,
pub limiter_release_ms: f64,
}
impl Default for NormalizationConfig {
fn default() -> Self {
Self {
target_lufs: -23.0, max_true_peak_dbtp: -1.0,
mode: NormalizationMode::LimitedGain,
enable_compression: false,
compression_threshold_lu: 10.0,
compression_ratio: 3.0,
enable_limiting: true,
limiter_attack_ms: 1.0,
limiter_release_ms: 100.0,
}
}
}
impl NormalizationConfig {
#[must_use]
pub fn ebu_r128() -> Self {
Self {
target_lufs: -23.0,
max_true_peak_dbtp: -1.0,
mode: NormalizationMode::LimitedGain,
enable_compression: false,
compression_threshold_lu: 10.0,
compression_ratio: 3.0,
enable_limiting: true,
limiter_attack_ms: 0.5,
limiter_release_ms: 100.0,
}
}
#[must_use]
pub fn atsc_a85() -> Self {
Self {
target_lufs: -24.0,
max_true_peak_dbtp: -2.0,
mode: NormalizationMode::LimitedGain,
enable_compression: false,
compression_threshold_lu: 10.0,
compression_ratio: 3.0,
enable_limiting: true,
limiter_attack_ms: 1.0,
limiter_release_ms: 100.0,
}
}
#[must_use]
pub fn streaming() -> Self {
Self {
target_lufs: -14.0,
max_true_peak_dbtp: -1.0,
mode: NormalizationMode::LimitedGain,
enable_compression: false,
compression_threshold_lu: 8.0,
compression_ratio: 2.5,
enable_limiting: true,
limiter_attack_ms: 0.1,
limiter_release_ms: 80.0,
}
}
#[must_use]
pub fn custom(target_lufs: f64) -> Self {
Self {
target_lufs,
..Default::default()
}
}
}
pub struct LoudnessNormalizer {
config: NormalizationConfig,
sample_rate: f64,
channels: usize,
}
impl LoudnessNormalizer {
#[must_use]
pub fn new(config: NormalizationConfig, sample_rate: f64, channels: usize) -> Self {
Self {
config,
sample_rate,
channels,
}
}
pub fn analyze(&self, frames: &[AudioFrame]) -> NormalizationParams {
let mut meter = R128Meter::new(self.sample_rate, self.channels);
for frame in frames {
let samples = self.extract_samples(frame);
meter.process_interleaved(&samples);
}
let measured_lufs = meter.integrated_loudness();
let measured_peak_dbtp = meter.true_peak_dbtp();
let lra = meter.loudness_range();
let gain_db = if measured_lufs.is_finite() {
self.config.target_lufs - measured_lufs
} else {
0.0
};
let predicted_peak_dbtp = measured_peak_dbtp + gain_db;
let limiting_gain_db = if predicted_peak_dbtp > self.config.max_true_peak_dbtp {
self.config.max_true_peak_dbtp - predicted_peak_dbtp
} else {
0.0
};
NormalizationParams {
measured_lufs,
target_lufs: self.config.target_lufs,
gain_db,
limiting_gain_db,
total_gain_db: gain_db + limiting_gain_db,
measured_peak_dbtp,
predicted_peak_dbtp: predicted_peak_dbtp + limiting_gain_db,
loudness_range: lra,
}
}
pub fn normalize(&self, frames: &mut [AudioFrame]) -> NormalizationParams {
let params = self.analyze(frames);
if params.total_gain_db.abs() < 0.01 {
return params;
}
let linear_gain = Self::db_to_linear(params.gain_db);
for frame in frames.iter_mut() {
self.apply_gain(frame, linear_gain);
}
if self.config.enable_limiting && params.limiting_gain_db < -0.1 {
self.apply_limiting(frames, self.config.max_true_peak_dbtp);
}
params
}
fn apply_gain(&self, frame: &mut AudioFrame, linear_gain: f64) {
match &mut frame.samples {
crate::frame::AudioBuffer::Interleaved(data) => {
let mut samples = self.bytes_to_samples_f64(data);
for sample in &mut samples {
*sample *= linear_gain;
}
}
crate::frame::AudioBuffer::Planar(planes) => {
for plane in planes {
let mut samples = self.bytes_to_samples_f64(plane);
for sample in &mut samples {
*sample *= linear_gain;
}
}
}
}
}
fn apply_limiting(&self, frames: &mut [AudioFrame], max_peak_dbtp: f64) {
let max_peak_linear = TruePeakDetector::dbtp_to_linear(max_peak_dbtp);
for frame in frames {
let mut samples = self.extract_samples(frame);
for sample in &mut samples {
if sample.abs() > max_peak_linear {
*sample = sample.signum() * max_peak_linear;
}
}
}
}
fn extract_samples(&self, frame: &AudioFrame) -> Vec<f64> {
match &frame.samples {
crate::frame::AudioBuffer::Interleaved(data) => self.bytes_to_samples_f64(data),
crate::frame::AudioBuffer::Planar(planes) => {
if planes.is_empty() {
return Vec::new();
}
let channels = planes.len();
let frames = planes[0].len() / std::mem::size_of::<f32>();
let mut interleaved = Vec::with_capacity(frames * channels);
for _ in 0..frames {
for plane in planes {
let samples = self.bytes_to_samples_f64(plane);
if let Some(&sample) = samples.first() {
interleaved.push(sample);
}
}
}
interleaved
}
}
}
fn bytes_to_samples_f64(&self, bytes: &bytes::Bytes) -> Vec<f64> {
let sample_count = bytes.len() / 4;
let mut samples = Vec::with_capacity(sample_count);
for i in 0..sample_count {
let offset = i * 4;
if offset + 4 <= bytes.len() {
let bytes_array = [
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
];
let sample = f32::from_le_bytes(bytes_array);
samples.push(f64::from(sample));
}
}
samples
}
#[must_use]
pub fn db_to_linear(db: f64) -> f64 {
10.0_f64.powf(db / 20.0)
}
#[must_use]
pub fn linear_to_db(linear: f64) -> f64 {
if linear <= 0.0 {
f64::NEG_INFINITY
} else {
20.0 * linear.log10()
}
}
}
#[derive(Clone, Debug)]
pub struct NormalizationParams {
pub measured_lufs: f64,
pub target_lufs: f64,
pub gain_db: f64,
pub limiting_gain_db: f64,
pub total_gain_db: f64,
pub measured_peak_dbtp: f64,
pub predicted_peak_dbtp: f64,
pub loudness_range: f64,
}
impl NormalizationParams {
#[must_use]
pub fn will_clip(&self, max_peak_dbtp: f64) -> bool {
self.predicted_peak_dbtp > max_peak_dbtp
}
#[must_use]
pub fn loudness_delta(&self) -> f64 {
self.measured_lufs - self.target_lufs
}
}
pub struct StreamingNormalizer {
linear_gain: f64,
max_peak: f64,
peak_detector: TruePeakDetector,
}
impl StreamingNormalizer {
#[must_use]
pub fn new(gain_db: f64, max_peak_dbtp: f64, sample_rate: f64, channels: usize) -> Self {
let linear_gain = LoudnessNormalizer::db_to_linear(gain_db);
let max_peak = TruePeakDetector::dbtp_to_linear(max_peak_dbtp);
let peak_detector = TruePeakDetector::new(sample_rate, channels);
Self {
linear_gain,
max_peak,
peak_detector,
}
}
pub fn process_interleaved(&mut self, samples: &mut [f64]) {
for sample in samples.iter_mut() {
*sample *= self.linear_gain;
if sample.abs() > self.max_peak {
*sample = sample.signum() * self.max_peak;
}
}
self.peak_detector.process_interleaved(samples);
}
pub fn process_planar(&mut self, channels: &mut [Vec<f64>]) {
for ch_samples in channels {
for sample in ch_samples.iter_mut() {
*sample *= self.linear_gain;
if sample.abs() > self.max_peak {
*sample = sample.signum() * self.max_peak;
}
}
}
}
#[must_use]
pub fn current_peak_dbtp(&self) -> f64 {
TruePeakDetector::linear_to_dbtp(
self.peak_detector
.get_all_peaks()
.iter()
.fold(0.0, |a, &b| a.max(b)),
)
}
pub fn reset(&mut self) {
self.peak_detector.reset();
}
}
pub struct BatchNormalizer {
target_lufs: f64,
max_peak_dbtp: f64,
file_stats: Vec<FileNormalizationStats>,
}
impl BatchNormalizer {
#[must_use]
pub fn new(target_lufs: f64, max_peak_dbtp: f64) -> Self {
Self {
target_lufs,
max_peak_dbtp,
file_stats: Vec::new(),
}
}
pub fn add_file(&mut self, file_id: String, measured_lufs: f64, measured_peak_dbtp: f64) {
let gain_db = self.target_lufs - measured_lufs;
let predicted_peak = measured_peak_dbtp + gain_db;
let limiting_gain = if predicted_peak > self.max_peak_dbtp {
self.max_peak_dbtp - predicted_peak
} else {
0.0
};
self.file_stats.push(FileNormalizationStats {
file_id,
measured_lufs,
measured_peak_dbtp,
gain_db,
limiting_gain_db: limiting_gain,
total_gain_db: gain_db + limiting_gain,
});
}
#[must_use]
pub fn get_file_params(&self, file_id: &str) -> Option<&FileNormalizationStats> {
self.file_stats.iter().find(|s| s.file_id == file_id)
}
#[must_use]
pub fn all_stats(&self) -> &[FileNormalizationStats] {
&self.file_stats
}
#[must_use]
pub fn calculate_album_gain(&self) -> f64 {
if self.file_stats.is_empty() {
return 0.0;
}
let max_lufs = self
.file_stats
.iter()
.map(|s| s.measured_lufs)
.fold(f64::NEG_INFINITY, f64::max);
self.target_lufs - max_lufs
}
}
#[derive(Clone, Debug)]
pub struct FileNormalizationStats {
pub file_id: String,
pub measured_lufs: f64,
pub measured_peak_dbtp: f64,
pub gain_db: f64,
pub limiting_gain_db: f64,
pub total_gain_db: f64,
}