loop: fix the crossfade seam's residual discontinuity, plus six review minors

Normalizes crossfadeWeight over crossfade-1 so the last rendered frame lands at exactly the incoming tap instead of a residual step; corrects the CLAUDE.md invariant and seam test to match. Shares lerpSource/crossfadedSource/maxCrossfade, fixes stale docs/constants, and clears crossfade on the loop-OFF gesture.
This commit is contained in:
2026-07-31 17:59:58 -04:00
parent 0fe4166d7d
commit 3cb22e984d
12 changed files with 172 additions and 83 deletions
@@ -12,6 +12,7 @@
#include <cstdint>
#include <vector>
#include "core/instrument/engine/loop/loop_span.h" // maxCrossfade (the shared drag-clamp bound)
#include "core/instrument/ui/envelope_edit.h" // nodeAtPoint / resolveNodeDrag
#include "core/instrument/ui/waveform_view.h" // waveformOverlayArea / markerAtPoint / snap
#include "shell/instrument/editor_internal.h"
@@ -21,6 +22,7 @@ namespace reasampler::vst {
using namespace reasampler::ui;
using namespace reasampler::instrument::ui;
using instrument::engine::loop::maxCrossfade;
bool ReaSamplerEditor::mouseDownWaveform(const FaceLayout& fl, int x, int y) {
const std::vector<AudioSample>& pcm = monoPcmFor(selectedId_);
@@ -54,7 +56,10 @@ bool ReaSamplerEditor::mouseDownWaveform(const FaceLayout& fl, int x, int y) {
const SetupMarkers m = pickedMarkers(frames);
// The crossfade handle first, and only when there IS a loop to fade: at a zero fade it
// sits exactly on the loop start, so it can only stay reachable by owning the top strip
// (waveform_view.h's handle-vs-column split) and being asked first.
// (waveform_view.h's handle-vs-column split) and being asked first. The same ambiguity
// recurs whenever ANY marker's frame lands on loopStart - crossfade (most plausibly the
// start marker dragged up against the fade edge), so this check has to run before the
// marker array below regardless of which marker the collision is with.
if (m.hasLoop &&
contains(markerHandleRect(overlay, frames, m.loopStart - m.crossfade), x, y)) {
beginMarkerDrag(WaveMarker::kLoopXfade, m, frames, x);
@@ -140,10 +145,9 @@ void ReaSamplerEditor::dragWaveform(const FaceLayout& fl, int x, int y) {
}
if (m.start < 0) m.start = 0;
if (m.start > frames - 1) m.start = frames - 1;
// Mirror resolveLoop's own bound so the handle can't be dragged somewhere the engine
// would silently clamp back: the fade reads the material ahead of the loop, and cannot
// outrun either that material or the loop itself.
m.crossfade = (std::min)(m.crossfade, (std::min)(m.loopStart, m.loopEnd - m.loopStart));
// Shares resolveLoop's own bound (loop_span.h's maxCrossfade) so the handle can't be
// dragged somewhere the engine would silently clamp back.
m.crossfade = (std::min)(m.crossfade, maxCrossfade(m.loopStart, m.loopEnd - m.loopStart));
if (m.crossfade < 0) m.crossfade = 0;
applyMarkers(m);
+4 -1
View File
@@ -225,7 +225,10 @@ void ReaSamplerEditor::applyMarkers(const SetupMarkers& m) {
loop.start = m.loopStart;
loop.end = m.loopEnd;
params_.loopOverride = loop;
params_.loopCrossfadeFrames = m.crossfade;
// OFF parks the crossfade at 0 too — loadSelection's own clear (a fresh capture has no
// loop to fade) is the same rule; leaving a stale length here would silently re-apply it
// (clamped) the next time a loop is dragged back in.
params_.loopCrossfadeFrames = loop.hasLoop ? m.crossfade : 0;
params_.startPoint = m.start;
}