1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-04 10:19:27 +02:00

High-resolution volume control and normalisation

- Store and output samples as 32-bit floats instead of 16-bit integers.
   This provides 24-25 bits of transparency, allowing for 42-48 dB of
   headroom to do volume control and normalisation without throwing
   away bits or dropping dynamic range below 96 dB CD quality.

 - Perform volume control and normalisation in 64-bit arithmetic.

 - Add a dynamic limiter with configurable threshold, attack time,
   release or decay time, and steepness for the sigmoid transfer
   function. This mimics the native Spotify limiter, offering greater
   dynamic range than the old limiter, that just reduced overall gain
   to prevent clipping.

 - Make the configurable threshold also apply to the old limiter, which
   is still available.

Resolves: librespot-org/librespot#608
This commit is contained in:
Roderick van Domburg 2021-02-24 21:39:42 +01:00
parent 56f1fb6dae
commit f29e5212c4
17 changed files with 327 additions and 53 deletions

View file

@ -22,13 +22,15 @@ use librespot::core::version;
use librespot::connect::discovery::{discovery, DiscoveryStream};
use librespot::connect::spirc::{Spirc, SpircTask};
use librespot::playback::audio_backend::{self, Sink, BACKENDS};
use librespot::playback::config::{Bitrate, NormalisationType, PlayerConfig};
use librespot::playback::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig};
use librespot::playback::mixer::{self, Mixer, MixerConfig};
use librespot::playback::player::{Player, PlayerEvent};
use librespot::playback::player::{NormalisationData, Player, PlayerEvent};
mod player_event_handler;
use crate::player_event_handler::{emit_sink_event, run_program_on_events};
const MILLIS: f32 = 1000.0;
fn device_id(name: &str) -> String {
hex::encode(Sha1::digest(name.as_bytes()))
}
@ -188,6 +190,12 @@ fn setup(args: &[String]) -> Setup {
"enable-volume-normalisation",
"Play all tracks at the same volume",
)
.optopt(
"",
"normalisation-method",
"Specify the normalisation method to use - [basic, dynamic]. Default is dynamic.",
"NORMALISATION_METHOD",
)
.optopt(
"",
"normalisation-gain-type",
@ -200,6 +208,30 @@ fn setup(args: &[String]) -> Setup {
"Pregain (dB) applied by volume normalisation",
"PREGAIN",
)
.optopt(
"",
"normalisation-threshold",
"Threshold (dBFS) to prevent clipping. Default is -1.0.",
"THRESHOLD",
)
.optopt(
"",
"normalisation-attack",
"Attack time (ms) in which the dynamic limiter is reducing gain. Default is 5.",
"ATTACK",
)
.optopt(
"",
"normalisation-release",
"Release or decay time (ms) in which the dynamic limiter is restoring gain. Default is 100.",
"RELEASE",
)
.optopt(
"",
"normalisation-steepness",
"Steepness of the dynamic limiting curve. Default is 1.0.",
"STEEPNESS",
)
.optopt(
"",
"volume-ctrl",
@ -390,15 +422,51 @@ fn setup(args: &[String]) -> Setup {
NormalisationType::from_str(gain_type).expect("Invalid normalisation type")
})
.unwrap_or(NormalisationType::default());
let normalisation_method = matches
.opt_str("normalisation-method")
.as_ref()
.map(|gain_type| {
NormalisationMethod::from_str(gain_type).expect("Invalid normalisation method")
})
.unwrap_or(NormalisationMethod::default());
PlayerConfig {
bitrate: bitrate,
gapless: !matches.opt_present("disable-gapless"),
normalisation: matches.opt_present("enable-volume-normalisation"),
normalisation_method: normalisation_method,
normalisation_type: gain_type,
normalisation_pregain: matches
.opt_str("normalisation-pregain")
.map(|pregain| pregain.parse::<f32>().expect("Invalid pregain float value"))
.unwrap_or(PlayerConfig::default().normalisation_pregain),
normalisation_threshold: NormalisationData::db_to_ratio(
matches
.opt_str("normalisation-threshold")
.map(|threshold| {
threshold
.parse::<f32>()
.expect("Invalid threshold float value")
})
.unwrap_or(PlayerConfig::default().normalisation_threshold),
),
normalisation_attack: matches
.opt_str("normalisation-attack")
.map(|attack| attack.parse::<f32>().expect("Invalid attack float value"))
.unwrap_or(PlayerConfig::default().normalisation_attack * MILLIS)
/ MILLIS,
normalisation_release: matches
.opt_str("normalisation-release")
.map(|release| release.parse::<f32>().expect("Invalid release float value"))
.unwrap_or(PlayerConfig::default().normalisation_release * MILLIS)
/ MILLIS,
normalisation_steepness: matches
.opt_str("normalisation-steepness")
.map(|steepness| {
steepness
.parse::<f32>()
.expect("Invalid steepness float value")
})
.unwrap_or(PlayerConfig::default().normalisation_steepness),
passthrough,
}
};