mirror of
https://github.com/librespot-org/librespot.git
synced 2024-11-08 16:45:43 +00:00
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:
parent
bae1834988
commit
ad19b69bfb
27 changed files with 433 additions and 309 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
// record the response time
|
||||
self.network_response_times_ms.push(response_time_ms);
|
||||
|
||||
// prune old response times. Keep at most three.
|
||||
while self.network_response_times_ms.len() > 3 {
|
||||
self.network_response_times_ms.remove(0);
|
||||
// prune old response times. Keep at most two so we can push a third.
|
||||
while self.network_response_times.len() >= 3 {
|
||||
self.network_response_times.remove(0);
|
||||
}
|
||||
|
||||
// record the response time
|
||||
self.network_response_times.push(response_time);
|
||||
|
||||
// stats::median is experimental. So we calculate the median of up to three ourselves.
|
||||
let ping_time_ms: usize = match self.network_response_times_ms.len() {
|
||||
1 => self.network_response_times_ms[0] as usize,
|
||||
2 => {
|
||||
((self.network_response_times_ms[0] + self.network_response_times_ms[1])
|
||||
/ 2) as usize
|
||||
}
|
||||
let ping_time = match self.network_response_times.len() {
|
||||
1 => self.network_response_times[0],
|
||||
2 => (self.network_response_times[0] + self.network_response_times[1]) / 2,
|
||||
3 => {
|
||||
let mut times = self.network_response_times_ms.clone();
|
||||
let mut times = self.network_response_times.clone();
|
||||
times.sort_unstable();
|
||||
times[1]
|
||||
}
|
||||
|
@ -296,7 +291,7 @@ impl AudioFileFetch {
|
|||
// store our new estimate for everyone to see
|
||||
self.shared
|
||||
.ping_time_ms
|
||||
.store(ping_time_ms, atomic::Ordering::Relaxed);
|
||||
.store(ping_time.as_millis() as usize, Ordering::Relaxed);
|
||||
}
|
||||
ReceivedData::Data(data) => {
|
||||
self.output
|
||||
|
@ -390,7 +385,7 @@ pub(super) async fn audio_file_fetch(
|
|||
|
||||
file_data_tx,
|
||||
complete_tx: Some(complete_tx),
|
||||
network_response_times_ms: Vec::new(),
|
||||
network_response_times: Vec::with_capacity(3),
|
||||
};
|
||||
|
||||
loop {
|
||||
|
@ -408,10 +403,8 @@ pub(super) async fn audio_file_fetch(
|
|||
}
|
||||
|
||||
if fetch.get_download_strategy() == DownloadStrategy::Streaming() {
|
||||
let number_of_open_requests = fetch
|
||||
.shared
|
||||
.number_of_open_requests
|
||||
.load(atomic::Ordering::SeqCst);
|
||||
let number_of_open_requests =
|
||||
fetch.shared.number_of_open_requests.load(Ordering::SeqCst);
|
||||
if number_of_open_requests < MAX_PREFETCH_REQUESTS {
|
||||
let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_requests;
|
||||
|
||||
|
@ -424,14 +417,15 @@ pub(super) async fn audio_file_fetch(
|
|||
};
|
||||
|
||||
let ping_time_seconds =
|
||||
0.001 * fetch.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64;
|
||||
Duration::from_millis(fetch.shared.ping_time_ms.load(Ordering::Relaxed) as u64)
|
||||
.as_secs_f32();
|
||||
let download_rate = fetch.session.channel().get_download_rate_estimate();
|
||||
|
||||
let desired_pending_bytes = max(
|
||||
(PREFETCH_THRESHOLD_FACTOR
|
||||
* ping_time_seconds
|
||||
* fetch.shared.stream_data_rate as f64) as usize,
|
||||
(FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f64)
|
||||
* fetch.shared.stream_data_rate as f32) as usize,
|
||||
(FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f32)
|
||||
as usize,
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -34,3 +34,7 @@ impl SinkAsBytes for StdoutSink {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdoutSink {
|
||||
pub const NAME: &'static str = "pipe";
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -62,3 +62,7 @@ impl SinkAsBytes for SubprocessSink {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SubprocessSink {
|
||||
pub const NAME: &'static str = "subprocess";
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,10 @@ impl Mixer for SoftMixer {
|
|||
}
|
||||
}
|
||||
|
||||
impl SoftMixer {
|
||||
pub const NAME: &'static str = "softmixer";
|
||||
}
|
||||
|
||||
struct SoftVolumeApplier {
|
||||
volume: Arc<AtomicU64>,
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
260
src/main.rs
260
src/main.rs
|
@ -17,6 +17,8 @@ use librespot::playback::config::{
|
|||
AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl,
|
||||
};
|
||||
use librespot::playback::dither;
|
||||
#[cfg(feature = "alsa-backend")]
|
||||
use librespot::playback::mixer::alsamixer::AlsaMixer;
|
||||
use librespot::playback::mixer::mappings::MappedCtrl;
|
||||
use librespot::playback::mixer::{self, MixerConfig, MixerFn};
|
||||
use librespot::playback::player::{db_to_ratio, Player};
|
||||
|
@ -24,17 +26,14 @@ use librespot::playback::player::{db_to_ratio, Player};
|
|||
mod player_event_handler;
|
||||
use player_event_handler::{emit_sink_event, run_program_on_events};
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::env;
|
||||
use std::io::{stderr, Write};
|
||||
use std::path::Path;
|
||||
use std::pin::Pin;
|
||||
use std::process::exit;
|
||||
use std::str::FromStr;
|
||||
use std::{env, time::Instant};
|
||||
use std::{
|
||||
io::{stderr, Write},
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
const MILLIS: f64 = 1000.0;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
fn device_id(name: &str) -> String {
|
||||
hex::encode(Sha1::digest(name.as_bytes()))
|
||||
|
@ -189,176 +188,216 @@ struct Setup {
|
|||
}
|
||||
|
||||
fn get_setup(args: &[String]) -> Setup {
|
||||
const AP_PORT: &str = "ap-port";
|
||||
const AUTOPLAY: &str = "autoplay";
|
||||
const BACKEND: &str = "backend";
|
||||
const BITRATE: &str = "b";
|
||||
const CACHE: &str = "c";
|
||||
const CACHE_SIZE_LIMIT: &str = "cache-size-limit";
|
||||
const DEVICE: &str = "device";
|
||||
const DEVICE_TYPE: &str = "device-type";
|
||||
const DISABLE_AUDIO_CACHE: &str = "disable-audio-cache";
|
||||
const DISABLE_DISCOVERY: &str = "disable-discovery";
|
||||
const DISABLE_GAPLESS: &str = "disable-gapless";
|
||||
const DITHER: &str = "dither";
|
||||
const EMIT_SINK_EVENTS: &str = "emit-sink-events";
|
||||
const ENABLE_VOLUME_NORMALISATION: &str = "enable-volume-normalisation";
|
||||
const FORMAT: &str = "format";
|
||||
const HELP: &str = "h";
|
||||
const INITIAL_VOLUME: &str = "initial-volume";
|
||||
const MIXER_CARD: &str = "mixer-card";
|
||||
const MIXER_INDEX: &str = "mixer-index";
|
||||
const MIXER_NAME: &str = "mixer-name";
|
||||
const NAME: &str = "name";
|
||||
const NORMALISATION_ATTACK: &str = "normalisation-attack";
|
||||
const NORMALISATION_GAIN_TYPE: &str = "normalisation-gain-type";
|
||||
const NORMALISATION_KNEE: &str = "normalisation-knee";
|
||||
const NORMALISATION_METHOD: &str = "normalisation-method";
|
||||
const NORMALISATION_PREGAIN: &str = "normalisation-pregain";
|
||||
const NORMALISATION_RELEASE: &str = "normalisation-release";
|
||||
const NORMALISATION_THRESHOLD: &str = "normalisation-threshold";
|
||||
const ONEVENT: &str = "onevent";
|
||||
const PASSTHROUGH: &str = "passthrough";
|
||||
const PASSWORD: &str = "password";
|
||||
const PROXY: &str = "proxy";
|
||||
const SYSTEM_CACHE: &str = "system-cache";
|
||||
const USERNAME: &str = "username";
|
||||
const VERBOSE: &str = "verbose";
|
||||
const VERSION: &str = "version";
|
||||
const VOLUME_CTRL: &str = "volume-ctrl";
|
||||
const VOLUME_RANGE: &str = "volume-range";
|
||||
const ZEROCONF_PORT: &str = "zeroconf-port";
|
||||
|
||||
let mut opts = getopts::Options::new();
|
||||
opts.optflag(
|
||||
"h",
|
||||
HELP,
|
||||
"help",
|
||||
"Print this help menu.",
|
||||
).optopt(
|
||||
"c",
|
||||
CACHE,
|
||||
"cache",
|
||||
"Path to a directory where files will be cached.",
|
||||
"PATH",
|
||||
).optopt(
|
||||
"",
|
||||
"system-cache",
|
||||
SYSTEM_CACHE,
|
||||
"Path to a directory where system files (credentials, volume) will be cached. Can be different from cache option value.",
|
||||
"PATH",
|
||||
).optopt(
|
||||
"",
|
||||
"cache-size-limit",
|
||||
CACHE_SIZE_LIMIT,
|
||||
"Limits the size of the cache for audio files.",
|
||||
"SIZE"
|
||||
).optflag("", "disable-audio-cache", "Disable caching of the audio data.")
|
||||
.optopt("n", "name", "Device name.", "NAME")
|
||||
.optopt("", "device-type", "Displayed device type.", "TYPE")
|
||||
).optflag("", DISABLE_AUDIO_CACHE, "Disable caching of the audio data.")
|
||||
.optopt("n", NAME, "Device name.", "NAME")
|
||||
.optopt("", DEVICE_TYPE, "Displayed device type.", "TYPE")
|
||||
.optopt(
|
||||
"b",
|
||||
BITRATE,
|
||||
"bitrate",
|
||||
"Bitrate (kbps) {96|160|320}. Defaults to 160.",
|
||||
"BITRATE",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"onevent",
|
||||
ONEVENT,
|
||||
"Run PROGRAM when a playback event occurs.",
|
||||
"PROGRAM",
|
||||
)
|
||||
.optflag("", "emit-sink-events", "Run program set by --onevent before sink is opened and after it is closed.")
|
||||
.optflag("v", "verbose", "Enable verbose output.")
|
||||
.optflag("V", "version", "Display librespot version string.")
|
||||
.optopt("u", "username", "Username to sign in with.", "USERNAME")
|
||||
.optopt("p", "password", "Password", "PASSWORD")
|
||||
.optopt("", "proxy", "HTTP proxy to use when connecting.", "URL")
|
||||
.optopt("", "ap-port", "Connect to AP with specified port. If no AP with that port are present fallback AP will be used. Available ports are usually 80, 443 and 4070.", "PORT")
|
||||
.optflag("", "disable-discovery", "Disable discovery mode.")
|
||||
.optflag("", EMIT_SINK_EVENTS, "Run program set by --onevent before sink is opened and after it is closed.")
|
||||
.optflag("v", VERBOSE, "Enable verbose output.")
|
||||
.optflag("V", VERSION, "Display librespot version string.")
|
||||
.optopt("u", USERNAME, "Username to sign in with.", "USERNAME")
|
||||
.optopt("p", PASSWORD, "Password", "PASSWORD")
|
||||
.optopt("", PROXY, "HTTP proxy to use when connecting.", "URL")
|
||||
.optopt("", AP_PORT, "Connect to AP with specified port. If no AP with that port are present fallback AP will be used. Available ports are usually 80, 443 and 4070.", "PORT")
|
||||
.optflag("", DISABLE_DISCOVERY, "Disable discovery mode.")
|
||||
.optopt(
|
||||
"",
|
||||
"backend",
|
||||
BACKEND,
|
||||
"Audio backend to use. Use '?' to list options.",
|
||||
"NAME",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"device",
|
||||
DEVICE,
|
||||
"Audio device to use. Use '?' to list options if using alsa, portaudio or rodio.",
|
||||
"NAME",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"format",
|
||||
FORMAT,
|
||||
"Output format {F64|F32|S32|S24|S24_3|S16}. Defaults to S16.",
|
||||
"FORMAT",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"dither",
|
||||
DITHER,
|
||||
"Specify the dither algorithm to use - [none, gpdf, tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats.",
|
||||
"DITHER",
|
||||
)
|
||||
.optopt("", "mixer", "Mixer to use {alsa|softvol}.", "MIXER")
|
||||
.optopt(
|
||||
"m",
|
||||
"mixer-name",
|
||||
MIXER_NAME,
|
||||
"Alsa mixer control, e.g. 'PCM' or 'Master'. Defaults to 'PCM'.",
|
||||
"NAME",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"mixer-card",
|
||||
MIXER_CARD,
|
||||
"Alsa mixer card, e.g 'hw:0' or similar from `aplay -l`. Defaults to DEVICE if specified, 'default' otherwise.",
|
||||
"MIXER_CARD",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"mixer-index",
|
||||
MIXER_INDEX,
|
||||
"Alsa index of the cards mixer. Defaults to 0.",
|
||||
"INDEX",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"initial-volume",
|
||||
INITIAL_VOLUME,
|
||||
"Initial volume in % from 0-100. Default for softvol: '50'. For the Alsa mixer: the current volume.",
|
||||
"VOLUME",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"zeroconf-port",
|
||||
ZEROCONF_PORT,
|
||||
"The port the internal server advertised over zeroconf uses.",
|
||||
"PORT",
|
||||
)
|
||||
.optflag(
|
||||
"",
|
||||
"enable-volume-normalisation",
|
||||
ENABLE_VOLUME_NORMALISATION,
|
||||
"Play all tracks at the same volume.",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"normalisation-method",
|
||||
NORMALISATION_METHOD,
|
||||
"Specify the normalisation method to use {basic|dynamic}. Defaults to dynamic.",
|
||||
"METHOD",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"normalisation-gain-type",
|
||||
NORMALISATION_GAIN_TYPE,
|
||||
"Specify the normalisation gain type to use {track|album}. Defaults to album.",
|
||||
"TYPE",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"normalisation-pregain",
|
||||
NORMALISATION_PREGAIN,
|
||||
"Pregain (dB) applied by volume normalisation. Defaults to 0.",
|
||||
"PREGAIN",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"normalisation-threshold",
|
||||
NORMALISATION_THRESHOLD,
|
||||
"Threshold (dBFS) to prevent clipping. Defaults to -1.0.",
|
||||
"THRESHOLD",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"normalisation-attack",
|
||||
NORMALISATION_ATTACK,
|
||||
"Attack time (ms) in which the dynamic limiter is reducing gain. Defaults to 5.",
|
||||
"TIME",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"normalisation-release",
|
||||
NORMALISATION_RELEASE,
|
||||
"Release or decay time (ms) in which the dynamic limiter is restoring gain. Defaults to 100.",
|
||||
"TIME",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"normalisation-knee",
|
||||
NORMALISATION_KNEE,
|
||||
"Knee steepness of the dynamic limiter. Defaults to 1.0.",
|
||||
"KNEE",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"volume-ctrl",
|
||||
VOLUME_CTRL,
|
||||
"Volume control type {cubic|fixed|linear|log}. Defaults to log.",
|
||||
"VOLUME_CTRL"
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"volume-range",
|
||||
VOLUME_RANGE,
|
||||
"Range of the volume control (dB). Default for softvol: 60. For the Alsa mixer: what the control supports.",
|
||||
"RANGE",
|
||||
)
|
||||
.optflag(
|
||||
"",
|
||||
"autoplay",
|
||||
AUTOPLAY,
|
||||
"Automatically play similar songs when your music ends.",
|
||||
)
|
||||
.optflag(
|
||||
"",
|
||||
"disable-gapless",
|
||||
DISABLE_GAPLESS,
|
||||
"Disable gapless playback.",
|
||||
)
|
||||
.optflag(
|
||||
"",
|
||||
"passthrough",
|
||||
PASSTHROUGH,
|
||||
"Pass raw stream to output, only works for pipe and subprocess.",
|
||||
);
|
||||
|
||||
|
@ -374,17 +413,17 @@ fn get_setup(args: &[String]) -> Setup {
|
|||
}
|
||||
};
|
||||
|
||||
if matches.opt_present("h") {
|
||||
if matches.opt_present(HELP) {
|
||||
println!("{}", usage(&args[0], &opts));
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if matches.opt_present("version") {
|
||||
if matches.opt_present(VERSION) {
|
||||
print_version();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
let verbose = matches.opt_present("verbose");
|
||||
let verbose = matches.opt_present(VERBOSE);
|
||||
setup_logging(verbose);
|
||||
|
||||
info!(
|
||||
|
@ -395,7 +434,7 @@ fn get_setup(args: &[String]) -> Setup {
|
|||
build_id = version::BUILD_ID
|
||||
);
|
||||
|
||||
let backend_name = matches.opt_str("backend");
|
||||
let backend_name = matches.opt_str(BACKEND);
|
||||
if backend_name == Some("?".into()) {
|
||||
list_backends();
|
||||
exit(0);
|
||||
|
@ -404,40 +443,41 @@ fn get_setup(args: &[String]) -> Setup {
|
|||
let backend = audio_backend::find(backend_name).expect("Invalid backend");
|
||||
|
||||
let format = matches
|
||||
.opt_str("format")
|
||||
.as_ref()
|
||||
.map(|format| AudioFormat::try_from(format).expect("Invalid output format"))
|
||||
.opt_str(FORMAT)
|
||||
.as_deref()
|
||||
.map(|format| AudioFormat::from_str(format).expect("Invalid output format"))
|
||||
.unwrap_or_default();
|
||||
|
||||
let device = matches.opt_str("device");
|
||||
let device = matches.opt_str(DEVICE);
|
||||
if device == Some("?".into()) {
|
||||
backend(device, format);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
let mixer_name = matches.opt_str("mixer");
|
||||
let mixer = mixer::find(mixer_name.as_ref()).expect("Invalid mixer");
|
||||
let mixer_name = matches.opt_str(MIXER_NAME);
|
||||
let mixer = mixer::find(mixer_name.as_deref()).expect("Invalid mixer");
|
||||
|
||||
let mixer_config = {
|
||||
let card = matches.opt_str("mixer-card").unwrap_or_else(|| {
|
||||
let card = matches.opt_str(MIXER_CARD).unwrap_or_else(|| {
|
||||
if let Some(ref device_name) = device {
|
||||
device_name.to_string()
|
||||
} else {
|
||||
String::from("default")
|
||||
MixerConfig::default().card
|
||||
}
|
||||
});
|
||||
let index = matches
|
||||
.opt_str("mixer-index")
|
||||
.opt_str(MIXER_INDEX)
|
||||
.map(|index| index.parse::<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();
|
||||
|
|
Loading…
Reference in a new issue