diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63b9037e..a433c847 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: - name: install linux deps run: | sudo apt update - sudo apt install --no-install-recommends libasound2-dev pkg-config + sudo apt install -y --no-install-recommends libasound2-dev pkg-config if: contains(matrix.os, 'ubuntu') - name: install ${{ matrix.toolchain }} toolchain @@ -55,7 +55,8 @@ jobs: cargo fmt --all -- --check if: matrix.toolchain == 'stable' && matrix.os == 'ubuntu-latest' - - run: cargo test --all-targets --all-features + - run: cargo test --all-targets + - run: cargo test --features=symphonia-all --all-targets cargo-publish: if: github.event_name == 'push' && github.ref == 'refs/heads/master' env: @@ -66,7 +67,7 @@ jobs: - name: Update apt run: sudo apt update - name: Install alsa - run: sudo apt install --no-install-recommends libasound2-dev pkg-config + run: sudo apt install -y --no-install-recommends libasound2-dev pkg-config - name: Run cargo publish for rodio continue-on-error: true run: | diff --git a/Cargo.toml b/Cargo.toml index 0d79dfdd..2880d572 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ claxon = { version = "0.4.2", optional = true } hound = { version = "3.3.1", optional = true } lewton = { version = "0.10", optional = true } minimp3 = { version = "0.5.0", optional = true } +symphonia = {version = "0.3", optional = true } [features] default = ["flac", "vorbis", "wav", "mp3"] @@ -24,6 +25,16 @@ vorbis = ["lewton"] wav = ["hound"] mp3 = ["minimp3"] wasm-bindgen = ["cpal/wasm-bindgen"] +symphonia-aac = ["symphonia/aac"] +symphonia-all = ["symphonia-aac", "symphonia-flac", "symphonia-isomp4", "symphonia-mp3", "symphonia-wav"] +symphonia-flac = ["symphonia/flac"] +symphonia-isomp4 = ["symphonia/isomp4"] +symphonia-mp3 = ["symphonia/mp3"] +symphonia-wav = ["symphonia/wav", "symphonia/pcm"] [dev-dependencies] quickcheck = "0.9.2" + +[[example]] +name = "music_m4a" +required-features = ["symphonia-isomp4", "symphonia-aac"] \ No newline at end of file diff --git a/README.md b/README.md index eb5967fe..e3d28965 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ Rust playback library. - WAV decoding is handled by [hound](https://github.com/ruud-v-a/hound). - Vorbis decoding is handled by [lewton](https://github.com/est31/lewton). - Flac decoding is handled by [claxon](https://github.com/ruuda/claxon). + - MP4 and AAC (both disabled by default) are handled by [Symphonia](https://github.com/pdeljanov/Symphonia). + + Alternatively, Symphonia can be used to decode any of the other codecs above with the exception of Vorbis. See the docs for more details on backends. # [Documentation](http://docs.rs/rodio) @@ -28,4 +31,4 @@ at your option. ### License of your contributions -Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. \ No newline at end of file diff --git a/examples/music.m4a b/examples/music.m4a new file mode 100644 index 00000000..ff813515 Binary files /dev/null and b/examples/music.m4a differ diff --git a/examples/music_m4a.rs b/examples/music_m4a.rs new file mode 100644 index 00000000..b8e494c0 --- /dev/null +++ b/examples/music_m4a.rs @@ -0,0 +1,11 @@ +use std::io::BufReader; + +fn main() { + let (_stream, handle) = rodio::OutputStream::try_default().unwrap(); + let sink = rodio::Sink::try_new(&handle).unwrap(); + + let file = std::fs::File::open("examples/music.m4a").unwrap(); + sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap()); + + sink.sleep_until_end(); +} diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 16aa3eb0..88b539d4 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -5,17 +5,27 @@ use std::fmt; #[allow(unused_imports)] use std::io::{Read, Seek, SeekFrom}; use std::mem; +use std::str::FromStr; use std::time::Duration; use crate::Source; -#[cfg(feature = "flac")] +#[cfg(feature = "symphonia")] +use self::read_seek_source::ReadSeekSource; +#[cfg(feature = "symphonia")] +use ::symphonia::core::io::{MediaSource, MediaSourceStream}; + +#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] mod flac; -#[cfg(feature = "mp3")] +#[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] mod mp3; +#[cfg(feature = "symphonia")] +mod read_seek_source; +#[cfg(feature = "symphonia")] +mod symphonia; #[cfg(feature = "vorbis")] mod vorbis; -#[cfg(feature = "wav")] +#[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] mod wav; /// Source of audio samples from decoding a file. @@ -33,27 +43,29 @@ enum DecoderImpl where R: Read + Seek, { - #[cfg(feature = "wav")] + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] Wav(wav::WavDecoder), #[cfg(feature = "vorbis")] Vorbis(vorbis::VorbisDecoder), - #[cfg(feature = "flac")] + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] Flac(flac::FlacDecoder), - #[cfg(feature = "mp3")] + #[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] Mp3(mp3::Mp3Decoder), + #[cfg(feature = "symphonia")] + Symphonia(symphonia::SymphoniaDecoder), None(::std::marker::PhantomData), } impl Decoder where - R: Read + Seek, + R: Read + Seek + Send + 'static, { /// Builds a new decoder. /// /// Attempts to automatically detect the format of the source of data. #[allow(unused_variables)] pub fn new(data: R) -> Result, DecoderError> { - #[cfg(feature = "wav")] + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] let data = match wav::WavDecoder::new(data) { Err(data) => data, Ok(decoder) => { @@ -61,7 +73,7 @@ where } }; - #[cfg(feature = "flac")] + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] let data = match flac::FlacDecoder::new(data) { Err(data) => data, Ok(decoder) => { @@ -77,7 +89,7 @@ where } }; - #[cfg(feature = "mp3")] + #[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] let data = match mp3::Mp3Decoder::new(data) { Err(data) => data, Ok(decoder) => { @@ -85,6 +97,21 @@ where } }; + #[cfg(feature = "symphonia")] + { + let mss = MediaSourceStream::new( + Box::new(ReadSeekSource::new(data)) as Box, + Default::default(), + ); + + match symphonia::SymphoniaDecoder::new(mss, None) { + Err(e) => Err(e), + Ok(decoder) => { + return Ok(Decoder(DecoderImpl::Symphonia(decoder))); + } + } + } + #[cfg(not(feature = "symphonia"))] Err(DecoderError::UnrecognizedFormat) } pub fn new_looped(data: R) -> Result, DecoderError> { @@ -92,7 +119,7 @@ where } /// Builds a new decoder from wav data. - #[cfg(feature = "wav")] + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] pub fn new_wav(data: R) -> Result, DecoderError> { match wav::WavDecoder::new(data) { Err(_) => Err(DecoderError::UnrecognizedFormat), @@ -100,8 +127,14 @@ where } } + /// Builds a new decoder from wav data. + #[cfg(feature = "symphonia-wav")] + pub fn new_wav(data: R) -> Result, DecoderError> { + Decoder::new_symphonia(data, "wav") + } + /// Builds a new decoder from flac data. - #[cfg(feature = "flac")] + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] pub fn new_flac(data: R) -> Result, DecoderError> { match flac::FlacDecoder::new(data) { Err(_) => Err(DecoderError::UnrecognizedFormat), @@ -109,6 +142,12 @@ where } } + /// Builds a new decoder from flac data. + #[cfg(feature = "symphonia-flac")] + pub fn new_flac(data: R) -> Result, DecoderError> { + Decoder::new_symphonia(data, "flac") + } + /// Builds a new decoder from vorbis data. #[cfg(feature = "vorbis")] pub fn new_vorbis(data: R) -> Result, DecoderError> { @@ -119,13 +158,81 @@ where } /// Builds a new decoder from mp3 data. - #[cfg(feature = "mp3")] + #[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] pub fn new_mp3(data: R) -> Result, DecoderError> { match mp3::Mp3Decoder::new(data) { Err(_) => Err(DecoderError::UnrecognizedFormat), Ok(decoder) => Ok(Decoder(DecoderImpl::Mp3(decoder))), } } + + /// Builds a new decoder from mp3 data. + #[cfg(feature = "symphonia-mp3")] + pub fn new_mp3(data: R) -> Result, DecoderError> { + Decoder::new_symphonia(data, "mp3") + } + + /// Builds a new decoder from aac data. + #[cfg(feature = "symphonia-aac")] + pub fn new_aac(data: R) -> Result, DecoderError> { + Decoder::new_symphonia(data, "aac") + } + + /// Builds a new decoder from mp4 data. + #[cfg(feature = "symphonia-isomp4")] + pub fn new_mp4(data: R, hint: Mp4Type) -> Result, DecoderError> { + Decoder::new_symphonia(data, &hint.to_string()) + } + + #[cfg(feature = "symphonia")] + fn new_symphonia(data: R, hint: &str) -> Result, DecoderError> { + let mss = MediaSourceStream::new( + Box::new(ReadSeekSource::new(data)) as Box, + Default::default(), + ); + + match symphonia::SymphoniaDecoder::new(mss, Some(hint)) { + Err(e) => Err(e), + Ok(decoder) => { + return Ok(Decoder(DecoderImpl::Symphonia(decoder))); + } + } + } +} + +#[derive(Debug)] +pub enum Mp4Type { + Mp4, + M4a, + M4p, + M4b, + M4r, + M4v, + Mov, +} + +impl FromStr for Mp4Type { + type Err = String; + + fn from_str(input: &str) -> Result { + match &input.to_lowercase()[..] { + "mp4" => Ok(Mp4Type::Mp4), + "m4a" => Ok(Mp4Type::M4a), + "m4p" => Ok(Mp4Type::M4p), + "m4b" => Ok(Mp4Type::M4b), + "m4r" => Ok(Mp4Type::M4r), + "m4v" => Ok(Mp4Type::M4v), + "mov" => Ok(Mp4Type::Mov), + _ => Err(format!("{} is not a valid mp4 extension", input)), + } + } +} + +impl fmt::Display for Mp4Type { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let res = self.to_string().to_lowercase(); + write!(f, "{}", res) + } } impl LoopedDecoder @@ -146,14 +253,16 @@ where #[inline] fn next(&mut self) -> Option { match &mut self.0 { - #[cfg(feature = "wav")] + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] DecoderImpl::Wav(source) => source.next(), #[cfg(feature = "vorbis")] DecoderImpl::Vorbis(source) => source.next(), - #[cfg(feature = "flac")] + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] DecoderImpl::Flac(source) => source.next(), - #[cfg(feature = "mp3")] + #[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.next(), + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source) => source.next(), DecoderImpl::None(_) => None, } } @@ -161,14 +270,16 @@ where #[inline] fn size_hint(&self) -> (usize, Option) { match &self.0 { - #[cfg(feature = "wav")] + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] DecoderImpl::Wav(source) => source.size_hint(), #[cfg(feature = "vorbis")] DecoderImpl::Vorbis(source) => source.size_hint(), - #[cfg(feature = "flac")] + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] DecoderImpl::Flac(source) => source.size_hint(), - #[cfg(feature = "mp3")] + #[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.size_hint(), + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source) => source.size_hint(), DecoderImpl::None(_) => (0, None), } } @@ -181,14 +292,16 @@ where #[inline] fn current_frame_len(&self) -> Option { match &self.0 { - #[cfg(feature = "wav")] + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] DecoderImpl::Wav(source) => source.current_frame_len(), #[cfg(feature = "vorbis")] DecoderImpl::Vorbis(source) => source.current_frame_len(), - #[cfg(feature = "flac")] + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] DecoderImpl::Flac(source) => source.current_frame_len(), - #[cfg(feature = "mp3")] + #[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.current_frame_len(), + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source) => source.current_frame_len(), DecoderImpl::None(_) => Some(0), } } @@ -196,14 +309,16 @@ where #[inline] fn channels(&self) -> u16 { match &self.0 { - #[cfg(feature = "wav")] + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] DecoderImpl::Wav(source) => source.channels(), #[cfg(feature = "vorbis")] DecoderImpl::Vorbis(source) => source.channels(), - #[cfg(feature = "flac")] + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] DecoderImpl::Flac(source) => source.channels(), - #[cfg(feature = "mp3")] + #[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.channels(), + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source) => source.channels(), DecoderImpl::None(_) => 0, } } @@ -211,14 +326,16 @@ where #[inline] fn sample_rate(&self) -> u32 { match &self.0 { - #[cfg(feature = "wav")] + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] DecoderImpl::Wav(source) => source.sample_rate(), #[cfg(feature = "vorbis")] DecoderImpl::Vorbis(source) => source.sample_rate(), - #[cfg(feature = "flac")] + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] DecoderImpl::Flac(source) => source.sample_rate(), - #[cfg(feature = "mp3")] + #[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.sample_rate(), + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source) => source.sample_rate(), DecoderImpl::None(_) => 1, } } @@ -226,14 +343,16 @@ where #[inline] fn total_duration(&self) -> Option { match &self.0 { - #[cfg(feature = "wav")] + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] DecoderImpl::Wav(source) => source.total_duration(), #[cfg(feature = "vorbis")] DecoderImpl::Vorbis(source) => source.total_duration(), - #[cfg(feature = "flac")] + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] DecoderImpl::Flac(source) => source.total_duration(), - #[cfg(feature = "mp3")] + #[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.total_duration(), + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source) => source.total_duration(), DecoderImpl::None(_) => Some(Duration::default()), } } @@ -248,21 +367,23 @@ where #[inline] fn next(&mut self) -> Option { if let Some(sample) = match &mut self.0 { - #[cfg(feature = "wav")] + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] DecoderImpl::Wav(source) => source.next(), #[cfg(feature = "vorbis")] DecoderImpl::Vorbis(source) => source.next(), - #[cfg(feature = "flac")] + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] DecoderImpl::Flac(source) => source.next(), - #[cfg(feature = "mp3")] + #[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.next(), + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source) => source.next(), DecoderImpl::None(_) => None, } { Some(sample) } else { let decoder = mem::replace(&mut self.0, DecoderImpl::None(Default::default())); let (decoder, sample) = match decoder { - #[cfg(feature = "wav")] + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] DecoderImpl::Wav(source) => { let mut reader = source.into_inner(); reader.seek(SeekFrom::Start(0)).ok()?; @@ -281,7 +402,7 @@ where let sample = source.next(); (DecoderImpl::Vorbis(source), sample) } - #[cfg(feature = "flac")] + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] DecoderImpl::Flac(source) => { let mut reader = source.into_inner(); reader.seek(SeekFrom::Start(0)).ok()?; @@ -289,7 +410,7 @@ where let sample = source.next(); (DecoderImpl::Flac(source), sample) } - #[cfg(feature = "mp3")] + #[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => { let mut reader = source.into_inner(); reader.seek(SeekFrom::Start(0)).ok()?; @@ -297,6 +418,14 @@ where let sample = source.next(); (DecoderImpl::Mp3(source), sample) } + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source) => { + let mut reader = Box::new(source).into_inner(); + reader.seek(SeekFrom::Start(0)).ok()?; + let mut source = symphonia::SymphoniaDecoder::new(reader, None).ok()?; + let sample = source.next(); + (DecoderImpl::Symphonia(source), sample) + } none @ DecoderImpl::None(_) => (none, None), }; self.0 = decoder; @@ -307,14 +436,16 @@ where #[inline] fn size_hint(&self) -> (usize, Option) { match &self.0 { - #[cfg(feature = "wav")] + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] DecoderImpl::Wav(source) => (source.size_hint().0, None), #[cfg(feature = "vorbis")] DecoderImpl::Vorbis(source) => (source.size_hint().0, None), - #[cfg(feature = "flac")] + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] DecoderImpl::Flac(source) => (source.size_hint().0, None), - #[cfg(feature = "mp3")] + #[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => (source.size_hint().0, None), + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source) => (source.size_hint().0, None), DecoderImpl::None(_) => (0, None), } } @@ -327,14 +458,16 @@ where #[inline] fn current_frame_len(&self) -> Option { match &self.0 { - #[cfg(feature = "wav")] + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] DecoderImpl::Wav(source) => source.current_frame_len(), #[cfg(feature = "vorbis")] DecoderImpl::Vorbis(source) => source.current_frame_len(), - #[cfg(feature = "flac")] + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] DecoderImpl::Flac(source) => source.current_frame_len(), - #[cfg(feature = "mp3")] + #[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.current_frame_len(), + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source) => source.current_frame_len(), DecoderImpl::None(_) => Some(0), } } @@ -342,14 +475,16 @@ where #[inline] fn channels(&self) -> u16 { match &self.0 { - #[cfg(feature = "wav")] + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] DecoderImpl::Wav(source) => source.channels(), #[cfg(feature = "vorbis")] DecoderImpl::Vorbis(source) => source.channels(), - #[cfg(feature = "flac")] + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] DecoderImpl::Flac(source) => source.channels(), - #[cfg(feature = "mp3")] + #[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.channels(), + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source) => source.channels(), DecoderImpl::None(_) => 0, } } @@ -357,14 +492,16 @@ where #[inline] fn sample_rate(&self) -> u32 { match &self.0 { - #[cfg(feature = "wav")] + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] DecoderImpl::Wav(source) => source.sample_rate(), #[cfg(feature = "vorbis")] DecoderImpl::Vorbis(source) => source.sample_rate(), - #[cfg(feature = "flac")] + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] DecoderImpl::Flac(source) => source.sample_rate(), - #[cfg(feature = "mp3")] + #[cfg(all(feature = "mp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.sample_rate(), + #[cfg(feature = "symphonia")] + DecoderImpl::Symphonia(source) => source.sample_rate(), DecoderImpl::None(_) => 1, } } @@ -380,13 +517,32 @@ where pub enum DecoderError { /// The format of the data has not been recognized. UnrecognizedFormat, + + /// An IO error occured while reading, writing, or seeking the stream. + #[cfg(feature = "symphonia")] + IoError(String), + + /// The stream contained malformed data and could not be decoded or demuxed. + #[cfg(feature = "symphonia")] + DecodeError(&'static str), + + /// A default or user-defined limit was reached while decoding or demuxing the stream. Limits + /// are used to prevent denial-of-service attacks from malicious streams. + #[cfg(feature = "symphonia")] + LimitError(&'static str), + + /// The demuxer or decoder needs to be reset before continuing. + #[cfg(feature = "symphonia")] + ResetRequired, + + /// No streams were found by the decoder + #[cfg(feature = "symphonia")] + NoStreams, } impl fmt::Display for DecoderError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - DecoderError::UnrecognizedFormat => write!(f, "Unrecognized format"), - } + write!(f, "{}", self.to_string()) } } @@ -394,6 +550,16 @@ impl Error for DecoderError { fn description(&self) -> &str { match self { DecoderError::UnrecognizedFormat => "Unrecognized format", + #[cfg(feature = "symphonia")] + DecoderError::IoError(msg) => &msg[..], + #[cfg(feature = "symphonia")] + DecoderError::DecodeError(msg) => msg, + #[cfg(feature = "symphonia")] + DecoderError::LimitError(msg) => msg, + #[cfg(feature = "symphonia")] + DecoderError::ResetRequired => "Reset required", + #[cfg(feature = "symphonia")] + DecoderError::NoStreams => "No streams", } } } diff --git a/src/decoder/mp3.rs b/src/decoder/mp3.rs index 47a4cc45..973dfe19 100644 --- a/src/decoder/mp3.rs +++ b/src/decoder/mp3.rs @@ -1,4 +1,4 @@ -use std::io::{Read, Seek}; +use std::io::{Read, Seek, SeekFrom}; use std::time::Duration; use crate::Source; @@ -18,9 +18,12 @@ impl Mp3Decoder where R: Read + Seek, { - pub fn new(data: R) -> Result { + pub fn new(mut data: R) -> Result { + if !is_mp3(data.by_ref()) { + return Err(data); + } let mut decoder = Decoder::new(data); - let current_frame = decoder.next_frame().map_err(|_| ())?; + let current_frame = decoder.next_frame().unwrap(); Ok(Mp3Decoder { decoder, @@ -80,3 +83,18 @@ where Some(v) } } + +/// Returns true if the stream contains mp3 data, then resets it to where it was. +fn is_mp3(mut data: R) -> bool +where + R: Read + Seek, +{ + let stream_pos = data.seek(SeekFrom::Current(0)).unwrap(); + let mut decoder = Decoder::new(data.by_ref()); + if decoder.next_frame().is_err() { + data.seek(SeekFrom::Start(stream_pos)).unwrap(); + return false; + } + + true +} diff --git a/src/decoder/read_seek_source.rs b/src/decoder/read_seek_source.rs new file mode 100644 index 00000000..10e12dcb --- /dev/null +++ b/src/decoder/read_seek_source.rs @@ -0,0 +1,37 @@ +use std::io::{Read, Result, Seek, SeekFrom}; + +use symphonia::core::io::MediaSource; + +pub struct ReadSeekSource { + inner: T, +} + +impl ReadSeekSource { + /// Instantiates a new `ReadSeekSource` by taking ownership and wrapping the provided + /// `Read + Seek`er. + pub fn new(inner: T) -> Self { + ReadSeekSource { inner } + } +} + +impl MediaSource for ReadSeekSource { + fn is_seekable(&self) -> bool { + true + } + + fn len(&self) -> Option { + None + } +} + +impl Read for ReadSeekSource { + fn read(&mut self, buf: &mut [u8]) -> Result { + self.inner.read(buf) + } +} + +impl Seek for ReadSeekSource { + fn seek(&mut self, pos: SeekFrom) -> Result { + self.inner.seek(pos) + } +} diff --git a/src/decoder/symphonia.rs b/src/decoder/symphonia.rs new file mode 100644 index 00000000..11fa0b23 --- /dev/null +++ b/src/decoder/symphonia.rs @@ -0,0 +1,147 @@ +use std::time::Duration; +use symphonia::{ + core::{ + audio::SampleBuffer, + codecs::{Decoder, DecoderOptions}, + errors::Error, + formats::{FormatOptions, FormatReader}, + io::MediaSourceStream, + meta::MetadataOptions, + probe::Hint, + units, + }, + default::get_probe, +}; + +use crate::Source; + +use super::DecoderError; + +pub struct SymphoniaDecoder { + decoder: Box, + current_frame_offset: usize, + format: Box, + buffer: SampleBuffer, + channels: usize, +} + +impl SymphoniaDecoder { + pub fn new(mss: MediaSourceStream, extension: Option<&str>) -> Result { + match SymphoniaDecoder::init(mss, extension) { + Err(e) => match e { + Error::IoError(e) => Err(DecoderError::IoError(e.to_string())), + Error::DecodeError(e) => Err(DecoderError::DecodeError(e)), + Error::SeekError(_) => { + unreachable!("Seek errors should not occur during initialization") + } + Error::Unsupported(_) => Err(DecoderError::UnrecognizedFormat), + Error::LimitError(e) => Err(DecoderError::LimitError(e)), + Error::ResetRequired => Err(DecoderError::ResetRequired), + }, + Ok(Some(decoder)) => Ok(decoder), + Ok(None) => Err(DecoderError::NoStreams), + } + } + + pub fn into_inner(self: Box) -> MediaSourceStream { + self.format.into_inner() + } + + fn init( + mss: MediaSourceStream, + extension: Option<&str>, + ) -> symphonia::core::errors::Result> { + let mut hint = Hint::new(); + if let Some(ext) = extension { + hint.with_extension(ext); + } + let format_opts: FormatOptions = Default::default(); + let metadata_opts: MetadataOptions = Default::default(); + let mut probed = get_probe().format(&hint, mss, &format_opts, &metadata_opts)?; + + let stream = match probed.format.default_track() { + Some(stream) => stream, + None => return Ok(None), + }; + + let mut decoder = symphonia::default::get_codecs().make( + &stream.codec_params, + &DecoderOptions { + verify: true, + ..Default::default() + }, + )?; + + let current_frame = probed.format.next_packet()?; + + let decoded = decoder.decode(¤t_frame)?; + let spec = decoded.spec().clone(); + let duration = units::Duration::from(decoded.capacity() as u64); + let mut buf = SampleBuffer::::new(duration, spec.to_owned()); + buf.copy_interleaved_ref(decoded); + + return Ok(Some(SymphoniaDecoder { + decoder, + current_frame_offset: 0, + format: probed.format, + buffer: buf, + channels: spec.channels.count(), + })); + } +} + +impl Source for SymphoniaDecoder { + #[inline] + fn current_frame_len(&self) -> Option { + Some(self.buffer.samples().len()) + } + + #[inline] + fn channels(&self) -> u16 { + self.channels as u16 + } + + #[inline] + fn sample_rate(&self) -> u32 { + self.format + .default_track() + .unwrap() + .codec_params + .sample_rate + .unwrap() + } + + #[inline] + fn total_duration(&self) -> Option { + None + } +} + +impl Iterator for SymphoniaDecoder { + type Item = i16; + + #[inline] + fn next(&mut self) -> Option { + if self.current_frame_offset == self.buffer.len() { + match self.format.next_packet() { + Ok(packet) => match self.decoder.decode(&packet) { + Ok(decoded) => { + let spec = decoded.spec(); + let duration = units::Duration::from(decoded.capacity() as u64); + let mut buf = SampleBuffer::::new(duration, spec.to_owned()); + buf.copy_interleaved_ref(decoded); + self.buffer = buf; + } + Err(_) => return None, + }, + Err(_) => return None, + } + self.current_frame_offset = 0; + } + + let sample = self.buffer.samples()[self.current_frame_offset]; + self.current_frame_offset += 1; + + Some(sample) + } +} diff --git a/src/lib.rs b/src/lib.rs index 07919471..13a871bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,6 +84,19 @@ //! let source = source.take_duration(Duration::from_secs(5)).repeat_infinite(); //! ``` //! +//! ## Alternative Decoder Backends +//! +//! [Symphonia](https://github.com/pdeljanov/Symphonia) is an alternative deocder library that can be used in place +//! of many of the default backends. +//! Currently, the main benefit is that Symphonia is the only backend that supports M4A and AAC, +//! but it may be used to implement additional optional functionality in the future. +//! +//! To use, enable either the `symphonia-all` feature to enable all Symphonia codecs +//! or enable specific codecs using one of the `symphonia-{codec name}` features. +//! If you enable one or more of the Symphonia codecs, you may want to set `default-features = false` in order +//! to avoid adding extra crates to your binary. +//! See the [available feature flags](https://docs.rs/crate/rodio/latest/features) for all options. +//! //! ## How it works under the hood //! //! Rodio spawns a background thread that is dedicated to reading from the sources and sending diff --git a/tests/flac_test.rs b/tests/flac_test.rs index 729ac31e..769310e8 100644 --- a/tests/flac_test.rs +++ b/tests/flac_test.rs @@ -1,12 +1,18 @@ +#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] use rodio::Source; -use std::{io::BufReader, time::Duration}; +use std::io::BufReader; +#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] +use std::time::Duration; #[test] fn test_flac_encodings() { // 16 bit FLAC file exported from Audacity (2 channels, compression level 5) let file = std::fs::File::open("tests/audacity16bit_level5.flac").unwrap(); let mut decoder = rodio::Decoder::new(BufReader::new(file)).unwrap(); - assert!(decoder.any(|x| x != 0)); // File is not just silence + // File is not just silence + assert!(decoder.any(|x| x != 0)); + // Symphonia does not expose functionality to get the duration so this check must be disabled + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] assert_eq!(decoder.total_duration(), Some(Duration::from_secs(3))); // duration is calculated correctly // 24 bit FLAC file exported from Audacity (2 channels, various compression levels) @@ -14,6 +20,7 @@ fn test_flac_encodings() { let file = std::fs::File::open(format!("tests/audacity24bit_level{}.flac", level)).unwrap(); let mut decoder = rodio::Decoder::new(BufReader::new(file)).unwrap(); assert!(decoder.any(|x| x != 0)); + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] assert_eq!(decoder.total_duration(), Some(Duration::from_secs(3))); } }