loop: crossfade the Gate sustain seam, and unshadow the loop handles that made loop points look gone
This commit is contained in:
@@ -52,20 +52,33 @@ 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.
|
||||
if (m.hasLoop &&
|
||||
contains(markerHandleRect(overlay, frames, m.loopStart - m.crossfade), x, y)) {
|
||||
beginMarkerDrag(WaveMarker::kLoopXfade, m, frames, x);
|
||||
return true;
|
||||
}
|
||||
const std::int64_t markerFrames[3] = {m.start, m.loopStart, m.loopEnd};
|
||||
const int hit = markerAtPoint(overlay, frames, markerFrames, 3, x, y);
|
||||
if (hit >= 0) {
|
||||
drag_ = DragKind::kWaveMarker;
|
||||
waveMarker_ = static_cast<WaveMarker>(hit);
|
||||
dragStartX_ = x;
|
||||
dragStartMarkers_ = m;
|
||||
dragSampleFrames_ = frames;
|
||||
dragStartParams_ = params_;
|
||||
beginMarkerDrag(static_cast<WaveMarker>(hit), m, frames, x);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::beginMarkerDrag(WaveMarker which, const SetupMarkers& m,
|
||||
std::int64_t frames, int x) {
|
||||
drag_ = DragKind::kWaveMarker;
|
||||
waveMarker_ = which;
|
||||
dragStartX_ = x;
|
||||
dragStartMarkers_ = m;
|
||||
dragSampleFrames_ = frames;
|
||||
dragStartParams_ = params_;
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::dragWaveform(const FaceLayout& fl, int x, int y) {
|
||||
const OverlayArea overlay = waveformOverlayArea(fl.bands.waveform);
|
||||
const int dx = x - dragStartX_;
|
||||
@@ -96,14 +109,17 @@ void ReaSamplerEditor::dragWaveform(const FaceLayout& fl, int x, int y) {
|
||||
|
||||
// Grabbed frame at grab time, from the snapshot (so the delta is measured from grab).
|
||||
const int idx = static_cast<int>(waveMarker_);
|
||||
const std::int64_t startVals[3] = {dragStartMarkers_.start, dragStartMarkers_.loopStart,
|
||||
dragStartMarkers_.loopEnd};
|
||||
const std::int64_t startVals[4] = {dragStartMarkers_.start, dragStartMarkers_.loopStart,
|
||||
dragStartMarkers_.loopEnd,
|
||||
dragStartMarkers_.loopStart -
|
||||
dragStartMarkers_.crossfade};
|
||||
std::int64_t newFrame = resolveDragFrame(overlay, frames, startVals[idx], dx);
|
||||
|
||||
// Snap to the nearest zero crossing in the decoded PCM. Pure over the cached mono
|
||||
// frames — no host types, no file I/O.
|
||||
// frames — no host types, no file I/O. The crossfade handle is exempt: it sets a fade
|
||||
// LENGTH, and the whole point of the fade is that its edges need no zero crossing.
|
||||
const std::vector<AudioSample>& pcm = monoPcmFor(selectedId_);
|
||||
if (!pcm.empty()) {
|
||||
if (!pcm.empty() && waveMarker_ != WaveMarker::kLoopXfade) {
|
||||
newFrame = nearestZeroCrossing(pcm.data(), static_cast<std::int64_t>(pcm.size()),
|
||||
newFrame);
|
||||
}
|
||||
@@ -116,12 +132,19 @@ void ReaSamplerEditor::dragWaveform(const FaceLayout& fl, int x, int y) {
|
||||
} else if (waveMarker_ == WaveMarker::kLoopStart) {
|
||||
m.loopStart = (std::min)(newFrame, m.loopEnd);
|
||||
m.hasLoop = true;
|
||||
} else { // kLoopEnd
|
||||
} else if (waveMarker_ == WaveMarker::kLoopEnd) {
|
||||
m.loopEnd = (std::max)(newFrame, m.loopStart);
|
||||
m.hasLoop = true;
|
||||
} else { // kLoopXfade — the handle sits at loopStart - crossfade, so left lengthens it
|
||||
m.crossfade = (std::max)(std::int64_t{0}, m.loopStart - newFrame);
|
||||
}
|
||||
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));
|
||||
if (m.crossfade < 0) m.crossfade = 0;
|
||||
|
||||
applyMarkers(m);
|
||||
invalidate(); // live feedback; the commit lands on release
|
||||
|
||||
@@ -98,6 +98,17 @@ void ReaSamplerEditor::paintWaveform(LICE_IBitmap* bmp, const Rect& band) {
|
||||
static_cast<float>(kLoopSpanFillAlpha), 0);
|
||||
}
|
||||
}
|
||||
// The crossfade region, at half the loop span's weight so the two read as nested rather
|
||||
// than as a second loop. Drawn before the marker bars so the bars stay on top.
|
||||
if (m.hasLoop && m.crossfade > 0) {
|
||||
const int fx = frameToX(overlay, frames, m.loopStart - m.crossfade);
|
||||
const int lx = frameToX(overlay, frames, m.loopStart);
|
||||
if (lx > fx) {
|
||||
LICE_FillRect(bmp, fx, overlayRect.y, lx - fx, overlayRect.height,
|
||||
toLice(roleColor(kRoleLoopMarker)),
|
||||
static_cast<float>(kLoopSpanFillAlpha) * 0.5f, 0);
|
||||
}
|
||||
}
|
||||
const std::int64_t markerFrames[3] = {m.start, m.loopStart, m.loopEnd};
|
||||
const Role markerRoles[3] = {kRoleStartMarker, kRoleLoopMarker, kRoleLoopMarker};
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
@@ -107,6 +118,15 @@ void ReaSamplerEditor::paintWaveform(LICE_IBitmap* bmp, const Rect& band) {
|
||||
LICE_FillRect(bmp, mx - 1, overlayRect.y, 2, overlayRect.height,
|
||||
toLice(roleColor(markerRoles[i])), alpha, 0);
|
||||
}
|
||||
// The crossfade's grab tab. Only offered with a loop set, matching the hit-test, and it
|
||||
// is the whole affordance for a zero-length fade — nothing else marks where it sits.
|
||||
if (m.hasLoop) {
|
||||
const Rect tab = markerHandleRect(overlay, frames, m.loopStart - m.crossfade);
|
||||
if (!tab.empty()) {
|
||||
LICE_FillRect(bmp, tab.x, tab.y, tab.width, tab.height,
|
||||
toLice(roleColor(kRoleLoopMarker)), 1.0f, 0);
|
||||
}
|
||||
}
|
||||
|
||||
paintEnvelopeOverlay(bmp, overlay, frames);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -93,8 +93,11 @@ private:
|
||||
using ParamControl = instrument::ui::DeckParam;
|
||||
|
||||
// The waveform markers on the waveform band: start-point + the sustain loop's two ends,
|
||||
// in draw + hit order.
|
||||
enum class WaveMarker { kStart = 0, kLoopStart = 1, kLoopEnd = 2, kCount = 3 };
|
||||
// in draw + hit order, then the crossfade handle. The crossfade is NOT part of the
|
||||
// full-height column hit-test — it answers only in its top-strip handle (waveform_view's
|
||||
// markerHandleRect), because at a zero fade it sits exactly on the loop start.
|
||||
enum class WaveMarker { kStart = 0, kLoopStart = 1, kLoopEnd = 2, kLoopXfade = 3,
|
||||
kCount = 4 };
|
||||
|
||||
// The interactive element under the pointer, resolved live in WM_MOUSEMOVE. `index`
|
||||
// disambiguates within a kind (tab ordinal, visible-card index, control-row id); -1 when
|
||||
@@ -291,12 +294,15 @@ private:
|
||||
std::string samplePathFor(const std::string& sampleId) const;
|
||||
|
||||
// The effective loop + start markers for the loaded capture: the parameter set's
|
||||
// override when one is set, else the bank's loop intrinsic / frame 0. Absent loop ->
|
||||
// loopStart==loopEnd==0. `frames` defaults loopEnd when the bank left the loop empty.
|
||||
// override when one is set, else the bank's loop intrinsic / frame 0. With no loop set,
|
||||
// the loop handles park on loop_span's defaultLoopBounds so both stay grabbable — the
|
||||
// frame-0 default they replace put loopStart under the start marker, where nothing could
|
||||
// reach it.
|
||||
struct SetupMarkers {
|
||||
std::int64_t start = 0;
|
||||
std::int64_t loopStart = 0;
|
||||
std::int64_t loopEnd = 0;
|
||||
std::int64_t crossfade = 0; // pre-seam fade, SOURCE frames; handle at loopStart - this
|
||||
bool hasLoop = false; // whether a sustain loop is set (drives the "no loop" affordance)
|
||||
};
|
||||
SetupMarkers pickedMarkers(std::int64_t frames) const;
|
||||
@@ -305,6 +311,10 @@ private:
|
||||
// callers decide live-drag vs final commit.
|
||||
void applyMarkers(const SetupMarkers& m);
|
||||
|
||||
// Arms a waveform-marker drag: the grabbed marker plus the snapshot the pixel-delta
|
||||
// resolver and the inter-marker clamps measure from.
|
||||
void beginMarkerDrag(WaveMarker which, const SetupMarkers& m, std::int64_t frames, int x);
|
||||
|
||||
// Deck knobs edit the parameter set's PlaySeconds (play mode + AHDSR; pitch engine + AD
|
||||
// pitch envelope) — wall-clock seconds, rate-free; the build resolves to frames.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user