Skip to content

Commit 72af0d2

Browse files
authored
New dynamic limiter for very wide dynamic ranges (#935)
New dynamic limiter for very wide dynamic ranges
1 parent 1e54913 commit 72af0d2

4 files changed

Lines changed: 149 additions & 180 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- [main] Verbose logging mode (`-v`, `--verbose`) now logs all parsed environment variables and command line arguments (credentials are redacted).
1616
- [playback] `Sink`: `write()` now receives ownership of the packet (breaking).
1717
- [playback] `pipe`: create file if it doesn't already exist
18+
- [playback] More robust dynamic limiter for very wide dynamic range (breaking)
1819

1920
### Added
2021
- [cache] Add `disable-credential-cache` flag (breaking).

playback/src/config.rs

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
use super::player::db_to_ratio;
2-
use crate::convert::i24;
3-
pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer};
1+
use std::{mem, str::FromStr, time::Duration};
42

5-
use std::mem;
6-
use std::str::FromStr;
7-
use std::time::Duration;
3+
pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer};
4+
use crate::{convert::i24, player::duration_to_coefficient};
85

96
#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
107
pub enum Bitrate {
@@ -133,11 +130,11 @@ pub struct PlayerConfig {
133130
pub normalisation: bool,
134131
pub normalisation_type: NormalisationType,
135132
pub normalisation_method: NormalisationMethod,
136-
pub normalisation_pregain: f64,
137-
pub normalisation_threshold: f64,
138-
pub normalisation_attack: Duration,
139-
pub normalisation_release: Duration,
140-
pub normalisation_knee: f64,
133+
pub normalisation_pregain_db: f32,
134+
pub normalisation_threshold_dbfs: f64,
135+
pub normalisation_attack_cf: f64,
136+
pub normalisation_release_cf: f64,
137+
pub normalisation_knee_db: f64,
141138

142139
// pass function pointers so they can be lazily instantiated *after* spawning a thread
143140
// (thereby circumventing Send bounds that they might not satisfy)
@@ -152,11 +149,11 @@ impl Default for PlayerConfig {
152149
normalisation: false,
153150
normalisation_type: NormalisationType::default(),
154151
normalisation_method: NormalisationMethod::default(),
155-
normalisation_pregain: 0.0,
156-
normalisation_threshold: db_to_ratio(-2.0),
157-
normalisation_attack: Duration::from_millis(5),
158-
normalisation_release: Duration::from_millis(100),
159-
normalisation_knee: 1.0,
152+
normalisation_pregain_db: 0.0,
153+
normalisation_threshold_dbfs: -2.0,
154+
normalisation_attack_cf: duration_to_coefficient(Duration::from_millis(5)),
155+
normalisation_release_cf: duration_to_coefficient(Duration::from_millis(100)),
156+
normalisation_knee_db: 5.0,
160157
passthrough: false,
161158
ditherer: Some(mk_ditherer::<TriangularDitherer>),
162159
}

playback/src/player.rs

Lines changed: 91 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,8 @@ struct PlayerInternal {
6161
event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>,
6262
converter: Converter,
6363

64-
limiter_active: bool,
65-
limiter_attack_counter: u32,
66-
limiter_release_counter: u32,
67-
limiter_peak_sample: f64,
68-
limiter_factor: f64,
69-
limiter_strength: f64,
64+
normalisation_integrator: f64,
65+
normalisation_peak: f64,
7066

7167
auto_normalise_as_album: bool,
7268
}
@@ -208,6 +204,14 @@ pub fn ratio_to_db(ratio: f64) -> f64 {
208204
ratio.log10() * DB_VOLTAGE_RATIO
209205
}
210206

207+
pub fn duration_to_coefficient(duration: Duration) -> f64 {
208+
f64::exp(-1.0 / (duration.as_secs_f64() * SAMPLES_PER_SECOND as f64))
209+
}
210+
211+
pub fn coefficient_to_duration(coefficient: f64) -> Duration {
212+
Duration::from_secs_f64(-1.0 / f64::ln(coefficient) / SAMPLES_PER_SECOND as f64)
213+
}
214+
211215
#[derive(Clone, Copy, Debug)]
212216
pub struct NormalisationData {
213217
track_gain_db: f32,
@@ -241,17 +245,18 @@ impl NormalisationData {
241245
return 1.0;
242246
}
243247

244-
let [gain_db, gain_peak] = if config.normalisation_type == NormalisationType::Album {
245-
[data.album_gain_db, data.album_peak]
248+
let (gain_db, gain_peak) = if config.normalisation_type == NormalisationType::Album {
249+
(data.album_gain_db as f64, data.album_peak as f64)
246250
} else {
247-
[data.track_gain_db, data.track_peak]
251+
(data.track_gain_db as f64, data.track_peak as f64)
248252
};
249253

250-
let normalisation_power = gain_db as f64 + config.normalisation_pregain;
254+
let normalisation_power = gain_db + config.normalisation_pregain_db as f64;
251255
let mut normalisation_factor = db_to_ratio(normalisation_power);
252256

253-
if normalisation_factor * gain_peak as f64 > config.normalisation_threshold {
254-
let limited_normalisation_factor = config.normalisation_threshold / gain_peak as f64;
257+
if normalisation_power + ratio_to_db(gain_peak) > config.normalisation_threshold_dbfs {
258+
let limited_normalisation_factor =
259+
db_to_ratio(config.normalisation_threshold_dbfs as f64) / gain_peak;
255260
let limited_normalisation_power = ratio_to_db(limited_normalisation_factor);
256261

257262
if config.normalisation_method == NormalisationMethod::Basic {
@@ -295,18 +300,25 @@ impl Player {
295300
debug!("Normalisation Type: {:?}", config.normalisation_type);
296301
debug!(
297302
"Normalisation Pregain: {:.1} dB",
298-
config.normalisation_pregain
303+
config.normalisation_pregain_db
299304
);
300305
debug!(
301306
"Normalisation Threshold: {:.1} dBFS",
302-
ratio_to_db(config.normalisation_threshold)
307+
config.normalisation_threshold_dbfs
303308
);
304309
debug!("Normalisation Method: {:?}", config.normalisation_method);
305310

306311
if config.normalisation_method == NormalisationMethod::Dynamic {
307-
debug!("Normalisation Attack: {:?}", config.normalisation_attack);
308-
debug!("Normalisation Release: {:?}", config.normalisation_release);
309-
debug!("Normalisation Knee: {:?}", config.normalisation_knee);
312+
// as_millis() has rounding errors (truncates)
313+
debug!(
314+
"Normalisation Attack: {:.0} ms",
315+
coefficient_to_duration(config.normalisation_attack_cf).as_secs_f64() * 1000.
316+
);
317+
debug!(
318+
"Normalisation Release: {:.0} ms",
319+
coefficient_to_duration(config.normalisation_release_cf).as_secs_f64() * 1000.
320+
);
321+
debug!("Normalisation Knee: {} dB", config.normalisation_knee_db);
310322
}
311323
}
312324

@@ -329,12 +341,8 @@ impl Player {
329341
event_senders: [event_sender].to_vec(),
330342
converter,
331343

332-
limiter_active: false,
333-
limiter_attack_counter: 0,
334-
limiter_release_counter: 0,
335-
limiter_peak_sample: 0.0,
336-
limiter_factor: 1.0,
337-
limiter_strength: 0.0,
344+
normalisation_peak: 0.0,
345+
normalisation_integrator: 0.0,
338346

339347
auto_normalise_as_album: false,
340348
};
@@ -1275,110 +1283,82 @@ impl PlayerInternal {
12751283
Some(mut packet) => {
12761284
if !packet.is_empty() {
12771285
if let AudioPacket::Samples(ref mut data) = packet {
1286+
// For the basic normalisation method, a normalisation factor of 1.0 indicates that
1287+
// there is nothing to normalise (all samples should pass unaltered). For the
1288+
// dynamic method, there may still be peaks that we want to shave off.
12781289
if self.config.normalisation
12791290
&& !(f64::abs(normalisation_factor - 1.0) <= f64::EPSILON
12801291
&& self.config.normalisation_method == NormalisationMethod::Basic)
12811292
{
1293+
// zero-cost shorthands
1294+
let threshold_db = self.config.normalisation_threshold_dbfs;
1295+
let knee_db = self.config.normalisation_knee_db;
1296+
let attack_cf = self.config.normalisation_attack_cf;
1297+
let release_cf = self.config.normalisation_release_cf;
1298+
12821299
for sample in data.iter_mut() {
1283-
let mut actual_normalisation_factor = normalisation_factor;
1300+
*sample *= normalisation_factor; // for both the basic and dynamic limiter
1301+
1302+
// Feedforward limiter in the log domain
1303+
// After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic
1304+
// Range Compressor Design—A Tutorial and Analysis. Journal of The Audio
1305+
// Engineering Society, 60, 399-408.
12841306
if self.config.normalisation_method == NormalisationMethod::Dynamic
12851307
{
1286-
if self.limiter_active {
1287-
// "S"-shaped curve with a configurable knee during attack and release:
1288-
// - > 1.0 yields soft knees at start and end, steeper in between
1289-
// - 1.0 yields a linear function from 0-100%
1290-
// - between 0.0 and 1.0 yields hard knees at start and end, flatter in between
1291-
// - 0.0 yields a step response to 50%, causing distortion
1292-
// - Rates < 0.0 invert the limiter and are invalid
1293-
let mut shaped_limiter_strength = self.limiter_strength;
1294-
if shaped_limiter_strength > 0.0
1295-
&& shaped_limiter_strength < 1.0
1296-
{
1297-
shaped_limiter_strength = 1.0
1298-
/ (1.0
1299-
+ f64::powf(
1300-
shaped_limiter_strength
1301-
/ (1.0 - shaped_limiter_strength),
1302-
-self.config.normalisation_knee,
1303-
));
1304-
}
1305-
actual_normalisation_factor =
1306-
(1.0 - shaped_limiter_strength) * normalisation_factor
1307-
+ shaped_limiter_strength * self.limiter_factor;
1308-
};
1308+
// steps 1 + 2: half-wave rectification and conversion into dB
1309+
let abs_sample_db = ratio_to_db(sample.abs());
13091310

1310-
// Cast the fields here for better readability
1311-
let normalisation_attack =
1312-
self.config.normalisation_attack.as_secs_f64();
1313-
let normalisation_release =
1314-
self.config.normalisation_release.as_secs_f64();
1315-
let limiter_release_counter =
1316-
self.limiter_release_counter as f64;
1317-
let limiter_attack_counter = self.limiter_attack_counter as f64;
1318-
let samples_per_second = SAMPLES_PER_SECOND as f64;
1319-
1320-
// Always check for peaks, even when the limiter is already active.
1321-
// There may be even higher peaks than we initially targeted.
1322-
// Check against the normalisation factor that would be applied normally.
1323-
let abs_sample = f64::abs(*sample * normalisation_factor);
1324-
if abs_sample > self.config.normalisation_threshold {
1325-
self.limiter_active = true;
1326-
if self.limiter_release_counter > 0 {
1327-
// A peak was encountered while releasing the limiter;
1328-
// synchronize with the current release limiter strength.
1329-
self.limiter_attack_counter = (((samples_per_second
1330-
* normalisation_release)
1331-
- limiter_release_counter)
1332-
/ (normalisation_release / normalisation_attack))
1333-
as u32;
1334-
self.limiter_release_counter = 0;
1335-
}
1336-
1337-
self.limiter_attack_counter =
1338-
self.limiter_attack_counter.saturating_add(1);
1311+
// Some tracks have samples that are precisely 0.0, but ratio_to_db(0.0)
1312+
// returns -inf and gets the peak detector stuck.
1313+
if !abs_sample_db.is_normal() {
1314+
continue;
1315+
}
13391316

1340-
self.limiter_strength = limiter_attack_counter
1341-
/ (samples_per_second * normalisation_attack);
1317+
// step 3: gain computer with soft knee
1318+
let biased_sample = abs_sample_db - threshold_db;
1319+
let limited_sample = if 2.0 * biased_sample < -knee_db {
1320+
abs_sample_db
1321+
} else if 2.0 * biased_sample.abs() <= knee_db {
1322+
abs_sample_db
1323+
- (biased_sample + knee_db / 2.0).powi(2)
1324+
/ (2.0 * knee_db)
1325+
} else {
1326+
threshold_db as f64
1327+
};
13421328

1343-
if abs_sample > self.limiter_peak_sample {
1344-
self.limiter_peak_sample = abs_sample;
1345-
self.limiter_factor =
1346-
self.config.normalisation_threshold
1347-
/ self.limiter_peak_sample;
1348-
}
1349-
} else if self.limiter_active {
1350-
if self.limiter_attack_counter > 0 {
1351-
// Release may start within the attack period, before
1352-
// the limiter reached full strength. For that reason
1353-
// start the release by synchronizing with the current
1354-
// attack limiter strength.
1355-
self.limiter_release_counter = (((samples_per_second
1356-
* normalisation_attack)
1357-
- limiter_attack_counter)
1358-
* (normalisation_release / normalisation_attack))
1359-
as u32;
1360-
self.limiter_attack_counter = 0;
1361-
}
1329+
// step 4: subtractor
1330+
let limiter_input = abs_sample_db - limited_sample;
13621331

1363-
self.limiter_release_counter =
1364-
self.limiter_release_counter.saturating_add(1);
1365-
1366-
if self.limiter_release_counter
1367-
> (samples_per_second * normalisation_release) as u32
1368-
{
1369-
self.reset_limiter();
1370-
} else {
1371-
self.limiter_strength = ((samples_per_second
1372-
* normalisation_release)
1373-
- limiter_release_counter)
1374-
/ (samples_per_second * normalisation_release);
1375-
}
1332+
// Spare the CPU unless the limiter is active or we are riding a peak.
1333+
if !(limiter_input > 0.0
1334+
|| self.normalisation_integrator > 0.0
1335+
|| self.normalisation_peak > 0.0)
1336+
{
1337+
continue;
13761338
}
1339+
1340+
// step 5: smooth, decoupled peak detector
1341+
self.normalisation_integrator = f64::max(
1342+
limiter_input,
1343+
release_cf * self.normalisation_integrator
1344+
+ (1.0 - release_cf) * limiter_input,
1345+
);
1346+
self.normalisation_peak = attack_cf * self.normalisation_peak
1347+
+ (1.0 - attack_cf) * self.normalisation_integrator;
1348+
1349+
// step 6: make-up gain applied later (volume attenuation)
1350+
// Applying the standard normalisation factor here won't work,
1351+
// because there are tracks with peaks as high as 6 dB above
1352+
// the default threshold, so that would clip.
1353+
1354+
// steps 7-8: conversion into level and multiplication into gain stage
1355+
*sample *= db_to_ratio(-self.normalisation_peak);
13771356
}
1378-
*sample *= actual_normalisation_factor;
13791357
}
13801358
}
13811359

1360+
// Apply volume attenuation last. TODO: make this so we can chain
1361+
// the normaliser and mixer as a processing pipeline.
13821362
if let Some(ref editor) = self.audio_filter {
13831363
editor.modify_stream(data)
13841364
}
@@ -1411,15 +1391,6 @@ impl PlayerInternal {
14111391
}
14121392
}
14131393

1414-
fn reset_limiter(&mut self) {
1415-
self.limiter_active = false;
1416-
self.limiter_release_counter = 0;
1417-
self.limiter_attack_counter = 0;
1418-
self.limiter_peak_sample = 0.0;
1419-
self.limiter_factor = 1.0;
1420-
self.limiter_strength = 0.0;
1421-
}
1422-
14231394
fn start_playback(
14241395
&mut self,
14251396
track_id: SpotifyId,

0 commit comments

Comments
 (0)