diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f49f11..7dae1c11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Minimal builds without `cpal` audio output are now supported. See `README.md` for instructions. (#349) - Added `Sample::is_zero()` method for checking zero samples. +- Added `DecoderBuilder` for improved configuration. +- Using `Decoder::TryFrom` for `File` now automatically wraps in `BufReader` and sets `byte_len`. + `TryFrom>` and `TryFrom` are also supported. ### Changed - Breaking: `OutputStreamBuilder` should now be used to initialize an audio output stream. @@ -29,9 +32,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Breaking: In the `Source` trait, the method `current_frame_len()` was renamed to `current_span_len()`. - Breaking: `Decoder` now outputs `f32` samples. - Breaking: The term 'frame' was renamed to 'span' in the crate and documentation. -- Breaking: Sources now use `f32` samples. To convert to and from other types of samples use functions from - `dasp_sample` crate. For example `DaspSample::from_sample(sample)`. Remove `integer-decoder` feature. - +- Breaking: `LoopedDecoder` now returns `None` if seeking fails during loop reset. +- Breaking: `ReadSeekSource::new()` now takes `Settings`. +- Breaking: Sources now use `f32` samples. To convert to and from other types of samples use + functions from `dasp_sample` crate. For example `DaspSample::from_sample(sample)`. ### Fixed - `ChannelVolume` no longer clips/overflows when converting from many channels to @@ -40,12 +44,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - An issue with `SignalGenerator` that caused it to create increasingly distorted waveforms over long run times has been corrected. (#201) - WAV and FLAC decoder duration calculation now calculated once and handles very large files - correctly -- Removed unwrap() calls in MP3, WAV, FLAC and Vorbis format detection for better error handling + correctly. +- Removed unwrap() calls in MP3, WAV, FLAC and Vorbis format detection for better error handling. +- `LoopedDecoder::size_hint` now correctly indicates an infinite stream. +- Symphonia decoder `total_duration` for Vorbis now return the correct value (#696). +- Symphonia decoder for MP4 now seeks correctly (#577). ### Deprecated - Deprecated `Sample::zero_value()` function in favor of `Sample::ZERO_VALUE` constant +### Removed +- Breaking: Removed `Mp4Type` enum in favor of using MIME type string "audio/mp4" for MP4 format detection with `Decoder::new_mp4` (#612). + # Version 0.20.1 (2024-11-08) ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 9f881afd..c41c5ddd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1105,6 +1105,7 @@ dependencies = [ "symphonia-codec-vorbis", "symphonia-core", "symphonia-format-isomp4", + "symphonia-format-ogg", "symphonia-format-riff", "symphonia-metadata", ] @@ -1211,6 +1212,18 @@ dependencies = [ "symphonia-utils-xiph", ] +[[package]] +name = "symphonia-format-ogg" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + [[package]] name = "symphonia-format-riff" version = "0.5.4" diff --git a/Cargo.toml b/Cargo.toml index eed950d3..dd3ecd87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,12 +48,14 @@ symphonia-all = [ "symphonia-flac", "symphonia-isomp4", "symphonia-mp3", + "symphonia-ogg", "symphonia-vorbis", "symphonia-wav", ] symphonia-flac = ["symphonia/flac"] symphonia-isomp4 = ["symphonia/isomp4"] symphonia-mp3 = ["symphonia/mp3"] +symphonia-ogg = ["symphonia/ogg"] symphonia-vorbis = ["symphonia/vorbis"] symphonia-wav = ["symphonia/wav", "symphonia/pcm", "symphonia/adpcm"] symphonia-alac = ["symphonia/isomp4", "symphonia/alac"] diff --git a/examples/automatic_gain_control.rs b/examples/automatic_gain_control.rs index 3387679c..aa0e36df 100644 --- a/examples/automatic_gain_control.rs +++ b/examples/automatic_gain_control.rs @@ -2,7 +2,6 @@ use rodio::source::Source; use rodio::Decoder; use std::error::Error; use std::fs::File; -use std::io::BufReader; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread; @@ -13,8 +12,8 @@ fn main() -> Result<(), Box> { let sink = rodio::Sink::connect_new(&stream_handle.mixer()); // Decode the sound file into a source - let file = BufReader::new(File::open("assets/music.flac")?); - let source = Decoder::new(file)?; + let file = File::open("assets/music.flac")?; + let source = Decoder::try_from(file)?; // Apply automatic gain control to the source let agc_source = source.automatic_gain_control(1.0, 4.0, 0.005, 5.0); diff --git a/examples/callback_on_end.rs b/examples/callback_on_end.rs index de8c83cc..220d558c 100644 --- a/examples/callback_on_end.rs +++ b/examples/callback_on_end.rs @@ -1,5 +1,4 @@ use std::error::Error; -use std::io::BufReader; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::Arc; @@ -8,7 +7,7 @@ fn main() -> Result<(), Box> { let sink = rodio::Sink::connect_new(&stream_handle.mixer()); let file = std::fs::File::open("assets/music.wav")?; - sink.append(rodio::Decoder::new(BufReader::new(file))?); + sink.append(rodio::Decoder::try_from(file)?); // lets increment a number after `music.wav` has played. We are going to use atomics // however you could also use a `Mutex` or send a message through a `std::sync::mpsc`. diff --git a/examples/into_file.rs b/examples/into_file.rs index 193932b2..bedd8f8a 100644 --- a/examples/into_file.rs +++ b/examples/into_file.rs @@ -1,13 +1,12 @@ use rodio::{output_to_wav, Source}; use std::error::Error; -use std::io::BufReader; /// Converts mp3 file to a wav file. /// This example does not use any audio devices /// and can be used in build configurations without `cpal` feature enabled. fn main() -> Result<(), Box> { let file = std::fs::File::open("assets/music.mp3")?; - let mut audio = rodio::Decoder::new(BufReader::new(file))? + let mut audio = rodio::Decoder::try_from(file)? .automatic_gain_control(1.0, 4.0, 0.005, 3.0) .speed(0.8); diff --git a/examples/music_flac.rs b/examples/music_flac.rs index 13013bab..5a949273 100644 --- a/examples/music_flac.rs +++ b/examples/music_flac.rs @@ -1,12 +1,11 @@ use std::error::Error; -use std::io::BufReader; fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(&stream_handle.mixer()); let file = std::fs::File::open("assets/music.flac")?; - sink.append(rodio::Decoder::new(BufReader::new(file))?); + sink.append(rodio::Decoder::try_from(file)?); sink.sleep_until_end(); diff --git a/examples/music_m4a.rs b/examples/music_m4a.rs index 15b58432..ca7c4391 100644 --- a/examples/music_m4a.rs +++ b/examples/music_m4a.rs @@ -1,12 +1,11 @@ use std::error::Error; -use std::io::BufReader; fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(&stream_handle.mixer()); let file = std::fs::File::open("assets/music.m4a")?; - sink.append(rodio::Decoder::new(BufReader::new(file))?); + sink.append(rodio::Decoder::try_from(file)?); sink.sleep_until_end(); diff --git a/examples/music_mp3.rs b/examples/music_mp3.rs index cb94159f..209848e2 100644 --- a/examples/music_mp3.rs +++ b/examples/music_mp3.rs @@ -1,12 +1,11 @@ use std::error::Error; -use std::io::BufReader; fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(&stream_handle.mixer()); let file = std::fs::File::open("assets/music.mp3")?; - sink.append(rodio::Decoder::new(BufReader::new(file))?); + sink.append(rodio::Decoder::try_from(file)?); sink.sleep_until_end(); diff --git a/examples/music_ogg.rs b/examples/music_ogg.rs index 74c36229..bd7e7b9b 100644 --- a/examples/music_ogg.rs +++ b/examples/music_ogg.rs @@ -1,12 +1,11 @@ use std::error::Error; -use std::io::BufReader; fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(&stream_handle.mixer()); let file = std::fs::File::open("assets/music.ogg")?; - sink.append(rodio::Decoder::new(BufReader::new(file))?); + sink.append(rodio::Decoder::try_from(file)?); sink.sleep_until_end(); diff --git a/examples/music_wav.rs b/examples/music_wav.rs index a29d929b..b482849a 100644 --- a/examples/music_wav.rs +++ b/examples/music_wav.rs @@ -1,12 +1,11 @@ use std::error::Error; -use std::io::BufReader; fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(&stream_handle.mixer()); let file = std::fs::File::open("assets/music.wav")?; - sink.append(rodio::Decoder::new(BufReader::new(file))?); + sink.append(rodio::Decoder::try_from(file)?); sink.sleep_until_end(); diff --git a/examples/reverb.rs b/examples/reverb.rs index dc74e71d..8bd30bdf 100644 --- a/examples/reverb.rs +++ b/examples/reverb.rs @@ -1,6 +1,5 @@ use rodio::Source; use std::error::Error; -use std::io::BufReader; use std::time::Duration; fn main() -> Result<(), Box> { @@ -8,7 +7,7 @@ fn main() -> Result<(), Box> { let sink = rodio::Sink::connect_new(&stream_handle.mixer()); let file = std::fs::File::open("assets/music.ogg")?; - let source = rodio::Decoder::new(BufReader::new(file))?; + let source = rodio::Decoder::try_from(file)?; let with_reverb = source.buffered().reverb(Duration::from_millis(40), 0.7); sink.append(with_reverb); diff --git a/examples/seek_mp3.rs b/examples/seek_mp3.rs index a78238bc..e98c7a46 100644 --- a/examples/seek_mp3.rs +++ b/examples/seek_mp3.rs @@ -1,5 +1,4 @@ use std::error::Error; -use std::io::BufReader; use std::time::Duration; fn main() -> Result<(), Box> { @@ -7,7 +6,7 @@ fn main() -> Result<(), Box> { let sink = rodio::Sink::connect_new(&stream_handle.mixer()); let file = std::fs::File::open("assets/music.mp3")?; - sink.append(rodio::Decoder::new(BufReader::new(file))?); + sink.append(rodio::Decoder::try_from(file)?); std::thread::sleep(std::time::Duration::from_secs(2)); sink.try_seek(Duration::from_secs(0))?; diff --git a/examples/spatial.rs b/examples/spatial.rs index 31505ecc..6edf3f0b 100644 --- a/examples/spatial.rs +++ b/examples/spatial.rs @@ -1,5 +1,4 @@ use std::error::Error; -use std::io::BufReader; use std::thread; use std::time::Duration; @@ -30,7 +29,7 @@ fn main() -> Result<(), Box> { ); let file = std::fs::File::open("assets/music.ogg")?; - let source = rodio::Decoder::new(BufReader::new(file))? + let source = rodio::Decoder::try_from(file)? .repeat_infinite() .take_duration(total_duration); sink.append(source); diff --git a/examples/stereo.rs b/examples/stereo.rs index 5b5ec500..c7f82182 100644 --- a/examples/stereo.rs +++ b/examples/stereo.rs @@ -2,14 +2,13 @@ use rodio::Source; use std::error::Error; -use std::io::BufReader; fn main() -> Result<(), Box> { let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?; let sink = rodio::Sink::connect_new(&stream_handle.mixer()); let file = std::fs::File::open("assets/RL.ogg")?; - sink.append(rodio::Decoder::new(BufReader::new(file))?.amplify(0.2)); + sink.append(rodio::Decoder::try_from(file)?.amplify(0.2)); sink.sleep_until_end(); diff --git a/src/decoder/builder.rs b/src/decoder/builder.rs new file mode 100644 index 00000000..60353127 --- /dev/null +++ b/src/decoder/builder.rs @@ -0,0 +1,328 @@ +//! Builder pattern for configuring and constructing decoders. +//! +//! This module provides a flexible builder API for creating decoders with custom settings. +//! The builder allows configuring format hints, seeking behavior, byte length and other +//! parameters that affect decoder behavior. +//! +//! # Examples +//! +//! ```no_run +//! use std::fs::File; +//! use rodio::Decoder; +//! +//! fn main() -> Result<(), Box> { +//! let file = File::open("audio.mp3")?; +//! let len = file.metadata()?.len(); +//! +//! Decoder::builder() +//! .with_data(file) +//! .with_byte_len(len) // Enable seeking and duration calculation +//! .with_hint("mp3") // Optional format hint +//! .with_gapless(true) // Enable gapless playback +//! .build()?; +//! +//! // Use the decoder... +//! Ok(()) +//! } +//! ``` +//! +//! # Settings +//! +//! The following settings can be configured: +//! +//! - `byte_len` - Total length of the input data in bytes +//! - `hint` - Format hint like "mp3", "wav", etc +//! - `mime_type` - MIME type hint for container formats +//! - `seekable` - Whether seeking operations are enabled +//! - `gapless` - Enable gapless playback +//! - `coarse_seek` - Use faster but less precise seeking + +use std::io::{Read, Seek}; + +#[cfg(feature = "symphonia")] +use self::read_seek_source::ReadSeekSource; +#[cfg(feature = "symphonia")] +use ::symphonia::core::io::{MediaSource, MediaSourceStream}; + +use super::*; + +/// Audio decoder configuration settings. +/// Support for these settings depends on the underlying decoder implementation. +/// Currently, settings are only used by the Symphonia decoder. +#[derive(Clone, Debug)] +pub struct Settings { + /// The length of the stream in bytes. + /// This is required for: + /// - Reliable seeking operations + /// - Duration calculations in formats that lack timing information (e.g. MP3, Vorbis) + /// + /// Can be obtained from file metadata or by seeking to the end of the stream. + pub(crate) byte_len: Option, + + /// Whether to use coarse seeking, or sample-accurate seeking instead. + pub(crate) coarse_seek: bool, + + /// Whether to trim frames for gapless playback. + /// Note: Disabling this may affect duration calculations for some formats + /// as padding frames will be included. + pub(crate) gapless: bool, + + /// An extension hint for the decoder about the format of the stream. + /// When known, this can help the decoder to select the correct codec. + pub(crate) hint: Option, + + /// An MIME type hint for the decoder about the format of the stream. + /// When known, this can help the decoder to select the correct demuxer. + pub(crate) mime_type: Option, + + /// Whether the decoder should report as seekable. + pub(crate) is_seekable: bool, +} + +impl Default for Settings { + fn default() -> Self { + Self { + byte_len: None, + coarse_seek: false, + gapless: true, + hint: None, + mime_type: None, + is_seekable: false, + } + } +} + +/// Builder for configuring and creating a decoder. +/// +/// This provides a flexible way to configure decoder settings before creating +/// the actual decoder instance. +/// +/// # Examples +/// +/// ```no_run +/// use std::fs::File; +/// use rodio::decoder::DecoderBuilder; +/// +/// fn main() -> Result<(), Box> { +/// let file = File::open("audio.mp3")?; +/// let decoder = DecoderBuilder::new() +/// .with_data(file) +/// .with_hint("mp3") +/// .with_gapless(true) +/// .build()?; +/// +/// // Use the decoder... +/// Ok(()) +/// } +/// ``` +#[derive(Clone, Debug)] +pub struct DecoderBuilder { + /// The input data source to decode. + data: Option, + /// Configuration settings for the decoder. + settings: Settings, +} + +impl Default for DecoderBuilder { + fn default() -> Self { + Self { + data: None, + settings: Settings::default(), + } + } +} + +impl DecoderBuilder { + /// Creates a new decoder builder with default settings. + /// + /// # Examples + /// ```no_run + /// use std::fs::File; + /// use rodio::decoder::DecoderBuilder; + /// + /// fn main() -> Result<(), Box> { + /// let file = File::open("audio.mp3")?; + /// let decoder = DecoderBuilder::new() + /// .with_data(file) + /// .build()?; + /// + /// // Use the decoder... + /// Ok(()) + /// } + /// ``` + pub fn new() -> Self { + Self::default() + } + + /// Sets the input data source to decode. + pub fn with_data(mut self, data: R) -> Self { + self.data = Some(data); + self + } + + /// Sets the byte length of the stream. + /// This is required for: + /// - Reliable seeking operations + /// - Duration calculations in formats that lack timing information (e.g. MP3, Vorbis) + /// + /// The byte length should typically be obtained from file metadata: + /// ```no_run + /// use std::fs::File; + /// use rodio::Decoder; + /// + /// fn main() -> Result<(), Box> { + /// let file = File::open("audio.mp3")?; + /// let len = file.metadata()?.len(); + /// let decoder = Decoder::builder() + /// .with_data(file) + /// .with_byte_len(len) + /// .build()?; + /// + /// // Use the decoder... + /// Ok(()) + /// } + /// ``` + /// + /// Alternatively, it can be obtained by seeking to the end of the stream. + /// + /// An incorrect byte length can lead to unexpected behavior, including but not limited to + /// incorrect duration calculations and seeking errors. + pub fn with_byte_len(mut self, byte_len: u64) -> Self { + self.settings.byte_len = Some(byte_len); + self + } + + /// Enables or disables coarse seeking. + /// + /// This needs `byte_len` to be set. Coarse seeking is faster but less accurate: + /// it may seek to a position slightly before or after the requested one, + /// especially when the bitrate is variable. + pub fn with_coarse_seek(mut self, coarse_seek: bool) -> Self { + self.settings.coarse_seek = coarse_seek; + self + } + + /// Enables or disables gapless playback. + /// + /// When enabled, removes silence between tracks for formats that support it. + pub fn with_gapless(mut self, gapless: bool) -> Self { + self.settings.gapless = gapless; + self + } + + /// Sets a format hint for the decoder. + /// + /// When known, this can help the decoder to select the correct codec faster. + /// Common values are "mp3", "wav", "flac", "ogg", etc. + pub fn with_hint(mut self, hint: &str) -> Self { + self.settings.hint = Some(hint.to_string()); + self + } + + /// Sets a mime type hint for the decoder. + /// + /// When known, this can help the decoder to select the correct demuxer faster. + /// Common values are "audio/mpeg", "audio/vnd.wav", "audio/flac", "audio/ogg", etc. + pub fn with_mime_type(mut self, mime_type: &str) -> Self { + self.settings.mime_type = Some(mime_type.to_string()); + self + } + + /// Configure whether the decoder should report as seekable. + /// + /// For reliable seeking behavior, `byte_len` should be set either from file metadata + /// or by seeking to the end of the stream. While seeking may work without `byte_len` + /// for some formats, it is not guaranteed. + /// + /// # Examples + /// ```no_run + /// use std::fs::File; + /// use rodio::Decoder; + /// + /// fn main() -> Result<(), Box> { + /// let file = File::open("audio.mp3")?; + /// let len = file.metadata()?.len(); + /// + /// // Recommended: Set both byte_len and seekable + /// let decoder = Decoder::builder() + /// .with_data(file) + /// .with_byte_len(len) + /// .with_seekable(true) + /// .build()?; + /// + /// // Use the decoder... + /// Ok(()) + /// } + /// ``` + pub fn with_seekable(mut self, is_seekable: bool) -> Self { + self.settings.is_seekable = is_seekable; + self + } + + /// Creates the decoder implementation with configured settings. + fn build_impl(self) -> Result<(DecoderImpl, Settings), DecoderError> { + let data = self.data.ok_or(DecoderError::UnrecognizedFormat)?; + + #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] + let data = match wav::WavDecoder::new(data) { + Ok(decoder) => return Ok((DecoderImpl::Wav(decoder), self.settings)), + Err(data) => data, + }; + #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] + let data = match flac::FlacDecoder::new(data) { + Ok(decoder) => return Ok((DecoderImpl::Flac(decoder), self.settings)), + Err(data) => data, + }; + + #[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))] + let data = match vorbis::VorbisDecoder::new(data) { + Ok(decoder) => return Ok((DecoderImpl::Vorbis(decoder), self.settings)), + Err(data) => data, + }; + + #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] + let data = match mp3::Mp3Decoder::new(data) { + Ok(decoder) => return Ok((DecoderImpl::Mp3(decoder), self.settings)), + Err(data) => data, + }; + + #[cfg(feature = "symphonia")] + { + let mss = MediaSourceStream::new( + Box::new(ReadSeekSource::new(data, &self.settings)) as Box, + Default::default(), + ); + + symphonia::SymphoniaDecoder::new(mss, &self.settings) + .map(|decoder| (DecoderImpl::Symphonia(decoder, PhantomData), self.settings)) + } + + #[cfg(not(feature = "symphonia"))] + Err(DecoderError::UnrecognizedFormat) + } + + /// Creates a new decoder with previously configured settings. + /// + /// # Errors + /// + /// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined + /// or is not supported. + pub fn build(self) -> Result, DecoderError> { + let (decoder, _) = self.build_impl()?; + Ok(Decoder(decoder)) + } + + /// Creates a new looped decoder with previously configured settings. + /// + /// # Errors + /// + /// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined + /// or is not supported. + pub fn build_looped(self) -> Result, DecoderError> { + let (decoder, settings) = self.build_impl()?; + Ok(LoopedDecoder { + inner: Some(decoder), + settings, + }) + } +} diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 84aa469e..868cb984 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -1,21 +1,69 @@ -//! Decodes samples from an audio file. +//! Decodes audio samples from various audio file formats. +//! +//! This module provides decoders for common audio formats like MP3, WAV, Vorbis and FLAC. +//! It supports both one-shot playback and looped playback of audio files. +//! +//! # Usage +//! +//! The simplest way to decode files (automatically sets up seeking and duration): +//! ```no_run +//! use std::fs::File; +//! use rodio::Decoder; +//! +//! let file = File::open("audio.mp3").unwrap(); +//! let decoder = Decoder::try_from(file).unwrap(); // Automatically sets byte_len from metadata +//! ``` +//! +//! For more control over decoder settings, use the builder pattern: +//! ```no_run +//! use std::fs::File; +//! use rodio::Decoder; +//! +//! let file = File::open("audio.mp3").unwrap(); +//! let len = file.metadata().unwrap().len(); +//! +//! let decoder = Decoder::builder() +//! .with_data(file) +//! .with_byte_len(len) // Enable seeking and duration calculation +//! .with_seekable(true) // Enable seeking operations +//! .with_hint("mp3") // Optional format hint +//! .with_gapless(true) // Enable gapless playback +//! .build() +//! .unwrap(); +//! ``` +//! +//! # Features +//! +//! The following audio formats are supported based on enabled features: +//! +//! - `wav` - WAV format support +//! - `flac` - FLAC format support +//! - `vorbis` - Vorbis format support +//! - `mp3` - MP3 format support via minimp3 +//! - `symphonia` - Enhanced format support via the Symphonia backend +//! +//! When using `symphonia`, additional formats like AAC and MP4 containers become available +//! if the corresponding features are enabled. + +use std::{ + error::Error, + fmt, + io::{BufReader, Read, Seek}, + marker::PhantomData, + time::Duration, +}; -use std::error::Error; -use std::fmt; #[allow(unused_imports)] -use std::io::{Read, Seek, SeekFrom}; -use std::mem; -use std::str::FromStr; -use std::time::Duration; +use std::io::SeekFrom; -use crate::source::SeekError; -use crate::{Sample, Source}; +use crate::{ + common::{ChannelCount, SampleRate}, + source::{SeekError, Source}, + Sample, +}; -#[cfg(feature = "symphonia")] -use self::read_seek_source::ReadSeekSource; -use crate::common::{ChannelCount, SampleRate}; -#[cfg(feature = "symphonia")] -use ::symphonia::core::io::{MediaSource, MediaSourceStream}; +pub mod builder; +pub use builder::{DecoderBuilder, Settings}; #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] mod flac; @@ -31,28 +79,36 @@ mod vorbis; #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] mod wav; -/// Source of audio samples from decoding a file. -/// -/// Supports MP3, WAV, Vorbis and Flac. -pub struct Decoder(DecoderImpl) -where - R: Read + Seek; +/// Source of audio samples decoded from an input stream. +/// See the [module-level documentation](self) for examples and usage. +pub struct Decoder(DecoderImpl); -/// Source of audio samples from decoding a file that never ends. When the -/// end of the file is reached the decoder starts again from the beginning. +/// Source of audio samples from decoding a file that never ends. +/// When the end of the file is reached, the decoder starts again from the beginning. /// -/// Supports MP3, WAV, Vorbis and Flac. -pub struct LoopedDecoder(DecoderImpl) -where - R: Read + Seek; +/// A `LoopedDecoder` will attempt to seek back to the start of the stream when it reaches +/// the end. If seeking fails for any reason (like IO errors), iteration will stop. +/// +/// # Examples +/// +/// ```no_run +/// use std::fs::File; +/// use rodio::Decoder; +/// +/// let file = File::open("audio.mp3").unwrap(); +/// let looped_decoder = Decoder::new_looped(file).unwrap(); +/// ``` +pub struct LoopedDecoder { + /// The underlying decoder implementation. + inner: Option>, + /// Configuration settings for the decoder. + settings: Settings, +} // Cannot really reduce the size of the VorbisDecoder. There are not any // arrays just a lot of struct fields. #[allow(clippy::large_enum_variant)] -enum DecoderImpl -where - R: Read + Seek, -{ +enum DecoderImpl { #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] Wav(wav::WavDecoder), #[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))] @@ -62,8 +118,7 @@ where #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] Mp3(mp3::Mp3Decoder), #[cfg(feature = "symphonia")] - Symphonia(symphonia::SymphoniaDecoder), - None(::std::marker::PhantomData), + Symphonia(symphonia::SymphoniaDecoder, PhantomData), } impl DecoderImpl { @@ -79,8 +134,7 @@ impl DecoderImpl { #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.next(), #[cfg(feature = "symphonia")] - DecoderImpl::Symphonia(source) => source.next(), - DecoderImpl::None(_) => None, + DecoderImpl::Symphonia(source, PhantomData) => source.next(), } } @@ -96,8 +150,7 @@ impl DecoderImpl { #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.size_hint(), #[cfg(feature = "symphonia")] - DecoderImpl::Symphonia(source) => source.size_hint(), - DecoderImpl::None(_) => (0, None), + DecoderImpl::Symphonia(source, PhantomData) => source.size_hint(), } } @@ -113,8 +166,7 @@ impl DecoderImpl { #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.current_span_len(), #[cfg(feature = "symphonia")] - DecoderImpl::Symphonia(source) => source.current_span_len(), - DecoderImpl::None(_) => Some(0), + DecoderImpl::Symphonia(source, PhantomData) => source.current_span_len(), } } @@ -130,8 +182,7 @@ impl DecoderImpl { #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.channels(), #[cfg(feature = "symphonia")] - DecoderImpl::Symphonia(source) => source.channels(), - DecoderImpl::None(_) => 0, + DecoderImpl::Symphonia(source, PhantomData) => source.channels(), } } @@ -147,11 +198,16 @@ impl DecoderImpl { #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.sample_rate(), #[cfg(feature = "symphonia")] - DecoderImpl::Symphonia(source) => source.sample_rate(), - DecoderImpl::None(_) => 1, + DecoderImpl::Symphonia(source, PhantomData) => source.sample_rate(), } } + /// Returns the total duration of this audio source. + /// + /// # Symphonia Notes + /// + /// For formats that lack timing information like MP3 and Vorbis, this requires the decoder to + /// be initialized with the correct byte length via `Decoder::builder().with_byte_len()`. #[inline] fn total_duration(&self) -> Option { match self { @@ -164,8 +220,7 @@ impl DecoderImpl { #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.total_duration(), #[cfg(feature = "symphonia")] - DecoderImpl::Symphonia(source) => source.total_duration(), - DecoderImpl::None(_) => Some(Duration::default()), + DecoderImpl::Symphonia(source, PhantomData) => source.total_duration(), } } @@ -181,214 +236,311 @@ impl DecoderImpl { #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] DecoderImpl::Mp3(source) => source.try_seek(pos), #[cfg(feature = "symphonia")] - DecoderImpl::Symphonia(source) => source.try_seek(pos), - DecoderImpl::None(_) => Err(SeekError::NotSupported { - underlying_source: "DecoderImpl::None", - }), + DecoderImpl::Symphonia(source, PhantomData) => source.try_seek(pos), } } } -impl Decoder +/// Converts a `File` into a `Decoder` with automatic optimizations. +/// This is the preferred way to decode files as it enables seeking optimizations +/// and accurate duration calculations. +/// +/// This implementation: +/// - Wraps the file in a `BufReader` for better performance +/// - Gets the file length from metadata to improve seeking operations and duration accuracy +/// - Enables seeking by default +/// +/// # Errors +/// +/// Returns an error if: +/// - The file metadata cannot be read +/// - The audio format cannot be recognized or is not supported +/// +/// # Examples +/// ```no_run +/// use std::fs::File; +/// use rodio::Decoder; +/// +/// let file = File::open("audio.mp3").unwrap(); +/// let decoder = Decoder::try_from(file).unwrap(); +/// ``` +impl TryFrom for Decoder> { + type Error = DecoderError; + + fn try_from(file: std::fs::File) -> Result { + let len = file + .metadata() + .map_err(|e| Self::Error::IoError(e.to_string()))? + .len(); + + Self::builder() + .with_data(BufReader::new(file)) + .with_byte_len(len) + .with_seekable(true) + .build() + } +} + +/// Converts a `BufReader` into a `Decoder`. +/// When working with files, prefer `TryFrom` as it will automatically set byte_len +/// for better seeking performance. +/// +/// # Errors +/// +/// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined +/// or is not supported. +/// +/// # Examples +/// ```no_run +/// use std::fs::File; +/// use std::io::BufReader; +/// use rodio::Decoder; +/// +/// let file = File::open("audio.mp3").unwrap(); +/// let reader = BufReader::new(file); +/// let decoder = Decoder::try_from(reader).unwrap(); +/// ``` +impl TryFrom> for Decoder> where R: Read + Seek + Send + Sync + '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(all(feature = "wav", not(feature = "symphonia-wav")))] - let data = match wav::WavDecoder::new(data) { - Err(data) => data, - Ok(decoder) => { - return Ok(Decoder(DecoderImpl::Wav(decoder))); - } - }; + type Error = DecoderError; - #[cfg(all(feature = "flac", not(feature = "symphonia-flac")))] - let data = match flac::FlacDecoder::new(data) { - Err(data) => data, - Ok(decoder) => { - return Ok(Decoder(DecoderImpl::Flac(decoder))); - } - }; - - #[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))] - let data = match vorbis::VorbisDecoder::new(data) { - Err(data) => data, - Ok(decoder) => { - return Ok(Decoder(DecoderImpl::Vorbis(decoder))); - } - }; - - #[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))] - let data = match mp3::Mp3Decoder::new(data) { - Err(data) => data, - Ok(decoder) => { - return Ok(Decoder(DecoderImpl::Mp3(decoder))); - } - }; + fn try_from(data: BufReader) -> Result { + Self::new(data) + } +} - #[cfg(feature = "symphonia")] - { - let mss = MediaSourceStream::new( - Box::new(ReadSeekSource::new(data)) as Box, - Default::default(), - ); +/// Converts a `Cursor` into a `Decoder`. +/// When working with files, prefer `TryFrom` as it will automatically set byte_len +/// for better seeking performance. +/// +/// This is useful for decoding audio data that's already in memory. +/// +/// # Errors +/// +/// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined +/// or is not supported. +/// +/// # Examples +/// ```no_run +/// use std::io::Cursor; +/// use rodio::Decoder; +/// +/// let data = std::fs::read("audio.mp3").unwrap(); +/// let cursor = Cursor::new(data); +/// let decoder = Decoder::try_from(cursor).unwrap(); +/// ``` +impl TryFrom> for Decoder> +where + T: AsRef<[u8]> + Send + Sync + 'static, +{ + type Error = DecoderError; - match symphonia::SymphoniaDecoder::new(mss, None) { - Err(e) => Err(e), - Ok(decoder) => Ok(Decoder(DecoderImpl::Symphonia(decoder))), - } - } - #[cfg(not(feature = "symphonia"))] - Err(DecoderError::UnrecognizedFormat) + fn try_from(data: std::io::Cursor) -> Result { + Self::new(data) } +} - /// Builds a new looped decoder. +impl Decoder { + /// Returns a builder for creating a new decoder with customizable settings. + /// + /// # Examples + /// ```no_run + /// use std::fs::File; + /// use rodio::Decoder; + /// + /// let file = File::open("audio.mp3").unwrap(); + /// let decoder = Decoder::builder() + /// .with_data(file) + /// .with_hint("mp3") + /// .with_gapless(true) + /// .build() + /// .unwrap(); + /// ``` + pub fn builder() -> DecoderBuilder { + DecoderBuilder::new() + } + + /// Builds a new decoder with default settings. /// /// Attempts to automatically detect the format of the source of data. - pub fn new_looped(data: R) -> Result, DecoderError> { - Self::new(data).map(LoopedDecoder::new) - } - - /// Builds a new decoder from wav data. - #[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), - Ok(decoder) => Ok(Decoder(DecoderImpl::Wav(decoder))), - } - } - - /// 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(all(feature = "flac", not(feature = "symphonia-flac")))] - pub fn new_flac(data: R) -> Result, DecoderError> { - match flac::FlacDecoder::new(data) { - Err(_) => Err(DecoderError::UnrecognizedFormat), - Ok(decoder) => Ok(Decoder(DecoderImpl::Flac(decoder))), - } - } - - /// 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(all(feature = "vorbis", not(feature = "symphonia-vorbis")))] - pub fn new_vorbis(data: R) -> Result, DecoderError> { - match vorbis::VorbisDecoder::new(data) { - Err(_) => Err(DecoderError::UnrecognizedFormat), - Ok(decoder) => Ok(Decoder(DecoderImpl::Vorbis(decoder))), - } - } - - /// Builds a new decoder from vorbis data. - #[cfg(feature = "symphonia-vorbis")] - pub fn new_vorbis(data: R) -> Result, DecoderError> { - Decoder::new_symphonia(data, "ogg") - } - - /// Builds a new decoder from mp3 data. - #[cfg(all(feature = "minimp3", 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))), - } + /// + /// # Errors + /// + /// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined + /// or is not supported. + pub fn new(data: R) -> Result { + DecoderBuilder::new().with_data(data).build() } - /// 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 looped decoder with default settings. + /// + /// Attempts to automatically detect the format of the source of data. + /// The decoder will restart from the beginning when it reaches the end. + /// + /// # Errors + /// + /// Returns `DecoderError::UnrecognizedFormat` if the audio format could not be determined + /// or is not supported. + pub fn new_looped(data: R) -> Result, DecoderError> { + DecoderBuilder::new().with_data(data).build_looped() } - /// Builds a new decoder from aac data. + /// Builds a new decoder with WAV format hint. + /// + /// This method provides a hint that the data is WAV format, which may help the decoder + /// identify the format more quickly. However, if WAV decoding fails, other formats + /// will still be attempted. + /// + /// # Errors + /// + /// Returns `DecoderError::UnrecognizedFormat` if no suitable decoder was found. + /// + /// # Examples + /// ```no_run + /// use rodio::Decoder; + /// use std::fs::File; + /// + /// let file = File::open("audio.wav").unwrap(); + /// let decoder = Decoder::new_wav(file).unwrap(); + /// ``` + #[cfg(any(feature = "wav", feature = "symphonia-wav"))] + pub fn new_wav(data: R) -> Result { + DecoderBuilder::new() + .with_data(data) + .with_hint("wav") + .build() + } + + /// Builds a new decoder with FLAC format hint. + /// + /// This method provides a hint that the data is FLAC format, which may help the decoder + /// identify the format more quickly. However, if FLAC decoding fails, other formats + /// will still be attempted. + /// + /// # Errors + /// + /// Returns `DecoderError::UnrecognizedFormat` if no suitable decoder was found. + /// + /// # Examples + /// ```no_run + /// use rodio::Decoder; + /// use std::fs::File; + /// + /// let file = File::open("audio.flac").unwrap(); + /// let decoder = Decoder::new_flac(file).unwrap(); + /// ``` + #[cfg(any(feature = "flac", feature = "symphonia-flac"))] + pub fn new_flac(data: R) -> Result { + DecoderBuilder::new() + .with_data(data) + .with_hint("flac") + .build() + } + + /// Builds a new decoder with Vorbis format hint. + /// + /// This method provides a hint that the data is Vorbis format, which may help the decoder + /// identify the format more quickly. However, if Vorbis decoding fails, other formats + /// will still be attempted. + /// + /// # Errors + /// + /// Returns `DecoderError::UnrecognizedFormat` if no suitable decoder was found. + /// + /// # Examples + /// ```no_run + /// use rodio::Decoder; + /// use std::fs::File; + /// + /// let file = File::open("audio.ogg").unwrap(); + /// let decoder = Decoder::new_vorbis(file).unwrap(); + /// ``` + #[cfg(any(feature = "vorbis", feature = "symphonia-vorbis"))] + pub fn new_vorbis(data: R) -> Result { + DecoderBuilder::new() + .with_data(data) + .with_hint("ogg") + .build() + } + + /// Builds a new decoder with MP3 format hint. + /// + /// This method provides a hint that the data is MP3 format, which may help the decoder + /// identify the format more quickly. However, if MP3 decoding fails, other formats + /// will still be attempted. + /// + /// # Errors + /// + /// Returns `DecoderError::UnrecognizedFormat` if no suitable decoder was found. + /// + /// # Examples + /// ```no_run + /// use rodio::Decoder; + /// use std::fs::File; + /// + /// let file = File::open("audio.mp3").unwrap(); + /// let decoder = Decoder::new_mp3(file).unwrap(); + /// ``` + #[cfg(any(feature = "minimp3", feature = "symphonia-mp3"))] + pub fn new_mp3(data: R) -> Result { + DecoderBuilder::new() + .with_data(data) + .with_hint("mp3") + .build() + } + + /// Builds a new decoder with AAC format hint. + /// + /// This method provides a hint that the data is AAC format, which may help the decoder + /// identify the format more quickly. However, if AAC decoding fails, other formats + /// will still be attempted. + /// + /// # Errors + /// + /// Returns `DecoderError::UnrecognizedFormat` if no suitable decoder was found. + /// + /// # Examples + /// ```no_run + /// use rodio::Decoder; + /// use std::fs::File; + /// + /// let file = File::open("audio.aac").unwrap(); + /// let decoder = Decoder::new_aac(file).unwrap(); + /// ``` #[cfg(feature = "symphonia-aac")] - pub fn new_aac(data: R) -> Result, DecoderError> { - Decoder::new_symphonia(data, "aac") + pub fn new_aac(data: R) -> Result { + DecoderBuilder::new() + .with_data(data) + .with_hint("aac") + .build() } - /// Builds a new decoder from mp4 data. + /// Builds a new decoder with MP4 container format hint. + /// + /// This method provides a hint that the data is in MP4 container format by setting + /// the MIME type to "audio/mp4". This may help the decoder identify the format + /// more quickly. However, if MP4 decoding fails, other formats will still be attempted. + /// + /// # Errors + /// + /// Returns `DecoderError::UnrecognizedFormat` if no suitable decoder was found. + /// + /// # Examples + /// ```no_run + /// use rodio::Decoder; + /// use std::fs::File; + /// + /// let file = File::open("audio.m4a").unwrap(); + /// let decoder = Decoder::new_mp4(file).unwrap(); + /// ``` #[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) => Ok(Decoder(DecoderImpl::Symphonia(decoder))), - } - } -} - -#[allow(missing_docs)] // Reason: will be removed, see: #612 -#[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!("{input} is not a valid mp4 extension")), - } - } -} - -impl fmt::Display for Mp4Type { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let text = match self { - Mp4Type::Mp4 => "mp4", - Mp4Type::M4a => "m4a", - Mp4Type::M4p => "m4p", - Mp4Type::M4b => "m4b", - Mp4Type::M4r => "m4r", - Mp4Type::M4v => "m4v", - Mp4Type::Mov => "mov", - }; - write!(f, "{text}") - } -} - -impl LoopedDecoder -where - R: Read + Seek, -{ - fn new(decoder: Decoder) -> LoopedDecoder { - Self(decoder.0) + pub fn new_mp4(data: R) -> Result { + DecoderBuilder::new() + .with_data(data) + .with_mime_type("audio/mp4") + .build() } } @@ -444,13 +596,20 @@ where { type Item = Sample; - #[inline] + /// Returns the next sample in the audio stream. + /// + /// When the end of the stream is reached, attempts to seek back to the start + /// and continue playing. If seeking fails, or if no decoder is available, + /// returns `None`. fn next(&mut self) -> Option { - if let Some(sample) = self.0.next() { - Some(sample) - } else { - let decoder = mem::replace(&mut self.0, DecoderImpl::None(Default::default())); - let (decoder, sample) = match decoder { + if let Some(inner) = &mut self.inner { + if let Some(sample) = inner.next() { + return Some(sample); + } + + // Take ownership of the decoder to reset it + let decoder = self.inner.take()?; + let (new_decoder, sample) = match decoder { #[cfg(all(feature = "wav", not(feature = "symphonia-wav")))] DecoderImpl::Wav(source) => { let mut reader = source.into_inner(); @@ -487,23 +646,39 @@ where (DecoderImpl::Mp3(source), sample) } #[cfg(feature = "symphonia")] - DecoderImpl::Symphonia(source) => { + DecoderImpl::Symphonia(source, PhantomData) => { let mut reader = source.into_inner(); reader.seek(SeekFrom::Start(0)).ok()?; - let mut source = symphonia::SymphoniaDecoder::new(reader, None).ok()?; + let mut source = + symphonia::SymphoniaDecoder::new(reader, &self.settings).ok()?; let sample = source.next(); - (DecoderImpl::Symphonia(source), sample) + (DecoderImpl::Symphonia(source, PhantomData), sample) } - none @ DecoderImpl::None(_) => (none, None), }; - self.0 = decoder; + self.inner = Some(new_decoder); sample + } else { + None } } + /// Returns the size hint for this iterator. + /// + /// The lower bound is: + /// - The minimum number of samples remaining in the current iteration if there is an active decoder + /// - 0 if there is no active decoder (inner is None) + /// + /// The upper bound is always `None` since the decoder loops indefinitely. + /// This differs from non-looped decoders which may provide a finite upper bound. + /// + /// Note that even with an active decoder, reaching the end of the stream may result + /// in the decoder becoming inactive if seeking back to the start fails. #[inline] fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() + ( + self.inner.as_ref().map_or(0, |inner| inner.size_hint().0), + None, + ) } } @@ -511,39 +686,76 @@ impl Source for LoopedDecoder where R: Read + Seek, { + /// Returns the current span length of the underlying decoder. + /// + /// Returns `None` if there is no active decoder. #[inline] fn current_span_len(&self) -> Option { - self.0.current_span_len() + self.inner.as_ref()?.current_span_len() } + /// Returns the number of channels in the audio stream. + /// + /// Returns the default channel count if there is no active decoder. #[inline] fn channels(&self) -> ChannelCount { - self.0.channels() + self.inner + .as_ref() + .map_or(ChannelCount::default(), |inner| inner.channels()) } + /// Returns the sample rate of the audio stream. + /// + /// Returns the default sample rate if there is no active decoder. #[inline] fn sample_rate(&self) -> SampleRate { - self.0.sample_rate() + self.inner + .as_ref() + .map_or(SampleRate::default(), |inner| inner.sample_rate()) } + /// Returns the total duration of this audio source. + /// + /// Always returns `None` for looped decoders since they have no fixed end point - + /// they will continue playing indefinitely by seeking back to the start when reaching + /// the end of the audio data. #[inline] fn total_duration(&self) -> Option { None } + /// Attempts to seek to a specific position in the audio stream. + /// + /// # Errors + /// + /// Returns `SeekError::NotSupported` if: + /// - There is no active decoder + /// - The underlying decoder does not support seeking + /// + /// May also return other `SeekError` variants if the underlying decoder's seek operation fails. + /// + /// # Note + /// + /// Even for looped playback, seeking past the end of the stream will not automatically + /// wrap around to the beginning - it will return an error just like a normal decoder. + /// Looping only occurs when reaching the end through normal playback. fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - self.0.try_seek(pos) + match &mut self.inner { + Some(inner) => inner.try_seek(pos), + None => Err(SeekError::Other(Box::new(DecoderError::IoError( + "Looped source ended when it failed to loop back".to_string(), + )))), + } } } -/// Error that can happen when creating a decoder. +/// Errors that can occur when creating a decoder. #[derive(Debug, Clone)] pub enum DecoderError { /// The format of the data has not been recognized. UnrecognizedFormat, /// An IO error occurred while reading, writing, or seeking the stream. - #[cfg(feature = "symphonia")] IoError(String), /// The stream contained malformed data and could not be decoded or demuxed. @@ -559,7 +771,7 @@ pub enum DecoderError { #[cfg(feature = "symphonia")] ResetRequired, - /// No streams were found by the decoder + /// No streams were found by the decoder. #[cfg(feature = "symphonia")] NoStreams, } diff --git a/src/decoder/read_seek_source.rs b/src/decoder/read_seek_source.rs index d9717023..d3a11252 100644 --- a/src/decoder/read_seek_source.rs +++ b/src/decoder/read_seek_source.rs @@ -2,39 +2,66 @@ use std::io::{Read, Result, Seek, SeekFrom}; use symphonia::core::io::MediaSource; +use super::Settings; + +/// A wrapper around a `Read + Seek` type that implements Symphonia's `MediaSource` trait. +/// +/// This type allows standard Rust I/O types to be used with Symphonia's media framework +/// by implementing the required `MediaSource` trait. pub struct ReadSeekSource { + /// The wrapped reader/seeker inner: T, + /// Optional length of the media source in bytes. + /// When known, this can help with seeking and duration calculations. + byte_len: Option, + /// Whether this media source reports as seekable. + is_seekable: bool, } impl ReadSeekSource { - /// Instantiates a new `ReadSeekSource` by taking ownership and wrapping the provided - /// `Read + Seek`er. + /// Creates a new `ReadSeekSource` by wrapping a reader/seeker. + /// + /// # Arguments + /// * `inner` - The reader/seeker to wrap + /// * `settings` - Decoder settings for configuring the source #[inline] - pub fn new(inner: T) -> Self { - ReadSeekSource { inner } + pub fn new(inner: T, settings: &Settings) -> Self { + ReadSeekSource { + inner, + byte_len: settings.byte_len, + is_seekable: settings.is_seekable, + } } } impl MediaSource for ReadSeekSource { + /// Returns whether this media source reports as seekable. #[inline] fn is_seekable(&self) -> bool { - true + self.is_seekable } + /// Returns the total length of the media source in bytes, if known. #[inline] fn byte_len(&self) -> Option { - None + self.byte_len } } impl Read for ReadSeekSource { #[inline] + /// Reads bytes from the underlying reader into the provided buffer. + /// + /// Delegates to the inner reader's implementation. fn read(&mut self, buf: &mut [u8]) -> Result { self.inner.read(buf) } } impl Seek for ReadSeekSource { + /// Seeks to a position in the underlying reader. + /// + /// Delegates to the inner reader's implementation. #[inline] fn seek(&mut self, pos: SeekFrom) -> Result { self.inner.seek(pos) diff --git a/src/decoder/symphonia.rs b/src/decoder/symphonia.rs index 42d0d834..539d033d 100644 --- a/src/decoder/symphonia.rs +++ b/src/decoder/symphonia.rs @@ -5,18 +5,20 @@ use symphonia::{ audio::{AudioBufferRef, SampleBuffer, SignalSpec}, codecs::{Decoder, DecoderOptions, CODEC_TYPE_NULL}, errors::Error, - formats::{FormatOptions, FormatReader, SeekedTo}, + formats::{FormatOptions, FormatReader, SeekMode, SeekTo, SeekedTo}, io::MediaSourceStream, meta::MetadataOptions, probe::Hint, - units::{self, Time}, + units, }, default::get_probe, }; -use super::DecoderError; -use crate::common::{ChannelCount, Sample, SampleRate}; -use crate::{source, Source}; +use super::{DecoderError, Settings}; +use crate::{ + common::{ChannelCount, Sample, SampleRate}, + source, Source, +}; // Decoder errors are not considered fatal. // The correct action is to just get a new packet and try again. @@ -27,17 +29,15 @@ pub(crate) struct SymphoniaDecoder { decoder: Box, current_span_offset: usize, format: Box, - total_duration: Option