diff --git a/CHANGELOG.md b/CHANGELOG.md index a2be0992..c0a91a29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [contrib] Hardened security of the systemd service units - [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] `pipe`: create file if it doesn't already exist +- [playback] More robust dynamic limiter for very wide dynamic range (breaking) ### Added - [cache] Add `disable-credential-cache` flag (breaking). diff --git a/metadata/src/audio/item.rs b/metadata/src/audio/item.rs index d2304810..c3df12e0 100644 --- a/metadata/src/audio/item.rs +++ b/metadata/src/audio/item.rs @@ -10,7 +10,9 @@ use crate::{ use super::file::AudioFiles; -use librespot_core::{session::UserData, date::Date, spotify_id::SpotifyItemType, Error, Session, SpotifyId}; +use librespot_core::{ + date::Date, session::UserData, spotify_id::SpotifyItemType, Error, Session, SpotifyId, +}; pub type AudioItemResult = Result; diff --git a/metadata/src/availability.rs b/metadata/src/availability.rs index d3c4615b..09016a5b 100644 --- a/metadata/src/availability.rs +++ b/metadata/src/availability.rs @@ -1,4 +1,8 @@ -use std::{convert::{TryFrom, TryInto}, fmt::Debug, ops::Deref}; +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, +}; use thiserror::Error; diff --git a/metadata/src/sale_period.rs b/metadata/src/sale_period.rs index 053d5e1c..b02d2368 100644 --- a/metadata/src/sale_period.rs +++ b/metadata/src/sale_period.rs @@ -1,4 +1,8 @@ -use std::{convert::{TryFrom, TryInto}, fmt::Debug, ops::Deref}; +use std::{ + convert::{TryFrom, TryInto}, + fmt::Debug, + ops::Deref, +}; use crate::{restriction::Restrictions, util::try_from_repeated_message}; diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index fd804a0e..682f8124 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -4,19 +4,27 @@ use crate::convert::Converter; use crate::decoder::AudioPacket; use std::fs::OpenOptions; use std::io::{self, Write}; +use std::process::exit; pub struct StdoutSink { output: Option>, - path: Option, + file: Option, format: AudioFormat, } impl Open for StdoutSink { - fn open(path: Option, format: AudioFormat) -> Self { + fn open(file: Option, format: AudioFormat) -> Self { + if let Some("?") = file.as_deref() { + info!("Usage:"); + println!(" Output to stdout: --backend pipe"); + println!(" Output to file: --backend pipe --device {{filename}}"); + exit(0); + } + info!("Using pipe sink with format: {:?}", format); Self { output: None, - path, + file, format, } } @@ -25,11 +33,12 @@ impl Open for StdoutSink { impl Sink for StdoutSink { fn start(&mut self) -> SinkResult<()> { if self.output.is_none() { - let output: Box = match self.path.as_deref() { - Some(path) => { + let output: Box = match self.file.as_deref() { + Some(file) => { let open_op = OpenOptions::new() .write(true) - .open(path) + .create(true) + .open(file) .map_err(|e| SinkError::ConnectionRefused(e.to_string()))?; Box::new(open_op) } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 9f4ad059..bbc5de1a 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -135,21 +135,18 @@ fn create_sink( host: &cpal::Host, device: Option, ) -> Result<(rodio::Sink, rodio::OutputStream), RodioError> { - let rodio_device = match device { - Some(ask) if &ask == "?" => { - let exit_code = match list_outputs(host) { - Ok(()) => 0, - Err(e) => { - error!("{}", e); - 1 - } - }; - exit(exit_code) - } + let rodio_device = match device.as_deref() { + Some("?") => match list_outputs(host) { + Ok(()) => exit(0), + Err(e) => { + error!("{}", e); + exit(1); + } + }, Some(device_name) => { host.output_devices()? .find(|d| d.name().ok().map_or(false, |name| name == device_name)) // Ignore devices for which getting name fails - .ok_or(RodioError::DeviceNotAvailable(device_name))? + .ok_or_else(|| RodioError::DeviceNotAvailable(device_name.to_string()))? } None => host .default_output_device() diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index c501cf83..63fc5d88 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -5,7 +5,7 @@ use crate::decoder::AudioPacket; use shell_words::split; use std::io::Write; -use std::process::{Child, Command, Stdio}; +use std::process::{exit, Child, Command, Stdio}; pub struct SubprocessSink { shell_command: String, @@ -15,16 +15,24 @@ pub struct SubprocessSink { impl Open for SubprocessSink { fn open(shell_command: Option, format: AudioFormat) -> Self { + let shell_command = match shell_command.as_deref() { + Some("?") => { + info!("Usage: --backend subprocess --device {{shell_command}}"); + exit(0); + } + Some(cmd) => cmd.to_owned(), + None => { + error!("subprocess sink requires specifying a shell command"); + exit(1); + } + }; + info!("Using subprocess sink with format: {:?}", format); - if let Some(shell_command) = shell_command { - SubprocessSink { - shell_command, - child: None, - format, - } - } else { - panic!("subprocess sink requires specifying a shell command"); + Self { + shell_command, + child: None, + format, } } } diff --git a/playback/src/config.rs b/playback/src/config.rs index b8313bf4..4070a26a 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,10 +1,7 @@ -use super::player::db_to_ratio; -use crate::convert::i24; -pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer}; +use std::{mem, str::FromStr, time::Duration}; -use std::mem; -use std::str::FromStr; -use std::time::Duration; +pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer}; +use crate::{convert::i24, player::duration_to_coefficient}; #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum Bitrate { @@ -133,11 +130,11 @@ pub struct PlayerConfig { pub normalisation: bool, pub normalisation_type: NormalisationType, pub normalisation_method: NormalisationMethod, - pub normalisation_pregain: f64, - pub normalisation_threshold: f64, - pub normalisation_attack: Duration, - pub normalisation_release: Duration, - pub normalisation_knee: f64, + pub normalisation_pregain_db: f32, + pub normalisation_threshold_dbfs: f64, + pub normalisation_attack_cf: f64, + pub normalisation_release_cf: f64, + pub normalisation_knee_db: f64, // pass function pointers so they can be lazily instantiated *after* spawning a thread // (thereby circumventing Send bounds that they might not satisfy) @@ -152,11 +149,11 @@ impl Default for PlayerConfig { normalisation: false, normalisation_type: NormalisationType::default(), normalisation_method: NormalisationMethod::default(), - normalisation_pregain: 0.0, - normalisation_threshold: db_to_ratio(-2.0), - normalisation_attack: Duration::from_millis(5), - normalisation_release: Duration::from_millis(100), - normalisation_knee: 1.0, + normalisation_pregain_db: 0.0, + normalisation_threshold_dbfs: -2.0, + normalisation_attack_cf: duration_to_coefficient(Duration::from_millis(5)), + normalisation_release_cf: duration_to_coefficient(Duration::from_millis(100)), + normalisation_knee_db: 5.0, passthrough: false, ditherer: Some(mk_ditherer::), } diff --git a/playback/src/player.rs b/playback/src/player.rs index 61d68bfc..aad6df5b 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -80,12 +80,8 @@ struct PlayerInternal { event_senders: Vec>, converter: Converter, - limiter_active: bool, - limiter_attack_counter: u32, - limiter_release_counter: u32, - limiter_peak_sample: f64, - limiter_factor: f64, - limiter_strength: f64, + normalisation_integrator: f64, + normalisation_peak: f64, auto_normalise_as_album: bool, } @@ -228,6 +224,14 @@ pub fn ratio_to_db(ratio: f64) -> f64 { 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)] pub struct NormalisationData { // Spotify provides these as `f32`, but audio metadata can contain up to `f64`. @@ -283,17 +287,18 @@ impl NormalisationData { return 1.0; } - let [gain_db, gain_peak] = if config.normalisation_type == NormalisationType::Album { - [data.album_gain_db, data.album_peak] + let (gain_db, gain_peak) = if config.normalisation_type == NormalisationType::Album { + (data.album_gain_db as f64, data.album_peak as f64) } else { - [data.track_gain_db, data.track_peak] + (data.track_gain_db as f64, data.track_peak as f64) }; - let normalisation_power = gain_db + config.normalisation_pregain; + let normalisation_power = gain_db + config.normalisation_pregain_db as f64; let mut normalisation_factor = db_to_ratio(normalisation_power); - if normalisation_factor * gain_peak > config.normalisation_threshold { - let limited_normalisation_factor = config.normalisation_threshold / gain_peak; + if normalisation_power + ratio_to_db(gain_peak) > config.normalisation_threshold_dbfs { + 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); if config.normalisation_method == NormalisationMethod::Basic { @@ -337,18 +342,25 @@ impl Player { debug!("Normalisation Type: {:?}", config.normalisation_type); debug!( "Normalisation Pregain: {:.1} dB", - config.normalisation_pregain + config.normalisation_pregain_db ); debug!( "Normalisation Threshold: {:.1} dBFS", - ratio_to_db(config.normalisation_threshold) + config.normalisation_threshold_dbfs ); debug!("Normalisation Method: {:?}", config.normalisation_method); if config.normalisation_method == NormalisationMethod::Dynamic { - debug!("Normalisation Attack: {:?}", config.normalisation_attack); - debug!("Normalisation Release: {:?}", config.normalisation_release); - debug!("Normalisation Knee: {:?}", config.normalisation_knee); + // as_millis() has rounding errors (truncates) + debug!( + "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); } } @@ -372,12 +384,8 @@ impl Player { event_senders: [event_sender].to_vec(), converter, - limiter_active: false, - limiter_attack_counter: 0, - limiter_release_counter: 0, - limiter_peak_sample: 0.0, - limiter_factor: 1.0, - limiter_strength: 0.0, + normalisation_peak: 0.0, + normalisation_integrator: 0.0, auto_normalise_as_album: false, }; @@ -1387,110 +1395,82 @@ impl PlayerInternal { Some((_, mut packet)) => { if !packet.is_empty() { 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 && !(f64::abs(normalisation_factor - 1.0) <= f64::EPSILON && 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() { - 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.limiter_active { - // "S"-shaped curve with a configurable knee during attack and release: - // - > 1.0 yields soft knees at start and end, steeper in between - // - 1.0 yields a linear function from 0-100% - // - between 0.0 and 1.0 yields hard knees at start and end, flatter in between - // - 0.0 yields a step response to 50%, causing distortion - // - Rates < 0.0 invert the limiter and are invalid - let mut shaped_limiter_strength = self.limiter_strength; - if shaped_limiter_strength > 0.0 - && shaped_limiter_strength < 1.0 - { - shaped_limiter_strength = 1.0 - / (1.0 - + f64::powf( - shaped_limiter_strength - / (1.0 - shaped_limiter_strength), - -self.config.normalisation_knee, - )); - } - actual_normalisation_factor = - (1.0 - shaped_limiter_strength) * normalisation_factor - + shaped_limiter_strength * self.limiter_factor; + // steps 1 + 2: half-wave rectification and conversion into dB + let abs_sample_db = ratio_to_db(sample.abs()); + + // Some tracks have samples that are precisely 0.0, but ratio_to_db(0.0) + // returns -inf and gets the peak detector stuck. + if !abs_sample_db.is_normal() { + continue; + } + + // step 3: gain computer with soft knee + let biased_sample = abs_sample_db - threshold_db; + let limited_sample = if 2.0 * biased_sample < -knee_db { + abs_sample_db + } else if 2.0 * biased_sample.abs() <= knee_db { + abs_sample_db + - (biased_sample + knee_db / 2.0).powi(2) + / (2.0 * knee_db) + } else { + threshold_db as f64 }; - // Cast the fields here for better readability - let normalisation_attack = - 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; + // step 4: subtractor + let limiter_input = abs_sample_db - limited_sample; - // Always check for peaks, even when the limiter is already active. - // There may be even higher peaks than we initially targeted. - // Check against the normalisation factor that would be applied normally. - let abs_sample = f64::abs(*sample * normalisation_factor); - if abs_sample > self.config.normalisation_threshold { - self.limiter_active = true; - 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); - } + // Spare the CPU unless the limiter is active or we are riding a peak. + if !(limiter_input > 0.0 + || self.normalisation_integrator > 0.0 + || self.normalisation_peak > 0.0) + { + continue; } + + // 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 { editor.modify_stream(data) } @@ -1523,15 +1503,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( &mut self, track_id: SpotifyId, diff --git a/src/main.rs b/src/main.rs index 818a1c0b..527a234b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,7 @@ use librespot::{ }, dither, mixer::{self, MixerConfig, MixerFn}, - player::{db_to_ratio, ratio_to_db, Player, PlayerEvent}, + player::{coefficient_to_duration, duration_to_coefficient, Player, PlayerEvent}, }, }; @@ -191,8 +191,8 @@ struct Setup { fn get_setup() -> Setup { const VALID_INITIAL_VOLUME_RANGE: RangeInclusive = 0..=100; const VALID_VOLUME_RANGE: RangeInclusive = 0.0..=100.0; - const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.0..=2.0; - const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive = -10.0..=10.0; + const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.0..=10.0; + const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive = -10.0..=10.0; const VALID_NORMALISATION_THRESHOLD_RANGE: RangeInclusive = -10.0..=0.0; const VALID_NORMALISATION_ATTACK_RANGE: RangeInclusive = 1..=500; const VALID_NORMALISATION_RELEASE_RANGE: RangeInclusive = 1..=1000; @@ -546,7 +546,7 @@ fn get_setup() -> Setup { .optopt( NORMALISATION_KNEE_SHORT, 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", ) .optopt( @@ -754,18 +754,7 @@ fn get_setup() -> Setup { }) .unwrap_or_default(); - #[cfg(any( - feature = "alsa-backend", - feature = "rodio-backend", - feature = "portaudio-backend" - ))] let device = opt_str(DEVICE); - - #[cfg(any( - feature = "alsa-backend", - feature = "rodio-backend", - feature = "portaudio-backend" - ))] if let Some(ref value) = device { if value == "?" { backend(device, format); @@ -775,25 +764,6 @@ fn get_setup() -> Setup { } } - #[cfg(not(any( - feature = "alsa-backend", - feature = "rodio-backend", - feature = "portaudio-backend" - )))] - let device: Option = None; - - #[cfg(not(any( - feature = "alsa-backend", - feature = "rodio-backend", - feature = "portaudio-backend" - )))] - if opt_present(DEVICE) { - warn!( - "The `--{}` / `-{}` option is not supported by the included audio backend(s), and has no effect.", - DEVICE, DEVICE_SHORT, - ); - } - #[cfg(feature = "alsa-backend")] let mixer_type = opt_str(MIXER_TYPE); #[cfg(not(feature = "alsa-backend"))] @@ -1296,11 +1266,11 @@ fn get_setup() -> Setup { let normalisation_method; let normalisation_type; - let normalisation_pregain; - let normalisation_threshold; - let normalisation_attack; - let normalisation_release; - let normalisation_knee; + let normalisation_pregain_db; + let normalisation_threshold_dbfs; + let normalisation_attack_cf; + let normalisation_release_cf; + let normalisation_knee_db; if !normalisation { for a in &[ @@ -1323,11 +1293,11 @@ fn get_setup() -> Setup { normalisation_method = player_default_config.normalisation_method; normalisation_type = player_default_config.normalisation_type; - normalisation_pregain = player_default_config.normalisation_pregain; - normalisation_threshold = player_default_config.normalisation_threshold; - normalisation_attack = player_default_config.normalisation_attack; - normalisation_release = player_default_config.normalisation_release; - normalisation_knee = player_default_config.normalisation_knee; + normalisation_pregain_db = player_default_config.normalisation_pregain_db; + normalisation_threshold_dbfs = player_default_config.normalisation_threshold_dbfs; + normalisation_attack_cf = player_default_config.normalisation_attack_cf; + normalisation_release_cf = player_default_config.normalisation_release_cf; + normalisation_knee_db = player_default_config.normalisation_knee_db; } else { normalisation_method = opt_str(NORMALISATION_METHOD) .as_deref() @@ -1377,8 +1347,8 @@ fn get_setup() -> Setup { }) .unwrap_or(player_default_config.normalisation_type); - normalisation_pregain = opt_str(NORMALISATION_PREGAIN) - .map(|pregain| match pregain.parse::() { + normalisation_pregain_db = opt_str(NORMALISATION_PREGAIN) + .map(|pregain| match pregain.parse::() { Ok(value) if (VALID_NORMALISATION_PREGAIN_RANGE).contains(&value) => value, _ => { let valid_values = &format!( @@ -1392,19 +1362,17 @@ fn get_setup() -> Setup { NORMALISATION_PREGAIN_SHORT, &pregain, valid_values, - &player_default_config.normalisation_pregain.to_string(), + &player_default_config.normalisation_pregain_db.to_string(), ); 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::() { - Ok(value) if (VALID_NORMALISATION_THRESHOLD_RANGE).contains(&value) => { - db_to_ratio(value) - } + Ok(value) if (VALID_NORMALISATION_THRESHOLD_RANGE).contains(&value) => value, _ => { let valid_values = &format!( "{} - {}", @@ -1417,18 +1385,20 @@ fn get_setup() -> Setup { NORMALISATION_THRESHOLD_SHORT, &threshold, valid_values, - &ratio_to_db(player_default_config.normalisation_threshold).to_string(), + &player_default_config + .normalisation_threshold_dbfs + .to_string(), ); 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::() { Ok(value) if (VALID_NORMALISATION_ATTACK_RANGE).contains(&value) => { - Duration::from_millis(value) + duration_to_coefficient(Duration::from_millis(value)) } _ => { let valid_values = &format!( @@ -1442,8 +1412,7 @@ fn get_setup() -> Setup { NORMALISATION_ATTACK_SHORT, &attack, valid_values, - &player_default_config - .normalisation_attack + &coefficient_to_duration(player_default_config.normalisation_attack_cf) .as_millis() .to_string(), ); @@ -1451,12 +1420,12 @@ fn get_setup() -> Setup { 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::() { Ok(value) if (VALID_NORMALISATION_RELEASE_RANGE).contains(&value) => { - Duration::from_millis(value) + duration_to_coefficient(Duration::from_millis(value)) } _ => { let valid_values = &format!( @@ -1470,18 +1439,19 @@ fn get_setup() -> Setup { NORMALISATION_RELEASE_SHORT, &release, valid_values, - &player_default_config - .normalisation_release - .as_millis() - .to_string(), + &coefficient_to_duration( + player_default_config.normalisation_release_cf, + ) + .as_millis() + .to_string(), ); 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::() { Ok(value) if (VALID_NORMALISATION_KNEE_RANGE).contains(&value) => value, _ => { @@ -1496,13 +1466,13 @@ fn get_setup() -> Setup { NORMALISATION_KNEE_SHORT, &knee, valid_values, - &player_default_config.normalisation_knee.to_string(), + &player_default_config.normalisation_knee_db.to_string(), ); exit(1); } }) - .unwrap_or(player_default_config.normalisation_knee); + .unwrap_or(player_default_config.normalisation_knee_db); } let ditherer_name = opt_str(DITHER); @@ -1544,11 +1514,11 @@ fn get_setup() -> Setup { normalisation, normalisation_type, normalisation_method, - normalisation_pregain, - normalisation_threshold, - normalisation_attack, - normalisation_release, - normalisation_knee, + normalisation_pregain_db, + normalisation_threshold_dbfs, + normalisation_attack_cf, + normalisation_release_cf, + normalisation_knee_db, ditherer, } };