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}