use crate::{LoudnessMeter, LoudnessMetrics, MeterConfig, Standard};
pub const EBU_TARGET_LUFS: f64 = -23.0;
pub const EBU_TOLERANCE_LU: f64 = 1.0;
pub const EBU_MAX_TRUEPEAK_DBTP: f64 = -1.0;
pub const EBU_LRA_MIN: f64 = 1.0;
pub const EBU_LRA_MAX: f64 = 30.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProgramType {
General,
Drama,
Documentary,
Sports,
Music,
News,
Commercial,
}
impl ProgramType {
pub fn name(&self) -> &'static str {
match self {
Self::General => "General Broadcast",
Self::Drama => "Drama",
Self::Documentary => "Documentary",
Self::Sports => "Sports",
Self::Music => "Music",
Self::News => "News",
Self::Commercial => "Commercial",
}
}
pub fn typical_lra_range(&self) -> (f64, f64) {
match self {
Self::General => (5.0, 15.0),
Self::Drama => (8.0, 18.0),
Self::Documentary => (6.0, 16.0),
Self::Sports => (4.0, 12.0),
Self::Music => (3.0, 10.0),
Self::News => (3.0, 8.0),
Self::Commercial => (2.0, 6.0),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ComplianceStatus {
Compliant,
TooLoud,
TooQuiet,
PeakExceeded,
Multiple,
Unknown,
}
impl ComplianceStatus {
pub fn is_compliant(&self) -> bool {
matches!(self, Self::Compliant)
}
pub fn description(&self) -> &'static str {
match self {
Self::Compliant => "Compliant with EBU R128",
Self::TooLoud => "Programme loudness exceeds +1 LU tolerance",
Self::TooQuiet => "Programme loudness exceeds -1 LU tolerance",
Self::PeakExceeded => "True peak exceeds -1.0 dBTP",
Self::Multiple => "Multiple compliance issues",
Self::Unknown => "Insufficient data for compliance check",
}
}
}
#[derive(Clone, Debug)]
pub struct EbuR128Compliance {
pub status: ComplianceStatus,
pub integrated_lufs: f64,
pub true_peak_dbtp: f64,
pub loudness_range: f64,
pub deviation_lu: f64,
pub loudness_ok: bool,
pub peak_ok: bool,
pub lra_ok: bool,
}
impl EbuR128Compliance {
pub fn from_metrics(metrics: &LoudnessMetrics) -> Self {
let integrated = metrics.integrated_lufs;
let peak = metrics.true_peak_dbtp;
let lra = metrics.loudness_range;
let loudness_ok = if integrated.is_finite() {
(EBU_TARGET_LUFS - EBU_TOLERANCE_LU..=EBU_TARGET_LUFS + EBU_TOLERANCE_LU)
.contains(&integrated)
} else {
false
};
let peak_ok = peak <= EBU_MAX_TRUEPEAK_DBTP;
let lra_ok = (EBU_LRA_MIN..=EBU_LRA_MAX).contains(&lra);
let deviation = if integrated.is_finite() {
integrated - EBU_TARGET_LUFS
} else {
0.0
};
let status = if !integrated.is_finite() {
ComplianceStatus::Unknown
} else if loudness_ok && peak_ok {
ComplianceStatus::Compliant
} else if !loudness_ok && !peak_ok {
ComplianceStatus::Multiple
} else if !peak_ok {
ComplianceStatus::PeakExceeded
} else if deviation > EBU_TOLERANCE_LU {
ComplianceStatus::TooLoud
} else {
ComplianceStatus::TooQuiet
};
Self {
status,
integrated_lufs: integrated,
true_peak_dbtp: peak,
loudness_range: lra,
deviation_lu: deviation,
loudness_ok,
peak_ok,
lra_ok,
}
}
pub fn recommended_gain(&self) -> f64 {
if self.integrated_lufs.is_finite() {
EBU_TARGET_LUFS - self.integrated_lufs
} else {
0.0
}
}
pub fn would_clip(&self, gain_db: f64) -> bool {
let adjusted_peak = self.true_peak_dbtp + gain_db;
adjusted_peak > EBU_MAX_TRUEPEAK_DBTP
}
pub fn safe_gain(&self) -> f64 {
let desired_gain = self.recommended_gain();
let max_safe_gain = EBU_MAX_TRUEPEAK_DBTP - self.true_peak_dbtp;
desired_gain.min(max_safe_gain)
}
}
pub struct EbuR128Meter {
meter: LoudnessMeter,
program_type: ProgramType,
}
impl EbuR128Meter {
pub fn new(
sample_rate: f64,
channels: usize,
program_type: ProgramType,
) -> crate::MeteringResult<Self> {
let config = MeterConfig::new(Standard::EbuR128, sample_rate, channels);
let meter = LoudnessMeter::new(config)?;
Ok(Self {
meter,
program_type,
})
}
pub fn process_f32(&mut self, samples: &[f32]) {
self.meter.process_f32(samples);
}
pub fn process_f64(&mut self, samples: &[f64]) {
self.meter.process_f64(samples);
}
pub fn metrics(&mut self) -> LoudnessMetrics {
self.meter.metrics()
}
pub fn check_compliance(&mut self) -> EbuR128Compliance {
let metrics = self.metrics();
EbuR128Compliance::from_metrics(&metrics)
}
pub fn is_lra_typical(&mut self) -> bool {
let metrics = self.metrics();
let (min, max) = self.program_type.typical_lra_range();
metrics.loudness_range >= min && metrics.loudness_range <= max
}
pub fn program_type(&self) -> ProgramType {
self.program_type
}
pub fn set_program_type(&mut self, program_type: ProgramType) {
self.program_type = program_type;
}
pub fn reset(&mut self) {
self.meter.reset();
}
pub fn meter(&self) -> &LoudnessMeter {
&self.meter
}
pub fn meter_mut(&mut self) -> &mut LoudnessMeter {
&mut self.meter
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ebu_constants() {
assert_eq!(EBU_TARGET_LUFS, -23.0);
assert_eq!(EBU_TOLERANCE_LU, 1.0);
assert_eq!(EBU_MAX_TRUEPEAK_DBTP, -1.0);
}
#[test]
fn test_program_type_names() {
assert_eq!(ProgramType::General.name(), "General Broadcast");
assert_eq!(ProgramType::Drama.name(), "Drama");
}
#[test]
fn test_program_type_lra_ranges() {
let (min, max) = ProgramType::News.typical_lra_range();
assert!(min < max);
assert!(min >= 0.0);
}
#[test]
fn test_compliance_status_descriptions() {
assert!(!ComplianceStatus::TooLoud.is_compliant());
assert!(ComplianceStatus::Compliant.is_compliant());
}
#[test]
fn test_compliance_from_metrics() {
let metrics = LoudnessMetrics {
integrated_lufs: -23.0,
true_peak_dbtp: -2.0,
loudness_range: 10.0,
..Default::default()
};
let compliance = EbuR128Compliance::from_metrics(&metrics);
assert!(compliance.status.is_compliant());
assert!(compliance.loudness_ok);
assert!(compliance.peak_ok);
}
#[test]
fn test_compliance_too_loud() {
let metrics = LoudnessMetrics {
integrated_lufs: -20.0,
true_peak_dbtp: -2.0,
loudness_range: 10.0,
..Default::default()
};
let compliance = EbuR128Compliance::from_metrics(&metrics);
assert_eq!(compliance.status, ComplianceStatus::TooLoud);
}
#[test]
fn test_compliance_peak_exceeded() {
let metrics = LoudnessMetrics {
integrated_lufs: -23.0,
true_peak_dbtp: 0.5,
loudness_range: 10.0,
..Default::default()
};
let compliance = EbuR128Compliance::from_metrics(&metrics);
assert_eq!(compliance.status, ComplianceStatus::PeakExceeded);
}
#[test]
fn test_recommended_gain() {
let metrics = LoudnessMetrics {
integrated_lufs: -20.0,
true_peak_dbtp: -2.0,
loudness_range: 10.0,
..Default::default()
};
let compliance = EbuR128Compliance::from_metrics(&metrics);
assert_eq!(compliance.recommended_gain(), -3.0);
}
#[test]
fn test_safe_gain() {
let metrics = LoudnessMetrics {
integrated_lufs: -30.0,
true_peak_dbtp: -2.0,
loudness_range: 10.0,
..Default::default()
};
let compliance = EbuR128Compliance::from_metrics(&metrics);
let safe = compliance.safe_gain();
assert!(compliance.true_peak_dbtp + safe <= EBU_MAX_TRUEPEAK_DBTP);
}
#[test]
fn test_would_clip() {
let metrics = LoudnessMetrics {
integrated_lufs: -25.0,
true_peak_dbtp: -1.5,
loudness_range: 10.0,
..Default::default()
};
let compliance = EbuR128Compliance::from_metrics(&metrics);
assert!(compliance.would_clip(1.0)); assert!(!compliance.would_clip(0.4)); }
}