Skip to main content

candle_examples/
bs1770.rs

1// Copied from https://github.com/ruuda/bs1770/blob/master/src/lib.rs
2// BS1770 -- Loudness analysis library conforming to ITU-R BS.1770
3// Copyright 2020 Ruud van Asseldonk
4
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// A copy of the License has been included in the root of the repository.
8
9//! Loudness analysis conforming to [ITU-R BS.1770-4][bs17704].
10//!
11//! This library offers the building blocks to perform BS.1770 loudness
12//! measurements, but you need to put the pieces together yourself.
13//!
14//! [bs17704]: https://www.itu.int/rec/R-REC-BS.1770-4-201510-I/en
15//!
16//! # Stereo integrated loudness example
17//!
18//! ```ignore
19//! # fn load_stereo_audio() -> [Vec<i16>; 2] {
20//! #     [vec![0; 48_000], vec![0; 48_000]]
21//! # }
22//! #
23//! let sample_rate_hz = 44_100;
24//! let bits_per_sample = 16;
25//! let channel_samples: [Vec<i16>; 2] = load_stereo_audio();
26//!
27//! // When converting integer samples to float, note that the maximum amplitude
28//! // is `1 << (bits_per_sample - 1)`, one bit is the sign bit.
29//! let normalizer = 1.0 / (1_u64 << (bits_per_sample - 1)) as f32;
30//!
31//! let channel_power: Vec<_> = channel_samples.iter().map(|samples| {
32//!     let mut meter = bs1770::ChannelLoudnessMeter::new(sample_rate_hz);
33//!     meter.push(samples.iter().map(|&s| s as f32 * normalizer));
34//!     meter.into_100ms_windows()
35//! }).collect();
36//!
37//! let stereo_power = bs1770::reduce_stereo(
38//!     channel_power[0].as_ref(),
39//!     channel_power[1].as_ref(),
40//! );
41//!
42//! let gated_power = bs1770::gated_mean(
43//!     stereo_power.as_ref()
44//! ).unwrap_or(bs1770::Power(0.0));
45//! println!("Integrated loudness: {:.1} LUFS", gated_power.loudness_lkfs());
46//! ```
47
48use std::f32;
49
50/// Coefficients for a 2nd-degree infinite impulse response filter.
51///
52/// Coefficient a0 is implicitly 1.0.
53#[derive(Clone)]
54struct Filter {
55    a1: f32,
56    a2: f32,
57    b0: f32,
58    b1: f32,
59    b2: f32,
60
61    // The past two input and output samples.
62    x1: f32,
63    x2: f32,
64    y1: f32,
65    y2: f32,
66}
67
68impl Filter {
69    /// Stage 1 of th BS.1770-4 pre-filter.
70    pub fn high_shelf(sample_rate_hz: f32) -> Filter {
71        // Coefficients taken from https://github.com/csteinmetz1/pyloudnorm/blob/
72        // 6baa64d59b7794bc812e124438692e7fd2e65c0c/pyloudnorm/meter.py#L135-L136.
73        let gain_db = 3.999_843_8;
74        let q = 0.707_175_25;
75        let center_hz = 1_681.974_5;
76
77        // Formula taken from https://github.com/csteinmetz1/pyloudnorm/blob/
78        // 6baa64d59b7794bc812e124438692e7fd2e65c0c/pyloudnorm/iirfilter.py#L134-L143.
79        let k = (f32::consts::PI * center_hz / sample_rate_hz).tan();
80        let vh = 10.0_f32.powf(gain_db / 20.0);
81        let vb = vh.powf(0.499_666_78);
82        let a0 = 1.0 + k / q + k * k;
83        Filter {
84            b0: (vh + vb * k / q + k * k) / a0,
85            b1: 2.0 * (k * k - vh) / a0,
86            b2: (vh - vb * k / q + k * k) / a0,
87            a1: 2.0 * (k * k - 1.0) / a0,
88            a2: (1.0 - k / q + k * k) / a0,
89
90            x1: 0.0,
91            x2: 0.0,
92            y1: 0.0,
93            y2: 0.0,
94        }
95    }
96
97    /// Stage 2 of th BS.1770-4 pre-filter.
98    pub fn high_pass(sample_rate_hz: f32) -> Filter {
99        // Coefficients taken from https://github.com/csteinmetz1/pyloudnorm/blob/
100        // 6baa64d59b7794bc812e124438692e7fd2e65c0c/pyloudnorm/meter.py#L135-L136.
101        let q = 0.500_327_05;
102        let center_hz = 38.135_47;
103
104        // Formula taken from https://github.com/csteinmetz1/pyloudnorm/blob/
105        // 6baa64d59b7794bc812e124438692e7fd2e65c0c/pyloudnorm/iirfilter.py#L145-L151
106        let k = (f32::consts::PI * center_hz / sample_rate_hz).tan();
107        Filter {
108            a1: 2.0 * (k * k - 1.0) / (1.0 + k / q + k * k),
109            a2: (1.0 - k / q + k * k) / (1.0 + k / q + k * k),
110            b0: 1.0,
111            b1: -2.0,
112            b2: 1.0,
113
114            x1: 0.0,
115            x2: 0.0,
116            y1: 0.0,
117            y2: 0.0,
118        }
119    }
120
121    /// Feed the next input sample, get the next output sample.
122    #[inline(always)]
123    pub fn apply(&mut self, x0: f32) -> f32 {
124        let y0 = 0.0 + self.b0 * x0 + self.b1 * self.x1 + self.b2 * self.x2
125            - self.a1 * self.y1
126            - self.a2 * self.y2;
127
128        self.x2 = self.x1;
129        self.x1 = x0;
130        self.y2 = self.y1;
131        self.y1 = y0;
132
133        y0
134    }
135}
136
137/// Compensated sum, for summing many values of different orders of magnitude
138/// accurately.
139#[derive(Copy, Clone, PartialEq)]
140struct Sum {
141    sum: f32,
142    residue: f32,
143}
144
145impl Sum {
146    #[inline(always)]
147    fn zero() -> Sum {
148        Sum {
149            sum: 0.0,
150            residue: 0.0,
151        }
152    }
153
154    #[inline(always)]
155    fn add(&mut self, x: f32) {
156        let sum = self.sum + (self.residue + x);
157        self.residue = (self.residue + x) - (sum - self.sum);
158        self.sum = sum;
159    }
160}
161
162/// The mean of the squares of the K-weighted samples in a window of time.
163///
164/// K-weighted power is equivalent to K-weighted loudness, the only difference
165/// is one of scale: power is quadratic in sample amplitudes, whereas loudness
166/// units are logarithmic. `loudness_lkfs` and `from_lkfs` convert between power,
167/// and K-weighted Loudness Units relative to nominal Full Scale (LKFS).
168///
169/// The term “LKFS” (Loudness Units, K-Weighted, relative to nominal Full Scale)
170/// is used in BS.1770-4 to emphasize K-weighting, but the term is otherwise
171/// interchangeable with the more widespread term “LUFS” (Loudness Units,
172/// relative to Full Scale). Loudness units are related to decibels in the
173/// following sense: boosting a signal that has a loudness of
174/// -<var>L<sub>K</sub></var> LUFS by <var>L<sub>K</sub></var> dB (by
175/// multiplying the amplitude by 10<sup><var>L<sub>K</sub></var>/20</sup>) will
176/// bring the loudness to 0 LUFS.
177///
178/// K-weighting refers to a high-shelf and high-pass filter that model the
179/// effect that humans perceive a certain amount of power in low frequencies to
180/// be less loud than the same amount of power in higher frequencies. In this
181/// library the `Power` type is used exclusively to refer to power after applying K-weighting.
182///
183/// The nominal “full scale” is the range [-1.0, 1.0]. Because the power is the
184/// mean square of the samples, if no input samples exceeded the full scale, the
185/// power will be in the range [0.0, 1.0]. However, the power delivered by
186/// multiple channels, which is a weighted sum over individual channel powers,
187/// can exceed this range, because the weighted sum is not normalized.
188#[derive(Copy, Clone, PartialEq, PartialOrd)]
189pub struct Power(pub f32);
190
191impl Power {
192    /// Convert Loudness Units relative to Full Scale into a squared sample amplitude.
193    ///
194    /// This is the inverse of `loudness_lkfs`.
195    pub fn from_lkfs(lkfs: f32) -> Power {
196        // The inverse of the formula below.
197        Power(10.0_f32.powf((lkfs + 0.691) * 0.1))
198    }
199
200    /// Return the loudness of this window in Loudness Units, K-weighted, relative to Full Scale.
201    ///
202    /// This is the inverse of `from_lkfs`.
203    pub fn loudness_lkfs(&self) -> f32 {
204        // Equation 2 (p.5) of BS.1770-4.
205        -0.691 + 10.0 * self.0.log10()
206    }
207}
208
209/// A `T` value for non-overlapping windows of audio, 100ms in length.
210///
211/// The `ChannelLoudnessMeter` applies K-weighting and then produces the power
212/// for non-overlapping windows of 100ms duration.
213///
214/// These non-overlapping 100ms windows can later be combined into overlapping
215/// windows of 400ms, spaced 100ms apart, to compute instantaneous loudness or
216/// to perform a gated measurement, or they can be combined into even larger
217/// windows for a momentary loudness measurement.
218#[derive(Copy, Clone, Debug)]
219pub struct Windows100ms<T> {
220    pub inner: T,
221}
222
223impl<T> Windows100ms<T> {
224    /// Wrap a new empty vector.
225    pub fn new() -> Windows100ms<Vec<T>> {
226        Windows100ms { inner: Vec::new() }
227    }
228
229    /// Apply `as_ref` to the inner value.
230    pub fn as_ref(&self) -> Windows100ms<&[Power]>
231    where
232        T: AsRef<[Power]>,
233    {
234        Windows100ms {
235            inner: self.inner.as_ref(),
236        }
237    }
238
239    /// Apply `as_mut` to the inner value.
240    pub fn as_mut(&mut self) -> Windows100ms<&mut [Power]>
241    where
242        T: AsMut<[Power]>,
243    {
244        Windows100ms {
245            inner: self.inner.as_mut(),
246        }
247    }
248
249    #[allow(clippy::len_without_is_empty)]
250    /// Apply `len` to the inner value.
251    pub fn len(&self) -> usize
252    where
253        T: AsRef<[Power]>,
254    {
255        self.inner.as_ref().len()
256    }
257}
258
259/// Measures K-weighted power of non-overlapping 100ms windows of a single channel of audio.
260///
261/// # Output
262///
263/// The output of the meter is an intermediate result in the form of power for
264/// 100ms non-overlapping windows. The windows need to be processed further to
265/// get one of the instantaneous, momentary, and integrated loudness
266/// measurements defined in BS.1770.
267///
268/// The windows can also be inspected directly; the data is meaningful
269/// on its own (the K-weighted power delivered in that window of time), but it
270/// is not something that BS.1770 defines a term for.
271///
272/// # Multichannel audio
273///
274/// To perform a loudness measurement of multichannel audio, construct a
275/// `ChannelLoudnessMeter` per channel, and later combine the measured power
276/// with e.g. `reduce_stereo`.
277///
278/// # Instantaneous loudness
279///
280/// The instantaneous loudness is the power over a 400ms window, so you can
281/// average four 100ms windows. No special functionality is implemented to help
282/// with that at this time. ([Pull requests would be accepted.][contribute])
283///
284/// # Momentary loudness
285///
286/// The momentary loudness is the power over a 3-second window, so you can
287/// average thirty 100ms windows. No special functionality is implemented to
288/// help with that at this time. ([Pull requests would be accepted.][contribute])
289///
290/// # Integrated loudness
291///
292/// Use `gated_mean` to perform an integrated loudness measurement:
293///
294/// ```ignore
295/// # use std::iter;
296/// # use bs1770::{ChannelLoudnessMeter, gated_mean};
297/// # let sample_rate_hz = 44_100;
298/// # let samples_per_100ms = sample_rate_hz / 10;
299/// # let mut meter = ChannelLoudnessMeter::new(sample_rate_hz);
300/// # meter.push((0..44_100).map(|i| (i as f32 * 0.01).sin()));
301/// let integrated_loudness_lkfs = gated_mean(meter.as_100ms_windows())
302///     .unwrap_or(bs1770::Power(0.0))
303///     .loudness_lkfs();
304/// ```
305///
306/// [contribute]: https://github.com/ruuda/bs1770/blob/master/CONTRIBUTING.md
307#[derive(Clone)]
308pub struct ChannelLoudnessMeter {
309    /// The number of samples that fit in 100ms of audio.
310    samples_per_100ms: u32,
311
312    /// Stage 1 filter (head effects, high shelf).
313    filter_stage1: Filter,
314
315    /// Stage 2 filter (high-pass).
316    filter_stage2: Filter,
317
318    /// Sum of the squares over non-overlapping windows of 100ms.
319    windows: Windows100ms<Vec<Power>>,
320
321    /// The number of samples in the current unfinished window.
322    count: u32,
323
324    /// The sum of the squares of the samples in the current unfinished window.
325    square_sum: Sum,
326}
327
328impl ChannelLoudnessMeter {
329    /// Construct a new loudness meter for the given sample rate.
330    pub fn new(sample_rate_hz: u32) -> ChannelLoudnessMeter {
331        ChannelLoudnessMeter {
332            samples_per_100ms: sample_rate_hz / 10,
333            filter_stage1: Filter::high_shelf(sample_rate_hz as f32),
334            filter_stage2: Filter::high_pass(sample_rate_hz as f32),
335            windows: Windows100ms::new(),
336            count: 0,
337            square_sum: Sum::zero(),
338        }
339    }
340
341    /// Feed input samples for loudness analysis.
342    ///
343    /// # Full scale
344    ///
345    /// Full scale for the input samples is the interval [-1.0, 1.0]. If your
346    /// input consists of signed integer samples, you can convert as follows:
347    ///
348    /// ```ignore
349    /// # let mut meter = bs1770::ChannelLoudnessMeter::new(44_100);
350    /// # let bits_per_sample = 16_usize;
351    /// # let samples = &[0_i16];
352    /// // Note that the maximum amplitude is `1 << (bits_per_sample - 1)`,
353    /// // one bit is the sign bit.
354    /// let normalizer = 1.0 / (1_u64 << (bits_per_sample - 1)) as f32;
355    /// meter.push(samples.iter().map(|&s| s as f32 * normalizer));
356    /// ```
357    ///
358    /// # Repeated calls
359    ///
360    /// You can call `push` multiple times to feed multiple batches of samples.
361    /// This is equivalent to feeding a single chained iterator. The leftover of
362    /// samples that did not fill a full 100ms window is not discarded:
363    ///
364    /// ```ignore
365    /// # use std::iter;
366    /// # use bs1770::ChannelLoudnessMeter;
367    /// let sample_rate_hz = 44_100;
368    /// let samples_per_100ms = sample_rate_hz / 10;
369    /// let mut meter = ChannelLoudnessMeter::new(sample_rate_hz);
370    ///
371    /// meter.push(iter::repeat(0.0).take(samples_per_100ms as usize - 1));
372    /// assert_eq!(meter.as_100ms_windows().len(), 0);
373    ///
374    /// meter.push(iter::once(0.0));
375    /// assert_eq!(meter.as_100ms_windows().len(), 1);
376    /// ```
377    pub fn push<I: Iterator<Item = f32>>(&mut self, samples: I) {
378        let normalizer = 1.0 / self.samples_per_100ms as f32;
379
380        // LLVM, if you could go ahead and inline those apply calls, and then
381        // unroll and vectorize the loop, that'd be terrific.
382        for x in samples {
383            let y = self.filter_stage1.apply(x);
384            let z = self.filter_stage2.apply(y);
385
386            self.square_sum.add(z * z);
387            self.count += 1;
388
389            // TODO: Should this branch be marked cold?
390            if self.count == self.samples_per_100ms {
391                let mean_squares = Power(self.square_sum.sum * normalizer);
392                self.windows.inner.push(mean_squares);
393                // We intentionally do not reset the residue. That way, leftover
394                // energy from this window is not lost, so for the file overall,
395                // the sum remains more accurate.
396                self.square_sum.sum = 0.0;
397                self.count = 0;
398            }
399        }
400    }
401
402    /// Return a reference to the 100ms windows analyzed so far.
403    pub fn as_100ms_windows(&self) -> Windows100ms<&[Power]> {
404        self.windows.as_ref()
405    }
406
407    /// Return all 100ms windows analyzed so far.
408    pub fn into_100ms_windows(self) -> Windows100ms<Vec<Power>> {
409        self.windows
410    }
411}
412
413/// Combine power for multiple channels by taking a weighted sum.
414///
415/// Note that BS.1770-4 defines power for a multi-channel signal as a weighted
416/// sum over channels which is not normalized. This means that a stereo signal
417/// is inherently louder than a mono signal. For a mono signal played back on
418/// stereo speakers, you should therefore still apply `reduce_stereo`, passing
419/// in the same signal for both channels.
420pub fn reduce_stereo(
421    left: Windows100ms<&[Power]>,
422    right: Windows100ms<&[Power]>,
423) -> Windows100ms<Vec<Power>> {
424    assert_eq!(
425        left.len(),
426        right.len(),
427        "Channels must have the same length."
428    );
429    let mut result = Vec::with_capacity(left.len());
430    for (l, r) in left.inner.iter().zip(right.inner) {
431        result.push(Power(l.0 + r.0));
432    }
433    Windows100ms { inner: result }
434}
435
436/// In-place version of `reduce_stereo` that stores the result in the former left channel.
437pub fn reduce_stereo_in_place(left: Windows100ms<&mut [Power]>, right: Windows100ms<&[Power]>) {
438    assert_eq!(
439        left.len(),
440        right.len(),
441        "Channels must have the same length."
442    );
443    for (l, r) in left.inner.iter_mut().zip(right.inner) {
444        l.0 += r.0;
445    }
446}
447
448/// Perform gating and averaging for a BS.1770-4 integrated loudness measurement.
449///
450/// The integrated loudness measurement is not just the average power over the
451/// entire signal. BS.1770-4 defines two stages of gating that exclude
452/// parts of the signal, to ensure that silent parts do not contribute to the
453/// loudness measurement. This function performs that gating, and returns the
454/// average power over the windows that were not excluded.
455///
456/// The result of this function is the integrated loudness measurement.
457///
458/// When no signal remains after applying the gate, this function returns
459/// `None`. In particular, this happens when all of the signal is softer than
460/// -70 LKFS, including a signal that consists of pure silence.
461pub fn gated_mean(windows_100ms: Windows100ms<&[Power]>) -> Option<Power> {
462    let mut gating_blocks = Vec::with_capacity(windows_100ms.len());
463
464    // Stage 1: an absolute threshold of -70 LKFS. (Equation 6, p.6.)
465    let absolute_threshold = Power::from_lkfs(-70.0);
466
467    // Iterate over all 400ms windows.
468    for window in windows_100ms.inner.windows(4) {
469        // Note that the sum over channels has already been performed at this point.
470        let gating_block_power = Power(0.25 * window.iter().map(|mean| mean.0).sum::<f32>());
471
472        if gating_block_power > absolute_threshold {
473            gating_blocks.push(gating_block_power);
474        }
475    }
476
477    if gating_blocks.is_empty() {
478        return None;
479    }
480
481    // Compute the loudness after applying the absolute gate, in order to
482    // determine the threshold for the relative gate.
483    let mut sum_power = Sum::zero();
484    for &gating_block_power in &gating_blocks {
485        sum_power.add(gating_block_power.0);
486    }
487    let absolute_gated_power = Power(sum_power.sum / (gating_blocks.len() as f32));
488
489    // Stage 2: Apply the relative gate.
490    let relative_threshold = Power::from_lkfs(absolute_gated_power.loudness_lkfs() - 10.0);
491    let mut sum_power = Sum::zero();
492    let mut n_blocks = 0_usize;
493    for &gating_block_power in &gating_blocks {
494        if gating_block_power > relative_threshold {
495            sum_power.add(gating_block_power.0);
496            n_blocks += 1;
497        }
498    }
499
500    if n_blocks == 0 {
501        return None;
502    }
503
504    let relative_gated_power = Power(sum_power.sum / n_blocks as f32);
505    Some(relative_gated_power)
506}