c3d67bc3da
Fixes the 2-point spline-EG early-free bug causing silent fade-ins, the pitch/filter enable-toggle Trigger-forcing hole, the contour-node/marker pixel shadow, missing deck-residue test coverage, and stale comments in knob_deck and spline_edit.
245 lines
12 KiB
C++
245 lines
12 KiB
C++
// editor_input_waveform.cpp — the WAVEFORM band's input: grabbing an envelope node or a
|
|
// start/loop marker, and resolving both drags live against the pure inverse maps
|
|
// (envelope_edit, waveform_view). Windows-only.
|
|
//
|
|
// Overlay contract: see waveform_view.h's WaveformSurface.
|
|
|
|
#include "shell/instrument/reasampler_editor.h"
|
|
|
|
#ifdef _WIN32
|
|
|
|
#include <algorithm>
|
|
#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/spline_edit.h" // the shared point-editing grammar
|
|
#include "core/instrument/ui/waveform_view.h" // waveformOverlayArea / markerAtPoint / snap
|
|
#include "shell/instrument/editor_internal.h"
|
|
#include "shell/instrument/reasampler_processor.h"
|
|
|
|
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_);
|
|
const std::int64_t frames = static_cast<std::int64_t>(pcm.size());
|
|
if (frames <= 0) return false;
|
|
const OverlayArea overlay = waveformOverlayArea(fl.bands.waveform);
|
|
|
|
const DeckEnableState gates = deckEnableState();
|
|
const bool splineLive = overlayIsSpline() && overlayEnvEnabled(overlayEnv_, gates);
|
|
const SplineGesture gesture = (GetKeyState(VK_CONTROL) & 0x8000) != 0
|
|
? SplineGesture::kControlLeft
|
|
: SplineGesture::kLeft;
|
|
|
|
// Envelope nodes first (they sit on top of the markers), then the wave markers. With no
|
|
// envelope overlay-active — or with its deck group's enable toggle off, which makes the
|
|
// same params' knobs inert — there are no grabbable nodes and the markers take every grab.
|
|
const double rate = liveSampleRate();
|
|
const bool nodesLive =
|
|
overlayEnv_ != OverlayEnv::kNone && !overlayEnvInert(overlayEnv_, gates);
|
|
if (rate > 0.0 && nodesLive) {
|
|
const std::int64_t startFrame = params_.startPoint.value_or(0);
|
|
const StageEnvelope env = packEnvelope(overlayEnv_, params_.play, frames, startFrame);
|
|
const double totalSeconds = static_cast<double>(frames) / rate;
|
|
const NodeHit nh = nodeAtPoint(env, overlay, totalSeconds, x, y);
|
|
if (nh.hit) {
|
|
drag_ = DragKind::kEnvNode;
|
|
envNode_ = nh.node;
|
|
dragStartX_ = x;
|
|
dragStartY_ = y;
|
|
dragStartEnv_ = env;
|
|
dragSampleFrames_ = frames;
|
|
dragStartParams_ = params_;
|
|
return true; // node moves once the cursor drags
|
|
}
|
|
}
|
|
// An existing contour node's grab/toggle/delete runs BEFORE the markers — mirroring
|
|
// markerHandleRect's tab-vs-column split (below): a coincident pixel (the default contour
|
|
// endpoint sits at the same x as the default start marker) is resolved by asking the
|
|
// NARROWER target first. pointAtPixel's pick radius is a small box around the node's own
|
|
// (x, y), not a full-height column, so this claims only genuine node hits — the marker's
|
|
// column stays grabbable at every other y along the same x. Never add here (kAdd is only
|
|
// tried once the markers have also passed on the click, below).
|
|
if (splineLive && splineOverlayClick(overlay, x, y, gesture, /*addOnEmptySpace=*/false)) {
|
|
return true;
|
|
}
|
|
|
|
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. 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);
|
|
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) {
|
|
beginMarkerDrag(static_cast<WaveMarker>(hit), m, frames, x);
|
|
return true;
|
|
}
|
|
// Nothing else wanted the click: now the drawn contour may take the empty space.
|
|
if (splineLive) return splineOverlayClick(overlay, x, y, gesture, /*addOnEmptySpace=*/true);
|
|
return false;
|
|
}
|
|
|
|
bool ReaSamplerEditor::splineOverlayClick(const OverlayArea& waveArea, int x, int y,
|
|
SplineGesture gesture, bool addOnEmptySpace) {
|
|
const VelocityCurve::Box box = splineOverlayBox(waveArea);
|
|
VelocityCurve& contour = splineFor(overlayEnv_);
|
|
SplineEdit edit = resolveSplineEdit(contour, box, gesture, x, y);
|
|
// resolveSplineEdit's outside-box grab/delete/toggle allowance (pointAtPixel's radius has
|
|
// no box check of its own) was designed for the popup's inset ring; the overlay box has NO
|
|
// inset (splineOverlayBox), so honoring it here would extend the grab halo 6px into the
|
|
// inter-band pad. kAdd already requires in-box (resolveSplineEdit's own check).
|
|
if (edit.kind != SplineEditKind::kNone && edit.kind != SplineEditKind::kAdd &&
|
|
!(x >= box.left && x < box.left + box.width && y >= box.top && y < box.top + box.height)) {
|
|
edit = SplineEdit{};
|
|
}
|
|
if (edit.kind == SplineEditKind::kAdd && !addOnEmptySpace) return false;
|
|
switch (edit.kind) {
|
|
case SplineEditKind::kNone:
|
|
return false;
|
|
case SplineEditKind::kDelete:
|
|
// deletePoint refuses the two endpoints, so a right-click on one is a safe no-op.
|
|
if (!contour.deletePoint(static_cast<std::size_t>(edit.index))) return true;
|
|
hover_ = HoverTarget{}; // a stale index would light a shifted node
|
|
commitAndReload();
|
|
return true;
|
|
case SplineEditKind::kToggleHard:
|
|
if (!contour.toggleHard(static_cast<std::size_t>(edit.index))) return true;
|
|
commitAndReload();
|
|
return true;
|
|
case SplineEditKind::kAdd:
|
|
case SplineEditKind::kGrab:
|
|
break;
|
|
}
|
|
// Snapshot BEFORE the add so a capture-loss rollback cancels the in-flight point too
|
|
// (the same contract the other parameter-editing drags keep).
|
|
dragStartParams_ = params_;
|
|
int index = edit.index;
|
|
if (edit.kind == SplineEditKind::kAdd) {
|
|
const VelocityPoint p = contour.pointFromPixel(box, x, y);
|
|
index = contour.addPoint(p.velocity, p.value);
|
|
// At the ceiling: refused, contour untouched, nothing to roll back. Swallow the click
|
|
// rather than letting it fall through to a marker grab under the cursor.
|
|
if (index < 0) return true;
|
|
}
|
|
drag_ = DragKind::kSplineNode;
|
|
curvePointIndex_ = index;
|
|
dragStartCurve_ = contour; // AFTER the add — resolvePointDrag's delta base
|
|
// The release-time drag-off-delete bound (onMouseUp), matching the popup's kCurveNode use
|
|
// of the same field — the two spline surfaces share one drag-off grammar, not just the
|
|
// click grammar.
|
|
dragCurveRect_ = waveArea.rect;
|
|
dragStartX_ = x;
|
|
dragStartY_ = y;
|
|
invalidate(); // live feedback; the commit lands on WM_LBUTTONUP
|
|
return true;
|
|
}
|
|
|
|
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_;
|
|
|
|
if (drag_ == DragKind::kSplineNode) {
|
|
// Same absolute-delta contract as the popup's node drag, against the grab-time contour
|
|
// and the overlay's own box.
|
|
if (curvePointIndex_ < 0) return;
|
|
splineFor(overlayEnv_) = VelocityCurve::resolvePointDrag(
|
|
dragStartCurve_, static_cast<std::size_t>(curvePointIndex_),
|
|
splineOverlayBox(overlay), dx, y - dragStartY_);
|
|
invalidate(); // live feedback; commit on release
|
|
return;
|
|
}
|
|
|
|
if (drag_ == DragKind::kEnvNode) {
|
|
// Resolve the grabbed envelope node's new params from the pixel delta (through the
|
|
// pure envelope_edit inverse map, clamped), then unpack them back onto the parameter
|
|
// set. The StageEnvelope was snapshotted at grab (dragStartEnv_) so the delta is
|
|
// absolute.
|
|
const std::int64_t frames = dragSampleFrames_;
|
|
const double rate = liveSampleRate();
|
|
if (frames <= 0 || rate <= 0.0) return;
|
|
const double totalSeconds = static_cast<double>(frames) / rate;
|
|
const StageEnvelope edited = resolveNodeDrag(dragStartEnv_, envNode_, overlay,
|
|
totalSeconds, envClampBounds(), dx,
|
|
y - dragStartY_);
|
|
unpackEnvelope(overlayEnv_, edited, params_.play);
|
|
if (dragCommitsLive(DragKind::kEnvNode)) commitLive();
|
|
invalidate(); // live feedback; commit on WM_LBUTTONUP
|
|
return;
|
|
}
|
|
|
|
// kWaveMarker: resolve the grabbed marker's new frame from the pixel delta,
|
|
// zero-crossing-snap it against the decoded PCM, apply the inter-marker clamps, and write
|
|
// the override live.
|
|
const std::int64_t frames = dragSampleFrames_;
|
|
if (frames <= 0) return;
|
|
|
|
// 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[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. 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() && waveMarker_ != WaveMarker::kLoopXfade) {
|
|
newFrame = nearestZeroCrossing(pcm.data(), static_cast<std::int64_t>(pcm.size()),
|
|
newFrame);
|
|
}
|
|
|
|
// Build the edited marker set from the snapshot, moving only the grabbed marker, then
|
|
// clamp: loopStart <= loopEnd, start in [0, frames-1]. Dragging a loop marker MAKES a loop.
|
|
SetupMarkers m = dragStartMarkers_;
|
|
if (waveMarker_ == WaveMarker::kStart) {
|
|
m.start = newFrame;
|
|
} else if (waveMarker_ == WaveMarker::kLoopStart) {
|
|
m.loopStart = (std::min)(newFrame, m.loopEnd);
|
|
m.hasLoop = true;
|
|
} 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;
|
|
// 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);
|
|
invalidate(); // live feedback; the commit lands on release
|
|
}
|
|
|
|
} // namespace reasampler::vst
|
|
|
|
#endif // _WIN32
|