Home

dev / openkara

publicthedavidweng/OpenKara· sync paused
Code Branches Pull requestsIssuesInsights
main
Home Code PRsIssues

#376 Crossfade overlap loop desyncs its output cursors when the resampler returns short

closed

Opened by dev · yesterday

devopened this issueAuthor· yesterday

Line references are pinned to main at bac2f765.

Pre-existing on main. Not introduced by any of the refactor PRs (#367–#372) — those relocated the code without changing it, so do not bisect to them.

Where

src-tauri/src/audio/output/crossfade_render.rs L83-146 (the chunked overlap loop) and L167-190 (the tail fill that runs after it).

What goes wrong

Three cursors track the same region of the output buffer and advance by different amounts:

  • chunk_start advances by the requested chunk size: chunk_start += chunk_frames; (L141), where chunk_frames is min(remaining_overlap, CROSSFADE_SCRATCH_FRAMES) (L85-86).
  • rendered_output_frames and active.rendered_frames advance by the produced frame count: mix_frames = (out_rendered.min(inc_rendered)) / device_channels (L118), applied at L132 and L137.

mix_frames equals chunk_frames only when both resamplers return a full chunk for both streams. Whenever either returns short — the usual case for rate conversion, and guaranteed near the end of a source — the cursors diverge and stay diverged for the rest of the callback.

Three consequences follow:

  1. Unmixed hole. The mixing loop at L119-130 only touches frames [chunk_start, chunk_start + mix_frames). Frames [chunk_start + mix_frames, chunk_start + chunk_frames) were already written by the outgoing mix_stem_resampled call at L94-102 (it writes into output[chunk_start..chunk_start + chunk_frames], L91-92) and never get the equal-power gain applied — they play back at full outgoing level in the middle of a fade.
  2. Overwrite of committed samples. After the loop, the tail fill starts at rendered_output_frames: let remaining_buf = &mut output[rendered_output_frames * device_channels..]; (L167-168). Since rendered_output_frames <= chunk_start, that slice starts behind where the loop stopped writing, so the tail render replaces already-crossfaded samples with un-crossfaded outgoing audio.
  3. Promotion stalls. Completion is judged on a.rendered_frames >= a.total_frames (L148-151), and incoming_source_frame advances by inc_consumed (L138) independently. With rendered_frames lagging real output, the overlap takes longer than the configured duration; the loop only bails when inc_rendered == 0 && mix_frames == 0 (L143-145), so a persistently short resampler burns callbacks without progressing the fade.

Conditions

A crossfade where either track's sample_rate_hz differs from the device rate, so mix_stem_resampled routes through rubato (mix_bus.rs L19-33) and can return fewer frames than requested. Same-rate/same-rate crossfades — which is what the existing unit tests in this file cover — never diverge, which is why this is not caught today.

Effect

Audible glitching during rate-converted crossfades: an unfaded outgoing burst inside the overlap, a rewritten segment at the splice point, and an overlap whose duration does not match the configured one.

Not fixing here

Filed for tracking only.

kilo-code-bot[bot]commented· yesterday

Sign in to comment.

Linked pull requests

No linked pull requests yet.

This issue appears to be a duplicate of github.com/thedavidweng/…/375.

Crossfade gapless handoff skips this callback's fade gain, stepping the level mid-fade (#375)

Similarity score: 92%

This comment was generated by Kilo Auto-Triage.