mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Merge branch 'dev' into new-api
This commit is contained in:
commit
0de55c6183
10 changed files with 205 additions and 241 deletions
|
@ -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
|
- [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).
|
- [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] 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).
|
||||||
|
|
|
@ -10,7 +10,9 @@ use crate::{
|
||||||
|
|
||||||
use super::file::AudioFiles;
|
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<AudioItem, Error>;
|
pub type AudioItemResult = Result<AudioItem, Error>;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
|
|
@ -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};
|
use crate::{restriction::Restrictions, util::try_from_repeated_message};
|
||||||
|
|
||||||
|
|
|
@ -4,19 +4,27 @@ use crate::convert::Converter;
|
||||||
use crate::decoder::AudioPacket;
|
use crate::decoder::AudioPacket;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
use std::process::exit;
|
||||||
|
|
||||||
pub struct StdoutSink {
|
pub struct StdoutSink {
|
||||||
output: Option<Box<dyn Write>>,
|
output: Option<Box<dyn Write>>,
|
||||||
path: Option<String>,
|
file: Option<String>,
|
||||||
format: AudioFormat,
|
format: AudioFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Open for StdoutSink {
|
impl Open for StdoutSink {
|
||||||
fn open(path: Option<String>, format: AudioFormat) -> Self {
|
fn open(file: Option<String>, 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);
|
info!("Using pipe sink with format: {:?}", format);
|
||||||
Self {
|
Self {
|
||||||
output: None,
|
output: None,
|
||||||
path,
|
file,
|
||||||
format,
|
format,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,11 +33,12 @@ impl Open for StdoutSink {
|
||||||
impl Sink for StdoutSink {
|
impl Sink for StdoutSink {
|
||||||
fn start(&mut self) -> SinkResult<()> {
|
fn start(&mut self) -> SinkResult<()> {
|
||||||
if self.output.is_none() {
|
if self.output.is_none() {
|
||||||
let output: Box<dyn Write> = match self.path.as_deref() {
|
let output: Box<dyn Write> = match self.file.as_deref() {
|
||||||
Some(path) => {
|
Some(file) => {
|
||||||
let open_op = OpenOptions::new()
|
let open_op = OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.open(path)
|
.create(true)
|
||||||
|
.open(file)
|
||||||
.map_err(|e| SinkError::ConnectionRefused(e.to_string()))?;
|
.map_err(|e| SinkError::ConnectionRefused(e.to_string()))?;
|
||||||
Box::new(open_op)
|
Box::new(open_op)
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,21 +135,18 @@ fn create_sink(
|
||||||
host: &cpal::Host,
|
host: &cpal::Host,
|
||||||
device: Option<String>,
|
device: Option<String>,
|
||||||
) -> Result<(rodio::Sink, rodio::OutputStream), RodioError> {
|
) -> Result<(rodio::Sink, rodio::OutputStream), RodioError> {
|
||||||
let rodio_device = match device {
|
let rodio_device = match device.as_deref() {
|
||||||
Some(ask) if &ask == "?" => {
|
Some("?") => match list_outputs(host) {
|
||||||
let exit_code = match list_outputs(host) {
|
Ok(()) => exit(0),
|
||||||
Ok(()) => 0,
|
Err(e) => {
|
||||||
Err(e) => {
|
error!("{}", e);
|
||||||
error!("{}", e);
|
exit(1);
|
||||||
1
|
}
|
||||||
}
|
},
|
||||||
};
|
|
||||||
exit(exit_code)
|
|
||||||
}
|
|
||||||
Some(device_name) => {
|
Some(device_name) => {
|
||||||
host.output_devices()?
|
host.output_devices()?
|
||||||
.find(|d| d.name().ok().map_or(false, |name| name == device_name)) // Ignore devices for which getting name fails
|
.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
|
None => host
|
||||||
.default_output_device()
|
.default_output_device()
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::decoder::AudioPacket;
|
||||||
use shell_words::split;
|
use shell_words::split;
|
||||||
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{exit, Child, Command, Stdio};
|
||||||
|
|
||||||
pub struct SubprocessSink {
|
pub struct SubprocessSink {
|
||||||
shell_command: String,
|
shell_command: String,
|
||||||
|
@ -15,16 +15,24 @@ pub struct SubprocessSink {
|
||||||
|
|
||||||
impl Open for SubprocessSink {
|
impl Open for SubprocessSink {
|
||||||
fn open(shell_command: Option<String>, format: AudioFormat) -> Self {
|
fn open(shell_command: Option<String>, 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);
|
info!("Using subprocess sink with format: {:?}", format);
|
||||||
|
|
||||||
if let Some(shell_command) = shell_command {
|
Self {
|
||||||
SubprocessSink {
|
shell_command,
|
||||||
shell_command,
|
child: None,
|
||||||
child: None,
|
format,
|
||||||
format,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic!("subprocess sink requires specifying a shell command");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>),
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,12 +80,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,
|
||||||
}
|
}
|
||||||
|
@ -228,6 +224,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 {
|
||||||
// Spotify provides these as `f32`, but audio metadata can contain up to `f64`.
|
// Spotify provides these as `f32`, but audio metadata can contain up to `f64`.
|
||||||
|
@ -283,17 +287,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 + 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 > config.normalisation_threshold {
|
if normalisation_power + ratio_to_db(gain_peak) > config.normalisation_threshold_dbfs {
|
||||||
let limited_normalisation_factor = config.normalisation_threshold / gain_peak;
|
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 {
|
||||||
|
@ -337,18 +342,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -372,12 +384,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,
|
||||||
};
|
};
|
||||||
|
@ -1387,110 +1395,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)
|
||||||
}
|
}
|
||||||
|
@ -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(
|
fn start_playback(
|
||||||
&mut self,
|
&mut self,
|
||||||
track_id: SpotifyId,
|
track_id: SpotifyId,
|
||||||
|
|
118
src/main.rs
118
src/main.rs
|
@ -29,7 +29,7 @@ use librespot::{
|
||||||
},
|
},
|
||||||
dither,
|
dither,
|
||||||
mixer::{self, MixerConfig, MixerFn},
|
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 {
|
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;
|
||||||
|
@ -546,7 +546,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(
|
||||||
|
@ -754,18 +754,7 @@ fn get_setup() -> Setup {
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
#[cfg(any(
|
|
||||||
feature = "alsa-backend",
|
|
||||||
feature = "rodio-backend",
|
|
||||||
feature = "portaudio-backend"
|
|
||||||
))]
|
|
||||||
let device = opt_str(DEVICE);
|
let device = opt_str(DEVICE);
|
||||||
|
|
||||||
#[cfg(any(
|
|
||||||
feature = "alsa-backend",
|
|
||||||
feature = "rodio-backend",
|
|
||||||
feature = "portaudio-backend"
|
|
||||||
))]
|
|
||||||
if let Some(ref value) = device {
|
if let Some(ref value) = device {
|
||||||
if value == "?" {
|
if value == "?" {
|
||||||
backend(device, format);
|
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<String> = 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")]
|
#[cfg(feature = "alsa-backend")]
|
||||||
let mixer_type = opt_str(MIXER_TYPE);
|
let mixer_type = opt_str(MIXER_TYPE);
|
||||||
#[cfg(not(feature = "alsa-backend"))]
|
#[cfg(not(feature = "alsa-backend"))]
|
||||||
|
@ -1296,11 +1266,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 &[
|
||||||
|
@ -1323,11 +1293,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()
|
||||||
|
@ -1377,8 +1347,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!(
|
||||||
|
@ -1392,19 +1362,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!(
|
||||||
"{} - {}",
|
"{} - {}",
|
||||||
|
@ -1417,18 +1385,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!(
|
||||||
|
@ -1442,8 +1412,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(),
|
||||||
);
|
);
|
||||||
|
@ -1451,12 +1420,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!(
|
||||||
|
@ -1470,18 +1439,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,
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -1496,13 +1466,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);
|
||||||
|
@ -1544,11 +1514,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,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue