diff --git a/CHANGELOG.md b/CHANGELOG.md index bf75382a..c00e884b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `AudioError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking) +- [audio, playback] Use `Duration` for time constants and functions (breaking) - [connect, playback] Moved volume controls from `librespot-connect` to `librespot-playback` crate - [connect] Synchronize player volume with mixer volume on playback - [playback] Store and pass samples in 64-bit floating point diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 8e076ebc..636194a8 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -18,70 +18,70 @@ use tokio::sync::{mpsc, oneshot}; use self::receive::{audio_file_fetch, request_range}; use crate::range_set::{Range, RangeSet}; +/// The minimum size of a block that is requested from the Spotify servers in one request. +/// This is the block size that is typically requested while doing a `seek()` on a file. +/// Note: smaller requests can happen if part of the block is downloaded already. const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 16; -// The minimum size of a block that is requested from the Spotify servers in one request. -// This is the block size that is typically requested while doing a seek() on a file. -// Note: smaller requests can happen if part of the block is downloaded already. +/// The amount of data that is requested when initially opening a file. +/// Note: if the file is opened to play from the beginning, the amount of data to +/// read ahead is requested in addition to this amount. If the file is opened to seek to +/// another position, then only this amount is requested on the first request. const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 16; -// The amount of data that is requested when initially opening a file. -// Note: if the file is opened to play from the beginning, the amount of data to -// read ahead is requested in addition to this amount. If the file is opened to seek to -// another position, then only this amount is requested on the first request. -const INITIAL_PING_TIME_ESTIMATE_SECONDS: f64 = 0.5; -// The pig time that is used for calculations before a ping time was actually measured. +/// The ping time that is used for calculations before a ping time was actually measured. +const INITIAL_PING_TIME_ESTIMATE: Duration = Duration::from_millis(500); -const MAXIMUM_ASSUMED_PING_TIME_SECONDS: f64 = 1.5; -// If the measured ping time to the Spotify server is larger than this value, it is capped -// to avoid run-away block sizes and pre-fetching. +/// If the measured ping time to the Spotify server is larger than this value, it is capped +/// to avoid run-away block sizes and pre-fetching. +const MAXIMUM_ASSUMED_PING_TIME: Duration = Duration::from_millis(1500); -pub const READ_AHEAD_BEFORE_PLAYBACK_SECONDS: f64 = 1.0; -// Before playback starts, this many seconds of data must be present. -// Note: the calculations are done using the nominal bitrate of the file. The actual amount -// of audio data may be larger or smaller. +/// Before playback starts, this many seconds of data must be present. +/// Note: the calculations are done using the nominal bitrate of the file. The actual amount +/// of audio data may be larger or smaller. +pub const READ_AHEAD_BEFORE_PLAYBACK: Duration = Duration::from_secs(1); -pub const READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS: f64 = 2.0; -// Same as READ_AHEAD_BEFORE_PLAYBACK_SECONDS, but the time is taken as a factor of the ping -// time to the Spotify server. -// Both, READ_AHEAD_BEFORE_PLAYBACK_SECONDS and READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS are -// obeyed. -// Note: the calculations are done using the nominal bitrate of the file. The actual amount -// of audio data may be larger or smaller. +/// Same as `READ_AHEAD_BEFORE_PLAYBACK`, but the time is taken as a factor of the ping +/// time to the Spotify server. Both `READ_AHEAD_BEFORE_PLAYBACK` and +/// `READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS` are obeyed. +/// Note: the calculations are done using the nominal bitrate of the file. The actual amount +/// of audio data may be larger or smaller. +pub const READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS: f32 = 2.0; -pub const READ_AHEAD_DURING_PLAYBACK_SECONDS: f64 = 5.0; -// While playing back, this many seconds of data ahead of the current read position are -// requested. -// Note: the calculations are done using the nominal bitrate of the file. The actual amount -// of audio data may be larger or smaller. +/// While playing back, this many seconds of data ahead of the current read position are +/// requested. +/// Note: the calculations are done using the nominal bitrate of the file. The actual amount +/// of audio data may be larger or smaller. +pub const READ_AHEAD_DURING_PLAYBACK: Duration = Duration::from_secs(5); -pub const READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS: f64 = 10.0; -// Same as READ_AHEAD_DURING_PLAYBACK_SECONDS, but the time is taken as a factor of the ping -// time to the Spotify server. -// Note: the calculations are done using the nominal bitrate of the file. The actual amount -// of audio data may be larger or smaller. +/// Same as `READ_AHEAD_DURING_PLAYBACK`, but the time is taken as a factor of the ping +/// time to the Spotify server. +/// Note: the calculations are done using the nominal bitrate of the file. The actual amount +/// of audio data may be larger or smaller. +pub const READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS: f32 = 10.0; -const PREFETCH_THRESHOLD_FACTOR: f64 = 4.0; -// If the amount of data that is pending (requested but not received) is less than a certain amount, -// data is pre-fetched in addition to the read ahead settings above. The threshold for requesting more -// data is calculated as -// < PREFETCH_THRESHOLD_FACTOR * * +/// If the amount of data that is pending (requested but not received) is less than a certain amount, +/// data is pre-fetched in addition to the read ahead settings above. The threshold for requesting more +/// data is calculated as ` < PREFETCH_THRESHOLD_FACTOR * * ` +const PREFETCH_THRESHOLD_FACTOR: f32 = 4.0; -const FAST_PREFETCH_THRESHOLD_FACTOR: f64 = 1.5; -// Similar to PREFETCH_THRESHOLD_FACTOR, but it also takes the current download rate into account. -// The formula used is -// < FAST_PREFETCH_THRESHOLD_FACTOR * * -// This mechanism allows for fast downloading of the remainder of the file. The number should be larger -// than 1 so the download rate ramps up until the bandwidth is saturated. The larger the value, the faster -// the download rate ramps up. However, this comes at the cost that it might hurt ping-time if a seek is -// performed while downloading. Values smaller than 1 cause the download rate to collapse and effectively -// only PREFETCH_THRESHOLD_FACTOR is in effect. Thus, set to zero if bandwidth saturation is not wanted. +/// Similar to `PREFETCH_THRESHOLD_FACTOR`, but it also takes the current download rate into account. +/// The formula used is ` < FAST_PREFETCH_THRESHOLD_FACTOR * * ` +/// This mechanism allows for fast downloading of the remainder of the file. The number should be larger +/// than `1.0` so the download rate ramps up until the bandwidth is saturated. The larger the value, the faster +/// the download rate ramps up. However, this comes at the cost that it might hurt ping time if a seek is +/// performed while downloading. Values smaller than `1.0` cause the download rate to collapse and effectively +/// only `PREFETCH_THRESHOLD_FACTOR` is in effect. Thus, set to `0.0` if bandwidth saturation is not wanted. +const FAST_PREFETCH_THRESHOLD_FACTOR: f32 = 1.5; +/// Limit the number of requests that are pending simultaneously before pre-fetching data. Pending +/// requests share bandwidth. Thus, havint too many requests can lead to the one that is needed next +/// for playback to be delayed leading to a buffer underrun. This limit has the effect that a new +/// pre-fetch request is only sent if less than `MAX_PREFETCH_REQUESTS` are pending. const MAX_PREFETCH_REQUESTS: usize = 4; -// Limit the number of requests that are pending simultaneously before pre-fetching data. Pending -// requests share bandwidth. Thus, havint too many requests can lead to the one that is needed next -// for playback to be delayed leading to a buffer underrun. This limit has the effect that a new -// pre-fetch request is only sent if less than MAX_PREFETCH_REQUESTS are pending. + +/// The time we will wait to obtain status updates on downloading. +const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(1); pub enum AudioFile { Cached(fs::File), @@ -131,10 +131,10 @@ impl StreamLoaderController { }) } - pub fn ping_time_ms(&self) -> usize { - self.stream_shared.as_ref().map_or(0, |shared| { - shared.ping_time_ms.load(atomic::Ordering::Relaxed) - }) + pub fn ping_time(&self) -> Duration { + Duration::from_millis(self.stream_shared.as_ref().map_or(0, |shared| { + shared.ping_time_ms.load(atomic::Ordering::Relaxed) as u64 + })) } fn send_stream_loader_command(&self, command: StreamLoaderCommand) { @@ -170,7 +170,7 @@ impl StreamLoaderController { { download_status = shared .cond - .wait_timeout(download_status, Duration::from_millis(1000)) + .wait_timeout(download_status, DOWNLOAD_TIMEOUT) .unwrap() .0; if range.length @@ -271,10 +271,10 @@ impl AudioFile { let mut initial_data_length = if play_from_beginning { INITIAL_DOWNLOAD_SIZE + max( - (READ_AHEAD_DURING_PLAYBACK_SECONDS * bytes_per_second as f64) as usize, - (INITIAL_PING_TIME_ESTIMATE_SECONDS + (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize, + (INITIAL_PING_TIME_ESTIMATE.as_secs_f32() * READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS - * bytes_per_second as f64) as usize, + * bytes_per_second as f32) as usize, ) } else { INITIAL_DOWNLOAD_SIZE @@ -368,7 +368,7 @@ impl AudioFileStreaming { let read_file = write_file.reopen().unwrap(); - //let (seek_tx, seek_rx) = mpsc::unbounded(); + // let (seek_tx, seek_rx) = mpsc::unbounded(); let (stream_loader_command_tx, stream_loader_command_rx) = mpsc::unbounded_channel::(); @@ -405,17 +405,19 @@ impl Read for AudioFileStreaming { let length_to_request = match *(self.shared.download_strategy.lock().unwrap()) { DownloadStrategy::RandomAccess() => length, DownloadStrategy::Streaming() => { - // Due to the read-ahead stuff, we potentially request more than the actual reqeust demanded. - let ping_time_seconds = - 0.0001 * self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64; + // Due to the read-ahead stuff, we potentially request more than the actual request demanded. + let ping_time_seconds = Duration::from_millis( + self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as u64, + ) + .as_secs_f32(); let length_to_request = length + max( - (READ_AHEAD_DURING_PLAYBACK_SECONDS * self.shared.stream_data_rate as f64) - as usize, + (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() + * self.shared.stream_data_rate as f32) as usize, (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS * ping_time_seconds - * self.shared.stream_data_rate as f64) as usize, + * self.shared.stream_data_rate as f32) as usize, ); min(length_to_request, self.shared.file_size - offset) } @@ -449,7 +451,7 @@ impl Read for AudioFileStreaming { download_status = self .shared .cond - .wait_timeout(download_status, Duration::from_millis(1000)) + .wait_timeout(download_status, DOWNLOAD_TIMEOUT) .unwrap() .0; } diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 0f056c96..64becc23 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -1,8 +1,9 @@ use std::cmp::{max, min}; use std::io::{Seek, SeekFrom, Write}; use std::sync::{atomic, Arc}; -use std::time::Instant; +use std::time::{Duration, Instant}; +use atomic::Ordering; use byteorder::{BigEndian, WriteBytesExt}; use bytes::Bytes; use futures_util::StreamExt; @@ -16,7 +17,7 @@ use crate::range_set::{Range, RangeSet}; use super::{AudioFileShared, DownloadStrategy, StreamLoaderCommand}; use super::{ - FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME_SECONDS, MAX_PREFETCH_REQUESTS, + FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, MAX_PREFETCH_REQUESTS, MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, }; @@ -57,7 +58,7 @@ struct PartialFileData { } enum ReceivedData { - ResponseTimeMs(usize), + ResponseTime(Duration), Data(PartialFileData), } @@ -74,7 +75,7 @@ async fn receive_data( let old_number_of_request = shared .number_of_open_requests - .fetch_add(1, atomic::Ordering::SeqCst); + .fetch_add(1, Ordering::SeqCst); let mut measure_ping_time = old_number_of_request == 0; @@ -86,14 +87,11 @@ async fn receive_data( }; if measure_ping_time { - let duration = Instant::now() - request_sent_time; - let duration_ms: u64; - if 0.001 * (duration.as_millis() as f64) > MAXIMUM_ASSUMED_PING_TIME_SECONDS { - duration_ms = (MAXIMUM_ASSUMED_PING_TIME_SECONDS * 1000.0) as u64; - } else { - duration_ms = duration.as_millis() as u64; + let mut duration = Instant::now() - request_sent_time; + if duration > MAXIMUM_ASSUMED_PING_TIME { + duration = MAXIMUM_ASSUMED_PING_TIME; } - let _ = file_data_tx.send(ReceivedData::ResponseTimeMs(duration_ms as usize)); + let _ = file_data_tx.send(ReceivedData::ResponseTime(duration)); measure_ping_time = false; } let data_size = data.len(); @@ -127,7 +125,7 @@ async fn receive_data( shared .number_of_open_requests - .fetch_sub(1, atomic::Ordering::SeqCst); + .fetch_sub(1, Ordering::SeqCst); if result.is_err() { warn!( @@ -149,7 +147,7 @@ struct AudioFileFetch { file_data_tx: mpsc::UnboundedSender, complete_tx: Option>, - network_response_times_ms: Vec, + network_response_times: Vec, } // Might be replaced by enum from std once stable @@ -237,7 +235,7 @@ impl AudioFileFetch { // download data from after the current read position first let mut tail_end = RangeSet::new(); - let read_position = self.shared.read_position.load(atomic::Ordering::Relaxed); + let read_position = self.shared.read_position.load(Ordering::Relaxed); tail_end.add_range(&Range::new( read_position, self.shared.file_size - read_position, @@ -267,26 +265,23 @@ impl AudioFileFetch { fn handle_file_data(&mut self, data: ReceivedData) -> ControlFlow { match data { - ReceivedData::ResponseTimeMs(response_time_ms) => { - trace!("Ping time estimated as: {} ms.", response_time_ms); + ReceivedData::ResponseTime(response_time) => { + trace!("Ping time estimated as: {}ms", response_time.as_millis()); - // record the response time - self.network_response_times_ms.push(response_time_ms); - - // prune old response times. Keep at most three. - while self.network_response_times_ms.len() > 3 { - self.network_response_times_ms.remove(0); + // prune old response times. Keep at most two so we can push a third. + while self.network_response_times.len() >= 3 { + self.network_response_times.remove(0); } + // record the response time + self.network_response_times.push(response_time); + // stats::median is experimental. So we calculate the median of up to three ourselves. - let ping_time_ms: usize = match self.network_response_times_ms.len() { - 1 => self.network_response_times_ms[0] as usize, - 2 => { - ((self.network_response_times_ms[0] + self.network_response_times_ms[1]) - / 2) as usize - } + let ping_time = match self.network_response_times.len() { + 1 => self.network_response_times[0], + 2 => (self.network_response_times[0] + self.network_response_times[1]) / 2, 3 => { - let mut times = self.network_response_times_ms.clone(); + let mut times = self.network_response_times.clone(); times.sort_unstable(); times[1] } @@ -296,7 +291,7 @@ impl AudioFileFetch { // store our new estimate for everyone to see self.shared .ping_time_ms - .store(ping_time_ms, atomic::Ordering::Relaxed); + .store(ping_time.as_millis() as usize, Ordering::Relaxed); } ReceivedData::Data(data) => { self.output @@ -390,7 +385,7 @@ pub(super) async fn audio_file_fetch( file_data_tx, complete_tx: Some(complete_tx), - network_response_times_ms: Vec::new(), + network_response_times: Vec::with_capacity(3), }; loop { @@ -408,10 +403,8 @@ pub(super) async fn audio_file_fetch( } if fetch.get_download_strategy() == DownloadStrategy::Streaming() { - let number_of_open_requests = fetch - .shared - .number_of_open_requests - .load(atomic::Ordering::SeqCst); + let number_of_open_requests = + fetch.shared.number_of_open_requests.load(Ordering::SeqCst); if number_of_open_requests < MAX_PREFETCH_REQUESTS { let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_requests; @@ -424,14 +417,15 @@ pub(super) async fn audio_file_fetch( }; let ping_time_seconds = - 0.001 * fetch.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64; + Duration::from_millis(fetch.shared.ping_time_ms.load(Ordering::Relaxed) as u64) + .as_secs_f32(); let download_rate = fetch.session.channel().get_download_rate_estimate(); let desired_pending_bytes = max( (PREFETCH_THRESHOLD_FACTOR * ping_time_seconds - * fetch.shared.stream_data_rate as f64) as usize, - (FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f64) + * fetch.shared.stream_data_rate as f32) as usize, + (FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f32) as usize, ); diff --git a/audio/src/lib.rs b/audio/src/lib.rs index e43cf728..4b486bbe 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -11,6 +11,6 @@ mod range_set; pub use decrypt::AudioDecrypt; pub use fetch::{AudioFile, StreamLoaderController}; pub use fetch::{ - READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS, - READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, + READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, + READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, }; diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 76cf7054..57dc4cdd 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -88,7 +88,7 @@ const CONTEXT_TRACKS_HISTORY: usize = 10; const CONTEXT_FETCH_THRESHOLD: u32 = 5; const VOLUME_STEPS: i64 = 64; -const VOLUME_STEP_SIZE: u16 = 1024; // (std::u16::MAX + 1) / VOLUME_STEPS +const VOLUME_STEP_SIZE: u16 = 1024; // (u16::MAX + 1) / VOLUME_STEPS pub struct Spirc { commands: mpsc::UnboundedSender, diff --git a/core/src/channel.rs b/core/src/channel.rs index 4a78a4aa..29c3c8aa 100644 --- a/core/src/channel.rs +++ b/core/src/channel.rs @@ -23,6 +23,8 @@ component! { } } +const ONE_SECOND_IN_MS: usize = 1000; + #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] pub struct ChannelError; @@ -74,8 +76,11 @@ impl ChannelManager { self.lock(|inner| { let current_time = Instant::now(); if let Some(download_measurement_start) = inner.download_measurement_start { - if (current_time - download_measurement_start).as_millis() > 1000 { - inner.download_rate_estimate = 1000 * inner.download_measurement_bytes + if (current_time - download_measurement_start).as_millis() + > ONE_SECOND_IN_MS as u128 + { + inner.download_rate_estimate = ONE_SECOND_IN_MS + * inner.download_measurement_bytes / (current_time - download_measurement_start).as_millis() as usize; inner.download_measurement_start = Some(current_time); inner.download_measurement_bytes = 0; diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 8a590c6f..b2f020f1 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -2,7 +2,7 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE}; +use crate::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE}; use alsa::device_name::HintIter; use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; use alsa::{Direction, Error, ValueOr}; @@ -10,8 +10,9 @@ use std::cmp::min; use std::ffi::CString; use std::io; use std::process::exit; +use std::time::Duration; -const BUFFERED_LATENCY: f32 = 0.125; // seconds +const BUFFERED_LATENCY: Duration = Duration::from_millis(125); const BUFFERED_PERIODS: Frames = 4; pub struct AlsaSink { @@ -57,7 +58,8 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box // latency = period_size * periods / (rate * bytes_per_frame) // For stereo samples encoded as 32-bit float, one frame has a length of eight bytes. let mut period_size = ((SAMPLES_PER_SECOND * format.size() as u32) as f32 - * (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as Frames; + * (BUFFERED_LATENCY.as_secs_f32() / BUFFERED_PERIODS as f32)) + as Frames; { let hwp = HwParams::any(&pcm)?; hwp.set_access(Access::RWInterleaved)?; @@ -80,7 +82,7 @@ impl Open for AlsaSink { fn open(device: Option, format: AudioFormat) -> Self { info!("Using Alsa sink with format: {:?}", format); - let name = match device.as_ref().map(AsRef::as_ref) { + let name = match device.as_deref() { Some("?") => { println!("Listing available Alsa outputs:"); list_outputs(); @@ -162,6 +164,8 @@ impl SinkAsBytes for AlsaSink { } impl AlsaSink { + pub const NAME: &'static str = "alsa"; + fn write_buf(&mut self) { let pcm = self.pcm.as_mut().unwrap(); let io = pcm.io_bytes(); diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index bd76863c..58f6cbc9 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -2,7 +2,7 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::{NUM_CHANNELS, SAMPLE_RATE}; use gstreamer as gst; use gstreamer_app as gst_app; @@ -139,3 +139,7 @@ impl SinkAsBytes for GstreamerSink { Ok(()) } } + +impl GstreamerSink { + pub const NAME: &'static str = "gstreamer"; +} diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 75c4eb9f..f55f20a8 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -2,7 +2,7 @@ use super::{Open, Sink}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::player::NUM_CHANNELS; +use crate::NUM_CHANNELS; use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, }; @@ -81,3 +81,7 @@ impl Sink for JackSink { Ok(()) } } + +impl JackSink { + pub const NAME: &'static str = "jackaudio"; +} diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 0996bfb6..31fb847c 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -90,6 +90,8 @@ use self::gstreamer::GstreamerSink; #[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] mod rodio; +#[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] +use self::rodio::RodioSink; #[cfg(feature = "sdl-backend")] mod sdl; @@ -104,23 +106,23 @@ use self::subprocess::SubprocessSink; pub const BACKENDS: &[(&str, SinkBuilder)] = &[ #[cfg(feature = "rodio-backend")] - ("rodio", rodio::mk_rodio), // default goes first + (RodioSink::NAME, rodio::mk_rodio), // default goes first #[cfg(feature = "alsa-backend")] - ("alsa", mk_sink::), + (AlsaSink::NAME, mk_sink::), #[cfg(feature = "portaudio-backend")] - ("portaudio", mk_sink::), + (PortAudioSink::NAME, mk_sink::), #[cfg(feature = "pulseaudio-backend")] - ("pulseaudio", mk_sink::), + (PulseAudioSink::NAME, mk_sink::), #[cfg(feature = "jackaudio-backend")] - ("jackaudio", mk_sink::), + (JackSink::NAME, mk_sink::), #[cfg(feature = "gstreamer-backend")] - ("gstreamer", mk_sink::), + (GstreamerSink::NAME, mk_sink::), #[cfg(feature = "rodiojack-backend")] ("rodiojack", rodio::mk_rodiojack), #[cfg(feature = "sdl-backend")] - ("sdl", mk_sink::), - ("pipe", mk_sink::), - ("subprocess", mk_sink::), + (SdlSink::NAME, mk_sink::), + (StdoutSink::NAME, mk_sink::), + (SubprocessSink::NAME, mk_sink::), ]; pub fn find(name: Option) -> Option { diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 6ad2773b..926219f9 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -34,3 +34,7 @@ impl SinkAsBytes for StdoutSink { Ok(()) } } + +impl StdoutSink { + pub const NAME: &'static str = "pipe"; +} diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index ccebcfdf..378deb48 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -2,7 +2,7 @@ use super::{Open, Sink}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::{NUM_CHANNELS, SAMPLE_RATE}; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; use portaudio_rs::stream::*; use std::io; @@ -57,7 +57,7 @@ impl<'a> Open for PortAudioSink<'a> { portaudio_rs::initialize().unwrap(); - let device_idx = match device.as_ref().map(AsRef::as_ref) { + let device_idx = match device.as_deref() { Some("?") => { list_outputs(); exit(0) @@ -178,3 +178,7 @@ impl<'a> Drop for PortAudioSink<'a> { portaudio_rs::terminate().unwrap(); } } + +impl<'a> PortAudioSink<'a> { + pub const NAME: &'static str = "portaudio"; +} diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 75bd49de..e36941ea 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -2,7 +2,7 @@ use super::{Open, Sink, SinkAsBytes}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::{NUM_CHANNELS, SAMPLE_RATE}; use libpulse_binding::{self as pulse, stream::Direction}; use libpulse_simple_binding::Simple; use std::io; @@ -55,7 +55,7 @@ impl Sink for PulseAudioSink { return Ok(()); } - let device = self.device.as_ref().map(|s| (*s).as_str()); + let device = self.device.as_deref(); let result = Simple::new( None, // Use the default server. APP_NAME, // Our application's name. @@ -104,3 +104,7 @@ impl SinkAsBytes for PulseAudioSink { } } } + +impl PulseAudioSink { + pub const NAME: &'static str = "pulseaudio"; +} diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 2951560a..1e999938 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -1,5 +1,6 @@ use std::process::exit; -use std::{io, thread, time}; +use std::time::Duration; +use std::{io, thread}; use cpal::traits::{DeviceTrait, HostTrait}; use thiserror::Error; @@ -8,7 +9,7 @@ use super::Sink; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::{NUM_CHANNELS, SAMPLE_RATE}; #[cfg(all( feature = "rodiojack-backend", @@ -203,8 +204,12 @@ impl Sink for RodioSink { // 44100 elements --> about 27 chunks while self.rodio_sink.len() > 26 { // sleep and wait for rodio to drain a bit - thread::sleep(time::Duration::from_millis(10)); + thread::sleep(Duration::from_millis(10)); } Ok(()) } } + +impl RodioSink { + pub const NAME: &'static str = "rodio"; +} diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index d07e562f..28d140e8 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -2,9 +2,10 @@ use super::{Open, Sink}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; +use crate::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; -use std::{io, thread, time}; +use std::time::Duration; +use std::{io, thread}; pub enum SdlSink { F32(AudioQueue), @@ -86,7 +87,7 @@ impl Sink for SdlSink { ($queue: expr, $size: expr) => {{ // sleep and wait for sdl thread to drain the queue a bit while $queue.size() > (NUM_CHANNELS as u32 * $size as u32 * SAMPLE_RATE) { - thread::sleep(time::Duration::from_millis(10)); + thread::sleep(Duration::from_millis(10)); } }}; } @@ -112,3 +113,7 @@ impl Sink for SdlSink { Ok(()) } } + +impl SdlSink { + pub const NAME: &'static str = "sdl"; +} diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 785fb3d2..64f04c88 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -62,3 +62,7 @@ impl SinkAsBytes for SubprocessSink { Ok(()) } } + +impl SubprocessSink { + pub const NAME: &'static str = "subprocess"; +} diff --git a/playback/src/config.rs b/playback/src/config.rs index a2e1c6c7..7604f59f 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -2,9 +2,9 @@ use super::player::db_to_ratio; use crate::convert::i24; pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer}; -use std::convert::TryFrom; use std::mem; use std::str::FromStr; +use std::time::Duration; #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum Bitrate { @@ -41,10 +41,10 @@ pub enum AudioFormat { S16, } -impl TryFrom<&String> for AudioFormat { - type Error = (); - fn try_from(s: &String) -> Result { - match s.to_uppercase().as_str() { +impl FromStr for AudioFormat { + type Err = (); + fn from_str(s: &str) -> Result { + match s.to_uppercase().as_ref() { "F64" => Ok(Self::F64), "F32" => Ok(Self::F32), "S32" => Ok(Self::S32), @@ -133,8 +133,8 @@ pub struct PlayerConfig { pub normalisation_method: NormalisationMethod, pub normalisation_pregain: f64, pub normalisation_threshold: f64, - pub normalisation_attack: f64, - pub normalisation_release: f64, + pub normalisation_attack: Duration, + pub normalisation_release: Duration, pub normalisation_knee: f64, // pass function pointers so they can be lazily instantiated *after* spawning a thread @@ -152,8 +152,8 @@ impl Default for PlayerConfig { normalisation_method: NormalisationMethod::default(), normalisation_pregain: 0.0, normalisation_threshold: db_to_ratio(-1.0), - normalisation_attack: 0.005, - normalisation_release: 0.1, + normalisation_attack: Duration::from_millis(5), + normalisation_release: Duration::from_millis(100), normalisation_knee: 1.0, passthrough: false, ditherer: Some(mk_ditherer::), @@ -184,7 +184,7 @@ impl Default for VolumeCtrl { } impl VolumeCtrl { - pub const MAX_VOLUME: u16 = std::u16::MAX; + pub const MAX_VOLUME: u16 = u16::MAX; // Taken from: https://www.dr-lex.be/info-stuff/volumecontrols.html pub const DEFAULT_DB_RANGE: f64 = 60.0; diff --git a/playback/src/convert.rs b/playback/src/convert.rs index 37e53fc0..1f1122a7 100644 --- a/playback/src/convert.rs +++ b/playback/src/convert.rs @@ -34,8 +34,21 @@ impl Converter { } } + /// To convert PCM samples from floating point normalized as `-1.0..=1.0` + /// to 32-bit signed integer, multiply by 2147483648 (0x80000000) and + /// saturate at the bounds of `i32`. const SCALE_S32: f64 = 2147483648.; + + /// To convert PCM samples from floating point normalized as `-1.0..=1.0` + /// to 24-bit signed integer, multiply by 8388608 (0x800000) and saturate + /// at the bounds of `i24`. const SCALE_S24: f64 = 8388608.; + + /// To convert PCM samples from floating point normalized as `-1.0..=1.0` + /// to 16-bit signed integer, multiply by 32768 (0x8000) and saturate at + /// the bounds of `i16`. When the samples were encoded using the same + /// scaling factor, like the reference Vorbis encoder does, this makes + /// conversions transparent. const SCALE_S16: f64 = 32768.; pub fn scale(&mut self, sample: f64, factor: f64) -> f64 { diff --git a/playback/src/decoder/lewton_decoder.rs b/playback/src/decoder/lewton_decoder.rs index 64a49e57..adf63e2a 100644 --- a/playback/src/decoder/lewton_decoder.rs +++ b/playback/src/decoder/lewton_decoder.rs @@ -6,6 +6,7 @@ use lewton::samples::InterleavedSamples; use std::error; use std::fmt; use std::io::{Read, Seek}; +use std::time::Duration; pub struct VorbisDecoder(OggStreamReader); pub struct VorbisError(lewton::VorbisError); @@ -24,7 +25,7 @@ where R: Read + Seek, { fn seek(&mut self, ms: i64) -> Result<(), AudioError> { - let absgp = ms * 44100 / 1000; + let absgp = Duration::from_millis(ms as u64 * crate::SAMPLE_RATE as u64).as_secs(); match self.0.seek_absgp_pg(absgp as u64) { Ok(_) => Ok(()), Err(err) => Err(AudioError::VorbisError(err.into())), diff --git a/playback/src/decoder/passthrough_decoder.rs b/playback/src/decoder/passthrough_decoder.rs index e064cba3..7c1ad532 100644 --- a/playback/src/decoder/passthrough_decoder.rs +++ b/playback/src/decoder/passthrough_decoder.rs @@ -1,8 +1,10 @@ // Passthrough decoder for librespot use super::{AudioDecoder, AudioError, AudioPacket}; +use crate::SAMPLE_RATE; use ogg::{OggReadError, Packet, PacketReader, PacketWriteEndInfo, PacketWriter}; use std::fmt; use std::io::{Read, Seek}; +use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; fn get_header(code: u8, rdr: &mut PacketReader) -> Result, PassthroughError> @@ -12,7 +14,7 @@ where let pck: Packet = rdr.read_packet_expected()?; let pkt_type = pck.data[0]; - debug!("Vorbis header type{}", &pkt_type); + debug!("Vorbis header type {}", &pkt_type); if pkt_type != code { return Err(PassthroughError(OggReadError::InvalidData)); @@ -96,7 +98,10 @@ impl AudioDecoder for PassthroughDecoder { self.stream_serial += 1; // hard-coded to 44.1 kHz - match self.rdr.seek_absgp(None, (ms * 44100 / 1000) as u64) { + match self.rdr.seek_absgp( + None, + Duration::from_millis(ms as u64 * SAMPLE_RATE as u64).as_secs(), + ) { Ok(_) => { // need to set some offset for next_page() let pck = self.rdr.read_packet().unwrap().unwrap(); diff --git a/playback/src/dither.rs b/playback/src/dither.rs index 63447ce8..2510b886 100644 --- a/playback/src/dither.rs +++ b/playback/src/dither.rs @@ -61,7 +61,7 @@ impl Ditherer for TriangularDitherer { } fn name(&self) -> &'static str { - "Triangular" + Self::NAME } fn noise(&mut self) -> f64 { @@ -69,6 +69,10 @@ impl Ditherer for TriangularDitherer { } } +impl TriangularDitherer { + pub const NAME: &'static str = "tpdf"; +} + pub struct GaussianDitherer { cached_rng: ThreadRng, distribution: Normal, @@ -84,7 +88,7 @@ impl Ditherer for GaussianDitherer { } fn name(&self) -> &'static str { - "Gaussian" + Self::NAME } fn noise(&mut self) -> f64 { @@ -92,6 +96,10 @@ impl Ditherer for GaussianDitherer { } } +impl GaussianDitherer { + pub const NAME: &'static str = "gpdf"; +} + pub struct HighPassDitherer { active_channel: usize, previous_noises: [f64; NUM_CHANNELS], @@ -110,7 +118,7 @@ impl Ditherer for HighPassDitherer { } fn name(&self) -> &'static str { - "Triangular, High Passed" + Self::NAME } fn noise(&mut self) -> f64 { @@ -122,6 +130,10 @@ impl Ditherer for HighPassDitherer { } } +impl HighPassDitherer { + pub const NAME: &'static str = "tpdf_hp"; +} + pub fn mk_ditherer() -> Box { Box::new(D::new()) } @@ -130,9 +142,9 @@ pub type DithererBuilder = fn() -> Box; pub fn find_ditherer(name: Option) -> Option { match name.as_deref() { - Some("tpdf") => Some(mk_ditherer::), - Some("gpdf") => Some(mk_ditherer::), - Some("tpdf_hp") => Some(mk_ditherer::), + Some(TriangularDitherer::NAME) => Some(mk_ditherer::), + Some(GaussianDitherer::NAME) => Some(mk_ditherer::), + Some(HighPassDitherer::NAME) => Some(mk_ditherer::), _ => None, } } diff --git a/playback/src/lib.rs b/playback/src/lib.rs index 31dadc44..689b8470 100644 --- a/playback/src/lib.rs +++ b/playback/src/lib.rs @@ -12,3 +12,7 @@ mod decoder; pub mod dither; pub mod mixer; pub mod player; + +pub const SAMPLE_RATE: u32 = 44100; +pub const NUM_CHANNELS: u8 = 2; +pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32; diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index fb6853bb..8bee9e0d 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -241,6 +241,8 @@ impl Mixer for AlsaMixer { } impl AlsaMixer { + pub const NAME: &'static str = "alsa"; + fn switched_off(&self) -> bool { if !self.has_switch { return false; diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index aaecd779..ed39582e 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -53,11 +53,11 @@ fn mk_sink(config: MixerConfig) -> Box { Box::new(M::open(config)) } -pub fn find>(name: Option) -> Option { - match name.as_ref().map(AsRef::as_ref) { - None | Some("softvol") => Some(mk_sink::), +pub fn find(name: Option<&str>) -> Option { + match name { + None | Some(SoftMixer::NAME) => Some(mk_sink::), #[cfg(feature = "alsa-backend")] - Some("alsa") => Some(mk_sink::), + Some(AlsaMixer::NAME) => Some(mk_sink::), _ => None, } } diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index d1c6eb20..27448237 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -42,6 +42,10 @@ impl Mixer for SoftMixer { } } +impl SoftMixer { + pub const NAME: &'static str = "softmixer"; +} + struct SoftVolumeApplier { volume: Arc, } diff --git a/playback/src/player.rs b/playback/src/player.rs index f5af69f8..4daac9b4 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -13,8 +13,8 @@ use tokio::sync::{mpsc, oneshot}; use crate::audio::{AudioDecrypt, AudioFile, StreamLoaderController}; use crate::audio::{ - READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS, - READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, + READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, + READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, }; use crate::audio_backend::Sink; use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}; @@ -26,9 +26,7 @@ use crate::decoder::{AudioDecoder, AudioError, AudioPacket, PassthroughDecoder, use crate::metadata::{AudioItem, FileFormat}; use crate::mixer::AudioFilter; -pub const SAMPLE_RATE: u32 = 44100; -pub const NUM_CHANNELS: u8 = 2; -pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32; +use crate::{NUM_CHANNELS, SAMPLES_PER_SECOND}; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; pub const DB_VOLTAGE_RATIO: f64 = 20.0; @@ -297,14 +295,8 @@ impl Player { debug!("Normalisation Method: {:?}", config.normalisation_method); if config.normalisation_method == NormalisationMethod::Dynamic { - debug!( - "Normalisation Attack: {:.0} ms", - config.normalisation_attack * 1000.0 - ); - debug!( - "Normalisation Release: {:.0} ms", - config.normalisation_release * 1000.0 - ); + debug!("Normalisation Attack: {:?}", config.normalisation_attack); + debug!("Normalisation Release: {:?}", config.normalisation_release); debug!("Normalisation Knee: {:?}", config.normalisation_knee); } } @@ -973,12 +965,12 @@ impl Future for PlayerInternal { let notify_about_position = match *reported_nominal_start_time { None => true, Some(reported_nominal_start_time) => { - // only notify if we're behind. If we're ahead it's probably due to a buffer of the backend and we;re actually in time. + // only notify if we're behind. If we're ahead it's probably due to a buffer of the backend and we're actually in time. let lag = (Instant::now() - reported_nominal_start_time) .as_millis() as i64 - stream_position_millis as i64; - lag > 1000 + lag > Duration::from_secs(1).as_millis() as i64 } }; if notify_about_position { @@ -1219,6 +1211,16 @@ impl PlayerInternal { + shaped_limiter_strength * self.limiter_factor; }; + // Cast the fields here for better readability + let normalisation_attack = + self.config.normalisation_attack.as_secs_f64(); + let normalisation_release = + self.config.normalisation_release.as_secs_f64(); + let limiter_release_counter = + self.limiter_release_counter as f64; + let limiter_attack_counter = self.limiter_attack_counter as f64; + let samples_per_second = SAMPLES_PER_SECOND as f64; + // Always check for peaks, even when the limiter is already active. // There may be even higher peaks than we initially targeted. // Check against the normalisation factor that would be applied normally. @@ -1228,21 +1230,19 @@ impl PlayerInternal { if self.limiter_release_counter > 0 { // A peak was encountered while releasing the limiter; // synchronize with the current release limiter strength. - self.limiter_attack_counter = (((SAMPLES_PER_SECOND - as f64 - * self.config.normalisation_release) - - self.limiter_release_counter as f64) - / (self.config.normalisation_release - / self.config.normalisation_attack)) + self.limiter_attack_counter = (((samples_per_second + * normalisation_release) + - limiter_release_counter) + / (normalisation_release / normalisation_attack)) as u32; self.limiter_release_counter = 0; } self.limiter_attack_counter = self.limiter_attack_counter.saturating_add(1); - self.limiter_strength = self.limiter_attack_counter as f64 - / (SAMPLES_PER_SECOND as f64 - * self.config.normalisation_attack); + + self.limiter_strength = limiter_attack_counter + / (samples_per_second * normalisation_attack); if abs_sample > self.limiter_peak_sample { self.limiter_peak_sample = abs_sample; @@ -1256,12 +1256,10 @@ impl PlayerInternal { // the limiter reached full strength. For that reason // start the release by synchronizing with the current // attack limiter strength. - self.limiter_release_counter = (((SAMPLES_PER_SECOND - as f64 - * self.config.normalisation_attack) - - self.limiter_attack_counter as f64) - * (self.config.normalisation_release - / self.config.normalisation_attack)) + self.limiter_release_counter = (((samples_per_second + * normalisation_attack) + - limiter_attack_counter) + * (normalisation_release / normalisation_attack)) as u32; self.limiter_attack_counter = 0; } @@ -1270,17 +1268,14 @@ impl PlayerInternal { self.limiter_release_counter.saturating_add(1); if self.limiter_release_counter - > (SAMPLES_PER_SECOND as f64 - * self.config.normalisation_release) - as u32 + > (samples_per_second * normalisation_release) as u32 { self.reset_limiter(); } else { - self.limiter_strength = ((SAMPLES_PER_SECOND as f64 - * self.config.normalisation_release) - - self.limiter_release_counter as f64) - / (SAMPLES_PER_SECOND as f64 - * self.config.normalisation_release); + self.limiter_strength = ((samples_per_second + * normalisation_release) + - limiter_release_counter) + / (samples_per_second * normalisation_release); } } } @@ -1806,18 +1801,18 @@ impl PlayerInternal { // Request our read ahead range let request_data_length = max( (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS - * (0.001 * stream_loader_controller.ping_time_ms() as f64) - * bytes_per_second as f64) as usize, - (READ_AHEAD_DURING_PLAYBACK_SECONDS * bytes_per_second as f64) as usize, + * stream_loader_controller.ping_time().as_secs_f32() + * bytes_per_second as f32) as usize, + (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize, ); stream_loader_controller.fetch_next(request_data_length); // Request the part we want to wait for blocking. This effecively means we wait for the previous request to partially complete. let wait_for_data_length = max( (READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS - * (0.001 * stream_loader_controller.ping_time_ms() as f64) - * bytes_per_second as f64) as usize, - (READ_AHEAD_BEFORE_PLAYBACK_SECONDS * bytes_per_second as f64) as usize, + * stream_loader_controller.ping_time().as_secs_f32() + * bytes_per_second as f32) as usize, + (READ_AHEAD_BEFORE_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize, ); stream_loader_controller.fetch_next_blocking(wait_for_data_length); } diff --git a/src/main.rs b/src/main.rs index 4f8b8f1b..a3687aaa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,8 @@ use librespot::playback::config::{ AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl, }; use librespot::playback::dither; +#[cfg(feature = "alsa-backend")] +use librespot::playback::mixer::alsamixer::AlsaMixer; use librespot::playback::mixer::mappings::MappedCtrl; use librespot::playback::mixer::{self, MixerConfig, MixerFn}; use librespot::playback::player::{db_to_ratio, Player}; @@ -24,17 +26,14 @@ use librespot::playback::player::{db_to_ratio, Player}; mod player_event_handler; use player_event_handler::{emit_sink_event, run_program_on_events}; -use std::convert::TryFrom; +use std::env; +use std::io::{stderr, Write}; use std::path::Path; +use std::pin::Pin; use std::process::exit; use std::str::FromStr; -use std::{env, time::Instant}; -use std::{ - io::{stderr, Write}, - pin::Pin, -}; - -const MILLIS: f64 = 1000.0; +use std::time::Duration; +use std::time::Instant; fn device_id(name: &str) -> String { hex::encode(Sha1::digest(name.as_bytes())) @@ -189,176 +188,216 @@ struct Setup { } fn get_setup(args: &[String]) -> Setup { + const AP_PORT: &str = "ap-port"; + const AUTOPLAY: &str = "autoplay"; + const BACKEND: &str = "backend"; + const BITRATE: &str = "b"; + const CACHE: &str = "c"; + const CACHE_SIZE_LIMIT: &str = "cache-size-limit"; + const DEVICE: &str = "device"; + const DEVICE_TYPE: &str = "device-type"; + const DISABLE_AUDIO_CACHE: &str = "disable-audio-cache"; + const DISABLE_DISCOVERY: &str = "disable-discovery"; + const DISABLE_GAPLESS: &str = "disable-gapless"; + const DITHER: &str = "dither"; + const EMIT_SINK_EVENTS: &str = "emit-sink-events"; + const ENABLE_VOLUME_NORMALISATION: &str = "enable-volume-normalisation"; + const FORMAT: &str = "format"; + const HELP: &str = "h"; + const INITIAL_VOLUME: &str = "initial-volume"; + const MIXER_CARD: &str = "mixer-card"; + const MIXER_INDEX: &str = "mixer-index"; + const MIXER_NAME: &str = "mixer-name"; + const NAME: &str = "name"; + const NORMALISATION_ATTACK: &str = "normalisation-attack"; + const NORMALISATION_GAIN_TYPE: &str = "normalisation-gain-type"; + const NORMALISATION_KNEE: &str = "normalisation-knee"; + const NORMALISATION_METHOD: &str = "normalisation-method"; + const NORMALISATION_PREGAIN: &str = "normalisation-pregain"; + const NORMALISATION_RELEASE: &str = "normalisation-release"; + const NORMALISATION_THRESHOLD: &str = "normalisation-threshold"; + const ONEVENT: &str = "onevent"; + const PASSTHROUGH: &str = "passthrough"; + const PASSWORD: &str = "password"; + const PROXY: &str = "proxy"; + const SYSTEM_CACHE: &str = "system-cache"; + const USERNAME: &str = "username"; + const VERBOSE: &str = "verbose"; + const VERSION: &str = "version"; + const VOLUME_CTRL: &str = "volume-ctrl"; + const VOLUME_RANGE: &str = "volume-range"; + const ZEROCONF_PORT: &str = "zeroconf-port"; + let mut opts = getopts::Options::new(); opts.optflag( - "h", + HELP, "help", "Print this help menu.", ).optopt( - "c", + CACHE, "cache", "Path to a directory where files will be cached.", "PATH", ).optopt( "", - "system-cache", + SYSTEM_CACHE, "Path to a directory where system files (credentials, volume) will be cached. Can be different from cache option value.", "PATH", ).optopt( "", - "cache-size-limit", + CACHE_SIZE_LIMIT, "Limits the size of the cache for audio files.", "SIZE" - ).optflag("", "disable-audio-cache", "Disable caching of the audio data.") - .optopt("n", "name", "Device name.", "NAME") - .optopt("", "device-type", "Displayed device type.", "TYPE") + ).optflag("", DISABLE_AUDIO_CACHE, "Disable caching of the audio data.") + .optopt("n", NAME, "Device name.", "NAME") + .optopt("", DEVICE_TYPE, "Displayed device type.", "TYPE") .optopt( - "b", + BITRATE, "bitrate", "Bitrate (kbps) {96|160|320}. Defaults to 160.", "BITRATE", ) .optopt( "", - "onevent", + ONEVENT, "Run PROGRAM when a playback event occurs.", "PROGRAM", ) - .optflag("", "emit-sink-events", "Run program set by --onevent before sink is opened and after it is closed.") - .optflag("v", "verbose", "Enable verbose output.") - .optflag("V", "version", "Display librespot version string.") - .optopt("u", "username", "Username to sign in with.", "USERNAME") - .optopt("p", "password", "Password", "PASSWORD") - .optopt("", "proxy", "HTTP proxy to use when connecting.", "URL") - .optopt("", "ap-port", "Connect to AP with specified port. If no AP with that port are present fallback AP will be used. Available ports are usually 80, 443 and 4070.", "PORT") - .optflag("", "disable-discovery", "Disable discovery mode.") + .optflag("", EMIT_SINK_EVENTS, "Run program set by --onevent before sink is opened and after it is closed.") + .optflag("v", VERBOSE, "Enable verbose output.") + .optflag("V", VERSION, "Display librespot version string.") + .optopt("u", USERNAME, "Username to sign in with.", "USERNAME") + .optopt("p", PASSWORD, "Password", "PASSWORD") + .optopt("", PROXY, "HTTP proxy to use when connecting.", "URL") + .optopt("", AP_PORT, "Connect to AP with specified port. If no AP with that port are present fallback AP will be used. Available ports are usually 80, 443 and 4070.", "PORT") + .optflag("", DISABLE_DISCOVERY, "Disable discovery mode.") .optopt( "", - "backend", + BACKEND, "Audio backend to use. Use '?' to list options.", "NAME", ) .optopt( "", - "device", + DEVICE, "Audio device to use. Use '?' to list options if using alsa, portaudio or rodio.", "NAME", ) .optopt( "", - "format", + FORMAT, "Output format {F64|F32|S32|S24|S24_3|S16}. Defaults to S16.", "FORMAT", ) .optopt( "", - "dither", + DITHER, "Specify the dither algorithm to use - [none, gpdf, tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats.", "DITHER", ) .optopt("", "mixer", "Mixer to use {alsa|softvol}.", "MIXER") .optopt( "m", - "mixer-name", + MIXER_NAME, "Alsa mixer control, e.g. 'PCM' or 'Master'. Defaults to 'PCM'.", "NAME", ) .optopt( "", - "mixer-card", + MIXER_CARD, "Alsa mixer card, e.g 'hw:0' or similar from `aplay -l`. Defaults to DEVICE if specified, 'default' otherwise.", "MIXER_CARD", ) .optopt( "", - "mixer-index", + MIXER_INDEX, "Alsa index of the cards mixer. Defaults to 0.", "INDEX", ) .optopt( "", - "initial-volume", + INITIAL_VOLUME, "Initial volume in % from 0-100. Default for softvol: '50'. For the Alsa mixer: the current volume.", "VOLUME", ) .optopt( "", - "zeroconf-port", + ZEROCONF_PORT, "The port the internal server advertised over zeroconf uses.", "PORT", ) .optflag( "", - "enable-volume-normalisation", + ENABLE_VOLUME_NORMALISATION, "Play all tracks at the same volume.", ) .optopt( "", - "normalisation-method", + NORMALISATION_METHOD, "Specify the normalisation method to use {basic|dynamic}. Defaults to dynamic.", "METHOD", ) .optopt( "", - "normalisation-gain-type", + NORMALISATION_GAIN_TYPE, "Specify the normalisation gain type to use {track|album}. Defaults to album.", "TYPE", ) .optopt( "", - "normalisation-pregain", + NORMALISATION_PREGAIN, "Pregain (dB) applied by volume normalisation. Defaults to 0.", "PREGAIN", ) .optopt( "", - "normalisation-threshold", + NORMALISATION_THRESHOLD, "Threshold (dBFS) to prevent clipping. Defaults to -1.0.", "THRESHOLD", ) .optopt( "", - "normalisation-attack", + NORMALISATION_ATTACK, "Attack time (ms) in which the dynamic limiter is reducing gain. Defaults to 5.", "TIME", ) .optopt( "", - "normalisation-release", + NORMALISATION_RELEASE, "Release or decay time (ms) in which the dynamic limiter is restoring gain. Defaults to 100.", "TIME", ) .optopt( "", - "normalisation-knee", + NORMALISATION_KNEE, "Knee steepness of the dynamic limiter. Defaults to 1.0.", "KNEE", ) .optopt( "", - "volume-ctrl", + VOLUME_CTRL, "Volume control type {cubic|fixed|linear|log}. Defaults to log.", "VOLUME_CTRL" ) .optopt( "", - "volume-range", + VOLUME_RANGE, "Range of the volume control (dB). Default for softvol: 60. For the Alsa mixer: what the control supports.", "RANGE", ) .optflag( "", - "autoplay", + AUTOPLAY, "Automatically play similar songs when your music ends.", ) .optflag( "", - "disable-gapless", + DISABLE_GAPLESS, "Disable gapless playback.", ) .optflag( "", - "passthrough", + PASSTHROUGH, "Pass raw stream to output, only works for pipe and subprocess.", ); @@ -374,17 +413,17 @@ fn get_setup(args: &[String]) -> Setup { } }; - if matches.opt_present("h") { + if matches.opt_present(HELP) { println!("{}", usage(&args[0], &opts)); exit(0); } - if matches.opt_present("version") { + if matches.opt_present(VERSION) { print_version(); exit(0); } - let verbose = matches.opt_present("verbose"); + let verbose = matches.opt_present(VERBOSE); setup_logging(verbose); info!( @@ -395,7 +434,7 @@ fn get_setup(args: &[String]) -> Setup { build_id = version::BUILD_ID ); - let backend_name = matches.opt_str("backend"); + let backend_name = matches.opt_str(BACKEND); if backend_name == Some("?".into()) { list_backends(); exit(0); @@ -404,40 +443,41 @@ fn get_setup(args: &[String]) -> Setup { let backend = audio_backend::find(backend_name).expect("Invalid backend"); let format = matches - .opt_str("format") - .as_ref() - .map(|format| AudioFormat::try_from(format).expect("Invalid output format")) + .opt_str(FORMAT) + .as_deref() + .map(|format| AudioFormat::from_str(format).expect("Invalid output format")) .unwrap_or_default(); - let device = matches.opt_str("device"); + let device = matches.opt_str(DEVICE); if device == Some("?".into()) { backend(device, format); exit(0); } - let mixer_name = matches.opt_str("mixer"); - let mixer = mixer::find(mixer_name.as_ref()).expect("Invalid mixer"); + let mixer_name = matches.opt_str(MIXER_NAME); + let mixer = mixer::find(mixer_name.as_deref()).expect("Invalid mixer"); let mixer_config = { - let card = matches.opt_str("mixer-card").unwrap_or_else(|| { + let card = matches.opt_str(MIXER_CARD).unwrap_or_else(|| { if let Some(ref device_name) = device { device_name.to_string() } else { - String::from("default") + MixerConfig::default().card } }); let index = matches - .opt_str("mixer-index") + .opt_str(MIXER_INDEX) .map(|index| index.parse::().unwrap()) .unwrap_or(0); let control = matches - .opt_str("mixer-name") - .unwrap_or_else(|| String::from("PCM")); + .opt_str(MIXER_NAME) + .unwrap_or_else(|| MixerConfig::default().control); let mut volume_range = matches - .opt_str("volume-range") + .opt_str(VOLUME_RANGE) .map(|range| range.parse::().unwrap()) - .unwrap_or_else(|| match mixer_name.as_ref().map(AsRef::as_ref) { - Some("alsa") => 0.0, // let Alsa query the control + .unwrap_or_else(|| match mixer_name.as_deref() { + #[cfg(feature = "alsa-backend")] + Some(AlsaMixer::NAME) => 0.0, // let Alsa query the control _ => VolumeCtrl::DEFAULT_DB_RANGE, }); if volume_range < 0.0 { @@ -449,8 +489,8 @@ fn get_setup(args: &[String]) -> Setup { ); } let volume_ctrl = matches - .opt_str("volume-ctrl") - .as_ref() + .opt_str(VOLUME_CTRL) + .as_deref() .map(|volume_ctrl| { VolumeCtrl::from_str_with_range(volume_ctrl, volume_range) .expect("Invalid volume control type") @@ -472,26 +512,26 @@ fn get_setup(args: &[String]) -> Setup { let cache = { let audio_dir; let system_dir; - if matches.opt_present("disable-audio-cache") { + if matches.opt_present(DISABLE_AUDIO_CACHE) { audio_dir = None; system_dir = matches - .opt_str("system-cache") - .or_else(|| matches.opt_str("c")) + .opt_str(SYSTEM_CACHE) + .or_else(|| matches.opt_str(CACHE)) .map(|p| p.into()); } else { - let cache_dir = matches.opt_str("c"); + let cache_dir = matches.opt_str(CACHE); audio_dir = cache_dir .as_ref() .map(|p| AsRef::::as_ref(p).join("files")); system_dir = matches - .opt_str("system-cache") + .opt_str(SYSTEM_CACHE) .or(cache_dir) .map(|p| p.into()); } let limit = if audio_dir.is_some() { matches - .opt_str("cache-size-limit") + .opt_str(CACHE_SIZE_LIMIT) .as_deref() .map(parse_file_size) .map(|e| { @@ -514,7 +554,7 @@ fn get_setup(args: &[String]) -> Setup { }; let initial_volume = matches - .opt_str("initial-volume") + .opt_str(INITIAL_VOLUME) .map(|initial_volume| { let volume = initial_volume.parse::().unwrap(); if volume > 100 { @@ -523,18 +563,19 @@ fn get_setup(args: &[String]) -> Setup { } (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 }) - .or_else(|| match mixer_name.as_ref().map(AsRef::as_ref) { - Some("alsa") => None, + .or_else(|| match mixer_name.as_deref() { + #[cfg(feature = "alsa-backend")] + Some(AlsaMixer::NAME) => None, _ => cache.as_ref().and_then(Cache::volume), }); let zeroconf_port = matches - .opt_str("zeroconf-port") + .opt_str(ZEROCONF_PORT) .map(|port| port.parse::().unwrap()) .unwrap_or(0); let name = matches - .opt_str("name") + .opt_str(NAME) .unwrap_or_else(|| "Librespot".to_string()); let credentials = { @@ -547,8 +588,8 @@ fn get_setup(args: &[String]) -> Setup { }; get_credentials( - matches.opt_str("username"), - matches.opt_str("password"), + matches.opt_str(USERNAME), + matches.opt_str(PASSWORD), cached_credentials, password, ) @@ -560,7 +601,7 @@ fn get_setup(args: &[String]) -> Setup { SessionConfig { user_agent: version::VERSION_STRING.to_string(), device_id, - proxy: matches.opt_str("proxy").or_else(|| std::env::var("http_proxy").ok()).map( + proxy: matches.opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map( |s| { match Url::parse(&s) { Ok(url) => { @@ -578,41 +619,41 @@ fn get_setup(args: &[String]) -> Setup { }, ), ap_port: matches - .opt_str("ap-port") + .opt_str(AP_PORT) .map(|port| port.parse::().expect("Invalid port")), } }; let player_config = { let bitrate = matches - .opt_str("b") - .as_ref() + .opt_str(BITRATE) + .as_deref() .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate")) .unwrap_or_default(); - let gapless = !matches.opt_present("disable-gapless"); + let gapless = !matches.opt_present(DISABLE_GAPLESS); - let normalisation = matches.opt_present("enable-volume-normalisation"); + let normalisation = matches.opt_present(ENABLE_VOLUME_NORMALISATION); let normalisation_method = matches - .opt_str("normalisation-method") - .as_ref() + .opt_str(NORMALISATION_METHOD) + .as_deref() .map(|method| { NormalisationMethod::from_str(method).expect("Invalid normalisation method") }) .unwrap_or_default(); let normalisation_type = matches - .opt_str("normalisation-gain-type") - .as_ref() + .opt_str(NORMALISATION_GAIN_TYPE) + .as_deref() .map(|gain_type| { NormalisationType::from_str(gain_type).expect("Invalid normalisation type") }) .unwrap_or_default(); let normalisation_pregain = matches - .opt_str("normalisation-pregain") + .opt_str(NORMALISATION_PREGAIN) .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) .unwrap_or(PlayerConfig::default().normalisation_pregain); let normalisation_threshold = matches - .opt_str("normalisation-threshold") + .opt_str(NORMALISATION_THRESHOLD) .map(|threshold| { db_to_ratio( threshold @@ -622,19 +663,23 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(PlayerConfig::default().normalisation_threshold); let normalisation_attack = matches - .opt_str("normalisation-attack") - .map(|attack| attack.parse::().expect("Invalid attack float value") / MILLIS) + .opt_str(NORMALISATION_ATTACK) + .map(|attack| { + Duration::from_millis(attack.parse::().expect("Invalid attack value")) + }) .unwrap_or(PlayerConfig::default().normalisation_attack); let normalisation_release = matches - .opt_str("normalisation-release") - .map(|release| release.parse::().expect("Invalid release float value") / MILLIS) + .opt_str(NORMALISATION_RELEASE) + .map(|release| { + Duration::from_millis(release.parse::().expect("Invalid release value")) + }) .unwrap_or(PlayerConfig::default().normalisation_release); let normalisation_knee = matches - .opt_str("normalisation-knee") + .opt_str(NORMALISATION_KNEE) .map(|knee| knee.parse::().expect("Invalid knee float value")) .unwrap_or(PlayerConfig::default().normalisation_knee); - let ditherer_name = matches.opt_str("dither"); + let ditherer_name = matches.opt_str(DITHER); let ditherer = match ditherer_name.as_deref() { // explicitly disabled on command line Some("none") => None, @@ -654,7 +699,7 @@ fn get_setup(args: &[String]) -> Setup { }, }; - let passthrough = matches.opt_present("passthrough"); + let passthrough = matches.opt_present(PASSTHROUGH); PlayerConfig { bitrate, @@ -674,12 +719,12 @@ fn get_setup(args: &[String]) -> Setup { let connect_config = { let device_type = matches - .opt_str("device-type") - .as_ref() + .opt_str(DEVICE_TYPE) + .as_deref() .map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type")) .unwrap_or_default(); let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed); - let autoplay = matches.opt_present("autoplay"); + let autoplay = matches.opt_present(AUTOPLAY); ConnectConfig { name, @@ -690,9 +735,9 @@ fn get_setup(args: &[String]) -> Setup { } }; - let enable_discovery = !matches.opt_present("disable-discovery"); - let player_event_program = matches.opt_str("onevent"); - let emit_sink_events = matches.opt_present("emit-sink-events"); + let enable_discovery = !matches.opt_present(DISABLE_DISCOVERY); + let player_event_program = matches.opt_str(ONEVENT); + let emit_sink_events = matches.opt_present(EMIT_SINK_EVENTS); Setup { format, @@ -714,8 +759,9 @@ fn get_setup(args: &[String]) -> Setup { #[tokio::main(flavor = "current_thread")] async fn main() { - if env::var("RUST_BACKTRACE").is_err() { - env::set_var("RUST_BACKTRACE", "full") + const RUST_BACKTRACE: &str = "RUST_BACKTRACE"; + if env::var(RUST_BACKTRACE).is_err() { + env::set_var(RUST_BACKTRACE, "full") } let args: Vec = std::env::args().collect();