#![forbid(unsafe_code)]
pub mod filter;
pub mod gate;
pub mod normalize;
pub mod peak;
pub mod r128;
pub mod report;
use crate::frame::AudioFrame;
use crate::AudioResult;
pub use filter::{KWeightFilter, KWeightFilterBank};
pub use gate::{BlockAccumulator, GatingProcessor};
pub use normalize::{
AutoGainConfig, AutoGainProcessor, BatchNormalizer, FileNormalizationStats, LoudnessNormalizer,
NormalizationConfig, NormalizationMode, NormalizationParams, StreamingNormalizer,
};
pub use peak::{SamplePeakDetector, TruePeakDetector, TruePeakLimiter, TruePeakLimiterConfig};
pub use r128::{AtscA85Ext, AtscA85Meter, ComplianceStatus, R128Compliance, R128Meter};
pub use report::{
AtscA85Compliance, EbuR128Compliance, LoudnessHistory, LoudnessReport, LoudnessStatistics,
NormalizationReport,
};
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum LoudnessStandard {
#[default]
EbuR128,
AtscA85,
Spotify,
YouTube,
AppleMusic,
AmazonMusic,
Tidal,
Custom(f64),
}
impl LoudnessStandard {
#[must_use]
pub fn target_lufs(&self) -> f64 {
match self {
Self::EbuR128 => -23.0,
Self::AtscA85 => -24.0,
Self::Spotify => -14.0,
Self::YouTube => -14.0,
Self::AppleMusic => -16.0,
Self::AmazonMusic => -14.0,
Self::Tidal => -14.0,
Self::Custom(target) => *target,
}
}
#[must_use]
pub fn max_true_peak_dbtp(&self) -> f64 {
match self {
Self::EbuR128 => -1.0,
Self::AtscA85 => -2.0,
Self::Spotify | Self::YouTube | Self::AppleMusic | Self::AmazonMusic | Self::Tidal => {
-1.0
}
Self::Custom(_) => -1.0,
}
}
#[must_use]
pub fn tolerance_lu(&self) -> f64 {
match self {
Self::EbuR128 => 1.0,
Self::AtscA85 => 2.0,
Self::Spotify | Self::YouTube | Self::AppleMusic | Self::AmazonMusic | Self::Tidal => {
1.0
}
Self::Custom(_) => 1.0,
}
}
#[must_use]
pub fn name(&self) -> &str {
match self {
Self::EbuR128 => "EBU R128",
Self::AtscA85 => "ATSC A/85",
Self::Spotify => "Spotify",
Self::YouTube => "YouTube",
Self::AppleMusic => "Apple Music",
Self::AmazonMusic => "Amazon Music",
Self::Tidal => "Tidal",
Self::Custom(_) => "Custom",
}
}
}
#[derive(Clone, Debug)]
pub struct LoudnessMetrics {
pub momentary_lufs: f64,
pub short_term_lufs: f64,
pub integrated_lufs: f64,
pub loudness_range: f64,
pub true_peak_dbtp: f64,
pub true_peak_linear: f64,
pub max_momentary: f64,
pub max_short_term: f64,
}
impl Default for LoudnessMetrics {
fn default() -> Self {
Self {
momentary_lufs: f64::NEG_INFINITY,
short_term_lufs: f64::NEG_INFINITY,
integrated_lufs: f64::NEG_INFINITY,
loudness_range: 0.0,
true_peak_dbtp: f64::NEG_INFINITY,
true_peak_linear: 0.0,
max_momentary: f64::NEG_INFINITY,
max_short_term: f64::NEG_INFINITY,
}
}
}
pub struct LoudnessMeter {
standard: LoudnessStandard,
meter: R128Meter,
history: LoudnessHistory,
sample_rate: f64,
channels: usize,
}
impl LoudnessMeter {
#[must_use]
pub fn new(standard: LoudnessStandard, sample_rate: f64, channels: usize) -> Self {
let meter = R128Meter::new(sample_rate, channels);
let history = LoudnessHistory::new(0.1);
Self {
standard,
meter,
history,
sample_rate,
channels,
}
}
pub fn measure(&mut self, frame: &AudioFrame) -> LoudnessMetrics {
let samples = self.extract_samples(frame);
self.meter.process_interleaved(&samples);
let timestamp = frame.timestamp.to_seconds();
self.history.add_sample(
self.meter.momentary_loudness(),
self.meter.short_term_loudness(),
timestamp,
);
self.get_metrics()
}
#[must_use]
pub fn get_metrics(&self) -> LoudnessMetrics {
LoudnessMetrics {
momentary_lufs: self.meter.momentary_loudness(),
short_term_lufs: self.meter.short_term_loudness(),
integrated_lufs: self.meter.integrated_loudness(),
loudness_range: self.meter.loudness_range(),
true_peak_dbtp: self.meter.true_peak_dbtp(),
true_peak_linear: self.meter.true_peak_linear(),
max_momentary: self.meter.max_momentary(),
max_short_term: self.meter.max_short_term(),
}
}
pub fn normalize(&self, frames: &mut [AudioFrame], target_lufs: f64) -> NormalizationParams {
let config = NormalizationConfig {
target_lufs,
max_true_peak_dbtp: self.standard.max_true_peak_dbtp(),
..Default::default()
};
let normalizer = LoudnessNormalizer::new(config, self.sample_rate, self.channels);
normalizer.normalize(frames)
}
pub fn normalize_to_standard(&self, frames: &mut [AudioFrame]) -> NormalizationParams {
self.normalize(frames, self.standard.target_lufs())
}
#[must_use]
pub fn report(&self, duration_seconds: f64) -> LoudnessReport {
LoudnessReport::from_meter(&self.meter, duration_seconds)
}
#[must_use]
pub fn check_compliance(&self) -> ComplianceStatus {
let integrated = self.meter.integrated_loudness();
let target = self.standard.target_lufs();
let tolerance = self.standard.tolerance_lu();
if integrated.is_infinite() {
ComplianceStatus::Unknown
} else if integrated >= target - tolerance && integrated <= target + tolerance {
ComplianceStatus::Compliant
} else if integrated > target + tolerance {
ComplianceStatus::TooLoud(integrated - target)
} else {
ComplianceStatus::TooQuiet(target - integrated)
}
}
#[must_use]
pub fn history(&self) -> &LoudnessHistory {
&self.history
}
pub fn reset(&mut self) {
self.meter.reset();
self.history = LoudnessHistory::new(0.1);
}
#[must_use]
pub fn standard(&self) -> LoudnessStandard {
self.standard
}
pub fn set_standard(&mut self, standard: LoudnessStandard) {
self.standard = standard;
}
fn extract_samples(&self, frame: &AudioFrame) -> Vec<f64> {
match &frame.samples {
crate::frame::AudioBuffer::Interleaved(data) => self.bytes_to_samples_f64(data),
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 samples = self.bytes_to_samples_f64(plane);
if let Some(&sample) = samples.get(frame_idx) {
interleaved.push(sample);
}
}
}
interleaved
}
}
}
fn bytes_to_samples_f64(&self, bytes: &bytes::Bytes) -> Vec<f64> {
let sample_count = bytes.len() / 4;
let mut samples = Vec::with_capacity(sample_count);
for i in 0..sample_count {
let offset = i * 4;
if offset + 4 <= bytes.len() {
let bytes_array = [
bytes[offset],
bytes[offset + 1],
bytes[offset + 2],
bytes[offset + 3],
];
let sample = f32::from_le_bytes(bytes_array);
samples.push(f64::from(sample));
}
}
samples
}
}
pub fn measure_loudness(
frames: &[AudioFrame],
sample_rate: f64,
channels: usize,
) -> AudioResult<LoudnessMetrics> {
let mut meter = LoudnessMeter::new(LoudnessStandard::EbuR128, sample_rate, channels);
for frame in frames {
meter.measure(frame);
}
Ok(meter.get_metrics())
}
pub fn normalize_loudness(
frames: &mut [AudioFrame],
target_lufs: f64,
sample_rate: f64,
channels: usize,
) -> AudioResult<NormalizationParams> {
let meter = LoudnessMeter::new(LoudnessStandard::Custom(target_lufs), sample_rate, channels);
Ok(meter.normalize(frames, target_lufs))
}