mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Improve format handling and support MP3
- Switch from `lewton` to `Symphonia`. This is a pure Rust demuxer and decoder in active development that supports a wide range of formats, including Ogg Vorbis, MP3, AAC and FLAC for future HiFi support. At the moment only Ogg Vorbis and MP3 are enabled; all AAC files are DRM-protected. - Bump MSRV to 1.51, required for `Symphonia`. - Filter out all files whose format is not specified. - Not all episodes seem to be encrypted. If we can't get an audio key, try and see if we can play the file without decryption. - After seeking, report the actual position instead of the target. - Remove the 0xa7 bytes offset from `Subfile`, `Symphonia` does not balk at Spotify's custom Ogg packet before it. This also simplifies handling of formats other than Ogg Vorbis. - When there is no next track to load, signal the UI that the player has stopped. Before, the player would get stuck in an infinite reloading loop when there was only one track in the queue and that track could not be loaded.
This commit is contained in:
parent
2d699e288a
commit
7921f23927
13 changed files with 445 additions and 240 deletions
119
Cargo.lock
generated
119
Cargo.lock
generated
|
@ -97,6 +97,12 @@ version = "1.0.51"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
|
checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.51"
|
version = "0.1.51"
|
||||||
|
@ -186,6 +192,12 @@ version = "3.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
|
checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytemuck"
|
||||||
|
version = "1.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
version = "1.4.3"
|
version = "1.4.3"
|
||||||
|
@ -446,6 +458,15 @@ version = "1.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.30"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "env_logger"
|
name = "env_logger"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
|
@ -1121,17 +1142,6 @@ version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lewton"
|
|
||||||
version = "0.10.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030"
|
|
||||||
dependencies = [
|
|
||||||
"byteorder",
|
|
||||||
"ogg",
|
|
||||||
"tinyvec",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.109"
|
version = "0.2.109"
|
||||||
|
@ -1398,7 +1408,6 @@ dependencies = [
|
||||||
"gstreamer",
|
"gstreamer",
|
||||||
"gstreamer-app",
|
"gstreamer-app",
|
||||||
"jack",
|
"jack",
|
||||||
"lewton",
|
|
||||||
"libpulse-binding",
|
"libpulse-binding",
|
||||||
"libpulse-simple-binding",
|
"libpulse-simple-binding",
|
||||||
"librespot-audio",
|
"librespot-audio",
|
||||||
|
@ -1413,6 +1422,7 @@ dependencies = [
|
||||||
"rodio",
|
"rodio",
|
||||||
"sdl2",
|
"sdl2",
|
||||||
"shell-words",
|
"shell-words",
|
||||||
|
"symphonia",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
|
@ -2470,6 +2480,91 @@ version = "2.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7e5f38aa07e792f4eebb0faa93cee088ec82c48222dd332897aae1569d9a4b7"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"symphonia-bundle-mp3",
|
||||||
|
"symphonia-codec-vorbis",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-format-ogg",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-bundle-mp3"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec4d97c4a61ece4651751dddb393ebecb7579169d9e758ae808fe507a5250790"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-codec-vorbis"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a29ed6748078effb35a05064a451493a78038918981dc1a76bdf5a2752d441fa"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-utils-xiph",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-core"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa135e97be0f4a666c31dfe5ef4c75435ba3d355fd6a73d2100aa79b14c104c9"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"bitflags",
|
||||||
|
"bytemuck",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-format-ogg"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7b2357288a79adfec532cfd86049696cfa5c58efeff83bd51687a528f18a519"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
"symphonia-utils-xiph",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-metadata"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5260599daba18d8fe905ca3eb3b42ba210529a6276886632412cc74984e79b1a"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-utils-xiph"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a37026c6948ff842e0bf94b4008579cc71ab16ed0ff9ca70a331f60f4f1e1e9"
|
||||||
|
dependencies = [
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.82"
|
version = "1.0.82"
|
||||||
|
|
|
@ -14,16 +14,20 @@ const AUDIO_AESIV: [u8; 16] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
pub struct AudioDecrypt<T: io::Read> {
|
pub struct AudioDecrypt<T: io::Read> {
|
||||||
cipher: Aes128Ctr,
|
// a `None` cipher is a convenience to make `AudioDecrypt` pass files unaltered
|
||||||
|
cipher: Option<Aes128Ctr>,
|
||||||
reader: T,
|
reader: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: io::Read> AudioDecrypt<T> {
|
impl<T: io::Read> AudioDecrypt<T> {
|
||||||
pub fn new(key: AudioKey, reader: T) -> AudioDecrypt<T> {
|
pub fn new(key: Option<AudioKey>, reader: T) -> AudioDecrypt<T> {
|
||||||
let cipher = Aes128Ctr::new(
|
let cipher = key.map(|key| {
|
||||||
GenericArray::from_slice(&key.0),
|
Aes128Ctr::new(
|
||||||
GenericArray::from_slice(&AUDIO_AESIV),
|
GenericArray::from_slice(&key.0),
|
||||||
);
|
GenericArray::from_slice(&AUDIO_AESIV),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
AudioDecrypt { cipher, reader }
|
AudioDecrypt { cipher, reader }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +36,9 @@ impl<T: io::Read> io::Read for AudioDecrypt<T> {
|
||||||
fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
|
fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
|
||||||
let len = self.reader.read(output)?;
|
let len = self.reader.read(output)?;
|
||||||
|
|
||||||
self.cipher.apply_keystream(&mut output[..len]);
|
if let Some(ref mut cipher) = self.cipher {
|
||||||
|
cipher.apply_keystream(&mut output[..len]);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(len)
|
Ok(len)
|
||||||
}
|
}
|
||||||
|
@ -42,7 +48,9 @@ impl<T: io::Read + io::Seek> io::Seek for AudioDecrypt<T> {
|
||||||
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
|
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
|
||||||
let newpos = self.reader.seek(pos)?;
|
let newpos = self.reader.seek(pos)?;
|
||||||
|
|
||||||
self.cipher.seek(newpos);
|
if let Some(ref mut cipher) = self.cipher {
|
||||||
|
cipher.seek(newpos);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(newpos)
|
Ok(newpos)
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,6 +57,7 @@ impl From<AudioFileError> for Error {
|
||||||
|
|
||||||
/// The minimum size of a block that is requested from the Spotify servers in one request.
|
/// 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.
|
/// This is the block size that is typically requested while doing a `seek()` on a file.
|
||||||
|
/// The Symphonia decoder requires this to be a power of 2 and > 32 kB.
|
||||||
/// Note: smaller requests can happen if part of the block is downloaded already.
|
/// Note: smaller requests can happen if part of the block is downloaded already.
|
||||||
pub const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 128;
|
pub const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 128;
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,6 @@ mod range_set;
|
||||||
pub use decrypt::AudioDecrypt;
|
pub use decrypt::AudioDecrypt;
|
||||||
pub use fetch::{AudioFile, AudioFileError, StreamLoaderController};
|
pub use fetch::{AudioFile, AudioFileError, StreamLoaderController};
|
||||||
pub use fetch::{
|
pub use fetch::{
|
||||||
READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK,
|
MINIMUM_DOWNLOAD_SIZE, READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS,
|
||||||
READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS,
|
READ_AHEAD_DURING_PLAYBACK, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS,
|
||||||
};
|
};
|
||||||
|
|
|
@ -687,7 +687,6 @@ impl SpircTask {
|
||||||
match self.play_status {
|
match self.play_status {
|
||||||
SpircPlayStatus::Stopped => Ok(()),
|
SpircPlayStatus::Stopped => Ok(()),
|
||||||
_ => {
|
_ => {
|
||||||
warn!("The player has stopped unexpectedly.");
|
|
||||||
self.state.set_status(PlayStatus::kPlayStatusStop);
|
self.state.set_status(PlayStatus::kPlayStatusStop);
|
||||||
self.play_status = SpircPlayStatus::Stopped;
|
self.play_status = SpircPlayStatus::Stopped;
|
||||||
self.notify(None)
|
self.notify(None)
|
||||||
|
@ -801,9 +800,7 @@ impl SpircTask {
|
||||||
self.load_track(start_playing, update.get_state().get_position_ms());
|
self.load_track(start_playing, update.get_state().get_position_ms());
|
||||||
} else {
|
} else {
|
||||||
info!("No more tracks left in queue");
|
info!("No more tracks left in queue");
|
||||||
self.state.set_status(PlayStatus::kPlayStatusStop);
|
self.handle_stop();
|
||||||
self.player.stop();
|
|
||||||
self.play_status = SpircPlayStatus::Stopped;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.notify(None)
|
self.notify(None)
|
||||||
|
@ -909,9 +906,7 @@ impl SpircTask {
|
||||||
<= update.get_device_state().get_became_active_at()
|
<= update.get_device_state().get_became_active_at()
|
||||||
{
|
{
|
||||||
self.device.set_is_active(false);
|
self.device.set_is_active(false);
|
||||||
self.state.set_status(PlayStatus::kPlayStatusStop);
|
self.handle_stop();
|
||||||
self.player.stop();
|
|
||||||
self.play_status = SpircPlayStatus::Stopped;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -920,6 +915,10 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_stop(&mut self) {
|
||||||
|
self.player.stop();
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_play(&mut self) {
|
fn handle_play(&mut self) {
|
||||||
match self.play_status {
|
match self.play_status {
|
||||||
SpircPlayStatus::Paused {
|
SpircPlayStatus::Paused {
|
||||||
|
@ -1036,13 +1035,14 @@ impl SpircTask {
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
*preloading_of_next_track_triggered = true;
|
*preloading_of_next_track_triggered = true;
|
||||||
if let Some(track_id) = self.preview_next_track() {
|
|
||||||
self.player.preload(track_id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
SpircPlayStatus::LoadingPause { .. }
|
_ => (),
|
||||||
| SpircPlayStatus::LoadingPlay { .. }
|
}
|
||||||
| SpircPlayStatus::Stopped => (),
|
|
||||||
|
if let Some(track_id) = self.preview_next_track() {
|
||||||
|
self.player.preload(track_id);
|
||||||
|
} else {
|
||||||
|
self.handle_stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1122,9 +1122,7 @@ impl SpircTask {
|
||||||
} else {
|
} else {
|
||||||
info!("Not playing next track because there are no more tracks left in queue.");
|
info!("Not playing next track because there are no more tracks left in queue.");
|
||||||
self.state.set_playing_track_index(0);
|
self.state.set_playing_track_index(0);
|
||||||
self.state.set_status(PlayStatus::kPlayStatusStop);
|
self.handle_stop();
|
||||||
self.player.stop();
|
|
||||||
self.play_status = SpircPlayStatus::Stopped;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1392,9 +1390,7 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.state.set_status(PlayStatus::kPlayStatusStop);
|
self.handle_stop();
|
||||||
self.player.stop();
|
|
||||||
self.play_status = SpircPlayStatus::Stopped;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,15 @@ impl From<&[AudioFileMessage]> for AudioFiles {
|
||||||
fn from(files: &[AudioFileMessage]) -> Self {
|
fn from(files: &[AudioFileMessage]) -> Self {
|
||||||
let audio_files = files
|
let audio_files = files
|
||||||
.iter()
|
.iter()
|
||||||
.map(|file| (file.get_format(), FileId::from(file.get_file_id())))
|
.filter_map(|file| {
|
||||||
|
let file_id = FileId::from(file.get_file_id());
|
||||||
|
if file.has_format() {
|
||||||
|
Some((file.get_format(), file_id))
|
||||||
|
} else {
|
||||||
|
trace!("Ignoring file <{}> with unspecified format", file_id);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
AudioFiles(audio_files)
|
AudioFiles(audio_files)
|
||||||
|
|
|
@ -18,15 +18,15 @@ path = "../metadata"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
byteorder = "1.4"
|
||||||
futures-executor = "0.3"
|
futures-executor = "0.3"
|
||||||
futures-util = { version = "0.3", default_features = false, features = ["alloc"] }
|
futures-util = { version = "0.3", default_features = false, features = ["alloc"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
byteorder = "1.4"
|
parking_lot = { version = "0.11", features = ["deadlock_detection"] }
|
||||||
shell-words = "1.0.0"
|
shell-words = "1.0.0"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] }
|
tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] }
|
||||||
zerocopy = { version = "0.3" }
|
zerocopy = { version = "0.3" }
|
||||||
parking_lot = { version = "0.11", features = ["deadlock_detection"] }
|
|
||||||
|
|
||||||
# Backends
|
# Backends
|
||||||
alsa = { version = "0.5", optional = true }
|
alsa = { version = "0.5", optional = true }
|
||||||
|
@ -43,8 +43,10 @@ glib = { version = "0.10", optional = true }
|
||||||
rodio = { version = "0.14", optional = true, default-features = false }
|
rodio = { version = "0.14", optional = true, default-features = false }
|
||||||
cpal = { version = "0.13", optional = true }
|
cpal = { version = "0.13", optional = true }
|
||||||
|
|
||||||
# Decoder
|
# Container and audio decoder
|
||||||
lewton = "0.10"
|
symphonia = { version = "0.4", default-features = false, features = ["mp3", "ogg", "vorbis"] }
|
||||||
|
|
||||||
|
# Legacy Ogg container decoder for the passthrough decoder
|
||||||
ogg = "0.8"
|
ogg = "0.8"
|
||||||
|
|
||||||
# Dithering
|
# Dithering
|
||||||
|
|
|
@ -71,7 +71,7 @@ macro_rules! sink_as_bytes {
|
||||||
self.write_bytes(samples_s16.as_bytes())
|
self.write_bytes(samples_s16.as_bytes())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
AudioPacket::OggData(samples) => self.write_bytes(samples),
|
AudioPacket::Raw(samples) => self.write_bytes(samples),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,11 +25,11 @@ impl<R> AudioDecoder for VorbisDecoder<R>
|
||||||
where
|
where
|
||||||
R: Read + Seek,
|
R: Read + Seek,
|
||||||
{
|
{
|
||||||
fn seek(&mut self, absgp: u64) -> DecoderResult<()> {
|
fn seek(&mut self, absgp: u64) -> Result<u64, DecoderError> {
|
||||||
self.0
|
self.0
|
||||||
.seek_absgp_pg(absgp)
|
.seek_absgp_pg(absgp)
|
||||||
.map_err(|e| DecoderError::LewtonDecoder(e.to_string()))?;
|
.map_err(|e| DecoderError::LewtonDecoder(e.to_string()))?;
|
||||||
Ok(())
|
Ok(absgp)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_packet(&mut self) -> DecoderResult<Option<AudioPacket>> {
|
fn next_packet(&mut self) -> DecoderResult<Option<AudioPacket>> {
|
||||||
|
|
|
@ -1,26 +1,28 @@
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
mod lewton_decoder;
|
use crate::metadata::audio::AudioFileFormat;
|
||||||
pub use lewton_decoder::VorbisDecoder;
|
|
||||||
|
|
||||||
mod passthrough_decoder;
|
mod passthrough_decoder;
|
||||||
pub use passthrough_decoder::PassthroughDecoder;
|
pub use passthrough_decoder::PassthroughDecoder;
|
||||||
|
|
||||||
|
mod symphonia_decoder;
|
||||||
|
pub use symphonia_decoder::SymphoniaDecoder;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum DecoderError {
|
pub enum DecoderError {
|
||||||
#[error("Lewton Decoder Error: {0}")]
|
|
||||||
LewtonDecoder(String),
|
|
||||||
#[error("Passthrough Decoder Error: {0}")]
|
#[error("Passthrough Decoder Error: {0}")]
|
||||||
PassthroughDecoder(String),
|
PassthroughDecoder(String),
|
||||||
|
#[error("Symphonia Decoder Error: {0}")]
|
||||||
|
SymphoniaDecoder(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type DecoderResult<T> = Result<T, DecoderError>;
|
pub type DecoderResult<T> = Result<T, DecoderError>;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum AudioPacketError {
|
pub enum AudioPacketError {
|
||||||
#[error("Decoder OggData Error: Can't return OggData on Samples")]
|
#[error("Decoder Raw Error: Can't return Raw on Samples")]
|
||||||
OggData,
|
Raw,
|
||||||
#[error("Decoder Samples Error: Can't return Samples on OggData")]
|
#[error("Decoder Samples Error: Can't return Samples on Raw")]
|
||||||
Samples,
|
Samples,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,25 +30,20 @@ pub type AudioPacketResult<T> = Result<T, AudioPacketError>;
|
||||||
|
|
||||||
pub enum AudioPacket {
|
pub enum AudioPacket {
|
||||||
Samples(Vec<f64>),
|
Samples(Vec<f64>),
|
||||||
OggData(Vec<u8>),
|
Raw(Vec<u8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioPacket {
|
impl AudioPacket {
|
||||||
pub fn samples_from_f32(f32_samples: Vec<f32>) -> Self {
|
|
||||||
let f64_samples = f32_samples.iter().map(|sample| *sample as f64).collect();
|
|
||||||
AudioPacket::Samples(f64_samples)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn samples(&self) -> AudioPacketResult<&[f64]> {
|
pub fn samples(&self) -> AudioPacketResult<&[f64]> {
|
||||||
match self {
|
match self {
|
||||||
AudioPacket::Samples(s) => Ok(s),
|
AudioPacket::Samples(s) => Ok(s),
|
||||||
AudioPacket::OggData(_) => Err(AudioPacketError::OggData),
|
AudioPacket::Raw(_) => Err(AudioPacketError::Raw),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn oggdata(&self) -> AudioPacketResult<&[u8]> {
|
pub fn oggdata(&self) -> AudioPacketResult<&[u8]> {
|
||||||
match self {
|
match self {
|
||||||
AudioPacket::OggData(d) => Ok(d),
|
AudioPacket::Raw(d) => Ok(d),
|
||||||
AudioPacket::Samples(_) => Err(AudioPacketError::Samples),
|
AudioPacket::Samples(_) => Err(AudioPacketError::Samples),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,12 +51,43 @@ impl AudioPacket {
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
AudioPacket::Samples(s) => s.is_empty(),
|
AudioPacket::Samples(s) => s.is_empty(),
|
||||||
AudioPacket::OggData(d) => d.is_empty(),
|
AudioPacket::Raw(d) => d.is_empty(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait AudioDecoder {
|
pub trait AudioDecoder {
|
||||||
fn seek(&mut self, absgp: u64) -> DecoderResult<()>;
|
fn seek(&mut self, absgp: u64) -> Result<u64, DecoderError>;
|
||||||
fn next_packet(&mut self) -> DecoderResult<Option<AudioPacket>>;
|
fn next_packet(&mut self) -> DecoderResult<Option<AudioPacket>>;
|
||||||
|
|
||||||
|
fn is_ogg_vorbis(format: AudioFileFormat) -> bool
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
matches!(
|
||||||
|
format,
|
||||||
|
AudioFileFormat::OGG_VORBIS_320
|
||||||
|
| AudioFileFormat::OGG_VORBIS_160
|
||||||
|
| AudioFileFormat::OGG_VORBIS_96
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_mp3(format: AudioFileFormat) -> bool
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
matches!(
|
||||||
|
format,
|
||||||
|
AudioFileFormat::MP3_320
|
||||||
|
| AudioFileFormat::MP3_256
|
||||||
|
| AudioFileFormat::MP3_160
|
||||||
|
| AudioFileFormat::MP3_96
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<symphonia::core::errors::Error> for DecoderError {
|
||||||
|
fn from(err: symphonia::core::errors::Error) -> Self {
|
||||||
|
Self::SymphoniaDecoder(err.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
// Passthrough decoder for librespot
|
// Passthrough decoder for librespot
|
||||||
use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult};
|
use std::{
|
||||||
|
io::{Read, Seek},
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: move this to the Symphonia Ogg demuxer
|
||||||
use ogg::{OggReadError, Packet, PacketReader, PacketWriteEndInfo, PacketWriter};
|
use ogg::{OggReadError, Packet, PacketReader, PacketWriteEndInfo, PacketWriter};
|
||||||
use std::io::{Read, Seek};
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult};
|
||||||
|
|
||||||
|
use crate::metadata::audio::AudioFileFormat;
|
||||||
|
|
||||||
fn get_header<T>(code: u8, rdr: &mut PacketReader<T>) -> DecoderResult<Box<[u8]>>
|
fn get_header<T>(code: u8, rdr: &mut PacketReader<T>) -> DecoderResult<Box<[u8]>>
|
||||||
where
|
where
|
||||||
|
@ -36,7 +43,14 @@ pub struct PassthroughDecoder<R: Read + Seek> {
|
||||||
|
|
||||||
impl<R: Read + Seek> PassthroughDecoder<R> {
|
impl<R: Read + Seek> PassthroughDecoder<R> {
|
||||||
/// Constructs a new Decoder from a given implementation of `Read + Seek`.
|
/// Constructs a new Decoder from a given implementation of `Read + Seek`.
|
||||||
pub fn new(rdr: R) -> DecoderResult<Self> {
|
pub fn new(rdr: R, format: AudioFileFormat) -> DecoderResult<Self> {
|
||||||
|
if !Self::is_ogg_vorbis(format) {
|
||||||
|
return Err(DecoderError::PassthroughDecoder(format!(
|
||||||
|
"Passthrough decoder is not implemented for format {:?}",
|
||||||
|
format
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
let mut rdr = PacketReader::new(rdr);
|
let mut rdr = PacketReader::new(rdr);
|
||||||
let since_epoch = SystemTime::now()
|
let since_epoch = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
|
@ -68,7 +82,7 @@ impl<R: Read + Seek> PassthroughDecoder<R> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Read + Seek> AudioDecoder for PassthroughDecoder<R> {
|
impl<R: Read + Seek> AudioDecoder for PassthroughDecoder<R> {
|
||||||
fn seek(&mut self, absgp: u64) -> DecoderResult<()> {
|
fn seek(&mut self, absgp: u64) -> Result<u64, DecoderError> {
|
||||||
// add an eos to previous stream if missing
|
// add an eos to previous stream if missing
|
||||||
if self.bos && !self.eos {
|
if self.bos && !self.eos {
|
||||||
match self.rdr.read_packet() {
|
match self.rdr.read_packet() {
|
||||||
|
@ -101,9 +115,10 @@ impl<R: Read + Seek> AudioDecoder for PassthroughDecoder<R> {
|
||||||
.map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?;
|
.map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?;
|
||||||
match pck {
|
match pck {
|
||||||
Some(pck) => {
|
Some(pck) => {
|
||||||
self.ofsgp_page = pck.absgp_page();
|
let new_page = pck.absgp_page();
|
||||||
debug!("Seek to offset page {}", self.ofsgp_page);
|
self.ofsgp_page = new_page;
|
||||||
Ok(())
|
debug!("Seek to offset page {}", new_page);
|
||||||
|
Ok(new_page)
|
||||||
}
|
}
|
||||||
None => Err(DecoderError::PassthroughDecoder(
|
None => Err(DecoderError::PassthroughDecoder(
|
||||||
"Packet is None".to_string(),
|
"Packet is None".to_string(),
|
||||||
|
@ -184,7 +199,7 @@ impl<R: Read + Seek> AudioDecoder for PassthroughDecoder<R> {
|
||||||
let data = self.wtr.inner_mut();
|
let data = self.wtr.inner_mut();
|
||||||
|
|
||||||
if !data.is_empty() {
|
if !data.is_empty() {
|
||||||
let ogg_data = AudioPacket::OggData(std::mem::take(data));
|
let ogg_data = AudioPacket::Raw(std::mem::take(data));
|
||||||
return Ok(Some(ogg_data));
|
return Ok(Some(ogg_data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,136 +1,173 @@
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use symphonia::core::{
|
||||||
|
audio::{SampleBuffer, SignalSpec},
|
||||||
|
codecs::{Decoder, DecoderOptions},
|
||||||
|
errors::Error,
|
||||||
|
formats::{FormatReader, SeekMode, SeekTo},
|
||||||
|
io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions},
|
||||||
|
meta::{MetadataOptions, StandardTagKey, Value},
|
||||||
|
probe::Hint,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult};
|
use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult};
|
||||||
|
|
||||||
use crate::audio::AudioFile;
|
use crate::{metadata::audio::AudioFileFormat, player::NormalisationData};
|
||||||
|
|
||||||
use symphonia::core::audio::{AudioBufferRef, Channels};
|
|
||||||
use symphonia::core::codecs::Decoder;
|
|
||||||
use symphonia::core::errors::Error as SymphoniaError;
|
|
||||||
use symphonia::core::formats::{FormatReader, SeekMode, SeekTo};
|
|
||||||
use symphonia::core::io::{MediaSource, MediaSourceStream};
|
|
||||||
use symphonia::core::units::TimeStamp;
|
|
||||||
use symphonia::default::{codecs::VorbisDecoder, formats::OggReader};
|
|
||||||
|
|
||||||
use std::io::{Read, Seek, SeekFrom};
|
|
||||||
|
|
||||||
impl<R> MediaSource for FileWithConstSize<R>
|
|
||||||
where
|
|
||||||
R: Read + Seek + Send,
|
|
||||||
{
|
|
||||||
fn is_seekable(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn byte_len(&self) -> Option<u64> {
|
|
||||||
Some(self.len())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FileWithConstSize<T> {
|
|
||||||
stream: T,
|
|
||||||
len: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> FileWithConstSize<T> {
|
|
||||||
pub fn len(&self) -> u64 {
|
|
||||||
self.len
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.len() == 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> FileWithConstSize<T>
|
|
||||||
where
|
|
||||||
T: Seek,
|
|
||||||
{
|
|
||||||
pub fn new(mut stream: T) -> Self {
|
|
||||||
stream.seek(SeekFrom::End(0)).unwrap();
|
|
||||||
let len = stream.stream_position().unwrap();
|
|
||||||
stream.seek(SeekFrom::Start(0)).unwrap();
|
|
||||||
Self { stream, len }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Read for FileWithConstSize<T>
|
|
||||||
where
|
|
||||||
T: Read,
|
|
||||||
{
|
|
||||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
|
||||||
self.stream.read(buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Seek for FileWithConstSize<T>
|
|
||||||
where
|
|
||||||
T: Seek,
|
|
||||||
{
|
|
||||||
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
|
|
||||||
self.stream.seek(pos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SymphoniaDecoder {
|
pub struct SymphoniaDecoder {
|
||||||
track_id: u32,
|
track_id: u32,
|
||||||
decoder: Box<dyn Decoder>,
|
decoder: Box<dyn Decoder>,
|
||||||
format: Box<dyn FormatReader>,
|
format: Box<dyn FormatReader>,
|
||||||
position: TimeStamp,
|
sample_buffer: SampleBuffer<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SymphoniaDecoder {
|
impl SymphoniaDecoder {
|
||||||
pub fn new<R>(input: R) -> DecoderResult<Self>
|
pub fn new<R>(input: R, format: AudioFileFormat) -> DecoderResult<Self>
|
||||||
where
|
where
|
||||||
R: Read + Seek,
|
R: MediaSource + 'static,
|
||||||
{
|
{
|
||||||
let mss_opts = Default::default();
|
let mss_opts = MediaSourceStreamOptions {
|
||||||
let mss = MediaSourceStream::new(Box::new(FileWithConstSize::new(input)), mss_opts);
|
buffer_len: librespot_audio::MINIMUM_DOWNLOAD_SIZE,
|
||||||
|
};
|
||||||
|
let mss = MediaSourceStream::new(Box::new(input), mss_opts);
|
||||||
|
|
||||||
|
// Not necessary, but speeds up loading.
|
||||||
|
let mut hint = Hint::new();
|
||||||
|
if Self::is_ogg_vorbis(format) {
|
||||||
|
hint.with_extension("ogg");
|
||||||
|
hint.mime_type("audio/ogg");
|
||||||
|
} else if Self::is_mp3(format) {
|
||||||
|
hint.with_extension("mp3");
|
||||||
|
hint.mime_type("audio/mp3");
|
||||||
|
}
|
||||||
|
|
||||||
let format_opts = Default::default();
|
let format_opts = Default::default();
|
||||||
let format = OggReader::try_new(mss, &format_opts).map_err(|e| DecoderError::SymphoniaDecoder(e.to_string()))?;
|
let metadata_opts: MetadataOptions = Default::default();
|
||||||
|
let decoder_opts: DecoderOptions = Default::default();
|
||||||
|
|
||||||
let track = format.default_track().unwrap();
|
let probed =
|
||||||
let decoder_opts = Default::default();
|
symphonia::default::get_probe().format(&hint, mss, &format_opts, &metadata_opts)?;
|
||||||
let decoder = VorbisDecoder::try_new(&track.codec_params, &decoder_opts)?;
|
let format = probed.format;
|
||||||
|
|
||||||
|
let track = format.default_track().ok_or_else(|| {
|
||||||
|
DecoderError::SymphoniaDecoder("Could not retrieve default track".into())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let decoder = symphonia::default::get_codecs().make(&track.codec_params, &decoder_opts)?;
|
||||||
|
|
||||||
|
let codec_params = decoder.codec_params();
|
||||||
|
let rate = codec_params.sample_rate.ok_or_else(|| {
|
||||||
|
DecoderError::SymphoniaDecoder("Could not retrieve sample rate".into())
|
||||||
|
})?;
|
||||||
|
let channels = codec_params.channels.ok_or_else(|| {
|
||||||
|
DecoderError::SymphoniaDecoder("Could not retrieve channel configuration".into())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if rate != crate::SAMPLE_RATE {
|
||||||
|
return Err(DecoderError::SymphoniaDecoder(format!(
|
||||||
|
"Unsupported sample rate: {}",
|
||||||
|
rate
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if channels.count() != crate::NUM_CHANNELS as usize {
|
||||||
|
return Err(DecoderError::SymphoniaDecoder(format!(
|
||||||
|
"Unsupported number of channels: {}",
|
||||||
|
channels
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: settle on a sane default depending on the format
|
||||||
|
let max_frames = decoder.codec_params().max_frames_per_packet.unwrap_or(8192);
|
||||||
|
let sample_buffer = SampleBuffer::new(max_frames, SignalSpec { rate, channels });
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
track_id: track.id,
|
track_id: track.id,
|
||||||
decoder: Box::new(decoder),
|
decoder,
|
||||||
format: Box::new(format),
|
format,
|
||||||
position: 0,
|
sample_buffer,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn normalisation_data(&mut self) -> Option<NormalisationData> {
|
||||||
|
let mut metadata = self.format.metadata();
|
||||||
|
loop {
|
||||||
|
if let Some(_discarded_revision) = metadata.pop() {
|
||||||
|
// Advance to the latest metadata revision.
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
let revision = metadata.current()?;
|
||||||
|
let tags = revision.tags();
|
||||||
|
|
||||||
|
if tags.is_empty() {
|
||||||
|
// The latest metadata entry in the log is empty.
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data = NormalisationData::default();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < tags.len() {
|
||||||
|
if let Value::Float(value) = tags[i].value {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
match tags[i].std_key {
|
||||||
|
Some(StandardTagKey::ReplayGainAlbumGain) => data.album_gain_db = value,
|
||||||
|
Some(StandardTagKey::ReplayGainAlbumPeak) => data.album_peak = value,
|
||||||
|
Some(StandardTagKey::ReplayGainTrackGain) => data.track_gain_db = value,
|
||||||
|
Some(StandardTagKey::ReplayGainTrackPeak) => data.track_peak = value,
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
break Some(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioDecoder for SymphoniaDecoder {
|
impl AudioDecoder for SymphoniaDecoder {
|
||||||
fn seek(&mut self, absgp: u64) -> DecoderResult<()> {
|
// TODO : change to position ms
|
||||||
|
fn seek(&mut self, absgp: u64) -> Result<u64, DecoderError> {
|
||||||
let seeked_to = self.format.seek(
|
let seeked_to = self.format.seek(
|
||||||
SeekMode::Accurate,
|
SeekMode::Accurate,
|
||||||
SeekTo::Time {
|
SeekTo::TimeStamp {
|
||||||
time: absgp, // TODO : move to Duration
|
ts: absgp, // TODO : move to Duration
|
||||||
track_id: Some(self.track_id),
|
track_id: self.track_id,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
self.position = seeked_to.actual_ts;
|
Ok(seeked_to.actual_ts)
|
||||||
// TODO : Ok(self.position)
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_packet(&mut self) -> DecoderResult<Option<AudioPacket>> {
|
fn next_packet(&mut self) -> DecoderResult<Option<AudioPacket>> {
|
||||||
let packet = match self.format.next_packet() {
|
let packet = match self.format.next_packet() {
|
||||||
Ok(packet) => packet,
|
Ok(packet) => packet,
|
||||||
Err(e) => {
|
Err(Error::IoError(err)) => {
|
||||||
log::error!("format error: {}", err);
|
if err.kind() == io::ErrorKind::UnexpectedEof {
|
||||||
return Err(DecoderError::SymphoniaDecoder(e.to_string())),
|
return Ok(None);
|
||||||
|
} else {
|
||||||
|
return Err(DecoderError::SymphoniaDecoder(err.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(err.into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match self.decoder.decode(&packet) {
|
match self.decoder.decode(&packet) {
|
||||||
Ok(audio_buf) => {
|
Ok(audio_buf) => {
|
||||||
self.position += packet.frames() as TimeStamp;
|
// TODO : track current playback position
|
||||||
Ok(Some(packet))
|
self.sample_buffer.copy_interleaved_ref(audio_buf);
|
||||||
|
Ok(Some(AudioPacket::Samples(
|
||||||
|
self.sample_buffer.samples().to_vec(),
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
// TODO: Handle non-fatal decoding errors and retry.
|
Err(Error::ResetRequired) => {
|
||||||
Err(e) =>
|
// This may happen after a seek.
|
||||||
return Err(DecoderError::SymphoniaDecoder(e.to_string())),
|
self.decoder.reset();
|
||||||
|
self.next_packet()
|
||||||
|
}
|
||||||
|
Err(err) => Err(err.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ use std::{
|
||||||
use byteorder::{LittleEndian, ReadBytesExt};
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
use futures_util::{future, stream::futures_unordered::FuturesUnordered, StreamExt, TryFutureExt};
|
use futures_util::{future, stream::futures_unordered::FuturesUnordered, StreamExt, TryFutureExt};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use symphonia::core::io::MediaSource;
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -28,7 +29,7 @@ use crate::{
|
||||||
config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig},
|
config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig},
|
||||||
convert::Converter,
|
convert::Converter,
|
||||||
core::{util::SeqGenerator, Error, Session, SpotifyId},
|
core::{util::SeqGenerator, Error, Session, SpotifyId},
|
||||||
decoder::{AudioDecoder, AudioPacket, DecoderError, PassthroughDecoder, VorbisDecoder},
|
decoder::{AudioDecoder, AudioPacket, PassthroughDecoder, SymphoniaDecoder},
|
||||||
metadata::audio::{AudioFileFormat, AudioItem},
|
metadata::audio::{AudioFileFormat, AudioItem},
|
||||||
mixer::AudioFilter,
|
mixer::AudioFilter,
|
||||||
};
|
};
|
||||||
|
@ -220,10 +221,12 @@ pub fn ratio_to_db(ratio: f64) -> f64 {
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct NormalisationData {
|
pub struct NormalisationData {
|
||||||
track_gain_db: f32,
|
// Spotify provides these as `f32`, but audio metadata can contain up to `f64`.
|
||||||
track_peak: f32,
|
// Also, this negates the need for casting during sample processing.
|
||||||
album_gain_db: f32,
|
pub track_gain_db: f64,
|
||||||
album_peak: f32,
|
pub track_peak: f64,
|
||||||
|
pub album_gain_db: f64,
|
||||||
|
pub album_peak: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for NormalisationData {
|
impl Default for NormalisationData {
|
||||||
|
@ -238,7 +241,7 @@ impl Default for NormalisationData {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NormalisationData {
|
impl NormalisationData {
|
||||||
fn parse_from_file<T: Read + Seek>(mut file: T) -> io::Result<NormalisationData> {
|
fn parse_from_ogg<T: Read + Seek>(mut file: T) -> io::Result<NormalisationData> {
|
||||||
const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144;
|
const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144;
|
||||||
|
|
||||||
let newpos = file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?;
|
let newpos = file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?;
|
||||||
|
@ -251,10 +254,10 @@ impl NormalisationData {
|
||||||
return Ok(NormalisationData::default());
|
return Ok(NormalisationData::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
let track_gain_db = file.read_f32::<LittleEndian>()?;
|
let track_gain_db = file.read_f32::<LittleEndian>()? as f64;
|
||||||
let track_peak = file.read_f32::<LittleEndian>()?;
|
let track_peak = file.read_f32::<LittleEndian>()? as f64;
|
||||||
let album_gain_db = file.read_f32::<LittleEndian>()?;
|
let album_gain_db = file.read_f32::<LittleEndian>()? as f64;
|
||||||
let album_peak = file.read_f32::<LittleEndian>()?;
|
let album_peak = file.read_f32::<LittleEndian>()? as f64;
|
||||||
|
|
||||||
let r = NormalisationData {
|
let r = NormalisationData {
|
||||||
track_gain_db,
|
track_gain_db,
|
||||||
|
@ -277,11 +280,11 @@ impl NormalisationData {
|
||||||
[data.track_gain_db, data.track_peak]
|
[data.track_gain_db, data.track_peak]
|
||||||
};
|
};
|
||||||
|
|
||||||
let normalisation_power = gain_db as f64 + config.normalisation_pregain;
|
let normalisation_power = gain_db + config.normalisation_pregain;
|
||||||
let mut normalisation_factor = db_to_ratio(normalisation_power);
|
let mut normalisation_factor = db_to_ratio(normalisation_power);
|
||||||
|
|
||||||
if normalisation_factor * gain_peak as f64 > config.normalisation_threshold {
|
if normalisation_factor * gain_peak > config.normalisation_threshold {
|
||||||
let limited_normalisation_factor = config.normalisation_threshold / gain_peak as f64;
|
let limited_normalisation_factor = config.normalisation_threshold / gain_peak;
|
||||||
let limited_normalisation_power = ratio_to_db(limited_normalisation_factor);
|
let limited_normalisation_power = ratio_to_db(limited_normalisation_factor);
|
||||||
|
|
||||||
if config.normalisation_method == NormalisationMethod::Basic {
|
if config.normalisation_method == NormalisationMethod::Basic {
|
||||||
|
@ -304,7 +307,7 @@ impl NormalisationData {
|
||||||
normalisation_factor * 100.0
|
normalisation_factor * 100.0
|
||||||
);
|
);
|
||||||
|
|
||||||
normalisation_factor as f64
|
normalisation_factor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -820,23 +823,34 @@ impl PlayerTrackLoader {
|
||||||
}
|
}
|
||||||
let duration_ms = audio.duration as u32;
|
let duration_ms = audio.duration as u32;
|
||||||
|
|
||||||
// (Most) podcasts seem to support only 96 kbps Vorbis, so fall back to it
|
// (Most) podcasts seem to support only 96 kbps Ogg Vorbis, so fall back to it
|
||||||
// TODO: update this logic once we also support MP3 and/or FLAC
|
|
||||||
let formats = match self.config.bitrate {
|
let formats = match self.config.bitrate {
|
||||||
Bitrate::Bitrate96 => [
|
Bitrate::Bitrate96 => [
|
||||||
AudioFileFormat::OGG_VORBIS_96,
|
AudioFileFormat::OGG_VORBIS_96,
|
||||||
|
AudioFileFormat::MP3_96,
|
||||||
AudioFileFormat::OGG_VORBIS_160,
|
AudioFileFormat::OGG_VORBIS_160,
|
||||||
|
AudioFileFormat::MP3_160,
|
||||||
|
AudioFileFormat::MP3_256,
|
||||||
AudioFileFormat::OGG_VORBIS_320,
|
AudioFileFormat::OGG_VORBIS_320,
|
||||||
|
AudioFileFormat::MP3_320,
|
||||||
],
|
],
|
||||||
Bitrate::Bitrate160 => [
|
Bitrate::Bitrate160 => [
|
||||||
AudioFileFormat::OGG_VORBIS_160,
|
AudioFileFormat::OGG_VORBIS_160,
|
||||||
|
AudioFileFormat::MP3_160,
|
||||||
AudioFileFormat::OGG_VORBIS_96,
|
AudioFileFormat::OGG_VORBIS_96,
|
||||||
|
AudioFileFormat::MP3_96,
|
||||||
|
AudioFileFormat::MP3_256,
|
||||||
AudioFileFormat::OGG_VORBIS_320,
|
AudioFileFormat::OGG_VORBIS_320,
|
||||||
|
AudioFileFormat::MP3_320,
|
||||||
],
|
],
|
||||||
Bitrate::Bitrate320 => [
|
Bitrate::Bitrate320 => [
|
||||||
AudioFileFormat::OGG_VORBIS_320,
|
AudioFileFormat::OGG_VORBIS_320,
|
||||||
|
AudioFileFormat::MP3_320,
|
||||||
|
AudioFileFormat::MP3_256,
|
||||||
AudioFileFormat::OGG_VORBIS_160,
|
AudioFileFormat::OGG_VORBIS_160,
|
||||||
|
AudioFileFormat::MP3_160,
|
||||||
AudioFileFormat::OGG_VORBIS_96,
|
AudioFileFormat::OGG_VORBIS_96,
|
||||||
|
AudioFileFormat::MP3_96,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -879,43 +893,48 @@ impl PlayerTrackLoader {
|
||||||
|
|
||||||
let is_cached = encrypted_file.is_cached();
|
let is_cached = encrypted_file.is_cached();
|
||||||
|
|
||||||
|
// Setting up demuxing and decoding will trigger a seek() so always start in random access mode.
|
||||||
let stream_loader_controller = encrypted_file.get_stream_loader_controller().ok()?;
|
let stream_loader_controller = encrypted_file.get_stream_loader_controller().ok()?;
|
||||||
|
|
||||||
let key = match self.session.audio_key().request(spotify_id, file_id).await {
|
|
||||||
Ok(key) => key,
|
|
||||||
Err(e) => {
|
|
||||||
error!("Unable to load decryption key: {:?}", e);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut decrypted_file = AudioDecrypt::new(key, encrypted_file);
|
|
||||||
|
|
||||||
// Parsing normalisation data will trigger a seek() so always start in random access mode.
|
|
||||||
stream_loader_controller.set_random_access_mode();
|
stream_loader_controller.set_random_access_mode();
|
||||||
|
|
||||||
let normalisation_data = match NormalisationData::parse_from_file(&mut decrypted_file) {
|
// Not all audio files are encrypted. If we can't get a key, try loading the track
|
||||||
Ok(data) => data,
|
// without decryption. If the file was encrypted after all, the decoder will fail
|
||||||
Err(_) => {
|
// parsing and bail out, so we should be safe from outputting ear-piercing noise.
|
||||||
warn!("Unable to extract normalisation data, using default values.");
|
let key = match self.session.audio_key().request(spotify_id, file_id).await {
|
||||||
NormalisationData::default()
|
Ok(key) => Some(key),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Unable to load key, continuing without decryption: {}", e);
|
||||||
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let decrypted_file = AudioDecrypt::new(key, encrypted_file);
|
||||||
|
let mut audio_file =
|
||||||
|
Subfile::new(decrypted_file, stream_loader_controller.len() as u64);
|
||||||
|
|
||||||
let audio_file = Subfile::new(decrypted_file, 0xa7);
|
let mut normalisation_data = None;
|
||||||
|
|
||||||
let result = if self.config.passthrough {
|
let result = if self.config.passthrough {
|
||||||
match PassthroughDecoder::new(audio_file) {
|
PassthroughDecoder::new(audio_file, format).map(|x| Box::new(x) as Decoder)
|
||||||
Ok(result) => Ok(Box::new(result) as Decoder),
|
|
||||||
Err(e) => Err(DecoderError::PassthroughDecoder(e.to_string())),
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
match VorbisDecoder::new(audio_file) {
|
// Spotify stores normalisation data in a custom Ogg packet instead of Vorbis comments.
|
||||||
Ok(result) => Ok(Box::new(result) as Decoder),
|
if SymphoniaDecoder::is_ogg_vorbis(format) {
|
||||||
Err(e) => Err(DecoderError::LewtonDecoder(e.to_string())),
|
normalisation_data = NormalisationData::parse_from_ogg(&mut audio_file).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SymphoniaDecoder::new(audio_file, format).map(|mut decoder| {
|
||||||
|
// For formats other that Vorbis, we'll try getting normalisation data from
|
||||||
|
// ReplayGain metadata fields, if present.
|
||||||
|
if normalisation_data.is_none() {
|
||||||
|
normalisation_data = decoder.normalisation_data();
|
||||||
|
}
|
||||||
|
Box::new(decoder) as Decoder
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let normalisation_data = normalisation_data.unwrap_or_else(|| {
|
||||||
|
warn!("Unable to get normalisation data, continuing with defaults.");
|
||||||
|
NormalisationData::default()
|
||||||
|
});
|
||||||
|
|
||||||
let mut decoder = match result {
|
let mut decoder = match result {
|
||||||
Ok(decoder) => decoder,
|
Ok(decoder) => decoder,
|
||||||
Err(e) if is_cached => {
|
Err(e) if is_cached => {
|
||||||
|
@ -1035,7 +1054,7 @@ impl Future for PlayerInternal {
|
||||||
track_id, e
|
track_id, e
|
||||||
);
|
);
|
||||||
debug_assert!(self.state.is_loading());
|
debug_assert!(self.state.is_loading());
|
||||||
self.send_event(PlayerEvent::EndOfTrack {
|
self.send_event(PlayerEvent::Unavailable {
|
||||||
track_id,
|
track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
})
|
})
|
||||||
|
@ -2184,27 +2203,24 @@ impl fmt::Debug for PlayerState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Subfile<T: Read + Seek> {
|
struct Subfile<T: Read + Seek> {
|
||||||
stream: T,
|
stream: T,
|
||||||
offset: u64,
|
length: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Read + Seek> Subfile<T> {
|
impl<T: Read + Seek> Subfile<T> {
|
||||||
pub fn new(mut stream: T, offset: u64) -> Subfile<T> {
|
pub fn new(mut stream: T, length: u64) -> Subfile<T> {
|
||||||
let target = SeekFrom::Start(offset);
|
match stream.seek(SeekFrom::Start(0)) {
|
||||||
match stream.seek(target) {
|
|
||||||
Ok(pos) => {
|
Ok(pos) => {
|
||||||
if pos != offset {
|
if pos != 0 {
|
||||||
error!(
|
error!("Subfile::new seeking to 0 but position is now {:?}", pos);
|
||||||
"Subfile::new seeking to {:?} but position is now {:?}",
|
|
||||||
target, pos
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => error!("Subfile new Error: {}", e),
|
Err(e) => error!("Subfile new Error: {}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
Subfile { stream, offset }
|
Subfile { stream, length }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2215,21 +2231,20 @@ impl<T: Read + Seek> Read for Subfile<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Read + Seek> Seek for Subfile<T> {
|
impl<T: Read + Seek> Seek for Subfile<T> {
|
||||||
fn seek(&mut self, mut pos: SeekFrom) -> io::Result<u64> {
|
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||||
pos = match pos {
|
self.stream.seek(pos)
|
||||||
SeekFrom::Start(offset) => SeekFrom::Start(offset + self.offset),
|
}
|
||||||
x => x,
|
}
|
||||||
};
|
|
||||||
|
impl<R> MediaSource for Subfile<R>
|
||||||
let newpos = self.stream.seek(pos)?;
|
where
|
||||||
|
R: Read + Seek + Send,
|
||||||
if newpos >= self.offset {
|
{
|
||||||
Ok(newpos - self.offset)
|
fn is_seekable(&self) -> bool {
|
||||||
} else {
|
true
|
||||||
Err(io::Error::new(
|
}
|
||||||
io::ErrorKind::UnexpectedEof,
|
|
||||||
"newpos < self.offset",
|
fn byte_len(&self) -> Option<u64> {
|
||||||
))
|
Some(self.length)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue