Skip to content

fix: correct Source trait semantics and span tracking bugs#831

Open
roderickvd wants to merge 10 commits intomasterfrom
fix/span-fixes-clean
Open

fix: correct Source trait semantics and span tracking bugs#831
roderickvd wants to merge 10 commits intomasterfrom
fix/span-fixes-clean

Conversation

@roderickvd
Copy link
Member

@roderickvd roderickvd commented Jan 14, 2026

Clarify Source::current_span_len() returns total span length (not remaining), while size_hint() returns remaining samples of the entire iterator. Fix multiple bugs in span boundary detection, seeking, and iterator implementations.

Opportunistic fixes:

  • fix division by zero, off-by-one error, and zero case handling
  • prevent counter overflows
  • optimize vector allocations

Mid-span seeking: best-effort tracking after try_seek() to detect span boundaries when seeking lands mid-span.

Fixes #691

Clarify Source::current_span_len() returns total span length (not remaining),
while size_hint() returns remaining samples. Fix multiple bugs in span boundary
detection, seeking, and iterator implementations.

Opportunistic fixes:
- fix division by zero, off-by-one error, and zero case handling
- prevent counter overflows
- optimize vector allocations

Mid-span seeking: best-effort tracking after try_seek() to detect span boundaries
when seeking lands mid-span.

Fixes #691
@roderickvd roderickvd requested a review from yara-blue January 14, 2026 23:11
@roderickvd
Copy link
Member Author

I did not fix the decoders because I had already done so with the infamous #786.

@roderickvd roderickvd force-pushed the fix/span-fixes-clean branch from 16c90ba to 7538971 Compare January 22, 2026 21:33
Parameter updates (channels/sample_rate) occur only at span boundaries
or during post-seek detection. Reset counters and enter a detection mode
on try_seek (Duration::ZERO is treated as start-of-span).
Use detect_span_boundary and reset_seek_span_tracking to detect span
boundaries and parameter changes.
@roderickvd
Copy link
Member Author

Verifying span boundary behavior in the other sources, latest commits I also added:

  1. stereo and multi-channel audio support to BltFilter
  2. mid-frame stream ending in ChannelVolume (akin to what we had fixed in queue)

w.r.t. the first, I wonder how many people use that, given it only ever worked correctly on mono sources.

@roderickvd
Copy link
Member Author

I want to take a final look at a couple of other sources, that may need to deal with mid-stream endings as well. In the meantime, feel free to review what we've got already, as I might apply the same patterns that we've already got.

@roderickvd
Copy link
Member Author

This thing is getting big but I think I've got it down now. Took me a good week's worth of evenings!

I've left the intermediate commits intact so you can see my journey. I've been going back-and-forth to defend against the fact that when source iterators end mid-frame, multi-channel audio and queuing blows up.

First I added defensive measures to "filter" sources that depend on proper frame alignment. Then, I removed it from the filters in favor of placing it with the "producer" sources: the decoders, mostly, and documenting the frame-alignment requirement.

I think it's better this way.


// Check if duration has expired.
if self.remaining_duration < self.duration_per_sample {
self.silence_samples_remaining =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this breaks the promise of take duration, should it not instead pass on a modified last span accounting for the target duration?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a similar thing could work for lineargainramp (there we would cut a span into two)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good point.

Copy link
Member Author

@roderickvd roderickvd Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, thinking about it more, is the contract for TakeDuration that get you get:
a. precisely the duration you've asked for (zero-padded if necessary)
b. up to the duration you asked for, or less if the source is exhausted before that?

I think it's b and then this is good to go. This implementation even contains a test that we never end mid-frame / always emit a full frame.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say b, since you can compose a by applying TakeDuration on the rodio equivalent of source.chain(silence).

Copy link
Member

@yara-blue yara-blue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scary how broken this secretly was. Thanks for going through the pain of doing this. It does convince me about some of the ideas in rodio-experiments.

@roderickvd
Copy link
Member Author

roderickvd commented Jan 27, 2026

Indeed! And the sad part is that I expect that 90% of use cases will never use it.

Two things I can think of:

  1. Expedite the introduction of your new source architecture: ensure that sources output to a unified sample rate, always.
  2. Adding a stable_parameters or similar/reciprocal method to the decoder builder, to configure eagerly reporting None as span length even if a decoder could theoretically have its parameters changed.

Point 2 is easily done and I had already put in... well you know.

Thanks for your review.

@yara-blue
Copy link
Member

Those perf. changes are indeed not insignificant, bummer. I would like to merge this sooner rather then later though (as it's not really isolated). How about we cut a release this week and then merge this after? That gives us until the next release to provide more performance either through 1 or 2.

I feel confident we can land the experimental source architecture within half a year, so before the next release. I'm using it right now in a side project and I'm really happy with how that code looks.

example:
https://github.com/yara-blue/mpdhaj/blob/4f781b71558add4fbe6a503991e58ee86727d475/src/player.rs#L136

@roderickvd
Copy link
Member Author

Those perf. changes are indeed not insignificant, bummer. I would like to merge this sooner rather then later though (as it's not really isolated). How about we cut a release this week and then merge this after? That gives us until the next release to provide more performance either through 1 or 2.

OK for me.


let sample_rate = input.sample_rate();
let applier = formula.to_applier(sample_rate.get());

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO(yara): Copy the new multichannel logic to the fixed sources.

samples_counted: 0,
cached_span_len: None,
last_sample_rate: sample_rate,
last_channels: channels,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these params (cached_span_len, last_sample_rate and last_channels) seem to be shared by most sources now. Is this something we should extract to a struct?

like:

struct PrevParams {
    span_len,
    sample_rate,
    channel_count,
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe even params, then the detect_span_boundary could maybe take, prev and current both of type Params.

Going even further, a private extension trait implemented for everything Source could provide a .params(&self) -> Params method


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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line does not add much for me (I dont know what dual-mode tracking means until I've read the function body)

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about we return a struct with fields named: at_span_boundary and parameters_changed or something along does lines.

pub(crate) mod test_utils {
use super::*;

/// Test helper source that can end mid-frame for testing incomplete frame handling.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but we do not support ending mid-frame?
how this is different from SamplesBuffer?

@@ -1,20 +1,25 @@
use std::time::Duration;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done till here. Might continue tomorrow

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

status update: since this overhauls almost every source I'm taking this one slow. I'm adding the source to rodio-experiments before I handle it, that way I can get read up on the original impl before I see this (makes an in depth review easier)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mhm this behavior was always redundant (inf zero + take dur). Especially now we are size hinting all the other generators as infinite we should consider bringing this in line.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of scope for this PR though

sample_rate: SampleRate,
num_samples: usize,
) -> Self {
assert_eq!(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather have this return a Result instead, it should then just have a single struct as error type.

}));

let output = MixerSource {
current_sources: Vec::with_capacity(16),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we ideally never allocate here. The 16 sources give us some headway. Its not the best solution, that would probably be tracking capacity and allocating a new Vec in the MixerHandle and then taking that new allocation together with the new Source.

But that is definitely out of the scope of this PR!!

For now maybe leave the 16 sources pre-allocated.

sample_count: 0,
current_channel: 0,
still_pending: Vec::new(),
pending_rx: rx,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this changed on main (the still pending list got removed entirely)

}
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

take care of on main as well

}

#[test]
fn span_ending_mid_frame() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this test gone? It seems quiet useful


#[cfg(any(
feature = "claxon",
feature = "minimp3",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is minimp3 removed here?

Copy link
Member

@yara-blue yara-blue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yay done reviewing :)

Should be a simple rebase to get main up to date again. I think there are some refactor opportunities and I left some questions here and there.

Let me know when it's ready for a second pass

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Seek breaks span

2 participants