Skip to content

Commit e01f1a2

Browse files
committed
refactor: move LoopedDecoder to its own module and refactor construction
1 parent 4f5dd27 commit e01f1a2

File tree

3 files changed

+267
-233
lines changed

3 files changed

+267
-233
lines changed

src/decoder/builder.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -845,10 +845,6 @@ impl<R: Read + Seek + Send + Sync + 'static> DecoderBuilder<R> {
845845
/// on each loop iteration, improving performance for repeated playback.
846846
pub fn build_looped(self) -> Result<LoopedDecoder<R>, DecoderError> {
847847
let (decoder, settings) = self.build_impl()?;
848-
Ok(LoopedDecoder {
849-
inner: Some(decoder),
850-
settings,
851-
cached_duration: None,
852-
})
848+
Ok(LoopedDecoder::new(decoder, settings))
853849
}
854850
}

src/decoder/looped.rs

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
use std::{
2+
io::{Read, Seek},
3+
marker::PhantomData,
4+
sync::Arc,
5+
time::Duration,
6+
};
7+
8+
use crate::{
9+
common::{ChannelCount, SampleRate},
10+
math::nz,
11+
source::{SeekError, Source},
12+
BitDepth, Sample,
13+
};
14+
15+
use super::{builder::Settings, DecoderError, DecoderImpl};
16+
17+
#[cfg(feature = "claxon")]
18+
use super::flac;
19+
#[cfg(feature = "minimp3")]
20+
use super::mp3;
21+
#[cfg(feature = "symphonia")]
22+
use super::symphonia;
23+
#[cfg(feature = "lewton")]
24+
use super::vorbis;
25+
#[cfg(feature = "hound")]
26+
use super::wav;
27+
28+
/// Source of audio samples from decoding a file that never ends.
29+
/// When the end of the file is reached, the decoder starts again from the beginning.
30+
///
31+
/// A `LoopedDecoder` will attempt to seek back to the start of the stream when it reaches
32+
/// the end. If seeking fails for any reason (like IO errors), iteration will stop.
33+
///
34+
/// For seekable sources with gapless playback enabled, this uses `try_seek(Duration::ZERO)`
35+
/// which is fast. For non-seekable sources or when gapless is disabled, it recreates the
36+
/// decoder but caches metadata from the first iteration to avoid expensive file scanning
37+
/// on subsequent loops.
38+
///
39+
/// # Examples
40+
///
41+
/// ```no_run
42+
/// use std::fs::File;
43+
/// use rodio::Decoder;
44+
///
45+
/// let file = File::open("audio.mp3").unwrap();
46+
/// let looped_decoder = Decoder::new_looped(file).unwrap();
47+
/// ```
48+
#[allow(dead_code)]
49+
pub struct LoopedDecoder<R: Read + Seek> {
50+
/// The underlying decoder implementation.
51+
pub(super) inner: Option<DecoderImpl<R>>,
52+
/// Configuration settings for the decoder.
53+
pub(super) settings: Settings,
54+
/// Used to avoid expensive file scanning on subsequent loops.
55+
cached_duration: Option<Duration>,
56+
}
57+
58+
impl<R> LoopedDecoder<R>
59+
where
60+
R: Read + Seek,
61+
{
62+
/// Create a new `LoopedDecoder` with the given decoder and settings.
63+
pub(super) fn new(decoder: DecoderImpl<R>, settings: Settings) -> Self {
64+
Self {
65+
inner: Some(decoder),
66+
settings,
67+
cached_duration: None,
68+
}
69+
}
70+
71+
/// Recreate decoder with cached metadata to avoid expensive file scanning.
72+
fn recreate_decoder_with_cache(
73+
&mut self,
74+
decoder: DecoderImpl<R>,
75+
) -> Option<(DecoderImpl<R>, Option<Sample>)> {
76+
// Create settings with cached metadata for fast recreation.
77+
// Note: total_duration is important even though LoopedDecoder::total_duration() returns
78+
// None, because the individual decoder's total_duration() is used for seek saturation
79+
// (clamping seeks beyond the end to the end position).
80+
let mut fast_settings = self.settings.clone();
81+
fast_settings.total_duration = self.cached_duration;
82+
83+
let (new_decoder, sample) = match decoder {
84+
#[cfg(feature = "hound")]
85+
DecoderImpl::Wav(source) => {
86+
let mut reader = source.into_inner();
87+
reader.rewind().ok()?;
88+
let mut source = wav::WavDecoder::new_with_settings(reader, &fast_settings).ok()?;
89+
let sample = source.next();
90+
(DecoderImpl::Wav(source), sample)
91+
}
92+
#[cfg(feature = "lewton")]
93+
DecoderImpl::Vorbis(source) => {
94+
let mut reader = source.into_inner().into_inner().into_inner();
95+
reader.rewind().ok()?;
96+
let mut source =
97+
vorbis::VorbisDecoder::new_with_settings(reader, &fast_settings).ok()?;
98+
let sample = source.next();
99+
(DecoderImpl::Vorbis(source), sample)
100+
}
101+
#[cfg(feature = "claxon")]
102+
DecoderImpl::Flac(source) => {
103+
let mut reader = source.into_inner();
104+
reader.rewind().ok()?;
105+
let mut source =
106+
flac::FlacDecoder::new_with_settings(reader, &fast_settings).ok()?;
107+
let sample = source.next();
108+
(DecoderImpl::Flac(source), sample)
109+
}
110+
#[cfg(feature = "minimp3")]
111+
DecoderImpl::Mp3(source) => {
112+
let mut reader = source.into_inner();
113+
reader.rewind().ok()?;
114+
let mut source = mp3::Mp3Decoder::new_with_settings(reader, &fast_settings).ok()?;
115+
let sample = source.next();
116+
(DecoderImpl::Mp3(source), sample)
117+
}
118+
#[cfg(feature = "symphonia")]
119+
DecoderImpl::Symphonia(source, PhantomData) => {
120+
let mut reader = source.into_inner();
121+
reader.rewind().ok()?;
122+
let mut source =
123+
symphonia::SymphoniaDecoder::new_with_settings(reader, &fast_settings).ok()?;
124+
let sample = source.next();
125+
(DecoderImpl::Symphonia(source, PhantomData), sample)
126+
}
127+
DecoderImpl::None(_, _) => return None,
128+
};
129+
Some((new_decoder, sample))
130+
}
131+
}
132+
133+
impl<R> Iterator for LoopedDecoder<R>
134+
where
135+
R: Read + Seek,
136+
{
137+
type Item = Sample;
138+
139+
/// Returns the next sample in the audio stream.
140+
///
141+
/// When the end of the stream is reached, attempts to seek back to the start and continue
142+
/// playing. For seekable sources with gapless playback, this uses fast seeking. For
143+
/// non-seekable sources or when gapless is disabled, recreates the decoder using cached
144+
/// metadata to avoid expensive file scanning.
145+
fn next(&mut self) -> Option<Self::Item> {
146+
if let Some(inner) = &mut self.inner {
147+
if let Some(sample) = inner.next() {
148+
return Some(sample);
149+
}
150+
151+
// Cache duration from current decoder before resetting (first time only)
152+
if self.cached_duration.is_none() {
153+
self.cached_duration = inner.total_duration();
154+
}
155+
156+
// Try seeking first for seekable sources - this is fast and gapless
157+
// Only use fast seeking when gapless=true, otherwise recreate normally
158+
if self.settings.gapless
159+
&& self.settings.is_seekable
160+
&& inner.try_seek(Duration::ZERO).is_ok()
161+
{
162+
return inner.next();
163+
}
164+
165+
// Fall back to recreation with cached metadata to avoid expensive scanning
166+
let decoder = self.inner.take()?;
167+
let (new_decoder, sample) = self.recreate_decoder_with_cache(decoder)?;
168+
self.inner = Some(new_decoder);
169+
sample
170+
} else {
171+
None
172+
}
173+
}
174+
175+
/// Returns the size hint for this iterator.
176+
///
177+
/// The lower bound is:
178+
/// - The minimum number of samples remaining in the current iteration if there is an active
179+
/// decoder
180+
/// - 0 if there is no active decoder (inner is None)
181+
///
182+
/// The upper bound is always `None` since the decoder loops indefinitely.
183+
///
184+
/// Note that even with an active decoder, reaching the end of the stream may result in the
185+
/// decoder becoming inactive if seeking back to the start fails.
186+
#[inline]
187+
fn size_hint(&self) -> (usize, Option<usize>) {
188+
(
189+
self.inner.as_ref().map_or(0, |inner| inner.size_hint().0),
190+
None,
191+
)
192+
}
193+
}
194+
195+
impl<R> Source for LoopedDecoder<R>
196+
where
197+
R: Read + Seek,
198+
{
199+
/// Returns the current span length of the underlying decoder.
200+
///
201+
/// Returns `None` if there is no active decoder.
202+
#[inline]
203+
fn current_span_len(&self) -> Option<usize> {
204+
self.inner.as_ref()?.current_span_len()
205+
}
206+
207+
/// Returns the number of channels in the audio stream.
208+
///
209+
/// Returns the default channel count if there is no active decoder.
210+
#[inline]
211+
fn channels(&self) -> ChannelCount {
212+
self.inner.as_ref().map_or(nz!(1), |inner| inner.channels())
213+
}
214+
215+
/// Returns the sample rate of the audio stream.
216+
///
217+
/// Returns the default sample rate if there is no active decoder.
218+
#[inline]
219+
fn sample_rate(&self) -> SampleRate {
220+
self.inner
221+
.as_ref()
222+
.map_or(nz!(44100), |inner| inner.sample_rate())
223+
}
224+
225+
/// Returns the total duration of this audio source.
226+
///
227+
/// Always returns `None` for looped decoders since they have no fixed end point -
228+
/// they will continue playing indefinitely by seeking back to the start when reaching
229+
/// the end of the audio data.
230+
#[inline]
231+
fn total_duration(&self) -> Option<Duration> {
232+
None
233+
}
234+
235+
/// Returns the bits per sample of the underlying decoder, if available.
236+
#[inline]
237+
fn bits_per_sample(&self) -> Option<BitDepth> {
238+
self.inner.as_ref()?.bits_per_sample()
239+
}
240+
241+
/// Attempts to seek to a specific position in the audio stream.
242+
///
243+
/// # Errors
244+
///
245+
/// Returns `SeekError::NotSupported` if:
246+
/// - There is no active decoder
247+
/// - The underlying decoder does not support seeking
248+
///
249+
/// May also return other `SeekError` variants if the underlying decoder's seek operation fails.
250+
///
251+
/// # Note
252+
///
253+
/// Even for looped playback, seeking past the end of the stream will not automatically
254+
/// wrap around to the beginning - it will return an error just like a normal decoder.
255+
/// Looping only occurs when reaching the end through normal playback.
256+
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
257+
match &mut self.inner {
258+
Some(inner) => inner.try_seek(pos),
259+
None => Err(SeekError::Other(Arc::new(DecoderError::IoError(
260+
"Looped source ended when it failed to loop back".to_string(),
261+
)))),
262+
}
263+
}
264+
}

0 commit comments

Comments
 (0)