Various code improvements (#777)

* Remove deprecated use of std::u16::MAX
* Use `FromStr` for fallible `&str` conversions
* DRY up strings into constants
* Change `as_ref().map()` into `as_deref()`
* Use `Duration` for time constants and functions
* Optimize `Vec` with response times
* Move comments for `rustdoc` to parse
This commit is contained in:
Roderick van Domburg 2021-05-31 22:32:39 +02:00 committed by GitHub
parent bae1834988
commit ad19b69bfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 433 additions and 309 deletions

View file

@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### 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] 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, playback] Moved volume controls from `librespot-connect` to `librespot-playback` crate
- [connect] Synchronize player volume with mixer volume on playback - [connect] Synchronize player volume with mixer volume on playback
- [playback] Store and pass samples in 64-bit floating point - [playback] Store and pass samples in 64-bit floating point

View file

@ -18,70 +18,70 @@ use tokio::sync::{mpsc, oneshot};
use self::receive::{audio_file_fetch, request_range}; use self::receive::{audio_file_fetch, request_range};
use crate::range_set::{Range, RangeSet}; 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; 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; 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 ping time that is used for calculations before a ping time was actually measured.
// The pig 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
// 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.
// 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.
// 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
// Note: the calculations are done using the nominal bitrate of the file. The actual amount /// of audio data may be larger or smaller.
// 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`, but the time is taken as a factor of the ping
// 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` and
// time to the Spotify server. /// `READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS` are obeyed.
// Both, READ_AHEAD_BEFORE_PLAYBACK_SECONDS and READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS are /// Note: the calculations are done using the nominal bitrate of the file. The actual amount
// obeyed. /// of audio data may be larger or smaller.
// Note: the calculations are done using the nominal bitrate of the file. The actual amount pub const READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS: f32 = 2.0;
// of audio data may be larger or smaller.
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
// While playing back, this many seconds of data ahead of the current read position are /// requested.
// requested. /// Note: the calculations are done using the nominal bitrate of the file. The actual amount
// Note: the calculations are done using the nominal bitrate of the file. The actual amount /// of audio data may be larger or smaller.
// 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`, but the time is taken as a factor of the ping
// Same as READ_AHEAD_DURING_PLAYBACK_SECONDS, but the time is taken as a factor of the ping /// time to the Spotify server.
// time to the Spotify server. /// Note: the calculations are done using the nominal bitrate of the file. The actual amount
// Note: the calculations are done using the nominal bitrate of the file. The actual amount /// of audio data may be larger or smaller.
// 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,
// 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 pre-fetched in addition to the read ahead settings above. The threshold for requesting more /// data is calculated as `<pending bytes> < PREFETCH_THRESHOLD_FACTOR * <ping time> * <nominal data rate>`
// data is calculated as const PREFETCH_THRESHOLD_FACTOR: f32 = 4.0;
// <pending bytes> < PREFETCH_THRESHOLD_FACTOR * <ping time> * <nominal data rate>
const FAST_PREFETCH_THRESHOLD_FACTOR: f64 = 1.5; /// Similar to `PREFETCH_THRESHOLD_FACTOR`, but it also takes the current download rate into account.
// Similar to PREFETCH_THRESHOLD_FACTOR, but it also takes the current download rate into account. /// The formula used is `<pending bytes> < FAST_PREFETCH_THRESHOLD_FACTOR * <ping time> * <measured download rate>`
// The formula used is /// This mechanism allows for fast downloading of the remainder of the file. The number should be larger
// <pending bytes> < FAST_PREFETCH_THRESHOLD_FACTOR * <ping time> * <measured download rate> /// than `1.0` so the download rate ramps up until the bandwidth is saturated. The larger the value, the faster
// This mechanism allows for fast downloading of the remainder of the file. The number should be larger /// the download rate ramps up. However, this comes at the cost that it might hurt ping time if a seek is
// than 1 so the download rate ramps up until the bandwidth is saturated. The larger the value, the faster /// performed while downloading. Values smaller than `1.0` cause the download rate to collapse and effectively
// the download rate ramps up. However, this comes at the cost that it might hurt ping-time if a seek is /// only `PREFETCH_THRESHOLD_FACTOR` is in effect. Thus, set to `0.0` if bandwidth saturation is not wanted.
// performed while downloading. Values smaller than 1 cause the download rate to collapse and effectively const FAST_PREFETCH_THRESHOLD_FACTOR: f32 = 1.5;
// only PREFETCH_THRESHOLD_FACTOR is in effect. Thus, set to zero if bandwidth saturation is not wanted.
/// 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; 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 /// The time we will wait to obtain status updates on downloading.
// for playback to be delayed leading to a buffer underrun. This limit has the effect that a new const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(1);
// pre-fetch request is only sent if less than MAX_PREFETCH_REQUESTS are pending.
pub enum AudioFile { pub enum AudioFile {
Cached(fs::File), Cached(fs::File),
@ -131,10 +131,10 @@ impl StreamLoaderController {
}) })
} }
pub fn ping_time_ms(&self) -> usize { pub fn ping_time(&self) -> Duration {
self.stream_shared.as_ref().map_or(0, |shared| { Duration::from_millis(self.stream_shared.as_ref().map_or(0, |shared| {
shared.ping_time_ms.load(atomic::Ordering::Relaxed) shared.ping_time_ms.load(atomic::Ordering::Relaxed) as u64
}) }))
} }
fn send_stream_loader_command(&self, command: StreamLoaderCommand) { fn send_stream_loader_command(&self, command: StreamLoaderCommand) {
@ -170,7 +170,7 @@ impl StreamLoaderController {
{ {
download_status = shared download_status = shared
.cond .cond
.wait_timeout(download_status, Duration::from_millis(1000)) .wait_timeout(download_status, DOWNLOAD_TIMEOUT)
.unwrap() .unwrap()
.0; .0;
if range.length if range.length
@ -271,10 +271,10 @@ impl AudioFile {
let mut initial_data_length = if play_from_beginning { let mut initial_data_length = if play_from_beginning {
INITIAL_DOWNLOAD_SIZE INITIAL_DOWNLOAD_SIZE
+ max( + max(
(READ_AHEAD_DURING_PLAYBACK_SECONDS * bytes_per_second as f64) as usize, (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize,
(INITIAL_PING_TIME_ESTIMATE_SECONDS (INITIAL_PING_TIME_ESTIMATE.as_secs_f32()
* READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS * READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS
* bytes_per_second as f64) as usize, * bytes_per_second as f32) as usize,
) )
} else { } else {
INITIAL_DOWNLOAD_SIZE INITIAL_DOWNLOAD_SIZE
@ -368,7 +368,7 @@ impl AudioFileStreaming {
let read_file = write_file.reopen().unwrap(); 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) = let (stream_loader_command_tx, stream_loader_command_rx) =
mpsc::unbounded_channel::<StreamLoaderCommand>(); mpsc::unbounded_channel::<StreamLoaderCommand>();
@ -405,17 +405,19 @@ impl Read for AudioFileStreaming {
let length_to_request = match *(self.shared.download_strategy.lock().unwrap()) { let length_to_request = match *(self.shared.download_strategy.lock().unwrap()) {
DownloadStrategy::RandomAccess() => length, DownloadStrategy::RandomAccess() => length,
DownloadStrategy::Streaming() => { DownloadStrategy::Streaming() => {
// Due to the read-ahead stuff, we potentially request more than the actual reqeust demanded. // Due to the read-ahead stuff, we potentially request more than the actual request demanded.
let ping_time_seconds = let ping_time_seconds = Duration::from_millis(
0.0001 * self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64; self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as u64,
)
.as_secs_f32();
let length_to_request = length let length_to_request = length
+ max( + max(
(READ_AHEAD_DURING_PLAYBACK_SECONDS * self.shared.stream_data_rate as f64) (READ_AHEAD_DURING_PLAYBACK.as_secs_f32()
as usize, * self.shared.stream_data_rate as f32) as usize,
(READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS
* ping_time_seconds * 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) min(length_to_request, self.shared.file_size - offset)
} }
@ -449,7 +451,7 @@ impl Read for AudioFileStreaming {
download_status = self download_status = self
.shared .shared
.cond .cond
.wait_timeout(download_status, Duration::from_millis(1000)) .wait_timeout(download_status, DOWNLOAD_TIMEOUT)
.unwrap() .unwrap()
.0; .0;
} }

View file

@ -1,8 +1,9 @@
use std::cmp::{max, min}; use std::cmp::{max, min};
use std::io::{Seek, SeekFrom, Write}; use std::io::{Seek, SeekFrom, Write};
use std::sync::{atomic, Arc}; use std::sync::{atomic, Arc};
use std::time::Instant; use std::time::{Duration, Instant};
use atomic::Ordering;
use byteorder::{BigEndian, WriteBytesExt}; use byteorder::{BigEndian, WriteBytesExt};
use bytes::Bytes; use bytes::Bytes;
use futures_util::StreamExt; use futures_util::StreamExt;
@ -16,7 +17,7 @@ use crate::range_set::{Range, RangeSet};
use super::{AudioFileShared, DownloadStrategy, StreamLoaderCommand}; use super::{AudioFileShared, DownloadStrategy, StreamLoaderCommand};
use super::{ 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, MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR,
}; };
@ -57,7 +58,7 @@ struct PartialFileData {
} }
enum ReceivedData { enum ReceivedData {
ResponseTimeMs(usize), ResponseTime(Duration),
Data(PartialFileData), Data(PartialFileData),
} }
@ -74,7 +75,7 @@ async fn receive_data(
let old_number_of_request = shared let old_number_of_request = shared
.number_of_open_requests .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; let mut measure_ping_time = old_number_of_request == 0;
@ -86,14 +87,11 @@ async fn receive_data(
}; };
if measure_ping_time { if measure_ping_time {
let duration = Instant::now() - request_sent_time; let mut duration = Instant::now() - request_sent_time;
let duration_ms: u64; if duration > MAXIMUM_ASSUMED_PING_TIME {
if 0.001 * (duration.as_millis() as f64) > MAXIMUM_ASSUMED_PING_TIME_SECONDS { duration = MAXIMUM_ASSUMED_PING_TIME;
duration_ms = (MAXIMUM_ASSUMED_PING_TIME_SECONDS * 1000.0) as u64;
} else {
duration_ms = duration.as_millis() as u64;
} }
let _ = file_data_tx.send(ReceivedData::ResponseTimeMs(duration_ms as usize)); let _ = file_data_tx.send(ReceivedData::ResponseTime(duration));
measure_ping_time = false; measure_ping_time = false;
} }
let data_size = data.len(); let data_size = data.len();
@ -127,7 +125,7 @@ async fn receive_data(
shared shared
.number_of_open_requests .number_of_open_requests
.fetch_sub(1, atomic::Ordering::SeqCst); .fetch_sub(1, Ordering::SeqCst);
if result.is_err() { if result.is_err() {
warn!( warn!(
@ -149,7 +147,7 @@ struct AudioFileFetch {
file_data_tx: mpsc::UnboundedSender<ReceivedData>, file_data_tx: mpsc::UnboundedSender<ReceivedData>,
complete_tx: Option<oneshot::Sender<NamedTempFile>>, complete_tx: Option<oneshot::Sender<NamedTempFile>>,
network_response_times_ms: Vec<usize>, network_response_times: Vec<Duration>,
} }
// Might be replaced by enum from std once stable // Might be replaced by enum from std once stable
@ -237,7 +235,7 @@ impl AudioFileFetch {
// download data from after the current read position first // download data from after the current read position first
let mut tail_end = RangeSet::new(); 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( tail_end.add_range(&Range::new(
read_position, read_position,
self.shared.file_size - read_position, self.shared.file_size - read_position,
@ -267,26 +265,23 @@ impl AudioFileFetch {
fn handle_file_data(&mut self, data: ReceivedData) -> ControlFlow { fn handle_file_data(&mut self, data: ReceivedData) -> ControlFlow {
match data { match data {
ReceivedData::ResponseTimeMs(response_time_ms) => { ReceivedData::ResponseTime(response_time) => {
trace!("Ping time estimated as: {} ms.", response_time_ms); trace!("Ping time estimated as: {}ms", response_time.as_millis());
// 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 // record the response time
self.network_response_times_ms.push(response_time_ms); self.network_response_times.push(response_time);
// prune old response times. Keep at most three.
while self.network_response_times_ms.len() > 3 {
self.network_response_times_ms.remove(0);
}
// stats::median is experimental. So we calculate the median of up to three ourselves. // 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() { let ping_time = match self.network_response_times.len() {
1 => self.network_response_times_ms[0] as usize, 1 => self.network_response_times[0],
2 => { 2 => (self.network_response_times[0] + self.network_response_times[1]) / 2,
((self.network_response_times_ms[0] + self.network_response_times_ms[1])
/ 2) as usize
}
3 => { 3 => {
let mut times = self.network_response_times_ms.clone(); let mut times = self.network_response_times.clone();
times.sort_unstable(); times.sort_unstable();
times[1] times[1]
} }
@ -296,7 +291,7 @@ impl AudioFileFetch {
// store our new estimate for everyone to see // store our new estimate for everyone to see
self.shared self.shared
.ping_time_ms .ping_time_ms
.store(ping_time_ms, atomic::Ordering::Relaxed); .store(ping_time.as_millis() as usize, Ordering::Relaxed);
} }
ReceivedData::Data(data) => { ReceivedData::Data(data) => {
self.output self.output
@ -390,7 +385,7 @@ pub(super) async fn audio_file_fetch(
file_data_tx, file_data_tx,
complete_tx: Some(complete_tx), complete_tx: Some(complete_tx),
network_response_times_ms: Vec::new(), network_response_times: Vec::with_capacity(3),
}; };
loop { loop {
@ -408,10 +403,8 @@ pub(super) async fn audio_file_fetch(
} }
if fetch.get_download_strategy() == DownloadStrategy::Streaming() { if fetch.get_download_strategy() == DownloadStrategy::Streaming() {
let number_of_open_requests = fetch let number_of_open_requests =
.shared fetch.shared.number_of_open_requests.load(Ordering::SeqCst);
.number_of_open_requests
.load(atomic::Ordering::SeqCst);
if number_of_open_requests < MAX_PREFETCH_REQUESTS { if number_of_open_requests < MAX_PREFETCH_REQUESTS {
let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_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 = 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 download_rate = fetch.session.channel().get_download_rate_estimate();
let desired_pending_bytes = max( let desired_pending_bytes = max(
(PREFETCH_THRESHOLD_FACTOR (PREFETCH_THRESHOLD_FACTOR
* ping_time_seconds * ping_time_seconds
* fetch.shared.stream_data_rate as f64) as usize, * fetch.shared.stream_data_rate as f32) as usize,
(FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f64) (FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f32)
as usize, as usize,
); );

View file

@ -11,6 +11,6 @@ mod range_set;
pub use decrypt::AudioDecrypt; pub use decrypt::AudioDecrypt;
pub use fetch::{AudioFile, StreamLoaderController}; pub use fetch::{AudioFile, StreamLoaderController};
pub use fetch::{ pub use fetch::{
READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS, READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK,
READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS,
}; };

View file

@ -88,7 +88,7 @@ const CONTEXT_TRACKS_HISTORY: usize = 10;
const CONTEXT_FETCH_THRESHOLD: u32 = 5; const CONTEXT_FETCH_THRESHOLD: u32 = 5;
const VOLUME_STEPS: i64 = 64; 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 { pub struct Spirc {
commands: mpsc::UnboundedSender<SpircCommand>, commands: mpsc::UnboundedSender<SpircCommand>,

View file

@ -23,6 +23,8 @@ component! {
} }
} }
const ONE_SECOND_IN_MS: usize = 1000;
#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)]
pub struct ChannelError; pub struct ChannelError;
@ -74,8 +76,11 @@ impl ChannelManager {
self.lock(|inner| { self.lock(|inner| {
let current_time = Instant::now(); let current_time = Instant::now();
if let Some(download_measurement_start) = inner.download_measurement_start { if let Some(download_measurement_start) = inner.download_measurement_start {
if (current_time - download_measurement_start).as_millis() > 1000 { if (current_time - download_measurement_start).as_millis()
inner.download_rate_estimate = 1000 * inner.download_measurement_bytes > 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; / (current_time - download_measurement_start).as_millis() as usize;
inner.download_measurement_start = Some(current_time); inner.download_measurement_start = Some(current_time);
inner.download_measurement_bytes = 0; inner.download_measurement_bytes = 0;

View file

@ -2,7 +2,7 @@ use super::{Open, Sink, SinkAsBytes};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; 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::device_name::HintIter;
use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; use alsa::pcm::{Access, Format, Frames, HwParams, PCM};
use alsa::{Direction, Error, ValueOr}; use alsa::{Direction, Error, ValueOr};
@ -10,8 +10,9 @@ use std::cmp::min;
use std::ffi::CString; use std::ffi::CString;
use std::io; use std::io;
use std::process::exit; 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; const BUFFERED_PERIODS: Frames = 4;
pub struct AlsaSink { 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) // latency = period_size * periods / (rate * bytes_per_frame)
// For stereo samples encoded as 32-bit float, one frame has a length of eight bytes. // 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 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)?; let hwp = HwParams::any(&pcm)?;
hwp.set_access(Access::RWInterleaved)?; hwp.set_access(Access::RWInterleaved)?;
@ -80,7 +82,7 @@ impl Open for AlsaSink {
fn open(device: Option<String>, format: AudioFormat) -> Self { fn open(device: Option<String>, format: AudioFormat) -> Self {
info!("Using Alsa sink with format: {:?}", format); info!("Using Alsa sink with format: {:?}", format);
let name = match device.as_ref().map(AsRef::as_ref) { let name = match device.as_deref() {
Some("?") => { Some("?") => {
println!("Listing available Alsa outputs:"); println!("Listing available Alsa outputs:");
list_outputs(); list_outputs();
@ -162,6 +164,8 @@ impl SinkAsBytes for AlsaSink {
} }
impl AlsaSink { impl AlsaSink {
pub const NAME: &'static str = "alsa";
fn write_buf(&mut self) { fn write_buf(&mut self) {
let pcm = self.pcm.as_mut().unwrap(); let pcm = self.pcm.as_mut().unwrap();
let io = pcm.io_bytes(); let io = pcm.io_bytes();

View file

@ -2,7 +2,7 @@ use super::{Open, Sink, SinkAsBytes};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use crate::{NUM_CHANNELS, SAMPLE_RATE};
use gstreamer as gst; use gstreamer as gst;
use gstreamer_app as gst_app; use gstreamer_app as gst_app;
@ -139,3 +139,7 @@ impl SinkAsBytes for GstreamerSink {
Ok(()) Ok(())
} }
} }
impl GstreamerSink {
pub const NAME: &'static str = "gstreamer";
}

View file

@ -2,7 +2,7 @@ use super::{Open, Sink};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use crate::player::NUM_CHANNELS; use crate::NUM_CHANNELS;
use jack::{ use jack::{
AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope,
}; };
@ -81,3 +81,7 @@ impl Sink for JackSink {
Ok(()) Ok(())
} }
} }
impl JackSink {
pub const NAME: &'static str = "jackaudio";
}

View file

@ -90,6 +90,8 @@ use self::gstreamer::GstreamerSink;
#[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] #[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))]
mod rodio; mod rodio;
#[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))]
use self::rodio::RodioSink;
#[cfg(feature = "sdl-backend")] #[cfg(feature = "sdl-backend")]
mod sdl; mod sdl;
@ -104,23 +106,23 @@ use self::subprocess::SubprocessSink;
pub const BACKENDS: &[(&str, SinkBuilder)] = &[ pub const BACKENDS: &[(&str, SinkBuilder)] = &[
#[cfg(feature = "rodio-backend")] #[cfg(feature = "rodio-backend")]
("rodio", rodio::mk_rodio), // default goes first (RodioSink::NAME, rodio::mk_rodio), // default goes first
#[cfg(feature = "alsa-backend")] #[cfg(feature = "alsa-backend")]
("alsa", mk_sink::<AlsaSink>), (AlsaSink::NAME, mk_sink::<AlsaSink>),
#[cfg(feature = "portaudio-backend")] #[cfg(feature = "portaudio-backend")]
("portaudio", mk_sink::<PortAudioSink>), (PortAudioSink::NAME, mk_sink::<PortAudioSink>),
#[cfg(feature = "pulseaudio-backend")] #[cfg(feature = "pulseaudio-backend")]
("pulseaudio", mk_sink::<PulseAudioSink>), (PulseAudioSink::NAME, mk_sink::<PulseAudioSink>),
#[cfg(feature = "jackaudio-backend")] #[cfg(feature = "jackaudio-backend")]
("jackaudio", mk_sink::<JackSink>), (JackSink::NAME, mk_sink::<JackSink>),
#[cfg(feature = "gstreamer-backend")] #[cfg(feature = "gstreamer-backend")]
("gstreamer", mk_sink::<GstreamerSink>), (GstreamerSink::NAME, mk_sink::<GstreamerSink>),
#[cfg(feature = "rodiojack-backend")] #[cfg(feature = "rodiojack-backend")]
("rodiojack", rodio::mk_rodiojack), ("rodiojack", rodio::mk_rodiojack),
#[cfg(feature = "sdl-backend")] #[cfg(feature = "sdl-backend")]
("sdl", mk_sink::<SdlSink>), (SdlSink::NAME, mk_sink::<SdlSink>),
("pipe", mk_sink::<StdoutSink>), (StdoutSink::NAME, mk_sink::<StdoutSink>),
("subprocess", mk_sink::<SubprocessSink>), (SubprocessSink::NAME, mk_sink::<SubprocessSink>),
]; ];
pub fn find(name: Option<String>) -> Option<SinkBuilder> { pub fn find(name: Option<String>) -> Option<SinkBuilder> {

View file

@ -34,3 +34,7 @@ impl SinkAsBytes for StdoutSink {
Ok(()) Ok(())
} }
} }
impl StdoutSink {
pub const NAME: &'static str = "pipe";
}

View file

@ -2,7 +2,7 @@ use super::{Open, Sink};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; 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::device::{get_default_output_index, DeviceIndex, DeviceInfo};
use portaudio_rs::stream::*; use portaudio_rs::stream::*;
use std::io; use std::io;
@ -57,7 +57,7 @@ impl<'a> Open for PortAudioSink<'a> {
portaudio_rs::initialize().unwrap(); portaudio_rs::initialize().unwrap();
let device_idx = match device.as_ref().map(AsRef::as_ref) { let device_idx = match device.as_deref() {
Some("?") => { Some("?") => {
list_outputs(); list_outputs();
exit(0) exit(0)
@ -178,3 +178,7 @@ impl<'a> Drop for PortAudioSink<'a> {
portaudio_rs::terminate().unwrap(); portaudio_rs::terminate().unwrap();
} }
} }
impl<'a> PortAudioSink<'a> {
pub const NAME: &'static str = "portaudio";
}

View file

@ -2,7 +2,7 @@ use super::{Open, Sink, SinkAsBytes};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; 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_binding::{self as pulse, stream::Direction};
use libpulse_simple_binding::Simple; use libpulse_simple_binding::Simple;
use std::io; use std::io;
@ -55,7 +55,7 @@ impl Sink for PulseAudioSink {
return Ok(()); return Ok(());
} }
let device = self.device.as_ref().map(|s| (*s).as_str()); let device = self.device.as_deref();
let result = Simple::new( let result = Simple::new(
None, // Use the default server. None, // Use the default server.
APP_NAME, // Our application's name. APP_NAME, // Our application's name.
@ -104,3 +104,7 @@ impl SinkAsBytes for PulseAudioSink {
} }
} }
} }
impl PulseAudioSink {
pub const NAME: &'static str = "pulseaudio";
}

View file

@ -1,5 +1,6 @@
use std::process::exit; use std::process::exit;
use std::{io, thread, time}; use std::time::Duration;
use std::{io, thread};
use cpal::traits::{DeviceTrait, HostTrait}; use cpal::traits::{DeviceTrait, HostTrait};
use thiserror::Error; use thiserror::Error;
@ -8,7 +9,7 @@ use super::Sink;
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use crate::{NUM_CHANNELS, SAMPLE_RATE};
#[cfg(all( #[cfg(all(
feature = "rodiojack-backend", feature = "rodiojack-backend",
@ -203,8 +204,12 @@ impl Sink for RodioSink {
// 44100 elements --> about 27 chunks // 44100 elements --> about 27 chunks
while self.rodio_sink.len() > 26 { while self.rodio_sink.len() > 26 {
// sleep and wait for rodio to drain a bit // sleep and wait for rodio to drain a bit
thread::sleep(time::Duration::from_millis(10)); thread::sleep(Duration::from_millis(10));
} }
Ok(()) Ok(())
} }
} }
impl RodioSink {
pub const NAME: &'static str = "rodio";
}

View file

@ -2,9 +2,10 @@ use super::{Open, Sink};
use crate::config::AudioFormat; use crate::config::AudioFormat;
use crate::convert::Converter; use crate::convert::Converter;
use crate::decoder::AudioPacket; use crate::decoder::AudioPacket;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use crate::{NUM_CHANNELS, SAMPLE_RATE};
use sdl2::audio::{AudioQueue, AudioSpecDesired}; use sdl2::audio::{AudioQueue, AudioSpecDesired};
use std::{io, thread, time}; use std::time::Duration;
use std::{io, thread};
pub enum SdlSink { pub enum SdlSink {
F32(AudioQueue<f32>), F32(AudioQueue<f32>),
@ -86,7 +87,7 @@ impl Sink for SdlSink {
($queue: expr, $size: expr) => {{ ($queue: expr, $size: expr) => {{
// sleep and wait for sdl thread to drain the queue a bit // sleep and wait for sdl thread to drain the queue a bit
while $queue.size() > (NUM_CHANNELS as u32 * $size as u32 * SAMPLE_RATE) { 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(()) Ok(())
} }
} }
impl SdlSink {
pub const NAME: &'static str = "sdl";
}

View file

@ -62,3 +62,7 @@ impl SinkAsBytes for SubprocessSink {
Ok(()) Ok(())
} }
} }
impl SubprocessSink {
pub const NAME: &'static str = "subprocess";
}

View file

@ -2,9 +2,9 @@ use super::player::db_to_ratio;
use crate::convert::i24; use crate::convert::i24;
pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer}; pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer};
use std::convert::TryFrom;
use std::mem; use std::mem;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration;
#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
pub enum Bitrate { pub enum Bitrate {
@ -41,10 +41,10 @@ pub enum AudioFormat {
S16, S16,
} }
impl TryFrom<&String> for AudioFormat { impl FromStr for AudioFormat {
type Error = (); type Err = ();
fn try_from(s: &String) -> Result<Self, Self::Error> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_uppercase().as_str() { match s.to_uppercase().as_ref() {
"F64" => Ok(Self::F64), "F64" => Ok(Self::F64),
"F32" => Ok(Self::F32), "F32" => Ok(Self::F32),
"S32" => Ok(Self::S32), "S32" => Ok(Self::S32),
@ -133,8 +133,8 @@ pub struct PlayerConfig {
pub normalisation_method: NormalisationMethod, pub normalisation_method: NormalisationMethod,
pub normalisation_pregain: f64, pub normalisation_pregain: f64,
pub normalisation_threshold: f64, pub normalisation_threshold: f64,
pub normalisation_attack: f64, pub normalisation_attack: Duration,
pub normalisation_release: f64, pub normalisation_release: Duration,
pub normalisation_knee: f64, pub normalisation_knee: f64,
// pass function pointers so they can be lazily instantiated *after* spawning a thread // pass function pointers so they can be lazily instantiated *after* spawning a thread
@ -152,8 +152,8 @@ impl Default for PlayerConfig {
normalisation_method: NormalisationMethod::default(), normalisation_method: NormalisationMethod::default(),
normalisation_pregain: 0.0, normalisation_pregain: 0.0,
normalisation_threshold: db_to_ratio(-1.0), normalisation_threshold: db_to_ratio(-1.0),
normalisation_attack: 0.005, normalisation_attack: Duration::from_millis(5),
normalisation_release: 0.1, normalisation_release: Duration::from_millis(100),
normalisation_knee: 1.0, normalisation_knee: 1.0,
passthrough: false, passthrough: false,
ditherer: Some(mk_ditherer::<TriangularDitherer>), ditherer: Some(mk_ditherer::<TriangularDitherer>),
@ -184,7 +184,7 @@ impl Default for VolumeCtrl {
} }
impl 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 // Taken from: https://www.dr-lex.be/info-stuff/volumecontrols.html
pub const DEFAULT_DB_RANGE: f64 = 60.0; pub const DEFAULT_DB_RANGE: f64 = 60.0;

View file

@ -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.; 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.; 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.; const SCALE_S16: f64 = 32768.;
pub fn scale(&mut self, sample: f64, factor: f64) -> f64 { pub fn scale(&mut self, sample: f64, factor: f64) -> f64 {

View file

@ -6,6 +6,7 @@ use lewton::samples::InterleavedSamples;
use std::error; use std::error;
use std::fmt; use std::fmt;
use std::io::{Read, Seek}; use std::io::{Read, Seek};
use std::time::Duration;
pub struct VorbisDecoder<R: Read + Seek>(OggStreamReader<R>); pub struct VorbisDecoder<R: Read + Seek>(OggStreamReader<R>);
pub struct VorbisError(lewton::VorbisError); pub struct VorbisError(lewton::VorbisError);
@ -24,7 +25,7 @@ where
R: Read + Seek, R: Read + Seek,
{ {
fn seek(&mut self, ms: i64) -> Result<(), AudioError> { 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) { match self.0.seek_absgp_pg(absgp as u64) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => Err(AudioError::VorbisError(err.into())), Err(err) => Err(AudioError::VorbisError(err.into())),

View file

@ -1,8 +1,10 @@
// Passthrough decoder for librespot // Passthrough decoder for librespot
use super::{AudioDecoder, AudioError, AudioPacket}; use super::{AudioDecoder, AudioError, AudioPacket};
use crate::SAMPLE_RATE;
use ogg::{OggReadError, Packet, PacketReader, PacketWriteEndInfo, PacketWriter}; use ogg::{OggReadError, Packet, PacketReader, PacketWriteEndInfo, PacketWriter};
use std::fmt; use std::fmt;
use std::io::{Read, Seek}; use std::io::{Read, Seek};
use std::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
fn get_header<T>(code: u8, rdr: &mut PacketReader<T>) -> Result<Box<[u8]>, PassthroughError> fn get_header<T>(code: u8, rdr: &mut PacketReader<T>) -> Result<Box<[u8]>, PassthroughError>
@ -12,7 +14,7 @@ where
let pck: Packet = rdr.read_packet_expected()?; let pck: Packet = rdr.read_packet_expected()?;
let pkt_type = pck.data[0]; let pkt_type = pck.data[0];
debug!("Vorbis header type{}", &pkt_type); debug!("Vorbis header type {}", &pkt_type);
if pkt_type != code { if pkt_type != code {
return Err(PassthroughError(OggReadError::InvalidData)); return Err(PassthroughError(OggReadError::InvalidData));
@ -96,7 +98,10 @@ impl<R: Read + Seek> AudioDecoder for PassthroughDecoder<R> {
self.stream_serial += 1; self.stream_serial += 1;
// hard-coded to 44.1 kHz // 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(_) => { Ok(_) => {
// need to set some offset for next_page() // need to set some offset for next_page()
let pck = self.rdr.read_packet().unwrap().unwrap(); let pck = self.rdr.read_packet().unwrap().unwrap();

View file

@ -61,7 +61,7 @@ impl Ditherer for TriangularDitherer {
} }
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"Triangular" Self::NAME
} }
fn noise(&mut self) -> f64 { fn noise(&mut self) -> f64 {
@ -69,6 +69,10 @@ impl Ditherer for TriangularDitherer {
} }
} }
impl TriangularDitherer {
pub const NAME: &'static str = "tpdf";
}
pub struct GaussianDitherer { pub struct GaussianDitherer {
cached_rng: ThreadRng, cached_rng: ThreadRng,
distribution: Normal<f64>, distribution: Normal<f64>,
@ -84,7 +88,7 @@ impl Ditherer for GaussianDitherer {
} }
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"Gaussian" Self::NAME
} }
fn noise(&mut self) -> f64 { fn noise(&mut self) -> f64 {
@ -92,6 +96,10 @@ impl Ditherer for GaussianDitherer {
} }
} }
impl GaussianDitherer {
pub const NAME: &'static str = "gpdf";
}
pub struct HighPassDitherer { pub struct HighPassDitherer {
active_channel: usize, active_channel: usize,
previous_noises: [f64; NUM_CHANNELS], previous_noises: [f64; NUM_CHANNELS],
@ -110,7 +118,7 @@ impl Ditherer for HighPassDitherer {
} }
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"Triangular, High Passed" Self::NAME
} }
fn noise(&mut self) -> f64 { 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<D: Ditherer + 'static>() -> Box<dyn Ditherer> { pub fn mk_ditherer<D: Ditherer + 'static>() -> Box<dyn Ditherer> {
Box::new(D::new()) Box::new(D::new())
} }
@ -130,9 +142,9 @@ pub type DithererBuilder = fn() -> Box<dyn Ditherer>;
pub fn find_ditherer(name: Option<String>) -> Option<DithererBuilder> { pub fn find_ditherer(name: Option<String>) -> Option<DithererBuilder> {
match name.as_deref() { match name.as_deref() {
Some("tpdf") => Some(mk_ditherer::<TriangularDitherer>), Some(TriangularDitherer::NAME) => Some(mk_ditherer::<TriangularDitherer>),
Some("gpdf") => Some(mk_ditherer::<GaussianDitherer>), Some(GaussianDitherer::NAME) => Some(mk_ditherer::<GaussianDitherer>),
Some("tpdf_hp") => Some(mk_ditherer::<HighPassDitherer>), Some(HighPassDitherer::NAME) => Some(mk_ditherer::<HighPassDitherer>),
_ => None, _ => None,
} }
} }

View file

@ -12,3 +12,7 @@ mod decoder;
pub mod dither; pub mod dither;
pub mod mixer; pub mod mixer;
pub mod player; 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;

View file

@ -241,6 +241,8 @@ impl Mixer for AlsaMixer {
} }
impl AlsaMixer { impl AlsaMixer {
pub const NAME: &'static str = "alsa";
fn switched_off(&self) -> bool { fn switched_off(&self) -> bool {
if !self.has_switch { if !self.has_switch {
return false; return false;

View file

@ -53,11 +53,11 @@ fn mk_sink<M: Mixer + 'static>(config: MixerConfig) -> Box<dyn Mixer> {
Box::new(M::open(config)) Box::new(M::open(config))
} }
pub fn find<T: AsRef<str>>(name: Option<T>) -> Option<MixerFn> { pub fn find(name: Option<&str>) -> Option<MixerFn> {
match name.as_ref().map(AsRef::as_ref) { match name {
None | Some("softvol") => Some(mk_sink::<SoftMixer>), None | Some(SoftMixer::NAME) => Some(mk_sink::<SoftMixer>),
#[cfg(feature = "alsa-backend")] #[cfg(feature = "alsa-backend")]
Some("alsa") => Some(mk_sink::<AlsaMixer>), Some(AlsaMixer::NAME) => Some(mk_sink::<AlsaMixer>),
_ => None, _ => None,
} }
} }

View file

@ -42,6 +42,10 @@ impl Mixer for SoftMixer {
} }
} }
impl SoftMixer {
pub const NAME: &'static str = "softmixer";
}
struct SoftVolumeApplier { struct SoftVolumeApplier {
volume: Arc<AtomicU64>, volume: Arc<AtomicU64>,
} }

View file

@ -13,8 +13,8 @@ use tokio::sync::{mpsc, oneshot};
use crate::audio::{AudioDecrypt, AudioFile, StreamLoaderController}; use crate::audio::{AudioDecrypt, AudioFile, StreamLoaderController};
use crate::audio::{ use crate::audio::{
READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_BEFORE_PLAYBACK_SECONDS, READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK,
READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS,
}; };
use crate::audio_backend::Sink; use crate::audio_backend::Sink;
use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}; 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::metadata::{AudioItem, FileFormat};
use crate::mixer::AudioFilter; use crate::mixer::AudioFilter;
pub const SAMPLE_RATE: u32 = 44100; use crate::{NUM_CHANNELS, SAMPLES_PER_SECOND};
pub const NUM_CHANNELS: u8 = 2;
pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32;
const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000;
pub const DB_VOLTAGE_RATIO: f64 = 20.0; pub const DB_VOLTAGE_RATIO: f64 = 20.0;
@ -297,14 +295,8 @@ impl Player {
debug!("Normalisation Method: {:?}", config.normalisation_method); debug!("Normalisation Method: {:?}", config.normalisation_method);
if config.normalisation_method == NormalisationMethod::Dynamic { if config.normalisation_method == NormalisationMethod::Dynamic {
debug!( debug!("Normalisation Attack: {:?}", config.normalisation_attack);
"Normalisation Attack: {:.0} ms", debug!("Normalisation Release: {:?}", config.normalisation_release);
config.normalisation_attack * 1000.0
);
debug!(
"Normalisation Release: {:.0} ms",
config.normalisation_release * 1000.0
);
debug!("Normalisation Knee: {:?}", config.normalisation_knee); debug!("Normalisation Knee: {:?}", config.normalisation_knee);
} }
} }
@ -973,12 +965,12 @@ impl Future for PlayerInternal {
let notify_about_position = match *reported_nominal_start_time { let notify_about_position = match *reported_nominal_start_time {
None => true, None => true,
Some(reported_nominal_start_time) => { 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) let lag = (Instant::now() - reported_nominal_start_time)
.as_millis() .as_millis()
as i64 as i64
- stream_position_millis as i64; - stream_position_millis as i64;
lag > 1000 lag > Duration::from_secs(1).as_millis() as i64
} }
}; };
if notify_about_position { if notify_about_position {
@ -1219,6 +1211,16 @@ impl PlayerInternal {
+ shaped_limiter_strength * self.limiter_factor; + 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. // Always check for peaks, even when the limiter is already active.
// There may be even higher peaks than we initially targeted. // There may be even higher peaks than we initially targeted.
// Check against the normalisation factor that would be applied normally. // Check against the normalisation factor that would be applied normally.
@ -1228,21 +1230,19 @@ impl PlayerInternal {
if self.limiter_release_counter > 0 { if self.limiter_release_counter > 0 {
// A peak was encountered while releasing the limiter; // A peak was encountered while releasing the limiter;
// synchronize with the current release limiter strength. // synchronize with the current release limiter strength.
self.limiter_attack_counter = (((SAMPLES_PER_SECOND self.limiter_attack_counter = (((samples_per_second
as f64 * normalisation_release)
* self.config.normalisation_release) - limiter_release_counter)
- self.limiter_release_counter as f64) / (normalisation_release / normalisation_attack))
/ (self.config.normalisation_release
/ self.config.normalisation_attack))
as u32; as u32;
self.limiter_release_counter = 0; self.limiter_release_counter = 0;
} }
self.limiter_attack_counter = self.limiter_attack_counter =
self.limiter_attack_counter.saturating_add(1); self.limiter_attack_counter.saturating_add(1);
self.limiter_strength = self.limiter_attack_counter as f64
/ (SAMPLES_PER_SECOND as f64 self.limiter_strength = limiter_attack_counter
* self.config.normalisation_attack); / (samples_per_second * normalisation_attack);
if abs_sample > self.limiter_peak_sample { if abs_sample > self.limiter_peak_sample {
self.limiter_peak_sample = abs_sample; self.limiter_peak_sample = abs_sample;
@ -1256,12 +1256,10 @@ impl PlayerInternal {
// the limiter reached full strength. For that reason // the limiter reached full strength. For that reason
// start the release by synchronizing with the current // start the release by synchronizing with the current
// attack limiter strength. // attack limiter strength.
self.limiter_release_counter = (((SAMPLES_PER_SECOND self.limiter_release_counter = (((samples_per_second
as f64 * normalisation_attack)
* self.config.normalisation_attack) - limiter_attack_counter)
- self.limiter_attack_counter as f64) * (normalisation_release / normalisation_attack))
* (self.config.normalisation_release
/ self.config.normalisation_attack))
as u32; as u32;
self.limiter_attack_counter = 0; self.limiter_attack_counter = 0;
} }
@ -1270,17 +1268,14 @@ impl PlayerInternal {
self.limiter_release_counter.saturating_add(1); self.limiter_release_counter.saturating_add(1);
if self.limiter_release_counter if self.limiter_release_counter
> (SAMPLES_PER_SECOND as f64 > (samples_per_second * normalisation_release) as u32
* self.config.normalisation_release)
as u32
{ {
self.reset_limiter(); self.reset_limiter();
} else { } else {
self.limiter_strength = ((SAMPLES_PER_SECOND as f64 self.limiter_strength = ((samples_per_second
* self.config.normalisation_release) * normalisation_release)
- self.limiter_release_counter as f64) - limiter_release_counter)
/ (SAMPLES_PER_SECOND as f64 / (samples_per_second * normalisation_release);
* self.config.normalisation_release);
} }
} }
} }
@ -1806,18 +1801,18 @@ impl PlayerInternal {
// Request our read ahead range // Request our read ahead range
let request_data_length = max( let request_data_length = max(
(READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS (READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS
* (0.001 * stream_loader_controller.ping_time_ms() as f64) * stream_loader_controller.ping_time().as_secs_f32()
* bytes_per_second as f64) as usize, * bytes_per_second as f32) as usize,
(READ_AHEAD_DURING_PLAYBACK_SECONDS * bytes_per_second as f64) as usize, (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize,
); );
stream_loader_controller.fetch_next(request_data_length); 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. // 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( let wait_for_data_length = max(
(READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS (READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS
* (0.001 * stream_loader_controller.ping_time_ms() as f64) * stream_loader_controller.ping_time().as_secs_f32()
* bytes_per_second as f64) as usize, * bytes_per_second as f32) as usize,
(READ_AHEAD_BEFORE_PLAYBACK_SECONDS * bytes_per_second as f64) 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); stream_loader_controller.fetch_next_blocking(wait_for_data_length);
} }

View file

@ -17,6 +17,8 @@ use librespot::playback::config::{
AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl, AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl,
}; };
use librespot::playback::dither; use librespot::playback::dither;
#[cfg(feature = "alsa-backend")]
use librespot::playback::mixer::alsamixer::AlsaMixer;
use librespot::playback::mixer::mappings::MappedCtrl; use librespot::playback::mixer::mappings::MappedCtrl;
use librespot::playback::mixer::{self, MixerConfig, MixerFn}; use librespot::playback::mixer::{self, MixerConfig, MixerFn};
use librespot::playback::player::{db_to_ratio, Player}; use librespot::playback::player::{db_to_ratio, Player};
@ -24,17 +26,14 @@ use librespot::playback::player::{db_to_ratio, Player};
mod player_event_handler; mod player_event_handler;
use player_event_handler::{emit_sink_event, run_program_on_events}; 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::path::Path;
use std::pin::Pin;
use std::process::exit; use std::process::exit;
use std::str::FromStr; use std::str::FromStr;
use std::{env, time::Instant}; use std::time::Duration;
use std::{ use std::time::Instant;
io::{stderr, Write},
pin::Pin,
};
const MILLIS: f64 = 1000.0;
fn device_id(name: &str) -> String { fn device_id(name: &str) -> String {
hex::encode(Sha1::digest(name.as_bytes())) hex::encode(Sha1::digest(name.as_bytes()))
@ -189,176 +188,216 @@ struct Setup {
} }
fn get_setup(args: &[String]) -> 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(); let mut opts = getopts::Options::new();
opts.optflag( opts.optflag(
"h", HELP,
"help", "help",
"Print this help menu.", "Print this help menu.",
).optopt( ).optopt(
"c", CACHE,
"cache", "cache",
"Path to a directory where files will be cached.", "Path to a directory where files will be cached.",
"PATH", "PATH",
).optopt( ).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 to a directory where system files (credentials, volume) will be cached. Can be different from cache option value.",
"PATH", "PATH",
).optopt( ).optopt(
"", "",
"cache-size-limit", CACHE_SIZE_LIMIT,
"Limits the size of the cache for audio files.", "Limits the size of the cache for audio files.",
"SIZE" "SIZE"
).optflag("", "disable-audio-cache", "Disable caching of the audio data.") ).optflag("", DISABLE_AUDIO_CACHE, "Disable caching of the audio data.")
.optopt("n", "name", "Device name.", "NAME") .optopt("n", NAME, "Device name.", "NAME")
.optopt("", "device-type", "Displayed device type.", "TYPE") .optopt("", DEVICE_TYPE, "Displayed device type.", "TYPE")
.optopt( .optopt(
"b", BITRATE,
"bitrate", "bitrate",
"Bitrate (kbps) {96|160|320}. Defaults to 160.", "Bitrate (kbps) {96|160|320}. Defaults to 160.",
"BITRATE", "BITRATE",
) )
.optopt( .optopt(
"", "",
"onevent", ONEVENT,
"Run PROGRAM when a playback event occurs.", "Run PROGRAM when a playback event occurs.",
"PROGRAM", "PROGRAM",
) )
.optflag("", "emit-sink-events", "Run program set by --onevent before sink is opened and after it is closed.") .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", VERBOSE, "Enable verbose output.")
.optflag("V", "version", "Display librespot version string.") .optflag("V", VERSION, "Display librespot version string.")
.optopt("u", "username", "Username to sign in with.", "USERNAME") .optopt("u", USERNAME, "Username to sign in with.", "USERNAME")
.optopt("p", "password", "Password", "PASSWORD") .optopt("p", PASSWORD, "Password", "PASSWORD")
.optopt("", "proxy", "HTTP proxy to use when connecting.", "URL") .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") .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("", DISABLE_DISCOVERY, "Disable discovery mode.")
.optopt( .optopt(
"", "",
"backend", BACKEND,
"Audio backend to use. Use '?' to list options.", "Audio backend to use. Use '?' to list options.",
"NAME", "NAME",
) )
.optopt( .optopt(
"", "",
"device", DEVICE,
"Audio device to use. Use '?' to list options if using alsa, portaudio or rodio.", "Audio device to use. Use '?' to list options if using alsa, portaudio or rodio.",
"NAME", "NAME",
) )
.optopt( .optopt(
"", "",
"format", FORMAT,
"Output format {F64|F32|S32|S24|S24_3|S16}. Defaults to S16.", "Output format {F64|F32|S32|S24|S24_3|S16}. Defaults to S16.",
"FORMAT", "FORMAT",
) )
.optopt( .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.", "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", "DITHER",
) )
.optopt("", "mixer", "Mixer to use {alsa|softvol}.", "MIXER") .optopt("", "mixer", "Mixer to use {alsa|softvol}.", "MIXER")
.optopt( .optopt(
"m", "m",
"mixer-name", MIXER_NAME,
"Alsa mixer control, e.g. 'PCM' or 'Master'. Defaults to 'PCM'.", "Alsa mixer control, e.g. 'PCM' or 'Master'. Defaults to 'PCM'.",
"NAME", "NAME",
) )
.optopt( .optopt(
"", "",
"mixer-card", MIXER_CARD,
"Alsa mixer card, e.g 'hw:0' or similar from `aplay -l`. Defaults to DEVICE if specified, 'default' otherwise.", "Alsa mixer card, e.g 'hw:0' or similar from `aplay -l`. Defaults to DEVICE if specified, 'default' otherwise.",
"MIXER_CARD", "MIXER_CARD",
) )
.optopt( .optopt(
"", "",
"mixer-index", MIXER_INDEX,
"Alsa index of the cards mixer. Defaults to 0.", "Alsa index of the cards mixer. Defaults to 0.",
"INDEX", "INDEX",
) )
.optopt( .optopt(
"", "",
"initial-volume", INITIAL_VOLUME,
"Initial volume in % from 0-100. Default for softvol: '50'. For the Alsa mixer: the current volume.", "Initial volume in % from 0-100. Default for softvol: '50'. For the Alsa mixer: the current volume.",
"VOLUME", "VOLUME",
) )
.optopt( .optopt(
"", "",
"zeroconf-port", ZEROCONF_PORT,
"The port the internal server advertised over zeroconf uses.", "The port the internal server advertised over zeroconf uses.",
"PORT", "PORT",
) )
.optflag( .optflag(
"", "",
"enable-volume-normalisation", ENABLE_VOLUME_NORMALISATION,
"Play all tracks at the same volume.", "Play all tracks at the same volume.",
) )
.optopt( .optopt(
"", "",
"normalisation-method", NORMALISATION_METHOD,
"Specify the normalisation method to use {basic|dynamic}. Defaults to dynamic.", "Specify the normalisation method to use {basic|dynamic}. Defaults to dynamic.",
"METHOD", "METHOD",
) )
.optopt( .optopt(
"", "",
"normalisation-gain-type", NORMALISATION_GAIN_TYPE,
"Specify the normalisation gain type to use {track|album}. Defaults to album.", "Specify the normalisation gain type to use {track|album}. Defaults to album.",
"TYPE", "TYPE",
) )
.optopt( .optopt(
"", "",
"normalisation-pregain", NORMALISATION_PREGAIN,
"Pregain (dB) applied by volume normalisation. Defaults to 0.", "Pregain (dB) applied by volume normalisation. Defaults to 0.",
"PREGAIN", "PREGAIN",
) )
.optopt( .optopt(
"", "",
"normalisation-threshold", NORMALISATION_THRESHOLD,
"Threshold (dBFS) to prevent clipping. Defaults to -1.0.", "Threshold (dBFS) to prevent clipping. Defaults to -1.0.",
"THRESHOLD", "THRESHOLD",
) )
.optopt( .optopt(
"", "",
"normalisation-attack", NORMALISATION_ATTACK,
"Attack time (ms) in which the dynamic limiter is reducing gain. Defaults to 5.", "Attack time (ms) in which the dynamic limiter is reducing gain. Defaults to 5.",
"TIME", "TIME",
) )
.optopt( .optopt(
"", "",
"normalisation-release", NORMALISATION_RELEASE,
"Release or decay time (ms) in which the dynamic limiter is restoring gain. Defaults to 100.", "Release or decay time (ms) in which the dynamic limiter is restoring gain. Defaults to 100.",
"TIME", "TIME",
) )
.optopt( .optopt(
"", "",
"normalisation-knee", NORMALISATION_KNEE,
"Knee steepness of the dynamic limiter. Defaults to 1.0.", "Knee steepness of the dynamic limiter. Defaults to 1.0.",
"KNEE", "KNEE",
) )
.optopt( .optopt(
"", "",
"volume-ctrl", VOLUME_CTRL,
"Volume control type {cubic|fixed|linear|log}. Defaults to log.", "Volume control type {cubic|fixed|linear|log}. Defaults to log.",
"VOLUME_CTRL" "VOLUME_CTRL"
) )
.optopt( .optopt(
"", "",
"volume-range", VOLUME_RANGE,
"Range of the volume control (dB). Default for softvol: 60. For the Alsa mixer: what the control supports.", "Range of the volume control (dB). Default for softvol: 60. For the Alsa mixer: what the control supports.",
"RANGE", "RANGE",
) )
.optflag( .optflag(
"", "",
"autoplay", AUTOPLAY,
"Automatically play similar songs when your music ends.", "Automatically play similar songs when your music ends.",
) )
.optflag( .optflag(
"", "",
"disable-gapless", DISABLE_GAPLESS,
"Disable gapless playback.", "Disable gapless playback.",
) )
.optflag( .optflag(
"", "",
"passthrough", PASSTHROUGH,
"Pass raw stream to output, only works for pipe and subprocess.", "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)); println!("{}", usage(&args[0], &opts));
exit(0); exit(0);
} }
if matches.opt_present("version") { if matches.opt_present(VERSION) {
print_version(); print_version();
exit(0); exit(0);
} }
let verbose = matches.opt_present("verbose"); let verbose = matches.opt_present(VERBOSE);
setup_logging(verbose); setup_logging(verbose);
info!( info!(
@ -395,7 +434,7 @@ fn get_setup(args: &[String]) -> Setup {
build_id = version::BUILD_ID build_id = version::BUILD_ID
); );
let backend_name = matches.opt_str("backend"); let backend_name = matches.opt_str(BACKEND);
if backend_name == Some("?".into()) { if backend_name == Some("?".into()) {
list_backends(); list_backends();
exit(0); exit(0);
@ -404,40 +443,41 @@ fn get_setup(args: &[String]) -> Setup {
let backend = audio_backend::find(backend_name).expect("Invalid backend"); let backend = audio_backend::find(backend_name).expect("Invalid backend");
let format = matches let format = matches
.opt_str("format") .opt_str(FORMAT)
.as_ref() .as_deref()
.map(|format| AudioFormat::try_from(format).expect("Invalid output format")) .map(|format| AudioFormat::from_str(format).expect("Invalid output format"))
.unwrap_or_default(); .unwrap_or_default();
let device = matches.opt_str("device"); let device = matches.opt_str(DEVICE);
if device == Some("?".into()) { if device == Some("?".into()) {
backend(device, format); backend(device, format);
exit(0); exit(0);
} }
let mixer_name = matches.opt_str("mixer"); let mixer_name = matches.opt_str(MIXER_NAME);
let mixer = mixer::find(mixer_name.as_ref()).expect("Invalid mixer"); let mixer = mixer::find(mixer_name.as_deref()).expect("Invalid mixer");
let mixer_config = { 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 { if let Some(ref device_name) = device {
device_name.to_string() device_name.to_string()
} else { } else {
String::from("default") MixerConfig::default().card
} }
}); });
let index = matches let index = matches
.opt_str("mixer-index") .opt_str(MIXER_INDEX)
.map(|index| index.parse::<u32>().unwrap()) .map(|index| index.parse::<u32>().unwrap())
.unwrap_or(0); .unwrap_or(0);
let control = matches let control = matches
.opt_str("mixer-name") .opt_str(MIXER_NAME)
.unwrap_or_else(|| String::from("PCM")); .unwrap_or_else(|| MixerConfig::default().control);
let mut volume_range = matches let mut volume_range = matches
.opt_str("volume-range") .opt_str(VOLUME_RANGE)
.map(|range| range.parse::<f64>().unwrap()) .map(|range| range.parse::<f64>().unwrap())
.unwrap_or_else(|| match mixer_name.as_ref().map(AsRef::as_ref) { .unwrap_or_else(|| match mixer_name.as_deref() {
Some("alsa") => 0.0, // let Alsa query the control #[cfg(feature = "alsa-backend")]
Some(AlsaMixer::NAME) => 0.0, // let Alsa query the control
_ => VolumeCtrl::DEFAULT_DB_RANGE, _ => VolumeCtrl::DEFAULT_DB_RANGE,
}); });
if volume_range < 0.0 { if volume_range < 0.0 {
@ -449,8 +489,8 @@ fn get_setup(args: &[String]) -> Setup {
); );
} }
let volume_ctrl = matches let volume_ctrl = matches
.opt_str("volume-ctrl") .opt_str(VOLUME_CTRL)
.as_ref() .as_deref()
.map(|volume_ctrl| { .map(|volume_ctrl| {
VolumeCtrl::from_str_with_range(volume_ctrl, volume_range) VolumeCtrl::from_str_with_range(volume_ctrl, volume_range)
.expect("Invalid volume control type") .expect("Invalid volume control type")
@ -472,26 +512,26 @@ fn get_setup(args: &[String]) -> Setup {
let cache = { let cache = {
let audio_dir; let audio_dir;
let system_dir; let system_dir;
if matches.opt_present("disable-audio-cache") { if matches.opt_present(DISABLE_AUDIO_CACHE) {
audio_dir = None; audio_dir = None;
system_dir = matches system_dir = matches
.opt_str("system-cache") .opt_str(SYSTEM_CACHE)
.or_else(|| matches.opt_str("c")) .or_else(|| matches.opt_str(CACHE))
.map(|p| p.into()); .map(|p| p.into());
} else { } else {
let cache_dir = matches.opt_str("c"); let cache_dir = matches.opt_str(CACHE);
audio_dir = cache_dir audio_dir = cache_dir
.as_ref() .as_ref()
.map(|p| AsRef::<Path>::as_ref(p).join("files")); .map(|p| AsRef::<Path>::as_ref(p).join("files"));
system_dir = matches system_dir = matches
.opt_str("system-cache") .opt_str(SYSTEM_CACHE)
.or(cache_dir) .or(cache_dir)
.map(|p| p.into()); .map(|p| p.into());
} }
let limit = if audio_dir.is_some() { let limit = if audio_dir.is_some() {
matches matches
.opt_str("cache-size-limit") .opt_str(CACHE_SIZE_LIMIT)
.as_deref() .as_deref()
.map(parse_file_size) .map(parse_file_size)
.map(|e| { .map(|e| {
@ -514,7 +554,7 @@ fn get_setup(args: &[String]) -> Setup {
}; };
let initial_volume = matches let initial_volume = matches
.opt_str("initial-volume") .opt_str(INITIAL_VOLUME)
.map(|initial_volume| { .map(|initial_volume| {
let volume = initial_volume.parse::<u16>().unwrap(); let volume = initial_volume.parse::<u16>().unwrap();
if volume > 100 { 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 (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16
}) })
.or_else(|| match mixer_name.as_ref().map(AsRef::as_ref) { .or_else(|| match mixer_name.as_deref() {
Some("alsa") => None, #[cfg(feature = "alsa-backend")]
Some(AlsaMixer::NAME) => None,
_ => cache.as_ref().and_then(Cache::volume), _ => cache.as_ref().and_then(Cache::volume),
}); });
let zeroconf_port = matches let zeroconf_port = matches
.opt_str("zeroconf-port") .opt_str(ZEROCONF_PORT)
.map(|port| port.parse::<u16>().unwrap()) .map(|port| port.parse::<u16>().unwrap())
.unwrap_or(0); .unwrap_or(0);
let name = matches let name = matches
.opt_str("name") .opt_str(NAME)
.unwrap_or_else(|| "Librespot".to_string()); .unwrap_or_else(|| "Librespot".to_string());
let credentials = { let credentials = {
@ -547,8 +588,8 @@ fn get_setup(args: &[String]) -> Setup {
}; };
get_credentials( get_credentials(
matches.opt_str("username"), matches.opt_str(USERNAME),
matches.opt_str("password"), matches.opt_str(PASSWORD),
cached_credentials, cached_credentials,
password, password,
) )
@ -560,7 +601,7 @@ fn get_setup(args: &[String]) -> Setup {
SessionConfig { SessionConfig {
user_agent: version::VERSION_STRING.to_string(), user_agent: version::VERSION_STRING.to_string(),
device_id, 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| { |s| {
match Url::parse(&s) { match Url::parse(&s) {
Ok(url) => { Ok(url) => {
@ -578,41 +619,41 @@ fn get_setup(args: &[String]) -> Setup {
}, },
), ),
ap_port: matches ap_port: matches
.opt_str("ap-port") .opt_str(AP_PORT)
.map(|port| port.parse::<u16>().expect("Invalid port")), .map(|port| port.parse::<u16>().expect("Invalid port")),
} }
}; };
let player_config = { let player_config = {
let bitrate = matches let bitrate = matches
.opt_str("b") .opt_str(BITRATE)
.as_ref() .as_deref()
.map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate")) .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
.unwrap_or_default(); .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 let normalisation_method = matches
.opt_str("normalisation-method") .opt_str(NORMALISATION_METHOD)
.as_ref() .as_deref()
.map(|method| { .map(|method| {
NormalisationMethod::from_str(method).expect("Invalid normalisation method") NormalisationMethod::from_str(method).expect("Invalid normalisation method")
}) })
.unwrap_or_default(); .unwrap_or_default();
let normalisation_type = matches let normalisation_type = matches
.opt_str("normalisation-gain-type") .opt_str(NORMALISATION_GAIN_TYPE)
.as_ref() .as_deref()
.map(|gain_type| { .map(|gain_type| {
NormalisationType::from_str(gain_type).expect("Invalid normalisation type") NormalisationType::from_str(gain_type).expect("Invalid normalisation type")
}) })
.unwrap_or_default(); .unwrap_or_default();
let normalisation_pregain = matches let normalisation_pregain = matches
.opt_str("normalisation-pregain") .opt_str(NORMALISATION_PREGAIN)
.map(|pregain| pregain.parse::<f64>().expect("Invalid pregain float value")) .map(|pregain| pregain.parse::<f64>().expect("Invalid pregain float value"))
.unwrap_or(PlayerConfig::default().normalisation_pregain); .unwrap_or(PlayerConfig::default().normalisation_pregain);
let normalisation_threshold = matches let normalisation_threshold = matches
.opt_str("normalisation-threshold") .opt_str(NORMALISATION_THRESHOLD)
.map(|threshold| { .map(|threshold| {
db_to_ratio( db_to_ratio(
threshold threshold
@ -622,19 +663,23 @@ fn get_setup(args: &[String]) -> Setup {
}) })
.unwrap_or(PlayerConfig::default().normalisation_threshold); .unwrap_or(PlayerConfig::default().normalisation_threshold);
let normalisation_attack = matches let normalisation_attack = matches
.opt_str("normalisation-attack") .opt_str(NORMALISATION_ATTACK)
.map(|attack| attack.parse::<f64>().expect("Invalid attack float value") / MILLIS) .map(|attack| {
Duration::from_millis(attack.parse::<u64>().expect("Invalid attack value"))
})
.unwrap_or(PlayerConfig::default().normalisation_attack); .unwrap_or(PlayerConfig::default().normalisation_attack);
let normalisation_release = matches let normalisation_release = matches
.opt_str("normalisation-release") .opt_str(NORMALISATION_RELEASE)
.map(|release| release.parse::<f64>().expect("Invalid release float value") / MILLIS) .map(|release| {
Duration::from_millis(release.parse::<u64>().expect("Invalid release value"))
})
.unwrap_or(PlayerConfig::default().normalisation_release); .unwrap_or(PlayerConfig::default().normalisation_release);
let normalisation_knee = matches let normalisation_knee = matches
.opt_str("normalisation-knee") .opt_str(NORMALISATION_KNEE)
.map(|knee| knee.parse::<f64>().expect("Invalid knee float value")) .map(|knee| knee.parse::<f64>().expect("Invalid knee float value"))
.unwrap_or(PlayerConfig::default().normalisation_knee); .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() { let ditherer = match ditherer_name.as_deref() {
// explicitly disabled on command line // explicitly disabled on command line
Some("none") => None, 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 { PlayerConfig {
bitrate, bitrate,
@ -674,12 +719,12 @@ fn get_setup(args: &[String]) -> Setup {
let connect_config = { let connect_config = {
let device_type = matches let device_type = matches
.opt_str("device-type") .opt_str(DEVICE_TYPE)
.as_ref() .as_deref()
.map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type")) .map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type"))
.unwrap_or_default(); .unwrap_or_default();
let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed); let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed);
let autoplay = matches.opt_present("autoplay"); let autoplay = matches.opt_present(AUTOPLAY);
ConnectConfig { ConnectConfig {
name, name,
@ -690,9 +735,9 @@ fn get_setup(args: &[String]) -> Setup {
} }
}; };
let enable_discovery = !matches.opt_present("disable-discovery"); let enable_discovery = !matches.opt_present(DISABLE_DISCOVERY);
let player_event_program = matches.opt_str("onevent"); let player_event_program = matches.opt_str(ONEVENT);
let emit_sink_events = matches.opt_present("emit-sink-events"); let emit_sink_events = matches.opt_present(EMIT_SINK_EVENTS);
Setup { Setup {
format, format,
@ -714,8 +759,9 @@ fn get_setup(args: &[String]) -> Setup {
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn main() { async fn main() {
if env::var("RUST_BACKTRACE").is_err() { const RUST_BACKTRACE: &str = "RUST_BACKTRACE";
env::set_var("RUST_BACKTRACE", "full") if env::var(RUST_BACKTRACE).is_err() {
env::set_var(RUST_BACKTRACE, "full")
} }
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();