#![forbid(unsafe_code)]
#![allow(clippy::cast_lossless)]
const ABSOLUTE_GATE_LUFS: f64 = -70.0;
const RELATIVE_GATE_OFFSET_LU: f64 = 10.0;
#[derive(Clone, Debug)]
pub struct GatingProcessor {
sample_rate: f64,
channels: usize,
channel_weights: Vec<f64>,
}
impl GatingProcessor {
#[must_use]
pub fn new(sample_rate: f64, channels: usize) -> Self {
let channel_weights = Self::calculate_channel_weights(channels);
Self {
sample_rate,
channels,
channel_weights,
}
}
fn calculate_channel_weights(channels: usize) -> Vec<f64> {
match channels {
1 => vec![1.0], 2 => vec![1.0, 1.0], 3 => vec![1.0, 1.0, 0.0], 5 => vec![1.0, 1.0, 1.0, 1.41, 1.41], 6 => vec![1.0, 1.0, 1.0, 0.0, 1.41, 1.41], 8 => vec![1.0, 1.0, 1.0, 0.0, 1.41, 1.41, 1.41, 1.41], _ => vec![1.0; channels], }
}
#[must_use]
pub fn calculate_block_power(&self, samples: &[f64]) -> f64 {
if samples.is_empty() || self.channels == 0 {
return 0.0;
}
let frames = samples.len() / self.channels;
if frames == 0 {
return 0.0;
}
let mut power_sum = 0.0;
let mut weight_sum = 0.0;
for frame in 0..frames {
for ch in 0..self.channels {
let idx = frame * self.channels + ch;
let weight = self.channel_weights.get(ch).copied().unwrap_or(1.0);
if weight > 0.0 {
let sample = samples.get(idx).copied().unwrap_or(0.0);
power_sum += weight * sample * sample;
weight_sum += weight;
}
}
}
if weight_sum > 0.0 {
power_sum / (weight_sum * frames as f64)
} else {
0.0
}
}
#[must_use]
pub fn calculate_block_power_planar(&self, channels: &[&[f64]]) -> f64 {
if channels.is_empty() {
return 0.0;
}
let frames = channels[0].len();
if frames == 0 {
return 0.0;
}
let mut power_sum = 0.0;
let mut weight_sum = 0.0;
for (ch_idx, samples) in channels.iter().enumerate() {
let weight = self.channel_weights.get(ch_idx).copied().unwrap_or(1.0);
if weight > 0.0 {
for &sample in samples.iter().take(frames) {
power_sum += weight * sample * sample;
}
weight_sum += weight;
}
}
if weight_sum > 0.0 {
power_sum / (weight_sum * frames as f64)
} else {
0.0
}
}
#[must_use]
pub fn power_to_lufs(power: f64) -> f64 {
if power <= 0.0 {
f64::NEG_INFINITY
} else {
-0.691 + 10.0 * power.log10()
}
}
#[must_use]
pub fn lufs_to_power(lufs: f64) -> f64 {
if lufs.is_infinite() && lufs.is_sign_negative() {
0.0
} else {
10.0_f64.powf((lufs + 0.691) / 10.0)
}
}
#[must_use]
pub fn calculate_gated_loudness(&self, block_powers: &[f64]) -> f64 {
if block_powers.is_empty() {
return f64::NEG_INFINITY;
}
let absolute_threshold = Self::lufs_to_power(ABSOLUTE_GATE_LUFS);
let mut gated_powers: Vec<f64> = block_powers
.iter()
.copied()
.filter(|&p| p >= absolute_threshold)
.collect();
if gated_powers.is_empty() {
return f64::NEG_INFINITY;
}
let ungated_power: f64 = gated_powers.iter().sum::<f64>() / gated_powers.len() as f64;
let ungated_lufs = Self::power_to_lufs(ungated_power);
let relative_threshold_lufs = ungated_lufs - RELATIVE_GATE_OFFSET_LU;
let relative_threshold = Self::lufs_to_power(relative_threshold_lufs);
gated_powers.retain(|&p| p >= relative_threshold);
if gated_powers.is_empty() {
return f64::NEG_INFINITY;
}
let gated_power: f64 = gated_powers.iter().sum::<f64>() / gated_powers.len() as f64;
Self::power_to_lufs(gated_power)
}
#[must_use]
pub fn calculate_loudness_range(&self, block_powers: &[f64]) -> f64 {
if block_powers.is_empty() {
return 0.0;
}
let absolute_threshold = Self::lufs_to_power(ABSOLUTE_GATE_LUFS);
let mut gated_lufs: Vec<f64> = block_powers
.iter()
.copied()
.filter(|&p| p >= absolute_threshold)
.map(Self::power_to_lufs)
.collect();
if gated_lufs.len() < 2 {
return 0.0;
}
gated_lufs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let low_idx = ((gated_lufs.len() - 1) as f64 * 0.10) as usize;
let high_idx = ((gated_lufs.len() - 1) as f64 * 0.95) as usize;
let low_percentile = gated_lufs[low_idx];
let high_percentile = gated_lufs[high_idx];
(high_percentile - low_percentile).max(0.0)
}
#[must_use]
pub fn absolute_gate_threshold() -> f64 {
ABSOLUTE_GATE_LUFS
}
#[must_use]
pub fn relative_gate_offset() -> f64 {
RELATIVE_GATE_OFFSET_LU
}
#[must_use]
pub fn channel_weights(&self) -> &[f64] {
&self.channel_weights
}
}
#[derive(Clone, Debug)]
pub struct BlockAccumulator {
gating: GatingProcessor,
block_powers: Vec<f64>,
block_size: usize,
current_block: Vec<f64>,
current_count: usize,
}
impl BlockAccumulator {
#[must_use]
pub fn new(sample_rate: f64, channels: usize, block_duration_ms: f64) -> Self {
let gating = GatingProcessor::new(sample_rate, channels);
let block_size = (sample_rate * block_duration_ms / 1000.0) as usize * channels;
Self {
gating,
block_powers: Vec::new(),
block_size,
current_block: vec![0.0; block_size],
current_count: 0,
}
}
pub fn add_samples(&mut self, samples: &[f64]) {
let mut offset = 0;
while offset < samples.len() {
let space_left = self.block_size - self.current_count;
let to_copy = (samples.len() - offset).min(space_left);
self.current_block[self.current_count..self.current_count + to_copy]
.copy_from_slice(&samples[offset..offset + to_copy]);
self.current_count += to_copy;
offset += to_copy;
if self.current_count >= self.block_size {
let power = self.gating.calculate_block_power(&self.current_block);
self.block_powers.push(power);
self.current_count = 0;
}
}
}
#[must_use]
pub fn integrated_loudness(&self) -> f64 {
self.gating.calculate_gated_loudness(&self.block_powers)
}
#[must_use]
pub fn loudness_range(&self) -> f64 {
self.gating.calculate_loudness_range(&self.block_powers)
}
#[must_use]
pub fn block_count(&self) -> usize {
self.block_powers.len()
}
pub fn reset(&mut self) {
self.block_powers.clear();
self.current_count = 0;
}
#[must_use]
pub fn block_powers(&self) -> &[f64] {
&self.block_powers
}
}