#![forbid(unsafe_code)]
#![allow(clippy::cast_lossless)]
use super::filter::KWeightFilterBank;
use super::gate::{BlockAccumulator, GatingProcessor};
use super::peak::TruePeakDetector;
const MOMENTARY_WINDOW_MS: f64 = 400.0;
const SHORT_TERM_WINDOW_MS: f64 = 3000.0;
const BLOCK_OVERLAP: f64 = 0.75;
#[derive(Clone, Debug)]
pub struct R128Meter {
sample_rate: f64,
channels: usize,
filter_bank: KWeightFilterBank,
gating: GatingProcessor,
momentary_window: SlidingWindow,
short_term_window: SlidingWindow,
integrated_blocks: BlockAccumulator,
peak_detector: TruePeakDetector,
filtered_buffer: Vec<f64>,
momentary_loudness: f64,
short_term_loudness: f64,
max_momentary: f64,
max_short_term: f64,
true_peak: f64,
}
impl R128Meter {
#[must_use]
pub fn new(sample_rate: f64, channels: usize) -> Self {
let filter_bank = KWeightFilterBank::new(channels, sample_rate);
let gating = GatingProcessor::new(sample_rate, channels);
let momentary_samples = (sample_rate * MOMENTARY_WINDOW_MS / 1000.0) as usize * channels;
let short_term_samples = (sample_rate * SHORT_TERM_WINDOW_MS / 1000.0) as usize * channels;
let momentary_window = SlidingWindow::new(momentary_samples, BLOCK_OVERLAP);
let short_term_window = SlidingWindow::new(short_term_samples, BLOCK_OVERLAP);
let integrated_blocks = BlockAccumulator::new(sample_rate, channels, MOMENTARY_WINDOW_MS);
let peak_detector = TruePeakDetector::new(sample_rate, channels);
Self {
sample_rate,
channels,
filter_bank,
gating,
momentary_window,
short_term_window,
integrated_blocks,
peak_detector,
filtered_buffer: Vec::new(),
momentary_loudness: f64::NEG_INFINITY,
short_term_loudness: f64::NEG_INFINITY,
max_momentary: f64::NEG_INFINITY,
max_short_term: f64::NEG_INFINITY,
true_peak: 0.0,
}
}
pub fn process_interleaved(&mut self, samples: &[f64]) {
if samples.is_empty() {
return;
}
self.filtered_buffer.resize(samples.len(), 0.0);
self.filter_bank
.process_interleaved(samples, self.channels, &mut self.filtered_buffer);
let peak = self.peak_detector.process_interleaved(samples);
self.true_peak = self.true_peak.max(peak);
if let Some(window_samples) = self.momentary_window.add_samples(&self.filtered_buffer) {
let power = self.gating.calculate_block_power(window_samples);
self.momentary_loudness = GatingProcessor::power_to_lufs(power);
self.max_momentary = self.max_momentary.max(self.momentary_loudness);
}
if let Some(window_samples) = self.short_term_window.add_samples(&self.filtered_buffer) {
let power = self.gating.calculate_block_power(window_samples);
self.short_term_loudness = GatingProcessor::power_to_lufs(power);
self.max_short_term = self.max_short_term.max(self.short_term_loudness);
}
self.integrated_blocks.add_samples(&self.filtered_buffer);
}
pub fn process_planar(&mut self, channels: &mut [Vec<f64>]) {
if channels.is_empty() {
return;
}
self.filter_bank.process_planar(channels);
let num_channels = channels.len();
let num_frames = channels[0].len();
let mut interleaved = vec![0.0; num_frames * num_channels];
for frame in 0..num_frames {
for (ch_idx, ch_samples) in channels.iter().enumerate() {
interleaved[frame * num_channels + ch_idx] = ch_samples[frame];
}
}
self.process_interleaved(&interleaved);
}
#[must_use]
pub fn momentary_loudness(&self) -> f64 {
self.momentary_loudness
}
#[must_use]
pub fn short_term_loudness(&self) -> f64 {
self.short_term_loudness
}
#[must_use]
pub fn integrated_loudness(&self) -> f64 {
self.integrated_blocks.integrated_loudness()
}
#[must_use]
pub fn loudness_range(&self) -> f64 {
self.integrated_blocks.loudness_range()
}
#[must_use]
pub fn max_momentary(&self) -> f64 {
self.max_momentary
}
#[must_use]
pub fn max_short_term(&self) -> f64 {
self.max_short_term
}
#[must_use]
pub fn true_peak_dbtp(&self) -> f64 {
TruePeakDetector::linear_to_dbtp(self.true_peak)
}
#[must_use]
pub fn true_peak_linear(&self) -> f64 {
self.true_peak
}
#[must_use]
pub fn channel_peaks(&self) -> Vec<f64> {
self.peak_detector.get_all_peaks()
}
pub fn reset(&mut self) {
self.filter_bank.reset();
self.momentary_window.reset();
self.short_term_window.reset();
self.integrated_blocks.reset();
self.peak_detector.reset();
self.filtered_buffer.clear();
self.momentary_loudness = f64::NEG_INFINITY;
self.short_term_loudness = f64::NEG_INFINITY;
self.max_momentary = f64::NEG_INFINITY;
self.max_short_term = f64::NEG_INFINITY;
self.true_peak = 0.0;
}
#[must_use]
pub fn sample_rate(&self) -> f64 {
self.sample_rate
}
#[must_use]
pub fn channels(&self) -> usize {
self.channels
}
#[must_use]
pub fn has_valid_integrated(&self) -> bool {
self.integrated_blocks.block_count() > 0 && !self.integrated_loudness().is_infinite()
}
#[must_use]
pub fn samples_processed(&self) -> usize {
self.momentary_window.total_samples_added() / self.channels
}
}
#[derive(Clone, Debug)]
struct SlidingWindow {
buffer: Vec<f64>,
window_size: usize,
hop_size: usize,
position: usize,
samples_since_hop: usize,
total_added: usize,
}
impl SlidingWindow {
fn new(window_size: usize, overlap: f64) -> Self {
let hop_size = ((1.0 - overlap) * window_size as f64) as usize;
Self {
buffer: vec![0.0; window_size],
window_size,
hop_size: hop_size.max(1),
position: 0,
samples_since_hop: 0,
total_added: 0,
}
}
fn add_samples(&mut self, samples: &[f64]) -> Option<&[f64]> {
let mut window_ready = false;
for &sample in samples {
self.buffer[self.position] = sample;
self.position = (self.position + 1) % self.window_size;
self.samples_since_hop += 1;
self.total_added += 1;
if self.samples_since_hop >= self.hop_size && self.total_added >= self.window_size {
window_ready = true;
self.samples_since_hop = 0;
}
}
if window_ready {
Some(self.get_window())
} else {
None
}
}
fn get_window(&self) -> &[f64] {
&self.buffer
}
fn reset(&mut self) {
self.buffer.fill(0.0);
self.position = 0;
self.samples_since_hop = 0;
self.total_added = 0;
}
fn total_samples_added(&self) -> usize {
self.total_added
}
}
pub type AtscA85Meter = R128Meter;
pub trait AtscA85Ext {
fn integrated_lkfs(&self) -> f64;
fn momentary_lkfs(&self) -> f64;
fn short_term_lkfs(&self) -> f64;
fn check_atsc_compliance(&self) -> ComplianceStatus;
}
impl AtscA85Ext for AtscA85Meter {
fn integrated_lkfs(&self) -> f64 {
self.integrated_loudness()
}
fn momentary_lkfs(&self) -> f64 {
self.momentary_loudness()
}
fn short_term_lkfs(&self) -> f64 {
self.short_term_loudness()
}
fn check_atsc_compliance(&self) -> ComplianceStatus {
let target = -24.0;
let tolerance = 2.0;
let lkfs = self.integrated_lkfs();
if lkfs.is_infinite() {
ComplianceStatus::Unknown
} else if lkfs >= target - tolerance && lkfs <= target + tolerance {
ComplianceStatus::Compliant
} else if lkfs > target + tolerance {
ComplianceStatus::TooLoud(lkfs - target)
} else {
ComplianceStatus::TooQuiet(target - lkfs)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ComplianceStatus {
Compliant,
TooLoud(f64),
TooQuiet(f64),
Unknown,
}
impl ComplianceStatus {
#[must_use]
pub fn is_compliant(&self) -> bool {
matches!(self, Self::Compliant)
}
#[must_use]
pub fn deviation(&self) -> Option<f64> {
match self {
Self::TooLoud(db) => Some(*db),
Self::TooQuiet(db) => Some(-*db),
Self::Compliant => Some(0.0),
Self::Unknown => None,
}
}
}
pub struct R128Compliance;
impl R128Compliance {
#[must_use]
pub fn check_program_loudness(integrated_lufs: f64) -> ComplianceStatus {
let target = -23.0;
let tolerance = 1.0;
if integrated_lufs.is_infinite() {
ComplianceStatus::Unknown
} else if integrated_lufs >= target - tolerance && integrated_lufs <= target + tolerance {
ComplianceStatus::Compliant
} else if integrated_lufs > target + tolerance {
ComplianceStatus::TooLoud(integrated_lufs - target)
} else {
ComplianceStatus::TooQuiet(target - integrated_lufs)
}
}
#[must_use]
pub fn check_true_peak(true_peak_dbtp: f64) -> bool {
true_peak_dbtp <= -1.0
}
#[must_use]
pub fn check_loudness_range(lra: f64) -> bool {
lra >= 1.0 && lra <= 30.0
}
#[must_use]
pub fn recommended_gain_adjustment(measured_lufs: f64, target_lufs: f64) -> f64 {
if measured_lufs.is_infinite() {
0.0
} else {
target_lufs - measured_lufs
}
}
}