Skip to main content

oximedia_metering/
lib.rs

1//! Professional broadcast audio metering for `OxiMedia`.
2//!
3//! This crate provides comprehensive, standards-compliant audio loudness measurement
4//! and metering for broadcast, streaming, and professional audio applications.
5//!
6//! # Supported Standards
7//!
8//! - **ITU-R BS.1770-4** - Algorithms to measure audio programme loudness and true-peak level
9//! - **ITU-R BS.1771** - Requirements for loudness and true-peak indicating meters
10//! - **EBU R128** - Loudness normalisation and permitted maximum level (European standard)
11//! - **ATSC A/85** - Techniques for Establishing and Maintaining Audio Loudness (US standard)
12//! - **Dolby Metadata** - Dialogue Intelligence metadata generation (metadata only, respects IP)
13//!
14//! # Features
15//!
16//! ## Loudness Measurement
17//!
18//! - **Momentary Loudness** - 400ms sliding window with 75% overlap
19//! - **Short-term Loudness** - 3-second sliding window with 75% overlap
20//! - **Integrated Loudness** - Gated program loudness (LKFS/LUFS)
21//! - **Loudness Range (LRA)** - Dynamic range measurement using percentile-based method
22//!
23//! ## True Peak Detection
24//!
25//! - **4x Oversampling** - Detects inter-sample peaks using sinc interpolation
26//! - **Per-channel Tracking** - Individual true peak levels for each channel
27//! - **dBTP Conversion** - True peak in dB relative to full scale
28//!
29//! ## Gating Algorithm
30//!
31//! - **Absolute Gate** - -70 LKFS threshold
32//! - **Relative Gate** - -10 LU below ungated loudness
33//! - **Two-stage Process** - ITU-R BS.1771 compliant gating
34//!
35//! ## Multi-channel Support
36//!
37//! Supports up to 7.1.4 Dolby Atmos layouts with proper channel weighting:
38//! - Mono (1.0)
39//! - Stereo (2.0)
40//! - 5.1 Surround
41//! - 7.1 Surround
42//! - 7.1.4 Dolby Atmos (bed channels)
43//!
44//! ## Compliance Checking
45//!
46//! - EBU R128 compliance (target: -23 LUFS ±1 LU, peak: -1 dBTP)
47//! - ATSC A/85 compliance (target: -24 LKFS ±2 dB, peak: -2 dBTP)
48//! - Streaming platform targets (Spotify, `YouTube`, Apple Music, etc.)
49//!
50//! # Example Usage
51//!
52//! ## Basic Loudness Metering
53//!
54//! ```rust,no_run
55//! use oximedia_metering::{LoudnessMeter, MeterConfig, Standard};
56//!
57//! // Create meter for EBU R128
58//! let config = MeterConfig::new(Standard::EbuR128, 48000.0, 2);
59//! let mut meter = LoudnessMeter::new(config).expect("Failed to create meter");
60//!
61//! // Process audio samples (interleaved f32)
62//! # let audio_samples: &[f32] = &[];
63//! meter.process_f32(audio_samples);
64//!
65//! // Get loudness metrics
66//! let metrics = meter.metrics();
67//! println!("Integrated: {:.1} LUFS", metrics.integrated_lufs);
68//! println!("LRA: {:.1} LU", metrics.loudness_range);
69//! println!("True Peak: {:.1} dBTP", metrics.true_peak_dbtp);
70//!
71//! // Check compliance
72//! let compliance = meter.check_compliance();
73//! if compliance.is_compliant() {
74//!     println!("Audio is compliant with {}", compliance.standard_name());
75//! }
76//!
77//! // Generate detailed report
78//! let report = meter.generate_report();
79//! println!("{}", report);
80//! ```
81//!
82//! ## Peak Metering
83//!
84//! ```rust,no_run
85//! use oximedia_metering::{PeakMeter, PeakMeterType};
86//!
87//! // Create a VU meter for stereo audio
88//! let mut vu_meter = PeakMeter::new(
89//!     PeakMeterType::Vu,
90//!     48000.0,
91//!     2,
92//!     2.0  // 2 second peak hold
93//! ).expect("Failed to create VU meter");
94//!
95//! # let audio_samples: &[f64] = &[];
96//! vu_meter.process_interleaved(audio_samples);
97//!
98//! let peaks = vu_meter.peak_dbfs();
99//! println!("L: {:.1} dBFS, R: {:.1} dBFS", peaks[0], peaks[1]);
100//!
101//! // Create an RMS meter with 300ms integration
102//! let mut rms_meter = PeakMeter::new(
103//!     PeakMeterType::Rms(0.3),
104//!     48000.0,
105//!     2,
106//!     0.0
107//! ).expect("Failed to create RMS meter");
108//! ```
109//!
110//! ## K-System Metering
111//!
112//! ```rust,no_run
113//! use oximedia_metering::{KSystemMeter, KSystemType};
114//!
115//! // Create K-14 meter (mastering standard)
116//! let mut k_meter = KSystemMeter::new(
117//!     KSystemType::K14,
118//!     48000.0,
119//!     2
120//! ).expect("Failed to create K-meter");
121//!
122//! # let audio_samples: &[f64] = &[];
123//! k_meter.process_interleaved(audio_samples);
124//!
125//! // Get levels relative to K-14 reference
126//! let rms_levels = k_meter.rms_relative_db();
127//! println!("RMS relative to K-14: L={:.1} dB, R={:.1} dB",
128//!          rms_levels[0], rms_levels[1]);
129//!
130//! if k_meter.is_overload() {
131//!     println!("Warning: Headroom exceeded!");
132//! }
133//! ```
134//!
135//! ## Phase Analysis
136//!
137//! ```rust,no_run
138//! use oximedia_metering::{PhaseCorrelationMeter, StereoWidthAnalyzer};
139//!
140//! // Create phase correlation meter
141//! let mut phase_meter = PhaseCorrelationMeter::new(48000.0, 0.4)
142//!     .expect("Failed to create phase meter");
143//!
144//! # let audio_samples: &[f64] = &[];
145//! phase_meter.process_interleaved(audio_samples);
146//!
147//! let correlation = phase_meter.correlation();
148//! println!("Phase correlation: {:.2}", correlation);
149//!
150//! if phase_meter.has_phase_issues() {
151//!     println!("Warning: Phase cancellation detected!");
152//! }
153//!
154//! // Stereo width analysis
155//! let mut width_analyzer = StereoWidthAnalyzer::new(48000.0)
156//!     .expect("Failed to create width analyzer");
157//!
158//! width_analyzer.process_interleaved(audio_samples);
159//! println!("Stereo width: {:.0}%", width_analyzer.width_percentage());
160//! ```
161//!
162//! ## Spectrum Analysis
163//!
164//! ```rust,no_run
165//! use oximedia_metering::{SpectrumAnalyzer, WindowFunction, WeightingCurve};
166//!
167//! // Create FFT-based spectrum analyzer
168//! let mut spectrum = SpectrumAnalyzer::new(
169//!     48000.0,
170//!     2048,
171//!     WindowFunction::Hann,
172//!     WeightingCurve::A,
173//!     1.0  // 1 second peak hold
174//! ).expect("Failed to create spectrum analyzer");
175//!
176//! # let audio_samples: &[f64] = &[];
177//! spectrum.process(audio_samples);
178//!
179//! let spectrum_db = spectrum.spectrum_db();
180//! for (i, &magnitude) in spectrum_db.iter().take(10).enumerate() {
181//!     let freq = spectrum.bin_frequency(i);
182//!     println!("{:.0} Hz: {:.1} dB", freq, magnitude);
183//! }
184//! ```
185//!
186//! ## Video Metering
187//!
188//! ```rust,no_run
189//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
190//! use oximedia_metering::{LuminanceMeter, GamutMeter, ColorGamut, QualityAnalyzer, Frame2D};
191//!
192//! // Luminance metering for HDR content
193//! let mut lum_meter = LuminanceMeter::new(1920, 1080, 1000.0, 256)
194//!     .expect("Failed to create luminance meter");
195//!
196//! # let luminance_frame = Frame2D::zeros(1080, 1920);
197//! lum_meter.process(&luminance_frame)?;
198//!
199//! println!("Peak: {:.1} nits", lum_meter.peak_nits());
200//! println!("Average: {:.1} nits", lum_meter.average_nits());
201//! println!("Dynamic range: {:.1} stops", lum_meter.dynamic_range_stops());
202//!
203//! if lum_meter.is_hdr10() {
204//!     println!("HDR10 content detected");
205//! }
206//!
207//! // Color gamut analysis
208//! let mut gamut_meter = GamutMeter::new(1920, 1080, ColorGamut::Rec2020)
209//!     .expect("Failed to create gamut meter");
210//!
211//! # let r_channel = Frame2D::zeros(1080, 1920);
212//! # let g_channel = Frame2D::zeros(1080, 1920);
213//! # let b_channel = Frame2D::zeros(1080, 1920);
214//! gamut_meter.process(&r_channel, &g_channel, &b_channel)?;
215//!
216//! println!("Rec.2020 coverage: {:.1}%", gamut_meter.gamut_coverage_percentage());
217//! println!("Max saturation: {:.2}", gamut_meter.max_saturation());
218//!
219//! // Video quality metrics (PSNR, SSIM)
220//! let quality = QualityAnalyzer::new(1920, 1080, 1.0)
221//!     .expect("Failed to create quality analyzer");
222//!
223//! # let reference_frame = Frame2D::zeros(1080, 1920);
224//! # let distorted_frame = Frame2D::zeros(1080, 1920);
225//! let metrics = quality.analyze(&reference_frame, &distorted_frame)?;
226//!
227//! println!("PSNR: {:.2} dB", metrics.psnr);
228//! println!("SSIM: {:.4}", metrics.ssim);
229//! println!("Quality: {}", metrics.rating());
230//! # Ok(())
231//! # }
232//! ```
233//!
234//! ## Meter Rendering
235//!
236//! ```rust,no_run
237//! use oximedia_metering::{BarMeterConfig, BarMeterData, ColorGradient, Orientation};
238//!
239//! // Configure a vertical bar meter
240//! let config = BarMeterConfig {
241//!     orientation: Orientation::Vertical,
242//!     width: 30,
243//!     height: 200,
244//!     min_value: -60.0,
245//!     max_value: 0.0,
246//!     gradient: ColorGradient::traffic_light(),
247//!     show_peak_hold: true,
248//!     show_scale: true,
249//!     ..Default::default()
250//! };
251//!
252//! // Create meter data from dBFS values
253//! let meter_data = BarMeterData::from_dbfs(
254//!     -12.0,  // Current level
255//!     -6.0,   // Peak hold
256//!     -60.0,  // Min range
257//!     0.0     // Max range
258//! );
259//!
260//! if meter_data.is_clipping {
261//!     println!("Clipping detected!");
262//! }
263//!
264//! // Get color for current level
265//! let color = config.gradient.color_at(meter_data.level);
266//! println!("Meter color: RGB({}, {}, {})", color.r, color.g, color.b);
267//! ```
268//!
269//! # Technical Implementation
270//!
271//! ## K-weighting Filter
272//!
273//! The K-weighting filter chain implements ITU-R BS.1770-4 specification:
274//! - **Stage 1**: High-pass filter at 78.5 Hz (head diffraction modeling)
275//! - **Stage 2**: High-shelf filter for revised low-frequency B-weighting (RLB)
276//!
277//! Both filters are implemented as second-order IIR biquad filters with
278//! precise coefficients calculated for the given sample rate.
279//!
280//! ## Block Processing
281//!
282//! Audio is processed in overlapping blocks:
283//! - **Block size**: 100ms (400ms blocks for momentary, 3000ms for short-term)
284//! - **Overlap**: 75% (blocks advance by 25% of their duration)
285//! - **Gating**: Applied on 400ms blocks with absolute (-70 LKFS) and relative (-10 LU) gates
286//!
287//! ## True Peak Detection
288//!
289//! Uses 4x oversampling with windowed sinc interpolation:
290//! - Lanczos-windowed sinc function (a=3)
291//! - Linear-phase FIR resampling
292//! - Per-sample peak tracking
293//!
294//! # Performance
295//!
296//! - **Real-time capable**: Processes audio faster than real-time on modern CPUs
297//! - **Memory efficient**: Circular buffers for sliding windows
298//! - **Zero-copy where possible**: Processes interleaved or planar audio in-place when possible
299//!
300//! # Standards References
301//!
302//! - ITU-R BS.1770-4 (10/2015): "Algorithms to measure audio programme loudness and true-peak audio level"
303//! - ITU-R BS.1771 (2006): "Requirements for loudness and true-peak indicating meters"
304//! - EBU R 128 (2020): "Loudness normalisation and permitted maximum level of audio signals"
305//! - ATSC A/85:2013: "Techniques for Establishing and Maintaining Audio Loudness for Digital Television"
306
307#![forbid(unsafe_code)]
308#![warn(missing_docs)]
309#![allow(clippy::module_name_repetitions)]
310#![allow(clippy::must_use_candidate)]
311#![allow(clippy::similar_names)]
312#![allow(clippy::unreadable_literal)]
313#![allow(clippy::cast_precision_loss)]
314#![allow(clippy::many_single_char_names)]
315#![allow(clippy::if_same_then_else)]
316#![allow(clippy::unused_self)]
317#![allow(clippy::cast_possible_truncation)]
318#![allow(clippy::cast_sign_loss)]
319#![allow(clippy::doc_markdown)]
320#![allow(clippy::fn_params_excessive_bools)]
321#![allow(clippy::let_and_return)]
322#![allow(clippy::match_same_arms)]
323#![allow(clippy::missing_errors_doc)]
324#![allow(clippy::struct_excessive_bools)]
325#![allow(clippy::format_push_string)]
326#![allow(clippy::trivially_copy_pass_by_ref)]
327#![allow(clippy::missing_panics_doc)]
328#![allow(dead_code)]
329#![allow(
330    clippy::float_cmp,
331    clippy::too_many_lines,
332    clippy::return_self_not_must_use
333)]
334
335pub mod atsc;
336pub mod ballistics;
337pub mod bs2051_weights;
338pub mod bs2132;
339pub mod clip_counter;
340pub mod correlation;
341pub mod dr_meter;
342pub mod dynamics;
343pub mod ebu;
344pub mod ebu_r128_impl;
345pub mod filters;
346pub mod gating;
347pub mod k_weighting;
348pub mod leq;
349pub mod lkfs;
350pub mod loudness_gate;
351pub mod loudness_history;
352pub mod m_s_meter;
353pub mod meter_type_config;
354pub mod ms_ssim;
355pub mod octave_bands;
356pub mod peak;
357pub mod phase;
358pub mod phase_scope;
359pub mod ppm;
360pub mod range;
361pub mod render;
362pub mod report;
363pub mod rms_envelope;
364pub mod silence_detect;
365pub mod spectral_balance;
366pub mod spectral_energy;
367pub mod spectrum;
368pub mod spectrum_bands;
369pub mod true_peak_meter;
370pub mod truepeak;
371pub mod video_color;
372pub mod video_luminance;
373pub mod video_quality;
374pub mod vmaf_estimate;
375pub mod vmaf_features;
376pub mod vu_meter;
377
378/// Backward-compatibility aliases for merged modules.
379pub use correlation as correlation_meter;
380/// Backward-compatibility alias: items from `dynamic_range_meter` now in [`dynamics`].
381pub use dynamics as dynamic_range_meter;
382/// Backward-compatibility alias: items from `peak_meter` now in [`peak`].
383pub use peak as peak_meter;
384/// Backward-compatibility alias: items from `phase_analysis` now in [`phase`].
385pub use phase as phase_analysis;
386/// Backward-compatibility alias: items from `true_peak` now in [`truepeak`].
387pub use truepeak as true_peak;
388
389// Wave 12 modules
390pub mod crest_factor;
391pub mod k_weighted;
392pub mod meter_bridge;
393
394// Wave 15 modules
395pub mod loudness_trend;
396pub mod noise_floor;
397pub mod stereo_balance;
398
399// Wave 16 modules
400pub mod k_weight_simd;
401pub mod temporal_noise;
402
403use oximedia_core::types::SampleFormat;
404use thiserror::Error;
405
406pub use atsc::{AtscA85Compliance, AtscA85Meter};
407pub use ballistics::{BallisticProcessor, BallisticType, MultiChannelBallistics};
408pub use correlation::{
409    CorrelationMeter, FrequencyBand, Goniometer as CorrelationGoniometer,
410    GoniometerPoint as CorrelationGoniometerPoint, MultibandMeter, PhaseRelationship,
411};
412pub use dynamics::{DynamicRangeMeter, PlrMeter};
413pub use ebu::{EbuR128Compliance, EbuR128Meter};
414pub use filters::{KWeightFilter, KWeightFilterBank};
415pub use gating::{GatingProcessor, GatingResult};
416pub use lkfs::{LkfsCalculator, LufsValue};
417pub use peak::{
418    dbfs_to_linear, linear_to_dbfs, KSystemMeter, KSystemType, PeakMeter, PeakMeterType,
419};
420pub use phase::{Goniometer, GoniometerPoint, PhaseCorrelationMeter, StereoWidthAnalyzer};
421pub use range::{LoudnessRange, LraCalculator};
422pub use render::{
423    colors, generate_db_scale, BarMeterConfig, BarMeterData, CircularMeterConfig, Color,
424    ColorGradient, Orientation, ScaleMark, ScaleType,
425};
426pub use report::{ComplianceReport, LoudnessReport, MeteringReport};
427pub use spectrum::{
428    CachedSpectrumAnalyzer, OctaveBand, OctaveBandAnalyzer, SpectrumAnalyzer, WeightingCurve,
429    WindowFunction,
430};
431pub use truepeak::{TruePeak, TruePeakDetector};
432pub use video_color::{
433    ColorGamut, ColorTemperatureMeter, GamutMeter, HsvColor, RgbColor, SaturationMeter,
434};
435pub use video_luminance::{BlackWhiteLevelMeter, LuminanceMeter};
436pub use video_quality::{
437    BlockinessDetector, Frame2D, PsnrCalculator, QualityAnalyzer, QualityMetrics, SsimCalculator,
438};
439
440/// Metering error types.
441#[derive(Error, Debug)]
442pub enum MeteringError {
443    /// Invalid configuration.
444    #[error("Invalid configuration: {0}")]
445    InvalidConfig(String),
446
447    /// Insufficient data for measurement.
448    #[error("Insufficient data: {0}")]
449    InsufficientData(String),
450
451    /// Sample format not supported.
452    #[error("Unsupported sample format: {0:?}")]
453    UnsupportedFormat(SampleFormat),
454
455    /// Channel configuration error.
456    #[error("Channel error: {0}")]
457    ChannelError(String),
458
459    /// Calculation error.
460    #[error("Calculation error: {0}")]
461    CalculationError(String),
462}
463
464/// Metering result type.
465pub type MeteringResult<T> = std::result::Result<T, MeteringError>;
466
467/// Broadcast loudness standard.
468#[derive(Clone, Copy, Debug, PartialEq, Default)]
469pub enum Standard {
470    /// EBU R128 (European Broadcasting Union).
471    ///
472    /// Target: -23 LUFS ±1 LU
473    /// Max True Peak: -1.0 dBTP
474    #[default]
475    EbuR128,
476
477    /// ATSC A/85 (Advanced Television Systems Committee - US).
478    ///
479    /// Target: -24 LKFS ±2 dB
480    /// Max True Peak: -2.0 dBTP
481    AtscA85,
482
483    /// Spotify streaming platform.
484    ///
485    /// Target: -14 LUFS
486    /// Max True Peak: -1.0 dBTP
487    Spotify,
488
489    /// `YouTube` streaming platform.
490    ///
491    /// Target: -14 LUFS
492    /// Max True Peak: -1.0 dBTP
493    YouTube,
494
495    /// Apple Music streaming platform.
496    ///
497    /// Target: -16 LUFS
498    /// Max True Peak: -1.0 dBTP
499    AppleMusic,
500
501    /// Netflix streaming platform.
502    ///
503    /// Target: -27 LUFS
504    /// Max True Peak: -2.0 dBTP
505    Netflix,
506
507    /// Amazon Prime Video.
508    ///
509    /// Target: -24 LUFS
510    /// Max True Peak: -2.0 dBTP
511    AmazonPrime,
512
513    /// Tidal HiFi streaming platform.
514    ///
515    /// Target: -14 LUFS
516    /// Max True Peak: -1.0 dBTP
517    TidalHiFi,
518
519    /// Amazon Music HD streaming platform.
520    ///
521    /// Target: -14 LUFS
522    /// Max True Peak: -1.0 dBTP
523    AmazonMusicHd,
524
525    /// Custom target loudness.
526    ///
527    /// Specify your own target in LUFS and max true peak in dBTP.
528    Custom {
529        /// Target loudness in LUFS.
530        target_lufs: f64,
531        /// Maximum true peak in dBTP.
532        max_peak_dbtp: f64,
533        /// Tolerance in LU.
534        tolerance_lu: f64,
535    },
536}
537
538impl Standard {
539    /// Get the target loudness in LUFS for this standard.
540    pub fn target_lufs(&self) -> f64 {
541        match self {
542            Self::EbuR128 => -23.0,
543            Self::AtscA85 | Self::AmazonPrime => -24.0,
544            Self::Spotify | Self::YouTube | Self::TidalHiFi | Self::AmazonMusicHd => -14.0,
545            Self::AppleMusic => -16.0,
546            Self::Netflix => -27.0,
547            Self::Custom { target_lufs, .. } => *target_lufs,
548        }
549    }
550
551    /// Get the maximum true peak in dBTP for this standard.
552    pub fn max_true_peak_dbtp(&self) -> f64 {
553        match self {
554            Self::EbuR128
555            | Self::Spotify
556            | Self::YouTube
557            | Self::AppleMusic
558            | Self::TidalHiFi
559            | Self::AmazonMusicHd => -1.0,
560            Self::AtscA85 | Self::Netflix | Self::AmazonPrime => -2.0,
561            Self::Custom { max_peak_dbtp, .. } => *max_peak_dbtp,
562        }
563    }
564
565    /// Get the tolerance in LU for this standard.
566    pub fn tolerance_lu(&self) -> f64 {
567        match self {
568            Self::EbuR128
569            | Self::Spotify
570            | Self::YouTube
571            | Self::AppleMusic
572            | Self::TidalHiFi
573            | Self::AmazonMusicHd => 1.0,
574            Self::AtscA85 | Self::Netflix | Self::AmazonPrime => 2.0,
575            Self::Custom { tolerance_lu, .. } => *tolerance_lu,
576        }
577    }
578
579    /// Get the standard name as a string.
580    pub fn name(&self) -> &str {
581        match self {
582            Self::EbuR128 => "EBU R128",
583            Self::AtscA85 => "ATSC A/85",
584            Self::Spotify => "Spotify",
585            Self::YouTube => "YouTube",
586            Self::AppleMusic => "Apple Music",
587            Self::Netflix => "Netflix",
588            Self::AmazonPrime => "Amazon Prime Video",
589            Self::TidalHiFi => "Tidal HiFi",
590            Self::AmazonMusicHd => "Amazon Music HD",
591            Self::Custom { .. } => "Custom",
592        }
593    }
594}
595
596/// Meter configuration.
597#[derive(Clone, Debug)]
598#[allow(clippy::struct_excessive_bools)]
599pub struct MeterConfig {
600    /// Broadcast standard to measure against.
601    pub standard: Standard,
602    /// Sample rate in Hz.
603    pub sample_rate: f64,
604    /// Number of audio channels.
605    pub channels: usize,
606    /// Enable true peak detection (4x oversampling).
607    pub enable_true_peak: bool,
608    /// Enable loudness range (LRA) calculation.
609    pub enable_lra: bool,
610    /// Enable momentary loudness tracking.
611    pub enable_momentary: bool,
612    /// Enable short-term loudness tracking.
613    pub enable_short_term: bool,
614    /// Enable integrated loudness (gated program loudness).
615    pub enable_integrated: bool,
616}
617
618impl MeterConfig {
619    /// Create a new meter configuration.
620    ///
621    /// # Arguments
622    ///
623    /// * `standard` - Broadcast standard
624    /// * `sample_rate` - Sample rate in Hz
625    /// * `channels` - Number of channels
626    pub fn new(standard: Standard, sample_rate: f64, channels: usize) -> Self {
627        Self {
628            standard,
629            sample_rate,
630            channels,
631            enable_true_peak: true,
632            enable_lra: true,
633            enable_momentary: true,
634            enable_short_term: true,
635            enable_integrated: true,
636        }
637    }
638
639    /// Create a minimal configuration (integrated loudness and true peak only).
640    pub fn minimal(standard: Standard, sample_rate: f64, channels: usize) -> Self {
641        Self {
642            standard,
643            sample_rate,
644            channels,
645            enable_true_peak: true,
646            enable_lra: false,
647            enable_momentary: false,
648            enable_short_term: false,
649            enable_integrated: true,
650        }
651    }
652
653    /// Validate the configuration.
654    ///
655    /// # Errors
656    ///
657    /// Returns `MeteringError::InvalidConfig` if any configuration parameters are out of valid range.
658    pub fn validate(&self) -> MeteringResult<()> {
659        if self.sample_rate < 8000.0 || self.sample_rate > 192_000.0 {
660            return Err(MeteringError::InvalidConfig(format!(
661                "Sample rate {} Hz is out of valid range (8000-192000 Hz)",
662                self.sample_rate
663            )));
664        }
665
666        if self.channels == 0 || self.channels > 16 {
667            return Err(MeteringError::InvalidConfig(format!(
668                "Channel count {} is out of valid range (1-16)",
669                self.channels
670            )));
671        }
672
673        if !self.enable_integrated && !self.enable_momentary && !self.enable_short_term {
674            return Err(MeteringError::InvalidConfig(
675                "At least one loudness measurement must be enabled".to_string(),
676            ));
677        }
678
679        Ok(())
680    }
681}
682
683/// Channel configuration for multi-channel audio.
684#[derive(Clone, Copy, Debug, PartialEq)]
685pub enum ChannelLayout {
686    /// Mono (1.0).
687    Mono,
688    /// Stereo (2.0).
689    Stereo,
690    /// 5.1 Surround (L, R, C, LFE, Ls, Rs).
691    Surround51,
692    /// 7.1 Surround (L, R, C, LFE, Ls, Rs, Lrs, Rrs).
693    Surround71,
694    /// 7.1.4 Dolby Atmos bed (L, R, C, LFE, Ls, Rs, Lrs, Rrs, Ltf, Rtf, Ltb, Rtb).
695    Atmos714,
696    /// NHK 22.2 immersive audio layout per ITU-R BS.2051-3.
697    ///
698    /// 24 speakers arranged in 3 layers:
699    /// - Top layer (9 ch): TpFL, TpFR, TpFC, TpC, TpBL, TpBR, TpSiL, TpSiR, TpBC
700    /// - Middle layer (10 ch): FL, FR, FC, LFE1, BL, BR, FLc, FRc, BC, LFE2
701    /// - Bottom layer (4 ch): BtFL, BtFR, BtFC, BtBC
702    /// - Plus 1 overhead centre (CH): TpFC (already in top)
703    ///
704    /// Channel order follows ITU-R BS.2051-3 Table 1 (22.2 layout).
705    Nhk222,
706    /// Custom channel configuration.
707    Custom(usize),
708}
709
710impl ChannelLayout {
711    /// Get the number of channels.
712    pub fn channel_count(&self) -> usize {
713        match self {
714            Self::Mono => 1,
715            Self::Stereo => 2,
716            Self::Surround51 => 6,
717            Self::Surround71 => 8,
718            Self::Atmos714 => 12,
719            Self::Nhk222 => 24,
720            Self::Custom(n) => *n,
721        }
722    }
723
724    /// Get ITU-R BS.1770-4 channel weights for this layout.
725    ///
726    /// Returns a vector of weights to apply to each channel during loudness calculation.
727    /// For the NHK 22.2 layout the weights follow ITU-R BS.2051-3 Section 6 (reproduced
728    /// below).  The standard specifies that LFE channels contribute zero power and that
729    /// surround/rear/overhead channels use a +1.5 dB gain (linear ≈ 1.189) relative to
730    /// front centre and left/right channels.
731    pub fn channel_weights(&self) -> Vec<f64> {
732        match self {
733            Self::Mono => vec![1.0],
734            Self::Stereo => vec![1.0, 1.0],
735            Self::Surround51 => {
736                // L, R, C, LFE, Ls, Rs
737                vec![1.0, 1.0, 1.0, 0.0, 1.41, 1.41]
738            }
739            Self::Surround71 => {
740                // L, R, C, LFE, Ls, Rs, Lrs, Rrs
741                vec![1.0, 1.0, 1.0, 0.0, 1.41, 1.41, 1.41, 1.41]
742            }
743            Self::Atmos714 => {
744                // L, R, C, LFE, Ls, Rs, Lrs, Rrs, Ltf, Rtf, Ltb, Rtb
745                vec![
746                    1.0, 1.0, 1.0, 0.0, 1.41, 1.41, 1.41, 1.41, 1.41, 1.41, 1.41, 1.41,
747                ]
748            }
749            Self::Nhk222 => {
750                // ITU-R BS.2051-3 NHK 22.2 channel weights.
751                //
752                // 24 channels in order (ITU-R BS.2051-3 Table 1):
753                //
754                // Top layer (9 channels):
755                //   0:TpFL  1:TpFR  2:TpFC  3:TpC  4:TpBL  5:TpBR  6:TpSiL  7:TpSiR  8:TpBC
756                // Middle layer (10 channels):
757                //   9:FL  10:FR  11:FC  12:LFE1  13:BL  14:BR  15:FLc  16:FRc  17:BC  18:LFE2
758                // Bottom layer (4 channels):
759                //  19:BtFL  20:BtFR  21:BtFC  22:BtBC
760                // Plus one overhead:
761                //  23:CH (overhead centre, equivalent to +1.5 dB weight)
762                //
763                // Weight rules from BS.2051-3 §6:
764                //   - Front centre (FC): 1.0 (0 dB reference)
765                //   - Left/Right front (FL, FR, FLc, FRc): 1.0
766                //   - Surround/Rear (BL, BR, BC): 1.189 (~+1.5 dB)
767                //   - Top layer: 1.189 (~+1.5 dB)
768                //   - Bottom layer: 1.189 (~+1.5 dB)
769                //   - Overhead centre (CH): 1.189 (~+1.5 dB)
770                //   - LFE1, LFE2: 0.0 (excluded per BS.1770-4 §3.4)
771
772                // √2 ≈ 1.41213, +1.5 dB ≈ 1.18850
773                const W_SURROUND: f64 = 1.188_502_227_4; // 10^(1.5/20)
774                const W_FRONT: f64 = 1.0;
775                const W_LFE: f64 = 0.0;
776
777                vec![
778                    // Top layer (9 ch)
779                    W_SURROUND, // TpFL
780                    W_SURROUND, // TpFR
781                    W_SURROUND, // TpFC
782                    W_SURROUND, // TpC
783                    W_SURROUND, // TpBL
784                    W_SURROUND, // TpBR
785                    W_SURROUND, // TpSiL
786                    W_SURROUND, // TpSiR
787                    W_SURROUND, // TpBC
788                    // Middle layer (10 ch)
789                    W_FRONT,    // FL
790                    W_FRONT,    // FR
791                    W_FRONT,    // FC
792                    W_LFE,      // LFE1
793                    W_SURROUND, // BL
794                    W_SURROUND, // BR
795                    W_FRONT,    // FLc
796                    W_FRONT,    // FRc
797                    W_SURROUND, // BC
798                    W_LFE,      // LFE2
799                    // Bottom layer (4 ch)
800                    W_SURROUND, // BtFL
801                    W_SURROUND, // BtFR
802                    W_SURROUND, // BtFC
803                    W_SURROUND, // BtBC
804                    // Overhead centre
805                    W_SURROUND, // CH
806                ]
807            }
808            Self::Custom(n) => vec![1.0; *n],
809        }
810    }
811
812    /// Create from channel count.
813    pub fn from_channel_count(count: usize) -> Self {
814        match count {
815            1 => Self::Mono,
816            2 => Self::Stereo,
817            6 => Self::Surround51,
818            8 => Self::Surround71,
819            12 => Self::Atmos714,
820            24 => Self::Nhk222,
821            n => Self::Custom(n),
822        }
823    }
824}
825
826/// Loudness measurement metrics.
827#[derive(Clone, Debug, Default)]
828pub struct LoudnessMetrics {
829    /// Momentary loudness in LUFS (400ms window).
830    pub momentary_lufs: f64,
831    /// Short-term loudness in LUFS (3s window).
832    pub short_term_lufs: f64,
833    /// Integrated loudness in LUFS (gated program loudness).
834    pub integrated_lufs: f64,
835    /// Loudness range in LU.
836    pub loudness_range: f64,
837    /// True peak in dBTP (maximum across all channels).
838    pub true_peak_dbtp: f64,
839    /// True peak in linear scale.
840    pub true_peak_linear: f64,
841    /// Maximum momentary loudness seen.
842    pub max_momentary: f64,
843    /// Maximum short-term loudness seen.
844    pub max_short_term: f64,
845    /// Per-channel true peaks in dBTP.
846    pub channel_peaks_dbtp: Vec<f64>,
847}
848
849/// Main loudness meter.
850///
851/// This is the primary interface for loudness measurement. It combines all
852/// measurement algorithms (LKFS, gating, true peak, LRA) into a single meter.
853pub struct LoudnessMeter {
854    config: MeterConfig,
855    lkfs_calculator: LkfsCalculator,
856    gating_processor: GatingProcessor,
857    true_peak_detector: Option<TruePeakDetector>,
858    lra_calculator: Option<LraCalculator>,
859    filter_bank: KWeightFilterBank,
860    channel_layout: ChannelLayout,
861    samples_processed: usize,
862}
863
864impl LoudnessMeter {
865    /// Create a new loudness meter.
866    ///
867    /// # Arguments
868    ///
869    /// * `config` - Meter configuration
870    ///
871    /// # Errors
872    ///
873    /// Returns error if configuration is invalid.
874    pub fn new(config: MeterConfig) -> MeteringResult<Self> {
875        config.validate()?;
876
877        let channel_layout = ChannelLayout::from_channel_count(config.channels);
878        let filter_bank = KWeightFilterBank::new(config.channels, config.sample_rate);
879        let lkfs_calculator = LkfsCalculator::new(config.sample_rate, config.channels);
880        let gating_processor = GatingProcessor::new(config.sample_rate, config.channels);
881
882        let true_peak_detector = if config.enable_true_peak {
883            Some(TruePeakDetector::new(config.sample_rate, config.channels))
884        } else {
885            None
886        };
887
888        let lra_calculator = if config.enable_lra {
889            Some(LraCalculator::new())
890        } else {
891            None
892        };
893
894        Ok(Self {
895            config,
896            lkfs_calculator,
897            gating_processor,
898            true_peak_detector,
899            lra_calculator,
900            filter_bank,
901            channel_layout,
902            samples_processed: 0,
903        })
904    }
905
906    /// Process f32 audio samples (interleaved).
907    ///
908    /// # Arguments
909    ///
910    /// * `samples` - Interleaved audio samples normalized to -1.0 to 1.0
911    pub fn process_f32(&mut self, samples: &[f32]) {
912        let f64_samples: Vec<f64> = samples.iter().map(|&s| f64::from(s)).collect();
913        self.process_f64(&f64_samples);
914    }
915
916    /// Process f64 audio samples (interleaved).
917    ///
918    /// # Arguments
919    ///
920    /// * `samples` - Interleaved audio samples normalized to -1.0 to 1.0
921    pub fn process_f64(&mut self, samples: &[f64]) {
922        if samples.is_empty() {
923            return;
924        }
925
926        // Apply K-weighting filter
927        let mut filtered = vec![0.0; samples.len()];
928        self.filter_bank
929            .process_interleaved(samples, self.config.channels, &mut filtered);
930
931        // Process LKFS calculation
932        self.lkfs_calculator.process_interleaved(&filtered);
933
934        // Process gating (for integrated loudness)
935        self.gating_processor.process_interleaved(&filtered);
936
937        // Process true peak (on original unfiltered samples)
938        if let Some(ref mut detector) = self.true_peak_detector {
939            detector.process_interleaved(samples);
940        }
941
942        self.samples_processed += samples.len() / self.config.channels;
943    }
944
945    /// Get current loudness metrics.
946    pub fn metrics(&mut self) -> LoudnessMetrics {
947        let momentary = if self.config.enable_momentary {
948            self.lkfs_calculator.momentary_loudness()
949        } else {
950            f64::NEG_INFINITY
951        };
952
953        let short_term = if self.config.enable_short_term {
954            self.lkfs_calculator.short_term_loudness()
955        } else {
956            f64::NEG_INFINITY
957        };
958
959        let integrated = if self.config.enable_integrated {
960            self.gating_processor.integrated_loudness()
961        } else {
962            f64::NEG_INFINITY
963        };
964
965        let loudness_range = if let Some(ref mut lra_calc) = self.lra_calculator {
966            let blocks = self.gating_processor.get_blocks_for_lra();
967            lra_calc.calculate(&blocks)
968        } else {
969            0.0
970        };
971
972        let (true_peak_dbtp, true_peak_linear, channel_peaks_dbtp) =
973            if let Some(ref detector) = self.true_peak_detector {
974                let peaks = detector.channel_peaks_dbtp();
975                let max_peak = detector.true_peak_dbtp();
976                let max_linear = detector.true_peak_linear();
977                (max_peak, max_linear, peaks)
978            } else {
979                (f64::NEG_INFINITY, 0.0, vec![])
980            };
981
982        LoudnessMetrics {
983            momentary_lufs: momentary,
984            short_term_lufs: short_term,
985            integrated_lufs: integrated,
986            loudness_range,
987            true_peak_dbtp,
988            true_peak_linear,
989            max_momentary: self.lkfs_calculator.max_momentary(),
990            max_short_term: self.lkfs_calculator.max_short_term(),
991            channel_peaks_dbtp,
992        }
993    }
994
995    /// Check compliance with the configured standard.
996    pub fn check_compliance(&mut self) -> ComplianceResult {
997        let metrics = self.metrics();
998        let standard = &self.config.standard;
999
1000        let target = standard.target_lufs();
1001        let tolerance = standard.tolerance_lu();
1002        let max_peak = standard.max_true_peak_dbtp();
1003
1004        let loudness_compliant = if metrics.integrated_lufs.is_finite() {
1005            metrics.integrated_lufs >= target - tolerance
1006                && metrics.integrated_lufs <= target + tolerance
1007        } else {
1008            false
1009        };
1010
1011        let peak_compliant = metrics.true_peak_dbtp <= max_peak;
1012
1013        let lra_acceptable = metrics.loudness_range >= 1.0 && metrics.loudness_range <= 30.0;
1014
1015        ComplianceResult {
1016            standard: *standard,
1017            loudness_compliant,
1018            peak_compliant,
1019            lra_acceptable,
1020            integrated_lufs: metrics.integrated_lufs,
1021            true_peak_dbtp: metrics.true_peak_dbtp,
1022            loudness_range: metrics.loudness_range,
1023            target_lufs: target,
1024            max_peak_dbtp: max_peak,
1025            deviation_lu: if metrics.integrated_lufs.is_finite() {
1026                metrics.integrated_lufs - target
1027            } else {
1028                0.0
1029            },
1030        }
1031    }
1032
1033    /// Generate a detailed loudness report.
1034    #[allow(clippy::cast_precision_loss)]
1035    pub fn generate_report(&mut self) -> LoudnessReport {
1036        let metrics = self.metrics();
1037        let compliance = self.check_compliance();
1038        let duration_seconds = self.samples_processed as f64 / self.config.sample_rate;
1039
1040        LoudnessReport::new(metrics, compliance, duration_seconds)
1041    }
1042
1043    /// Reset the meter to initial state.
1044    pub fn reset(&mut self) {
1045        self.lkfs_calculator.reset();
1046        self.gating_processor.reset();
1047        if let Some(ref mut detector) = self.true_peak_detector {
1048            detector.reset();
1049        }
1050        if let Some(ref mut lra_calc) = self.lra_calculator {
1051            lra_calc.reset();
1052        }
1053        self.filter_bank.reset();
1054        self.samples_processed = 0;
1055    }
1056
1057    /// Get the meter configuration.
1058    pub fn config(&self) -> &MeterConfig {
1059        &self.config
1060    }
1061
1062    /// Get the number of samples processed (per channel).
1063    pub fn samples_processed(&self) -> usize {
1064        self.samples_processed
1065    }
1066
1067    /// Get the duration of processed audio in seconds.
1068    #[allow(clippy::cast_precision_loss)]
1069    pub fn duration_seconds(&self) -> f64 {
1070        self.samples_processed as f64 / self.config.sample_rate
1071    }
1072}
1073
1074/// Compliance result.
1075#[derive(Clone, Debug)]
1076pub struct ComplianceResult {
1077    /// Standard being checked.
1078    pub standard: Standard,
1079    /// Is loudness compliant?
1080    pub loudness_compliant: bool,
1081    /// Is peak compliant?
1082    pub peak_compliant: bool,
1083    /// Is LRA acceptable?
1084    pub lra_acceptable: bool,
1085    /// Measured integrated loudness.
1086    pub integrated_lufs: f64,
1087    /// Measured true peak.
1088    pub true_peak_dbtp: f64,
1089    /// Measured loudness range.
1090    pub loudness_range: f64,
1091    /// Target loudness.
1092    pub target_lufs: f64,
1093    /// Maximum allowed peak.
1094    pub max_peak_dbtp: f64,
1095    /// Deviation from target in LU.
1096    pub deviation_lu: f64,
1097}
1098
1099impl ComplianceResult {
1100    /// Check if fully compliant (loudness and peak).
1101    pub fn is_compliant(&self) -> bool {
1102        self.loudness_compliant && self.peak_compliant
1103    }
1104
1105    /// Get the standard name.
1106    pub fn standard_name(&self) -> &str {
1107        self.standard.name()
1108    }
1109
1110    /// Get recommended gain adjustment to meet target.
1111    ///
1112    /// Returns gain in dB (positive = increase, negative = decrease).
1113    pub fn recommended_gain_db(&self) -> f64 {
1114        if self.integrated_lufs.is_finite() {
1115            self.target_lufs - self.integrated_lufs
1116        } else {
1117            0.0
1118        }
1119    }
1120}
1121
1122#[cfg(test)]
1123mod tests {
1124    use super::*;
1125
1126    /// EBU R128 reference signal test: 997 Hz sine at -23 LUFS.
1127    ///
1128    /// The K-weighting pre-filter (Stage 1: high-shelf at 1681 Hz, G≈4 dB) adds
1129    /// approximately +3.41 dB power gain at 997 Hz, so the raw signal amplitude
1130    /// must be compensated by the inverse of that gain.
1131    ///
1132    /// Calibration procedure:
1133    ///   1. Target LUFS = -23; after filter the mean-square must equal
1134    ///      `10^((-23 + 0.691) / 10)`.
1135    ///   2. The K-weight filter at 997 Hz has power gain ≈ 2.193 (+3.41 dB).
1136    ///   3. Required pre-filter RMS² = target_power / filter_gain.
1137    ///   4. For a sine wave: peak amplitude A = sqrt(2 × RMS²).
1138    ///
1139    /// We generate 10 seconds of stereo audio (enough for gating to converge)
1140    /// and assert the integrated loudness is within ±0.5 LUFS of -23.0.
1141    #[test]
1142    fn test_ebu_r128_reference_signal() {
1143        let sample_rate = 48000.0_f64;
1144        let channels = 2_usize;
1145        let duration_secs = 10.0_f64;
1146        let freq_hz = 997.0_f64;
1147
1148        // K-weighting pre-filter adds ~3.41 dB power gain at 997 Hz for the
1149        // standard ITU-R BS.1770-4 coefficients implemented in filters.rs.
1150        // amplitude calibrated so the integrated loudness converges to -23 LUFS.
1151        let k_weight_power_gain_db = 3.41_f64;
1152        let target_power = 10.0_f64.powf((-23.0_f64 + 0.691) / 10.0);
1153        let filter_power_gain = 10.0_f64.powf(k_weight_power_gain_db / 10.0);
1154        // For a stereo signal with identical L/R: the gating normalises by
1155        // total_weight = 2.0, so block_ms = (ch0_ms + ch1_ms) / N / 2 = A²/2 * gain.
1156        // Solving A²/2 * gain = target_power:
1157        let amplitude = (2.0 * target_power / filter_power_gain).sqrt();
1158
1159        let total_samples = (sample_rate * duration_secs) as usize;
1160        let mut interleaved = Vec::with_capacity(total_samples * channels);
1161
1162        for i in 0..total_samples {
1163            let t = i as f64 / sample_rate;
1164            let sample = amplitude * (2.0 * std::f64::consts::PI * freq_hz * t).sin();
1165            // Stereo: identical L and R channels
1166            interleaved.push(sample);
1167            interleaved.push(sample);
1168        }
1169
1170        let config = MeterConfig::new(Standard::EbuR128, sample_rate, channels);
1171        let mut meter = LoudnessMeter::new(config).expect("Failed to create LoudnessMeter");
1172        meter.process_f64(&interleaved);
1173
1174        let metrics = meter.metrics();
1175        let integrated = metrics.integrated_lufs;
1176
1177        assert!(
1178            integrated.is_finite(),
1179            "Integrated loudness should be finite, got {integrated}"
1180        );
1181        assert!(
1182            (integrated - (-23.0)).abs() <= 0.5,
1183            "Expected -23.0 LUFS ±0.5, got {integrated:.2} LUFS"
1184        );
1185    }
1186
1187    /// Verify TidalHiFi standard has correct target loudness.
1188    #[test]
1189    fn test_tidal_hifi_standard() {
1190        let s = Standard::TidalHiFi;
1191        assert_eq!(s.target_lufs(), -14.0);
1192        assert_eq!(s.max_true_peak_dbtp(), -1.0);
1193        assert_eq!(s.name(), "Tidal HiFi");
1194    }
1195
1196    /// Verify AmazonMusicHd standard has correct target loudness.
1197    #[test]
1198    fn test_amazon_music_hd_standard() {
1199        let s = Standard::AmazonMusicHd;
1200        assert_eq!(s.target_lufs(), -14.0);
1201        assert_eq!(s.max_true_peak_dbtp(), -1.0);
1202        assert_eq!(s.name(), "Amazon Music HD");
1203    }
1204}