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_startadvances by the requested chunk size:chunk_start += chunk_frames;(L141), wherechunk_framesismin(remaining_overlap, CROSSFADE_SCRATCH_FRAMES)(L85-86).rendered_output_framesandactive.rendered_framesadvance 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:
- 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 outgoingmix_stem_resampledcall at L94-102 (it writes intooutput[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. - 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). Sincerendered_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. - Promotion stalls. Completion is judged on
a.rendered_frames >= a.total_frames(L148-151), andincoming_source_frameadvances byinc_consumed(L138) independently. Withrendered_frameslagging real output, the overlap takes longer than the configured duration; the loop only bails wheninc_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.