Skip to content

Commit ad19b69

Browse files
authored
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
1 parent bae1834 commit ad19b69

27 files changed

Lines changed: 441 additions & 317 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515

1616
### Changed
1717
- [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)
18+
- [audio, playback] Use `Duration` for time constants and functions (breaking)
1819
- [connect, playback] Moved volume controls from `librespot-connect` to `librespot-playback` crate
1920
- [connect] Synchronize player volume with mixer volume on playback
2021
- [playback] Store and pass samples in 64-bit floating point

audio/src/fetch/mod.rs

Lines changed: 77 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -18,70 +18,70 @@ use tokio::sync::{mpsc, oneshot};
1818
use self::receive::{audio_file_fetch, request_range};
1919
use crate::range_set::{Range, RangeSet};
2020

21+
/// The minimum size of a block that is requested from the Spotify servers in one request.
22+
/// This is the block size that is typically requested while doing a `seek()` on a file.
23+
/// Note: smaller requests can happen if part of the block is downloaded already.
2124
const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 16;
22-
// The minimum size of a block that is requested from the Spotify servers in one request.
23-
// This is the block size that is typically requested while doing a seek() on a file.
24-
// Note: smaller requests can happen if part of the block is downloaded already.
2525

26+
/// The amount of data that is requested when initially opening a file.
27+
/// Note: if the file is opened to play from the beginning, the amount of data to
28+
/// read ahead is requested in addition to this amount. If the file is opened to seek to
29+
/// another position, then only this amount is requested on the first request.
2630
const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 16;
27-
// The amount of data that is requested when initially opening a file.
28-
// Note: if the file is opened to play from the beginning, the amount of data to
29-
// read ahead is requested in addition to this amount. If the file is opened to seek to
30-
// another position, then only this amount is requested on the first request.
31-
32-
const INITIAL_PING_TIME_ESTIMATE_SECONDS: f64 = 0.5;
33-
// The pig time that is used for calculations before a ping time was actually measured.
34-
35-
const MAXIMUM_ASSUMED_PING_TIME_SECONDS: f64 = 1.5;
36-
// If the measured ping time to the Spotify server is larger than this value, it is capped
37-
// to avoid run-away block sizes and pre-fetching.
38-
39-
pub const READ_AHEAD_BEFORE_PLAYBACK_SECONDS: f64 = 1.0;
40-
// Before playback starts, this many seconds of data must be present.
41-
// Note: the calculations are done using the nominal bitrate of the file. The actual amount
42-
// of audio data may be larger or smaller.
43-
44-
pub const READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS: f64 = 2.0;
45-
// Same as READ_AHEAD_BEFORE_PLAYBACK_SECONDS, but the time is taken as a factor of the ping
46-
// time to the Spotify server.
47-
// Both, READ_AHEAD_BEFORE_PLAYBACK_SECONDS and READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS are
48-
// obeyed.
49-
// Note: the calculations are done using the nominal bitrate of the file. The actual amount
50-
// of audio data may be larger or smaller.
51-
52-
pub const READ_AHEAD_DURING_PLAYBACK_SECONDS: f64 = 5.0;
53-
// While playing back, this many seconds of data ahead of the current read position are
54-
// requested.
55-
// Note: the calculations are done using the nominal bitrate of the file. The actual amount
56-
// of audio data may be larger or smaller.
57-
58-
pub const READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS: f64 = 10.0;
59-
// Same as READ_AHEAD_DURING_PLAYBACK_SECONDS, but the time is taken as a factor of the ping
60-
// time to the Spotify server.
61-
// Note: the calculations are done using the nominal bitrate of the file. The actual amount
62-
// of audio data may be larger or smaller.
63-
64-
const PREFETCH_THRESHOLD_FACTOR: f64 = 4.0;
65-
// If the amount of data that is pending (requested but not received) is less than a certain amount,
66-
// data is pre-fetched in addition to the read ahead settings above. The threshold for requesting more
67-
// data is calculated as
68-
// <pending bytes> < PREFETCH_THRESHOLD_FACTOR * <ping time> * <nominal data rate>
69-
70-
const FAST_PREFETCH_THRESHOLD_FACTOR: f64 = 1.5;
71-
// Similar to PREFETCH_THRESHOLD_FACTOR, but it also takes the current download rate into account.
72-
// The formula used is
73-
// <pending bytes> < FAST_PREFETCH_THRESHOLD_FACTOR * <ping time> * <measured download rate>
74-
// This mechanism allows for fast downloading of the remainder of the file. The number should be larger
75-
// than 1 so the download rate ramps up until the bandwidth is saturated. The larger the value, the faster
76-
// the download rate ramps up. However, this comes at the cost that it might hurt ping-time if a seek is
77-
// performed while downloading. Values smaller than 1 cause the download rate to collapse and effectively
78-
// only PREFETCH_THRESHOLD_FACTOR is in effect. Thus, set to zero if bandwidth saturation is not wanted.
7931

32+
/// The ping time that is used for calculations before a ping time was actually measured.
33+
const INITIAL_PING_TIME_ESTIMATE: Duration = Duration::from_millis(500);
34+
35+
/// If the measured ping time to the Spotify server is larger than this value, it is capped
36+
/// to avoid run-away block sizes and pre-fetching.
37+
const MAXIMUM_ASSUMED_PING_TIME: Duration = Duration::from_millis(1500);
38+
39+
/// Before playback starts, this many seconds of data must be present.
40+
/// Note: the calculations are done using the nominal bitrate of the file. The actual amount
41+
/// of audio data may be larger or smaller.
42+
pub const READ_AHEAD_BEFORE_PLAYBACK: Duration = Duration::from_secs(1);
43+
44+
/// Same as `READ_AHEAD_BEFORE_PLAYBACK`, but the time is taken as a factor of the ping
45+
/// time to the Spotify server. Both `READ_AHEAD_BEFORE_PLAYBACK` and
46+
/// `READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS` are obeyed.
47+
/// Note: the calculations are done using the nominal bitrate of the file. The actual amount
48+
/// of audio data may be larger or smaller.
49+
pub const READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS: f32 = 2.0;
50+
51+
/// While playing back, this many seconds of data ahead of the current read position are
52+
/// requested.
53+
/// Note: the calculations are done using the nominal bitrate of the file. The actual amount
54+
/// of audio data may be larger or smaller.
55+
pub const READ_AHEAD_DURING_PLAYBACK: Duration = Duration::from_secs(5);
56+
57+
/// Same as `READ_AHEAD_DURING_PLAYBACK`, but the time is taken as a factor of the ping
58+
/// time to the Spotify server.
59+
/// Note: the calculations are done using the nominal bitrate of the file. The actual amount
60+
/// of audio data may be larger or smaller.
61+
pub const READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS: f32 = 10.0;
62+
63+
/// If the amount of data that is pending (requested but not received) is less than a certain amount,
64+
/// data is pre-fetched in addition to the read ahead settings above. The threshold for requesting more
65+
/// data is calculated as `<pending bytes> < PREFETCH_THRESHOLD_FACTOR * <ping time> * <nominal data rate>`
66+
const PREFETCH_THRESHOLD_FACTOR: f32 = 4.0;
67+
68+
/// Similar to `PREFETCH_THRESHOLD_FACTOR`, but it also takes the current download rate into account.
69+
/// The formula used is `<pending bytes> < FAST_PREFETCH_THRESHOLD_FACTOR * <ping time> * <measured download rate>`
70+
/// This mechanism allows for fast downloading of the remainder of the file. The number should be larger
71+
/// than `1.0` so the download rate ramps up until the bandwidth is saturated. The larger the value, the faster
72+
/// the download rate ramps up. However, this comes at the cost that it might hurt ping time if a seek is
73+
/// performed while downloading. Values smaller than `1.0` cause the download rate to collapse and effectively
74+
/// only `PREFETCH_THRESHOLD_FACTOR` is in effect. Thus, set to `0.0` if bandwidth saturation is not wanted.
75+
const FAST_PREFETCH_THRESHOLD_FACTOR: f32 = 1.5;
76+
77+
/// Limit the number of requests that are pending simultaneously before pre-fetching data. Pending
78+
/// requests share bandwidth. Thus, havint too many requests can lead to the one that is needed next
79+
/// for playback to be delayed leading to a buffer underrun. This limit has the effect that a new
80+
/// pre-fetch request is only sent if less than `MAX_PREFETCH_REQUESTS` are pending.
8081
const MAX_PREFETCH_REQUESTS: usize = 4;
81-
// Limit the number of requests that are pending simultaneously before pre-fetching data. Pending
82-
// requests share bandwidth. Thus, havint too many requests can lead to the one that is needed next
83-
// for playback to be delayed leading to a buffer underrun. This limit has the effect that a new
84-
// pre-fetch request is only sent if less than MAX_PREFETCH_REQUESTS are pending.
82+
83+
/// The time we will wait to obtain status updates on downloading.
84+
const DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(1);
8585

8686
pub enum AudioFile {
8787
Cached(fs::File),
@@ -131,10 +131,10 @@ impl StreamLoaderController {
131131
})
132132
}
133133

134-
pub fn ping_time_ms(&self) -> usize {
135-
self.stream_shared.as_ref().map_or(0, |shared| {
136-
shared.ping_time_ms.load(atomic::Ordering::Relaxed)
137-
})
134+
pub fn ping_time(&self) -> Duration {
135+
Duration::from_millis(self.stream_shared.as_ref().map_or(0, |shared| {
136+
shared.ping_time_ms.load(atomic::Ordering::Relaxed) as u64
137+
}))
138138
}
139139

140140
fn send_stream_loader_command(&self, command: StreamLoaderCommand) {
@@ -170,7 +170,7 @@ impl StreamLoaderController {
170170
{
171171
download_status = shared
172172
.cond
173-
.wait_timeout(download_status, Duration::from_millis(1000))
173+
.wait_timeout(download_status, DOWNLOAD_TIMEOUT)
174174
.unwrap()
175175
.0;
176176
if range.length
@@ -271,10 +271,10 @@ impl AudioFile {
271271
let mut initial_data_length = if play_from_beginning {
272272
INITIAL_DOWNLOAD_SIZE
273273
+ max(
274-
(READ_AHEAD_DURING_PLAYBACK_SECONDS * bytes_per_second as f64) as usize,
275-
(INITIAL_PING_TIME_ESTIMATE_SECONDS
274+
(READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize,
275+
(INITIAL_PING_TIME_ESTIMATE.as_secs_f32()
276276
* READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS
277-
* bytes_per_second as f64) as usize,
277+
* bytes_per_second as f32) as usize,
278278
)
279279
} else {
280280
INITIAL_DOWNLOAD_SIZE
@@ -368,7 +368,7 @@ impl AudioFileStreaming {
368368

369369
let read_file = write_file.reopen().unwrap();
370370

371-
//let (seek_tx, seek_rx) = mpsc::unbounded();
371+
// let (seek_tx, seek_rx) = mpsc::unbounded();
372372
let (stream_loader_command_tx, stream_loader_command_rx) =
373373
mpsc::unbounded_channel::<StreamLoaderCommand>();
374374

@@ -405,17 +405,19 @@ impl Read for AudioFileStreaming {
405405
let length_to_request = match *(self.shared.download_strategy.lock().unwrap()) {
406406
DownloadStrategy::RandomAccess() => length,
407407
DownloadStrategy::Streaming() => {
408-
// Due to the read-ahead stuff, we potentially request more than the actual reqeust demanded.
409-
let ping_time_seconds =
410-
0.0001 * self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64;
408+
// Due to the read-ahead stuff, we potentially request more than the actual request demanded.
409+
let ping_time_seconds = Duration::from_millis(
410+
self.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as u64,
411+
)
412+
.as_secs_f32();
411413

412414
let length_to_request = length
413415
+ max(
414-
(READ_AHEAD_DURING_PLAYBACK_SECONDS * self.shared.stream_data_rate as f64)
415-
as usize,
416+
(READ_AHEAD_DURING_PLAYBACK.as_secs_f32()
417+
* self.shared.stream_data_rate as f32) as usize,
416418
(READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS
417419
* ping_time_seconds
418-
* self.shared.stream_data_rate as f64) as usize,
420+
* self.shared.stream_data_rate as f32) as usize,
419421
);
420422
min(length_to_request, self.shared.file_size - offset)
421423
}
@@ -449,7 +451,7 @@ impl Read for AudioFileStreaming {
449451
download_status = self
450452
.shared
451453
.cond
452-
.wait_timeout(download_status, Duration::from_millis(1000))
454+
.wait_timeout(download_status, DOWNLOAD_TIMEOUT)
453455
.unwrap()
454456
.0;
455457
}

audio/src/fetch/receive.rs

Lines changed: 32 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use std::cmp::{max, min};
22
use std::io::{Seek, SeekFrom, Write};
33
use std::sync::{atomic, Arc};
4-
use std::time::Instant;
4+
use std::time::{Duration, Instant};
55

6+
use atomic::Ordering;
67
use byteorder::{BigEndian, WriteBytesExt};
78
use bytes::Bytes;
89
use futures_util::StreamExt;
@@ -16,7 +17,7 @@ use crate::range_set::{Range, RangeSet};
1617

1718
use super::{AudioFileShared, DownloadStrategy, StreamLoaderCommand};
1819
use super::{
19-
FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME_SECONDS, MAX_PREFETCH_REQUESTS,
20+
FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, MAX_PREFETCH_REQUESTS,
2021
MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR,
2122
};
2223

@@ -57,7 +58,7 @@ struct PartialFileData {
5758
}
5859

5960
enum ReceivedData {
60-
ResponseTimeMs(usize),
61+
ResponseTime(Duration),
6162
Data(PartialFileData),
6263
}
6364

@@ -74,7 +75,7 @@ async fn receive_data(
7475

7576
let old_number_of_request = shared
7677
.number_of_open_requests
77-
.fetch_add(1, atomic::Ordering::SeqCst);
78+
.fetch_add(1, Ordering::SeqCst);
7879

7980
let mut measure_ping_time = old_number_of_request == 0;
8081

@@ -86,14 +87,11 @@ async fn receive_data(
8687
};
8788

8889
if measure_ping_time {
89-
let duration = Instant::now() - request_sent_time;
90-
let duration_ms: u64;
91-
if 0.001 * (duration.as_millis() as f64) > MAXIMUM_ASSUMED_PING_TIME_SECONDS {
92-
duration_ms = (MAXIMUM_ASSUMED_PING_TIME_SECONDS * 1000.0) as u64;
93-
} else {
94-
duration_ms = duration.as_millis() as u64;
90+
let mut duration = Instant::now() - request_sent_time;
91+
if duration > MAXIMUM_ASSUMED_PING_TIME {
92+
duration = MAXIMUM_ASSUMED_PING_TIME;
9593
}
96-
let _ = file_data_tx.send(ReceivedData::ResponseTimeMs(duration_ms as usize));
94+
let _ = file_data_tx.send(ReceivedData::ResponseTime(duration));
9795
measure_ping_time = false;
9896
}
9997
let data_size = data.len();
@@ -127,7 +125,7 @@ async fn receive_data(
127125

128126
shared
129127
.number_of_open_requests
130-
.fetch_sub(1, atomic::Ordering::SeqCst);
128+
.fetch_sub(1, Ordering::SeqCst);
131129

132130
if result.is_err() {
133131
warn!(
@@ -149,7 +147,7 @@ struct AudioFileFetch {
149147

150148
file_data_tx: mpsc::UnboundedSender<ReceivedData>,
151149
complete_tx: Option<oneshot::Sender<NamedTempFile>>,
152-
network_response_times_ms: Vec<usize>,
150+
network_response_times: Vec<Duration>,
153151
}
154152

155153
// Might be replaced by enum from std once stable
@@ -237,7 +235,7 @@ impl AudioFileFetch {
237235

238236
// download data from after the current read position first
239237
let mut tail_end = RangeSet::new();
240-
let read_position = self.shared.read_position.load(atomic::Ordering::Relaxed);
238+
let read_position = self.shared.read_position.load(Ordering::Relaxed);
241239
tail_end.add_range(&Range::new(
242240
read_position,
243241
self.shared.file_size - read_position,
@@ -267,26 +265,23 @@ impl AudioFileFetch {
267265

268266
fn handle_file_data(&mut self, data: ReceivedData) -> ControlFlow {
269267
match data {
270-
ReceivedData::ResponseTimeMs(response_time_ms) => {
271-
trace!("Ping time estimated as: {} ms.", response_time_ms);
272-
273-
// record the response time
274-
self.network_response_times_ms.push(response_time_ms);
268+
ReceivedData::ResponseTime(response_time) => {
269+
trace!("Ping time estimated as: {}ms", response_time.as_millis());
275270

276-
// prune old response times. Keep at most three.
277-
while self.network_response_times_ms.len() > 3 {
278-
self.network_response_times_ms.remove(0);
271+
// prune old response times. Keep at most two so we can push a third.
272+
while self.network_response_times.len() >= 3 {
273+
self.network_response_times.remove(0);
279274
}
280275

276+
// record the response time
277+
self.network_response_times.push(response_time);
278+
281279
// stats::median is experimental. So we calculate the median of up to three ourselves.
282-
let ping_time_ms: usize = match self.network_response_times_ms.len() {
283-
1 => self.network_response_times_ms[0] as usize,
284-
2 => {
285-
((self.network_response_times_ms[0] + self.network_response_times_ms[1])
286-
/ 2) as usize
287-
}
280+
let ping_time = match self.network_response_times.len() {
281+
1 => self.network_response_times[0],
282+
2 => (self.network_response_times[0] + self.network_response_times[1]) / 2,
288283
3 => {
289-
let mut times = self.network_response_times_ms.clone();
284+
let mut times = self.network_response_times.clone();
290285
times.sort_unstable();
291286
times[1]
292287
}
@@ -296,7 +291,7 @@ impl AudioFileFetch {
296291
// store our new estimate for everyone to see
297292
self.shared
298293
.ping_time_ms
299-
.store(ping_time_ms, atomic::Ordering::Relaxed);
294+
.store(ping_time.as_millis() as usize, Ordering::Relaxed);
300295
}
301296
ReceivedData::Data(data) => {
302297
self.output
@@ -390,7 +385,7 @@ pub(super) async fn audio_file_fetch(
390385

391386
file_data_tx,
392387
complete_tx: Some(complete_tx),
393-
network_response_times_ms: Vec::new(),
388+
network_response_times: Vec::with_capacity(3),
394389
};
395390

396391
loop {
@@ -408,10 +403,8 @@ pub(super) async fn audio_file_fetch(
408403
}
409404

410405
if fetch.get_download_strategy() == DownloadStrategy::Streaming() {
411-
let number_of_open_requests = fetch
412-
.shared
413-
.number_of_open_requests
414-
.load(atomic::Ordering::SeqCst);
406+
let number_of_open_requests =
407+
fetch.shared.number_of_open_requests.load(Ordering::SeqCst);
415408
if number_of_open_requests < MAX_PREFETCH_REQUESTS {
416409
let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_requests;
417410

@@ -424,14 +417,15 @@ pub(super) async fn audio_file_fetch(
424417
};
425418

426419
let ping_time_seconds =
427-
0.001 * fetch.shared.ping_time_ms.load(atomic::Ordering::Relaxed) as f64;
420+
Duration::from_millis(fetch.shared.ping_time_ms.load(Ordering::Relaxed) as u64)
421+
.as_secs_f32();
428422
let download_rate = fetch.session.channel().get_download_rate_estimate();
429423

430424
let desired_pending_bytes = max(
431425
(PREFETCH_THRESHOLD_FACTOR
432426
* ping_time_seconds
433-
* fetch.shared.stream_data_rate as f64) as usize,
434-
(FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f64)
427+
* fetch.shared.stream_data_rate as f32) as usize,
428+
(FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f32)
435429
as usize,
436430
);
437431

0 commit comments

Comments
 (0)