Skip to main content

ebur128_stream/
analyzer.rs

1//! [`Analyzer`] and [`AnalyzerBuilder`] — the entry points to the crate.
2//!
3//! Construct an analyzer by chaining builder methods and calling
4//! [`AnalyzerBuilder::build`]. Push samples with
5//! [`Analyzer::push_planar`] or [`Analyzer::push_interleaved`]. Read
6//! intermediate values via [`Analyzer::snapshot`] or finalize with
7//! [`Analyzer::finalize`].
8
9#[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
23/// Maximum number of channels supported in a single layout.
24///
25/// Covers every standard audio format up to and including
26/// 22.2 immersive surround. Bumped from 8 in v0.2.
27pub(crate) const MAX_CHANNELS: usize = 24;
28
29/// Acceptable sample rates (Hz). Other rates are rejected at `build()`.
30///
31/// Includes telephony / streaming-codec rates (22.05, 32 kHz) in
32/// addition to the broadcast / production rates from BS.1770-4.
33const SUPPORTED_RATES: &[u32] = &[22_050, 32_000, 44_100, 48_000, 88_200, 96_000, 192_000];
34
35/// The K-weighting calibration offset, in dB, per ITU-R BS.1770-4 §4.1.
36pub(crate) const LUFS_OFFSET: f64 = -0.691;
37
38/// Absolute gate threshold for integrated loudness, per BS.1770-4 §5.6.
39#[cfg(feature = "alloc")]
40pub(crate) const ABSOLUTE_GATE_LUFS: f64 = -70.0;
41
42/// Relative gate offset for integrated loudness, per BS.1770-4 §5.6.
43#[cfg(feature = "alloc")]
44pub(crate) const RELATIVE_GATE_OFFSET_LU: f64 = -10.0;
45
46/// Relative gate offset for LRA, per EBU Tech 3342.
47#[cfg(feature = "alloc")]
48pub(crate) const LRA_RELATIVE_GATE_OFFSET_LU: f64 = -20.0;
49
50/// Builder for [`Analyzer`].
51///
52/// # Example
53///
54/// ```
55/// use ebur128_stream::{AnalyzerBuilder, Channel, Mode};
56/// let analyzer = AnalyzerBuilder::new()
57///     .sample_rate(48_000)
58///     .channels(&[Channel::Left, Channel::Right])
59///     .modes(Mode::Integrated | Mode::Momentary)
60///     .build()
61///     .unwrap();
62/// # let _ = analyzer;
63/// ```
64#[derive(Debug, Clone)]
65pub struct AnalyzerBuilder {
66    sample_rate: u32,
67    channels: [Channel; MAX_CHANNELS],
68    n_channels: u8,
69    /// `true` if the supplied layout exceeded `MAX_CHANNELS` and the
70    /// builder should fail at build-time.
71    overflow: bool,
72    modes: Mode,
73    /// Discriminates "user never called .modes()" from
74    /// "user passed Mode::empty()".
75    modes_set: bool,
76    channels_set: bool,
77    /// Optional programme-length hint, in seconds. Used to pre-reserve
78    /// the integrated/LRA programme buffers so steady-state pushes
79    /// don't trigger Vec growth.
80    expected_duration_secs: Option<f64>,
81}
82
83impl Default for AnalyzerBuilder {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89impl AnalyzerBuilder {
90    /// Start a fresh builder with no configuration.
91    #[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    /// Hint at the expected programme length. The builder uses this to
106    /// pre-reserve internal buffers so steady-state `push_*` calls
107    /// trigger no allocations even with [`Mode::Integrated`] /
108    /// [`Mode::Lra`] enabled.
109    ///
110    /// Over-estimating wastes a few KB; under-estimating just causes
111    /// the buffer to grow as normal. This is a hint, not a hard cap.
112    ///
113    /// # Example
114    ///
115    /// ```
116    /// # use ebur128_stream::{AnalyzerBuilder, Channel, Mode};
117    /// # use core::time::Duration;
118    /// let analyzer = AnalyzerBuilder::new()
119    ///     .sample_rate(48_000)
120    ///     .channels(&[Channel::Left, Channel::Right])
121    ///     .modes(Mode::Integrated | Mode::Lra)
122    ///     .expected_duration(Duration::from_secs(60 * 90))   // 90 minutes
123    ///     .build()?;
124    /// # let _ = analyzer;
125    /// # Ok::<(), ebur128_stream::Error>(())
126    /// ```
127    #[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    /// Set the input sample rate, in hertz. Must be one of
134    /// 22 050, 32 000, 44 100, 48 000, 88 200, 96 000, 192 000.
135    #[must_use]
136    pub fn sample_rate(mut self, hz: u32) -> Self {
137        self.sample_rate = hz;
138        self
139    }
140
141    /// Set the channel layout. The order must match the order of channel
142    /// slices passed to [`Analyzer::push_planar`] or the interleave
143    /// order passed to [`Analyzer::push_interleaved`].
144    #[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    /// Set the analysis modes. At least one mode must be selected.
159    #[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    /// Validate configuration and produce an [`Analyzer`].
167    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/// Streaming EBU R128 loudness analyzer.
214///
215/// See the [crate-level documentation](crate) for an overview of the
216/// API. Build with [`AnalyzerBuilder`].
217#[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    /// Per-channel K-weighting filter state.
225    filters: [KFilter; MAX_CHANNELS],
226    /// Per-channel running sum of squared K-weighted samples within the
227    /// current 100 ms block.
228    block_acc: BlockAccumulator,
229    /// Sliding ring buffer of the last `MOMENTARY_BLOCKS` weighted-MS sums.
230    momentary_ring: [f32; MOMENTARY_BLOCKS],
231    momentary_filled: usize,
232    momentary_idx: usize,
233    /// Sliding ring buffer of the last `SHORT_TERM_BLOCKS` weighted-MS sums.
234    short_term_ring: [f32; SHORT_TERM_BLOCKS],
235    short_term_filled: usize,
236    short_term_idx: usize,
237    /// Programme buffer of per-gating-block weighted-MS values, used by
238    /// integrated loudness.
239    #[cfg(feature = "alloc")]
240    programme_blocks: Vec<f32>,
241    /// Per-100ms short-term-window MS samples, used by LRA percentile
242    /// computation.
243    #[cfg(feature = "alloc")]
244    short_term_samples: Vec<f32>,
245    /// Per-channel true-peak oversampling state.
246    #[cfg(feature = "alloc")]
247    true_peak: Option<TruePeakState>,
248    /// Programme-wide max momentary LUFS observed so far.
249    momentary_max: Option<f64>,
250    /// Programme-wide max short-term LUFS observed so far.
251    short_term_max: Option<f64>,
252    /// Total samples ingested per channel — used for programme duration.
253    samples_ingested: u64,
254    /// Cached snapshot — invalidated on each `push_*` call.
255    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        // Build per-channel filters. KFilter has the configured
270        // sample-rate-specific biquad coefficients baked in.
271        // KFilter is Copy so we can populate the fixed-size array via
272        // a single template, regardless of MAX_CHANNELS.
273        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        // Pre-reserve programme buffers from the expected_duration hint.
281        // 10 blocks/s for both buffers; cap to a sane upper bound to
282        // protect against malicious huge hints.
283        #[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    /// Configured sample rate, in hertz.
328    #[inline]
329    #[must_use]
330    pub fn sample_rate(&self) -> u32 {
331        self.sample_rate
332    }
333
334    /// Configured channel layout.
335    #[inline]
336    #[must_use]
337    pub fn channels(&self) -> &[Channel] {
338        &self.channels[..self.n_channels]
339    }
340
341    /// Configured modes.
342    #[inline]
343    #[must_use]
344    pub fn modes(&self) -> Mode {
345        self.modes
346    }
347
348    /// Number of samples in one 100 ms block at the configured rate.
349    #[inline]
350    #[must_use]
351    pub fn samples_per_block(&self) -> u32 {
352        self.samples_per_block
353    }
354
355    /// Push samples laid out as one slice per channel, in the same order
356    /// as the configured channel layout.
357    ///
358    /// All slices must have the same length.
359    ///
360    /// # Errors
361    ///
362    /// - [`Error::ChannelMismatch`] if `channels.len()` differs from the
363    ///   configured layout length.
364    /// - [`Error::PlanarLengthMismatch`] if slices have different lengths.
365    /// - [`Error::NonFiniteSample`] if any sample is `NaN` or infinity.
366    ///
367    /// # Example
368    ///
369    /// ```
370    /// use ebur128_stream::{AnalyzerBuilder, Channel, Mode};
371    ///
372    /// let mut analyzer = AnalyzerBuilder::new()
373    ///     .sample_rate(48_000)
374    ///     .channels(&[Channel::Left, Channel::Right])
375    ///     .modes(Mode::Momentary)
376    ///     .build()?;
377    ///
378    /// let left  = vec![0.0_f32; 9_600]; // 100 ms
379    /// let right = vec![0.0_f32; 9_600];
380    /// analyzer.push_planar::<f32>(&[&left, &right])?;
381    /// # Ok::<(), ebur128_stream::Error>(())
382    /// ```
383    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    /// Push samples laid out as interleaved frames
420    /// (`[L0, R0, L1, R1, ...]`).
421    ///
422    /// `samples.len()` must be a whole multiple of the channel count.
423    ///
424    /// # Errors
425    ///
426    /// - [`Error::InterleavedLengthNotMultiple`] if the buffer length
427    ///   isn't divisible by the channel count.
428    /// - [`Error::NonFiniteSample`] if any sample is `NaN` or infinity.
429    ///
430    /// # Example
431    ///
432    /// ```
433    /// use ebur128_stream::{AnalyzerBuilder, Channel, Mode};
434    ///
435    /// let mut analyzer = AnalyzerBuilder::new()
436    ///     .sample_rate(48_000)
437    ///     .channels(&[Channel::Left, Channel::Right])
438    ///     .modes(Mode::Momentary)
439    ///     .build()?;
440    ///
441    /// // 100 ms of stereo silence (9 600 frames × 2 channels).
442    /// let stereo = vec![0.0_f32; 9_600 * 2];
443    /// analyzer.push_interleaved::<f32>(&stereo)?;
444    /// # Ok::<(), ebur128_stream::Error>(())
445    /// ```
446    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    /// Process a single multi-channel frame.
475    #[inline]
476    fn process_frame(&mut self, frame: &[f32]) {
477        // 1) feed true-peak FIR with raw (unfiltered) samples
478        #[cfg(feature = "alloc")]
479        if let Some(tp) = self.true_peak.as_mut() {
480            tp.feed_frame(frame);
481        }
482        // 2) K-weight each channel
483        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        // 3) accumulate squared values into the 100 ms block
488        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    /// Called whenever the 100 ms block aggregator fires.
496    fn on_block_emitted(&mut self, per_channel_ms: &[f32]) {
497        // Channel-weighted sum: Σ_ch (w_ch * MS_ch).
498        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        // ---- Momentary (4 blocks = 400 ms) ----
504        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            // Programme buffer captures the same 400 ms gating block.
515            #[cfg(feature = "alloc")]
516            if self.modes.contains(Mode::Integrated) {
517                self.programme_blocks.push(mean);
518            }
519        }
520
521        // ---- Short-term (30 blocks = 3 s) ----
522        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    /// Take a snapshot of the current measurements.
540    ///
541    /// Cheap to call repeatedly; momentary and short-term values are
542    /// `O(1)`. Integrated and LRA values are `O(programme blocks)` but
543    /// cached between pushes.
544    ///
545    /// # Example
546    ///
547    /// ```
548    /// use ebur128_stream::{AnalyzerBuilder, Channel, Mode};
549    ///
550    /// let mut analyzer = AnalyzerBuilder::new()
551    ///     .sample_rate(48_000)
552    ///     .channels(&[Channel::Center])
553    ///     .modes(Mode::Momentary)
554    ///     .build()?;
555    ///
556    /// analyzer.push_interleaved::<f32>(&vec![0.1; 48_000])?;  // 1 s
557    /// let s = analyzer.snapshot();
558    /// // Momentary fills after 400 ms; with 1 s pushed it's available.
559    /// assert!(s.momentary_lufs().is_some());
560    /// # Ok::<(), ebur128_stream::Error>(())
561    /// ```
562    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    /// Consume the analyzer and return the final [`Report`].
622    ///
623    /// # Example
624    ///
625    /// ```
626    /// use ebur128_stream::{AnalyzerBuilder, Channel, Mode};
627    ///
628    /// let mut a = AnalyzerBuilder::new()
629    ///     .sample_rate(48_000)
630    ///     .channels(&[Channel::Center])
631    ///     .modes(Mode::Integrated)
632    ///     .build()?;
633    /// a.push_interleaved::<f32>(&vec![0.0; 48_000])?;
634    /// let report = a.finalize();
635    /// // Silent input clears no gating block — integrated is None.
636    /// assert!(report.integrated_lufs().is_none());
637    /// # Ok::<(), ebur128_stream::Error>(())
638    /// ```
639    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    /// Reset all internal state, retaining configuration.
652    ///
653    /// Use this when reusing one analyzer across multiple programmes
654    /// (e.g. between songs in a playback application).
655    ///
656    /// # Example
657    ///
658    /// ```
659    /// use ebur128_stream::{AnalyzerBuilder, Channel, Mode};
660    ///
661    /// let mut a = AnalyzerBuilder::new()
662    ///     .sample_rate(48_000)
663    ///     .channels(&[Channel::Center])
664    ///     .modes(Mode::Momentary)
665    ///     .build()?;
666    /// a.push_interleaved::<f32>(&vec![0.1; 48_000])?;
667    /// a.reset();
668    /// assert_eq!(a.snapshot().programme_duration_seconds(), 0.0);
669    /// # Ok::<(), ebur128_stream::Error>(())
670    /// ```
671    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/// Convert a mean-square value to LUFS via the BS.1770-4 calibration.
698/// Returns `None` for zero / negative MS (silence).
699#[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        // Above MAX_CHANNELS (24).
817        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        // 22.2: 24-channel layout (22 main + LFE1 + LFE2).
830        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]; // 100 ms × 24 channels
838        a.push_interleaved::<f32>(&buf).unwrap();
839        assert_eq!(a.channels().len(), 24);
840    }
841}