From 57709e9a488fb930f74c2e4fc5aa64424e560f40 Mon Sep 17 00:00:00 2001 From: David Holroyd Date: Tue, 16 Jun 2026 10:08:46 +0100 Subject: [PATCH] Skip biquad when no loudness mode is requested Mode::TRUE_PEAK and Mode::SAMPLE_PEAK previously included Mode::M's bit in their encoding, which forced EbuR128 to always run the BS.1770 biquad on every block even when the caller only wanted peak values and never read the filtered samples. Drop the M bit from both peak modes; allow peak-only construction without an audio_data buffer, and thread a calculate_loudness flag into Filter so the biquad loop, filter_state updates, and dest writes are skipped entirely when no loudness metric is requested. --- benches/ebur128.rs | 8 ++- benches/filter.rs | 17 ++--- src/ebur128.rs | 24 +++++-- src/filter.rs | 156 ++++++++++++++++++++++++--------------------- 4 files changed, 120 insertions(+), 85 deletions(-) diff --git a/benches/ebur128.rs b/benches/ebur128.rs index 2205c98..832fbd1 100644 --- a/benches/ebur128.rs +++ b/benches/ebur128.rs @@ -17,7 +17,9 @@ fn get_results_c(ebu: &EbuR128C, mode: ModeC) { if mode.contains(ModeC::S) { black_box(ebu.loudness_shortterm().unwrap()); } - black_box(ebu.loudness_window(1).unwrap()); + if mode.intersects(ModeC::M | ModeC::S | ModeC::I | ModeC::LRA) { + black_box(ebu.loudness_window(1).unwrap()); + } if mode.contains(ModeC::LRA) { black_box(ebu.loudness_range().unwrap()); @@ -52,7 +54,9 @@ fn get_results(ebu: &EbuR128, mode: Mode) { if mode.contains(Mode::S) { black_box(ebu.loudness_shortterm().unwrap()); } - black_box(ebu.loudness_window(1).unwrap()); + if mode.intersects(Mode::M | Mode::S | Mode::I | Mode::LRA) { + black_box(ebu.loudness_window(1).unwrap()); + } if mode.contains(Mode::LRA) { black_box(ebu.loudness_range().unwrap()); diff --git a/benches/filter.rs b/benches/filter.rs index d6bb78e..ee120a5 100644 --- a/benches/filter.rs +++ b/benches/filter.rs @@ -30,6 +30,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { black_box(2), black_box(false), black_box(false), + black_box(true), ); drop(black_box(f)); }) @@ -79,7 +80,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { } { - let mut f = filter::Filter::new(48_000, 2, *sample_peak, false); + let mut f = filter::Filter::new(48_000, 2, *sample_peak, false, true); group.bench_function("Rust/Interleaved", |b| { b.iter(|| { f.process( @@ -91,7 +92,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { }) }); - let mut f = filter::Filter::new(48_000, 2, *sample_peak, false); + let mut f = filter::Filter::new(48_000, 2, *sample_peak, false, true); group.bench_function("Rust/Planar", |b| { b.iter(|| { f.process( @@ -143,7 +144,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { } { - let mut f = filter::Filter::new(48_000, 2, *sample_peak, false); + let mut f = filter::Filter::new(48_000, 2, *sample_peak, false, true); group.bench_function("Rust/Interleaved", |b| { b.iter(|| { f.process( @@ -155,7 +156,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { }) }); - let mut f = filter::Filter::new(48_000, 2, *sample_peak, false); + let mut f = filter::Filter::new(48_000, 2, *sample_peak, false, true); group.bench_function("Rust/Planar", |b| { b.iter(|| { f.process( @@ -207,7 +208,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { } { - let mut f = filter::Filter::new(48_000, 2, *sample_peak, false); + let mut f = filter::Filter::new(48_000, 2, *sample_peak, false, true); group.bench_function("Rust/Interleaved", |b| { b.iter(|| { f.process( @@ -219,7 +220,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { }) }); - let mut f = filter::Filter::new(48_000, 2, *sample_peak, false); + let mut f = filter::Filter::new(48_000, 2, *sample_peak, false, true); group.bench_function("Rust/Planar", |b| { b.iter(|| { f.process( @@ -271,7 +272,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { } { - let mut f = filter::Filter::new(48_000, 2, *sample_peak, false); + let mut f = filter::Filter::new(48_000, 2, *sample_peak, false, true); group.bench_function("Rust/Interleaved", |b| { b.iter(|| { f.process( @@ -283,7 +284,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { }) }); - let mut f = filter::Filter::new(48_000, 2, *sample_peak, false); + let mut f = filter::Filter::new(48_000, 2, *sample_peak, false, true); group.bench_function("Rust/Planar", |b| { b.iter(|| { f.process( diff --git a/src/ebur128.rs b/src/ebur128.rs index ff7b88c..fd6fcd6 100644 --- a/src/ebur128.rs +++ b/src/ebur128.rs @@ -66,9 +66,9 @@ bitflags! { /// can call [`EbuR128::loudness_range`](struct.EbuR128.html#method.loudness_range) const LRA = 0b00001000 | Mode::S.bits; /// can call [`EbuR128::sample_peak`](struct.EbuR128.html#method.sample_peak) - const SAMPLE_PEAK = 0b00010000 | Mode::M.bits; + const SAMPLE_PEAK = 0b00010000; /// can call [`EbuR128::true_peak`](struct.EbuR128.html#method.true_peak) - const TRUE_PEAK = 0b00110001; + const TRUE_PEAK = 0b00110000; /// uses histogram algorithm to calculate loudness const HISTOGRAM = 0b01000000; } @@ -300,15 +300,22 @@ impl EbuR128 { let history = usize::MAX; let samples_in_100ms = (rate as usize + 5) / 10; + let calculate_loudness = mode.intersects(Mode::M | Mode::S | Mode::I | Mode::LRA); let window = if mode.contains(Mode::S) { 3000 } else if mode.contains(Mode::M) { 400 + } else if mode.intersects(Mode::SAMPLE_PEAK | Mode::TRUE_PEAK) { + 0 } else { return Err(Error::InvalidMode); }; - let audio_data = Self::allocate_audio_data(channels, rate, window)?; + let audio_data = if window > 0 { + Self::allocate_audio_data(channels, rate, window)? + } else { + Vec::new().into_boxed_slice() + }; // start at the beginning of the buffer let audio_data_index = 0; @@ -324,6 +331,7 @@ impl EbuR128 { channels, mode.contains(Mode::SAMPLE_PEAK), mode.contains(Mode::TRUE_PEAK), + calculate_loudness, ); let channel_map = default_channel_map(channels); @@ -448,7 +456,11 @@ impl EbuR128 { return Ok(()); } - self.audio_data = Self::allocate_audio_data(channels, rate, self.window)?; + self.audio_data = if self.window > 0 { + Self::allocate_audio_data(channels, rate, self.window)? + } else { + Vec::new().into_boxed_slice() + }; if self.channels != channels { self.channels = channels; @@ -462,11 +474,15 @@ impl EbuR128 { self.samples_in_100ms = (rate as usize + 5) / 10; } + let calculate_loudness = self + .mode + .intersects(Mode::M | Mode::S | Mode::I | Mode::LRA); self.filter = crate::filter::Filter::new( rate, channels, self.mode.contains(Mode::SAMPLE_PEAK), self.mode.contains(Mode::TRUE_PEAK), + calculate_loudness, ); // the first block needs 400ms of audio data diff --git a/src/filter.rs b/src/filter.rs index 86b9270..ecca4d9 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -34,6 +34,9 @@ pub struct Filter { /// One filter state per channel. filter_state: Box<[[f64; 5]]>, + /// Whether to run the K-weighting biquad and write filtered samples to `dest`. + /// Set false for peak-only modes where no loudness energy is ever read. + calculate_loudness: bool, /// Whether to measure sample peak. calculate_sample_peak: bool, /// Previously measured sample peak. @@ -115,6 +118,7 @@ impl Filter { channels: u32, calculate_sample_peak: bool, calculate_true_peak: bool, + calculate_loudness: bool, ) -> Self { assert!(rate > 0); assert!(channels > 0); @@ -132,6 +136,7 @@ impl Filter { b, a, filter_state: vec![[0.0; 5]; channels as usize].into_boxed_slice(), + calculate_loudness, calculate_sample_peak, sample_peak: vec![0.0; channels as usize].into_boxed_slice(), tp, @@ -169,7 +174,6 @@ impl Filter { dest_index: usize, channel_map: &[crate::ebur128::Channel], ) { - assert!(dest.len() % self.channels as usize == 0); assert!(channel_map.len() == self.channels as usize); assert!(src.channels() == self.channels as usize); assert!(self.filter_state.len() == self.channels as usize); @@ -197,48 +201,52 @@ impl Filter { } } - let dest_stride = dest.len() / self.channels as usize; - assert!(dest_index + src.frames() <= dest_stride); + if self.calculate_loudness { + assert!(dest.len() % self.channels as usize == 0); + let dest_stride = dest.len() / self.channels as usize; + assert!(dest_index + src.frames() <= dest_stride); - for (c, (channel_map, dest)) in - Iterator::zip(channel_map.iter(), dest.chunks_exact_mut(dest_stride)).enumerate() - { - if *channel_map == crate::ebur128::Channel::Unused { - continue; - } + for (c, (channel_map, dest)) in + Iterator::zip(channel_map.iter(), dest.chunks_exact_mut(dest_stride)) + .enumerate() + { + if *channel_map == crate::ebur128::Channel::Unused { + continue; + } + + assert!(c < src.channels()); + + let Filter { + ref mut filter_state, + ref a, + ref b, + .. + } = *self; + let filter_state = &mut filter_state[c]; + + src.foreach_sample_zipped(c, dest[dest_index..].iter_mut(), |src, dest| { + filter_state[0] = (*src).to_sample::() + - a[1] * filter_state[1] + - a[2] * filter_state[2] + - a[3] * filter_state[3] + - a[4] * filter_state[4]; + *dest = b[0] * filter_state[0] + + b[1] * filter_state[1] + + b[2] * filter_state[2] + + b[3] * filter_state[3] + + b[4] * filter_state[4]; + + filter_state[4] = filter_state[3]; + filter_state[3] = filter_state[2]; + filter_state[2] = filter_state[1]; + filter_state[1] = filter_state[0]; + }); - assert!(c < src.channels()); - - let Filter { - ref mut filter_state, - ref a, - ref b, - .. - } = *self; - let filter_state = &mut filter_state[c]; - - src.foreach_sample_zipped(c, dest[dest_index..].iter_mut(), |src, dest| { - filter_state[0] = (*src).to_sample::() - - a[1] * filter_state[1] - - a[2] * filter_state[2] - - a[3] * filter_state[3] - - a[4] * filter_state[4]; - *dest = b[0] * filter_state[0] - + b[1] * filter_state[1] - + b[2] * filter_state[2] - + b[3] * filter_state[3] - + b[4] * filter_state[4]; - - filter_state[4] = filter_state[3]; - filter_state[3] = filter_state[2]; - filter_state[2] = filter_state[1]; - filter_state[1] = filter_state[0]; - }); - - if ftz.is_none() { - for v in filter_state { - if v.abs() < f64::EPSILON { - *v = 0.0; + if ftz.is_none() { + for v in filter_state { + if v.abs() < f64::EPSILON { + *v = 0.0; + } } } } @@ -261,37 +269,39 @@ impl Filter { assert!(self.filter_state.len() == self.channels as usize); ftz::with_ftz(|ftz| { - for (c, channel_map) in channel_map.iter().enumerate() { - if *channel_map == crate::ebur128::Channel::Unused { - continue; - } + if self.calculate_loudness { + for (c, channel_map) in channel_map.iter().enumerate() { + if *channel_map == crate::ebur128::Channel::Unused { + continue; + } + + assert!(c < src.channels()); + + let Filter { + ref mut filter_state, + ref a, + .. + } = *self; + let filter_state = &mut filter_state[c]; + + src.foreach_sample(c, |src| { + filter_state[0] = (*src).to_sample::() + - a[1] * filter_state[1] + - a[2] * filter_state[2] + - a[3] * filter_state[3] + - a[4] * filter_state[4]; + + filter_state[4] = filter_state[3]; + filter_state[3] = filter_state[2]; + filter_state[2] = filter_state[1]; + filter_state[1] = filter_state[0]; + }); - assert!(c < src.channels()); - - let Filter { - ref mut filter_state, - ref a, - .. - } = *self; - let filter_state = &mut filter_state[c]; - - src.foreach_sample(c, |src| { - filter_state[0] = (*src).to_sample::() - - a[1] * filter_state[1] - - a[2] * filter_state[2] - - a[3] * filter_state[3] - - a[4] * filter_state[4]; - - filter_state[4] = filter_state[3]; - filter_state[3] = filter_state[2]; - filter_state[2] = filter_state[1]; - filter_state[1] = filter_state[0]; - }); - - if ftz.is_none() { - for v in filter_state { - if v.abs() < f64::EPSILON { - *v = 0.0; + if ftz.is_none() { + for v in filter_state { + if v.abs() < f64::EPSILON { + *v = 0.0; + } } } } @@ -559,6 +569,7 @@ mod tests { signal.channels, calculate_sample_peak, calculate_true_peak, + true, ); let mut data_out_tmp = vec![0.0f64; frames * signal.channels as usize]; @@ -647,6 +658,7 @@ mod tests { signal.channels, calculate_sample_peak, calculate_true_peak, + true, ); let mut data_out_tmp = vec![0.0f64; frames * signal.channels as usize]; @@ -735,6 +747,7 @@ mod tests { signal.channels, calculate_sample_peak, calculate_true_peak, + true, ); let mut data_out_tmp = vec![0.0f64; frames * signal.channels as usize]; @@ -823,6 +836,7 @@ mod tests { signal.channels, calculate_sample_peak, calculate_true_peak, + true, ); let mut data_out_tmp = vec![0.0f64; frames * signal.channels as usize];