Skip to content
Open
27 changes: 20 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `Chirp` and `Empty` now implement `Iterator::size_hint` and `ExactSizeIterator`.
- `SamplesBuffer` now implements `ExactSizeIterator`.
- All sources now implement `Iterator::size_hint()`.
- All sources now implement `ExactSizeIterator` when their inner source does.
- `Zero` now implements `try_seek`, `total_duration` and `Copy`.
- Added `Source::is_exhausted()` helper method to check if a source has no more samples.
- Added `Red` noise generator that is more practical than `Brownian` noise.
Expand All @@ -29,21 +29,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `DitherAlgorithm` enum for algorithm selection
- `Source::dither()` function for applying dithering
- Added `64bit` feature to opt-in to 64-bit sample precision (`f64`).
- `Chirp` now implements `try_seek`.

### 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.
- 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.

### 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 All @@ -60,8 +70,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `Gaussian` noise generator has standard deviation of 0.6 for perceptual equivalence.
- `Velvet` noise generator takes density in Hz as `usize` instead of `f32`.
- Upgraded `cpal` to v0.17.
- Clarified `Source::current_span_len()` contract documentation.
- Improved queue, mixer and sample rate conversion performance.
- 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.
- `Zero::new_samples()` now panics when it is not a multiple of the channel count.
- Improved queue, buffer, mixer and sample rate conversion performance.

## Version [0.21.1] (2025-07-14)

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
103 changes: 76 additions & 27 deletions src/decoder/symphonia.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,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 _;

pub(crate) struct SymphoniaDecoder {
decoder: Box<dyn Decoder>,
Expand All @@ -28,6 +30,8 @@ pub(crate) struct SymphoniaDecoder {
buffer: SampleBuffer<Sample>,
spec: SignalSpec,
seek_mode: SeekMode,
samples_in_current_frame: usize,
silence_samples_remaining: usize,
}

impl SymphoniaDecoder {
Expand Down Expand Up @@ -145,6 +149,8 @@ impl SymphoniaDecoder {
buffer,
spec,
seek_mode,
samples_in_current_frame: 0,
silence_samples_remaining: 0,
}))
}

Expand Down Expand Up @@ -297,38 +303,81 @@ 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()?;
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;
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_span_offset >= self.buffer.len() {
let decoded = loop {
let packet = match self.format.next_packet() {
Ok(packet) => packet,
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