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
Loading