fix: correct Source trait semantics and span tracking bugs#831
fix: correct Source trait semantics and span tracking bugs#831roderickvd wants to merge 10 commits intomasterfrom
Conversation
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
|
I did not fix the decoders because I had already done so with the infamous #786. |
16c90ba to
7538971
Compare
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.
|
Verifying span boundary behavior in the other sources, latest commits I also added:
w.r.t. the first, I wonder how many people use that, given it only ever worked correctly on mono sources. |
|
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. |
Document that Sources must emit complete frames and pad with silence when ending mid-frame, and ensure that decoders do so.
|
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 = |
There was a problem hiding this comment.
this breaks the promise of take duration, should it not instead pass on a modified last span accounting for the target duration?
There was a problem hiding this comment.
a similar thing could work for lineargainramp (there we would cut a span into two)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I would say b, since you can compose a by applying TakeDuration on the rodio equivalent of source.chain(silence).
|
Indeed! And the sad part is that I expect that 90% of use cases will never use it. Two things I can think of:
Point 2 is easily done and I had already put in... well you know. Thanks for your review. |
|
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: |
OK for me. |
|
|
||
| let sample_rate = input.sample_rate(); | ||
| let applier = formula.to_applier(sample_rate.get()); | ||
|
|
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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,
}There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
but we do not support ending mid-frame?
how this is different from SamplesBuffer?
| @@ -1,20 +1,25 @@ | |||
| use std::time::Duration; | |||
There was a problem hiding this comment.
done till here. Might continue tomorrow
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
out of scope for this PR though
| sample_rate: SampleRate, | ||
| num_samples: usize, | ||
| ) -> Self { | ||
| assert_eq!( |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
this changed on main (the still pending list got removed entirely)
| } | ||
| } | ||
| } | ||
|
|
| } | ||
|
|
||
| #[test] | ||
| fn span_ending_mid_frame() { |
There was a problem hiding this comment.
why is this test gone? It seems quiet useful
|
|
||
| #[cfg(any( | ||
| feature = "claxon", | ||
| feature = "minimp3", |
yara-blue
left a comment
There was a problem hiding this comment.
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
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:
Mid-span seeking: best-effort tracking after try_seek() to detect span boundaries when seeking lands mid-span.
Fixes #691