loop: crossfade the Gate sustain seam, and unshadow the loop handles that made loop points look gone

This commit is contained in:
2026-07-31 17:36:36 -04:00
parent a90ccd9a00
commit 0fe4166d7d
25 changed files with 991 additions and 64 deletions
+21 -6
View File
@@ -17,6 +17,7 @@
#include "core/ui/bank_grid.h" // ThumbnailKey / thumbnailKeyString (the pure key)
#include "core/util/file_bytes.h" // shared whole-file loader
#include "ext_keys.h"
#include "core/instrument/engine/loop/loop_span.h" // defaultLoopBounds (the shared ghost span)
#include "core/instrument/ui/browser_scroll.h" // nameMatchesQuery (type-to-filter)
#include "shell/instrument/reaper_bridge.h"
#include "shell/instrument/reasampler_processor.h"
@@ -31,6 +32,8 @@ using capture::WavLayout;
using capture::extractFloatFrames;
using capture::parseWavLayout;
using capture::resolveBankFile;
using instrument::engine::loop::LoopBounds;
using instrument::engine::loop::defaultLoopBounds;
using instrument::ui::nameMatchesQuery;
using ui::ThumbnailKey;
using ui::thumbnailKeyString;
@@ -160,10 +163,12 @@ void ReaSamplerEditor::loadSelection(const std::string& id) {
// loaded, so a load swaps the sound and keeps the settings. The three CAPTURE-ANCHORED
// overrides are: a root, a loop span and a start frame all name positions in the
// OUTGOING capture and mean nothing in the new one, so they clear and the new capture
// plays from its own bank intrinsics.
// plays from its own bank intrinsics. The loop crossfade goes with the span it belongs
// to — it is a length in the outgoing capture's frames.
selectedId_ = id;
params_.rootOverride.reset();
params_.loopOverride.reset();
params_.loopCrossfadeFrames = 0;
params_.startPoint.reset();
commitAndReload();
}
@@ -196,10 +201,17 @@ ReaSamplerEditor::SetupMarkers ReaSamplerEditor::pickedMarkers(std::int64_t fram
m.loopEnd = params_.loopOverride->end;
}
if (params_.startPoint) m.start = *params_.startPoint;
// Default an unset loop's end to the sample length so the loop markers have somewhere sane
// to sit before the user drags (loopStart stays 0). The "no loop" state is m.hasLoop==false;
// the markers are still drawn (drag one to CREATE a loop).
if (!m.hasLoop && m.loopEnd == 0) m.loopEnd = frames > 0 ? frames : 0;
m.crossfade = params_.loopCrossfadeFrames;
// A collapsed or inverted span is the OFF state (the engine refuses it either way), so
// park the handles on the shared default rather than leaving them stacked on each other
// where neither could be grabbed apart again. The markers are still drawn at 'no loop'
// weight — drag one to CREATE a loop.
if (!m.hasLoop || m.loopEnd <= m.loopStart) {
m.hasLoop = false;
const LoopBounds d = defaultLoopBounds(frames);
m.loopStart = d.start;
m.loopEnd = d.end;
}
return m;
}
@@ -207,10 +219,13 @@ void ReaSamplerEditor::applyMarkers(const SetupMarkers& m) {
// Write the edited markers into the parameter set as the loop/start override. The bank
// intrinsic is never written (read-only bank consumer).
SampleLoop loop;
loop.hasLoop = m.hasLoop;
// Collapsing the span onto itself is the OFF gesture — record it as such so the next
// pickedMarkers re-offers the default handles instead of two coincident ones.
loop.hasLoop = m.hasLoop && m.loopEnd > m.loopStart;
loop.start = m.loopStart;
loop.end = m.loopEnd;
params_.loopOverride = loop;
params_.loopCrossfadeFrames = m.crossfade;
params_.startPoint = m.start;
}