New dynamic limiter for very wide dynamic ranges (#935)

New dynamic limiter for very wide dynamic ranges
This commit is contained in:
Roderick van Domburg 2022-01-14 23:31:29 +01:00 committed by GitHub
parent 1e54913523
commit 72af0d2014
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 150 additions and 181 deletions

View file

@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [main] Verbose logging mode (`-v`, `--verbose`) now logs all parsed environment variables and command line arguments (credentials are redacted). - [main] Verbose logging mode (`-v`, `--verbose`) now logs all parsed environment variables and command line arguments (credentials are redacted).
- [playback] `Sink`: `write()` now receives ownership of the packet (breaking). - [playback] `Sink`: `write()` now receives ownership of the packet (breaking).
- [playback] `pipe`: create file if it doesn't already exist - [playback] `pipe`: create file if it doesn't already exist
- [playback] More robust dynamic limiter for very wide dynamic range (breaking)
### Added ### Added
- [cache] Add `disable-credential-cache` flag (breaking). - [cache] Add `disable-credential-cache` flag (breaking).

View file

@ -1,10 +1,7 @@
use super::player::db_to_ratio; use std::{mem, str::FromStr, time::Duration};
use crate::convert::i24;
pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer};
use std::mem; pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer};
use std::str::FromStr; use crate::{convert::i24, player::duration_to_coefficient};
use std::time::Duration;
#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
pub enum Bitrate { pub enum Bitrate {
@ -133,11 +130,11 @@ pub struct PlayerConfig {
pub normalisation: bool, pub normalisation: bool,
pub normalisation_type: NormalisationType, pub normalisation_type: NormalisationType,
pub normalisation_method: NormalisationMethod, pub normalisation_method: NormalisationMethod,
pub normalisation_pregain: f64, pub normalisation_pregain_db: f32,
pub normalisation_threshold: f64, pub normalisation_threshold_dbfs: f64,
pub normalisation_attack: Duration, pub normalisation_attack_cf: f64,
pub normalisation_release: Duration, pub normalisation_release_cf: f64,
pub normalisation_knee: f64, pub normalisation_knee_db: f64,
// pass function pointers so they can be lazily instantiated *after* spawning a thread // pass function pointers so they can be lazily instantiated *after* spawning a thread
// (thereby circumventing Send bounds that they might not satisfy) // (thereby circumventing Send bounds that they might not satisfy)
@ -152,11 +149,11 @@ impl Default for PlayerConfig {
normalisation: false, normalisation: false,
normalisation_type: NormalisationType::default(), normalisation_type: NormalisationType::default(),
normalisation_method: NormalisationMethod::default(), normalisation_method: NormalisationMethod::default(),
normalisation_pregain: 0.0, normalisation_pregain_db: 0.0,
normalisation_threshold: db_to_ratio(-2.0), normalisation_threshold_dbfs: -2.0,
normalisation_attack: Duration::from_millis(5), normalisation_attack_cf: duration_to_coefficient(Duration::from_millis(5)),
normalisation_release: Duration::from_millis(100), normalisation_release_cf: duration_to_coefficient(Duration::from_millis(100)),
normalisation_knee: 1.0, normalisation_knee_db: 5.0,
passthrough: false, passthrough: false,
ditherer: Some(mk_ditherer::<TriangularDitherer>), ditherer: Some(mk_ditherer::<TriangularDitherer>),
} }

View file

@ -61,12 +61,8 @@ struct PlayerInternal {
event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>, event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>,
converter: Converter, converter: Converter,
limiter_active: bool, normalisation_integrator: f64,
limiter_attack_counter: u32, normalisation_peak: f64,
limiter_release_counter: u32,
limiter_peak_sample: f64,
limiter_factor: f64,
limiter_strength: f64,
auto_normalise_as_album: bool, auto_normalise_as_album: bool,
} }
@ -208,6 +204,14 @@ pub fn ratio_to_db(ratio: f64) -> f64 {
ratio.log10() * DB_VOLTAGE_RATIO ratio.log10() * DB_VOLTAGE_RATIO
} }
pub fn duration_to_coefficient(duration: Duration) -> f64 {
f64::exp(-1.0 / (duration.as_secs_f64() * SAMPLES_PER_SECOND as f64))
}
pub fn coefficient_to_duration(coefficient: f64) -> Duration {
Duration::from_secs_f64(-1.0 / f64::ln(coefficient) / SAMPLES_PER_SECOND as f64)
}
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct NormalisationData { pub struct NormalisationData {
track_gain_db: f32, track_gain_db: f32,
@ -241,17 +245,18 @@ impl NormalisationData {
return 1.0; return 1.0;
} }
let [gain_db, gain_peak] = if config.normalisation_type == NormalisationType::Album { let (gain_db, gain_peak) = if config.normalisation_type == NormalisationType::Album {
[data.album_gain_db, data.album_peak] (data.album_gain_db as f64, data.album_peak as f64)
} else { } else {
[data.track_gain_db, data.track_peak] (data.track_gain_db as f64, data.track_peak as f64)
}; };
let normalisation_power = gain_db as f64 + config.normalisation_pregain; let normalisation_power = gain_db + config.normalisation_pregain_db as f64;
let mut normalisation_factor = db_to_ratio(normalisation_power); let mut normalisation_factor = db_to_ratio(normalisation_power);
if normalisation_factor * gain_peak as f64 > config.normalisation_threshold { if normalisation_power + ratio_to_db(gain_peak) > config.normalisation_threshold_dbfs {
let limited_normalisation_factor = config.normalisation_threshold / gain_peak as f64; let limited_normalisation_factor =
db_to_ratio(config.normalisation_threshold_dbfs as f64) / gain_peak;
let limited_normalisation_power = ratio_to_db(limited_normalisation_factor); let limited_normalisation_power = ratio_to_db(limited_normalisation_factor);
if config.normalisation_method == NormalisationMethod::Basic { if config.normalisation_method == NormalisationMethod::Basic {
@ -295,18 +300,25 @@ impl Player {
debug!("Normalisation Type: {:?}", config.normalisation_type); debug!("Normalisation Type: {:?}", config.normalisation_type);
debug!( debug!(
"Normalisation Pregain: {:.1} dB", "Normalisation Pregain: {:.1} dB",
config.normalisation_pregain config.normalisation_pregain_db
); );
debug!( debug!(
"Normalisation Threshold: {:.1} dBFS", "Normalisation Threshold: {:.1} dBFS",
ratio_to_db(config.normalisation_threshold) config.normalisation_threshold_dbfs
); );
debug!("Normalisation Method: {:?}", config.normalisation_method); debug!("Normalisation Method: {:?}", config.normalisation_method);
if config.normalisation_method == NormalisationMethod::Dynamic { if config.normalisation_method == NormalisationMethod::Dynamic {
debug!("Normalisation Attack: {:?}", config.normalisation_attack); // as_millis() has rounding errors (truncates)
debug!("Normalisation Release: {:?}", config.normalisation_release); debug!(
debug!("Normalisation Knee: {:?}", config.normalisation_knee); "Normalisation Attack: {:.0} ms",
coefficient_to_duration(config.normalisation_attack_cf).as_secs_f64() * 1000.
);
debug!(
"Normalisation Release: {:.0} ms",
coefficient_to_duration(config.normalisation_release_cf).as_secs_f64() * 1000.
);
debug!("Normalisation Knee: {} dB", config.normalisation_knee_db);
} }
} }
@ -329,12 +341,8 @@ impl Player {
event_senders: [event_sender].to_vec(), event_senders: [event_sender].to_vec(),
converter, converter,
limiter_active: false, normalisation_peak: 0.0,
limiter_attack_counter: 0, normalisation_integrator: 0.0,
limiter_release_counter: 0,
limiter_peak_sample: 0.0,
limiter_factor: 1.0,
limiter_strength: 0.0,
auto_normalise_as_album: false, auto_normalise_as_album: false,
}; };
@ -1275,110 +1283,82 @@ impl PlayerInternal {
Some(mut packet) => { Some(mut packet) => {
if !packet.is_empty() { if !packet.is_empty() {
if let AudioPacket::Samples(ref mut data) = packet { if let AudioPacket::Samples(ref mut data) = packet {
// For the basic normalisation method, a normalisation factor of 1.0 indicates that
// there is nothing to normalise (all samples should pass unaltered). For the
// dynamic method, there may still be peaks that we want to shave off.
if self.config.normalisation if self.config.normalisation
&& !(f64::abs(normalisation_factor - 1.0) <= f64::EPSILON && !(f64::abs(normalisation_factor - 1.0) <= f64::EPSILON
&& self.config.normalisation_method == NormalisationMethod::Basic) && self.config.normalisation_method == NormalisationMethod::Basic)
{ {
// zero-cost shorthands
let threshold_db = self.config.normalisation_threshold_dbfs;
let knee_db = self.config.normalisation_knee_db;
let attack_cf = self.config.normalisation_attack_cf;
let release_cf = self.config.normalisation_release_cf;
for sample in data.iter_mut() { for sample in data.iter_mut() {
let mut actual_normalisation_factor = normalisation_factor; *sample *= normalisation_factor; // for both the basic and dynamic limiter
// Feedforward limiter in the log domain
// After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic
// Range Compressor Design—A Tutorial and Analysis. Journal of The Audio
// Engineering Society, 60, 399-408.
if self.config.normalisation_method == NormalisationMethod::Dynamic if self.config.normalisation_method == NormalisationMethod::Dynamic
{ {
if self.limiter_active { // steps 1 + 2: half-wave rectification and conversion into dB
// "S"-shaped curve with a configurable knee during attack and release: let abs_sample_db = ratio_to_db(sample.abs());
// - > 1.0 yields soft knees at start and end, steeper in between
// - 1.0 yields a linear function from 0-100% // Some tracks have samples that are precisely 0.0, but ratio_to_db(0.0)
// - between 0.0 and 1.0 yields hard knees at start and end, flatter in between // returns -inf and gets the peak detector stuck.
// - 0.0 yields a step response to 50%, causing distortion if !abs_sample_db.is_normal() {
// - Rates < 0.0 invert the limiter and are invalid continue;
let mut shaped_limiter_strength = self.limiter_strength; }
if shaped_limiter_strength > 0.0
&& shaped_limiter_strength < 1.0 // step 3: gain computer with soft knee
{ let biased_sample = abs_sample_db - threshold_db;
shaped_limiter_strength = 1.0 let limited_sample = if 2.0 * biased_sample < -knee_db {
/ (1.0 abs_sample_db
+ f64::powf( } else if 2.0 * biased_sample.abs() <= knee_db {
shaped_limiter_strength abs_sample_db
/ (1.0 - shaped_limiter_strength), - (biased_sample + knee_db / 2.0).powi(2)
-self.config.normalisation_knee, / (2.0 * knee_db)
)); } else {
} threshold_db as f64
actual_normalisation_factor =
(1.0 - shaped_limiter_strength) * normalisation_factor
+ shaped_limiter_strength * self.limiter_factor;
}; };
// Cast the fields here for better readability // step 4: subtractor
let normalisation_attack = let limiter_input = abs_sample_db - limited_sample;
self.config.normalisation_attack.as_secs_f64();
let normalisation_release =
self.config.normalisation_release.as_secs_f64();
let limiter_release_counter =
self.limiter_release_counter as f64;
let limiter_attack_counter = self.limiter_attack_counter as f64;
let samples_per_second = SAMPLES_PER_SECOND as f64;
// Always check for peaks, even when the limiter is already active. // Spare the CPU unless the limiter is active or we are riding a peak.
// There may be even higher peaks than we initially targeted. if !(limiter_input > 0.0
// Check against the normalisation factor that would be applied normally. || self.normalisation_integrator > 0.0
let abs_sample = f64::abs(*sample * normalisation_factor); || self.normalisation_peak > 0.0)
if abs_sample > self.config.normalisation_threshold { {
self.limiter_active = true; continue;
if self.limiter_release_counter > 0 {
// A peak was encountered while releasing the limiter;
// synchronize with the current release limiter strength.
self.limiter_attack_counter = (((samples_per_second
* normalisation_release)
- limiter_release_counter)
/ (normalisation_release / normalisation_attack))
as u32;
self.limiter_release_counter = 0;
}
self.limiter_attack_counter =
self.limiter_attack_counter.saturating_add(1);
self.limiter_strength = limiter_attack_counter
/ (samples_per_second * normalisation_attack);
if abs_sample > self.limiter_peak_sample {
self.limiter_peak_sample = abs_sample;
self.limiter_factor =
self.config.normalisation_threshold
/ self.limiter_peak_sample;
}
} else if self.limiter_active {
if self.limiter_attack_counter > 0 {
// Release may start within the attack period, before
// the limiter reached full strength. For that reason
// start the release by synchronizing with the current
// attack limiter strength.
self.limiter_release_counter = (((samples_per_second
* normalisation_attack)
- limiter_attack_counter)
* (normalisation_release / normalisation_attack))
as u32;
self.limiter_attack_counter = 0;
}
self.limiter_release_counter =
self.limiter_release_counter.saturating_add(1);
if self.limiter_release_counter
> (samples_per_second * normalisation_release) as u32
{
self.reset_limiter();
} else {
self.limiter_strength = ((samples_per_second
* normalisation_release)
- limiter_release_counter)
/ (samples_per_second * normalisation_release);
}
} }
// step 5: smooth, decoupled peak detector
self.normalisation_integrator = f64::max(
limiter_input,
release_cf * self.normalisation_integrator
+ (1.0 - release_cf) * limiter_input,
);
self.normalisation_peak = attack_cf * self.normalisation_peak
+ (1.0 - attack_cf) * self.normalisation_integrator;
// step 6: make-up gain applied later (volume attenuation)
// Applying the standard normalisation factor here won't work,
// because there are tracks with peaks as high as 6 dB above
// the default threshold, so that would clip.
// steps 7-8: conversion into level and multiplication into gain stage
*sample *= db_to_ratio(-self.normalisation_peak);
} }
*sample *= actual_normalisation_factor;
} }
} }
// Apply volume attenuation last. TODO: make this so we can chain
// the normaliser and mixer as a processing pipeline.
if let Some(ref editor) = self.audio_filter { if let Some(ref editor) = self.audio_filter {
editor.modify_stream(data) editor.modify_stream(data)
} }
@ -1411,15 +1391,6 @@ impl PlayerInternal {
} }
} }
fn reset_limiter(&mut self) {
self.limiter_active = false;
self.limiter_release_counter = 0;
self.limiter_attack_counter = 0;
self.limiter_peak_sample = 0.0;
self.limiter_factor = 1.0;
self.limiter_strength = 0.0;
}
fn start_playback( fn start_playback(
&mut self, &mut self,
track_id: SpotifyId, track_id: SpotifyId,

View file

@ -20,7 +20,7 @@ use librespot::playback::dither;
#[cfg(feature = "alsa-backend")] #[cfg(feature = "alsa-backend")]
use librespot::playback::mixer::alsamixer::AlsaMixer; use librespot::playback::mixer::alsamixer::AlsaMixer;
use librespot::playback::mixer::{self, MixerConfig, MixerFn}; use librespot::playback::mixer::{self, MixerConfig, MixerFn};
use librespot::playback::player::{db_to_ratio, ratio_to_db, Player}; use librespot::playback::player::{coefficient_to_duration, duration_to_coefficient, Player};
mod player_event_handler; mod player_event_handler;
use player_event_handler::{emit_sink_event, run_program_on_events}; use player_event_handler::{emit_sink_event, run_program_on_events};
@ -186,8 +186,8 @@ struct Setup {
fn get_setup() -> Setup { fn get_setup() -> Setup {
const VALID_INITIAL_VOLUME_RANGE: RangeInclusive<u16> = 0..=100; const VALID_INITIAL_VOLUME_RANGE: RangeInclusive<u16> = 0..=100;
const VALID_VOLUME_RANGE: RangeInclusive<f64> = 0.0..=100.0; const VALID_VOLUME_RANGE: RangeInclusive<f64> = 0.0..=100.0;
const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive<f64> = 0.0..=2.0; const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive<f64> = 0.0..=10.0;
const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive<f64> = -10.0..=10.0; const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive<f32> = -10.0..=10.0;
const VALID_NORMALISATION_THRESHOLD_RANGE: RangeInclusive<f64> = -10.0..=0.0; const VALID_NORMALISATION_THRESHOLD_RANGE: RangeInclusive<f64> = -10.0..=0.0;
const VALID_NORMALISATION_ATTACK_RANGE: RangeInclusive<u64> = 1..=500; const VALID_NORMALISATION_ATTACK_RANGE: RangeInclusive<u64> = 1..=500;
const VALID_NORMALISATION_RELEASE_RANGE: RangeInclusive<u64> = 1..=1000; const VALID_NORMALISATION_RELEASE_RANGE: RangeInclusive<u64> = 1..=1000;
@ -540,7 +540,7 @@ fn get_setup() -> Setup {
.optopt( .optopt(
NORMALISATION_KNEE_SHORT, NORMALISATION_KNEE_SHORT,
NORMALISATION_KNEE, NORMALISATION_KNEE,
"Knee steepness of the dynamic limiter from 0.0 to 2.0. Defaults to 1.0.", "Knee width (dB) of the dynamic limiter from 0.0 to 10.0. Defaults to 5.0.",
"KNEE", "KNEE",
) )
.optopt( .optopt(
@ -1257,11 +1257,11 @@ fn get_setup() -> Setup {
let normalisation_method; let normalisation_method;
let normalisation_type; let normalisation_type;
let normalisation_pregain; let normalisation_pregain_db;
let normalisation_threshold; let normalisation_threshold_dbfs;
let normalisation_attack; let normalisation_attack_cf;
let normalisation_release; let normalisation_release_cf;
let normalisation_knee; let normalisation_knee_db;
if !normalisation { if !normalisation {
for a in &[ for a in &[
@ -1284,11 +1284,11 @@ fn get_setup() -> Setup {
normalisation_method = player_default_config.normalisation_method; normalisation_method = player_default_config.normalisation_method;
normalisation_type = player_default_config.normalisation_type; normalisation_type = player_default_config.normalisation_type;
normalisation_pregain = player_default_config.normalisation_pregain; normalisation_pregain_db = player_default_config.normalisation_pregain_db;
normalisation_threshold = player_default_config.normalisation_threshold; normalisation_threshold_dbfs = player_default_config.normalisation_threshold_dbfs;
normalisation_attack = player_default_config.normalisation_attack; normalisation_attack_cf = player_default_config.normalisation_attack_cf;
normalisation_release = player_default_config.normalisation_release; normalisation_release_cf = player_default_config.normalisation_release_cf;
normalisation_knee = player_default_config.normalisation_knee; normalisation_knee_db = player_default_config.normalisation_knee_db;
} else { } else {
normalisation_method = opt_str(NORMALISATION_METHOD) normalisation_method = opt_str(NORMALISATION_METHOD)
.as_deref() .as_deref()
@ -1338,8 +1338,8 @@ fn get_setup() -> Setup {
}) })
.unwrap_or(player_default_config.normalisation_type); .unwrap_or(player_default_config.normalisation_type);
normalisation_pregain = opt_str(NORMALISATION_PREGAIN) normalisation_pregain_db = opt_str(NORMALISATION_PREGAIN)
.map(|pregain| match pregain.parse::<f64>() { .map(|pregain| match pregain.parse::<f32>() {
Ok(value) if (VALID_NORMALISATION_PREGAIN_RANGE).contains(&value) => value, Ok(value) if (VALID_NORMALISATION_PREGAIN_RANGE).contains(&value) => value,
_ => { _ => {
let valid_values = &format!( let valid_values = &format!(
@ -1353,19 +1353,17 @@ fn get_setup() -> Setup {
NORMALISATION_PREGAIN_SHORT, NORMALISATION_PREGAIN_SHORT,
&pregain, &pregain,
valid_values, valid_values,
&player_default_config.normalisation_pregain.to_string(), &player_default_config.normalisation_pregain_db.to_string(),
); );
exit(1); exit(1);
} }
}) })
.unwrap_or(player_default_config.normalisation_pregain); .unwrap_or(player_default_config.normalisation_pregain_db);
normalisation_threshold = opt_str(NORMALISATION_THRESHOLD) normalisation_threshold_dbfs = opt_str(NORMALISATION_THRESHOLD)
.map(|threshold| match threshold.parse::<f64>() { .map(|threshold| match threshold.parse::<f64>() {
Ok(value) if (VALID_NORMALISATION_THRESHOLD_RANGE).contains(&value) => { Ok(value) if (VALID_NORMALISATION_THRESHOLD_RANGE).contains(&value) => value,
db_to_ratio(value)
}
_ => { _ => {
let valid_values = &format!( let valid_values = &format!(
"{} - {}", "{} - {}",
@ -1378,18 +1376,20 @@ fn get_setup() -> Setup {
NORMALISATION_THRESHOLD_SHORT, NORMALISATION_THRESHOLD_SHORT,
&threshold, &threshold,
valid_values, valid_values,
&ratio_to_db(player_default_config.normalisation_threshold).to_string(), &player_default_config
.normalisation_threshold_dbfs
.to_string(),
); );
exit(1); exit(1);
} }
}) })
.unwrap_or(player_default_config.normalisation_threshold); .unwrap_or(player_default_config.normalisation_threshold_dbfs);
normalisation_attack = opt_str(NORMALISATION_ATTACK) normalisation_attack_cf = opt_str(NORMALISATION_ATTACK)
.map(|attack| match attack.parse::<u64>() { .map(|attack| match attack.parse::<u64>() {
Ok(value) if (VALID_NORMALISATION_ATTACK_RANGE).contains(&value) => { Ok(value) if (VALID_NORMALISATION_ATTACK_RANGE).contains(&value) => {
Duration::from_millis(value) duration_to_coefficient(Duration::from_millis(value))
} }
_ => { _ => {
let valid_values = &format!( let valid_values = &format!(
@ -1403,8 +1403,7 @@ fn get_setup() -> Setup {
NORMALISATION_ATTACK_SHORT, NORMALISATION_ATTACK_SHORT,
&attack, &attack,
valid_values, valid_values,
&player_default_config &coefficient_to_duration(player_default_config.normalisation_attack_cf)
.normalisation_attack
.as_millis() .as_millis()
.to_string(), .to_string(),
); );
@ -1412,12 +1411,12 @@ fn get_setup() -> Setup {
exit(1); exit(1);
} }
}) })
.unwrap_or(player_default_config.normalisation_attack); .unwrap_or(player_default_config.normalisation_attack_cf);
normalisation_release = opt_str(NORMALISATION_RELEASE) normalisation_release_cf = opt_str(NORMALISATION_RELEASE)
.map(|release| match release.parse::<u64>() { .map(|release| match release.parse::<u64>() {
Ok(value) if (VALID_NORMALISATION_RELEASE_RANGE).contains(&value) => { Ok(value) if (VALID_NORMALISATION_RELEASE_RANGE).contains(&value) => {
Duration::from_millis(value) duration_to_coefficient(Duration::from_millis(value))
} }
_ => { _ => {
let valid_values = &format!( let valid_values = &format!(
@ -1431,18 +1430,19 @@ fn get_setup() -> Setup {
NORMALISATION_RELEASE_SHORT, NORMALISATION_RELEASE_SHORT,
&release, &release,
valid_values, valid_values,
&player_default_config &coefficient_to_duration(
.normalisation_release player_default_config.normalisation_release_cf,
.as_millis() )
.to_string(), .as_millis()
.to_string(),
); );
exit(1); exit(1);
} }
}) })
.unwrap_or(player_default_config.normalisation_release); .unwrap_or(player_default_config.normalisation_release_cf);
normalisation_knee = opt_str(NORMALISATION_KNEE) normalisation_knee_db = opt_str(NORMALISATION_KNEE)
.map(|knee| match knee.parse::<f64>() { .map(|knee| match knee.parse::<f64>() {
Ok(value) if (VALID_NORMALISATION_KNEE_RANGE).contains(&value) => value, Ok(value) if (VALID_NORMALISATION_KNEE_RANGE).contains(&value) => value,
_ => { _ => {
@ -1457,13 +1457,13 @@ fn get_setup() -> Setup {
NORMALISATION_KNEE_SHORT, NORMALISATION_KNEE_SHORT,
&knee, &knee,
valid_values, valid_values,
&player_default_config.normalisation_knee.to_string(), &player_default_config.normalisation_knee_db.to_string(),
); );
exit(1); exit(1);
} }
}) })
.unwrap_or(player_default_config.normalisation_knee); .unwrap_or(player_default_config.normalisation_knee_db);
} }
let ditherer_name = opt_str(DITHER); let ditherer_name = opt_str(DITHER);
@ -1505,11 +1505,11 @@ fn get_setup() -> Setup {
normalisation, normalisation,
normalisation_type, normalisation_type,
normalisation_method, normalisation_method,
normalisation_pregain, normalisation_pregain_db,
normalisation_threshold, normalisation_threshold_dbfs,
normalisation_attack, normalisation_attack_cf,
normalisation_release, normalisation_release_cf,
normalisation_knee, normalisation_knee_db,
ditherer, ditherer,
} }
}; };