diff --git a/CHANGELOG.md b/CHANGELOG.md index c3e12f9d..9fa920ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. @@ -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. @@ -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 @@ -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) diff --git a/src/math.rs b/src/math.rs index fffe1de1..dbc56ec9 100644 --- a/src/math.rs +++ b/src/math.rs @@ -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() }` diff --git a/src/mixer.rs b/src/mixer.rs index 1e5e5fea..b231f99f 100644 --- a/src/mixer.rs +++ b/src/mixer.rs @@ -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, }; @@ -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>, @@ -120,10 +120,14 @@ impl Iterator for MixerSource { fn next(&mut self) -> Option { 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 { @@ -133,7 +137,33 @@ impl Iterator for MixerSource { #[inline] fn size_hint(&self) -> (usize, Option) { - (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 = 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) } } @@ -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); diff --git a/src/queue.rs b/src/queue.rs index 182e8f32..274f907f 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -5,7 +5,9 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; -use crate::source::{Empty, SeekError, Source, Zero}; +use dasp_sample::Sample as _; + +use crate::source::{Empty, SeekError, Source}; use crate::Sample; use crate::common::{ChannelCount, SampleRate}; @@ -36,7 +38,7 @@ pub fn queue(keep_alive_if_empty: bool) -> (Arc, SourcesQueue signal_after_end: None, input: input.clone(), samples_consumed_in_span: 0, - padding_samples_remaining: 0, + silence_samples_remaining: 0, }; (input, output) @@ -72,7 +74,8 @@ impl SourcesQueueInput { /// /// The `Receiver` will be signalled when the sound has finished playing. /// - /// Enable the feature flag `crossbeam-channel` in rodio to use a `crossbeam_channel::Receiver` instead. + /// Enable the feature flag `crossbeam-channel` in rodio to use a `crossbeam_channel::Receiver` + /// instead. #[inline] pub fn append_with_signal(&self, source: T) -> Receiver<()> where @@ -94,6 +97,11 @@ impl SourcesQueueInput { .store(keep_alive_if_empty, Ordering::Release); } + /// Returns whether the queue stays alive if there's no more sound to play. + pub fn keep_alive_if_empty(&self) -> bool { + self.keep_alive_if_empty.load(Ordering::Acquire) + } + /// Removes all the sounds from the queue. Returns the number of sounds cleared. pub fn clear(&self) -> usize { let mut sounds = self.next_sounds.lock().unwrap(); @@ -102,6 +110,7 @@ impl SourcesQueueInput { len } } + /// The output of the queue. Implements `Source`. pub struct SourcesQueueOutput { // The current iterator that produces samples. @@ -116,93 +125,74 @@ pub struct SourcesQueueOutput { // Track samples consumed in the current span to detect mid-span endings. samples_consumed_in_span: usize, - // When a source ends mid-frame, this counts how many silence samples to inject - // to complete the frame before transitioning to the next source. - padding_samples_remaining: usize, -} - -/// Returns a threshold span length that ensures frame alignment. -/// -/// Spans must end on frame boundaries (multiples of channel count) to prevent -/// channel misalignment. Returns ~512 samples rounded to the nearest frame. -#[inline] -fn threshold(channels: ChannelCount) -> usize { - const BASE_SAMPLES: usize = 512; - let ch = channels.get() as usize; - BASE_SAMPLES.div_ceil(ch) * ch + // This counts how many silence samples to inject when a source ends. + silence_samples_remaining: usize, } impl Source for SourcesQueueOutput { #[inline] fn current_span_len(&self) -> Option { - // 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 Some(threshold(self.current.channels())); - } + let len = match self.current.current_span_len() { + Some(len) if len == 0 && self.silence_samples_remaining > 0 => { + // - Current source ended mid-frame, and we're injecting silence to frame-align it. + self.silence_samples_remaining + } + Some(len) if len > 0 || !self.input.keep_alive_if_empty() => { + // - Current source is not exhausted, and is reporting some span length, or + // - Current source is exhausted, and won't output silence after it: end of queue. + len + } + _ => { + // - Current source is not exhausted, and is reporting no span length, or + // - Current source is exhausted, and will output silence after it. + self.current.channels().get() as usize + } + }; - // 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); + // Special case: if the current source is `Empty` and there are queued sounds after it. + if len == 0 + && self + .current + .total_duration() + .is_some_and(|duration| duration.is_zero()) + { + if let Some((next, _)) = self.input.next_sounds.lock().unwrap().front() { + return next + .current_span_len() + .or_else(|| Some(next.channels().get() as usize)); + } } - // Otherwise we use a frame-aligned threshold value. - Some(threshold(self.current.channels())) + // A queue must never return None: that could cause downstream sources to assume sample + // rate or channel count would never change from one queue item to the next. + Some(len) } #[inline] fn channels(&self) -> ChannelCount { - if !self.current.is_exhausted() { - // Current source is active (producing samples) - // - Initially: never (Empty is exhausted immediately) - // - After append: the appended source while playing - // - With keep_alive: Zero (silence) while playing - self.current.channels() - } else if let Some((next, _)) = self.input.next_sounds.lock().unwrap().front() { - // Current source exhausted, peek at next queued source - // This is critical: UniformSourceIterator queries metadata during append, - // before any samples are pulled. We must report the next source's metadata. - next.channels() - } else { - // Queue is empty, no sources queued - // - Initially: Empty - // - With keep_alive: exhausted Zero between silence chunks (matches Empty) - // - Without keep_alive: Empty (will end on next()) - self.current.channels() + if self.current.is_exhausted() && self.silence_samples_remaining == 0 { + if let Some((next, _)) = self.input.next_sounds.lock().unwrap().front() { + // Current source exhausted, peek at next queued source + // This is critical: UniformSourceIterator queries metadata during append, + // before any samples are pulled. We must report the next source's metadata. + return next.channels(); + } } + + self.current.channels() } #[inline] fn sample_rate(&self) -> SampleRate { - if !self.current.is_exhausted() { - // Current source is active (producing samples) - self.current.sample_rate() - } else if let Some((next, _)) = self.input.next_sounds.lock().unwrap().front() { - // Current source exhausted, peek at next queued source - // This prevents wrong resampling setup in UniformSourceIterator - next.sample_rate() - } else { - // Queue is empty, no sources queued - self.current.sample_rate() + if self.current.is_exhausted() && self.silence_samples_remaining == 0 { + if let Some((next, _)) = self.input.next_sounds.lock().unwrap().front() { + // Current source exhausted, peek at next queued source + // This prevents wrong resampling setup in UniformSourceIterator + return next.sample_rate(); + } } + + self.current.sample_rate() } #[inline] @@ -232,34 +222,41 @@ impl Iterator for SourcesQueueOutput { fn next(&mut self) -> Option { loop { // If we're padding to complete a frame, return silence. - if self.padding_samples_remaining > 0 { - self.padding_samples_remaining -= 1; - return Some(0.0); + if self.silence_samples_remaining > 0 { + self.silence_samples_remaining -= 1; + return Some(Sample::EQUILIBRIUM); } // Basic situation that will happen most of the time. if let Some(sample) = self.current.next() { - self.samples_consumed_in_span += 1; + let channels = self.current.channels().get() as usize; + self.samples_consumed_in_span = (self.samples_consumed_in_span + 1) % channels; return Some(sample); } - // Source ended - check if we ended mid-frame and need padding. - let channels = self.current.channels().get() as usize; - let incomplete_frame_samples = self.samples_consumed_in_span % channels; - if incomplete_frame_samples > 0 { - // We're mid-frame - need to pad with silence to complete it. - self.padding_samples_remaining = channels - incomplete_frame_samples; - // Reset counter now since we're transitioning to a new span. - self.samples_consumed_in_span = 0; - // Continue loop - next iteration will inject silence. - continue; + // Current source is exhausted - check if we ended mid-frame and need padding. + if self.samples_consumed_in_span > 0 { + let channels = self.current.channels().get() as usize; + let incomplete_frame_samples = self.samples_consumed_in_span % channels; + if incomplete_frame_samples > 0 { + // We're mid-frame - need to pad with silence to complete it. + self.silence_samples_remaining = channels - incomplete_frame_samples; + // Reset counter now since we're transitioning to a new span. + self.samples_consumed_in_span = 0; + // Continue loop - next iterations will inject silence. + continue; + } } - // Reset counter and move to next sound. - // In order to avoid inlining this expensive operation, the code is in another function. - self.samples_consumed_in_span = 0; + // Move to next sound, play silence, or end. + // In order to avoid inlining that expensive operation, the code is in another function. if self.go_next().is_err() { - return None; + if self.input.keep_alive_if_empty() { + self.silence_samples_remaining = self.current.channels().get() as usize; + continue; + } else { + return None; + } } } } @@ -272,7 +269,7 @@ impl Iterator for SourcesQueueOutput { impl SourcesQueueOutput { // Called when `current` is empty, and we must jump to the next element. - // Returns `Ok` if the sound should continue playing, or an error if it should stop. + // Returns `Ok` if there is another sound should continue playing, or `Err` when there is not. // // This method is separate so that it is not inlined. fn go_next(&mut self) -> Result<(), ()> { @@ -282,27 +279,12 @@ impl SourcesQueueOutput { let (next, signal_after_end) = { let mut next = self.input.next_sounds.lock().unwrap(); - - if let Some(next) = next.pop_front() { - next - } else { - let channels = self.current.channels(); - let silence = Box::new(Zero::new_samples( - channels, - self.current.sample_rate(), - threshold(channels), - )) as Box<_>; - if self.input.keep_alive_if_empty.load(Ordering::Acquire) { - // Play a short silence in order to avoid spinlocking. - (silence, None) - } else { - return Err(()); - } - } + next.pop_front().ok_or(())? }; self.current = next; self.signal_after_end = signal_after_end; + self.samples_consumed_in_span = 0; Ok(()) } } @@ -371,7 +353,6 @@ mod tests { } #[test] - #[ignore] // TODO: not yet implemented fn no_delay_when_added() { let (tx, mut rx) = queue::queue(true); diff --git a/src/source/blt.rs b/src/source/blt.rs index 5d06779b..7816b347 100644 --- a/src/source/blt.rs +++ b/src/source/blt.rs @@ -3,11 +3,10 @@ use crate::math::PI; use crate::{Sample, Source}; use std::time::Duration; -use super::SeekError; +// Implemented following https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html +use super::{detect_span_boundary, reset_seek_span_tracking, SeekError}; -// Implemented following http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt - -/// Internal function that builds a `BltFilter` object. +/// Builds a `BltFilter` object with a low-pass filter. pub fn low_pass(input: I, freq: u32) -> BltFilter where I: Source, @@ -15,6 +14,7 @@ where low_pass_with_q(input, freq, 0.5) } +/// Builds a `BltFilter` object with a high-pass filter. pub fn high_pass(input: I, freq: u32) -> BltFilter where I: Source, @@ -27,15 +27,7 @@ pub fn low_pass_with_q(input: I, freq: u32, q: Float) -> BltFilter where I: Source, { - BltFilter { - input, - formula: BltFormula::LowPass { freq, q }, - applier: None, - x_n1: 0.0, - x_n2: 0.0, - y_n1: 0.0, - y_n2: 0.0, - } + blt_filter(input, BltFormula::LowPass { freq, q }) } /// Same as high_pass but allows the q value (bandwidth) to be changed @@ -43,30 +35,40 @@ pub fn high_pass_with_q(input: I, freq: u32, q: Float) -> BltFilter where I: Source, { + blt_filter(input, BltFormula::HighPass { freq, q }) +} + +/// Common constructor for BLT filters +fn blt_filter(input: I, formula: BltFormula) -> BltFilter +where + I: Source, +{ + let sample_rate = input.sample_rate(); + let channels = input.channels(); + BltFilter { - input, - formula: BltFormula::HighPass { freq, q }, - applier: None, - x_n1: 0.0, - x_n2: 0.0, - y_n1: 0.0, - y_n2: 0.0, + inner: Some(BltInner::new(input, formula, channels)), + last_sample_rate: sample_rate, + last_channels: channels, + samples_counted: 0, + cached_span_len: None, } } /// This applies an audio filter, it can be a high or low pass filter. #[derive(Clone, Debug)] pub struct BltFilter { - input: I, - formula: BltFormula, - applier: Option, - x_n1: Float, - x_n2: Float, - y_n1: Float, - y_n2: Float, + inner: Option>, + last_sample_rate: SampleRate, + last_channels: ChannelCount, + samples_counted: usize, + cached_span_len: Option, } -impl BltFilter { +impl BltFilter +where + I: Source, +{ /// Modifies this filter so that it becomes a low-pass filter. pub fn to_low_pass(&mut self, freq: u32) { self.to_low_pass_with_q(freq, 0.5); @@ -79,32 +81,36 @@ impl BltFilter { /// Same as to_low_pass but allows the q value (bandwidth) to be changed pub fn to_low_pass_with_q(&mut self, freq: u32, q: Float) { - self.formula = BltFormula::LowPass { freq, q }; - self.applier = None; + self.inner + .as_mut() + .unwrap() + .set_formula(BltFormula::LowPass { freq, q }); } /// Same as to_high_pass but allows the q value (bandwidth) to be changed pub fn to_high_pass_with_q(&mut self, freq: u32, q: Float) { - self.formula = BltFormula::HighPass { freq, q }; - self.applier = None; + self.inner + .as_mut() + .unwrap() + .set_formula(BltFormula::HighPass { freq, q }); } /// Returns a reference to the inner source. #[inline] pub fn inner(&self) -> &I { - &self.input + self.inner.as_ref().unwrap().inner() } /// Returns a mutable reference to the inner source. #[inline] pub fn inner_mut(&mut self) -> &mut I { - &mut self.input + self.inner.as_mut().unwrap().inner_mut() } /// Returns the inner source. #[inline] pub fn into_inner(self) -> I { - self.input + self.inner.unwrap().into_inner() } } @@ -116,66 +122,401 @@ where #[inline] fn next(&mut self) -> Option { - let last_in_span = self.input.current_span_len() == Some(1); + let sample = self.inner.as_mut().unwrap().next()?; + + let input_span_len = self.inner.as_ref().unwrap().current_span_len(); + let current_sample_rate = self.inner.as_ref().unwrap().sample_rate(); + let current_channels = self.inner.as_ref().unwrap().channels(); + + let (at_boundary, parameters_changed) = detect_span_boundary( + &mut self.samples_counted, + &mut self.cached_span_len, + input_span_len, + current_sample_rate, + self.last_sample_rate, + current_channels, + self.last_channels, + ); + + if at_boundary && parameters_changed { + let sample_rate_changed = current_sample_rate != self.last_sample_rate; + let channels_changed = current_channels != self.last_channels; + + self.last_sample_rate = current_sample_rate; + self.last_channels = current_channels; + + // If channel count changed, reconstruct with new variant (this also recreates applier) + // Otherwise, just recreate applier if sample rate changed + if channels_changed { + let old_inner = self.inner.take().unwrap(); + let (input, formula) = old_inner.into_parts(); + self.inner = Some(BltInner::new(input, formula, current_channels)); + } else if sample_rate_changed { + self.inner + .as_mut() + .unwrap() + .recreate_applier(current_sample_rate); + } + } + + Some(sample) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.as_ref().unwrap().size_hint() + } +} + +impl ExactSizeIterator for BltFilter where I: Source + ExactSizeIterator {} + +impl Source for BltFilter +where + I: Source, +{ + #[inline] + fn current_span_len(&self) -> Option { + self.inner.as_ref().unwrap().current_span_len() + } + + #[inline] + fn channels(&self) -> ChannelCount { + self.inner.as_ref().unwrap().channels() + } + + #[inline] + fn sample_rate(&self) -> SampleRate { + self.inner.as_ref().unwrap().sample_rate() + } + + #[inline] + fn total_duration(&self) -> Option { + self.inner.as_ref().unwrap().total_duration() + } - if self.applier.is_none() { - self.applier = Some(self.formula.to_applier(self.input.sample_rate().get())); + #[inline] + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + self.inner.as_mut().unwrap().try_seek(pos)?; + + reset_seek_span_tracking( + &mut self.samples_counted, + &mut self.cached_span_len, + pos, + self.inner.as_ref().unwrap().current_span_len(), + ); + + Ok(()) + } +} + +#[derive(Clone, Debug)] +enum BltInner { + Mono(BltMono), + Stereo(BltStereo), + Multi(BltMulti), +} + +impl BltInner +where + I: Source, +{ + fn new(input: I, formula: BltFormula, channels: ChannelCount) -> Self { + let channels_count = channels.get() as usize; + + let sample_rate = input.sample_rate(); + let applier = formula.to_applier(sample_rate.get()); + + match channels_count { + 1 => BltInner::Mono(BltMono { + input, + formula, + applier, + x_n1: 0.0, + x_n2: 0.0, + y_n1: 0.0, + y_n2: 0.0, + }), + 2 => BltInner::Stereo(BltStereo { + input, + formula, + applier, + x_n1: [0.0; 2], + x_n2: [0.0; 2], + y_n1: [0.0; 2], + y_n2: [0.0; 2], + is_right_channel: false, + }), + n => BltInner::Multi(BltMulti { + input, + formula, + applier, + x_n1: vec![0.0; n].into_boxed_slice(), + x_n2: vec![0.0; n].into_boxed_slice(), + y_n1: vec![0.0; n].into_boxed_slice(), + y_n2: vec![0.0; n].into_boxed_slice(), + position: 0, + }), } + } - let sample = self.input.next()?; - let result = self - .applier - .as_ref() - .unwrap() - .apply(sample, self.x_n1, self.x_n2, self.y_n1, self.y_n2); + fn set_formula(&mut self, formula: BltFormula) { + let sample_rate = self.inner().sample_rate(); + let applier = formula.to_applier(sample_rate.get()); - self.y_n2 = self.y_n1; - self.x_n2 = self.x_n1; - self.y_n1 = result; - self.x_n1 = sample; + match self { + BltInner::Mono(mono) => { + mono.formula = formula; + mono.applier = applier; + } + BltInner::Stereo(stereo) => { + stereo.formula = formula; + stereo.applier = applier; + } + BltInner::Multi(multi) => { + multi.formula = formula; + multi.applier = applier; + } + } + } - if last_in_span { - self.applier = None; + fn recreate_applier(&mut self, sample_rate: SampleRate) { + match self { + BltInner::Mono(mono) => { + mono.applier = mono.formula.to_applier(sample_rate.get()); + } + BltInner::Stereo(stereo) => { + stereo.applier = stereo.formula.to_applier(sample_rate.get()); + } + BltInner::Multi(multi) => { + multi.applier = multi.formula.to_applier(sample_rate.get()); + } } + } - Some(result) + fn into_parts(self) -> (I, BltFormula) { + match self { + BltInner::Mono(mono) => (mono.input, mono.formula), + BltInner::Stereo(stereo) => (stereo.input, stereo.formula), + BltInner::Multi(multi) => (multi.input, multi.formula), + } } #[inline] - fn size_hint(&self) -> (usize, Option) { - self.input.size_hint() + fn inner(&self) -> &I { + match self { + BltInner::Mono(mono) => &mono.input, + BltInner::Stereo(stereo) => &stereo.input, + BltInner::Multi(multi) => &multi.input, + } + } + + #[inline] + fn inner_mut(&mut self) -> &mut I { + match self { + BltInner::Mono(mono) => &mut mono.input, + BltInner::Stereo(stereo) => &mut stereo.input, + BltInner::Multi(multi) => &mut multi.input, + } + } + + #[inline] + fn into_inner(self) -> I { + match self { + BltInner::Mono(mono) => mono.input, + BltInner::Stereo(stereo) => stereo.input, + BltInner::Multi(multi) => multi.input, + } } } -impl ExactSizeIterator for BltFilter where I: Source + ExactSizeIterator {} +impl Iterator for BltInner +where + I: Source, +{ + type Item = Sample; -impl Source for BltFilter + #[inline] + fn next(&mut self) -> Option { + match self { + BltInner::Mono(mono) => mono.process_next(), + BltInner::Stereo(stereo) => stereo.process_next(), + BltInner::Multi(multi) => multi.process_next(), + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner().size_hint() + } +} + +impl Source for BltInner where I: Source, { #[inline] fn current_span_len(&self) -> Option { - self.input.current_span_len() + self.inner().current_span_len() } #[inline] fn channels(&self) -> ChannelCount { - self.input.channels() + self.inner().channels() } #[inline] fn sample_rate(&self) -> SampleRate { - self.input.sample_rate() + self.inner().sample_rate() } #[inline] fn total_duration(&self) -> Option { - self.input.total_duration() + self.inner().total_duration() } #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - self.input.try_seek(pos) + match self { + BltInner::Mono(mono) => { + mono.input.try_seek(pos)?; + mono.x_n1 = 0.0; + mono.x_n2 = 0.0; + mono.y_n1 = 0.0; + mono.y_n2 = 0.0; + } + BltInner::Stereo(stereo) => { + stereo.input.try_seek(pos)?; + stereo.x_n1 = [0.0; 2]; + stereo.x_n2 = [0.0; 2]; + stereo.y_n1 = [0.0; 2]; + stereo.y_n2 = [0.0; 2]; + stereo.is_right_channel = false; + } + BltInner::Multi(multi) => { + multi.input.try_seek(pos)?; + multi.x_n1.fill(0.0); + multi.x_n2.fill(0.0); + multi.y_n1.fill(0.0); + multi.y_n2.fill(0.0); + multi.position = 0; + } + } + Ok(()) + } +} + +/// Mono channel BLT filter optimized for single-channel processing. +#[derive(Clone, Debug)] +struct BltMono { + input: I, + formula: BltFormula, + applier: BltApplier, + x_n1: Float, + x_n2: Float, + y_n1: Float, + y_n2: Float, +} + +impl BltMono +where + I: Source, +{ + #[inline] + fn process_next(&mut self) -> Option { + let sample = self.input.next()?; + + let result = self + .applier + .apply(sample, self.x_n1, self.x_n2, self.y_n1, self.y_n2); + + self.y_n2 = self.y_n1; + self.x_n2 = self.x_n1; + self.y_n1 = result; + self.x_n1 = sample; + + Some(result) + } +} + +/// Stereo channel BLT filter with optimized two-channel processing. +#[derive(Clone, Debug)] +struct BltStereo { + input: I, + formula: BltFormula, + applier: BltApplier, + x_n1: [Float; 2], + x_n2: [Float; 2], + y_n1: [Float; 2], + y_n2: [Float; 2], + is_right_channel: bool, +} + +impl BltStereo +where + I: Source, +{ + #[inline] + fn process_next(&mut self) -> Option { + let sample = self.input.next()?; + + let channel = self.is_right_channel as usize; + self.is_right_channel = !self.is_right_channel; + + let result = self.applier.apply( + sample, + self.x_n1[channel], + self.x_n2[channel], + self.y_n1[channel], + self.y_n2[channel], + ); + + self.y_n2[channel] = self.y_n1[channel]; + self.x_n2[channel] = self.x_n1[channel]; + self.y_n1[channel] = result; + self.x_n1[channel] = sample; + + Some(result) + } +} + +/// Generic multi-channel BLT filter for surround sound or other configurations. +#[derive(Clone, Debug)] +struct BltMulti { + input: I, + formula: BltFormula, + applier: BltApplier, + x_n1: Box<[Float]>, + x_n2: Box<[Float]>, + y_n1: Box<[Float]>, + y_n2: Box<[Float]>, + position: usize, +} + +impl BltMulti +where + I: Source, +{ + #[inline] + fn process_next(&mut self) -> Option { + let sample = self.input.next()?; + + let channel = self.position; + self.position = (self.position + 1) % self.x_n1.len(); + + let result = self.applier.apply( + sample, + self.x_n1[channel], + self.x_n2[channel], + self.y_n1[channel], + self.y_n2[channel], + ); + + self.y_n2[channel] = self.y_n1[channel]; + self.x_n2[channel] = self.x_n1[channel]; + self.y_n1[channel] = result; + self.x_n1[channel] = sample; + + Some(result) } } diff --git a/src/source/buffered.rs b/src/source/buffered.rs index 974389dc..e45ea2ff 100644 --- a/src/source/buffered.rs +++ b/src/source/buffered.rs @@ -1,4 +1,3 @@ -use std::cmp; use std::mem; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -59,7 +58,7 @@ struct SpanData where I: Source, { - data: Vec, + data: Box<[I::Item]>, channels: ChannelCount, rate: SampleRate, next: Mutex>>, @@ -107,10 +106,12 @@ where let channels = input.channels(); let rate = input.sample_rate(); - let data: Vec = 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::>() + .into_boxed_slice(); if data.is_empty() { return Arc::new(Span::End); @@ -204,11 +205,12 @@ where { #[inline] fn current_span_len(&self) -> Option { - 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] diff --git a/src/source/channel_volume.rs b/src/source/channel_volume.rs index 4b5b2e87..2ed6a892 100644 --- a/src/source/channel_volume.rs +++ b/src/source/channel_volume.rs @@ -1,5 +1,7 @@ use std::time::Duration; +use dasp_sample::Sample as _; + use super::SeekError; use crate::common::{ChannelCount, SampleRate}; use crate::{Float, Sample, Source}; @@ -67,18 +69,33 @@ where #[inline] fn next(&mut self) -> Option { - // TODO Need a test for this if self.current_channel >= self.channel_volumes.len() { self.current_channel = 0; self.current_sample = None; - let num_channels = self.input.channels(); - for _ in 0..num_channels.get() { + + let mut samples_read = 0; + for _ in 0..self.input.channels().get() { if let Some(s) = self.input.next() { - self.current_sample = Some(self.current_sample.unwrap_or(0.0) + s); + self.current_sample = + Some(self.current_sample.unwrap_or(Sample::EQUILIBRIUM) + s); + samples_read += 1; + } else { + // Input ended mid-frame. This shouldn't happen per the Source contract, + // but handle it defensively: average only the samples we actually got. + break; } } - self.current_sample.map(|s| s / num_channels.get() as Float); + + // Divide by actual samples read, not the expected channel count. + // This handles the case where the input stream ends mid-frame. + if samples_read > 0 { + self.current_sample = self.current_sample.map(|s| s / samples_read as Float); + } else { + // No samples were read - input is exhausted + return None; + } } + let result = self .current_sample .map(|s| s * self.channel_volumes[self.current_channel]); @@ -124,3 +141,136 @@ where self.input.try_seek(pos) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::math::nz; + + /// Test helper source that allows setting false span length to simulate + /// sources that end before their promised span length. + #[derive(Debug, Clone)] + struct TestSource { + samples: Vec, + pos: usize, + channels: ChannelCount, + sample_rate: SampleRate, + total_span_len: Option, + } + + impl TestSource { + fn new(samples: &[Sample]) -> Self { + let samples = samples.to_vec(); + Self { + total_span_len: Some(samples.len()), + pos: 0, + channels: nz!(1), + sample_rate: nz!(44100), + samples, + } + } + + fn with_channels(mut self, count: ChannelCount) -> Self { + self.channels = count; + self + } + + fn with_false_span_len(mut self, total_len: Option) -> Self { + self.total_span_len = total_len; + self + } + } + + impl Iterator for TestSource { + type Item = Sample; + + fn next(&mut self) -> Option { + let res = self.samples.get(self.pos).copied(); + self.pos += 1; + res + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.samples.len().saturating_sub(self.pos); + (remaining, Some(remaining)) + } + } + + impl Source for TestSource { + fn current_span_len(&self) -> Option { + self.total_span_len + } + + fn channels(&self) -> ChannelCount { + self.channels + } + + fn sample_rate(&self) -> SampleRate { + self.sample_rate + } + + fn total_duration(&self) -> Option { + None + } + + fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> { + Err(SeekError::NotSupported { + underlying_source: std::any::type_name::(), + }) + } + } + + #[test] + fn test_mono_to_stereo() { + let input = TestSource::new(&[1.0, 2.0, 3.0]).with_channels(nz!(1)); + let mut channel_vol = ChannelVolume::new(input, vec![0.5, 0.8]); + assert_eq!(channel_vol.next(), Some(1.0 * 0.5)); + assert_eq!(channel_vol.next(), Some(1.0 * 0.8)); + assert_eq!(channel_vol.next(), Some(2.0 * 0.5)); + assert_eq!(channel_vol.next(), Some(2.0 * 0.8)); + assert_eq!(channel_vol.next(), Some(3.0 * 0.5)); + assert_eq!(channel_vol.next(), Some(3.0 * 0.8)); + assert_eq!(channel_vol.next(), None); + } + + #[test] + fn test_stereo_to_mono() { + let input = TestSource::new(&[1.0, 2.0, 3.0, 4.0]).with_channels(nz!(2)); + let mut channel_vol = ChannelVolume::new(input, vec![1.0]); + assert_eq!(channel_vol.next(), Some(1.5)); + assert_eq!(channel_vol.next(), Some(3.5)); + assert_eq!(channel_vol.next(), None); + } + + #[test] + fn test_stereo_to_stereo_with_mixing() { + let input = TestSource::new(&[1.0, 3.0, 2.0, 4.0]).with_channels(nz!(2)); + let mut channel_vol = ChannelVolume::new(input, vec![0.5, 2.0]); + assert_eq!(channel_vol.next(), Some(2.0 * 0.5)); // 1.0 + assert_eq!(channel_vol.next(), Some(2.0 * 2.0)); // 4.0 + assert_eq!(channel_vol.next(), Some(3.0 * 0.5)); // 1.5 + assert_eq!(channel_vol.next(), Some(3.0 * 2.0)); // 6.0 + assert_eq!(channel_vol.next(), None); + } + + #[test] + fn test_stream_ends_mid_frame() { + let input = TestSource::new(&[1.0, 2.0, 3.0, 4.0, 5.0]) + .with_channels(nz!(2)) + .with_false_span_len(Some(6)); // Promises 6 but only delivers 5 + + let mut channel_vol = ChannelVolume::new(input, vec![1.0, 1.0]); + + assert_eq!(channel_vol.next(), Some(1.5)); + assert_eq!(channel_vol.next(), Some(1.5)); + + assert_eq!(channel_vol.next(), Some(3.5)); + assert_eq!(channel_vol.next(), Some(3.5)); + + // Third partial frame: only got 5.0, divide by 1 (actual count) not 2 + assert_eq!(channel_vol.next(), Some(5.0)); + assert_eq!(channel_vol.next(), Some(5.0)); + + assert_eq!(channel_vol.next(), None); + } +} diff --git a/src/source/chirp.rs b/src/source/chirp.rs index ce5c0bf3..88c5bc0e 100644 --- a/src/source/chirp.rs +++ b/src/source/chirp.rs @@ -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 { @@ -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(()) + } } diff --git a/src/source/delay.rs b/src/source/delay.rs index 0f55aa32..5c835dc5 100644 --- a/src/source/delay.rs +++ b/src/source/delay.rs @@ -84,6 +84,8 @@ where } } +impl ExactSizeIterator for Delay where I: Iterator + Source + ExactSizeIterator {} + impl Source for Delay where I: Iterator + Source, diff --git a/src/source/dither.rs b/src/source/dither.rs index adc8b436..4a009883 100644 --- a/src/source/dither.rs +++ b/src/source/dither.rs @@ -29,7 +29,11 @@ use rand::{rngs::SmallRng, Rng}; use std::time::Duration; use crate::{ - source::noise::{Blue, WhiteGaussian, WhiteTriangular, WhiteUniform}, + source::{ + detect_span_boundary, + noise::{Blue, WhiteGaussian, WhiteTriangular, WhiteUniform}, + reset_seek_span_tracking, SeekError, + }, BitDepth, ChannelCount, Float, Sample, SampleRate, Source, }; @@ -161,8 +165,11 @@ pub struct Dither { input: I, noise: NoiseGenerator, current_channel: usize, - remaining_in_span: Option, + last_sample_rate: SampleRate, + last_channels: ChannelCount, lsb_amplitude: Float, + samples_counted: usize, + cached_span_len: Option, } impl Dither @@ -179,14 +186,16 @@ 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, + samples_counted: 0, + cached_span_len: None, } } @@ -213,21 +222,30 @@ where #[inline] fn next(&mut self) -> Option { - 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); + let input_span_len = self.input.current_span_len(); + let current_sample_rate = self.input.sample_rate(); + let current_channels = self.input.channels(); + + let (at_boundary, parameters_changed) = detect_span_boundary( + &mut self.samples_counted, + &mut self.cached_span_len, + input_span_len, + current_sample_rate, + self.last_sample_rate, + current_channels, + self.last_channels, + ); + + if at_boundary { + if parameters_changed { + self.noise + .update_parameters(current_sample_rate, current_channels); + self.last_sample_rate = current_sample_rate; + self.last_channels = current_channels; + } self.current_channel = 0; - self.remaining_in_span = self.input.current_span_len(); } let noise_sample = self @@ -236,7 +254,7 @@ where .expect("Noise generator should always produce samples"); // Advance to next channel (wrapping around) - self.current_channel = (self.current_channel + 1) % num_channels.get() as usize; + self.current_channel = (self.current_channel + 1) % self.input.channels().get() as usize; // Apply subtractive dithering at the target quantization level Some(input_sample - noise_sample * self.lsb_amplitude) @@ -275,8 +293,16 @@ where } #[inline] - fn try_seek(&mut self, pos: Duration) -> Result<(), crate::source::SeekError> { - self.input.try_seek(pos) + fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { + self.input.try_seek(pos)?; + self.current_channel = 0; + reset_seek_span_tracking( + &mut self.samples_counted, + &mut self.cached_span_len, + pos, + self.input.current_span_len(), + ); + Ok(()) } } diff --git a/src/source/done.rs b/src/source/done.rs index 5bc922b7..8e486cdb 100644 --- a/src/source/done.rs +++ b/src/source/done.rs @@ -66,6 +66,8 @@ where } } +impl ExactSizeIterator for Done where I: Source + ExactSizeIterator {} + impl Source for Done where I: Source, diff --git a/src/source/empty_callback.rs b/src/source/empty_callback.rs index 4ae62437..d240102b 100644 --- a/src/source/empty_callback.rs +++ b/src/source/empty_callback.rs @@ -29,12 +29,19 @@ impl Iterator for EmptyCallback { (self.callback)(); None } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (0, Some(0)) + } } +impl ExactSizeIterator for EmptyCallback {} + impl Source for EmptyCallback { #[inline] fn current_span_len(&self) -> Option { - None + Some(0) } #[inline] @@ -49,7 +56,7 @@ impl Source for EmptyCallback { #[inline] fn total_duration(&self) -> Option { - Some(Duration::new(0, 0)) + Some(Duration::ZERO) } #[inline] diff --git a/src/source/from_iter.rs b/src/source/from_iter.rs index c8c14062..e1252016 100644 --- a/src/source/from_iter.rs +++ b/src/source/from_iter.rs @@ -66,7 +66,7 @@ where if let Some(cur) = &self.current_source { (cur.size_hint().0, None) } else { - (0, None) + (0, Some(0)) } } } @@ -78,36 +78,13 @@ where { #[inline] fn current_span_len(&self) -> Option { - // This function is non-trivial because the boundary between the current source and the - // next must 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. - const THRESHOLD: usize = 10240; - - // Try the current `current_span_len`. if let Some(src) = &self.current_source { if !src.is_exhausted() { return src.current_span_len(); } } - // Try the size hint. - if let Some(src) = &self.current_source { - if let Some(val) = src.size_hint().1 { - if val < THRESHOLD && val != 0 { - return Some(val); - } - } - } - - // Otherwise we use the constant value. - Some(THRESHOLD) + None } #[inline] diff --git a/src/source/limit.rs b/src/source/limit.rs index a16139ae..a87ba671 100644 --- a/src/source/limit.rs +++ b/src/source/limit.rs @@ -60,13 +60,78 @@ use std::time::Duration; -use super::SeekError; +use super::{detect_span_boundary, reset_seek_span_tracking, SeekError}; use crate::{ common::{ChannelCount, Sample, SampleRate}, math::{self, duration_to_coefficient}, Float, Source, }; +/// Creates a limiter that processes the input audio source. +/// +/// This function applies the specified limiting settings to control audio peaks. +/// The limiter uses feedforward processing with configurable attack/release times +/// and soft-knee characteristics for natural-sounding dynamic range control. +/// +/// # Arguments +/// +/// * `input` - Audio source to process +/// * `settings` - Limiter configuration (threshold, knee, timing) +/// +/// # Returns +/// +/// A [`Limit`] source that applies the limiting to the input audio. +/// +/// # Example +/// +/// ```rust +/// use rodio::source::{SineWave, Source, LimitSettings}; +/// +/// let source = SineWave::new(440.0).amplify(2.0); +/// let settings = LimitSettings::default().with_threshold(-6.0); +/// let limited = source.limit(settings); +/// ``` +pub(crate) fn limit(input: I, settings: LimitSettings) -> Limit { + let sample_rate = input.sample_rate(); + let attack = duration_to_coefficient(settings.attack, sample_rate); + let release = duration_to_coefficient(settings.release, sample_rate); + let channels = input.channels(); + let channels_count = channels.get() as usize; + + let base = LimitBase::new(settings.threshold, settings.knee_width, attack, release); + + let inner = match channels_count { + 1 => LimitInner::Mono(LimitMono { + input, + base, + limiter_integrator: 0.0, + limiter_peak: 0.0, + }), + 2 => LimitInner::Stereo(LimitStereo { + input, + base, + limiter_integrators: [0.0; 2], + limiter_peaks: [0.0; 2], + is_right_channel: false, + }), + n => LimitInner::MultiChannel(LimitMulti { + input, + base, + limiter_integrators: vec![0.0; n].into_boxed_slice(), + limiter_peaks: vec![0.0; n].into_boxed_slice(), + position: 0, + }), + }; + + Limit { + inner: Some(inner), + last_channels: channels, + last_sample_rate: sample_rate, + samples_counted: 0, + cached_span_len: None, + } +} + /// Configuration settings for audio limiting. /// /// This struct defines how the limiter behaves, including when to start limiting @@ -126,8 +191,6 @@ use crate::{ /// .with_attack(Duration::from_millis(3)) // Faster attack /// .with_release(Duration::from_millis(50)); // Faster release /// ``` -#[derive(Debug, Clone)] -/// Configuration settings for audio limiting. /// /// # dB vs. dBFS Reference /// @@ -146,6 +209,7 @@ use crate::{ /// - **-6 dBFS**: Generous headroom (gentle limiting) /// - **-12 dBFS**: Conservative level (preserves significant dynamics) /// - **-20 dBFS**: Very quiet level (background/ambient sounds) +#[derive(Debug, Clone)] pub struct LimitSettings { /// Level where limiting begins (dBFS, must be negative). /// @@ -454,64 +518,6 @@ impl LimitSettings { } } -/// Creates a limiter that processes the input audio source. -/// -/// This function applies the specified limiting settings to control audio peaks. -/// The limiter uses feedforward processing with configurable attack/release times -/// and soft-knee characteristics for natural-sounding dynamic range control. -/// -/// # Arguments -/// -/// * `input` - Audio source to process -/// * `settings` - Limiter configuration (threshold, knee, timing) -/// -/// # Returns -/// -/// A [`Limit`] source that applies the limiting to the input audio. -/// -/// # Example -/// -/// ```rust -/// use rodio::source::{SineWave, Source, LimitSettings}; -/// -/// let source = SineWave::new(440.0).amplify(2.0); -/// let settings = LimitSettings::default().with_threshold(-6.0); -/// let limited = source.limit(settings); -/// ``` -pub(crate) fn limit(input: I, settings: LimitSettings) -> Limit { - let sample_rate = input.sample_rate(); - let attack = duration_to_coefficient(settings.attack, sample_rate); - let release = duration_to_coefficient(settings.release, sample_rate); - let channels = input.channels().get() as usize; - - let base = LimitBase::new(settings.threshold, settings.knee_width, attack, release); - - let inner = match channels { - 1 => LimitInner::Mono(LimitMono { - input, - base, - limiter_integrator: 0.0, - limiter_peak: 0.0, - }), - 2 => LimitInner::Stereo(LimitStereo { - input, - base, - limiter_integrators: [0.0; 2], - limiter_peaks: [0.0; 2], - position: 0, - }), - n => LimitInner::MultiChannel(LimitMulti { - input, - base, - limiter_integrators: vec![0.0; n], - limiter_peaks: vec![0.0; n], - position: 0, - }), - }; - - Limit(inner) -} - /// A source filter that applies audio limiting to prevent peaks from exceeding a threshold. /// /// This filter reduces the amplitude of audio signals that exceed the configured threshold @@ -554,23 +560,20 @@ pub(crate) fn limit(input: I, settings: LimitSettings) -> Limit { /// - **Stereo**: Two-channel optimized with interleaved processing /// - **Multi-channel**: Generic implementation for 3+ channels /// -/// # Channel Count Stability -/// -/// **Important**: The limiter is optimized for sources with fixed channel counts. -/// Most audio files (music, podcasts, etc.) maintain constant channel counts, -/// making this optimization safe and beneficial. -/// -/// If the underlying source changes channel count mid-stream (rare), the limiter -/// will continue to function but performance may be degraded. For such cases, -/// recreate the limiter when the channel count changes. -/// /// # Type Parameters /// /// * `I` - The input audio source type that implements [`Source`] #[derive(Clone, Debug)] -pub struct Limit(LimitInner) +pub struct Limit where - I: Source; + I: Source, +{ + inner: Option>, + last_channels: ChannelCount, + last_sample_rate: SampleRate, + samples_counted: usize, + cached_span_len: Option, +} impl Source for Limit where @@ -578,27 +581,34 @@ where { #[inline] fn current_span_len(&self) -> Option { - self.0.current_span_len() + self.inner.as_ref().unwrap().current_span_len() } #[inline] fn sample_rate(&self) -> SampleRate { - self.0.sample_rate() + self.inner.as_ref().unwrap().sample_rate() } #[inline] fn channels(&self) -> ChannelCount { - self.0.channels() + self.inner.as_ref().unwrap().channels() } #[inline] fn total_duration(&self) -> Option { - self.0.total_duration() + self.inner.as_ref().unwrap().total_duration() } #[inline] fn try_seek(&mut self, position: Duration) -> Result<(), SeekError> { - self.0.try_seek(position) + self.inner.as_mut().unwrap().try_seek(position)?; + reset_seek_span_tracking( + &mut self.samples_counted, + &mut self.cached_span_len, + position, + self.inner.as_ref().unwrap().current_span_len(), + ); + Ok(()) } } @@ -615,7 +625,7 @@ where /// Useful for inspecting source properties without consuming the filter. #[inline] pub fn inner(&self) -> &I { - self.0.inner() + self.inner.as_ref().unwrap().inner() } /// Returns a mutable reference to the inner audio source. @@ -625,7 +635,7 @@ where /// underlying source. #[inline] pub fn inner_mut(&mut self) -> &mut I { - self.0.inner_mut() + self.inner.as_mut().unwrap().inner_mut() } /// Consumes the limiter and returns the inner audio source. @@ -635,7 +645,7 @@ where /// Useful when limiting is no longer needed but the source should continue. #[inline] pub fn into_inner(self) -> I { - self.0.into_inner() + self.inner.unwrap().into_inner() } } @@ -648,16 +658,81 @@ where /// Provides the next limited sample. #[inline] fn next(&mut self) -> Option { - self.0.next() + let sample = self.inner.as_mut().unwrap().next()?; + + let input_span_len = self.inner.as_ref().unwrap().current_span_len(); + let current_channels = self.inner.as_ref().unwrap().channels(); + let current_sample_rate = self.inner.as_ref().unwrap().sample_rate(); + + let (at_boundary, parameters_changed) = detect_span_boundary( + &mut self.samples_counted, + &mut self.cached_span_len, + input_span_len, + current_sample_rate, + self.last_sample_rate, + current_channels, + self.last_channels, + ); + + if at_boundary && parameters_changed { + self.last_channels = current_channels; + self.last_sample_rate = current_sample_rate; + let new_channels_count = current_channels.get() as usize; + + let needs_reconstruction = match self.inner.as_ref().unwrap() { + LimitInner::Mono(_) => new_channels_count != 1, + LimitInner::Stereo(_) => new_channels_count != 2, + LimitInner::MultiChannel(multi) => { + new_channels_count != multi.limiter_integrators.len() + } + }; + + if needs_reconstruction { + let old_inner = self.inner.take().unwrap(); + + let (input, base) = match old_inner { + LimitInner::Mono(mono) => (mono.input, mono.base), + LimitInner::Stereo(stereo) => (stereo.input, stereo.base), + LimitInner::MultiChannel(multi) => (multi.input, multi.base), + }; + + self.inner = Some(match new_channels_count { + 1 => LimitInner::Mono(LimitMono { + input, + base, + limiter_integrator: 0.0, + limiter_peak: 0.0, + }), + 2 => LimitInner::Stereo(LimitStereo { + input, + base, + limiter_integrators: [0.0; 2], + limiter_peaks: [0.0; 2], + is_right_channel: false, + }), + n => LimitInner::MultiChannel(LimitMulti { + input, + base, + limiter_integrators: vec![0.0; n].into_boxed_slice(), + limiter_peaks: vec![0.0; n].into_boxed_slice(), + position: 0, + }), + }); + } + } + + Some(sample) } /// Provides size hints from the inner limiter. #[inline] fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() + self.inner.as_ref().unwrap().size_hint() } } +impl ExactSizeIterator for Limit where I: Source + ExactSizeIterator {} + /// Internal limiter implementation that adapts to different channel configurations. /// /// This enum is private and automatically selects the most efficient implementation @@ -716,13 +791,8 @@ struct LimitBase { /// This variant is automatically selected by [`Limit`] for mono audio sources. /// It uses minimal state (single integrator and peak detector) for optimal /// performance with single-channel audio. -/// -/// # Internal Use -/// -/// This struct is used internally by [`LimitInner::Mono`] and is not intended -/// for direct construction. Use [`Source::limit()`] instead. #[derive(Clone, Debug)] -pub struct LimitMono { +struct LimitMono { /// Input audio source input: I, /// Common limiter parameters @@ -744,13 +814,8 @@ pub struct LimitMono { /// The fixed arrays and channel position tracking provide optimal performance /// for interleaved stereo sample processing, avoiding the dynamic allocation /// overhead of the multi-channel variant. -/// -/// # Internal Use -/// -/// This struct is used internally by [`LimitInner::Stereo`] and is not intended -/// for direct construction. Use [`Source::limit()`] instead. #[derive(Clone, Debug)] -pub struct LimitStereo { +struct LimitStereo { /// Input audio source input: I, /// Common limiter parameters @@ -759,8 +824,8 @@ pub struct LimitStereo { limiter_integrators: [Float; 2], /// Peak detection states for left and right channels limiter_peaks: [Float; 2], - /// Current channel position (0 = left, 1 = right) - position: u8, + /// Current channel: true = right, false = left + is_right_channel: bool, } /// Generic multi-channel limiter for surround sound or other configurations. @@ -775,21 +840,16 @@ pub struct LimitStereo { /// While this variant has slightly more overhead than the mono/stereo variants /// due to vector allocation and dynamic indexing, it provides the flexibility /// needed for complex audio setups while maintaining good performance. -/// -/// # Internal Use -/// -/// This struct is used internally by [`LimitInner::MultiChannel`] and is not -/// intended for direct construction. Use [`Source::limit()`] instead. #[derive(Clone, Debug)] -pub struct LimitMulti { +struct LimitMulti { /// Input audio source input: I, /// Common limiter parameters base: LimitBase, /// Peak detector integrator states (one per channel) - limiter_integrators: Vec, + limiter_integrators: Box<[Float]>, /// Peak detector states (one per channel) - limiter_peaks: Vec, + limiter_peaks: Box<[Float]>, /// Current channel position (0 to channels-1) position: usize, } @@ -908,8 +968,8 @@ where /// updates. #[inline] fn process_next(&mut self, sample: I::Item) -> I::Item { - let channel = self.position as usize; - self.position ^= 1; + let channel = self.is_right_channel as usize; + self.is_right_channel = !self.is_right_channel; let processed = self.base.process_channel( sample, @@ -1131,7 +1191,7 @@ mod tests { use std::time::Duration; fn create_test_buffer( - samples: Vec, + samples: &[Sample], channels: ChannelCount, sample_rate: SampleRate, ) -> SamplesBuffer { @@ -1141,26 +1201,35 @@ mod tests { #[test] fn test_limiter_creation() { // Test mono - let buffer = create_test_buffer(vec![0.5, 0.8, 1.0, 0.3], nz!(1), nz!(44100)); + let buffer = create_test_buffer(&[0.5, 0.8, 1.0, 0.3], nz!(1), nz!(44100)); let limiter = limit(buffer, LimitSettings::default()); assert_eq!(limiter.channels(), nz!(1)); assert_eq!(limiter.sample_rate(), nz!(44100)); - matches!(limiter.0, LimitInner::Mono(_)); + assert!(matches!( + limiter.inner.as_ref().unwrap(), + LimitInner::Mono(_) + )); // Test stereo let buffer = create_test_buffer( - vec![0.5, 0.8, 1.0, 0.3, 0.2, 0.6, 0.9, 0.4], + &[0.5, 0.8, 1.0, 0.3, 0.2, 0.6, 0.9, 0.4], nz!(2), nz!(44100), ); let limiter = limit(buffer, LimitSettings::default()); assert_eq!(limiter.channels(), nz!(2)); - matches!(limiter.0, LimitInner::Stereo(_)); + assert!(matches!( + limiter.inner.as_ref().unwrap(), + LimitInner::Stereo(_) + )); // Test multichannel - let buffer = create_test_buffer(vec![0.5; 12], nz!(3), nz!(44100)); + let buffer = create_test_buffer(&[0.5; 12], nz!(3), nz!(44100)); let limiter = limit(buffer, LimitSettings::default()); assert_eq!(limiter.channels(), nz!(3)); - matches!(limiter.0, LimitInner::MultiChannel(_)); + assert!(matches!( + limiter.inner.as_ref().unwrap(), + LimitInner::MultiChannel(_) + )); } } diff --git a/src/source/mod.rs b/src/source/mod.rs index b458d13a..425f82e9 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -164,17 +164,21 @@ pub use self::noise::{Pink, WhiteUniform}; /// `sample_rate` too frequently. /// /// In order to properly handle this situation, the `current_span_len()` method should return -/// the number of samples that remain in the iterator before the samples rate and number of -/// channels can potentially change. +/// the total number of samples in the current span (i.e., before the sample rate and number of +/// channels can potentially change). /// pub trait Source: Iterator { - /// Returns the number of samples before the current span ends. + /// Returns the total length of the current span in samples. + /// + /// A span is a contiguous block of samples with unchanging channel count and sample rate. + /// This method returns the total number of samples in the current span, not the number + /// of samples remaining to be read. /// /// `None` means "infinite" or "until the sound ends". Sources that return `Some(x)` should /// return `Some(0)` if and only if when there's no more data. /// - /// After the engine has finished reading the specified number of samples, it will check - /// whether the value of `channels()` and/or `sample_rate()` have changed. + /// After the engine has finished reading the number of samples returned by this method, + /// it will check whether the value of `channels()` and/or `sample_rate()` have changed. /// /// # Frame Alignment /// @@ -836,3 +840,58 @@ source_pointer_impl!(Source for Box); source_pointer_impl!(Source for Box); source_pointer_impl!(<'a, Src> Source for &'a mut Src where Src: Source,); + +/// Detects if we're at a span boundary using dual-mode tracking. +/// Returns a tuple indicating whether we're at a span boundary and if parameters changed. +#[inline] +pub(crate) fn detect_span_boundary( + samples_counted: &mut usize, + cached_span_len: &mut Option, + input_span_len: Option, + current_sample_rate: SampleRate, + last_sample_rate: SampleRate, + current_channels: ChannelCount, + last_channels: ChannelCount, +) -> (bool, bool) { + *samples_counted = samples_counted.saturating_add(1); + + // If input reports no span length, then by contract parameters are stable. + let mut parameters_changed = false; + let at_boundary = input_span_len.is_some_and(|_| { + let known_boundary = cached_span_len.map(|cached_len| *samples_counted >= cached_len); + + // In span-counting mode, the only moment that parameters can change is at a span boundary. + // In detection mode after try_seek, we check every sample until we detect a boundary. + if known_boundary.is_none_or(|at_boundary| at_boundary) { + parameters_changed = + current_channels != last_channels || current_sample_rate != last_sample_rate; + } + + known_boundary.unwrap_or(parameters_changed) + }); + + if at_boundary { + *samples_counted = 0; + *cached_span_len = input_span_len; + } + + (at_boundary, parameters_changed) +} + +/// Resets span tracking state after a seek operation. +#[inline] +pub(crate) fn reset_seek_span_tracking( + samples_counted: &mut usize, + cached_span_len: &mut Option, + pos: Duration, + input_span_len: Option, +) { + *samples_counted = 0; + if pos == Duration::ZERO { + // Set span-counting mode when seeking to start + *cached_span_len = input_span_len; + } else { + // Set detection mode for arbitrary positions + *cached_span_len = None; + } +} diff --git a/src/source/noise.rs b/src/source/noise.rs index 32cea2d9..b2712549 100644 --- a/src/source/noise.rs +++ b/src/source/noise.rs @@ -166,6 +166,11 @@ impl Iterator for WhiteUniform { fn next(&mut self) -> Option { Some(self.sampler.sample()) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl_noise_source!(WhiteUniform); @@ -221,6 +226,11 @@ impl Iterator for WhiteTriangular { fn next(&mut self) -> Option { Some(self.sampler.sample()) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl_noise_source!(WhiteTriangular); @@ -319,6 +329,11 @@ impl Iterator for Velvet { Some(output) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl_noise_source!(Velvet); @@ -393,6 +408,11 @@ impl Iterator for WhiteGaussian { fn next(&mut self) -> Option { Some(self.sampler.sample()) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl_noise_source!(WhiteGaussian); @@ -492,6 +512,11 @@ impl Iterator for Pink { // Normalize by number of generators to keep output in reasonable range Some(sum / PINK_NOISE_GENERATORS as Sample) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.white_noise.size_hint() + } } impl_noise_source!(Pink); @@ -555,6 +580,11 @@ impl Iterator for Blue { self.prev_white = white; Some(blue) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.white_noise.size_hint() + } } impl_noise_source!(Blue); @@ -618,6 +648,11 @@ impl Iterator for Violet { self.prev = blue; Some(violet) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.blue_noise.size_hint() + } } impl_noise_source!(Violet); @@ -673,6 +708,11 @@ impl> Iterator for IntegratedNoise { self.accumulator = self.accumulator * self.leak_factor + white; Some(self.accumulator * self.scale) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.white_noise.size_hint() + } } /// Brownian noise generator - true stochastic Brownian motion with Gaussian increments. @@ -724,6 +764,11 @@ impl Iterator for Brownian { fn next(&mut self) -> Option { self.inner.next() } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } } impl Source for Brownian { @@ -746,6 +791,7 @@ impl Source for Brownian { fn try_seek(&mut self, _pos: Duration) -> Result<(), crate::source::SeekError> { // Stateless noise generators can seek to any position since all positions // are equally random and don't depend on previous state + self.inner.accumulator = 0.0; Ok(()) } } @@ -801,6 +847,11 @@ impl Iterator for Red { fn next(&mut self) -> Option { self.inner.next() } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } } impl Source for Red { @@ -823,6 +874,7 @@ impl Source for Red { fn try_seek(&mut self, _pos: Duration) -> Result<(), crate::source::SeekError> { // Stateless noise generators can seek to any position since all positions // are equally random and don't depend on previous state + self.inner.accumulator = 0.0; Ok(()) } } diff --git a/src/source/pausable.rs b/src/source/pausable.rs index 5143b622..1b05ab0d 100644 --- a/src/source/pausable.rs +++ b/src/source/pausable.rs @@ -98,10 +98,14 @@ where #[inline] fn size_hint(&self) -> (usize, Option) { - self.input.size_hint() + let (lower, upper) = self.input.size_hint(); + let paused_samples = self.remaining_paused_samples as usize; + (lower + paused_samples, upper.map(|u| u + paused_samples)) } } +impl ExactSizeIterator for Pausable where I: Source + ExactSizeIterator {} + impl Source for Pausable where I: Source, diff --git a/src/source/periodic.rs b/src/source/periodic.rs index 09fc5477..b251ea31 100644 --- a/src/source/periodic.rs +++ b/src/source/periodic.rs @@ -14,7 +14,7 @@ where // TODO: handle the fact that the samples rate can change let update_frequency = (period.as_secs_f32() * (source.sample_rate().get() as f32) - * (source.channels().get() as f32)) as u32; + * (source.channels().get() as f32)) as usize; PeriodicAccess { input: source, @@ -35,10 +35,10 @@ pub struct PeriodicAccess { modifier: F, // The frequency with which local_volume should be updated by remote_volume - update_frequency: u32, + update_frequency: usize, // How many samples remain until it is time to update local_volume with remote_volume. - samples_until_update: u32, + samples_until_update: usize, } impl PeriodicAccess @@ -91,6 +91,13 @@ where } } +impl ExactSizeIterator for PeriodicAccess +where + I: Source + ExactSizeIterator, + F: FnMut(&mut I), +{ +} + impl Source for PeriodicAccess where I: Source, diff --git a/src/source/position.rs b/src/source/position.rs index 1788625b..52821bb7 100644 --- a/src/source/position.rs +++ b/src/source/position.rs @@ -1,20 +1,25 @@ use std::time::Duration; -use super::SeekError; -use crate::common::{ChannelCount, SampleRate}; -use crate::math::nz; +use super::{detect_span_boundary, reset_seek_span_tracking, SeekError}; +use crate::common::{ChannelCount, Float, SampleRate}; +use crate::math::{duration_from_secs, duration_to_float}; use crate::Source; /// Internal function that builds a `TrackPosition` object. See trait docs for /// details -pub fn track_position(source: I) -> TrackPosition { +pub fn track_position(source: I) -> TrackPosition +where + I: Source, +{ + let channels = source.channels(); + let sample_rate = source.sample_rate(); TrackPosition { input: source, samples_counted: 0, offset_duration: 0.0, - current_span_sample_rate: nz!(1), - current_span_channels: nz!(1), - current_span_len: None, + current_span_sample_rate: sample_rate, + current_span_channels: channels, + cached_span_len: None, } } @@ -23,10 +28,10 @@ pub fn track_position(source: I) -> TrackPosition { pub struct TrackPosition { input: I, samples_counted: usize, - offset_duration: f64, + offset_duration: Float, current_span_sample_rate: SampleRate, current_span_channels: ChannelCount, - current_span_len: Option, + cached_span_len: Option, } impl TrackPosition { @@ -65,18 +70,11 @@ where /// track_position after speedup's and delay's. #[inline] pub fn get_pos(&self) -> Duration { - let seconds = self.samples_counted as f64 - / self.input.sample_rate().get() as f64 - / self.input.channels().get() as f64 + let seconds = self.samples_counted as Float + / self.input.sample_rate().get() as Float + / self.input.channels().get() as Float + self.offset_duration; - Duration::from_secs_f64(seconds) - } - - #[inline] - fn set_current_span(&mut self) { - self.current_span_len = self.current_span_len(); - self.current_span_sample_rate = self.sample_rate(); - self.current_span_channels = self.channels(); + duration_from_secs(seconds) } } @@ -88,28 +86,42 @@ where #[inline] fn next(&mut self) -> Option { - // This should only be executed once at the first call to next. - if self.current_span_len.is_none() { - self.set_current_span(); + let item = self.input.next()?; + + let input_span_len = self.input.current_span_len(); + let current_sample_rate = self.input.sample_rate(); + let current_channels = self.input.channels(); + + // Capture samples_counted before detect_span_boundary resets it + let samples_before_boundary = self.samples_counted; + + let (at_boundary, parameters_changed) = detect_span_boundary( + &mut self.samples_counted, + &mut self.cached_span_len, + input_span_len, + current_sample_rate, + self.current_span_sample_rate, + current_channels, + self.current_span_channels, + ); + + if at_boundary { + // At span boundary - accumulate duration using OLD parameters and the sample + // count from before the boundary (detect_span_boundary increments first, then + // resets at boundary, so samples_before_boundary + 1 gives us the completed count) + let completed_samples = samples_before_boundary.saturating_add(1); + + self.offset_duration += completed_samples as Float + / self.current_span_sample_rate.get() as Float + / self.current_span_channels.get() as Float; + + if parameters_changed { + self.current_span_sample_rate = current_sample_rate; + self.current_span_channels = current_channels; + } } - let item = self.input.next(); - if item.is_some() { - self.samples_counted += 1; - - // At the end of a span add the duration of this span to - // offset_duration and start collecting samples again. - if Some(self.samples_counted) == self.current_span_len() { - self.offset_duration += self.samples_counted as f64 - / self.current_span_sample_rate.get() as f64 - / self.current_span_channels.get() as f64; - - // Reset. - self.samples_counted = 0; - self.set_current_span(); - }; - }; - item + Some(item) } #[inline] @@ -118,6 +130,8 @@ where } } +impl ExactSizeIterator for TrackPosition where I: Source + ExactSizeIterator {} + impl Source for TrackPosition where I: Source, @@ -144,15 +158,17 @@ where #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - let result = self.input.try_seek(pos); - if result.is_ok() { - self.offset_duration = pos.as_secs_f64(); - // This assumes that the seek implementation of the codec always - // starts again at the beginning of a span. Which is the case with - // symphonia. - self.samples_counted = 0; - } - result + self.input.try_seek(pos)?; + self.offset_duration = duration_to_float(pos); + reset_seek_span_tracking( + &mut self.samples_counted, + &mut self.cached_span_len, + pos, + self.input.current_span_len(), + ); + self.current_span_sample_rate = self.input.sample_rate(); + self.current_span_channels = self.input.channels(); + Ok(()) } } diff --git a/src/source/repeat.rs b/src/source/repeat.rs index ef29968e..601b679c 100644 --- a/src/source/repeat.rs +++ b/src/source/repeat.rs @@ -46,7 +46,7 @@ where #[inline] fn size_hint(&self) -> (usize, Option) { // infinite - (0, None) + (usize::MAX, None) } } diff --git a/src/source/sawtooth.rs b/src/source/sawtooth.rs index 2bfc2309..e5bc97f1 100644 --- a/src/source/sawtooth.rs +++ b/src/source/sawtooth.rs @@ -36,6 +36,11 @@ impl Iterator for SawtoothWave { fn next(&mut self) -> Option { self.test_saw.next() } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl Source for SawtoothWave { diff --git a/src/source/signal_generator.rs b/src/source/signal_generator.rs index 34ffa443..58b29941 100644 --- a/src/source/signal_generator.rs +++ b/src/source/signal_generator.rs @@ -133,6 +133,11 @@ impl Iterator for SignalGenerator { self.phase = (self.phase + self.phase_step).rem_euclid(1.0); val } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl Source for SignalGenerator { diff --git a/src/source/sine.rs b/src/source/sine.rs index 1481d6b2..17c97008 100644 --- a/src/source/sine.rs +++ b/src/source/sine.rs @@ -36,6 +36,11 @@ impl Iterator for SineWave { fn next(&mut self) -> Option { self.test_sine.next() } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl Source for SineWave { diff --git a/src/source/skip.rs b/src/source/skip.rs index dfae5b92..8d80bf8e 100644 --- a/src/source/skip.rs +++ b/src/source/skip.rs @@ -22,7 +22,7 @@ fn do_skip_duration(input: &mut I, mut duration: Duration) where I: Source, { - while duration > Duration::new(0, 0) { + while duration > Duration::ZERO { if input.current_span_len().is_none() { // Sample rate and the amount of channels will be the same till the end. do_skip_duration_unchecked(input, duration); @@ -129,6 +129,8 @@ where } } +impl ExactSizeIterator for SkipDuration where I: Source + ExactSizeIterator {} + impl Source for SkipDuration where I: Source, @@ -150,10 +152,9 @@ where #[inline] fn total_duration(&self) -> Option { - self.input.total_duration().map(|val| { - val.checked_sub(self.skipped_duration) - .unwrap_or_else(|| Duration::from_secs(0)) - }) + self.input + .total_duration() + .map(|val| val.saturating_sub(self.skipped_duration)) } #[inline] diff --git a/src/source/skippable.rs b/src/source/skippable.rs index 0fc46560..24fa02bb 100644 --- a/src/source/skippable.rs +++ b/src/source/skippable.rs @@ -70,6 +70,8 @@ where } } +impl ExactSizeIterator for Skippable where I: Source + ExactSizeIterator {} + impl Source for Skippable where I: Source, diff --git a/src/source/square.rs b/src/source/square.rs index d4acb321..ebb16e82 100644 --- a/src/source/square.rs +++ b/src/source/square.rs @@ -36,6 +36,11 @@ impl Iterator for SquareWave { fn next(&mut self) -> Option { self.test_square.next() } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl Source for SquareWave { diff --git a/src/source/stoppable.rs b/src/source/stoppable.rs index 57cc7dae..44f21940 100644 --- a/src/source/stoppable.rs +++ b/src/source/stoppable.rs @@ -66,6 +66,8 @@ where } } +impl ExactSizeIterator for Stoppable where I: Source + ExactSizeIterator {} + impl Source for Stoppable where I: Source, diff --git a/src/source/take.rs b/src/source/take.rs index d9957e6f..ad953cc9 100644 --- a/src/source/take.rs +++ b/src/source/take.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use super::SeekError; +use super::{detect_span_boundary, reset_seek_span_tracking, SeekError}; use crate::common::{ChannelCount, SampleRate}; use crate::math::NANOS_PER_SEC; use crate::{Float, Sample, Source}; @@ -10,13 +10,18 @@ pub fn take_duration(input: I, duration: Duration) -> TakeDuration where I: Source, { + let sample_rate = input.sample_rate(); + let channels = input.channels(); TakeDuration { - current_span_len: input.current_span_len(), duration_per_sample: TakeDuration::get_duration_per_sample(&input), input, remaining_duration: duration, requested_duration: duration, filter: None, + last_sample_rate: sample_rate, + last_channels: channels, + samples_counted: 0, + cached_span_len: None, } } @@ -44,10 +49,12 @@ pub struct TakeDuration { remaining_duration: Duration, requested_duration: Duration, filter: Option, - // Remaining samples in current span. - current_span_len: Option, - // Only updated when the current span len is exhausted. + // Cached duration per sample, updated when sample rate or channels change. duration_per_sample: Duration, + last_sample_rate: SampleRate, + last_channels: ChannelCount, + samples_counted: usize, + cached_span_len: Option, } impl TakeDuration @@ -99,19 +106,29 @@ where type Item = ::Item; fn next(&mut self) -> Option<::Item> { - if let Some(span_len) = self.current_span_len.take() { - if span_len > 0 { - self.current_span_len = Some(span_len - 1); - } else { - self.current_span_len = self.input.current_span_len(); - // Sample rate might have changed + if self.remaining_duration < self.duration_per_sample { + None + } else if let Some(sample) = self.input.next() { + let input_span_len = self.input.current_span_len(); + let current_sample_rate = self.input.sample_rate(); + let current_channels = self.input.channels(); + + let (at_boundary, parameters_changed) = detect_span_boundary( + &mut self.samples_counted, + &mut self.cached_span_len, + input_span_len, + current_sample_rate, + self.last_sample_rate, + current_channels, + self.last_channels, + ); + + if at_boundary && parameters_changed { + self.last_sample_rate = current_sample_rate; + self.last_channels = current_channels; self.duration_per_sample = Self::get_duration_per_sample(&self.input); } - } - if self.remaining_duration <= self.duration_per_sample { - None - } else if let Some(sample) = self.input.next() { let sample = match &self.filter { Some(filter) => filter.apply(sample, self), None => sample, @@ -125,9 +142,31 @@ where } } - // TODO: size_hint + #[inline] + fn size_hint(&self) -> (usize, Option) { + let remaining_nanos = self.remaining_duration.as_secs() * 1_000_000_000 + + self.remaining_duration.subsec_nanos() as u64; + let nanos_per_sample = self.duration_per_sample.as_secs() * 1_000_000_000 + + self.duration_per_sample.subsec_nanos() as u64; + + if nanos_per_sample == 0 || remaining_nanos == 0 { + return (0, Some(0)); + } + + let remaining_samples = (remaining_nanos / nanos_per_sample) as usize; + + let (inner_lower, inner_upper) = self.input.size_hint(); + let lower = inner_lower.min(remaining_samples); + let upper = inner_upper + .map(|u| u.min(remaining_samples)) + .or(Some(remaining_samples)); + + (lower, upper) + } } +impl ExactSizeIterator for TakeDuration where I: Source + ExactSizeIterator {} + impl Source for TakeDuration where I: Iterator + Source, @@ -138,6 +177,11 @@ where + self.remaining_duration.subsec_nanos() as u64; let nanos_per_sample = self.duration_per_sample.as_secs() * NANOS_PER_SEC + self.duration_per_sample.subsec_nanos() as u64; + + if nanos_per_sample == 0 || remaining_nanos == 0 { + return Some(0); + } + let remaining_samples = (remaining_nanos / nanos_per_sample) as usize; self.input @@ -171,6 +215,45 @@ where #[inline] fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> { - self.input.try_seek(pos) + let result = self.input.try_seek(pos); + if result.is_ok() { + // Recalculate remaining duration after seek + self.remaining_duration = self.requested_duration.saturating_sub(pos); + reset_seek_span_tracking( + &mut self.samples_counted, + &mut self.cached_span_len, + pos, + self.input.current_span_len(), + ); + } + result + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::source::SineWave; + + #[test] + fn test_size_hint_with_zero_remaining() { + let source = SineWave::new(440.0).take_duration(Duration::ZERO); + assert_eq!(source.size_hint(), (0, Some(0))); + } + + #[test] + fn test_exact_duration_boundary() { + use crate::source::SineWave; + + let sample_rate = 48000; + let nanos_per_sample = (1_000_000_000 as Float / sample_rate as Float) as usize; + + let n_samples = 10; + let exact_duration = Duration::from_nanos((nanos_per_sample * n_samples) as u64); + + let source = SineWave::new(440.0).take_duration(exact_duration); + + let count = source.count(); + assert_eq!(count, n_samples); } } diff --git a/src/source/triangle.rs b/src/source/triangle.rs index 6cafc911..86a0c970 100644 --- a/src/source/triangle.rs +++ b/src/source/triangle.rs @@ -36,6 +36,11 @@ impl Iterator for TriangleWave { fn next(&mut self) -> Option { self.test_tri.next() } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } } impl Source for TriangleWave { diff --git a/src/static_buffer.rs b/src/static_buffer.rs index 44ff5998..d5b77ae5 100644 --- a/src/static_buffer.rs +++ b/src/static_buffer.rs @@ -114,6 +114,8 @@ impl Iterator for StaticSamplesBuffer { } } +impl ExactSizeIterator for StaticSamplesBuffer {} + #[cfg(test)] mod tests { use crate::math::nz; diff --git a/src/wav_output.rs b/src/wav_output.rs index f9234169..5f8f5ced 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -126,6 +126,11 @@ impl> Iterator for WholeFrames { self.pos += 1; Some(to_yield) } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.source.size_hint() + } } #[cfg(test)]