Don't end a resampler chunk at a same-format span boundary - #908
Conversation
`fill_input_buffer` clamped each read to `current_span_len()`, and
`resample_chunk` then flagged any short read as `partial_len` — which
rubato treats as a short/final chunk and zero-pads. Decoders report one
span per decoded packet, so a single continuous stream arrives as many
small spans and a discontinuity was injected at every one of them.
Vorbis is the clearest case: it switches to short blocks (commonly 128
or 256 samples) around transients, so real files move in and out of the
degraded regime depending on programme material.
Measured on a 440 Hz stereo sine resampled 48 kHz -> 44.1 kHz, as the
fraction of output energy away from the fundamental:
span size before after
single 0.054 % 0.054 %
4096 frames 0.054 % 0.054 %
2048 frames 0.054 % 0.054 %
1024 frames 1.02 % 0.054 %
512 frames 23.79 % 0.054 %
256 frames 90.96 % 0.054 %
128 frames 91.00 % 0.054 %
Output is bit-identical to the single-span case at every span size once
the chunk is no longer cut at same-format boundaries. Output length also
stops drifting (44110/44156/44250 -> a consistent 44124).
A span boundary only matters to the resampler when the sample rate or
channel count actually changes, so keep pulling input across boundaries
and stop only on a genuine format change or end of stream. `partial_len`
now fires only at real end-of-stream.
Adds a regression test asserting the invariant directly: output must stay
spectrally pure regardless of span size. It fails on the parent commit
(1024-frame spans already breach the threshold) and passes here.
Bisected to ca72dac ("add dedicated input datastructure"); the released
v0.22.2 predates the rubato resampler and is unaffected.
Fixes RustAudio#907.
`Sample` is `f32` by default and `f64` under the `64bit` feature, so hardcoding `f32` in the signal generator broke `--all-features` builds. Generate the tone in `Sample` instead. Verified with the CI command under both feature sets.
The f64 accumulation cast is a no-op with `64bit` enabled but required without it, so annotate rather than drop it.
|
Closing as a low effort - likely automated - contribution. I urge you to be more respectful of open source maintainers time in the future. |
|
Definitely automated @yara-blue, and I apologize for that. That being said, it fixes a real issue. Is the issue not clear, the solution heavy-handed, or something else? |
|
@benface we would welcome contributions that:
Number one I think is clear. Yes I believe this solves a real issue, but it doesn't solve it fully. There's another pre-existing bug there layered on top with span position tracking - can you see it? Further, Rodio already has signal generators that you could use to simplify and improve much needed readability of the test. Finally, that test does not show the case where span boundaries actually change channel count or sample rate. I hope this helps guide your future contributions. Use AI as your tool but own the result yourself. |
|
Appreciate it @roderickvd. I will try to find the time to do a proper fix. |
Fixes #907.
Problem
fill_input_bufferclamps each read tocurrent_span_len(), andresample_chunkthen flags any short read aspartial_len:partial_lenmeans "short/final chunk" and rubato zero-pads it. Decoders report one span per decoded packet, so a single continuous stream arrives as many small spans — and a discontinuity was being injected at every boundary. Instrumenting a 0.5 s resample showedpartial_lenfiring on 25 of 46 chunk calls.Vorbis is the clearest case: it switches to short blocks (commonly 128 or 256 samples) around transients, so real files move in and out of the degraded regime depending on programme material. That matches the "only some files, only on some devices" character of #367 and #584.
Fix
A span boundary only matters to the resampler when the sample rate or channel count actually changes. So keep pulling input across boundaries, and stop only on a genuine format change or end of stream.
partial_lenthen fires only at real end-of-stream, which is what it means.Measurements
440 Hz stereo sine, 48 kHz → 44.1 kHz, measuring the fraction of output energy away from the fundamental (Goertzel on the left channel) while varying only the span size:
Output is bit-identical to the single-span case at every span size once chunks are no longer cut at same-format boundaries. Output length also stops drifting with span size (44110 / 44156 / 44250 → a consistent 44124).
Confirmed by ear as well: the downstream game where this surfaced (48 kHz Ogg Vorbis on a 44.1 kHz output device, previously audible hiss/stutter) plays clean with this patch.
Test
Adds
small_spans_do_not_degrade_resampled_signal, asserting the invariant directly — output must stay spectrally pure regardless of span size. It fails on the parent commit (1024-frame spans already breach the threshold) and passes here.Worth noting the existing
test_span_boundary_same_formatcovers this shape but only asserts the output sample count, which is why it stayed green while the audio was being corrupted.Scope
v0.22.2predates the rubato resampler and is unaffected.cargo test71 passing,cargo fmt --checkandcargo clippyclean.I'm happy to adjust the approach — I realise this module is under active refactoring, so if the intended design is for
fill_input_bufferto stay single-span and the padding to be handled downstream instead, I can rework it that way.