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