Skip to content

Don't end a resampler chunk at a same-format span boundary - #908

Closed
benface wants to merge 3 commits into
RustAudio:masterfrom
benface:fix-resample-across-span-boundaries
Closed

Don't end a resampler chunk at a same-format span boundary#908
benface wants to merge 3 commits into
RustAudio:masterfrom
benface:fix-resample-across-span-boundaries

Conversation

@benface

@benface benface commented Jul 25, 2026

Copy link
Copy Markdown

Fixes #907.

Problem

fill_input_buffer clamps each read to current_span_len(), and resample_chunk then flags any short read as partial_len:

partial_len: (frames_in < needed_by_resampler).then_some(frames_in.raw()),

partial_len means "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 showed partial_len firing 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_len then 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:

span size before after
single span 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 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_format covers this shape but only asserts the output sample count, which is why it stayed green while the audio was being corrupted.

Scope

  • Bisected to ca72dac ("add dedicated input datastructure"). Released v0.22.2 predates the rubato resampler and is unaffected.
  • cargo test 71 passing, cargo fmt --check and cargo clippy clean.

I'm happy to adjust the approach — I realise this module is under active refactoring, so if the intended design is for fill_input_buffer to stay single-span and the padding to be handled downstream instead, I can rework it that way.

benface added 3 commits July 25, 2026 14:00
`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.
@yara-blue

Copy link
Copy Markdown
Member

Closing as a low effort - likely automated - contribution. I urge you to be more respectful of open source maintainers time in the future.

@yara-blue yara-blue closed this Jul 26, 2026
@benface

benface commented Jul 26, 2026

Copy link
Copy Markdown
Author

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?

@roderickvd

Copy link
Copy Markdown
Member

@benface we would welcome contributions that:

  1. read succinctly
  2. show understanding of the problem and solution
  3. apply best practices

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.

@benface

benface commented Jul 27, 2026

Copy link
Copy Markdown
Author

Appreciate it @roderickvd. I will try to find the time to do a proper fix.

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.

Resampler output depends on input span chunking — Vorbis-sized spans destroy the signal (regression on master, bisected to ca72dac)

3 participants