WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit b65e416

Browse files
roderickvd0----0
andcommitted
fix: audio distortion at queue source boundaries
Fixes incorrect sample_rate() and channels() metadata returned by SourcesQueueOutput when transitioning between sources with different formats or from an empty queue. This caused audio distortion at boundaries where metadata changes occurred. Changes: - Clarified Source::current_span_len() contract: returns total span size, Some(0) when exhausted - Fixed SamplesBuffer to return Some(total_size) or Some(0) when exhausted (was None) - Fixed SamplesBuffer::size_hint() to return remaining samples - Updated SourcesQueueOutput to peek next source metadata when current is exhausted - Added Source::is_exhausted() helper method for cleaner exhaustion checks throughout codebase - Implemented ExactSizeIterator for SamplesBuffer - Improved SkipDuration precision to avoid rounding errors This supersedes PR #812 with a cleaner implementation following the proper current_span_len() contract. Enabled previously ignored queue::tests::basic test to prevent regressions. Fixes #811 Co-authored-by: 0----0 <[email protected]>
1 parent 2effbfc commit b65e416

File tree

7 files changed

+84
-37
lines changed

7 files changed

+84
-37
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
### Added
1313

1414
- `Chirp` now implements `Iterator::size_hint` and `ExactSizeIterator`.
15+
- `SamplesBuffer` now implements `ExactSizeIterator`.
16+
- Added `Source::is_exhausted()` helper method to check if a source has no more samples.
1517
- Added `Red` noise generator that is more practical than `Brownian` noise.
1618
- Added `std_dev()` to `WhiteUniform` and `WhiteTriangular`.
1719
- Added a macro `nz!` which facilitates creating NonZero's for `SampleRate` and
@@ -31,13 +33,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3133
- `Chirp::next` now returns `None` when the total duration has been reached, and will work
3234
correctly for a number of samples greater than 2^24.
3335
- `PeriodicAccess` is slightly more accurate for 44.1 kHz sample rate families.
36+
- Fixed audio distortion when queueing sources with different sample rates/channel counts or transitioning from empty queue.
37+
- Fixed `SamplesBuffer` to correctly report exhaustion and remaining samples.
38+
- Improved precision in `SkipDuration` to avoid off-by-a-few-samples errors.
3439

3540
### Changed
3641
- `output_to_wav` renamed to `wav_to_file` and now takes ownership of the `Source`.
3742
- `Blue` noise generator uses uniform instead of Gaussian noise for better performance.
3843
- `Gaussian` noise generator has standard deviation of 0.6 for perceptual equivalence.
3944
- `Velvet` noise generator takes density in Hz as `usize` instead of `f32`.
4045
- Upgrade `cpal` to v0.17.
46+
- Clarified `Source::current_span_len()` contract documentation.
4147

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

src/buffer.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ impl SamplesBuffer {
7373
impl Source for SamplesBuffer {
7474
#[inline]
7575
fn current_span_len(&self) -> Option<usize> {
76-
None
76+
if self.pos >= self.data.len() {
77+
Some(0)
78+
} else {
79+
Some(self.data.len())
80+
}
7781
}
7882

7983
#[inline]
@@ -126,10 +130,13 @@ impl Iterator for SamplesBuffer {
126130

127131
#[inline]
128132
fn size_hint(&self) -> (usize, Option<usize>) {
129-
(self.data.len(), Some(self.data.len()))
133+
let remaining = self.data.len() - self.pos;
134+
(remaining, Some(remaining))
130135
}
131136
}
132137

138+
impl ExactSizeIterator for SamplesBuffer {}
139+
133140
#[cfg(test)]
134141
mod tests {
135142
use crate::buffer::SamplesBuffer;

src/queue.rs

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ pub struct SourcesQueueOutput {
113113
}
114114

115115
const THRESHOLD: usize = 512;
116+
const SILENCE_SAMPLE_RATE: SampleRate = nz!(44100);
117+
const SILENCE_CHANNELS: ChannelCount = nz!(1);
116118

117119
impl Source for SourcesQueueOutput {
118120
#[inline]
@@ -129,15 +131,13 @@ impl Source for SourcesQueueOutput {
129131
// constant.
130132

131133
// Try the current `current_span_len`.
132-
if let Some(val) = self.current.current_span_len() {
133-
if val != 0 {
134-
return Some(val);
135-
} else if self.input.keep_alive_if_empty.load(Ordering::Acquire)
136-
&& self.input.next_sounds.lock().unwrap().is_empty()
137-
{
138-
// The next source will be a filler silence which will have the length of `THRESHOLD`
139-
return Some(THRESHOLD);
140-
}
134+
if !self.current.is_exhausted() {
135+
return self.current.current_span_len();
136+
} else if self.input.keep_alive_if_empty.load(Ordering::Acquire)
137+
&& self.input.next_sounds.lock().unwrap().is_empty()
138+
{
139+
// The next source will be a filler silence which will have the length of `THRESHOLD`
140+
return Some(THRESHOLD);
141141
}
142142

143143
// Try the size hint.
@@ -154,12 +154,28 @@ impl Source for SourcesQueueOutput {
154154

155155
#[inline]
156156
fn channels(&self) -> ChannelCount {
157-
self.current.channels()
157+
// When current source is exhausted, peek at the next source's metadata
158+
if !self.current.is_exhausted() {
159+
self.current.channels()
160+
} else if let Some((next, _)) = self.input.next_sounds.lock().unwrap().first() {
161+
next.channels()
162+
} else {
163+
// Queue is empty - return silence metadata
164+
SILENCE_CHANNELS
165+
}
158166
}
159167

160168
#[inline]
161169
fn sample_rate(&self) -> SampleRate {
162-
self.current.sample_rate()
170+
// When current source is exhausted, peek at the next source's metadata
171+
if !self.current.is_exhausted() {
172+
self.current.sample_rate()
173+
} else if let Some((next, _)) = self.input.next_sounds.lock().unwrap().first() {
174+
next.sample_rate()
175+
} else {
176+
// Queue is empty - return silence metadata
177+
SILENCE_SAMPLE_RATE
178+
}
163179
}
164180

165181
#[inline]
@@ -221,7 +237,11 @@ impl SourcesQueueOutput {
221237
let mut next = self.input.next_sounds.lock().unwrap();
222238

223239
if next.is_empty() {
224-
let silence = Box::new(Zero::new_samples(nz!(1), nz!(44100), THRESHOLD)) as Box<_>;
240+
let silence = Box::new(Zero::new_samples(
241+
SILENCE_CHANNELS,
242+
SILENCE_SAMPLE_RATE,
243+
THRESHOLD,
244+
)) as Box<_>;
225245
if self.input.keep_alive_if_empty.load(Ordering::Acquire) {
226246
// Play a short silence in order to avoid spinlocking.
227247
(silence, None)
@@ -247,7 +267,6 @@ mod tests {
247267
use crate::source::Source;
248268

249269
#[test]
250-
#[ignore] // FIXME: samples rate and channel not updated immediately after transition
251270
fn basic() {
252271
let (tx, mut rx) = queue::queue(false);
253272

src/source/from_iter.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,8 @@ where
9292

9393
// Try the current `current_span_len`.
9494
if let Some(src) = &self.current_source {
95-
if let Some(val) = src.current_span_len() {
96-
if val != 0 {
97-
return Some(val);
98-
}
95+
if !src.is_exhausted() {
96+
return src.current_span_len();
9997
}
10098
}
10199

src/source/mod.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,24 @@ pub use self::noise::{Pink, WhiteUniform};
168168
/// channels can potentially change.
169169
///
170170
pub trait Source: Iterator<Item = Sample> {
171-
/// Returns the number of samples before the current span ends. `None` means "infinite" or
172-
/// "until the sound ends".
173-
/// Should never return 0 unless there's no more data.
171+
/// Returns the number of samples before the current span ends.
172+
///
173+
/// `None` means "infinite" or "until the sound ends". Sources that return `Some(x)` should
174+
/// return `Some(0)` if and only if when there's no more data.
174175
///
175176
/// After the engine has finished reading the specified number of samples, it will check
176177
/// whether the value of `channels()` and/or `sample_rate()` have changed.
178+
///
179+
/// Note: This returns the total span size, not the remaining samples. Use `Iterator::size_hint`
180+
/// to determine how many samples remain in the iterator.
177181
fn current_span_len(&self) -> Option<usize>;
178182

183+
/// Returns true if the source is exhausted (has no more samples available).
184+
#[inline]
185+
fn is_exhausted(&self) -> bool {
186+
self.current_span_len() == Some(0)
187+
}
188+
179189
/// Returns the number of channels. Channels are always interleaved.
180190
/// Should never be Zero
181191
fn channels(&self) -> ChannelCount;

src/source/repeat.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,25 +56,28 @@ where
5656
{
5757
#[inline]
5858
fn current_span_len(&self) -> Option<usize> {
59-
match self.inner.current_span_len() {
60-
Some(0) => self.next.current_span_len(),
61-
a => a,
59+
if self.inner.is_exhausted() {
60+
self.next.current_span_len()
61+
} else {
62+
self.inner.current_span_len()
6263
}
6364
}
6465

6566
#[inline]
6667
fn channels(&self) -> ChannelCount {
67-
match self.inner.current_span_len() {
68-
Some(0) => self.next.channels(),
69-
_ => self.inner.channels(),
68+
if self.inner.is_exhausted() {
69+
self.next.channels()
70+
} else {
71+
self.inner.channels()
7072
}
7173
}
7274

7375
#[inline]
7476
fn sample_rate(&self) -> SampleRate {
75-
match self.inner.current_span_len() {
76-
Some(0) => self.next.sample_rate(),
77-
_ => self.inner.sample_rate(),
77+
if self.inner.is_exhausted() {
78+
self.next.sample_rate()
79+
} else {
80+
self.inner.sample_rate()
7881
}
7982
}
8083

src/source/skip.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,22 @@ where
3939
return;
4040
}
4141

42-
let ns_per_sample: u128 =
43-
NS_PER_SECOND / input.sample_rate().get() as u128 / input.channels().get() as u128;
42+
let sample_rate = input.sample_rate().get() as u128;
43+
let channels = input.channels().get() as u128;
44+
45+
let samples_per_channel = duration.as_nanos() * sample_rate / NS_PER_SECOND;
46+
let samples_to_skip: u128 = samples_per_channel * channels;
4447

4548
// Check if we need to skip only part of the current span.
46-
if span_len as u128 * ns_per_sample > duration.as_nanos() {
47-
skip_samples(input, (duration.as_nanos() / ns_per_sample) as usize);
49+
if span_len as u128 > samples_to_skip {
50+
skip_samples(input, samples_to_skip as usize);
4851
return;
4952
}
5053

54+
duration -= Duration::from_nanos(
55+
(NS_PER_SECOND * span_len as u128 / channels / sample_rate) as u64,
56+
);
5157
skip_samples(input, span_len);
52-
53-
duration -= Duration::from_nanos((span_len * ns_per_sample as usize) as u64);
5458
}
5559
}
5660

0 commit comments

Comments
 (0)