Skip to content

Commit b7c504e

Browse files
authored
Merge pull request #831 from RustAudio/fix/span-fixes-clean
fix: correct Source trait semantics and span tracking bugs
2 parents 174ce9b + 423688c commit b7c504e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1750
-753
lines changed

CHANGELOG.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
### Added
1313

1414
- Added `Skippable::skipped` function to check if the inner source was skipped.
15+
- All sources now implement `ExactSizeIterator` when their inner source does.
16+
- All sources now implement `Iterator::size_hint()`.
17+
- `Chirp` now implements `try_seek`.
1518

1619
### Changed
1720

1821
- Breaking: `Done` now calls a callback instead of decrementing an `Arc<AtomicUsize>`.
1922
- Updated `cpal` to v0.18.
23+
- Clarified `Source::current_span_len()` documentation to specify it returns total span length.
24+
- Explicitly document the requirement for sources to return complete frames.
25+
- Ensured decoders to always return complete frames, as well as `TakeDuration` when expired.
26+
- Breaking: `Zero::new_samples()` now returns `Result<Self, ZeroError>` requiring a frame-aligned number of samples.
27+
- Improved queue, buffer, mixer and sample rate conversion performance.
2028

2129
### Fixed
2230

2331
- Fixed `Player::skip_one` not decreasing the player's length immediately.
32+
- Fixed `Source::current_span_len()` to consistently return total span length.
33+
- Fixed `Source::size_hint()` to consistently report actual bounds based on current sources.
34+
- Fixed `Pausable::size_hint()` to correctly account for paused samples.
35+
- Fixed `MixerSource` and `LinearRamp` to prevent overflow with very long playback.
36+
- Fixed `PeriodicAccess` to prevent overflow with very long periods.
37+
- Fixed `BltFilter` to work correctly with stereo and multi-channel audio.
38+
- Fixed `ChannelVolume` to work correclty with stereo and multi-channel audio.
39+
- Fixed `Brownian` and `Red` noise generators to reset after seeking.
40+
- Fixed sources to correctly handle sample rate and channel count changes at span boundaries.
41+
- Fixed sources to detect parameter updates after mid-span seeks.
2442

2543
## Version [0.22.2] (2026-02-22)
2644

2745
### Fixed
2846

29-
- Incorrectly set system default audio buffer size breaks playback. We no longer use the system default (introduced in 0.22 through cpal upgrade) and instead set a safe buffer duration.
47+
- Incorrectly set system default audio buffer size breaks playback. We no longer use the system default (introduced in 0.22 through cpal upgrade) and instead set a safe buffer duration.
3048
- Audio output fallback picked null device leading to no output.
3149
- Mixer did not actually add sources sometimes.
3250

@@ -38,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3856
## Version [0.22.1] (2026-02-22)
3957

4058
### Fixed
59+
4160
- docs.rs could not build the documentation.
4261

4362
## Version [0.22] (2026-02-22)
@@ -65,19 +84,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6584
- Added `SampleRateConverter::inner` to get underlying iterator by ref.
6685

6786
### Fixed
87+
6888
- docs.rs will now document all features, including those that are optional.
6989
- `Chirp::next` now returns `None` when the total duration has been reached, and will work
7090
correctly for a number of samples greater than 2^24.
7191
- `PeriodicAccess` is slightly more accurate for 44.1 kHz sample rate families.
7292
- Fixed audio distortion when queueing sources with different sample rates/channel counts or transitioning from empty queue.
7393
- Fixed `SamplesBuffer` to correctly report exhaustion and remaining samples.
7494
- Improved precision in `SkipDuration` to avoid off-by-a-few-samples errors.
75-
- Fixed channel misalignment in queue with non-power-of-2 channel counts (e.g., 6 channels) by ensuring frame-aligned span lengths.
76-
- Fixed channel misalignment when sources end before their promised span length by padding with silence to complete frames.
7795
- Fixed `Empty` source to properly report exhaustion.
7896
- Fixed `Zero::current_span_len` returning remaining samples instead of span length.
7997

8098
### Changed
99+
81100
- Breaking: _Sink_ terms are replaced with _Player_ and _Stream_ terms replaced
82101
with _Sink_. This is a simple rename, functionality is identical.
83102
- `OutputStream` is now `MixerDeviceSink` (in anticipation of future

UPGRADE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ changes and new features, see [CHANGELOG.md](CHANGELOG.md).
1010
- To retain old behavior replace the `Arc<AtomicUsize>` argument in `Done::new` with
1111
`move |_| { number.fetch_sub(1, std::sync::atomic::Ordering::Relaxed) }`.
1212
- `Done` has now two generics instead of one: `<I: Source, F: FnMut(&mut I)>`.
13+
- `Zero::new_samples()` now returns `Result<Zero, ZeroError>` instead of `Zero`.
14+
Previously the function accepted any `num_samples` value; passing one that is not a
15+
multiple of `channels` now returns an `Err` instead of producing a mis-aligned source.
1316

1417
# rodio 0.21.1 to 0.22
1518
- _Sink_ terms are replaced with _Player_ and _Stream_ terms replaced

src/decoder/flac.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::io::{Read, Seek, SeekFrom};
22
use std::mem;
33
use std::time::Duration;
44

5-
use crate::source::SeekError;
5+
use crate::source::{padding_samples_needed, SeekError};
66
use crate::Source;
77

88
use crate::common::{ChannelCount, Sample, SampleRate};
@@ -24,6 +24,8 @@ where
2424
sample_rate: SampleRate,
2525
channels: ChannelCount,
2626
total_duration: Option<Duration>,
27+
samples_in_current_frame: usize,
28+
silence_samples_remaining: usize,
2729
}
2830

2931
impl<R> FlacDecoder<R>
@@ -69,6 +71,8 @@ where
6971
)
7072
.expect("flac should never have zero channels"),
7173
total_duration,
74+
samples_in_current_frame: 0,
75+
silence_samples_remaining: 0,
7276
})
7377
}
7478

@@ -119,6 +123,12 @@ where
119123
#[inline]
120124
fn next(&mut self) -> Option<Self::Item> {
121125
loop {
126+
// If padding to complete a frame, return silence
127+
if self.silence_samples_remaining > 0 {
128+
self.silence_samples_remaining -= 1;
129+
return Some(Sample::EQUILIBRIUM);
130+
}
131+
122132
if self.current_block_off < self.current_block.len() {
123133
// Read from current block.
124134
let real_offset = (self.current_block_off % self.channels.get() as usize)
@@ -142,6 +152,8 @@ where
142152
(raw_val << (32 - bits)).to_sample()
143153
}
144154
};
155+
self.samples_in_current_frame =
156+
(self.samples_in_current_frame + 1) % self.channels.get() as usize;
145157
return Some(real_val);
146158
}
147159

@@ -153,7 +165,16 @@ where
153165
self.current_block_channel_len = (block.len() / block.channels()) as usize;
154166
self.current_block = block.into_buffer();
155167
}
156-
_ => return None,
168+
_ => {
169+
// Input exhausted - check if mid-frame
170+
self.silence_samples_remaining =
171+
padding_samples_needed(self.samples_in_current_frame, self.channels);
172+
if self.silence_samples_remaining > 0 {
173+
self.samples_in_current_frame = 0;
174+
continue; // Loop will inject silence
175+
}
176+
return None;
177+
}
157178
}
158179
}
159180
}

src/decoder/mp3.rs

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::num::NonZero;
33
use std::time::Duration;
44

55
use crate::common::{ChannelCount, Sample, SampleRate};
6-
use crate::source::SeekError;
6+
use crate::source::{padding_samples_needed, SeekError};
77
use crate::Source;
88

99
use dasp_sample::Sample as _;
@@ -21,6 +21,8 @@ where
2121
// what minimp3 calls frames rodio calls spans
2222
current_span: Frame,
2323
current_span_offset: usize,
24+
samples_in_current_frame: usize,
25+
silence_samples_remaining: usize,
2426
}
2527

2628
impl<R> Mp3Decoder<R>
@@ -43,6 +45,8 @@ where
4345
decoder,
4446
current_span,
4547
current_span_offset: 0,
48+
samples_in_current_frame: 0,
49+
silence_samples_remaining: 0,
4650
})
4751
}
4852

@@ -98,21 +102,40 @@ where
98102
type Item = Sample;
99103

100104
fn next(&mut self) -> Option<Self::Item> {
101-
let current_span_len = self.current_span_len()?;
102-
if self.current_span_offset == current_span_len {
103-
if let Ok(span) = self.decoder.next_frame() {
104-
// if let Ok(span) = self.decoder.decode_frame() {
105-
self.current_span = span;
106-
self.current_span_offset = 0;
107-
} else {
108-
return None;
105+
loop {
106+
// If padding to complete a frame, return silence
107+
if self.silence_samples_remaining > 0 {
108+
self.silence_samples_remaining -= 1;
109+
return Some(Sample::EQUILIBRIUM);
110+
}
111+
112+
let current_span_len = self.current_span_len()?;
113+
if self.current_span_offset == current_span_len {
114+
if let Ok(span) = self.decoder.next_frame() {
115+
self.current_span = span;
116+
self.current_span_offset = 0;
117+
} else {
118+
// Input exhausted - check if mid-frame
119+
let channels = self.channels();
120+
self.silence_samples_remaining =
121+
padding_samples_needed(self.samples_in_current_frame, channels);
122+
if self.silence_samples_remaining > 0 {
123+
self.samples_in_current_frame = 0;
124+
continue; // Loop will inject silence
125+
}
126+
return None;
127+
}
109128
}
110-
}
111129

112-
let v = self.current_span.data[self.current_span_offset];
113-
self.current_span_offset += 1;
130+
let v = self.current_span.data[self.current_span_offset];
131+
self.current_span_offset += 1;
114132

115-
Some(v.to_sample())
133+
let channels = self.channels();
134+
self.samples_in_current_frame =
135+
(self.samples_in_current_frame + 1) % channels.get() as usize;
136+
137+
return Some(v.to_sample());
138+
}
116139
}
117140
}
118141

src/decoder/symphonia.rs

Lines changed: 81 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ use symphonia::{
2020
use super::{DecoderError, Settings};
2121
use crate::{
2222
common::{assert_error_traits, ChannelCount, Sample, SampleRate},
23-
source, Source,
23+
source::{self, padding_samples_needed},
24+
Source,
2425
};
26+
use dasp_sample::Sample as _;
2527

2628
#[derive(Clone)]
2729
pub(crate) struct Registry(Arc<RwLock<CodecRegistry>>);
@@ -55,6 +57,8 @@ pub(crate) struct SymphoniaDecoder {
5557
spec: SignalSpec,
5658
seek_mode: SeekMode,
5759
selected_track_id: u32,
60+
samples_in_current_frame: usize,
61+
silence_samples_remaining: usize,
5862
}
5963

6064
impl SymphoniaDecoder {
@@ -176,6 +180,8 @@ impl SymphoniaDecoder {
176180
spec,
177181
seek_mode,
178182
selected_track_id: track_id,
183+
samples_in_current_frame: 0,
184+
silence_samples_remaining: 0,
179185
}))
180186
}
181187

@@ -328,44 +334,87 @@ impl Iterator for SymphoniaDecoder {
328334
type Item = Sample;
329335

330336
fn next(&mut self) -> Option<Self::Item> {
331-
if self.current_span_offset >= self.buffer.len() {
332-
let decoded = loop {
333-
let packet = self.format.next_packet().ok()?;
334-
335-
// If the packet does not belong to the selected track, skip over it
336-
if packet.track_id() != self.selected_track_id {
337-
continue;
338-
}
337+
loop {
338+
// If padding to complete a frame, return silence
339+
if self.silence_samples_remaining > 0 {
340+
self.silence_samples_remaining -= 1;
341+
return Some(Sample::EQUILIBRIUM);
342+
}
339343

340-
let decoded = match self.decoder.decode(&packet) {
341-
Ok(decoded) => decoded,
342-
Err(Error::DecodeError(_)) => {
343-
// Skip over packets that cannot be decoded. This ensures the iterator
344-
// continues processing subsequent packets instead of terminating due to
345-
// non-critical decode errors.
346-
continue;
344+
if self.current_span_offset >= self.buffer.len() {
345+
let decoded = loop {
346+
let packet = match self.format.next_packet() {
347+
Ok(packet) => {
348+
if packet.track_id() == self.selected_track_id {
349+
packet
350+
} else {
351+
continue;
352+
}
353+
}
354+
Err(_) => {
355+
// Input exhausted - check if mid-frame
356+
let channels = self.channels();
357+
self.silence_samples_remaining =
358+
padding_samples_needed(self.samples_in_current_frame, channels);
359+
if self.silence_samples_remaining > 0 {
360+
self.samples_in_current_frame = 0;
361+
break None;
362+
}
363+
return None;
364+
}
365+
};
366+
let decoded = match self.decoder.decode(&packet) {
367+
Ok(decoded) => decoded,
368+
Err(Error::DecodeError(_)) => {
369+
// Skip over packets that cannot be decoded. This ensures the iterator
370+
// continues processing subsequent packets instead of terminating due to
371+
// non-critical decode errors.
372+
continue;
373+
}
374+
Err(_) => {
375+
// Input exhausted - check if mid-frame
376+
let channels = self.channels();
377+
self.silence_samples_remaining =
378+
padding_samples_needed(self.samples_in_current_frame, channels);
379+
if self.silence_samples_remaining > 0 {
380+
self.samples_in_current_frame = 0;
381+
break None;
382+
}
383+
return None;
384+
}
385+
};
386+
387+
// Loop until we get a packet with audio frames. This is necessary because some
388+
// formats can have packets with only metadata, particularly when rewinding, in
389+
// which case the iterator would otherwise end with `None`.
390+
// Note: checking `decoded.frames()` is more reliable than `packet.dur()`, which
391+
// can resturn non-zero durations for packets without audio frames.
392+
if decoded.frames() > 0 {
393+
break Some(decoded);
347394
}
348-
Err(_) => return None,
349395
};
350396

351-
// Loop until we get a packet with audio frames. This is necessary because some
352-
// formats can have packets with only metadata, particularly when rewinding, in
353-
// which case the iterator would otherwise end with `None`.
354-
// Note: checking `decoded.frames()` is more reliable than `packet.dur()`, which
355-
// can resturn non-zero durations for packets without audio frames.
356-
if decoded.frames() > 0 {
357-
break decoded;
397+
match decoded {
398+
Some(decoded) => {
399+
decoded.spec().clone_into(&mut self.spec);
400+
self.buffer = SymphoniaDecoder::get_buffer(decoded, &self.spec);
401+
self.current_span_offset = 0;
402+
}
403+
None => {
404+
// Break out happened due to exhaustion, continue to emit padding
405+
continue;
406+
}
358407
}
359-
};
408+
}
360409

361-
decoded.spec().clone_into(&mut self.spec);
362-
self.buffer = SymphoniaDecoder::get_buffer(decoded, &self.spec);
363-
self.current_span_offset = 0;
364-
}
410+
let sample = *self.buffer.samples().get(self.current_span_offset)?;
411+
self.current_span_offset += 1;
365412

366-
let sample = *self.buffer.samples().get(self.current_span_offset)?;
367-
self.current_span_offset += 1;
413+
let channels = self.channels();
414+
self.samples_in_current_frame =
415+
(self.samples_in_current_frame + 1) % channels.get() as usize;
368416

369-
Some(sample)
417+
return Some(sample);
418+
}
370419
}
371420
}

0 commit comments

Comments
 (0)