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
- [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

View file

@ -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
// <pending bytes> < PREFETCH_THRESHOLD_FACTOR * <ping time> * <nominal data rate>
/// 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 `<pending bytes> < PREFETCH_THRESHOLD_FACTOR * <ping time> * <nominal data rate>`
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
// <pending bytes> < FAST_PREFETCH_THRESHOLD_FACTOR * <ping time> * <measured download rate>
// 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 `<pending bytes> < FAST_PREFETCH_THRESHOLD_FACTOR * <ping time> * <measured download rate>`
/// 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::<StreamLoaderCommand>();
@ -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;
}

View file

@ -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<ReceivedData>,
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
@ -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());
// 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_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);
}
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,
);

View file

@ -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,
};

View file

@ -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<SpircCommand>,

View file

@ -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;

View file

@ -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<String>, 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();

View file

@ -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";
}

View file

@ -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";
}

View file

@ -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>),
(AlsaSink::NAME, mk_sink::<AlsaSink>),
#[cfg(feature = "portaudio-backend")]
("portaudio", mk_sink::<PortAudioSink>),
(PortAudioSink::NAME, mk_sink::<PortAudioSink>),
#[cfg(feature = "pulseaudio-backend")]
("pulseaudio", mk_sink::<PulseAudioSink>),
(PulseAudioSink::NAME, mk_sink::<PulseAudioSink>),
#[cfg(feature = "jackaudio-backend")]
("jackaudio", mk_sink::<JackSink>),
(JackSink::NAME, mk_sink::<JackSink>),
#[cfg(feature = "gstreamer-backend")]
("gstreamer", mk_sink::<GstreamerSink>),
(GstreamerSink::NAME, mk_sink::<GstreamerSink>),
#[cfg(feature = "rodiojack-backend")]
("rodiojack", rodio::mk_rodiojack),
#[cfg(feature = "sdl-backend")]
("sdl", mk_sink::<SdlSink>),
("pipe", mk_sink::<StdoutSink>),
("subprocess", mk_sink::<SubprocessSink>),
(SdlSink::NAME, mk_sink::<SdlSink>),
(StdoutSink::NAME, mk_sink::<StdoutSink>),
(SubprocessSink::NAME, mk_sink::<SubprocessSink>),
];
pub fn find(name: Option<String>) -> Option<SinkBuilder> {

View file

@ -34,3 +34,7 @@ impl SinkAsBytes for StdoutSink {
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::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";
}

View file

@ -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";
}

View file

@ -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";
}

View file

@ -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<f32>),
@ -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";
}

View file

@ -62,3 +62,7 @@ impl SinkAsBytes for SubprocessSink {
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;
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<Self, Self::Error> {
match s.to_uppercase().as_str() {
impl FromStr for AudioFormat {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
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::<TriangularDitherer>),
@ -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;

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.;
/// 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 {

View file

@ -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<R: Read + Seek>(OggStreamReader<R>);
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())),

View file

@ -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<T>(code: u8, rdr: &mut PacketReader<T>) -> Result<Box<[u8]>, 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<R: Read + Seek> AudioDecoder for PassthroughDecoder<R> {
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();

View file

@ -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<f64>,
@ -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<D: Ditherer + 'static>() -> Box<dyn Ditherer> {
Box::new(D::new())
}
@ -130,9 +142,9 @@ pub type DithererBuilder = fn() -> Box<dyn Ditherer>;
pub fn find_ditherer(name: Option<String>) -> Option<DithererBuilder> {
match name.as_deref() {
Some("tpdf") => Some(mk_ditherer::<TriangularDitherer>),
Some("gpdf") => Some(mk_ditherer::<GaussianDitherer>),
Some("tpdf_hp") => Some(mk_ditherer::<HighPassDitherer>),
Some(TriangularDitherer::NAME) => Some(mk_ditherer::<TriangularDitherer>),
Some(GaussianDitherer::NAME) => Some(mk_ditherer::<GaussianDitherer>),
Some(HighPassDitherer::NAME) => Some(mk_ditherer::<HighPassDitherer>),
_ => None,
}
}

View file

@ -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;

View file

@ -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;

View file

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

View file

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

View file

@ -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);
}

View file

@ -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::<u32>().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::<f64>().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::<Path>::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::<u16>().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::<u16>().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::<u16>().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::<f64>().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::<f64>().expect("Invalid attack float value") / MILLIS)
.opt_str(NORMALISATION_ATTACK)
.map(|attack| {
Duration::from_millis(attack.parse::<u64>().expect("Invalid attack value"))
})
.unwrap_or(PlayerConfig::default().normalisation_attack);
let normalisation_release = matches
.opt_str("normalisation-release")
.map(|release| release.parse::<f64>().expect("Invalid release float value") / MILLIS)
.opt_str(NORMALISATION_RELEASE)
.map(|release| {
Duration::from_millis(release.parse::<u64>().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::<f64>().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<String> = std::env::args().collect();