use crate::{LoudnessMeter, LoudnessMetrics, MeterConfig, Standard};
pub const ATSC_TARGET_LKFS: f64 = -24.0;
pub const ATSC_TOLERANCE_DB: f64 = 2.0;
pub const ATSC_MAX_TRUEPEAK_DBTP: f64 = -2.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AtscProgramType {
General,
Commercial,
News,
Sports,
LongForm,
}
impl AtscProgramType {
pub fn name(&self) -> &'static str {
match self {
Self::General => "General",
Self::Commercial => "Commercial",
Self::News => "News",
Self::Sports => "Sports",
Self::LongForm => "Long-form",
}
}
pub fn target_lkfs(&self) -> f64 {
match self {
Self::General | Self::Commercial | Self::News | Self::Sports => ATSC_TARGET_LKFS,
Self::LongForm => -24.0, }
}
pub fn tolerance(&self) -> f64 {
match self {
Self::Commercial => 2.0, _ => 2.0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AtscComplianceStatus {
Compliant,
TooLoud,
TooQuiet,
PeakExceeded,
Multiple,
Unknown,
}
impl AtscComplianceStatus {
pub fn is_compliant(&self) -> bool {
matches!(self, Self::Compliant)
}
pub fn description(&self) -> &'static str {
match self {
Self::Compliant => "Compliant with ATSC A/85",
Self::TooLoud => "Programme loudness exceeds +2 dB tolerance",
Self::TooQuiet => "Programme loudness exceeds -2 dB tolerance",
Self::PeakExceeded => "True peak exceeds -2.0 dBTP",
Self::Multiple => "Multiple compliance issues",
Self::Unknown => "Insufficient data for compliance check",
}
}
}
#[derive(Clone, Debug)]
pub struct AtscA85Compliance {
pub status: AtscComplianceStatus,
pub integrated_lkfs: f64,
pub true_peak_dbtp: f64,
pub loudness_range: f64,
pub deviation_db: f64,
pub loudness_ok: bool,
pub peak_ok: bool,
pub program_type: AtscProgramType,
}
impl AtscA85Compliance {
pub fn from_metrics(metrics: &LoudnessMetrics, program_type: AtscProgramType) -> Self {
let target = program_type.target_lkfs();
let tolerance = program_type.tolerance();
let integrated = metrics.integrated_lufs; let peak = metrics.true_peak_dbtp;
let lra = metrics.loudness_range;
let loudness_ok = if integrated.is_finite() {
integrated >= target - tolerance && integrated <= target + tolerance
} else {
false
};
let peak_ok = peak <= ATSC_MAX_TRUEPEAK_DBTP;
let deviation = if integrated.is_finite() {
integrated - target
} else {
0.0
};
let status = if !integrated.is_finite() {
AtscComplianceStatus::Unknown
} else if loudness_ok && peak_ok {
AtscComplianceStatus::Compliant
} else if !loudness_ok && !peak_ok {
AtscComplianceStatus::Multiple
} else if !peak_ok {
AtscComplianceStatus::PeakExceeded
} else if deviation > tolerance {
AtscComplianceStatus::TooLoud
} else {
AtscComplianceStatus::TooQuiet
};
Self {
status,
integrated_lkfs: integrated,
true_peak_dbtp: peak,
loudness_range: lra,
deviation_db: deviation,
loudness_ok,
peak_ok,
program_type,
}
}
pub fn recommended_gain(&self) -> f64 {
if self.integrated_lkfs.is_finite() {
self.program_type.target_lkfs() - self.integrated_lkfs
} else {
0.0
}
}
pub fn would_clip(&self, gain_db: f64) -> bool {
let adjusted_peak = self.true_peak_dbtp + gain_db;
adjusted_peak > ATSC_MAX_TRUEPEAK_DBTP
}
pub fn safe_gain(&self) -> f64 {
let desired_gain = self.recommended_gain();
let max_safe_gain = ATSC_MAX_TRUEPEAK_DBTP - self.true_peak_dbtp;
desired_gain.min(max_safe_gain)
}
}
pub struct AtscA85Meter {
meter: LoudnessMeter,
program_type: AtscProgramType,
}
impl AtscA85Meter {
pub fn new(
sample_rate: f64,
channels: usize,
program_type: AtscProgramType,
) -> crate::MeteringResult<Self> {
let config = MeterConfig::new(Standard::AtscA85, 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) -> AtscA85Compliance {
let metrics = self.metrics();
AtscA85Compliance::from_metrics(&metrics, self.program_type)
}
pub fn program_type(&self) -> AtscProgramType {
self.program_type
}
pub fn set_program_type(&mut self, program_type: AtscProgramType) {
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
}
}
pub fn is_lkfs_compliant(lkfs: f64, program_type: AtscProgramType) -> bool {
if !lkfs.is_finite() {
return false;
}
let target = program_type.target_lkfs();
let tolerance = program_type.tolerance();
lkfs >= target - tolerance && lkfs <= target + tolerance
}
pub fn is_peak_compliant(true_peak_dbtp: f64) -> bool {
true_peak_dbtp <= ATSC_MAX_TRUEPEAK_DBTP
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_atsc_constants() {
assert_eq!(ATSC_TARGET_LKFS, -24.0);
assert_eq!(ATSC_TOLERANCE_DB, 2.0);
assert_eq!(ATSC_MAX_TRUEPEAK_DBTP, -2.0);
}
#[test]
fn test_program_type_names() {
assert_eq!(AtscProgramType::General.name(), "General");
assert_eq!(AtscProgramType::Commercial.name(), "Commercial");
}
#[test]
fn test_program_type_targets() {
assert_eq!(AtscProgramType::General.target_lkfs(), -24.0);
assert_eq!(AtscProgramType::Commercial.target_lkfs(), -24.0);
}
#[test]
fn test_compliance_status() {
assert!(AtscComplianceStatus::Compliant.is_compliant());
assert!(!AtscComplianceStatus::TooLoud.is_compliant());
}
#[test]
fn test_compliance_from_metrics() {
let metrics = LoudnessMetrics {
integrated_lufs: -24.0,
true_peak_dbtp: -3.0,
loudness_range: 10.0,
..Default::default()
};
let compliance = AtscA85Compliance::from_metrics(&metrics, AtscProgramType::General);
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: -3.0,
loudness_range: 10.0,
..Default::default()
};
let compliance = AtscA85Compliance::from_metrics(&metrics, AtscProgramType::General);
assert_eq!(compliance.status, AtscComplianceStatus::TooLoud);
}
#[test]
fn test_compliance_peak_exceeded() {
let metrics = LoudnessMetrics {
integrated_lufs: -24.0,
true_peak_dbtp: -1.0,
loudness_range: 10.0,
..Default::default()
};
let compliance = AtscA85Compliance::from_metrics(&metrics, AtscProgramType::General);
assert_eq!(compliance.status, AtscComplianceStatus::PeakExceeded);
}
#[test]
fn test_is_lkfs_compliant() {
assert!(is_lkfs_compliant(-24.0, AtscProgramType::General));
assert!(is_lkfs_compliant(-22.0, AtscProgramType::General));
assert!(is_lkfs_compliant(-26.0, AtscProgramType::General));
assert!(!is_lkfs_compliant(-20.0, AtscProgramType::General));
assert!(!is_lkfs_compliant(-28.0, AtscProgramType::General));
}
#[test]
fn test_is_peak_compliant() {
assert!(is_peak_compliant(-3.0));
assert!(is_peak_compliant(-2.0));
assert!(!is_peak_compliant(-1.0));
assert!(!is_peak_compliant(0.0));
}
#[test]
fn test_recommended_gain() {
let metrics = LoudnessMetrics {
integrated_lufs: -20.0,
true_peak_dbtp: -3.0,
loudness_range: 10.0,
..Default::default()
};
let compliance = AtscA85Compliance::from_metrics(&metrics, AtscProgramType::General);
assert_eq!(compliance.recommended_gain(), -4.0);
}
#[test]
fn test_safe_gain() {
let metrics = LoudnessMetrics {
integrated_lufs: -30.0,
true_peak_dbtp: -3.0,
loudness_range: 10.0,
..Default::default()
};
let compliance = AtscA85Compliance::from_metrics(&metrics, AtscProgramType::General);
let safe = compliance.safe_gain();
assert!(compliance.true_peak_dbtp + safe <= ATSC_MAX_TRUEPEAK_DBTP);
}
}