diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 5651211e4..ec748a982 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 180040e66..1122a427b 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 eb455b0fc..f1978a0d6 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 4b63d3522..05a060cbe 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, format: AudioFormat, sample_rate: u32) - } #[cfg(feature = "rodiojack-backend")] -pub fn mk_rodiojack(device: Option, format: AudioFormat, sample_rate: u32) -> Box { +pub fn mk_rodiojack( + device: Option, + format: AudioFormat, + sample_rate: u32, +) -> Box { 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, format: AudioFormat, sample_rate: u32) -> RodioSink { +pub fn open( + host: cpal::Host, + device: Option, + 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 f90cd4cee..952e43bfb 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 { 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::), diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 52be10852..17f996d87 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 736b3c3f7..38290d5ee 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 0a8b8d6c1..2d89d30e9 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 { + fn get_soft_volume(&self) -> Box { 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 061f39b94..2f7d21f76 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 { + fn get_soft_volume(&self) -> Box { Box::new(SoftVolume(self.volume.clone())) } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 96bd70817..f50e72ef9 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, sink_status: SinkStatus, sink_event_callback: Option, - volume_getter: Box, + sample_pipeline: SamplePipeline, event_senders: Vec>, - converter: Converter, - - normalisation_integrator: f64, - normalisation_peak: f64, auto_normalise_as_album: bool, @@ -265,22 +256,6 @@ impl PlayerEvent { pub type PlayerEventChannel = mpsc::UnboundedReceiver; -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( config: PlayerConfig, session: Session, - volume_getter: Box, + volume_getter: Box, 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 34a71fc15..e534bea71 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 48edf1c95..24ab4b3c6 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::() { - 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::() { 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::() { @@ -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() {