#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use crate::blocks::{BlockAccumulator, MOMENTARY_BLOCKS, SHORT_TERM_BLOCKS};
use crate::channel::Channel;
use crate::error::Error;
use crate::filter::KFilter;
use crate::mode::Mode;
#[cfg(feature = "alloc")]
use crate::peak::TruePeakState;
use crate::report::Report;
use crate::sample::Sample;
use crate::snapshot::Snapshot;
pub(crate) const MAX_CHANNELS: usize = 24;
const SUPPORTED_RATES: &[u32] = &[22_050, 32_000, 44_100, 48_000, 88_200, 96_000, 192_000];
pub(crate) const LUFS_OFFSET: f64 = -0.691;
#[cfg(feature = "alloc")]
pub(crate) const ABSOLUTE_GATE_LUFS: f64 = -70.0;
#[cfg(feature = "alloc")]
pub(crate) const RELATIVE_GATE_OFFSET_LU: f64 = -10.0;
#[cfg(feature = "alloc")]
pub(crate) const LRA_RELATIVE_GATE_OFFSET_LU: f64 = -20.0;
#[derive(Debug, Clone)]
pub struct AnalyzerBuilder {
sample_rate: u32,
channels: [Channel; MAX_CHANNELS],
n_channels: u8,
overflow: bool,
modes: Mode,
modes_set: bool,
channels_set: bool,
expected_duration_secs: Option<f64>,
}
impl Default for AnalyzerBuilder {
fn default() -> Self {
Self::new()
}
}
impl AnalyzerBuilder {
#[must_use]
pub fn new() -> Self {
Self {
sample_rate: 48_000,
channels: [Channel::Other; MAX_CHANNELS],
n_channels: 0,
overflow: false,
modes: Mode::All,
modes_set: false,
channels_set: false,
expected_duration_secs: None,
}
}
#[must_use]
pub fn expected_duration(mut self, duration: core::time::Duration) -> Self {
self.expected_duration_secs = Some(duration.as_secs_f64());
self
}
#[must_use]
pub fn sample_rate(mut self, hz: u32) -> Self {
self.sample_rate = hz;
self
}
#[must_use]
pub fn channels(mut self, layout: &[Channel]) -> Self {
self.channels_set = true;
if layout.len() > MAX_CHANNELS {
self.overflow = true;
return self;
}
self.n_channels = layout.len() as u8;
for (i, &c) in layout.iter().enumerate() {
self.channels[i] = c;
}
self
}
#[must_use]
pub fn modes(mut self, modes: Mode) -> Self {
self.modes = modes;
self.modes_set = true;
self
}
pub fn build(self) -> Result<Analyzer, Error> {
if !SUPPORTED_RATES.contains(&self.sample_rate) {
return Err(Error::InvalidSampleRate {
hz: self.sample_rate,
});
}
if self.overflow {
return Err(Error::InvalidChannelLayout {
got: usize::MAX,
max: MAX_CHANNELS,
});
}
if !self.channels_set || self.n_channels == 0 {
return Err(Error::InvalidChannelLayout {
got: 0,
max: MAX_CHANNELS,
});
}
if self.modes_set && self.modes.is_empty() {
return Err(Error::NoModesSelected);
}
let modes = if self.modes_set {
self.modes
} else {
Mode::All
};
#[cfg(not(feature = "alloc"))]
if modes.contains(Mode::Integrated) {
return Err(Error::IntegratedRequiresAlloc);
}
#[cfg(not(feature = "alloc"))]
if modes.contains(Mode::Lra) {
return Err(Error::LraRequiresAlloc);
}
Ok(Analyzer::new(
self.sample_rate,
self.channels,
self.n_channels as usize,
modes,
self.expected_duration_secs,
))
}
}
#[derive(Debug)]
pub struct Analyzer {
sample_rate: u32,
channels: [Channel; MAX_CHANNELS],
n_channels: usize,
modes: Mode,
samples_per_block: u32,
filters: [KFilter; MAX_CHANNELS],
block_acc: BlockAccumulator,
momentary_ring: [f32; MOMENTARY_BLOCKS],
momentary_filled: usize,
momentary_idx: usize,
short_term_ring: [f32; SHORT_TERM_BLOCKS],
short_term_filled: usize,
short_term_idx: usize,
#[cfg(feature = "alloc")]
programme_blocks: Vec<f32>,
#[cfg(feature = "alloc")]
short_term_samples: Vec<f32>,
#[cfg(feature = "alloc")]
true_peak: Option<TruePeakState>,
momentary_max: Option<f64>,
short_term_max: Option<f64>,
samples_ingested: u64,
cached_snapshot: Option<Snapshot>,
}
impl Analyzer {
fn new(
sample_rate: u32,
channels: [Channel; MAX_CHANNELS],
n_channels: usize,
modes: Mode,
expected_duration_secs: Option<f64>,
) -> Self {
#[cfg(not(feature = "alloc"))]
let _ = expected_duration_secs;
let samples_per_block = sample_rate / 10;
let template = KFilter::new(sample_rate);
let mut filters: [KFilter; MAX_CHANNELS] = [template; MAX_CHANNELS];
for f in filters.iter_mut().take(n_channels) {
f.reset();
}
let block_acc = BlockAccumulator::new(n_channels, samples_per_block);
#[cfg(feature = "alloc")]
let reserve_blocks: usize = expected_duration_secs
.map(|s| (s.max(0.0) * 10.0).min(48.0 * 3_600.0 * 10.0) as usize)
.unwrap_or(0);
Self {
sample_rate,
channels,
n_channels,
modes,
samples_per_block,
filters,
block_acc,
momentary_ring: [0.0; MOMENTARY_BLOCKS],
momentary_filled: 0,
momentary_idx: 0,
short_term_ring: [0.0; SHORT_TERM_BLOCKS],
short_term_filled: 0,
short_term_idx: 0,
#[cfg(feature = "alloc")]
programme_blocks: if modes.contains(Mode::Integrated) {
Vec::with_capacity(reserve_blocks)
} else {
Vec::new()
},
#[cfg(feature = "alloc")]
short_term_samples: if modes.contains(Mode::Lra) {
Vec::with_capacity(reserve_blocks)
} else {
Vec::new()
},
#[cfg(feature = "alloc")]
true_peak: if modes.contains(Mode::TruePeak) {
Some(TruePeakState::new(n_channels, sample_rate))
} else {
None
},
momentary_max: None,
short_term_max: None,
samples_ingested: 0,
cached_snapshot: None,
}
}
#[inline]
#[must_use]
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
#[inline]
#[must_use]
pub fn channels(&self) -> &[Channel] {
&self.channels[..self.n_channels]
}
#[inline]
#[must_use]
pub fn modes(&self) -> Mode {
self.modes
}
#[inline]
#[must_use]
pub fn samples_per_block(&self) -> u32 {
self.samples_per_block
}
pub fn push_planar<S: Sample>(&mut self, channels: &[&[S]]) -> Result<(), Error> {
if channels.len() != self.n_channels {
return Err(Error::ChannelMismatch {
expected: self.n_channels,
got: channels.len(),
});
}
if channels.is_empty() {
return Ok(());
}
let frames = channels[0].len();
for ch in &channels[1..] {
if ch.len() != frames {
return Err(Error::PlanarLengthMismatch {
first: frames,
got: ch.len(),
});
}
}
self.cached_snapshot = None;
let n_ch = self.n_channels;
for i in 0..frames {
let mut frame: [f32; MAX_CHANNELS] = [0.0; MAX_CHANNELS];
for (ch_idx, slice) in channels.iter().enumerate() {
let v = slice[i].to_f32();
if !v.is_finite() {
return Err(Error::NonFiniteSample);
}
frame[ch_idx] = v;
}
self.process_frame(&frame[..n_ch]);
}
self.samples_ingested = self.samples_ingested.saturating_add(frames as u64);
Ok(())
}
pub fn push_interleaved<S: Sample>(&mut self, samples: &[S]) -> Result<(), Error> {
let n_ch = self.n_channels;
if n_ch == 0 {
return Ok(());
}
if samples.len() % n_ch != 0 {
return Err(Error::InterleavedLengthNotMultiple {
samples: samples.len(),
channels: n_ch,
});
}
self.cached_snapshot = None;
let frames = samples.len() / n_ch;
for f in 0..frames {
let mut frame: [f32; MAX_CHANNELS] = [0.0; MAX_CHANNELS];
for ch in 0..n_ch {
let v = samples[f * n_ch + ch].to_f32();
if !v.is_finite() {
return Err(Error::NonFiniteSample);
}
frame[ch] = v;
}
self.process_frame(&frame[..n_ch]);
}
self.samples_ingested = self.samples_ingested.saturating_add(frames as u64);
Ok(())
}
#[inline]
fn process_frame(&mut self, frame: &[f32]) {
#[cfg(feature = "alloc")]
if let Some(tp) = self.true_peak.as_mut() {
tp.feed_frame(frame);
}
let mut weighted: [f32; MAX_CHANNELS] = [0.0; MAX_CHANNELS];
for (ch, &s) in frame.iter().enumerate() {
weighted[ch] = self.filters[ch].process(s);
}
let block_complete = self.block_acc.push_frame(&weighted[..frame.len()]);
if block_complete {
let block_ms = self.block_acc.take_block();
self.on_block_emitted(&block_ms);
}
}
fn on_block_emitted(&mut self, per_channel_ms: &[f32]) {
let mut weighted_sum: f32 = 0.0;
for (i, &ms) in per_channel_ms.iter().enumerate() {
weighted_sum += self.channels[i].weight() * ms;
}
self.momentary_ring[self.momentary_idx] = weighted_sum;
self.momentary_idx = (self.momentary_idx + 1) % MOMENTARY_BLOCKS;
if self.momentary_filled < MOMENTARY_BLOCKS {
self.momentary_filled += 1;
}
if self.momentary_filled == MOMENTARY_BLOCKS {
let mean = self.momentary_ring.iter().sum::<f32>() / MOMENTARY_BLOCKS as f32;
if let Some(lufs) = ms_to_lufs(mean) {
self.momentary_max = Some(self.momentary_max.map_or(lufs, |m| m.max(lufs)));
}
#[cfg(feature = "alloc")]
if self.modes.contains(Mode::Integrated) {
self.programme_blocks.push(mean);
}
}
self.short_term_ring[self.short_term_idx] = weighted_sum;
self.short_term_idx = (self.short_term_idx + 1) % SHORT_TERM_BLOCKS;
if self.short_term_filled < SHORT_TERM_BLOCKS {
self.short_term_filled += 1;
}
if self.short_term_filled == SHORT_TERM_BLOCKS {
let mean = self.short_term_ring.iter().sum::<f32>() / SHORT_TERM_BLOCKS as f32;
if let Some(lufs) = ms_to_lufs(mean) {
self.short_term_max = Some(self.short_term_max.map_or(lufs, |m| m.max(lufs)));
}
#[cfg(feature = "alloc")]
if self.modes.contains(Mode::Lra) {
self.short_term_samples.push(mean);
}
}
}
pub fn snapshot(&mut self) -> Snapshot {
if let Some(cached) = self.cached_snapshot {
return cached;
}
let momentary_lufs =
if self.modes.contains(Mode::Momentary) && self.momentary_filled == MOMENTARY_BLOCKS {
let mean = self.momentary_ring.iter().sum::<f32>() / MOMENTARY_BLOCKS as f32;
ms_to_lufs(mean)
} else {
None
};
let short_term_lufs = if self.modes.contains(Mode::ShortTerm)
&& self.short_term_filled == SHORT_TERM_BLOCKS
{
let mean = self.short_term_ring.iter().sum::<f32>() / SHORT_TERM_BLOCKS as f32;
ms_to_lufs(mean)
} else {
None
};
#[cfg(feature = "alloc")]
let integrated_lufs = if self.modes.contains(Mode::Integrated) {
crate::gating::compute_integrated(&self.programme_blocks)
} else {
None
};
#[cfg(not(feature = "alloc"))]
let integrated_lufs: Option<f64> = None;
#[cfg(feature = "alloc")]
let true_peak_dbtp = self.true_peak.as_ref().and_then(|tp| tp.peak_dbtp());
#[cfg(not(feature = "alloc"))]
let true_peak_dbtp: Option<f64> = None;
#[cfg(feature = "alloc")]
let loudness_range_lu = if self.modes.contains(Mode::Lra) {
crate::lra::compute_lra(&self.short_term_samples)
} else {
None
};
#[cfg(not(feature = "alloc"))]
let loudness_range_lu: Option<f64> = None;
let programme_duration_seconds = self.samples_ingested as f64 / self.sample_rate as f64;
let snap = Snapshot {
momentary_lufs,
short_term_lufs,
integrated_lufs,
true_peak_dbtp,
loudness_range_lu,
programme_duration_seconds,
};
self.cached_snapshot = Some(snap);
snap
}
pub fn finalize(mut self) -> Report {
let snap = self.snapshot();
Report {
integrated_lufs: snap.integrated_lufs,
loudness_range_lu: snap.loudness_range_lu,
true_peak_dbtp: snap.true_peak_dbtp,
momentary_max_lufs: self.momentary_max,
short_term_max_lufs: self.short_term_max,
programme_duration_seconds: snap.programme_duration_seconds,
}
}
pub fn reset(&mut self) {
for f in self.filters.iter_mut().take(self.n_channels) {
f.reset();
}
self.block_acc.reset();
self.momentary_ring = [0.0; MOMENTARY_BLOCKS];
self.momentary_filled = 0;
self.momentary_idx = 0;
self.short_term_ring = [0.0; SHORT_TERM_BLOCKS];
self.short_term_filled = 0;
self.short_term_idx = 0;
#[cfg(feature = "alloc")]
{
self.programme_blocks.clear();
self.short_term_samples.clear();
if let Some(tp) = self.true_peak.as_mut() {
tp.reset();
}
}
self.momentary_max = None;
self.short_term_max = None;
self.samples_ingested = 0;
self.cached_snapshot = None;
}
}
#[inline]
pub(crate) fn ms_to_lufs(ms: f32) -> Option<f64> {
if ms <= 0.0 {
None
} else {
Some(LUFS_OFFSET + 10.0 * libm::log10(ms as f64))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "alloc")]
use alloc::vec;
#[test]
fn smoke_builder_rejects_bad_sample_rate() {
let err = AnalyzerBuilder::new()
.sample_rate(44_101)
.channels(&[Channel::Left, Channel::Right])
.modes(Mode::Integrated | Mode::Momentary)
.build()
.unwrap_err();
assert!(matches!(err, Error::InvalidSampleRate { hz: 44_101 }));
}
#[test]
fn smoke_builder_accepts_supported_rate() {
let a = AnalyzerBuilder::new()
.sample_rate(48_000)
.channels(&[Channel::Left, Channel::Right])
.modes(Mode::Momentary)
.build()
.unwrap();
assert_eq!(a.sample_rate(), 48_000);
assert_eq!(a.channels().len(), 2);
assert_eq!(a.samples_per_block(), 4_800);
}
#[test]
fn smoke_builder_rejects_empty_channels() {
let err = AnalyzerBuilder::new()
.sample_rate(48_000)
.channels(&[])
.modes(Mode::Momentary)
.build()
.unwrap_err();
assert!(matches!(err, Error::InvalidChannelLayout { got: 0, .. }));
}
#[test]
fn smoke_builder_rejects_no_modes() {
let err = AnalyzerBuilder::new()
.sample_rate(48_000)
.channels(&[Channel::Left])
.modes(Mode::empty())
.build()
.unwrap_err();
assert!(matches!(err, Error::NoModesSelected));
}
#[test]
fn smoke_push_interleaved_validates_length() {
let mut a = AnalyzerBuilder::new()
.sample_rate(48_000)
.channels(&[Channel::Left, Channel::Right])
.modes(Mode::Momentary)
.build()
.unwrap();
let err = a.push_interleaved::<f32>(&[0.0, 0.0, 0.0]).unwrap_err();
assert!(matches!(
err,
Error::InterleavedLengthNotMultiple {
samples: 3,
channels: 2
}
));
}
#[test]
fn smoke_push_planar_validates_channel_count() {
let mut a = AnalyzerBuilder::new()
.sample_rate(48_000)
.channels(&[Channel::Left, Channel::Right])
.modes(Mode::Momentary)
.build()
.unwrap();
let mono: &[f32] = &[0.0; 100];
let err = a.push_planar::<f32>(&[mono]).unwrap_err();
assert!(matches!(
err,
Error::ChannelMismatch {
expected: 2,
got: 1
}
));
}
#[cfg(feature = "alloc")]
#[test]
fn smoke_reset_clears_state() {
let mut a = AnalyzerBuilder::new()
.sample_rate(48_000)
.channels(&[Channel::Left])
.modes(Mode::Momentary)
.build()
.unwrap();
let buf: Vec<f32> = vec![0.5; 4_800 * 5];
a.push_interleaved::<f32>(&buf).unwrap();
a.reset();
let snap = a.snapshot();
assert_eq!(snap.programme_duration_seconds(), 0.0);
assert!(snap.momentary_lufs().is_none());
}
#[test]
fn smoke_too_many_channels_rejected() {
let many = [Channel::Other; 32];
let err = AnalyzerBuilder::new()
.sample_rate(48_000)
.channels(&many)
.modes(Mode::Momentary)
.build()
.unwrap_err();
assert!(matches!(err, Error::InvalidChannelLayout { .. }));
}
#[test]
fn supports_22_2_immersive_24_channels() {
let layout = [Channel::Other; 24];
let mut a = AnalyzerBuilder::new()
.sample_rate(48_000)
.channels(&layout)
.modes(Mode::Momentary)
.build()
.unwrap();
let buf = vec![0.05f32; 4_800 * 24]; a.push_interleaved::<f32>(&buf).unwrap();
assert_eq!(a.channels().len(), 24);
}
}