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:
Roderick van Domburg 2022-01-03 00:13:28 +01:00
parent 2d699e288a
commit 7921f23927
No known key found for this signature in database
GPG key ID: FE2585E713F9F30A
13 changed files with 445 additions and 240 deletions

119
Cargo.lock generated
View file

@ -97,6 +97,12 @@ version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
[[package]]
name = "arrayvec"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
[[package]]
name = "async-trait"
version = "0.1.51"
@ -186,6 +192,12 @@ version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
[[package]]
name = "bytemuck"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f"
[[package]]
name = "byteorder"
version = "1.4.3"
@ -446,6 +458,15 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "env_logger"
version = "0.8.4"
@ -1121,17 +1142,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "libc"
version = "0.2.109"
@ -1398,7 +1408,6 @@ dependencies = [
"gstreamer",
"gstreamer-app",
"jack",
"lewton",
"libpulse-binding",
"libpulse-simple-binding",
"librespot-audio",
@ -1413,6 +1422,7 @@ dependencies = [
"rodio",
"sdl2",
"shell-words",
"symphonia",
"thiserror",
"tokio",
"zerocopy",
@ -2470,6 +2480,91 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "syn"
version = "1.0.82"

View file

@ -14,16 +14,20 @@ const AUDIO_AESIV: [u8; 16] = [
];
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,
}
impl<T: io::Read> AudioDecrypt<T> {
pub fn new(key: AudioKey, reader: T) -> AudioDecrypt<T> {
let cipher = Aes128Ctr::new(
pub fn new(key: Option<AudioKey>, reader: T) -> AudioDecrypt<T> {
let cipher = key.map(|key| {
Aes128Ctr::new(
GenericArray::from_slice(&key.0),
GenericArray::from_slice(&AUDIO_AESIV),
);
)
});
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> {
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)
}
@ -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> {
let newpos = self.reader.seek(pos)?;
self.cipher.seek(newpos);
if let Some(ref mut cipher) = self.cipher {
cipher.seek(newpos);
}
Ok(newpos)
}

View file

@ -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.
/// 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.
pub const MINIMUM_DOWNLOAD_SIZE: usize = 1024 * 128;

View file

@ -9,6 +9,6 @@ mod range_set;
pub use decrypt::AudioDecrypt;
pub use fetch::{AudioFile, AudioFileError, StreamLoaderController};
pub use fetch::{
READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK,
READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS,
MINIMUM_DOWNLOAD_SIZE, READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS,
READ_AHEAD_DURING_PLAYBACK, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS,
};

View file

@ -687,7 +687,6 @@ impl SpircTask {
match self.play_status {
SpircPlayStatus::Stopped => Ok(()),
_ => {
warn!("The player has stopped unexpectedly.");
self.state.set_status(PlayStatus::kPlayStatusStop);
self.play_status = SpircPlayStatus::Stopped;
self.notify(None)
@ -801,9 +800,7 @@ impl SpircTask {
self.load_track(start_playing, update.get_state().get_position_ms());
} else {
info!("No more tracks left in queue");
self.state.set_status(PlayStatus::kPlayStatusStop);
self.player.stop();
self.play_status = SpircPlayStatus::Stopped;
self.handle_stop();
}
self.notify(None)
@ -909,9 +906,7 @@ impl SpircTask {
<= update.get_device_state().get_became_active_at()
{
self.device.set_is_active(false);
self.state.set_status(PlayStatus::kPlayStatusStop);
self.player.stop();
self.play_status = SpircPlayStatus::Stopped;
self.handle_stop();
}
Ok(())
}
@ -920,6 +915,10 @@ impl SpircTask {
}
}
fn handle_stop(&mut self) {
self.player.stop();
}
fn handle_play(&mut self) {
match self.play_status {
SpircPlayStatus::Paused {
@ -1036,13 +1035,14 @@ impl SpircTask {
..
} => {
*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 => (),
} else {
self.handle_stop();
}
}
@ -1122,9 +1122,7 @@ impl SpircTask {
} else {
info!("Not playing next track because there are no more tracks left in queue.");
self.state.set_playing_track_index(0);
self.state.set_status(PlayStatus::kPlayStatusStop);
self.player.stop();
self.play_status = SpircPlayStatus::Stopped;
self.handle_stop();
}
}
@ -1392,9 +1390,7 @@ impl SpircTask {
}
}
None => {
self.state.set_status(PlayStatus::kPlayStatusStop);
self.player.stop();
self.play_status = SpircPlayStatus::Stopped;
self.handle_stop();
}
}
}

View file

@ -20,7 +20,15 @@ impl From<&[AudioFileMessage]> for AudioFiles {
fn from(files: &[AudioFileMessage]) -> Self {
let audio_files = files
.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();
AudioFiles(audio_files)

View file

@ -18,15 +18,15 @@ path = "../metadata"
version = "0.3.1"
[dependencies]
byteorder = "1.4"
futures-executor = "0.3"
futures-util = { version = "0.3", default_features = false, features = ["alloc"] }
log = "0.4"
byteorder = "1.4"
parking_lot = { version = "0.11", features = ["deadlock_detection"] }
shell-words = "1.0.0"
thiserror = "1.0"
tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] }
zerocopy = { version = "0.3" }
parking_lot = { version = "0.11", features = ["deadlock_detection"] }
# Backends
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 }
cpal = { version = "0.13", optional = true }
# Decoder
lewton = "0.10"
# Container and audio decoder
symphonia = { version = "0.4", default-features = false, features = ["mp3", "ogg", "vorbis"] }
# Legacy Ogg container decoder for the passthrough decoder
ogg = "0.8"
# Dithering

View file

@ -71,7 +71,7 @@ macro_rules! sink_as_bytes {
self.write_bytes(samples_s16.as_bytes())
}
},
AudioPacket::OggData(samples) => self.write_bytes(samples),
AudioPacket::Raw(samples) => self.write_bytes(samples),
}
}
};

View file

@ -25,11 +25,11 @@ impl<R> AudioDecoder for VorbisDecoder<R>
where
R: Read + Seek,
{
fn seek(&mut self, absgp: u64) -> DecoderResult<()> {
fn seek(&mut self, absgp: u64) -> Result<u64, DecoderError> {
self.0
.seek_absgp_pg(absgp)
.map_err(|e| DecoderError::LewtonDecoder(e.to_string()))?;
Ok(())
Ok(absgp)
}
fn next_packet(&mut self) -> DecoderResult<Option<AudioPacket>> {

View file

@ -1,26 +1,28 @@
use thiserror::Error;
mod lewton_decoder;
pub use lewton_decoder::VorbisDecoder;
use crate::metadata::audio::AudioFileFormat;
mod passthrough_decoder;
pub use passthrough_decoder::PassthroughDecoder;
mod symphonia_decoder;
pub use symphonia_decoder::SymphoniaDecoder;
#[derive(Error, Debug)]
pub enum DecoderError {
#[error("Lewton Decoder Error: {0}")]
LewtonDecoder(String),
#[error("Passthrough Decoder Error: {0}")]
PassthroughDecoder(String),
#[error("Symphonia Decoder Error: {0}")]
SymphoniaDecoder(String),
}
pub type DecoderResult<T> = Result<T, DecoderError>;
#[derive(Error, Debug)]
pub enum AudioPacketError {
#[error("Decoder OggData Error: Can't return OggData on Samples")]
OggData,
#[error("Decoder Samples Error: Can't return Samples on OggData")]
#[error("Decoder Raw Error: Can't return Raw on Samples")]
Raw,
#[error("Decoder Samples Error: Can't return Samples on Raw")]
Samples,
}
@ -28,25 +30,20 @@ pub type AudioPacketResult<T> = Result<T, AudioPacketError>;
pub enum AudioPacket {
Samples(Vec<f64>),
OggData(Vec<u8>),
Raw(Vec<u8>),
}
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]> {
match self {
AudioPacket::Samples(s) => Ok(s),
AudioPacket::OggData(_) => Err(AudioPacketError::OggData),
AudioPacket::Raw(_) => Err(AudioPacketError::Raw),
}
}
pub fn oggdata(&self) -> AudioPacketResult<&[u8]> {
match self {
AudioPacket::OggData(d) => Ok(d),
AudioPacket::Raw(d) => Ok(d),
AudioPacket::Samples(_) => Err(AudioPacketError::Samples),
}
}
@ -54,12 +51,43 @@ impl AudioPacket {
pub fn is_empty(&self) -> bool {
match self {
AudioPacket::Samples(s) => s.is_empty(),
AudioPacket::OggData(d) => d.is_empty(),
AudioPacket::Raw(d) => d.is_empty(),
}
}
}
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 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())
}
}

View file

@ -1,8 +1,15 @@
// 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 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]>>
where
@ -36,7 +43,14 @@ pub struct PassthroughDecoder<R: Read + Seek> {
impl<R: Read + Seek> PassthroughDecoder<R> {
/// 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 since_epoch = SystemTime::now()
.duration_since(UNIX_EPOCH)
@ -68,7 +82,7 @@ impl<R: Read + Seek> 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
if self.bos && !self.eos {
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()))?;
match pck {
Some(pck) => {
self.ofsgp_page = pck.absgp_page();
debug!("Seek to offset page {}", self.ofsgp_page);
Ok(())
let new_page = pck.absgp_page();
self.ofsgp_page = new_page;
debug!("Seek to offset page {}", new_page);
Ok(new_page)
}
None => Err(DecoderError::PassthroughDecoder(
"Packet is None".to_string(),
@ -184,7 +199,7 @@ impl<R: Read + Seek> AudioDecoder for PassthroughDecoder<R> {
let data = self.wtr.inner_mut();
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));
}
}

View file

@ -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 crate::audio::AudioFile;
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)
}
}
use crate::{metadata::audio::AudioFileFormat, player::NormalisationData};
pub struct SymphoniaDecoder {
track_id: u32,
decoder: Box<dyn Decoder>,
format: Box<dyn FormatReader>,
position: TimeStamp,
sample_buffer: SampleBuffer<f64>,
}
impl SymphoniaDecoder {
pub fn new<R>(input: R) -> DecoderResult<Self>
pub fn new<R>(input: R, format: AudioFileFormat) -> DecoderResult<Self>
where
R: Read + Seek,
R: MediaSource + 'static,
{
let mss_opts = Default::default();
let mss = MediaSourceStream::new(Box::new(FileWithConstSize::new(input)), mss_opts);
let mss_opts = MediaSourceStreamOptions {
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 = 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 decoder_opts = Default::default();
let decoder = VorbisDecoder::try_new(&track.codec_params, &decoder_opts)?;
let probed =
symphonia::default::get_probe().format(&hint, mss, &format_opts, &metadata_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 {
track_id: track.id,
decoder: Box::new(decoder),
format: Box::new(format),
position: 0,
decoder,
format,
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 {
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(
SeekMode::Accurate,
SeekTo::Time {
time: absgp, // TODO : move to Duration
track_id: Some(self.track_id),
SeekTo::TimeStamp {
ts: absgp, // TODO : move to Duration
track_id: self.track_id,
},
)?;
self.position = seeked_to.actual_ts;
// TODO : Ok(self.position)
Ok(())
Ok(seeked_to.actual_ts)
}
fn next_packet(&mut self) -> DecoderResult<Option<AudioPacket>> {
let packet = match self.format.next_packet() {
Ok(packet) => packet,
Err(e) => {
log::error!("format error: {}", err);
return Err(DecoderError::SymphoniaDecoder(e.to_string())),
Err(Error::IoError(err)) => {
if err.kind() == io::ErrorKind::UnexpectedEof {
return Ok(None);
} else {
return Err(DecoderError::SymphoniaDecoder(err.to_string()));
}
}
Err(err) => {
return Err(err.into());
}
};
match self.decoder.decode(&packet) {
Ok(audio_buf) => {
self.position += packet.frames() as TimeStamp;
Ok(Some(packet))
// TODO : track current playback position
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(e) =>
return Err(DecoderError::SymphoniaDecoder(e.to_string())),
Err(Error::ResetRequired) => {
// This may happen after a seek.
self.decoder.reset();
self.next_packet()
}
Err(err) => Err(err.into()),
}
}
}

View file

@ -16,6 +16,7 @@ use std::{
use byteorder::{LittleEndian, ReadBytesExt};
use futures_util::{future, stream::futures_unordered::FuturesUnordered, StreamExt, TryFutureExt};
use parking_lot::Mutex;
use symphonia::core::io::MediaSource;
use tokio::sync::{mpsc, oneshot};
use crate::{
@ -28,7 +29,7 @@ use crate::{
config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig},
convert::Converter,
core::{util::SeqGenerator, Error, Session, SpotifyId},
decoder::{AudioDecoder, AudioPacket, DecoderError, PassthroughDecoder, VorbisDecoder},
decoder::{AudioDecoder, AudioPacket, PassthroughDecoder, SymphoniaDecoder},
metadata::audio::{AudioFileFormat, AudioItem},
mixer::AudioFilter,
};
@ -220,10 +221,12 @@ pub fn ratio_to_db(ratio: f64) -> f64 {
#[derive(Clone, Copy, Debug)]
pub struct NormalisationData {
track_gain_db: f32,
track_peak: f32,
album_gain_db: f32,
album_peak: f32,
// Spotify provides these as `f32`, but audio metadata can contain up to `f64`.
// Also, this negates the need for casting during sample processing.
pub track_gain_db: f64,
pub track_peak: f64,
pub album_gain_db: f64,
pub album_peak: f64,
}
impl Default for NormalisationData {
@ -238,7 +241,7 @@ impl Default for 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;
let newpos = file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?;
@ -251,10 +254,10 @@ impl NormalisationData {
return Ok(NormalisationData::default());
}
let track_gain_db = file.read_f32::<LittleEndian>()?;
let track_peak = file.read_f32::<LittleEndian>()?;
let album_gain_db = file.read_f32::<LittleEndian>()?;
let album_peak = file.read_f32::<LittleEndian>()?;
let track_gain_db = file.read_f32::<LittleEndian>()? as f64;
let track_peak = file.read_f32::<LittleEndian>()? as f64;
let album_gain_db = file.read_f32::<LittleEndian>()? as f64;
let album_peak = file.read_f32::<LittleEndian>()? as f64;
let r = NormalisationData {
track_gain_db,
@ -277,11 +280,11 @@ impl NormalisationData {
[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);
if normalisation_factor * gain_peak as f64 > config.normalisation_threshold {
let limited_normalisation_factor = config.normalisation_threshold / gain_peak as f64;
if normalisation_factor * gain_peak > config.normalisation_threshold {
let limited_normalisation_factor = config.normalisation_threshold / gain_peak;
let limited_normalisation_power = ratio_to_db(limited_normalisation_factor);
if config.normalisation_method == NormalisationMethod::Basic {
@ -304,7 +307,7 @@ impl NormalisationData {
normalisation_factor * 100.0
);
normalisation_factor as f64
normalisation_factor
}
}
@ -820,23 +823,34 @@ impl PlayerTrackLoader {
}
let duration_ms = audio.duration as u32;
// (Most) podcasts seem to support only 96 kbps Vorbis, so fall back to it
// TODO: update this logic once we also support MP3 and/or FLAC
// (Most) podcasts seem to support only 96 kbps Ogg Vorbis, so fall back to it
let formats = match self.config.bitrate {
Bitrate::Bitrate96 => [
AudioFileFormat::OGG_VORBIS_96,
AudioFileFormat::MP3_96,
AudioFileFormat::OGG_VORBIS_160,
AudioFileFormat::MP3_160,
AudioFileFormat::MP3_256,
AudioFileFormat::OGG_VORBIS_320,
AudioFileFormat::MP3_320,
],
Bitrate::Bitrate160 => [
AudioFileFormat::OGG_VORBIS_160,
AudioFileFormat::MP3_160,
AudioFileFormat::OGG_VORBIS_96,
AudioFileFormat::MP3_96,
AudioFileFormat::MP3_256,
AudioFileFormat::OGG_VORBIS_320,
AudioFileFormat::MP3_320,
],
Bitrate::Bitrate320 => [
AudioFileFormat::OGG_VORBIS_320,
AudioFileFormat::MP3_320,
AudioFileFormat::MP3_256,
AudioFileFormat::OGG_VORBIS_160,
AudioFileFormat::MP3_160,
AudioFileFormat::OGG_VORBIS_96,
AudioFileFormat::MP3_96,
],
};
@ -879,43 +893,48 @@ impl PlayerTrackLoader {
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 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();
let normalisation_data = match NormalisationData::parse_from_file(&mut decrypted_file) {
Ok(data) => data,
Err(_) => {
warn!("Unable to extract normalisation data, using default values.");
NormalisationData::default()
// Not all audio files are encrypted. If we can't get a key, try loading the track
// without decryption. If the file was encrypted after all, the decoder will fail
// parsing and bail out, so we should be safe from outputting ear-piercing noise.
let key = match self.session.audio_key().request(spotify_id, file_id).await {
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 {
match PassthroughDecoder::new(audio_file) {
Ok(result) => Ok(Box::new(result) as Decoder),
Err(e) => Err(DecoderError::PassthroughDecoder(e.to_string())),
}
PassthroughDecoder::new(audio_file, format).map(|x| Box::new(x) as Decoder)
} else {
match VorbisDecoder::new(audio_file) {
Ok(result) => Ok(Box::new(result) as Decoder),
Err(e) => Err(DecoderError::LewtonDecoder(e.to_string())),
// Spotify stores normalisation data in a custom Ogg packet instead of Vorbis comments.
if SymphoniaDecoder::is_ogg_vorbis(format) {
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 {
Ok(decoder) => decoder,
Err(e) if is_cached => {
@ -1035,7 +1054,7 @@ impl Future for PlayerInternal {
track_id, e
);
debug_assert!(self.state.is_loading());
self.send_event(PlayerEvent::EndOfTrack {
self.send_event(PlayerEvent::Unavailable {
track_id,
play_request_id,
})
@ -2184,27 +2203,24 @@ impl fmt::Debug for PlayerState {
}
}
}
struct Subfile<T: Read + Seek> {
stream: T,
offset: u64,
length: u64,
}
impl<T: Read + Seek> Subfile<T> {
pub fn new(mut stream: T, offset: u64) -> Subfile<T> {
let target = SeekFrom::Start(offset);
match stream.seek(target) {
pub fn new(mut stream: T, length: u64) -> Subfile<T> {
match stream.seek(SeekFrom::Start(0)) {
Ok(pos) => {
if pos != offset {
error!(
"Subfile::new seeking to {:?} but position is now {:?}",
target, pos
);
if pos != 0 {
error!("Subfile::new seeking to 0 but position is now {:?}", pos);
}
}
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> {
fn seek(&mut self, mut pos: SeekFrom) -> io::Result<u64> {
pos = match pos {
SeekFrom::Start(offset) => SeekFrom::Start(offset + self.offset),
x => x,
};
let newpos = self.stream.seek(pos)?;
if newpos >= self.offset {
Ok(newpos - self.offset)
} else {
Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"newpos < self.offset",
))
}
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.stream.seek(pos)
}
}
impl<R> MediaSource for Subfile<R>
where
R: Read + Seek + Send,
{
fn is_seekable(&self) -> bool {
true
}
fn byte_len(&self) -> Option<u64> {
Some(self.length)
}
}