const ABSOLUTE_GATE: f64 = -70.0;
const RELATIVE_GATE_OFFSET: f64 = -10.0;
const GATING_BLOCK_MS: f64 = 400.0;
const GATING_OVERLAP: f64 = 0.75;
#[derive(Clone, Copy, Debug)]
struct GatingBlock {
loudness: f64,
power: f64,
timestamp: f64,
}
impl GatingBlock {
fn new(loudness: f64, power: f64, timestamp: f64) -> Self {
Self {
loudness,
power,
timestamp,
}
}
}
pub struct GatingProcessor {
sample_rate: f64,
channels: usize,
block_size: usize,
block_accumulator: Vec<f64>,
block_sample_count: usize,
channel_weights: Vec<f64>,
blocks: Vec<GatingBlock>,
timestamp: f64,
total_samples: usize,
}
impl GatingProcessor {
pub fn new(sample_rate: f64, channels: usize) -> Self {
let block_size = (sample_rate * GATING_BLOCK_MS / 1000.0) as usize;
let channel_weights = Self::calculate_channel_weights(channels);
Self {
sample_rate,
channels,
block_size,
block_accumulator: vec![0.0; channels],
block_sample_count: 0,
channel_weights,
blocks: Vec::new(),
timestamp: 0.0,
total_samples: 0,
}
}
pub fn process_interleaved(&mut self, samples: &[f64]) {
let frames = samples.len() / self.channels;
for frame in 0..frames {
for ch in 0..self.channels {
let idx = frame * self.channels + ch;
if idx < samples.len() {
let sample = samples[idx];
self.block_accumulator[ch] += sample * sample * self.channel_weights[ch];
}
}
self.block_sample_count += 1;
self.total_samples += 1;
if self.block_sample_count >= self.block_size {
self.complete_block();
}
self.timestamp = self.total_samples as f64 / self.sample_rate;
}
}
fn complete_block(&mut self) {
if self.block_sample_count == 0 {
return;
}
let mut block_ms = 0.0;
for &channel_ms in &self.block_accumulator {
block_ms += channel_ms / self.block_sample_count as f64;
}
let total_weight: f64 = self.channel_weights.iter().sum();
if total_weight > 0.0 {
block_ms /= total_weight;
}
let loudness = if block_ms > 0.0 {
-0.691 + 10.0 * block_ms.log10()
} else {
f64::NEG_INFINITY
};
self.blocks
.push(GatingBlock::new(loudness, block_ms, self.timestamp));
self.block_accumulator.fill(0.0);
self.block_sample_count = 0;
}
pub fn integrated_loudness(&self) -> f64 {
if self.blocks.is_empty() {
return f64::NEG_INFINITY;
}
let absolute_gated: Vec<&GatingBlock> = self
.blocks
.iter()
.filter(|block| block.loudness >= ABSOLUTE_GATE)
.collect();
if absolute_gated.is_empty() {
return f64::NEG_INFINITY;
}
let ungated_sum: f64 = absolute_gated.iter().map(|block| block.power).sum();
let ungated_mean = ungated_sum / absolute_gated.len() as f64;
let ungated_loudness = if ungated_mean > 0.0 {
-0.691 + 10.0 * ungated_mean.log10()
} else {
f64::NEG_INFINITY
};
let relative_gate = ungated_loudness + RELATIVE_GATE_OFFSET;
let relative_gated: Vec<&GatingBlock> = absolute_gated
.into_iter()
.filter(|block| block.loudness >= relative_gate)
.collect();
if relative_gated.is_empty() {
return f64::NEG_INFINITY;
}
let gated_sum: f64 = relative_gated.iter().map(|block| block.power).sum();
let gated_mean = gated_sum / relative_gated.len() as f64;
if gated_mean > 0.0 {
-0.691 + 10.0 * gated_mean.log10()
} else {
f64::NEG_INFINITY
}
}
pub fn get_blocks_for_lra(&self) -> Vec<f64> {
self.blocks
.iter()
.filter(|block| block.loudness >= ABSOLUTE_GATE)
.map(|block| block.loudness)
.collect()
}
pub fn get_all_blocks(&self) -> Vec<(f64, f64)> {
self.blocks
.iter()
.map(|block| (block.loudness, block.timestamp))
.collect()
}
pub fn block_count(&self) -> usize {
self.blocks.len()
}
fn calculate_channel_weights(channels: usize) -> Vec<f64> {
match channels {
1 => vec![1.0],
2 => vec![1.0, 1.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],
}
}
pub fn reset(&mut self) {
self.block_accumulator.fill(0.0);
self.block_sample_count = 0;
self.blocks.clear();
self.timestamp = 0.0;
self.total_samples = 0;
}
}
#[derive(Clone, Debug)]
pub struct GatingResult {
pub integrated_lufs: f64,
pub total_blocks: usize,
pub absolute_gated_count: usize,
pub relative_gated_count: usize,
pub ungated_lufs: f64,
pub relative_gate_threshold: f64,
}
impl GatingResult {
pub fn is_valid(&self) -> bool {
self.integrated_lufs.is_finite() && self.relative_gated_count > 0
}
pub fn gated_percentage(&self) -> f64 {
if self.total_blocks == 0 {
return 0.0;
}
(1.0 - (self.relative_gated_count as f64 / self.total_blocks as f64)) * 100.0
}
}
pub fn calculate_gating_result(processor: &GatingProcessor) -> GatingResult {
let integrated = processor.integrated_loudness();
let total_blocks = processor.block_count();
let blocks = processor.blocks.clone();
let absolute_gated: Vec<&GatingBlock> = blocks
.iter()
.filter(|block| block.loudness >= ABSOLUTE_GATE)
.collect();
let absolute_gated_count = absolute_gated.len();
let (ungated_lufs, relative_gate_threshold, relative_gated_count) = if absolute_gated.is_empty()
{
(f64::NEG_INFINITY, ABSOLUTE_GATE, 0)
} else {
let ungated_sum: f64 = absolute_gated.iter().map(|block| block.power).sum();
let ungated_mean = ungated_sum / absolute_gated.len() as f64;
let ungated = if ungated_mean > 0.0 {
-0.691 + 10.0 * ungated_mean.log10()
} else {
f64::NEG_INFINITY
};
let rel_gate = ungated + RELATIVE_GATE_OFFSET;
let rel_count = absolute_gated
.iter()
.filter(|block| block.loudness >= rel_gate)
.count();
(ungated, rel_gate, rel_count)
};
GatingResult {
integrated_lufs: integrated,
total_blocks,
absolute_gated_count,
relative_gated_count,
ungated_lufs,
relative_gate_threshold,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gating_processor_creates() {
let processor = GatingProcessor::new(48000.0, 2);
assert_eq!(processor.block_count(), 0);
}
#[test]
fn test_gating_constants() {
assert_eq!(ABSOLUTE_GATE, -70.0);
assert_eq!(RELATIVE_GATE_OFFSET, -10.0);
}
#[test]
fn test_empty_processor_returns_neg_infinity() {
let processor = GatingProcessor::new(48000.0, 2);
assert_eq!(processor.integrated_loudness(), f64::NEG_INFINITY);
}
#[test]
fn test_gating_result_validity() {
let result = GatingResult {
integrated_lufs: -23.0,
total_blocks: 100,
absolute_gated_count: 90,
relative_gated_count: 80,
ungated_lufs: -22.0,
relative_gate_threshold: -32.0,
};
assert!(result.is_valid());
}
#[test]
fn test_gated_percentage() {
let result = GatingResult {
integrated_lufs: -23.0,
total_blocks: 100,
absolute_gated_count: 90,
relative_gated_count: 50,
ungated_lufs: -22.0,
relative_gate_threshold: -32.0,
};
assert_eq!(result.gated_percentage(), 50.0);
}
}