1#[cfg(feature = "alloc")]
10use alloc::vec::Vec;
11
12use crate::blocks::{BlockAccumulator, MOMENTARY_BLOCKS, SHORT_TERM_BLOCKS};
13use crate::channel::Channel;
14use crate::error::Error;
15use crate::filter::KFilter;
16use crate::mode::Mode;
17#[cfg(feature = "alloc")]
18use crate::peak::TruePeakState;
19use crate::report::Report;
20use crate::sample::Sample;
21use crate::snapshot::Snapshot;
22
23pub(crate) const MAX_CHANNELS: usize = 24;
28
29const SUPPORTED_RATES: &[u32] = &[22_050, 32_000, 44_100, 48_000, 88_200, 96_000, 192_000];
34
35pub(crate) const LUFS_OFFSET: f64 = -0.691;
37
38#[cfg(feature = "alloc")]
40pub(crate) const ABSOLUTE_GATE_LUFS: f64 = -70.0;
41
42#[cfg(feature = "alloc")]
44pub(crate) const RELATIVE_GATE_OFFSET_LU: f64 = -10.0;
45
46#[cfg(feature = "alloc")]
48pub(crate) const LRA_RELATIVE_GATE_OFFSET_LU: f64 = -20.0;
49
50#[derive(Debug, Clone)]
65pub struct AnalyzerBuilder {
66 sample_rate: u32,
67 channels: [Channel; MAX_CHANNELS],
68 n_channels: u8,
69 overflow: bool,
72 modes: Mode,
73 modes_set: bool,
76 channels_set: bool,
77 expected_duration_secs: Option<f64>,
81}
82
83impl Default for AnalyzerBuilder {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89impl AnalyzerBuilder {
90 #[must_use]
92 pub fn new() -> Self {
93 Self {
94 sample_rate: 48_000,
95 channels: [Channel::Other; MAX_CHANNELS],
96 n_channels: 0,
97 overflow: false,
98 modes: Mode::All,
99 modes_set: false,
100 channels_set: false,
101 expected_duration_secs: None,
102 }
103 }
104
105 #[must_use]
128 pub fn expected_duration(mut self, duration: core::time::Duration) -> Self {
129 self.expected_duration_secs = Some(duration.as_secs_f64());
130 self
131 }
132
133 #[must_use]
136 pub fn sample_rate(mut self, hz: u32) -> Self {
137 self.sample_rate = hz;
138 self
139 }
140
141 #[must_use]
145 pub fn channels(mut self, layout: &[Channel]) -> Self {
146 self.channels_set = true;
147 if layout.len() > MAX_CHANNELS {
148 self.overflow = true;
149 return self;
150 }
151 self.n_channels = layout.len() as u8;
152 for (i, &c) in layout.iter().enumerate() {
153 self.channels[i] = c;
154 }
155 self
156 }
157
158 #[must_use]
160 pub fn modes(mut self, modes: Mode) -> Self {
161 self.modes = modes;
162 self.modes_set = true;
163 self
164 }
165
166 pub fn build(self) -> Result<Analyzer, Error> {
168 if !SUPPORTED_RATES.contains(&self.sample_rate) {
169 return Err(Error::InvalidSampleRate {
170 hz: self.sample_rate,
171 });
172 }
173 if self.overflow {
174 return Err(Error::InvalidChannelLayout {
175 got: usize::MAX,
176 max: MAX_CHANNELS,
177 });
178 }
179 if !self.channels_set || self.n_channels == 0 {
180 return Err(Error::InvalidChannelLayout {
181 got: 0,
182 max: MAX_CHANNELS,
183 });
184 }
185 if self.modes_set && self.modes.is_empty() {
186 return Err(Error::NoModesSelected);
187 }
188 let modes = if self.modes_set {
189 self.modes
190 } else {
191 Mode::All
192 };
193
194 #[cfg(not(feature = "alloc"))]
195 if modes.contains(Mode::Integrated) {
196 return Err(Error::IntegratedRequiresAlloc);
197 }
198 #[cfg(not(feature = "alloc"))]
199 if modes.contains(Mode::Lra) {
200 return Err(Error::LraRequiresAlloc);
201 }
202
203 Ok(Analyzer::new(
204 self.sample_rate,
205 self.channels,
206 self.n_channels as usize,
207 modes,
208 self.expected_duration_secs,
209 ))
210 }
211}
212
213#[derive(Debug)]
218pub struct Analyzer {
219 sample_rate: u32,
220 channels: [Channel; MAX_CHANNELS],
221 n_channels: usize,
222 modes: Mode,
223 samples_per_block: u32,
224 filters: [KFilter; MAX_CHANNELS],
226 block_acc: BlockAccumulator,
229 momentary_ring: [f32; MOMENTARY_BLOCKS],
231 momentary_filled: usize,
232 momentary_idx: usize,
233 short_term_ring: [f32; SHORT_TERM_BLOCKS],
235 short_term_filled: usize,
236 short_term_idx: usize,
237 #[cfg(feature = "alloc")]
240 programme_blocks: Vec<f32>,
241 #[cfg(feature = "alloc")]
244 short_term_samples: Vec<f32>,
245 #[cfg(feature = "alloc")]
247 true_peak: Option<TruePeakState>,
248 momentary_max: Option<f64>,
250 short_term_max: Option<f64>,
252 samples_ingested: u64,
254 cached_snapshot: Option<Snapshot>,
256}
257
258impl Analyzer {
259 fn new(
260 sample_rate: u32,
261 channels: [Channel; MAX_CHANNELS],
262 n_channels: usize,
263 modes: Mode,
264 expected_duration_secs: Option<f64>,
265 ) -> Self {
266 #[cfg(not(feature = "alloc"))]
267 let _ = expected_duration_secs;
268 let samples_per_block = sample_rate / 10;
269 let template = KFilter::new(sample_rate);
274 let mut filters: [KFilter; MAX_CHANNELS] = [template; MAX_CHANNELS];
275 for f in filters.iter_mut().take(n_channels) {
276 f.reset();
277 }
278 let block_acc = BlockAccumulator::new(n_channels, samples_per_block);
279
280 #[cfg(feature = "alloc")]
284 let reserve_blocks: usize = expected_duration_secs
285 .map(|s| (s.max(0.0) * 10.0).min(48.0 * 3_600.0 * 10.0) as usize)
286 .unwrap_or(0);
287
288 Self {
289 sample_rate,
290 channels,
291 n_channels,
292 modes,
293 samples_per_block,
294 filters,
295 block_acc,
296 momentary_ring: [0.0; MOMENTARY_BLOCKS],
297 momentary_filled: 0,
298 momentary_idx: 0,
299 short_term_ring: [0.0; SHORT_TERM_BLOCKS],
300 short_term_filled: 0,
301 short_term_idx: 0,
302 #[cfg(feature = "alloc")]
303 programme_blocks: if modes.contains(Mode::Integrated) {
304 Vec::with_capacity(reserve_blocks)
305 } else {
306 Vec::new()
307 },
308 #[cfg(feature = "alloc")]
309 short_term_samples: if modes.contains(Mode::Lra) {
310 Vec::with_capacity(reserve_blocks)
311 } else {
312 Vec::new()
313 },
314 #[cfg(feature = "alloc")]
315 true_peak: if modes.contains(Mode::TruePeak) {
316 Some(TruePeakState::new(n_channels, sample_rate))
317 } else {
318 None
319 },
320 momentary_max: None,
321 short_term_max: None,
322 samples_ingested: 0,
323 cached_snapshot: None,
324 }
325 }
326
327 #[inline]
329 #[must_use]
330 pub fn sample_rate(&self) -> u32 {
331 self.sample_rate
332 }
333
334 #[inline]
336 #[must_use]
337 pub fn channels(&self) -> &[Channel] {
338 &self.channels[..self.n_channels]
339 }
340
341 #[inline]
343 #[must_use]
344 pub fn modes(&self) -> Mode {
345 self.modes
346 }
347
348 #[inline]
350 #[must_use]
351 pub fn samples_per_block(&self) -> u32 {
352 self.samples_per_block
353 }
354
355 pub fn push_planar<S: Sample>(&mut self, channels: &[&[S]]) -> Result<(), Error> {
384 if channels.len() != self.n_channels {
385 return Err(Error::ChannelMismatch {
386 expected: self.n_channels,
387 got: channels.len(),
388 });
389 }
390 if channels.is_empty() {
391 return Ok(());
392 }
393 let frames = channels[0].len();
394 for ch in &channels[1..] {
395 if ch.len() != frames {
396 return Err(Error::PlanarLengthMismatch {
397 first: frames,
398 got: ch.len(),
399 });
400 }
401 }
402 self.cached_snapshot = None;
403 let n_ch = self.n_channels;
404 for i in 0..frames {
405 let mut frame: [f32; MAX_CHANNELS] = [0.0; MAX_CHANNELS];
406 for (ch_idx, slice) in channels.iter().enumerate() {
407 let v = slice[i].to_f32();
408 if !v.is_finite() {
409 return Err(Error::NonFiniteSample);
410 }
411 frame[ch_idx] = v;
412 }
413 self.process_frame(&frame[..n_ch]);
414 }
415 self.samples_ingested = self.samples_ingested.saturating_add(frames as u64);
416 Ok(())
417 }
418
419 pub fn push_interleaved<S: Sample>(&mut self, samples: &[S]) -> Result<(), Error> {
447 let n_ch = self.n_channels;
448 if n_ch == 0 {
449 return Ok(());
450 }
451 if samples.len() % n_ch != 0 {
452 return Err(Error::InterleavedLengthNotMultiple {
453 samples: samples.len(),
454 channels: n_ch,
455 });
456 }
457 self.cached_snapshot = None;
458 let frames = samples.len() / n_ch;
459 for f in 0..frames {
460 let mut frame: [f32; MAX_CHANNELS] = [0.0; MAX_CHANNELS];
461 for ch in 0..n_ch {
462 let v = samples[f * n_ch + ch].to_f32();
463 if !v.is_finite() {
464 return Err(Error::NonFiniteSample);
465 }
466 frame[ch] = v;
467 }
468 self.process_frame(&frame[..n_ch]);
469 }
470 self.samples_ingested = self.samples_ingested.saturating_add(frames as u64);
471 Ok(())
472 }
473
474 #[inline]
476 fn process_frame(&mut self, frame: &[f32]) {
477 #[cfg(feature = "alloc")]
479 if let Some(tp) = self.true_peak.as_mut() {
480 tp.feed_frame(frame);
481 }
482 let mut weighted: [f32; MAX_CHANNELS] = [0.0; MAX_CHANNELS];
484 for (ch, &s) in frame.iter().enumerate() {
485 weighted[ch] = self.filters[ch].process(s);
486 }
487 let block_complete = self.block_acc.push_frame(&weighted[..frame.len()]);
489 if block_complete {
490 let block_ms = self.block_acc.take_block();
491 self.on_block_emitted(&block_ms);
492 }
493 }
494
495 fn on_block_emitted(&mut self, per_channel_ms: &[f32]) {
497 let mut weighted_sum: f32 = 0.0;
499 for (i, &ms) in per_channel_ms.iter().enumerate() {
500 weighted_sum += self.channels[i].weight() * ms;
501 }
502
503 self.momentary_ring[self.momentary_idx] = weighted_sum;
505 self.momentary_idx = (self.momentary_idx + 1) % MOMENTARY_BLOCKS;
506 if self.momentary_filled < MOMENTARY_BLOCKS {
507 self.momentary_filled += 1;
508 }
509 if self.momentary_filled == MOMENTARY_BLOCKS {
510 let mean = self.momentary_ring.iter().sum::<f32>() / MOMENTARY_BLOCKS as f32;
511 if let Some(lufs) = ms_to_lufs(mean) {
512 self.momentary_max = Some(self.momentary_max.map_or(lufs, |m| m.max(lufs)));
513 }
514 #[cfg(feature = "alloc")]
516 if self.modes.contains(Mode::Integrated) {
517 self.programme_blocks.push(mean);
518 }
519 }
520
521 self.short_term_ring[self.short_term_idx] = weighted_sum;
523 self.short_term_idx = (self.short_term_idx + 1) % SHORT_TERM_BLOCKS;
524 if self.short_term_filled < SHORT_TERM_BLOCKS {
525 self.short_term_filled += 1;
526 }
527 if self.short_term_filled == SHORT_TERM_BLOCKS {
528 let mean = self.short_term_ring.iter().sum::<f32>() / SHORT_TERM_BLOCKS as f32;
529 if let Some(lufs) = ms_to_lufs(mean) {
530 self.short_term_max = Some(self.short_term_max.map_or(lufs, |m| m.max(lufs)));
531 }
532 #[cfg(feature = "alloc")]
533 if self.modes.contains(Mode::Lra) {
534 self.short_term_samples.push(mean);
535 }
536 }
537 }
538
539 pub fn snapshot(&mut self) -> Snapshot {
563 if let Some(cached) = self.cached_snapshot {
564 return cached;
565 }
566
567 let momentary_lufs =
568 if self.modes.contains(Mode::Momentary) && self.momentary_filled == MOMENTARY_BLOCKS {
569 let mean = self.momentary_ring.iter().sum::<f32>() / MOMENTARY_BLOCKS as f32;
570 ms_to_lufs(mean)
571 } else {
572 None
573 };
574
575 let short_term_lufs = if self.modes.contains(Mode::ShortTerm)
576 && self.short_term_filled == SHORT_TERM_BLOCKS
577 {
578 let mean = self.short_term_ring.iter().sum::<f32>() / SHORT_TERM_BLOCKS as f32;
579 ms_to_lufs(mean)
580 } else {
581 None
582 };
583
584 #[cfg(feature = "alloc")]
585 let integrated_lufs = if self.modes.contains(Mode::Integrated) {
586 crate::gating::compute_integrated(&self.programme_blocks)
587 } else {
588 None
589 };
590 #[cfg(not(feature = "alloc"))]
591 let integrated_lufs: Option<f64> = None;
592
593 #[cfg(feature = "alloc")]
594 let true_peak_dbtp = self.true_peak.as_ref().and_then(|tp| tp.peak_dbtp());
595 #[cfg(not(feature = "alloc"))]
596 let true_peak_dbtp: Option<f64> = None;
597
598 #[cfg(feature = "alloc")]
599 let loudness_range_lu = if self.modes.contains(Mode::Lra) {
600 crate::lra::compute_lra(&self.short_term_samples)
601 } else {
602 None
603 };
604 #[cfg(not(feature = "alloc"))]
605 let loudness_range_lu: Option<f64> = None;
606
607 let programme_duration_seconds = self.samples_ingested as f64 / self.sample_rate as f64;
608
609 let snap = Snapshot {
610 momentary_lufs,
611 short_term_lufs,
612 integrated_lufs,
613 true_peak_dbtp,
614 loudness_range_lu,
615 programme_duration_seconds,
616 };
617 self.cached_snapshot = Some(snap);
618 snap
619 }
620
621 pub fn finalize(mut self) -> Report {
640 let snap = self.snapshot();
641 Report {
642 integrated_lufs: snap.integrated_lufs,
643 loudness_range_lu: snap.loudness_range_lu,
644 true_peak_dbtp: snap.true_peak_dbtp,
645 momentary_max_lufs: self.momentary_max,
646 short_term_max_lufs: self.short_term_max,
647 programme_duration_seconds: snap.programme_duration_seconds,
648 }
649 }
650
651 pub fn reset(&mut self) {
672 for f in self.filters.iter_mut().take(self.n_channels) {
673 f.reset();
674 }
675 self.block_acc.reset();
676 self.momentary_ring = [0.0; MOMENTARY_BLOCKS];
677 self.momentary_filled = 0;
678 self.momentary_idx = 0;
679 self.short_term_ring = [0.0; SHORT_TERM_BLOCKS];
680 self.short_term_filled = 0;
681 self.short_term_idx = 0;
682 #[cfg(feature = "alloc")]
683 {
684 self.programme_blocks.clear();
685 self.short_term_samples.clear();
686 if let Some(tp) = self.true_peak.as_mut() {
687 tp.reset();
688 }
689 }
690 self.momentary_max = None;
691 self.short_term_max = None;
692 self.samples_ingested = 0;
693 self.cached_snapshot = None;
694 }
695}
696
697#[inline]
700pub(crate) fn ms_to_lufs(ms: f32) -> Option<f64> {
701 if ms <= 0.0 {
702 None
703 } else {
704 Some(LUFS_OFFSET + 10.0 * libm::log10(ms as f64))
705 }
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711 #[cfg(feature = "alloc")]
712 use alloc::vec;
713
714 #[test]
715 fn smoke_builder_rejects_bad_sample_rate() {
716 let err = AnalyzerBuilder::new()
717 .sample_rate(44_101)
718 .channels(&[Channel::Left, Channel::Right])
719 .modes(Mode::Integrated | Mode::Momentary)
720 .build()
721 .unwrap_err();
722 assert!(matches!(err, Error::InvalidSampleRate { hz: 44_101 }));
723 }
724
725 #[test]
726 fn smoke_builder_accepts_supported_rate() {
727 let a = AnalyzerBuilder::new()
728 .sample_rate(48_000)
729 .channels(&[Channel::Left, Channel::Right])
730 .modes(Mode::Momentary)
731 .build()
732 .unwrap();
733 assert_eq!(a.sample_rate(), 48_000);
734 assert_eq!(a.channels().len(), 2);
735 assert_eq!(a.samples_per_block(), 4_800);
736 }
737
738 #[test]
739 fn smoke_builder_rejects_empty_channels() {
740 let err = AnalyzerBuilder::new()
741 .sample_rate(48_000)
742 .channels(&[])
743 .modes(Mode::Momentary)
744 .build()
745 .unwrap_err();
746 assert!(matches!(err, Error::InvalidChannelLayout { got: 0, .. }));
747 }
748
749 #[test]
750 fn smoke_builder_rejects_no_modes() {
751 let err = AnalyzerBuilder::new()
752 .sample_rate(48_000)
753 .channels(&[Channel::Left])
754 .modes(Mode::empty())
755 .build()
756 .unwrap_err();
757 assert!(matches!(err, Error::NoModesSelected));
758 }
759
760 #[test]
761 fn smoke_push_interleaved_validates_length() {
762 let mut a = AnalyzerBuilder::new()
763 .sample_rate(48_000)
764 .channels(&[Channel::Left, Channel::Right])
765 .modes(Mode::Momentary)
766 .build()
767 .unwrap();
768 let err = a.push_interleaved::<f32>(&[0.0, 0.0, 0.0]).unwrap_err();
769 assert!(matches!(
770 err,
771 Error::InterleavedLengthNotMultiple {
772 samples: 3,
773 channels: 2
774 }
775 ));
776 }
777
778 #[test]
779 fn smoke_push_planar_validates_channel_count() {
780 let mut a = AnalyzerBuilder::new()
781 .sample_rate(48_000)
782 .channels(&[Channel::Left, Channel::Right])
783 .modes(Mode::Momentary)
784 .build()
785 .unwrap();
786 let mono: &[f32] = &[0.0; 100];
787 let err = a.push_planar::<f32>(&[mono]).unwrap_err();
788 assert!(matches!(
789 err,
790 Error::ChannelMismatch {
791 expected: 2,
792 got: 1
793 }
794 ));
795 }
796
797 #[cfg(feature = "alloc")]
798 #[test]
799 fn smoke_reset_clears_state() {
800 let mut a = AnalyzerBuilder::new()
801 .sample_rate(48_000)
802 .channels(&[Channel::Left])
803 .modes(Mode::Momentary)
804 .build()
805 .unwrap();
806 let buf: Vec<f32> = vec![0.5; 4_800 * 5];
807 a.push_interleaved::<f32>(&buf).unwrap();
808 a.reset();
809 let snap = a.snapshot();
810 assert_eq!(snap.programme_duration_seconds(), 0.0);
811 assert!(snap.momentary_lufs().is_none());
812 }
813
814 #[test]
815 fn smoke_too_many_channels_rejected() {
816 let many = [Channel::Other; 32];
818 let err = AnalyzerBuilder::new()
819 .sample_rate(48_000)
820 .channels(&many)
821 .modes(Mode::Momentary)
822 .build()
823 .unwrap_err();
824 assert!(matches!(err, Error::InvalidChannelLayout { .. }));
825 }
826
827 #[test]
828 fn supports_22_2_immersive_24_channels() {
829 let layout = [Channel::Other; 24];
831 let mut a = AnalyzerBuilder::new()
832 .sample_rate(48_000)
833 .channels(&layout)
834 .modes(Mode::Momentary)
835 .build()
836 .unwrap();
837 let buf = vec![0.05f32; 4_800 * 24]; a.push_interleaved::<f32>(&buf).unwrap();
839 assert_eq!(a.channels().len(), 24);
840 }
841}