Skip to content

Commit a1a1219

Browse files
authored
fix: empty source exhaustion (#822)
* fix: update queue metadata after append * refactor: improve Empty and Zero sources
1 parent 639d86f commit a1a1219

File tree

4 files changed

+128
-47
lines changed

4 files changed

+128
-47
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
### Added
1313

14-
- `Chirp` now implements `Iterator::size_hint` and `ExactSizeIterator`.
14+
- `Chirp` and `Empty` now implement `Iterator::size_hint` and `ExactSizeIterator`.
1515
- `SamplesBuffer` now implements `ExactSizeIterator`.
16+
- `Zero` now implements `try_seek`, `total_duration` and `Copy`.
1617
- Added `Source::is_exhausted()` helper method to check if a source has no more samples.
1718
- Added `Red` noise generator that is more practical than `Brownian` noise.
1819
- Added `std_dev()` to `WhiteUniform` and `WhiteTriangular`.
@@ -38,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3839
- Improved precision in `SkipDuration` to avoid off-by-a-few-samples errors.
3940
- Fixed channel misalignment in queue with non-power-of-2 channel counts (e.g., 6 channels) by ensuring frame-aligned span lengths.
4041
- Fixed channel misalignment when sources end before their promised span length by padding with silence to complete frames.
42+
- Fixed `Empty` source to properly report exhaustion.
43+
- Fixed `Zero::current_span_len` returning remaining samples instead of span length.
4144

4245
### Changed
4346
- `output_to_wav` renamed to `wav_to_file` and now takes ownership of the `Source`.

src/queue.rs

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use std::sync::atomic::{AtomicBool, Ordering};
44
use std::sync::{Arc, Mutex};
55
use std::time::Duration;
66

7-
use crate::math::nz;
87
use crate::source::{Empty, SeekError, Source, Zero};
98
use crate::Sample;
109

@@ -121,9 +120,6 @@ pub struct SourcesQueueOutput {
121120
padding_samples_remaining: usize,
122121
}
123122

124-
const SILENCE_SAMPLE_RATE: SampleRate = nz!(44100);
125-
const SILENCE_CHANNELS: ChannelCount = nz!(1);
126-
127123
/// Returns a threshold span length that ensures frame alignment.
128124
///
129125
/// Spans must end on frame boundaries (multiples of channel count) to prevent
@@ -173,27 +169,38 @@ impl Source for SourcesQueueOutput {
173169

174170
#[inline]
175171
fn channels(&self) -> ChannelCount {
176-
// When current source is exhausted, peek at the next source's metadata
177172
if !self.current.is_exhausted() {
173+
// Current source is active (producing samples)
174+
// - Initially: never (Empty is exhausted immediately)
175+
// - After append: the appended source while playing
176+
// - With keep_alive: Zero (silence) while playing
178177
self.current.channels()
179178
} else if let Some((next, _)) = self.input.next_sounds.lock().unwrap().first() {
179+
// Current source exhausted, peek at next queued source
180+
// This is critical: UniformSourceIterator queries metadata during append,
181+
// before any samples are pulled. We must report the next source's metadata.
180182
next.channels()
181183
} else {
182-
// Queue is empty - return silence metadata
183-
SILENCE_CHANNELS
184+
// Queue is empty, no sources queued
185+
// - Initially: Empty
186+
// - With keep_alive: exhausted Zero between silence chunks (matches Empty)
187+
// - Without keep_alive: Empty (will end on next())
188+
self.current.channels()
184189
}
185190
}
186191

187192
#[inline]
188193
fn sample_rate(&self) -> SampleRate {
189-
// When current source is exhausted, peek at the next source's metadata
190194
if !self.current.is_exhausted() {
195+
// Current source is active (producing samples)
191196
self.current.sample_rate()
192197
} else if let Some((next, _)) = self.input.next_sounds.lock().unwrap().first() {
198+
// Current source exhausted, peek at next queued source
199+
// This prevents wrong resampling setup in UniformSourceIterator
193200
next.sample_rate()
194201
} else {
195-
// Queue is empty - return silence metadata
196-
SILENCE_SAMPLE_RATE
202+
// Queue is empty, no sources queued
203+
self.current.sample_rate()
197204
}
198205
}
199206

@@ -276,10 +283,11 @@ impl SourcesQueueOutput {
276283
let mut next = self.input.next_sounds.lock().unwrap();
277284

278285
if next.is_empty() {
286+
let channels = self.current.channels();
279287
let silence = Box::new(Zero::new_samples(
280-
SILENCE_CHANNELS,
281-
SILENCE_SAMPLE_RATE,
282-
threshold(SILENCE_CHANNELS),
288+
channels,
289+
self.current.sample_rate(),
290+
threshold(channels),
283291
)) as Box<_>;
284292
if self.input.keep_alive_if_empty.load(Ordering::Acquire) {
285293
// Play a short silence in order to avoid spinlocking.
@@ -381,6 +389,40 @@ mod tests {
381389
assert_eq!(rx.next(), Some(-10.0));
382390
}
383391

392+
#[test]
393+
fn append_updates_metadata() {
394+
for keep_alive in [false, true] {
395+
let (tx, rx) = queue::queue(keep_alive);
396+
assert_eq!(
397+
rx.channels(),
398+
nz!(1),
399+
"Initial channels should be 1 (keep_alive={keep_alive})"
400+
);
401+
assert_eq!(
402+
rx.sample_rate(),
403+
nz!(48000),
404+
"Initial sample rate should be 48000 (keep_alive={keep_alive})"
405+
);
406+
407+
tx.append(SamplesBuffer::new(
408+
nz!(2),
409+
nz!(44100),
410+
vec![0.1, 0.2, 0.3, 0.4],
411+
));
412+
413+
assert_eq!(
414+
rx.channels(),
415+
nz!(2),
416+
"Channels should update to 2 (keep_alive={keep_alive})"
417+
);
418+
assert_eq!(
419+
rx.sample_rate(),
420+
nz!(44100),
421+
"Sample rate should update to 44100 (keep_alive={keep_alive})"
422+
);
423+
}
424+
}
425+
384426
#[test]
385427
fn span_ending_mid_frame() {
386428
let mut test_source1 = TestSource::new(&[0.1, 0.2, 0.1, 0.2, 0.1])

src/source/empty.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,15 @@ use crate::math::nz;
66
use crate::{Sample, Source};
77

88
/// An empty source.
9-
#[derive(Debug, Copy, Clone)]
10-
pub struct Empty();
11-
12-
impl Default for Empty {
13-
#[inline]
14-
fn default() -> Self {
15-
Self::new()
16-
}
17-
}
9+
#[derive(Debug, Default, Copy, Clone)]
10+
pub struct Empty;
1811

1912
impl Empty {
2013
/// An empty source that immediately ends without ever returning a sample to
2114
/// play
2215
#[inline]
23-
pub fn new() -> Empty {
24-
Empty()
16+
pub fn new() -> Self {
17+
Self
2518
}
2619
}
2720

@@ -32,12 +25,19 @@ impl Iterator for Empty {
3225
fn next(&mut self) -> Option<Self::Item> {
3326
None
3427
}
28+
29+
#[inline]
30+
fn size_hint(&self) -> (usize, Option<usize>) {
31+
(0, Some(0))
32+
}
3533
}
3634

35+
impl ExactSizeIterator for Empty {}
36+
3737
impl Source for Empty {
3838
#[inline]
3939
fn current_span_len(&self) -> Option<usize> {
40-
None
40+
Some(0)
4141
}
4242

4343
#[inline]
@@ -52,7 +52,7 @@ impl Source for Empty {
5252

5353
#[inline]
5454
fn total_duration(&self) -> Option<Duration> {
55-
Some(Duration::new(0, 0))
55+
Some(Duration::ZERO)
5656
}
5757

5858
#[inline]

src/source/zero.rs

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,46 @@
11
use std::time::Duration;
22

3+
use dasp_sample::Sample as DaspSample;
4+
35
use super::SeekError;
46
use crate::common::{ChannelCount, SampleRate};
57
use crate::{Sample, Source};
68

79
/// An source that produces samples with value zero (silence). Depending on if
810
/// it where created with [`Zero::new`] or [`Zero::new_samples`] it can be never
911
/// ending or finite.
10-
#[derive(Clone, Debug)]
12+
#[derive(Copy, Clone, Debug)]
1113
pub struct Zero {
1214
channels: ChannelCount,
1315
sample_rate: SampleRate,
14-
num_samples: Option<usize>,
16+
total_samples: Option<usize>,
17+
position: usize,
1518
}
1619

1720
impl Zero {
1821
/// Create a new source that never ends and produces total silence.
1922
#[inline]
20-
pub fn new(channels: ChannelCount, sample_rate: SampleRate) -> Zero {
21-
Zero {
23+
pub fn new(channels: ChannelCount, sample_rate: SampleRate) -> Self {
24+
Self {
2225
channels,
2326
sample_rate,
24-
num_samples: None,
27+
total_samples: None,
28+
position: 0,
2529
}
2630
}
31+
2732
/// Create a new source that never ends and produces total silence.
2833
#[inline]
2934
pub fn new_samples(
3035
channels: ChannelCount,
3136
sample_rate: SampleRate,
3237
num_samples: usize,
33-
) -> Zero {
34-
Zero {
38+
) -> Self {
39+
Self {
3540
channels,
3641
sample_rate,
37-
num_samples: Some(num_samples),
42+
total_samples: Some(num_samples),
43+
position: 0,
3844
}
3945
}
4046
}
@@ -44,23 +50,33 @@ impl Iterator for Zero {
4450

4551
#[inline]
4652
fn next(&mut self) -> Option<Self::Item> {
47-
if let Some(num_samples) = self.num_samples {
48-
if num_samples > 0 {
49-
self.num_samples = Some(num_samples - 1);
50-
Some(0.0)
53+
if let Some(total_samples) = self.total_samples {
54+
if self.position < total_samples {
55+
self.position += 1;
5156
} else {
52-
None
57+
return None;
58+
}
59+
}
60+
61+
Some(Sample::EQUILIBRIUM)
62+
}
63+
64+
#[inline]
65+
fn size_hint(&self) -> (usize, Option<usize>) {
66+
match self.total_samples {
67+
Some(total_samples) => {
68+
let remaining = total_samples - self.position;
69+
(remaining, Some(remaining))
5370
}
54-
} else {
55-
Some(0.0)
71+
None => (usize::MAX, None),
5672
}
5773
}
5874
}
5975

6076
impl Source for Zero {
6177
#[inline]
6278
fn current_span_len(&self) -> Option<usize> {
63-
self.num_samples
79+
self.total_samples
6480
}
6581

6682
#[inline]
@@ -73,13 +89,33 @@ impl Source for Zero {
7389
self.sample_rate
7490
}
7591

76-
#[inline]
7792
fn total_duration(&self) -> Option<Duration> {
78-
None
93+
self.total_samples.map(|total| {
94+
let sample_rate = self.sample_rate.get() as u64;
95+
let frames = total / self.channels.get() as usize;
96+
let secs = frames as u64 / sample_rate;
97+
let nanos = ((frames as u64 % sample_rate) * 1_000_000_000) / sample_rate;
98+
Duration::new(secs, nanos as u32)
99+
})
79100
}
80101

81-
#[inline]
82-
fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> {
102+
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
103+
if let (Some(total_samples), Some(total_duration)) =
104+
(self.total_samples, self.total_duration())
105+
{
106+
let mut target = pos;
107+
if target > total_duration {
108+
target = total_duration;
109+
}
110+
111+
let target_samples = (target.as_secs_f32()
112+
* self.sample_rate.get() as f32
113+
* self.channels.get() as f32) as usize;
114+
let target_samples = target_samples.min(total_samples);
115+
116+
self.position = target_samples;
117+
}
118+
83119
Ok(())
84120
}
85121
}

0 commit comments

Comments
 (0)