From efec96b9cc2f8dab49b67aeae62c8dcd2a44331f Mon Sep 17 00:00:00 2001
From: JasonLG1979 <jasonlevigray3@gmail.com>
Date: Wed, 21 Jun 2023 23:32:03 -0500
Subject: [PATCH] Put it all together

---
 playback/src/audio_backend/gstreamer.rs |   4 +-
 playback/src/audio_backend/jackaudio.rs |   5 +-
 playback/src/audio_backend/portaudio.rs |  12 +-
 playback/src/audio_backend/rodio.rs     |  13 +-
 playback/src/config.rs                  |  18 +-
 playback/src/mixer/alsamixer.rs         |   2 +-
 playback/src/mixer/mappings.rs          |   2 +-
 playback/src/mixer/mod.rs               |   4 +-
 playback/src/mixer/softmixer.rs         |   2 +-
 playback/src/player.rs                  | 298 ++----------------------
 playback/src/sample_pipeline.rs         |   2 +-
 src/main.rs                             | 104 +++++++--
 12 files changed, 152 insertions(+), 314 deletions(-)

diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs
index 5651211e..ec748a98 100644
--- a/playback/src/audio_backend/gstreamer.rs
+++ b/playback/src/audio_backend/gstreamer.rs
@@ -13,9 +13,7 @@ use std::sync::Arc;
 
 use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
 
-use crate::{
-    config::AudioFormat, convert::Converter, decoder::AudioPacket, NUM_CHANNELS,
-};
+use crate::{config::AudioFormat, convert::Converter, decoder::AudioPacket, NUM_CHANNELS};
 
 pub struct GstreamerSink {
     appsrc: gst_app::AppSrc,
diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs
index 180040e6..1122a427 100644
--- a/playback/src/audio_backend/jackaudio.rs
+++ b/playback/src/audio_backend/jackaudio.rs
@@ -42,7 +42,10 @@ impl Open for JackSink {
         if format != AudioFormat::F32 {
             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, _status) =
diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs
index eb455b0f..f1978a0d 100644
--- a/playback/src/audio_backend/portaudio.rs
+++ b/playback/src/audio_backend/portaudio.rs
@@ -119,9 +119,15 @@ impl<'a> Sink for PortAudioSink<'a> {
         }
 
         match self {
-            Self::F32(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),
+            Self::F32(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(())
diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs
index 4b63d352..05a060cb 100644
--- a/playback/src/audio_backend/rodio.rs
+++ b/playback/src/audio_backend/rodio.rs
@@ -23,7 +23,11 @@ pub fn mk_rodio(device: Option<String>, format: AudioFormat, sample_rate: u32) -
 }
 
 #[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(
         cpal::host_from_id(cpal::HostId::Jack).unwrap(),
         device,
@@ -166,7 +170,12 @@ fn create_sink(
     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!(
         "Using Rodio sink with format {format:?} and cpal host: {}",
         host.id().name()
diff --git a/playback/src/config.rs b/playback/src/config.rs
index f90cd4ce..952e43bf 100644
--- a/playback/src/config.rs
+++ b/playback/src/config.rs
@@ -1,7 +1,7 @@
 use std::{mem, str::FromStr, time::Duration};
 
 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.
 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> {
         use SampleRate::*;
 
+        let lowercase_input = s.to_lowercase();
+
         // Match against both the actual
         // stringified value and how most
         // humans would write a sample rate.
-        match s.to_uppercase().as_ref() {
+        match lowercase_input.as_str() {
             "hz44100" | "44100hz" | "44100" | "44.1khz" => Ok(Hz44100),
             "hz48000" | "48000hz" | "48000" | "48khz" => Ok(Hz48000),
             "hz88200" | "88200hz" | "88200" | "88.2khz" => Ok(Hz88200),
@@ -348,6 +350,9 @@ pub struct PlayerConfig {
     pub gapless: bool,
     pub passthrough: bool,
 
+    pub interpolation_quality: InterpolationQuality,
+    pub sample_rate: SampleRate,
+
     pub normalisation: bool,
     pub normalisation_type: NormalisationType,
     pub normalisation_method: NormalisationMethod,
@@ -368,12 +373,17 @@ impl Default for PlayerConfig {
             bitrate: Bitrate::default(),
             gapless: true,
             normalisation: false,
+            interpolation_quality: InterpolationQuality::default(),
+            sample_rate: SampleRate::default(),
             normalisation_type: NormalisationType::default(),
             normalisation_method: NormalisationMethod::default(),
             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)),
+            // Dummy value. We can't use the default because
+            // 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,
             passthrough: false,
             ditherer: Some(mk_ditherer::<TriangularDitherer>),
diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs
index 52be1085..17f996d8 100644
--- a/playback/src/mixer/alsamixer.rs
+++ b/playback/src/mixer/alsamixer.rs
@@ -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::{Mixer, MixerConfig, VolumeCtrl};
diff --git a/playback/src/mixer/mappings.rs b/playback/src/mixer/mappings.rs
index 736b3c3f..38290d5e 100644
--- a/playback/src/mixer/mappings.rs
+++ b/playback/src/mixer/mappings.rs
@@ -1,5 +1,5 @@
 use super::VolumeCtrl;
-use crate::player::db_to_ratio;
+use crate::db_to_ratio;
 
 pub trait MappedCtrl {
     fn to_mapped(&self, volume: u16) -> f64;
diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs
index 0a8b8d6c..2d89d30e 100644
--- a/playback/src/mixer/mod.rs
+++ b/playback/src/mixer/mod.rs
@@ -13,12 +13,12 @@ pub trait Mixer: Send {
     fn set_volume(&self, volume: 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)
     }
 }
 
-pub trait VolumeGetter {
+pub trait VolumeGetter: Send {
     fn attenuation_factor(&self) -> f64;
 }
 
diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs
index 061f39b9..2f7d21f7 100644
--- a/playback/src/mixer/softmixer.rs
+++ b/playback/src/mixer/softmixer.rs
@@ -35,7 +35,7 @@ impl Mixer for SoftMixer {
             .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()))
     }
 }
diff --git a/playback/src/player.rs b/playback/src/player.rs
index 96bd7081..f50e72ef 100644
--- a/playback/src/player.rs
+++ b/playback/src/player.rs
@@ -29,22 +29,18 @@ use crate::{
         READ_AHEAD_DURING_PLAYBACK,
     },
     audio_backend::Sink,
-    config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig},
-    convert::Converter,
+    config::{Bitrate, PlayerConfig},
     core::{util::SeqGenerator, Error, Session, SpotifyId},
     decoder::{AudioDecoder, AudioPacket, AudioPacketPosition, SymphoniaDecoder},
     metadata::audio::{AudioFileFormat, AudioFiles, AudioItem},
     mixer::VolumeGetter,
+    sample_pipeline::SamplePipeline,
 };
 
 #[cfg(feature = "passthrough-decoder")]
 use crate::decoder::PassthroughDecoder;
 
-use crate::SAMPLES_PER_SECOND;
-
 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
 // 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,
     preload: PlayerPreload,
-    sink: Box<dyn Sink>,
     sink_status: SinkStatus,
     sink_event_callback: Option<SinkEventCallback>,
-    volume_getter: Box<dyn VolumeGetter + Send>,
+    sample_pipeline: SamplePipeline,
     event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>,
-    converter: Converter,
-
-    normalisation_integrator: f64,
-    normalisation_peak: f64,
 
     auto_normalise_as_album: bool,
 
@@ -265,22 +256,6 @@ impl 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)]
 pub struct NormalisationData {
     // Spotify provides these as `f32`, but audio metadata can contain up to `f64`.
@@ -335,86 +310,13 @@ impl NormalisationData {
             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 {
     pub fn new<F>(
         config: PlayerConfig,
         session: Session,
-        volume_getter: Box<dyn VolumeGetter + Send>,
+        volume_getter: Box<dyn VolumeGetter>,
         sink_builder: F,
     ) -> Self
     where
@@ -422,32 +324,6 @@ impl Player {
     {
         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 thread_name = format!("player:{}", player_id);
@@ -455,7 +331,7 @@ impl Player {
         let builder = thread::Builder::new().name(thread_name.clone());
 
         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 {
                 session,
@@ -465,15 +341,10 @@ impl Player {
 
                 state: PlayerState::Stopped,
                 preload: PlayerPreload::None,
-                sink: sink_builder(),
                 sink_status: SinkStatus::Closed,
                 sink_event_callback: None,
-                volume_getter,
+                sample_pipeline,
                 event_senders: vec![],
-                converter,
-
-                normalisation_peak: 0.0,
-                normalisation_integrator: 0.0,
 
                 auto_normalise_as_album: false,
 
@@ -685,7 +556,6 @@ enum PlayerState {
         decoder: Decoder,
         audio_item: AudioItem,
         normalisation_data: NormalisationData,
-        normalisation_factor: f64,
         stream_loader_controller: StreamLoaderController,
         bytes_per_second: usize,
         duration_ms: u32,
@@ -699,7 +569,6 @@ enum PlayerState {
         decoder: Decoder,
         normalisation_data: NormalisationData,
         audio_item: AudioItem,
-        normalisation_factor: f64,
         stream_loader_controller: StreamLoaderController,
         bytes_per_second: usize,
         duration_ms: u32,
@@ -810,7 +679,6 @@ impl PlayerState {
                 decoder,
                 audio_item,
                 normalisation_data,
-                normalisation_factor,
                 stream_loader_controller,
                 duration_ms,
                 bytes_per_second,
@@ -824,7 +692,6 @@ impl PlayerState {
                     decoder,
                     audio_item,
                     normalisation_data,
-                    normalisation_factor,
                     stream_loader_controller,
                     duration_ms,
                     bytes_per_second,
@@ -855,7 +722,6 @@ impl PlayerState {
                 decoder,
                 audio_item,
                 normalisation_data,
-                normalisation_factor,
                 stream_loader_controller,
                 duration_ms,
                 bytes_per_second,
@@ -870,7 +736,6 @@ impl PlayerState {
                     decoder,
                     audio_item,
                     normalisation_data,
-                    normalisation_factor,
                     stream_loader_controller,
                     duration_ms,
                     bytes_per_second,
@@ -1271,11 +1136,12 @@ impl Future for PlayerInternal {
             if self.state.is_playing() {
                 self.ensure_sink_running();
 
+                let sample_pipeline_latency_ms = self.sample_pipeline.get_latency_ms();
+
                 if let PlayerState::Playing {
                     track_id,
                     play_request_id,
                     ref mut decoder,
-                    normalisation_factor,
                     ref mut stream_position_ms,
                     ref mut reported_nominal_start_time,
                     ..
@@ -1284,7 +1150,9 @@ impl Future for PlayerInternal {
                     match decoder.next_packet() {
                         Ok(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(
                                     &mut *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) => {
                             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 {
                 callback(SinkStatus::Running);
             }
-            match self.sink.start() {
+            match self.sample_pipeline.start() {
                 Ok(()) => self.sink_status = SinkStatus::Running,
                 Err(e) => {
                     error!("{}", e);
@@ -1437,7 +1305,7 @@ impl PlayerInternal {
         match self.sink_status {
             SinkStatus::Running => {
                 trace!("== Stopping sink ==");
-                match self.sink.stop() {
+                match self.sample_pipeline.stop() {
                     Ok(()) => {
                         self.sink_status = if temporarily {
                             SinkStatus::TemporarilyClosed
@@ -1557,132 +1425,16 @@ impl PlayerInternal {
         }
     }
 
-    fn handle_packet(
-        &mut self,
-        packet: Option<(AudioPacketPosition, AudioPacket)>,
-        normalisation_factor: f64,
-    ) {
+    fn handle_packet(&mut self, packet: Option<(AudioPacketPosition, AudioPacket)>) {
         match packet {
-            Some((_, mut packet)) => {
+            Some((_, packet)) => {
                 if !packet.is_empty() {
-                    if let AudioPacket::Samples(ref mut data) = 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) {
+                    if let Err(e) = self.sample_pipeline.write(packet) {
                         error!("{}", e);
                         self.handle_pause();
                     }
                 }
             }
-
             None => {
                 self.state.playing_to_end_of_track();
                 if let PlayerState::EndOfTrack {
@@ -1716,16 +1468,10 @@ impl PlayerInternal {
 
         let position_ms = loaded_track.stream_position_ms;
 
-        let mut config = self.config.clone();
-        if config.normalisation_type == NormalisationType::Auto {
-            if self.auto_normalise_as_album {
-                config.normalisation_type = NormalisationType::Album;
-            } else {
-                config.normalisation_type = NormalisationType::Track;
-            }
-        };
-        let normalisation_factor =
-            NormalisationData::get_factor(&config, loaded_track.normalisation_data);
+        self.sample_pipeline.set_normalisation_factor(
+            self.auto_normalise_as_album,
+            loaded_track.normalisation_data,
+        );
 
         if start_playback {
             self.ensure_sink_running();
@@ -1741,7 +1487,6 @@ impl PlayerInternal {
                 decoder: loaded_track.decoder,
                 audio_item: loaded_track.audio_item,
                 normalisation_data: loaded_track.normalisation_data,
-                normalisation_factor,
                 stream_loader_controller: loaded_track.stream_loader_controller,
                 duration_ms: loaded_track.duration_ms,
                 bytes_per_second: loaded_track.bytes_per_second,
@@ -1760,7 +1505,6 @@ impl PlayerInternal {
                 decoder: loaded_track.decoder,
                 audio_item: loaded_track.audio_item,
                 normalisation_data: loaded_track.normalisation_data,
-                normalisation_factor,
                 stream_loader_controller: loaded_track.stream_loader_controller,
                 duration_ms: loaded_track.duration_ms,
                 bytes_per_second: loaded_track.bytes_per_second,
diff --git a/playback/src/sample_pipeline.rs b/playback/src/sample_pipeline.rs
index 34a71fc1..e534bea7 100644
--- a/playback/src/sample_pipeline.rs
+++ b/playback/src/sample_pipeline.rs
@@ -1,5 +1,4 @@
 use crate::{
-    MS_PER_PAGE,
     audio_backend::{Sink, SinkResult},
     config::PlayerConfig,
     convert::Converter,
@@ -8,6 +7,7 @@ use crate::{
     normaliser::Normaliser,
     player::NormalisationData,
     resampler::StereoInterleavedResampler,
+    MS_PER_PAGE,
 };
 
 pub struct SamplePipeline {
diff --git a/src/main.rs b/src/main.rs
index 48edf1c9..24ab4b3c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -24,11 +24,12 @@ use librespot::{
     playback::{
         audio_backend::{self, SinkBuilder, BACKENDS},
         config::{
-            AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl,
+            AudioFormat, Bitrate, InterpolationQuality, NormalisationMethod, NormalisationType,
+            PlayerConfig, SampleRate, VolumeCtrl,
         },
         dither,
         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 ZEROCONF_PORT: &str = "zeroconf-port";
     const ZEROCONF_INTERFACE: &str = "zeroconf-interface";
+    const INTERPOLATION_QUALITY: &str = "interpolation-quality";
+    const SAMPLE_RATE: &str = "sample-rate";
 
     // Mostly arbitrary.
     const AP_PORT_SHORT: &str = "a";
@@ -576,6 +579,16 @@ fn get_setup() -> Setup {
         ZEROCONF_INTERFACE,
         "Comma-separated interface IP addresses on which zeroconf will bind. Defaults to all interfaces. Ignored by DNS-SD.",
         "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")]
@@ -732,10 +745,18 @@ fn get_setup() -> Setup {
 
     let invalid_error_msg =
         |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() {
-                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() {
@@ -761,6 +782,42 @@ fn get_setup() -> Setup {
         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)
         .as_deref()
         .map(|format| {
@@ -782,7 +839,7 @@ fn get_setup() -> Setup {
     let device = opt_str(DEVICE);
     if let Some(ref value) = device {
         if value == "?" {
-            backend(device, format);
+            backend(device, format, sample_rate.as_u32());
             exit(0);
         } else if value.is_empty() {
             empty_string_error_msg(DEVICE, DEVICE_SHORT);
@@ -1491,9 +1548,8 @@ fn get_setup() -> Setup {
 
             normalisation_attack_cf = opt_str(NORMALISATION_ATTACK)
                 .map(|attack| match attack.parse::<u64>() {
-                    Ok(value) if (VALID_NORMALISATION_ATTACK_RANGE).contains(&value) => {
-                        duration_to_coefficient(Duration::from_millis(value))
-                    }
+                    Ok(value) if (VALID_NORMALISATION_ATTACK_RANGE).contains(&value) => sample_rate
+                        .duration_to_normalisation_coefficient(Duration::from_millis(value)),
                     _ => {
                         let valid_values = &format!(
                             "{} - {}",
@@ -1506,7 +1562,10 @@ fn get_setup() -> Setup {
                             NORMALISATION_ATTACK_SHORT,
                             &attack,
                             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()
                                 .to_string(),
                         );
@@ -1514,12 +1573,15 @@ fn get_setup() -> Setup {
                         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)
                 .map(|release| match release.parse::<u64>() {
                     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!(
@@ -1533,17 +1595,20 @@ fn get_setup() -> Setup {
                             NORMALISATION_RELEASE_SHORT,
                             &release,
                             valid_values,
-                            &coefficient_to_duration(
-                                player_default_config.normalisation_release_cf,
-                            )
-                            .as_millis()
-                            .to_string(),
+                            &sample_rate
+                                .normalisation_coefficient_to_duration(
+                                    player_default_config.normalisation_release_cf,
+                                )
+                                .as_millis()
+                                .to_string(),
                         );
 
                         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)
                 .map(|knee| match knee.parse::<f64>() {
@@ -1608,6 +1673,8 @@ fn get_setup() -> Setup {
             bitrate,
             gapless,
             passthrough,
+            interpolation_quality,
+            sample_rate,
             normalisation,
             normalisation_type,
             normalisation_method,
@@ -1734,8 +1801,9 @@ async fn main() {
                 let format = setup.format;
                 let backend = setup.backend;
                 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 || {
-                    (backend)(device, format)
+                    (backend)(device, format, sample_rate)
                 });
 
                 if let Some(player_event_program) = setup.player_event_program.clone() {