mirror of
https://github.com/librespot-org/librespot.git
synced 2025-02-06 17:47:10 +00:00
Put it all together
This commit is contained in:
parent
e1ea400220
commit
efec96b9cc
12 changed files with 152 additions and 314 deletions
|
@ -13,9 +13,7 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
|
use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
|
||||||
|
|
||||||
use crate::{
|
use crate::{config::AudioFormat, convert::Converter, decoder::AudioPacket, NUM_CHANNELS};
|
||||||
config::AudioFormat, convert::Converter, decoder::AudioPacket, NUM_CHANNELS,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct GstreamerSink {
|
pub struct GstreamerSink {
|
||||||
appsrc: gst_app::AppSrc,
|
appsrc: gst_app::AppSrc,
|
||||||
|
|
|
@ -42,7 +42,10 @@ impl Open for JackSink {
|
||||||
if format != AudioFormat::F32 {
|
if format != AudioFormat::F32 {
|
||||||
warn!("JACK currently does not support {format:?} output");
|
warn!("JACK currently does not support {format:?} output");
|
||||||
}
|
}
|
||||||
info!("Using JACK sink with format {:?}, sample rate: {sample_rate}", AudioFormat::F32);
|
info!(
|
||||||
|
"Using JACK sink with format {:?}, sample rate: {sample_rate}",
|
||||||
|
AudioFormat::F32
|
||||||
|
);
|
||||||
|
|
||||||
let client_name = client_name.unwrap_or_else(|| "librespot".to_string());
|
let client_name = client_name.unwrap_or_else(|| "librespot".to_string());
|
||||||
let (client, _status) =
|
let (client, _status) =
|
||||||
|
|
|
@ -119,9 +119,15 @@ impl<'a> Sink for PortAudioSink<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Self::F32(stream, parameters, sample_rate) => start_sink!(ref mut stream, ref parameters, ref sample_rate),
|
Self::F32(stream, parameters, sample_rate) => {
|
||||||
Self::S32(stream, parameters, sample_rate) => start_sink!(ref mut stream, ref parameters, ref sample_rate),
|
start_sink!(ref mut stream, ref parameters, ref sample_rate)
|
||||||
Self::S16(stream, parameters, sample_rate) => start_sink!(ref mut stream, ref parameters, ref sample_rate),
|
}
|
||||||
|
Self::S32(stream, parameters, sample_rate) => {
|
||||||
|
start_sink!(ref mut stream, ref parameters, ref sample_rate)
|
||||||
|
}
|
||||||
|
Self::S16(stream, parameters, sample_rate) => {
|
||||||
|
start_sink!(ref mut stream, ref parameters, ref sample_rate)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -23,7 +23,11 @@ pub fn mk_rodio(device: Option<String>, format: AudioFormat, sample_rate: u32) -
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "rodiojack-backend")]
|
#[cfg(feature = "rodiojack-backend")]
|
||||||
pub fn mk_rodiojack(device: Option<String>, format: AudioFormat, sample_rate: u32) -> Box<dyn Sink> {
|
pub fn mk_rodiojack(
|
||||||
|
device: Option<String>,
|
||||||
|
format: AudioFormat,
|
||||||
|
sample_rate: u32,
|
||||||
|
) -> Box<dyn Sink> {
|
||||||
Box::new(open(
|
Box::new(open(
|
||||||
cpal::host_from_id(cpal::HostId::Jack).unwrap(),
|
cpal::host_from_id(cpal::HostId::Jack).unwrap(),
|
||||||
device,
|
device,
|
||||||
|
@ -166,7 +170,12 @@ fn create_sink(
|
||||||
Ok((sink, stream))
|
Ok((sink, stream))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open(host: cpal::Host, device: Option<String>, format: AudioFormat, sample_rate: u32) -> RodioSink {
|
pub fn open(
|
||||||
|
host: cpal::Host,
|
||||||
|
device: Option<String>,
|
||||||
|
format: AudioFormat,
|
||||||
|
sample_rate: u32,
|
||||||
|
) -> RodioSink {
|
||||||
info!(
|
info!(
|
||||||
"Using Rodio sink with format {format:?} and cpal host: {}",
|
"Using Rodio sink with format {format:?} and cpal host: {}",
|
||||||
host.id().name()
|
host.id().name()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{mem, str::FromStr, time::Duration};
|
use std::{mem, str::FromStr, time::Duration};
|
||||||
|
|
||||||
pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer};
|
pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer};
|
||||||
use crate::{convert::i24, player::duration_to_coefficient, RESAMPLER_INPUT_SIZE, SAMPLE_RATE};
|
use crate::{convert::i24, RESAMPLER_INPUT_SIZE, SAMPLE_RATE};
|
||||||
|
|
||||||
// Reciprocals allow us to multiply instead of divide during interpolation.
|
// Reciprocals allow us to multiply instead of divide during interpolation.
|
||||||
const HZ48000_RESAMPLE_FACTOR_RECIPROCAL: f64 = SAMPLE_RATE as f64 / 48_000.0;
|
const HZ48000_RESAMPLE_FACTOR_RECIPROCAL: f64 = SAMPLE_RATE as f64 / 48_000.0;
|
||||||
|
@ -152,10 +152,12 @@ impl FromStr for SampleRate {
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
use SampleRate::*;
|
use SampleRate::*;
|
||||||
|
|
||||||
|
let lowercase_input = s.to_lowercase();
|
||||||
|
|
||||||
// Match against both the actual
|
// Match against both the actual
|
||||||
// stringified value and how most
|
// stringified value and how most
|
||||||
// humans would write a sample rate.
|
// humans would write a sample rate.
|
||||||
match s.to_uppercase().as_ref() {
|
match lowercase_input.as_str() {
|
||||||
"hz44100" | "44100hz" | "44100" | "44.1khz" => Ok(Hz44100),
|
"hz44100" | "44100hz" | "44100" | "44.1khz" => Ok(Hz44100),
|
||||||
"hz48000" | "48000hz" | "48000" | "48khz" => Ok(Hz48000),
|
"hz48000" | "48000hz" | "48000" | "48khz" => Ok(Hz48000),
|
||||||
"hz88200" | "88200hz" | "88200" | "88.2khz" => Ok(Hz88200),
|
"hz88200" | "88200hz" | "88200" | "88.2khz" => Ok(Hz88200),
|
||||||
|
@ -348,6 +350,9 @@ pub struct PlayerConfig {
|
||||||
pub gapless: bool,
|
pub gapless: bool,
|
||||||
pub passthrough: bool,
|
pub passthrough: bool,
|
||||||
|
|
||||||
|
pub interpolation_quality: InterpolationQuality,
|
||||||
|
pub sample_rate: SampleRate,
|
||||||
|
|
||||||
pub normalisation: bool,
|
pub normalisation: bool,
|
||||||
pub normalisation_type: NormalisationType,
|
pub normalisation_type: NormalisationType,
|
||||||
pub normalisation_method: NormalisationMethod,
|
pub normalisation_method: NormalisationMethod,
|
||||||
|
@ -368,12 +373,17 @@ impl Default for PlayerConfig {
|
||||||
bitrate: Bitrate::default(),
|
bitrate: Bitrate::default(),
|
||||||
gapless: true,
|
gapless: true,
|
||||||
normalisation: false,
|
normalisation: false,
|
||||||
|
interpolation_quality: InterpolationQuality::default(),
|
||||||
|
sample_rate: SampleRate::default(),
|
||||||
normalisation_type: NormalisationType::default(),
|
normalisation_type: NormalisationType::default(),
|
||||||
normalisation_method: NormalisationMethod::default(),
|
normalisation_method: NormalisationMethod::default(),
|
||||||
normalisation_pregain_db: 0.0,
|
normalisation_pregain_db: 0.0,
|
||||||
normalisation_threshold_dbfs: -2.0,
|
normalisation_threshold_dbfs: -2.0,
|
||||||
normalisation_attack_cf: duration_to_coefficient(Duration::from_millis(5)),
|
// Dummy value. We can't use the default because
|
||||||
normalisation_release_cf: duration_to_coefficient(Duration::from_millis(100)),
|
// no matter what it's dependent on the sample rate.
|
||||||
|
normalisation_attack_cf: 0.0,
|
||||||
|
// Same with release.
|
||||||
|
normalisation_release_cf: 0.0,
|
||||||
normalisation_knee_db: 5.0,
|
normalisation_knee_db: 5.0,
|
||||||
passthrough: false,
|
passthrough: false,
|
||||||
ditherer: Some(mk_ditherer::<TriangularDitherer>),
|
ditherer: Some(mk_ditherer::<TriangularDitherer>),
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::player::{db_to_ratio, ratio_to_db};
|
use crate::{db_to_ratio, ratio_to_db};
|
||||||
|
|
||||||
use super::mappings::{LogMapping, MappedCtrl, VolumeMapping};
|
use super::mappings::{LogMapping, MappedCtrl, VolumeMapping};
|
||||||
use super::{Mixer, MixerConfig, VolumeCtrl};
|
use super::{Mixer, MixerConfig, VolumeCtrl};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use super::VolumeCtrl;
|
use super::VolumeCtrl;
|
||||||
use crate::player::db_to_ratio;
|
use crate::db_to_ratio;
|
||||||
|
|
||||||
pub trait MappedCtrl {
|
pub trait MappedCtrl {
|
||||||
fn to_mapped(&self, volume: u16) -> f64;
|
fn to_mapped(&self, volume: u16) -> f64;
|
||||||
|
|
|
@ -13,12 +13,12 @@ pub trait Mixer: Send {
|
||||||
fn set_volume(&self, volume: u16);
|
fn set_volume(&self, volume: u16);
|
||||||
fn volume(&self) -> u16;
|
fn volume(&self) -> u16;
|
||||||
|
|
||||||
fn get_soft_volume(&self) -> Box<dyn VolumeGetter + Send> {
|
fn get_soft_volume(&self) -> Box<dyn VolumeGetter> {
|
||||||
Box::new(NoOpVolume)
|
Box::new(NoOpVolume)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait VolumeGetter {
|
pub trait VolumeGetter: Send {
|
||||||
fn attenuation_factor(&self) -> f64;
|
fn attenuation_factor(&self) -> f64;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ impl Mixer for SoftMixer {
|
||||||
.store(mapped_volume.to_bits(), Ordering::Relaxed)
|
.store(mapped_volume.to_bits(), Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_soft_volume(&self) -> Box<dyn VolumeGetter + Send> {
|
fn get_soft_volume(&self) -> Box<dyn VolumeGetter> {
|
||||||
Box::new(SoftVolume(self.volume.clone()))
|
Box::new(SoftVolume(self.volume.clone()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,22 +29,18 @@ use crate::{
|
||||||
READ_AHEAD_DURING_PLAYBACK,
|
READ_AHEAD_DURING_PLAYBACK,
|
||||||
},
|
},
|
||||||
audio_backend::Sink,
|
audio_backend::Sink,
|
||||||
config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig},
|
config::{Bitrate, PlayerConfig},
|
||||||
convert::Converter,
|
|
||||||
core::{util::SeqGenerator, Error, Session, SpotifyId},
|
core::{util::SeqGenerator, Error, Session, SpotifyId},
|
||||||
decoder::{AudioDecoder, AudioPacket, AudioPacketPosition, SymphoniaDecoder},
|
decoder::{AudioDecoder, AudioPacket, AudioPacketPosition, SymphoniaDecoder},
|
||||||
metadata::audio::{AudioFileFormat, AudioFiles, AudioItem},
|
metadata::audio::{AudioFileFormat, AudioFiles, AudioItem},
|
||||||
mixer::VolumeGetter,
|
mixer::VolumeGetter,
|
||||||
|
sample_pipeline::SamplePipeline,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "passthrough-decoder")]
|
#[cfg(feature = "passthrough-decoder")]
|
||||||
use crate::decoder::PassthroughDecoder;
|
use crate::decoder::PassthroughDecoder;
|
||||||
|
|
||||||
use crate::SAMPLES_PER_SECOND;
|
|
||||||
|
|
||||||
const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000;
|
const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000;
|
||||||
pub const DB_VOLTAGE_RATIO: f64 = 20.0;
|
|
||||||
pub const PCM_AT_0DBFS: f64 = 1.0;
|
|
||||||
|
|
||||||
// Spotify inserts a custom Ogg packet at the start with custom metadata values, that you would
|
// Spotify inserts a custom Ogg packet at the start with custom metadata values, that you would
|
||||||
// otherwise expect in Vorbis comments. This packet isn't well-formed and players may balk at it.
|
// otherwise expect in Vorbis comments. This packet isn't well-formed and players may balk at it.
|
||||||
|
@ -75,15 +71,10 @@ struct PlayerInternal {
|
||||||
|
|
||||||
state: PlayerState,
|
state: PlayerState,
|
||||||
preload: PlayerPreload,
|
preload: PlayerPreload,
|
||||||
sink: Box<dyn Sink>,
|
|
||||||
sink_status: SinkStatus,
|
sink_status: SinkStatus,
|
||||||
sink_event_callback: Option<SinkEventCallback>,
|
sink_event_callback: Option<SinkEventCallback>,
|
||||||
volume_getter: Box<dyn VolumeGetter + Send>,
|
sample_pipeline: SamplePipeline,
|
||||||
event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>,
|
event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>,
|
||||||
converter: Converter,
|
|
||||||
|
|
||||||
normalisation_integrator: f64,
|
|
||||||
normalisation_peak: f64,
|
|
||||||
|
|
||||||
auto_normalise_as_album: bool,
|
auto_normalise_as_album: bool,
|
||||||
|
|
||||||
|
@ -265,22 +256,6 @@ impl PlayerEvent {
|
||||||
|
|
||||||
pub type PlayerEventChannel = mpsc::UnboundedReceiver<PlayerEvent>;
|
pub type PlayerEventChannel = mpsc::UnboundedReceiver<PlayerEvent>;
|
||||||
|
|
||||||
pub fn db_to_ratio(db: f64) -> f64 {
|
|
||||||
f64::powf(10.0, db / DB_VOLTAGE_RATIO)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)]
|
#[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`.
|
||||||
|
@ -335,86 +310,13 @@ impl NormalisationData {
|
||||||
album_peak,
|
album_peak,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f64 {
|
|
||||||
if !config.normalisation {
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (gain_db, gain_peak) = if config.normalisation_type == NormalisationType::Album {
|
|
||||||
(data.album_gain_db, data.album_peak)
|
|
||||||
} else {
|
|
||||||
(data.track_gain_db, data.track_peak)
|
|
||||||
};
|
|
||||||
|
|
||||||
// As per the ReplayGain 1.0 & 2.0 (proposed) spec:
|
|
||||||
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification#Clipping_prevention
|
|
||||||
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Clipping_prevention
|
|
||||||
let normalisation_factor = if config.normalisation_method == NormalisationMethod::Basic {
|
|
||||||
// For Basic Normalisation, factor = min(ratio of (ReplayGain + PreGain), 1.0 / peak level).
|
|
||||||
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification#Peak_amplitude
|
|
||||||
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Peak_amplitude
|
|
||||||
// We then limit that to 1.0 as not to exceed dBFS (0.0 dB).
|
|
||||||
let factor = f64::min(
|
|
||||||
db_to_ratio(gain_db + config.normalisation_pregain_db),
|
|
||||||
PCM_AT_0DBFS / gain_peak,
|
|
||||||
);
|
|
||||||
|
|
||||||
if factor > PCM_AT_0DBFS {
|
|
||||||
info!(
|
|
||||||
"Lowering gain by {:.2} dB for the duration of this track to avoid potentially exceeding dBFS.",
|
|
||||||
ratio_to_db(factor)
|
|
||||||
);
|
|
||||||
|
|
||||||
PCM_AT_0DBFS
|
|
||||||
} else {
|
|
||||||
factor
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For Dynamic Normalisation it's up to the player to decide,
|
|
||||||
// factor = ratio of (ReplayGain + PreGain).
|
|
||||||
// We then let the dynamic limiter handle gain reduction.
|
|
||||||
let factor = db_to_ratio(gain_db + config.normalisation_pregain_db);
|
|
||||||
let threshold_ratio = db_to_ratio(config.normalisation_threshold_dbfs);
|
|
||||||
|
|
||||||
if factor > PCM_AT_0DBFS {
|
|
||||||
let factor_db = gain_db + config.normalisation_pregain_db;
|
|
||||||
let limiting_db = factor_db + config.normalisation_threshold_dbfs.abs();
|
|
||||||
|
|
||||||
warn!(
|
|
||||||
"This track may exceed dBFS by {:.2} dB and be subject to {:.2} dB of dynamic limiting at it's peak.",
|
|
||||||
factor_db, limiting_db
|
|
||||||
);
|
|
||||||
} else if factor > threshold_ratio {
|
|
||||||
let limiting_db = gain_db
|
|
||||||
+ config.normalisation_pregain_db
|
|
||||||
+ config.normalisation_threshold_dbfs.abs();
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"This track may be subject to {:.2} dB of dynamic limiting at it's peak.",
|
|
||||||
limiting_db
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
factor
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!("Normalisation Data: {:?}", data);
|
|
||||||
debug!(
|
|
||||||
"Calculated Normalisation Factor for {:?}: {:.2}%",
|
|
||||||
config.normalisation_type,
|
|
||||||
normalisation_factor * 100.0
|
|
||||||
);
|
|
||||||
|
|
||||||
normalisation_factor
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
pub fn new<F>(
|
pub fn new<F>(
|
||||||
config: PlayerConfig,
|
config: PlayerConfig,
|
||||||
session: Session,
|
session: Session,
|
||||||
volume_getter: Box<dyn VolumeGetter + Send>,
|
volume_getter: Box<dyn VolumeGetter>,
|
||||||
sink_builder: F,
|
sink_builder: F,
|
||||||
) -> Self
|
) -> Self
|
||||||
where
|
where
|
||||||
|
@ -422,32 +324,6 @@ impl Player {
|
||||||
{
|
{
|
||||||
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
|
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
if config.normalisation {
|
|
||||||
debug!("Normalisation Type: {:?}", config.normalisation_type);
|
|
||||||
debug!(
|
|
||||||
"Normalisation Pregain: {:.1} dB",
|
|
||||||
config.normalisation_pregain_db
|
|
||||||
);
|
|
||||||
debug!(
|
|
||||||
"Normalisation Threshold: {:.1} dBFS",
|
|
||||||
config.normalisation_threshold_dbfs
|
|
||||||
);
|
|
||||||
debug!("Normalisation Method: {:?}", config.normalisation_method);
|
|
||||||
|
|
||||||
if config.normalisation_method == NormalisationMethod::Dynamic {
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let player_id = PLAYER_COUNTER.fetch_add(1, Ordering::AcqRel);
|
let player_id = PLAYER_COUNTER.fetch_add(1, Ordering::AcqRel);
|
||||||
|
|
||||||
let thread_name = format!("player:{}", player_id);
|
let thread_name = format!("player:{}", player_id);
|
||||||
|
@ -455,7 +331,7 @@ impl Player {
|
||||||
let builder = thread::Builder::new().name(thread_name.clone());
|
let builder = thread::Builder::new().name(thread_name.clone());
|
||||||
|
|
||||||
let handle = match builder.spawn(move || {
|
let handle = match builder.spawn(move || {
|
||||||
let converter = Converter::new(config.ditherer);
|
let sample_pipeline = SamplePipeline::new(&config, sink_builder(), volume_getter);
|
||||||
|
|
||||||
let internal = PlayerInternal {
|
let internal = PlayerInternal {
|
||||||
session,
|
session,
|
||||||
|
@ -465,15 +341,10 @@ impl Player {
|
||||||
|
|
||||||
state: PlayerState::Stopped,
|
state: PlayerState::Stopped,
|
||||||
preload: PlayerPreload::None,
|
preload: PlayerPreload::None,
|
||||||
sink: sink_builder(),
|
|
||||||
sink_status: SinkStatus::Closed,
|
sink_status: SinkStatus::Closed,
|
||||||
sink_event_callback: None,
|
sink_event_callback: None,
|
||||||
volume_getter,
|
sample_pipeline,
|
||||||
event_senders: vec![],
|
event_senders: vec![],
|
||||||
converter,
|
|
||||||
|
|
||||||
normalisation_peak: 0.0,
|
|
||||||
normalisation_integrator: 0.0,
|
|
||||||
|
|
||||||
auto_normalise_as_album: false,
|
auto_normalise_as_album: false,
|
||||||
|
|
||||||
|
@ -685,7 +556,6 @@ enum PlayerState {
|
||||||
decoder: Decoder,
|
decoder: Decoder,
|
||||||
audio_item: AudioItem,
|
audio_item: AudioItem,
|
||||||
normalisation_data: NormalisationData,
|
normalisation_data: NormalisationData,
|
||||||
normalisation_factor: f64,
|
|
||||||
stream_loader_controller: StreamLoaderController,
|
stream_loader_controller: StreamLoaderController,
|
||||||
bytes_per_second: usize,
|
bytes_per_second: usize,
|
||||||
duration_ms: u32,
|
duration_ms: u32,
|
||||||
|
@ -699,7 +569,6 @@ enum PlayerState {
|
||||||
decoder: Decoder,
|
decoder: Decoder,
|
||||||
normalisation_data: NormalisationData,
|
normalisation_data: NormalisationData,
|
||||||
audio_item: AudioItem,
|
audio_item: AudioItem,
|
||||||
normalisation_factor: f64,
|
|
||||||
stream_loader_controller: StreamLoaderController,
|
stream_loader_controller: StreamLoaderController,
|
||||||
bytes_per_second: usize,
|
bytes_per_second: usize,
|
||||||
duration_ms: u32,
|
duration_ms: u32,
|
||||||
|
@ -810,7 +679,6 @@ impl PlayerState {
|
||||||
decoder,
|
decoder,
|
||||||
audio_item,
|
audio_item,
|
||||||
normalisation_data,
|
normalisation_data,
|
||||||
normalisation_factor,
|
|
||||||
stream_loader_controller,
|
stream_loader_controller,
|
||||||
duration_ms,
|
duration_ms,
|
||||||
bytes_per_second,
|
bytes_per_second,
|
||||||
|
@ -824,7 +692,6 @@ impl PlayerState {
|
||||||
decoder,
|
decoder,
|
||||||
audio_item,
|
audio_item,
|
||||||
normalisation_data,
|
normalisation_data,
|
||||||
normalisation_factor,
|
|
||||||
stream_loader_controller,
|
stream_loader_controller,
|
||||||
duration_ms,
|
duration_ms,
|
||||||
bytes_per_second,
|
bytes_per_second,
|
||||||
|
@ -855,7 +722,6 @@ impl PlayerState {
|
||||||
decoder,
|
decoder,
|
||||||
audio_item,
|
audio_item,
|
||||||
normalisation_data,
|
normalisation_data,
|
||||||
normalisation_factor,
|
|
||||||
stream_loader_controller,
|
stream_loader_controller,
|
||||||
duration_ms,
|
duration_ms,
|
||||||
bytes_per_second,
|
bytes_per_second,
|
||||||
|
@ -870,7 +736,6 @@ impl PlayerState {
|
||||||
decoder,
|
decoder,
|
||||||
audio_item,
|
audio_item,
|
||||||
normalisation_data,
|
normalisation_data,
|
||||||
normalisation_factor,
|
|
||||||
stream_loader_controller,
|
stream_loader_controller,
|
||||||
duration_ms,
|
duration_ms,
|
||||||
bytes_per_second,
|
bytes_per_second,
|
||||||
|
@ -1271,11 +1136,12 @@ impl Future for PlayerInternal {
|
||||||
if self.state.is_playing() {
|
if self.state.is_playing() {
|
||||||
self.ensure_sink_running();
|
self.ensure_sink_running();
|
||||||
|
|
||||||
|
let sample_pipeline_latency_ms = self.sample_pipeline.get_latency_ms();
|
||||||
|
|
||||||
if let PlayerState::Playing {
|
if let PlayerState::Playing {
|
||||||
track_id,
|
track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
ref mut decoder,
|
ref mut decoder,
|
||||||
normalisation_factor,
|
|
||||||
ref mut stream_position_ms,
|
ref mut stream_position_ms,
|
||||||
ref mut reported_nominal_start_time,
|
ref mut reported_nominal_start_time,
|
||||||
..
|
..
|
||||||
|
@ -1284,7 +1150,9 @@ impl Future for PlayerInternal {
|
||||||
match decoder.next_packet() {
|
match decoder.next_packet() {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
if let Some((ref packet_position, ref packet)) = result {
|
if let Some((ref packet_position, ref packet)) = result {
|
||||||
let new_stream_position_ms = packet_position.position_ms;
|
let new_stream_position_ms = packet_position
|
||||||
|
.position_ms
|
||||||
|
.saturating_sub(sample_pipeline_latency_ms);
|
||||||
let expected_position_ms = std::mem::replace(
|
let expected_position_ms = std::mem::replace(
|
||||||
&mut *stream_position_ms,
|
&mut *stream_position_ms,
|
||||||
new_stream_position_ms,
|
new_stream_position_ms,
|
||||||
|
@ -1357,7 +1225,7 @@ impl Future for PlayerInternal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.handle_packet(result, normalisation_factor);
|
self.handle_packet(result);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Skipping to next track, unable to get next packet for track <{:?}>: {:?}", track_id, e);
|
error!("Skipping to next track, unable to get next packet for track <{:?}>: {:?}", track_id, e);
|
||||||
|
@ -1423,7 +1291,7 @@ impl PlayerInternal {
|
||||||
if let Some(callback) = &mut self.sink_event_callback {
|
if let Some(callback) = &mut self.sink_event_callback {
|
||||||
callback(SinkStatus::Running);
|
callback(SinkStatus::Running);
|
||||||
}
|
}
|
||||||
match self.sink.start() {
|
match self.sample_pipeline.start() {
|
||||||
Ok(()) => self.sink_status = SinkStatus::Running,
|
Ok(()) => self.sink_status = SinkStatus::Running,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{}", e);
|
error!("{}", e);
|
||||||
|
@ -1437,7 +1305,7 @@ impl PlayerInternal {
|
||||||
match self.sink_status {
|
match self.sink_status {
|
||||||
SinkStatus::Running => {
|
SinkStatus::Running => {
|
||||||
trace!("== Stopping sink ==");
|
trace!("== Stopping sink ==");
|
||||||
match self.sink.stop() {
|
match self.sample_pipeline.stop() {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.sink_status = if temporarily {
|
self.sink_status = if temporarily {
|
||||||
SinkStatus::TemporarilyClosed
|
SinkStatus::TemporarilyClosed
|
||||||
|
@ -1557,132 +1425,16 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_packet(
|
fn handle_packet(&mut self, packet: Option<(AudioPacketPosition, AudioPacket)>) {
|
||||||
&mut self,
|
|
||||||
packet: Option<(AudioPacketPosition, AudioPacket)>,
|
|
||||||
normalisation_factor: f64,
|
|
||||||
) {
|
|
||||||
match packet {
|
match packet {
|
||||||
Some((_, mut packet)) => {
|
Some((_, packet)) => {
|
||||||
if !packet.is_empty() {
|
if !packet.is_empty() {
|
||||||
if let AudioPacket::Samples(ref mut data) = packet {
|
if let Err(e) = self.sample_pipeline.write(packet) {
|
||||||
// Get the volume for the packet.
|
|
||||||
// In the case of hardware volume control this will
|
|
||||||
// always be 1.0 (no change).
|
|
||||||
let volume = self.volume_getter.attenuation_factor();
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
// No matter the case we apply volume attenuation last if there is any.
|
|
||||||
if !self.config.normalisation {
|
|
||||||
if volume < 1.0 {
|
|
||||||
for sample in data.iter_mut() {
|
|
||||||
*sample *= volume;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if self.config.normalisation_method == NormalisationMethod::Basic
|
|
||||||
&& (normalisation_factor < 1.0 || volume < 1.0)
|
|
||||||
{
|
|
||||||
for sample in data.iter_mut() {
|
|
||||||
*sample *= normalisation_factor * volume;
|
|
||||||
}
|
|
||||||
} else if self.config.normalisation_method == NormalisationMethod::Dynamic {
|
|
||||||
// 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() {
|
|
||||||
*sample *= normalisation_factor;
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
// Some tracks have samples that are precisely 0.0. That's silence
|
|
||||||
// and we know we don't need to limit that, in which we can spare
|
|
||||||
// the CPU cycles.
|
|
||||||
//
|
|
||||||
// Also, calling `ratio_to_db(0.0)` returns `inf` and would get the
|
|
||||||
// peak detector stuck. Also catch the unlikely case where a sample
|
|
||||||
// is decoded as `NaN` or some other non-normal value.
|
|
||||||
let limiter_db = if sample.is_normal() {
|
|
||||||
// step 1-4: half-wave rectification and conversion into dB
|
|
||||||
// and gain computer with soft knee and subtractor
|
|
||||||
let bias_db = ratio_to_db(sample.abs()) - threshold_db;
|
|
||||||
let knee_boundary_db = bias_db * 2.0;
|
|
||||||
|
|
||||||
if knee_boundary_db < -knee_db {
|
|
||||||
0.0
|
|
||||||
} else if knee_boundary_db.abs() <= knee_db {
|
|
||||||
// The textbook equation:
|
|
||||||
// ratio_to_db(sample.abs()) - (ratio_to_db(sample.abs()) - (bias_db + knee_db / 2.0).powi(2) / (2.0 * knee_db))
|
|
||||||
// Simplifies to:
|
|
||||||
// ((2.0 * bias_db) + knee_db).powi(2) / (8.0 * knee_db)
|
|
||||||
// Which in our case further simplifies to:
|
|
||||||
// (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
|
|
||||||
// because knee_boundary_db is 2.0 * bias_db.
|
|
||||||
(knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
|
|
||||||
} else {
|
|
||||||
// Textbook:
|
|
||||||
// ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db.
|
|
||||||
bias_db
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Spare the CPU unless (1) the limiter is engaged, (2) we
|
|
||||||
// were in attack or (3) we were in release, and that attack/
|
|
||||||
// release wasn't finished yet.
|
|
||||||
if limiter_db > 0.0
|
|
||||||
|| self.normalisation_integrator > 0.0
|
|
||||||
|| self.normalisation_peak > 0.0
|
|
||||||
{
|
|
||||||
// step 5: smooth, decoupled peak detector
|
|
||||||
// Textbook:
|
|
||||||
// release_cf * self.normalisation_integrator + (1.0 - release_cf) * limiter_db
|
|
||||||
// Simplifies to:
|
|
||||||
// release_cf * self.normalisation_integrator - release_cf * limiter_db + limiter_db
|
|
||||||
self.normalisation_integrator = f64::max(
|
|
||||||
limiter_db,
|
|
||||||
release_cf * self.normalisation_integrator
|
|
||||||
- release_cf * limiter_db
|
|
||||||
+ limiter_db,
|
|
||||||
);
|
|
||||||
// Textbook:
|
|
||||||
// attack_cf * self.normalisation_peak + (1.0 - attack_cf) * self.normalisation_integrator
|
|
||||||
// Simplifies to:
|
|
||||||
// attack_cf * self.normalisation_peak - attack_cf * self.normalisation_integrator + self.normalisation_integrator
|
|
||||||
self.normalisation_peak = attack_cf * self.normalisation_peak
|
|
||||||
- attack_cf * self.normalisation_integrator
|
|
||||||
+ 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 *= volume;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = self.sink.write(packet, &mut self.converter) {
|
|
||||||
error!("{}", e);
|
error!("{}", e);
|
||||||
self.handle_pause();
|
self.handle_pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None => {
|
None => {
|
||||||
self.state.playing_to_end_of_track();
|
self.state.playing_to_end_of_track();
|
||||||
if let PlayerState::EndOfTrack {
|
if let PlayerState::EndOfTrack {
|
||||||
|
@ -1716,16 +1468,10 @@ impl PlayerInternal {
|
||||||
|
|
||||||
let position_ms = loaded_track.stream_position_ms;
|
let position_ms = loaded_track.stream_position_ms;
|
||||||
|
|
||||||
let mut config = self.config.clone();
|
self.sample_pipeline.set_normalisation_factor(
|
||||||
if config.normalisation_type == NormalisationType::Auto {
|
self.auto_normalise_as_album,
|
||||||
if self.auto_normalise_as_album {
|
loaded_track.normalisation_data,
|
||||||
config.normalisation_type = NormalisationType::Album;
|
);
|
||||||
} else {
|
|
||||||
config.normalisation_type = NormalisationType::Track;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let normalisation_factor =
|
|
||||||
NormalisationData::get_factor(&config, loaded_track.normalisation_data);
|
|
||||||
|
|
||||||
if start_playback {
|
if start_playback {
|
||||||
self.ensure_sink_running();
|
self.ensure_sink_running();
|
||||||
|
@ -1741,7 +1487,6 @@ impl PlayerInternal {
|
||||||
decoder: loaded_track.decoder,
|
decoder: loaded_track.decoder,
|
||||||
audio_item: loaded_track.audio_item,
|
audio_item: loaded_track.audio_item,
|
||||||
normalisation_data: loaded_track.normalisation_data,
|
normalisation_data: loaded_track.normalisation_data,
|
||||||
normalisation_factor,
|
|
||||||
stream_loader_controller: loaded_track.stream_loader_controller,
|
stream_loader_controller: loaded_track.stream_loader_controller,
|
||||||
duration_ms: loaded_track.duration_ms,
|
duration_ms: loaded_track.duration_ms,
|
||||||
bytes_per_second: loaded_track.bytes_per_second,
|
bytes_per_second: loaded_track.bytes_per_second,
|
||||||
|
@ -1760,7 +1505,6 @@ impl PlayerInternal {
|
||||||
decoder: loaded_track.decoder,
|
decoder: loaded_track.decoder,
|
||||||
audio_item: loaded_track.audio_item,
|
audio_item: loaded_track.audio_item,
|
||||||
normalisation_data: loaded_track.normalisation_data,
|
normalisation_data: loaded_track.normalisation_data,
|
||||||
normalisation_factor,
|
|
||||||
stream_loader_controller: loaded_track.stream_loader_controller,
|
stream_loader_controller: loaded_track.stream_loader_controller,
|
||||||
duration_ms: loaded_track.duration_ms,
|
duration_ms: loaded_track.duration_ms,
|
||||||
bytes_per_second: loaded_track.bytes_per_second,
|
bytes_per_second: loaded_track.bytes_per_second,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
MS_PER_PAGE,
|
|
||||||
audio_backend::{Sink, SinkResult},
|
audio_backend::{Sink, SinkResult},
|
||||||
config::PlayerConfig,
|
config::PlayerConfig,
|
||||||
convert::Converter,
|
convert::Converter,
|
||||||
|
@ -8,6 +7,7 @@ use crate::{
|
||||||
normaliser::Normaliser,
|
normaliser::Normaliser,
|
||||||
player::NormalisationData,
|
player::NormalisationData,
|
||||||
resampler::StereoInterleavedResampler,
|
resampler::StereoInterleavedResampler,
|
||||||
|
MS_PER_PAGE,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct SamplePipeline {
|
pub struct SamplePipeline {
|
||||||
|
|
104
src/main.rs
104
src/main.rs
|
@ -24,11 +24,12 @@ use librespot::{
|
||||||
playback::{
|
playback::{
|
||||||
audio_backend::{self, SinkBuilder, BACKENDS},
|
audio_backend::{self, SinkBuilder, BACKENDS},
|
||||||
config::{
|
config::{
|
||||||
AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl,
|
AudioFormat, Bitrate, InterpolationQuality, NormalisationMethod, NormalisationType,
|
||||||
|
PlayerConfig, SampleRate, VolumeCtrl,
|
||||||
},
|
},
|
||||||
dither,
|
dither,
|
||||||
mixer::{self, MixerConfig, MixerFn},
|
mixer::{self, MixerConfig, MixerFn},
|
||||||
player::{coefficient_to_duration, duration_to_coefficient, Player},
|
player::Player,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -239,6 +240,8 @@ fn get_setup() -> Setup {
|
||||||
const VOLUME_RANGE: &str = "volume-range";
|
const VOLUME_RANGE: &str = "volume-range";
|
||||||
const ZEROCONF_PORT: &str = "zeroconf-port";
|
const ZEROCONF_PORT: &str = "zeroconf-port";
|
||||||
const ZEROCONF_INTERFACE: &str = "zeroconf-interface";
|
const ZEROCONF_INTERFACE: &str = "zeroconf-interface";
|
||||||
|
const INTERPOLATION_QUALITY: &str = "interpolation-quality";
|
||||||
|
const SAMPLE_RATE: &str = "sample-rate";
|
||||||
|
|
||||||
// Mostly arbitrary.
|
// Mostly arbitrary.
|
||||||
const AP_PORT_SHORT: &str = "a";
|
const AP_PORT_SHORT: &str = "a";
|
||||||
|
@ -576,6 +579,16 @@ fn get_setup() -> Setup {
|
||||||
ZEROCONF_INTERFACE,
|
ZEROCONF_INTERFACE,
|
||||||
"Comma-separated interface IP addresses on which zeroconf will bind. Defaults to all interfaces. Ignored by DNS-SD.",
|
"Comma-separated interface IP addresses on which zeroconf will bind. Defaults to all interfaces. Ignored by DNS-SD.",
|
||||||
"IP"
|
"IP"
|
||||||
|
).optopt(
|
||||||
|
"",
|
||||||
|
INTERPOLATION_QUALITY,
|
||||||
|
"Interpolation Quality to use if Resampling {Low|Medium|High}. Defaults to Low.",
|
||||||
|
"QUALITY"
|
||||||
|
).optopt(
|
||||||
|
"",
|
||||||
|
SAMPLE_RATE,
|
||||||
|
"Sample Rate to Resample to {44.1kHz|48kHz|88.2kHz|96kHz}. Defaults to 44.1kHz meaning no resampling.",
|
||||||
|
"SAMPLERATE"
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(feature = "passthrough-decoder")]
|
#[cfg(feature = "passthrough-decoder")]
|
||||||
|
@ -732,10 +745,18 @@ fn get_setup() -> Setup {
|
||||||
|
|
||||||
let invalid_error_msg =
|
let invalid_error_msg =
|
||||||
|long: &str, short: &str, invalid: &str, valid_values: &str, default_value: &str| {
|
|long: &str, short: &str, invalid: &str, valid_values: &str, default_value: &str| {
|
||||||
error!("Invalid `--{long}` / `-{short}`: \"{invalid}\"");
|
if short.is_empty() {
|
||||||
|
error!("Invalid `--{long}`: \"{invalid}\"");
|
||||||
|
} else {
|
||||||
|
error!("Invalid `--{long}` / `-{short}`: \"{invalid}\"");
|
||||||
|
}
|
||||||
|
|
||||||
if !valid_values.is_empty() {
|
if !valid_values.is_empty() {
|
||||||
println!("Valid `--{long}` / `-{short}` values: {valid_values}");
|
if short.is_empty() {
|
||||||
|
println!("Valid `--{long}` values: {valid_values}");
|
||||||
|
} else {
|
||||||
|
println!("Valid `--{long}` / `-{short}` values: {valid_values}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !default_value.is_empty() {
|
if !default_value.is_empty() {
|
||||||
|
@ -761,6 +782,42 @@ fn get_setup() -> Setup {
|
||||||
exit(1);
|
exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let interpolation_quality = opt_str(INTERPOLATION_QUALITY)
|
||||||
|
.as_deref()
|
||||||
|
.map(|interpolation_quality| {
|
||||||
|
InterpolationQuality::from_str(interpolation_quality).unwrap_or_else(|_| {
|
||||||
|
let default_value = &format!("{}", InterpolationQuality::default());
|
||||||
|
invalid_error_msg(
|
||||||
|
INTERPOLATION_QUALITY,
|
||||||
|
"",
|
||||||
|
interpolation_quality,
|
||||||
|
"Low, Medium, High",
|
||||||
|
default_value,
|
||||||
|
);
|
||||||
|
|
||||||
|
exit(1);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let sample_rate = opt_str(SAMPLE_RATE)
|
||||||
|
.as_deref()
|
||||||
|
.map(|sample_rate| {
|
||||||
|
SampleRate::from_str(sample_rate).unwrap_or_else(|_| {
|
||||||
|
let default_value = &format!("{}", SampleRate::default());
|
||||||
|
invalid_error_msg(
|
||||||
|
SAMPLE_RATE,
|
||||||
|
"",
|
||||||
|
sample_rate,
|
||||||
|
"44.1kHz, 48kHz, 88.2kHz, 96kHz",
|
||||||
|
default_value,
|
||||||
|
);
|
||||||
|
|
||||||
|
exit(1);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let format = opt_str(FORMAT)
|
let format = opt_str(FORMAT)
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(|format| {
|
.map(|format| {
|
||||||
|
@ -782,7 +839,7 @@ fn get_setup() -> Setup {
|
||||||
let device = opt_str(DEVICE);
|
let device = opt_str(DEVICE);
|
||||||
if let Some(ref value) = device {
|
if let Some(ref value) = device {
|
||||||
if value == "?" {
|
if value == "?" {
|
||||||
backend(device, format);
|
backend(device, format, sample_rate.as_u32());
|
||||||
exit(0);
|
exit(0);
|
||||||
} else if value.is_empty() {
|
} else if value.is_empty() {
|
||||||
empty_string_error_msg(DEVICE, DEVICE_SHORT);
|
empty_string_error_msg(DEVICE, DEVICE_SHORT);
|
||||||
|
@ -1491,9 +1548,8 @@ fn get_setup() -> Setup {
|
||||||
|
|
||||||
normalisation_attack_cf = 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) => sample_rate
|
||||||
duration_to_coefficient(Duration::from_millis(value))
|
.duration_to_normalisation_coefficient(Duration::from_millis(value)),
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
let valid_values = &format!(
|
let valid_values = &format!(
|
||||||
"{} - {}",
|
"{} - {}",
|
||||||
|
@ -1506,7 +1562,10 @@ fn get_setup() -> Setup {
|
||||||
NORMALISATION_ATTACK_SHORT,
|
NORMALISATION_ATTACK_SHORT,
|
||||||
&attack,
|
&attack,
|
||||||
valid_values,
|
valid_values,
|
||||||
&coefficient_to_duration(player_default_config.normalisation_attack_cf)
|
&sample_rate
|
||||||
|
.normalisation_coefficient_to_duration(
|
||||||
|
player_default_config.normalisation_attack_cf,
|
||||||
|
)
|
||||||
.as_millis()
|
.as_millis()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
@ -1514,12 +1573,15 @@ fn get_setup() -> Setup {
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or(player_default_config.normalisation_attack_cf);
|
.unwrap_or(
|
||||||
|
sample_rate.duration_to_normalisation_coefficient(Duration::from_millis(5)),
|
||||||
|
);
|
||||||
|
|
||||||
normalisation_release_cf = 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_to_coefficient(Duration::from_millis(value))
|
sample_rate
|
||||||
|
.duration_to_normalisation_coefficient(Duration::from_millis(value))
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let valid_values = &format!(
|
let valid_values = &format!(
|
||||||
|
@ -1533,17 +1595,20 @@ fn get_setup() -> Setup {
|
||||||
NORMALISATION_RELEASE_SHORT,
|
NORMALISATION_RELEASE_SHORT,
|
||||||
&release,
|
&release,
|
||||||
valid_values,
|
valid_values,
|
||||||
&coefficient_to_duration(
|
&sample_rate
|
||||||
player_default_config.normalisation_release_cf,
|
.normalisation_coefficient_to_duration(
|
||||||
)
|
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_cf);
|
.unwrap_or(
|
||||||
|
sample_rate.duration_to_normalisation_coefficient(Duration::from_millis(100)),
|
||||||
|
);
|
||||||
|
|
||||||
normalisation_knee_db = opt_str(NORMALISATION_KNEE)
|
normalisation_knee_db = opt_str(NORMALISATION_KNEE)
|
||||||
.map(|knee| match knee.parse::<f64>() {
|
.map(|knee| match knee.parse::<f64>() {
|
||||||
|
@ -1608,6 +1673,8 @@ fn get_setup() -> Setup {
|
||||||
bitrate,
|
bitrate,
|
||||||
gapless,
|
gapless,
|
||||||
passthrough,
|
passthrough,
|
||||||
|
interpolation_quality,
|
||||||
|
sample_rate,
|
||||||
normalisation,
|
normalisation,
|
||||||
normalisation_type,
|
normalisation_type,
|
||||||
normalisation_method,
|
normalisation_method,
|
||||||
|
@ -1734,8 +1801,9 @@ async fn main() {
|
||||||
let format = setup.format;
|
let format = setup.format;
|
||||||
let backend = setup.backend;
|
let backend = setup.backend;
|
||||||
let device = setup.device.clone();
|
let device = setup.device.clone();
|
||||||
|
let sample_rate = player_config.sample_rate.as_u32();
|
||||||
let player = Player::new(player_config, session.clone(), soft_volume, move || {
|
let player = Player::new(player_config, session.clone(), soft_volume, move || {
|
||||||
(backend)(device, format)
|
(backend)(device, format, sample_rate)
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(player_event_program) = setup.player_event_program.clone() {
|
if let Some(player_event_program) = setup.player_event_program.clone() {
|
||||||
|
|
Loading…
Reference in a new issue