Skip to content
Open
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
15 changes: 11 additions & 4 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 @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added `64bit` feature to opt-in to 64-bit sample precision (`f64`).

### 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.
Expand All @@ -41,9 +42,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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 `Limit`, `TakeDuration` and `TrackPosition` to handle mid-span seeks.
- Fixed `MixerSource` to prevent overflow with very long playback.
- Fixed `PeriodicAccess` to prevent overflow with very long periods.

### 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,7 +67,7 @@ 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.
- Clarified `Source::current_span_len()` documentation to specify it returns total span length.
- Improved queue, mixer and sample rate conversion performance.

## Version [0.21.1] (2025-07-14)
Expand Down
14 changes: 14 additions & 0 deletions src/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,20 @@ pub(crate) fn duration_to_float(duration: Duration) -> Float {
}
}

/// Convert Float to Duration with appropriate precision for the Sample type.
#[inline]
#[must_use]
pub(crate) fn duration_from_secs(secs: Float) -> Duration {
#[cfg(not(feature = "64bit"))]
{
Duration::from_secs_f32(secs)
}
#[cfg(feature = "64bit")]
{
Duration::from_secs_f64(secs)
}
}

/// Utility macro for getting a `NonZero` from a literal. Especially
/// useful for passing in `ChannelCount` and `Samplerate`.
/// Equivalent to: `const { core::num::NonZero::new($n).unwrap() }`
Expand Down
52 changes: 41 additions & 11 deletions src/mixer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ pub fn mixer(channels: ChannelCount, sample_rate: SampleRate) -> (Mixer, MixerSo
}));

let output = MixerSource {
current_sources: Vec::with_capacity(16),
current_sources: Vec::new(),
input: input.clone(),
sample_count: 0,
still_pending: vec![],
current_channel: 0,
still_pending: Vec::new(),
pending_rx: rx,
};

Expand Down Expand Up @@ -74,8 +74,8 @@ pub struct MixerSource {
// The pending sounds.
input: Mixer,

// The number of samples produced so far.
sample_count: usize,
// Current channel position within the frame.
current_channel: u16,

// A temporary vec used in start_pending_sources.
still_pending: Vec<Box<dyn Source + Send>>,
Expand Down Expand Up @@ -120,10 +120,14 @@ impl Iterator for MixerSource {
fn next(&mut self) -> Option<Self::Item> {
self.start_pending_sources();

self.sample_count += 1;

let sum = self.sum_current_sources();

// Advance frame position (wraps at channel count, never overflows)
self.current_channel += 1;
if self.current_channel >= self.input.0.channels.get() {
self.current_channel = 0;
}

if self.current_sources.is_empty() {
None
} else {
Expand All @@ -133,7 +137,33 @@ impl Iterator for MixerSource {

#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
(0, None)
if self.current_sources.is_empty() {
return (0, Some(0));
}

// The mixer continues as long as ANY source is playing, so bounds are
// determined by the longest source, not the shortest.
let mut min = 0;
let mut max: Option<usize> = Some(0);

for source in &self.current_sources {
let (source_min, source_max) = source.size_hint();
// Lower bound: guaranteed to produce at least until longest source's lower bound
min = min.max(source_min);

match (max, source_max) {
(Some(current_max), Some(source_max_val)) => {
// Upper bound: might produce up to longest source's upper bound
max = Some(current_max.max(source_max_val));
}
_ => {
// If any source is unbounded, the mixer is unbounded
max = None;
}
}
}

(min, max)
}
}

Expand All @@ -144,9 +174,9 @@ impl MixerSource {
// sound will play on the wrong channels, e.g. left / right will be reversed.
fn start_pending_sources(&mut self) {
while let Ok(source) = self.pending_rx.try_recv() {
let in_step = self
.sample_count
.is_multiple_of(source.channels().get() as usize);
// Only start sources at frame boundaries (when current_channel == 0)
// to ensure correct channel alignment
let in_step = self.current_channel == 0;

if in_step {
self.current_sources.push(source);
Expand Down
25 changes: 2 additions & 23 deletions src/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,37 +135,16 @@ fn threshold(channels: ChannelCount) -> usize {
impl Source for SourcesQueueOutput {
#[inline]
fn current_span_len(&self) -> Option<usize> {
// This function is non-trivial because the boundary between two sounds in the queue should
// be a span boundary as well.
//
// The current sound is free to return `None` for `current_span_len()`, in which case
// we *should* return the number of samples remaining the current sound.
// This can be estimated with `size_hint()`.
//
// If the `size_hint` is `None` as well, we are in the worst case scenario. To handle this
// situation we force a span to have a maximum number of samples indicate by this
// constant.

// Try the current `current_span_len`.
if !self.current.is_exhausted() {
return self.current.current_span_len();
} else if self.input.keep_alive_if_empty.load(Ordering::Acquire)
&& self.input.next_sounds.lock().unwrap().is_empty()
{
// The next source will be a filler silence which will have a frame-aligned length
// Return what that Zero's current_span_len() will be: Some(threshold(channels)).
return Some(threshold(self.current.channels()));
}

// Try the size hint.
let (lower_bound, _) = self.current.size_hint();
// The iterator default implementation just returns 0.
// That's a problematic value, so skip it.
if lower_bound > 0 {
return Some(lower_bound);
}

// Otherwise we use a frame-aligned threshold value.
Some(threshold(self.current.channels()))
None
}

#[inline]
Expand Down
23 changes: 19 additions & 4 deletions src/source/blt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,10 @@ where

#[inline]
fn next(&mut self) -> Option<Sample> {
let last_in_span = self.input.current_span_len() == Some(1);
let current_sample_rate = self.input.sample_rate();

if self.applier.is_none() {
self.applier = Some(self.formula.to_applier(self.input.sample_rate().get()));
self.applier = Some(self.formula.to_applier(current_sample_rate.get()));
}

let sample = self.input.next()?;
Expand All @@ -134,7 +134,14 @@ where
self.y_n1 = result;
self.x_n1 = sample;

if last_in_span {
// Check if sample rate changed after getting the next sample.
// Only check when span is finite and not exhausted.
let sample_rate_changed = self
.input
.current_span_len()
.is_some_and(|len| len > 0 && current_sample_rate != self.input.sample_rate());

if sample_rate_changed {
self.applier = None;
}

Expand Down Expand Up @@ -175,7 +182,15 @@ where

#[inline]
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
self.input.try_seek(pos)
self.input.try_seek(pos)?;

// Reset filter state to avoid artifacts from previous position
self.x_n1 = 0.0;
self.x_n2 = 0.0;
self.y_n1 = 0.0;
self.y_n2 = 0.0;

Ok(())
}
}

Expand Down
20 changes: 11 additions & 9 deletions src/source/buffered.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::cmp;
use std::mem;
use std::sync::{Arc, Mutex};
use std::time::Duration;
Expand Down Expand Up @@ -59,7 +58,7 @@ struct SpanData<I>
where
I: Source,
{
data: Vec<I::Item>,
data: Box<[I::Item]>,
channels: ChannelCount,
rate: SampleRate,
next: Mutex<Arc<Span<I>>>,
Expand Down Expand Up @@ -107,10 +106,12 @@ where

let channels = input.channels();
let rate = input.sample_rate();
let data: Vec<I::Item> = input
let max_samples = span_len.unwrap_or(32768);
let data: Box<[I::Item]> = input
.by_ref()
.take(cmp::min(span_len.unwrap_or(32768), 32768))
.collect();
.take(max_samples)
.collect::<Vec<_>>()
.into_boxed_slice();

if data.is_empty() {
return Arc::new(Span::End);
Expand Down Expand Up @@ -204,11 +205,12 @@ where
{
#[inline]
fn current_span_len(&self) -> Option<usize> {
match &*self.current_span {
Span::Data(SpanData { data, .. }) => Some(data.len() - self.position_in_span),
Span::End => Some(0),
let len = match &*self.current_span {
Span::Data(SpanData { data, .. }) => data.len(),
Span::End => 0,
Span::Input(_) => unreachable!(),
}
};
Some(len)
}

#[inline]
Expand Down
21 changes: 10 additions & 11 deletions src/source/chirp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,6 @@ impl Chirp {
elapsed_samples: 0,
}
}

#[allow(dead_code)]
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
let mut target = (pos.as_secs_f64() * self.sample_rate.get() as f64) as u64;
if target >= self.total_samples {
target = self.total_samples;
}

self.elapsed_samples = target;
Ok(())
}
}

impl Iterator for Chirp {
Expand Down Expand Up @@ -101,4 +90,14 @@ impl Source for Chirp {
let secs = self.total_samples as f64 / self.sample_rate.get() as f64;
Some(Duration::from_secs_f64(secs))
}

fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
let mut target = (pos.as_secs_f64() * self.sample_rate.get() as f64) as u64;
if target >= self.total_samples {
target = self.total_samples;
}

self.elapsed_samples = target;
Ok(())
}
}
2 changes: 2 additions & 0 deletions src/source/delay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ where
}
}

impl<I> ExactSizeIterator for Delay<I> where I: Iterator + Source + ExactSizeIterator {}

impl<I> Source for Delay<I>
where
I: Iterator + Source,
Expand Down
35 changes: 19 additions & 16 deletions src/source/dither.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ pub struct Dither<I> {
input: I,
noise: NoiseGenerator,
current_channel: usize,
remaining_in_span: Option<usize>,
last_sample_rate: SampleRate,
last_channels: ChannelCount,
lsb_amplitude: Float,
}

Expand All @@ -179,13 +180,13 @@ where

let sample_rate = input.sample_rate();
let channels = input.channels();
let active_span_len = input.current_span_len();

Self {
input,
noise: NoiseGenerator::new(algorithm, sample_rate, channels),
current_channel: 0,
remaining_in_span: active_span_len,
last_sample_rate: sample_rate,
last_channels: channels,
lsb_amplitude,
}
}
Expand Down Expand Up @@ -213,23 +214,25 @@ where

#[inline]
fn next(&mut self) -> Option<Self::Item> {
if let Some(ref mut remaining) = self.remaining_in_span {
*remaining = remaining.saturating_sub(1);
}

// Consume next input sample *after* decrementing span position and *before* checking for
// span boundary crossing. This ensures that the source has its parameters updated
// correctly before we generate noise for the next sample.
let input_sample = self.input.next()?;
let num_channels = self.input.channels();

if self.remaining_in_span == Some(0) {
self.noise
.update_parameters(self.input.sample_rate(), num_channels);
self.current_channel = 0;
self.remaining_in_span = self.input.current_span_len();
if self.input.current_span_len().is_some_and(|len| len > 0) {
let current_sample_rate = self.input.sample_rate();
let current_channels = self.input.channels();
let parameters_changed = current_sample_rate != self.last_sample_rate
|| current_channels != self.last_channels;

if parameters_changed {
self.noise
.update_parameters(current_sample_rate, current_channels);
self.current_channel = 0;
self.last_sample_rate = current_sample_rate;
self.last_channels = current_channels;
}
}

let num_channels = self.input.channels();

let noise_sample = self
.noise
.next(self.current_channel)
Expand Down
Loading