Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

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

### Changed

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

### Fixed

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

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

### Fixed

- 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.
- 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.
- Audio output fallback picked null device leading to no output.
- Mixer did not actually add sources sometimes.

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

### Fixed

- docs.rs could not build the documentation.

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

### Fixed

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

### Changed

- Breaking: _Sink_ terms are replaced with _Player_ and _Stream_ terms replaced
with _Sink_. This is a simple rename, functionality is identical.
- `OutputStream` is now `MixerDeviceSink` (in anticipation of future
Expand Down
3 changes: 3 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ changes and new features, see [CHANGELOG.md](CHANGELOG.md).
- To retain old behavior replace the `Arc<AtomicUsize>` argument in `Done::new` with
`move |_| { number.fetch_sub(1, std::sync::atomic::Ordering::Relaxed) }`.
- `Done` has now two generics instead of one: `<I: Source, F: FnMut(&mut I)>`.
- `Zero::new_samples()` now returns `Result<Zero, ZeroError>` instead of `Zero`.
Previously the function accepted any `num_samples` value; passing one that is not a
multiple of `channels` now returns an `Err` instead of producing a mis-aligned source.

# rodio 0.21.1 to 0.22
- _Sink_ terms are replaced with _Player_ and _Stream_ terms replaced
Expand Down
25 changes: 23 additions & 2 deletions src/decoder/flac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::io::{Read, Seek, SeekFrom};
use std::mem;
use std::time::Duration;

use crate::source::SeekError;
use crate::source::{padding_samples_needed, SeekError};
use crate::Source;

use crate::common::{ChannelCount, Sample, SampleRate};
Expand All @@ -24,6 +24,8 @@ where
sample_rate: SampleRate,
channels: ChannelCount,
total_duration: Option<Duration>,
samples_in_current_frame: usize,
silence_samples_remaining: usize,
}

impl<R> FlacDecoder<R>
Expand Down Expand Up @@ -69,6 +71,8 @@ where
)
.expect("flac should never have zero channels"),
total_duration,
samples_in_current_frame: 0,
silence_samples_remaining: 0,
})
}

Expand Down Expand Up @@ -119,6 +123,12 @@ where
#[inline]
fn next(&mut self) -> Option<Self::Item> {
loop {
// If padding to complete a frame, return silence
if self.silence_samples_remaining > 0 {
self.silence_samples_remaining -= 1;
return Some(Sample::EQUILIBRIUM);
}

if self.current_block_off < self.current_block.len() {
// Read from current block.
let real_offset = (self.current_block_off % self.channels.get() as usize)
Expand All @@ -142,6 +152,8 @@ where
(raw_val << (32 - bits)).to_sample()
}
};
self.samples_in_current_frame =
(self.samples_in_current_frame + 1) % self.channels.get() as usize;
return Some(real_val);
}

Expand All @@ -153,7 +165,16 @@ where
self.current_block_channel_len = (block.len() / block.channels()) as usize;
self.current_block = block.into_buffer();
}
_ => return None,
_ => {
// Input exhausted - check if mid-frame
self.silence_samples_remaining =
padding_samples_needed(self.samples_in_current_frame, self.channels);
if self.silence_samples_remaining > 0 {
self.samples_in_current_frame = 0;
continue; // Loop will inject silence
}
return None;
}
}
}
}
Expand Down
49 changes: 36 additions & 13 deletions src/decoder/mp3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::num::NonZero;
use std::time::Duration;

use crate::common::{ChannelCount, Sample, SampleRate};
use crate::source::SeekError;
use crate::source::{padding_samples_needed, SeekError};
use crate::Source;

use dasp_sample::Sample as _;
Expand All @@ -21,6 +21,8 @@ where
// what minimp3 calls frames rodio calls spans
current_span: Frame,
current_span_offset: usize,
samples_in_current_frame: usize,
silence_samples_remaining: usize,
}

impl<R> Mp3Decoder<R>
Expand All @@ -43,6 +45,8 @@ where
decoder,
current_span,
current_span_offset: 0,
samples_in_current_frame: 0,
silence_samples_remaining: 0,
})
}

Expand Down Expand Up @@ -98,21 +102,40 @@ where
type Item = Sample;

fn next(&mut self) -> Option<Self::Item> {
let current_span_len = self.current_span_len()?;
if self.current_span_offset == current_span_len {
if let Ok(span) = self.decoder.next_frame() {
// if let Ok(span) = self.decoder.decode_frame() {
self.current_span = span;
self.current_span_offset = 0;
} else {
return None;
loop {
// If padding to complete a frame, return silence
if self.silence_samples_remaining > 0 {
self.silence_samples_remaining -= 1;
return Some(Sample::EQUILIBRIUM);
}

let current_span_len = self.current_span_len()?;
if self.current_span_offset == current_span_len {
if let Ok(span) = self.decoder.next_frame() {
self.current_span = span;
self.current_span_offset = 0;
} else {
// Input exhausted - check if mid-frame
let channels = self.channels();
self.silence_samples_remaining =
padding_samples_needed(self.samples_in_current_frame, channels);
if self.silence_samples_remaining > 0 {
self.samples_in_current_frame = 0;
continue; // Loop will inject silence
}
return None;
}
}
}

let v = self.current_span.data[self.current_span_offset];
self.current_span_offset += 1;
let v = self.current_span.data[self.current_span_offset];
self.current_span_offset += 1;

Some(v.to_sample())
let channels = self.channels();
self.samples_in_current_frame =
(self.samples_in_current_frame + 1) % channels.get() as usize;

return Some(v.to_sample());
}
}
}

Expand Down
113 changes: 81 additions & 32 deletions src/decoder/symphonia.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ use symphonia::{
use super::{DecoderError, Settings};
use crate::{
common::{assert_error_traits, ChannelCount, Sample, SampleRate},
source, Source,
source::{self, padding_samples_needed},
Source,
};
use dasp_sample::Sample as _;

#[derive(Clone)]
pub(crate) struct Registry(Arc<RwLock<CodecRegistry>>);
Expand Down Expand Up @@ -55,6 +57,8 @@ pub(crate) struct SymphoniaDecoder {
spec: SignalSpec,
seek_mode: SeekMode,
selected_track_id: u32,
samples_in_current_frame: usize,
silence_samples_remaining: usize,
}

impl SymphoniaDecoder {
Expand Down Expand Up @@ -176,6 +180,8 @@ impl SymphoniaDecoder {
spec,
seek_mode,
selected_track_id: track_id,
samples_in_current_frame: 0,
silence_samples_remaining: 0,
}))
}

Expand Down Expand Up @@ -328,44 +334,87 @@ impl Iterator for SymphoniaDecoder {
type Item = Sample;

fn next(&mut self) -> Option<Self::Item> {
if self.current_span_offset >= self.buffer.len() {
let decoded = loop {
let packet = self.format.next_packet().ok()?;

// If the packet does not belong to the selected track, skip over it
if packet.track_id() != self.selected_track_id {
continue;
}
loop {
// If padding to complete a frame, return silence
if self.silence_samples_remaining > 0 {
self.silence_samples_remaining -= 1;
return Some(Sample::EQUILIBRIUM);
}

let decoded = match self.decoder.decode(&packet) {
Ok(decoded) => decoded,
Err(Error::DecodeError(_)) => {
// Skip over packets that cannot be decoded. This ensures the iterator
// continues processing subsequent packets instead of terminating due to
// non-critical decode errors.
continue;
if self.current_span_offset >= self.buffer.len() {
let decoded = loop {
let packet = match self.format.next_packet() {
Ok(packet) => {
if packet.track_id() == self.selected_track_id {
packet
} else {
continue;
}
}
Err(_) => {
// Input exhausted - check if mid-frame
let channels = self.channels();
self.silence_samples_remaining =
padding_samples_needed(self.samples_in_current_frame, channels);
if self.silence_samples_remaining > 0 {
self.samples_in_current_frame = 0;
break None;
}
return None;
}
};
let decoded = match self.decoder.decode(&packet) {
Ok(decoded) => decoded,
Err(Error::DecodeError(_)) => {
// Skip over packets that cannot be decoded. This ensures the iterator
// continues processing subsequent packets instead of terminating due to
// non-critical decode errors.
continue;
}
Err(_) => {
// Input exhausted - check if mid-frame
let channels = self.channels();
self.silence_samples_remaining =
padding_samples_needed(self.samples_in_current_frame, channels);
if self.silence_samples_remaining > 0 {
self.samples_in_current_frame = 0;
break None;
}
return None;
}
};

// Loop until we get a packet with audio frames. This is necessary because some
// formats can have packets with only metadata, particularly when rewinding, in
// which case the iterator would otherwise end with `None`.
// Note: checking `decoded.frames()` is more reliable than `packet.dur()`, which
// can resturn non-zero durations for packets without audio frames.
if decoded.frames() > 0 {
break Some(decoded);
}
Err(_) => return None,
};

// Loop until we get a packet with audio frames. This is necessary because some
// formats can have packets with only metadata, particularly when rewinding, in
// which case the iterator would otherwise end with `None`.
// Note: checking `decoded.frames()` is more reliable than `packet.dur()`, which
// can resturn non-zero durations for packets without audio frames.
if decoded.frames() > 0 {
break decoded;
match decoded {
Some(decoded) => {
decoded.spec().clone_into(&mut self.spec);
self.buffer = SymphoniaDecoder::get_buffer(decoded, &self.spec);
self.current_span_offset = 0;
}
None => {
// Break out happened due to exhaustion, continue to emit padding
continue;
}
}
};
}

decoded.spec().clone_into(&mut self.spec);
self.buffer = SymphoniaDecoder::get_buffer(decoded, &self.spec);
self.current_span_offset = 0;
}
let sample = *self.buffer.samples().get(self.current_span_offset)?;
self.current_span_offset += 1;

let sample = *self.buffer.samples().get(self.current_span_offset)?;
self.current_span_offset += 1;
let channels = self.channels();
self.samples_in_current_frame =
(self.samples_in_current_frame + 1) % channels.get() as usize;

Some(sample)
return Some(sample);
}
}
}
Loading
Loading