Skip to content

Commit 16c90ba

Browse files
committed
refactor: SourcesQueueOutput silence and span logic
1 parent 5d305d1 commit 16c90ba

1 file changed

Lines changed: 67 additions & 88 deletions

File tree

src/queue.rs

Lines changed: 67 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use std::sync::atomic::{AtomicBool, Ordering};
55
use std::sync::{Arc, Mutex};
66
use std::time::Duration;
77

8-
use crate::source::{Empty, SeekError, Source, Zero};
8+
use dasp_sample::Sample as _;
9+
10+
use crate::source::{Empty, SeekError, Source};
911
use crate::Sample;
1012

1113
use crate::common::{ChannelCount, SampleRate};
@@ -36,7 +38,7 @@ pub fn queue(keep_alive_if_empty: bool) -> (Arc<SourcesQueueInput>, SourcesQueue
3638
signal_after_end: None,
3739
input: input.clone(),
3840
samples_consumed_in_span: 0,
39-
padding_samples_remaining: 0,
41+
silence_samples_remaining: 0,
4042
};
4143

4244
(input, output)
@@ -116,72 +118,55 @@ pub struct SourcesQueueOutput {
116118
// Track samples consumed in the current span to detect mid-span endings.
117119
samples_consumed_in_span: usize,
118120

119-
// When a source ends mid-frame, this counts how many silence samples to inject
120-
// to complete the frame before transitioning to the next source.
121-
padding_samples_remaining: usize,
122-
}
123-
124-
/// Returns a threshold span length that ensures frame alignment.
125-
///
126-
/// Spans must end on frame boundaries (multiples of channel count) to prevent
127-
/// channel misalignment. Returns ~512 samples rounded to the nearest frame.
128-
#[inline]
129-
fn threshold(channels: ChannelCount) -> usize {
130-
const BASE_SAMPLES: usize = 512;
131-
let ch = channels.get() as usize;
132-
BASE_SAMPLES.div_ceil(ch) * ch
121+
// This counts how many silence samples to inject when a source ends.
122+
silence_samples_remaining: usize,
133123
}
134124

135125
impl Source for SourcesQueueOutput {
136126
#[inline]
137127
fn current_span_len(&self) -> Option<usize> {
138128
if !self.current.is_exhausted() {
139-
return self.current.current_span_len();
140-
} else if self.input.keep_alive_if_empty.load(Ordering::Acquire)
141-
&& self.input.next_sounds.lock().unwrap().is_empty()
142-
{
143-
// Return what that Zero's current_span_len() will be: Some(threshold(channels)).
144-
return Some(threshold(self.current.channels()));
129+
self.current.current_span_len().or_else(|| {
130+
// If the current source doesn't report a span length, provide one that ensures
131+
// frame alignment. Returning None here could cause downstream sources to
132+
// incorrectly assume all later queued sources have the same sample rate and
133+
// channel count.
134+
Some(self.current.channels().get() as usize)
135+
})
136+
} else if self.input.keep_alive_if_empty.load(Ordering::Acquire) {
137+
// We will be playing frames of silence until the next sound is loaded.
138+
Some(self.current.channels().get() as usize)
139+
} else {
140+
// Queue is empty, no sources queued
141+
None
145142
}
146-
147-
None
148143
}
149144

150145
#[inline]
151146
fn channels(&self) -> ChannelCount {
152-
if !self.current.is_exhausted() {
153-
// Current source is active (producing samples)
154-
// - Initially: never (Empty is exhausted immediately)
155-
// - After append: the appended source while playing
156-
// - With keep_alive: Zero (silence) while playing
157-
self.current.channels()
158-
} else if let Some((next, _)) = self.input.next_sounds.lock().unwrap().front() {
159-
// Current source exhausted, peek at next queued source
160-
// This is critical: UniformSourceIterator queries metadata during append,
161-
// before any samples are pulled. We must report the next source's metadata.
162-
next.channels()
163-
} else {
164-
// Queue is empty, no sources queued
165-
// - Initially: Empty
166-
// - With keep_alive: exhausted Zero between silence chunks (matches Empty)
167-
// - Without keep_alive: Empty (will end on next())
168-
self.current.channels()
147+
if self.current.is_exhausted() {
148+
if let Some((next, _)) = self.input.next_sounds.lock().unwrap().front() {
149+
// Current source exhausted, peek at next queued source
150+
// This is critical: UniformSourceIterator queries metadata during append,
151+
// before any samples are pulled. We must report the next source's metadata.
152+
return next.channels();
153+
}
169154
}
155+
156+
self.current.channels()
170157
}
171158

172159
#[inline]
173160
fn sample_rate(&self) -> SampleRate {
174-
if !self.current.is_exhausted() {
175-
// Current source is active (producing samples)
176-
self.current.sample_rate()
177-
} else if let Some((next, _)) = self.input.next_sounds.lock().unwrap().front() {
178-
// Current source exhausted, peek at next queued source
179-
// This prevents wrong resampling setup in UniformSourceIterator
180-
next.sample_rate()
181-
} else {
182-
// Queue is empty, no sources queued
183-
self.current.sample_rate()
161+
if self.current.is_exhausted() {
162+
if let Some((next, _)) = self.input.next_sounds.lock().unwrap().front() {
163+
// Current source exhausted, peek at next queued source
164+
// This prevents wrong resampling setup in UniformSourceIterator
165+
return next.sample_rate();
166+
}
184167
}
168+
169+
self.current.sample_rate()
185170
}
186171

187172
#[inline]
@@ -211,34 +196,45 @@ impl Iterator for SourcesQueueOutput {
211196
fn next(&mut self) -> Option<Self::Item> {
212197
loop {
213198
// If we're padding to complete a frame, return silence.
214-
if self.padding_samples_remaining > 0 {
215-
self.padding_samples_remaining -= 1;
216-
return Some(0.0);
199+
if self.silence_samples_remaining > 0 {
200+
self.silence_samples_remaining -= 1;
201+
return Some(Sample::EQUILIBRIUM);
217202
}
218203

219204
// Basic situation that will happen most of the time.
220205
if let Some(sample) = self.current.next() {
221-
self.samples_consumed_in_span += 1;
206+
self.samples_consumed_in_span = self
207+
.samples_consumed_in_span
208+
.checked_add(1)
209+
.unwrap_or_else(|| {
210+
self.samples_consumed_in_span % self.channels().get() as usize + 1
211+
});
222212
return Some(sample);
223213
}
224214

225-
// Source ended - check if we ended mid-frame and need padding.
226-
let channels = self.current.channels().get() as usize;
227-
let incomplete_frame_samples = self.samples_consumed_in_span % channels;
228-
if incomplete_frame_samples > 0 {
229-
// We're mid-frame - need to pad with silence to complete it.
230-
self.padding_samples_remaining = channels - incomplete_frame_samples;
231-
// Reset counter now since we're transitioning to a new span.
232-
self.samples_consumed_in_span = 0;
233-
// Continue loop - next iteration will inject silence.
234-
continue;
215+
// Current source is exhausted - check if we ended mid-frame and need padding.
216+
if self.samples_consumed_in_span > 0 {
217+
let channels = self.current.channels().get() as usize;
218+
let incomplete_frame_samples = self.samples_consumed_in_span % channels;
219+
if incomplete_frame_samples > 0 {
220+
// We're mid-frame - need to pad with silence to complete it.
221+
self.silence_samples_remaining = channels - incomplete_frame_samples;
222+
// Reset counter now since we're transitioning to a new span.
223+
self.samples_consumed_in_span = 0;
224+
// Continue loop - next iterations will inject silence.
225+
continue;
226+
}
235227
}
236228

237-
// Reset counter and move to next sound.
238-
// In order to avoid inlining this expensive operation, the code is in another function.
239-
self.samples_consumed_in_span = 0;
229+
// Move to next sound, play silence, or end.
230+
// In order to avoid inlining that expensive operation, the code is in another function.
240231
if self.go_next().is_err() {
241-
return None;
232+
if self.input.keep_alive_if_empty.load(Ordering::Acquire) {
233+
self.silence_samples_remaining = self.current.channels().get() as usize;
234+
continue;
235+
} else {
236+
return None;
237+
}
242238
}
243239
}
244240
}
@@ -251,7 +247,7 @@ impl Iterator for SourcesQueueOutput {
251247

252248
impl SourcesQueueOutput {
253249
// Called when `current` is empty, and we must jump to the next element.
254-
// Returns `Ok` if the sound should continue playing, or an error if it should stop.
250+
// Returns `Ok` if there is another sound should continue playing, or `Err` when there is not.
255251
//
256252
// This method is separate so that it is not inlined.
257253
fn go_next(&mut self) -> Result<(), ()> {
@@ -261,23 +257,7 @@ impl SourcesQueueOutput {
261257

262258
let (next, signal_after_end) = {
263259
let mut next = self.input.next_sounds.lock().unwrap();
264-
265-
if let Some(next) = next.pop_front() {
266-
next
267-
} else {
268-
let channels = self.current.channels();
269-
let silence = Box::new(Zero::new_samples(
270-
channels,
271-
self.current.sample_rate(),
272-
threshold(channels),
273-
)) as Box<_>;
274-
if self.input.keep_alive_if_empty.load(Ordering::Acquire) {
275-
// Play a short silence in order to avoid spinlocking.
276-
(silence, None)
277-
} else {
278-
return Err(());
279-
}
280-
}
260+
next.pop_front().ok_or(())?
281261
};
282262

283263
self.current = next;
@@ -350,7 +330,6 @@ mod tests {
350330
}
351331

352332
#[test]
353-
#[ignore] // TODO: not yet implemented
354333
fn no_delay_when_added() {
355334
let (tx, mut rx) = queue::queue(true);
356335

0 commit comments

Comments
 (0)