#![forbid(unsafe_code)]
use crate::frame::AudioFrame;
use std::collections::VecDeque;
pub struct KWeightingFilter {
pre_b: [f64; 3],
pre_a: [f64; 3],
rlb_b: [f64; 3],
rlb_a: [f64; 3],
pre_state: Vec<FilterState>,
rlb_state: Vec<FilterState>,
}
#[derive(Clone, Debug)]
struct FilterState {
x1: f64,
x2: f64,
y1: f64,
y2: f64,
}
impl FilterState {
fn new() -> Self {
Self {
x1: 0.0,
x2: 0.0,
y1: 0.0,
y2: 0.0,
}
}
fn reset(&mut self) {
self.x1 = 0.0;
self.x2 = 0.0;
self.y1 = 0.0;
self.y2 = 0.0;
}
}
impl KWeightingFilter {
pub fn new(sample_rate: f64, channels: usize) -> Self {
let f0 = 1681.974450955533;
let g = 3.999843853973347;
let q = 0.7071752369554196;
let k = (std::f64::consts::PI * f0 / sample_rate).tan();
let k2 = k * k;
let norm = 1.0 / (1.0 + k / q + k2);
let pre_b = [g * norm, -2.0 * g * norm, g * norm];
let pre_a = [1.0, 2.0 * (k2 - 1.0) * norm, (1.0 - k / q + k2) * norm];
let f0_rlb = 38.13547087602444;
let q_rlb = 0.5003270373238773;
let k_rlb = (std::f64::consts::PI * f0_rlb / sample_rate).tan();
let k2_rlb = k_rlb * k_rlb;
let norm_rlb = 1.0 / (1.0 + k_rlb / q_rlb + k2_rlb);
let rlb_b = [norm_rlb, -2.0 * norm_rlb, norm_rlb];
let rlb_a = [
1.0,
2.0 * (k2_rlb - 1.0) * norm_rlb,
(1.0 - k_rlb / q_rlb + k2_rlb) * norm_rlb,
];
Self {
pre_b,
pre_a,
rlb_b,
rlb_a,
pre_state: vec![FilterState::new(); channels],
rlb_state: vec![FilterState::new(); channels],
}
}
pub fn process(&mut self, sample: f64, channel: usize) -> f64 {
let pre_out = self.process_biquad(
sample,
&self.pre_b,
&self.pre_a,
&mut self.pre_state[channel],
);
self.process_biquad(pre_out, &self.rlb_b, &self.rlb_a, &mut self.rlb_state[channel])
}
fn process_biquad(&self, x: f64, b: &[f64; 3], a: &[f64; 3], state: &mut FilterState) -> f64 {
let y = b[0] * x + b[1] * state.x1 + b[2] * state.x2 - a[1] * state.y1 - a[2] * state.y2;
state.x2 = state.x1;
state.x1 = x;
state.y2 = state.y1;
state.y1 = y;
y
}
pub fn reset(&mut self) {
for state in &mut self.pre_state {
state.reset();
}
for state in &mut self.rlb_state {
state.reset();
}
}
}
pub struct GatingAlgorithm {
absolute_gate: f64,
relative_gate_offset: f64,
blocks: Vec<BlockMeasurement>,
}
#[derive(Clone, Debug)]
struct BlockMeasurement {
loudness: f64,
timestamp: f64,
}
impl GatingAlgorithm {
pub fn new() -> Self {
Self {
absolute_gate: -70.0,
relative_gate_offset: -10.0,
blocks: Vec::new(),
}
}
pub fn add_block(&mut self, loudness: f64, timestamp: f64) {
if loudness.is_finite() {
self.blocks.push(BlockMeasurement {
loudness,
timestamp,
});
}
}
pub fn calculate_gated_loudness(&self) -> f64 {
if self.blocks.is_empty() {
return f64::NEG_INFINITY;
}
let absolute_gated: Vec<_> = self
.blocks
.iter()
.filter(|b| b.loudness >= self.absolute_gate)
.collect();
if absolute_gated.is_empty() {
return f64::NEG_INFINITY;
}
let ungated_sum: f64 = absolute_gated
.iter()
.map(|b| 10_f64.powf(b.loudness / 10.0))
.sum();
let ungated_loudness = 10.0 * (ungated_sum / absolute_gated.len() as f64).log10();
let relative_gate = ungated_loudness + self.relative_gate_offset;
let relative_gated: Vec<_> = absolute_gated
.iter()
.filter(|b| b.loudness >= relative_gate)
.collect();
if relative_gated.is_empty() {
return f64::NEG_INFINITY;
}
let gated_sum: f64 = relative_gated
.iter()
.map(|b| 10_f64.powf(b.loudness / 10.0))
.sum();
10.0 * (gated_sum / relative_gated.len() as f64).log10()
}
pub fn absolute_gated_blocks(&self) -> Vec<f64> {
self.blocks
.iter()
.filter(|b| b.loudness >= self.absolute_gate)
.map(|b| b.loudness)
.collect()
}
pub fn reset(&mut self) {
self.blocks.clear();
}
}
impl Default for GatingAlgorithm {
fn default() -> Self {
Self::new()
}
}
pub struct Bs1770Meter {
sample_rate: f64,
channels: usize,
k_weight: KWeightingFilter,
momentary_window: SlidingWindow,
short_term_window: SlidingWindow,
gating: GatingAlgorithm,
channel_weights: Vec<f64>,
timestamp: f64,
max_momentary: f64,
max_short_term: f64,
}
struct SlidingWindow {
size: usize,
overlap: usize,
buffers: Vec<VecDeque<f64>>,
block_size: usize,
block_accumulator: Vec<f64>,
block_count: usize,
}
impl SlidingWindow {
fn new(duration_ms: usize, sample_rate: f64, channels: usize) -> Self {
let block_size = (sample_rate * 0.1) as usize; let num_blocks = duration_ms / 100;
Self {
size: num_blocks,
overlap: (num_blocks * 3) / 4,
buffers: vec![VecDeque::with_capacity(num_blocks); channels],
block_size,
block_accumulator: vec![0.0; channels],
block_count: 0,
}
}
fn add_samples(&mut self, samples: &[f64]) -> Option<f64> {
for (ch, &sample) in samples.iter().enumerate() {
self.block_accumulator[ch] += sample * sample;
}
self.block_count += 1;
if self.block_count >= self.block_size {
for ch in 0..samples.len() {
let ms = self.block_accumulator[ch] / self.block_count as f64;
self.buffers[ch].push_back(ms);
if self.buffers[ch].len() > self.size {
self.buffers[ch].pop_front();
}
}
self.block_accumulator.fill(0.0);
self.block_count = 0;
if self.buffers[0].len() == self.size {
return Some(self.calculate_loudness());
}
}
None
}
fn calculate_loudness(&self) -> f64 {
let mut sum = 0.0;
for buffer in &self.buffers {
let channel_sum: f64 = buffer.iter().sum();
sum += channel_sum / self.size as f64;
}
if sum > 0.0 {
-0.691 + 10.0 * sum.log10()
} else {
f64::NEG_INFINITY
}
}
fn reset(&mut self) {
for buffer in &mut self.buffers {
buffer.clear();
}
self.block_accumulator.fill(0.0);
self.block_count = 0;
}
}
impl Bs1770Meter {
pub fn new(sample_rate: f64, channels: usize) -> Self {
let k_weight = KWeightingFilter::new(sample_rate, channels);
let momentary_window = SlidingWindow::new(400, sample_rate, channels);
let short_term_window = SlidingWindow::new(3000, sample_rate, channels);
let gating = GatingAlgorithm::new();
let channel_weights = if channels == 1 {
vec![1.0]
} else if channels == 2 {
vec![1.0, 1.0]
} else if channels == 5 {
vec![1.0, 1.0, 1.0, 1.41, 1.41]
} else if channels == 6 {
vec![1.0, 1.0, 1.0, 0.0, 1.41, 1.41]
} else {
vec![1.0; channels]
};
Self {
sample_rate,
channels,
k_weight,
momentary_window,
short_term_window,
gating,
channel_weights,
timestamp: 0.0,
max_momentary: f64::NEG_INFINITY,
max_short_term: f64::NEG_INFINITY,
}
}
pub fn process(&mut self, samples: &[f64]) {
let frames = samples.len() / self.channels;
for frame_idx in 0..frames {
let mut k_weighted = vec![0.0; self.channels];
for ch in 0..self.channels {
let idx = frame_idx * self.channels + ch;
if idx < samples.len() {
k_weighted[ch] = self.k_weight.process(samples[idx], ch);
}
}
if let Some(momentary) = self.momentary_window.add_samples(&k_weighted) {
self.max_momentary = self.max_momentary.max(momentary);
self.gating.add_block(momentary, self.timestamp);
}
if let Some(short_term) = self.short_term_window.add_samples(&k_weighted) {
self.max_short_term = self.max_short_term.max(short_term);
}
self.timestamp += 1.0 / self.sample_rate;
}
}
pub fn momentary_loudness(&self) -> f64 {
self.momentary_window.calculate_loudness()
}
pub fn short_term_loudness(&self) -> f64 {
self.short_term_window.calculate_loudness()
}
pub fn integrated_loudness(&self) -> f64 {
self.gating.calculate_gated_loudness()
}
pub fn max_momentary(&self) -> f64 {
self.max_momentary
}
pub fn max_short_term(&self) -> f64 {
self.max_short_term
}
pub fn reset(&mut self) {
self.k_weight.reset();
self.momentary_window.reset();
self.short_term_window.reset();
self.gating.reset();
self.timestamp = 0.0;
self.max_momentary = f64::NEG_INFINITY;
self.max_short_term = f64::NEG_INFINITY;
}
pub fn gating_blocks(&self) -> Vec<f64> {
self.gating.absolute_gated_blocks()
}
}
pub struct LoudnessRangeCalculator {
histogram: Vec<(f64, usize)>,
}
impl LoudnessRangeCalculator {
pub fn new() -> Self {
Self {
histogram: Vec::new(),
}
}
pub fn calculate(&mut self, blocks: &[f64]) -> f64 {
if blocks.len() < 2 {
return 0.0;
}
self.histogram.clear();
for &loudness in blocks {
if loudness.is_finite() {
self.histogram.push((loudness, 1));
}
}
self.histogram.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let total = self.histogram.len();
let p10_idx = (total as f64 * 0.10) as usize;
let p95_idx = (total as f64 * 0.95) as usize;
if p95_idx >= total || p10_idx >= p95_idx {
return 0.0;
}
let p10 = self.histogram[p10_idx].0;
let p95 = self.histogram[p95_idx].0;
(p95 - p10).abs()
}
pub fn histogram(&self) -> &[(f64, usize)] {
&self.histogram
}
pub fn reset(&mut self) {
self.histogram.clear();
}
}
impl Default for LoudnessRangeCalculator {
fn default() -> Self {
Self::new()
}
}
pub struct Bs1864Practice {
program_type: ProgramType,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ProgramType {
GeneralBroadcast,
Drama,
Documentary,
Sports,
Music,
News,
Commercial,
Cinema,
}
impl Bs1864Practice {
pub fn new(program_type: ProgramType) -> Self {
Self { program_type }
}
pub fn target_loudness(&self) -> f64 {
match self.program_type {
ProgramType::GeneralBroadcast => -23.0,
ProgramType::Drama => -23.0,
ProgramType::Documentary => -23.0,
ProgramType::Sports => -23.0,
ProgramType::Music => -23.0,
ProgramType::News => -23.0,
ProgramType::Commercial => -23.0,
ProgramType::Cinema => -27.0,
}
}
pub fn tolerance(&self) -> f64 {
match self.program_type {
ProgramType::GeneralBroadcast => 1.0,
ProgramType::Drama => 1.0,
ProgramType::Documentary => 1.0,
ProgramType::Sports => 2.0,
ProgramType::Music => 2.0,
ProgramType::News => 1.0,
ProgramType::Commercial => 1.0,
ProgramType::Cinema => 2.0,
}
}
pub fn max_true_peak(&self) -> f64 {
match self.program_type {
ProgramType::Cinema => 0.0, _ => -1.0, }
}
pub fn check_compliance(&self, integrated: f64, true_peak: f64) -> ComplianceResult {
let target = self.target_loudness();
let tolerance = self.tolerance();
let max_peak = self.max_true_peak();
let loudness_ok = integrated >= target - tolerance && integrated <= target + tolerance;
let peak_ok = true_peak <= max_peak;
ComplianceResult {
loudness_compliant: loudness_ok,
peak_compliant: peak_ok,
target_loudness: target,
measured_loudness: integrated,
loudness_deviation: integrated - target,
max_peak_allowed: max_peak,
measured_peak: true_peak,
}
}
}
#[derive(Clone, Debug)]
pub struct ComplianceResult {
pub loudness_compliant: bool,
pub peak_compliant: bool,
pub target_loudness: f64,
pub measured_loudness: f64,
pub loudness_deviation: f64,
pub max_peak_allowed: f64,
pub measured_peak: f64,
}
pub struct Bs2217Streaming {
platform: StreamingPlatform,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum StreamingPlatform {
Spotify,
YouTube,
AppleMusic,
Netflix,
AmazonPrime,
DisneyPlus,
HboMax,
Tidal,
Deezer,
Custom { target: f64, max_peak: f64 },
}
impl Bs2217Streaming {
pub fn new(platform: StreamingPlatform) -> Self {
Self { platform }
}
pub fn target_loudness(&self) -> f64 {
match self.platform {
StreamingPlatform::Spotify => -14.0,
StreamingPlatform::YouTube => -14.0,
StreamingPlatform::AppleMusic => -16.0,
StreamingPlatform::Netflix => -27.0,
StreamingPlatform::AmazonPrime => -24.0,
StreamingPlatform::DisneyPlus => -27.0,
StreamingPlatform::HboMax => -27.0,
StreamingPlatform::Tidal => -14.0,
StreamingPlatform::Deezer => -14.0,
StreamingPlatform::Custom { target, .. } => target,
}
}
pub fn max_true_peak(&self) -> f64 {
match self.platform {
StreamingPlatform::Spotify => -1.0,
StreamingPlatform::YouTube => -1.0,
StreamingPlatform::AppleMusic => -1.0,
StreamingPlatform::Netflix => -2.0,
StreamingPlatform::AmazonPrime => -2.0,
StreamingPlatform::DisneyPlus => -2.0,
StreamingPlatform::HboMax => -2.0,
StreamingPlatform::Tidal => -1.0,
StreamingPlatform::Deezer => -1.0,
StreamingPlatform::Custom { max_peak, .. } => max_peak,
}
}
pub fn tolerance(&self) -> f64 {
match self.platform {
StreamingPlatform::Netflix
| StreamingPlatform::AmazonPrime
| StreamingPlatform::DisneyPlus
| StreamingPlatform::HboMax => 2.0,
_ => 1.0,
}
}
pub fn applies_normalization(&self) -> bool {
matches!(
self.platform,
StreamingPlatform::Spotify
| StreamingPlatform::YouTube
| StreamingPlatform::AppleMusic
| StreamingPlatform::Tidal
| StreamingPlatform::Deezer
)
}
pub fn platform_name(&self) -> &str {
match self.platform {
StreamingPlatform::Spotify => "Spotify",
StreamingPlatform::YouTube => "YouTube",
StreamingPlatform::AppleMusic => "Apple Music",
StreamingPlatform::Netflix => "Netflix",
StreamingPlatform::AmazonPrime => "Amazon Prime Video",
StreamingPlatform::DisneyPlus => "Disney+",
StreamingPlatform::HboMax => "HBO Max",
StreamingPlatform::Tidal => "Tidal",
StreamingPlatform::Deezer => "Deezer",
StreamingPlatform::Custom { .. } => "Custom",
}
}
pub fn check_compliance(&self, integrated: f64, true_peak: f64) -> StreamingCompliance {
let target = self.target_loudness();
let tolerance = self.tolerance();
let max_peak = self.max_true_peak();
let loudness_ok = integrated >= target - tolerance && integrated <= target + tolerance;
let peak_ok = true_peak <= max_peak;
let normalization_gain = if self.applies_normalization() {
Some(target - integrated)
} else {
None
};
StreamingCompliance {
platform: self.platform_name().to_string(),
loudness_compliant: loudness_ok,
peak_compliant: peak_ok,
target_loudness: target,
measured_loudness: integrated,
loudness_deviation: integrated - target,
max_peak_allowed: max_peak,
measured_peak: true_peak,
applies_normalization: self.applies_normalization(),
normalization_gain,
}
}
}
#[derive(Clone, Debug)]
pub struct StreamingCompliance {
pub platform: String,
pub loudness_compliant: bool,
pub peak_compliant: bool,
pub target_loudness: f64,
pub measured_loudness: f64,
pub loudness_deviation: f64,
pub max_peak_allowed: f64,
pub measured_peak: f64,
pub applies_normalization: bool,
pub normalization_gain: Option<f64>,
}
pub struct ItuMeter {
bs1770: Bs1770Meter,
lra_calc: LoudnessRangeCalculator,
true_peak: TruePeakDetector,
sample_rate: f64,
channels: usize,
}
struct TruePeakDetector {
max_peak: f64,
channels: usize,
}
impl TruePeakDetector {
fn new(channels: usize) -> Self {
Self {
max_peak: 0.0,
channels,
}
}
fn process(&mut self, samples: &[f64]) {
for &sample in samples {
let abs_sample = sample.abs();
if abs_sample > self.max_peak {
self.max_peak = abs_sample;
}
}
}
fn true_peak_dbtp(&self) -> f64 {
if self.max_peak > 0.0 {
20.0 * self.max_peak.log10()
} else {
f64::NEG_INFINITY
}
}
fn reset(&mut self) {
self.max_peak = 0.0;
}
}
impl ItuMeter {
pub fn new(sample_rate: f64, channels: usize) -> Self {
Self {
bs1770: Bs1770Meter::new(sample_rate, channels),
lra_calc: LoudnessRangeCalculator::new(),
true_peak: TruePeakDetector::new(channels),
sample_rate,
channels,
}
}
pub fn process(&mut self, frame: &AudioFrame) {
let samples = extract_samples_f64(frame);
self.bs1770.process(&samples);
self.true_peak.process(&samples);
}
pub fn get_metrics(&mut self) -> ItuMetrics {
let blocks = self.bs1770.gating_blocks();
let lra = self.lra_calc.calculate(&blocks);
ItuMetrics {
momentary_lufs: self.bs1770.momentary_loudness(),
short_term_lufs: self.bs1770.short_term_loudness(),
integrated_lufs: self.bs1770.integrated_loudness(),
max_momentary: self.bs1770.max_momentary(),
max_short_term: self.bs1770.max_short_term(),
loudness_range: lra,
true_peak_dbtp: self.true_peak.true_peak_dbtp(),
}
}
pub fn check_bs1864_compliance(&mut self, program_type: ProgramType) -> ComplianceResult {
let practice = Bs1864Practice::new(program_type);
let metrics = self.get_metrics();
practice.check_compliance(metrics.integrated_lufs, metrics.true_peak_dbtp)
}
pub fn check_streaming_compliance(
&mut self,
platform: StreamingPlatform,
) -> StreamingCompliance {
let streaming = Bs2217Streaming::new(platform);
let metrics = self.get_metrics();
streaming.check_compliance(metrics.integrated_lufs, metrics.true_peak_dbtp)
}
pub fn reset(&mut self) {
self.bs1770.reset();
self.lra_calc.reset();
self.true_peak.reset();
}
}
#[derive(Clone, Debug)]
pub struct ItuMetrics {
pub momentary_lufs: f64,
pub short_term_lufs: f64,
pub integrated_lufs: f64,
pub max_momentary: f64,
pub max_short_term: f64,
pub loudness_range: f64,
pub true_peak_dbtp: f64,
}
fn extract_samples_f64(frame: &AudioFrame) -> Vec<f64> {
match &frame.samples {
crate::frame::AudioBuffer::Interleaved(data) => {
let sample_count = data.len() / 4;
let mut samples = Vec::with_capacity(sample_count);
for i in 0..sample_count {
let offset = i * 4;
if offset + 4 <= data.len() {
let bytes_array = [
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
];
let sample = f32::from_le_bytes(bytes_array);
samples.push(f64::from(sample));
}
}
samples
}
crate::frame::AudioBuffer::Planar(planes) => {
if planes.is_empty() {
return Vec::new();
}
let channels = planes.len();
let sample_size = std::mem::size_of::<f32>();
let frames = planes[0].len() / sample_size;
let mut interleaved = Vec::with_capacity(frames * channels);
for frame_idx in 0..frames {
for plane in planes {
let offset = frame_idx * sample_size;
if offset + 4 <= plane.len() {
let bytes_array = [
plane[offset],
plane[offset + 1],
plane[offset + 2],
plane[offset + 3],
];
let sample = f32::from_le_bytes(bytes_array);
interleaved.push(f64::from(sample));
}
}
}
interleaved
}
}
}