instrument: spline EGs — hard points on the one shared spline, a drawn contour per envelope beside its staged state, payload v13
This commit is contained in:
@@ -11,7 +11,7 @@ The pure engine/geometry core this shell wraps (`sampler_core`, `pitch_shift`,
|
||||
`sample_map`, `component_state_io`, `play_params.h`, `editor_geometry`, `sample_bands`,
|
||||
`sample_chrome`, `keyboard_strip`, `waveform_view`, `capture_browser`, `browser_scroll`,
|
||||
`param_slider`, `trigger_seam`, `velocity_curve`, `embed_strip`, `knob_deck`,
|
||||
`deck_groups`, `curve_popup`, `master_gain`, `reasampler_uid.h`) lives in `core/instrument/*` and
|
||||
`deck_groups`, `curve_popup`, `spline_edit`, `master_gain`, `reasampler_uid.h`) lives in `core/instrument/*` and
|
||||
`core/wire` and is documented there — this directory consumes it but does not own it.
|
||||
|
||||
## Invariants
|
||||
@@ -101,7 +101,7 @@ declared ahead of the instrument slots at that member in `reasampler_processor.h
|
||||
|
||||
- `reaper_bridge` — READ-ONLY bank consumer: receives bank snapshots from the extension and exposes them as a read-only view. **Never writes to the extension's bank** — this is a load-bearing invariant; no mutation path exists in this module. **pS-usage:** gains `writeUsageExtState` (prefix-guarded — accepts only `rsusage_`-prefixed keys, refuses all others) so the processor can publish usage without weakening the read-only-bank invariant.
|
||||
- `reasampler_processor` (`shell/instrument/`: `reasampler_processor.cpp` lifecycle + `process()`, `processor_state.cpp` component-state I/O + UI-thread parameter accessors, `processor_reload.cpp` the off-audio-thread `reloadInstrument`/publish family — Q-W2v, T4-12 split; `process()` and its per-block work stay ONE TU on purpose, no cross-TU call on the per-sample path) — VST3 `SingleComponentEffect` shell: declares event-input bus + **permanently stereo** output (GA fix: dynamic mono↔stereo bus renegotiation deleted; `ChannelMode` is now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread `reloadInstrument` + atomic pointer swap so `process()` does no allocation, no file I/O, no bridge calls. The instance state is `{loaded capture id, one InstrumentParams}`, and `reloadInstrument` resolves + decodes exactly that one capture into the `SampleData` the engine plays. **Self-contained playback (pS):** `ComponentState` v10 adds a `SampleRefs` table — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName); `reloadInstrument` decodes directly from `SampleRefs`, bank-free (plays with the extension absent). The bank/bridge is a browser source: loading a capture copies its reference in; the reopen-heal timer + poll-to-play apparatus are removed. `retireIdleDrain()` retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (`setVoiceCount`/`setVoiceMode`/`setMonoTrigger`) rebuild the engine from the already-decoded `SampleData` via the drain-slot swap — no bank re-read, no WAV re-decode, no audible cut to ringing tails. **FB1:** applies the post-mixer `masterGainLinear` (from `ComponentState` v8) as a per-sample ramp over the summed output — no zipper noise. **GA v9:** `channelModeExplicit_` flag persisted; `channelModeFor()` auto-defaults the mode from the loaded capture's channel count when the flag is not set. **pS:** `ComponentState` bumped v9→v10 (`SampleRefs` table); pre-v10 blobs lift to empty refs and re-save self-contained. **pS-usage:** publishes instance usage (held `SampleRefs` paths) to `rsusage_<instanceGuid>` at the tail of `reloadInstrument` (off audio thread) via `reaper_bridge::writeUsageExtState`; `ComponentState` bumped v10→**v11** (`instanceGuid` field); pre-v11 blobs mint guid on first publish.
|
||||
- `reasampler_editor` — VST3 `IPlugView` LICE editor shell: hosts a LICE-drawn child window; the Sample face is home and Browse is a modal picker over it. Split on the Sample face's BAND axis, mirroring the pure `sample_bands` allocator: `editor_session` (session/bridge state, caches, commit-and-reload), `editor_controls` (parameter plumbing + the ONE `faceLayout` band resolve every paint and hit-test path shares), then matching paint and input sets — `editor_paint`/`editor_input` (dispatch + drag router + hover dispatch), `_chrome`, `_waveform`, `_deck` — plus the two band-independent surfaces (`_browse` for the modal picker, `_curve` for the velocity-curve popup) and `editor_platform` (IPlugView/Win32 window plumbing). Shared internals in `editor_internal.h`, no TU of its own. Drop-onto-editor ingest is NOT shipped (deferred).
|
||||
- `reasampler_editor` — VST3 `IPlugView` LICE editor shell: hosts a LICE-drawn child window; the Sample face is home and Browse is a modal picker over it. Split on the Sample face's BAND axis, mirroring the pure `sample_bands` allocator: `editor_session` (session/bridge state, caches, commit-and-reload), `editor_controls` (the control-value domain maps + the node-drag bounds that must match them, plus the ONE `faceLayout` band resolve every paint and hit-test path shares), `editor_models` (the orthogonal half: which stored struct each transient editor selection names — the staged-envelope pack/unpack, the drawn contour, and the three velocity curves), then matching paint and input sets — `editor_paint`/`editor_input` (dispatch + drag router + hover dispatch), `_chrome`, `_waveform`, `_deck` — plus the two band-independent surfaces (`_browse` for the modal picker, `_curve` for the velocity-curve popup) and `editor_platform` (IPlugView/Win32 window plumbing). Shared internals in `editor_internal.h`, no TU of its own. Drop-onto-editor ingest is NOT shipped (deferred).
|
||||
- `reasampler_embed` — implements `IReaperUIEmbedInterface` so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout to `embed_strip`. A read-only readout: the loaded capture across the keyboard span with its root marked, plus the activity level. It takes no mouse input (there is nothing on the strip to select).
|
||||
- `vst_entry` — VST3 entry point: `GetPluginFactory` export, class registration, channel-forked class UIDs.
|
||||
- `editor_internal.h` — INTERNAL shared helpers for the `reasampler_editor` TU family, included only by the editor's own shell TUs (`editor_session` / `editor_controls` / `editor_paint_*` / `editor_input_*` / `editor_platform`), never a public seam: the `Rect`↔kit adapters, small draw primitives (knob face / title band), label helpers, and the velocity-curve box derivation — the helpers more than one band TU needs. The deck's control ids, group ids and group composition are the pure `deck_groups` module's, not this file's. The piano-strip and root-key draws live in `editor_paint_chrome`, their only consumer, not here.
|
||||
|
||||
@@ -51,6 +51,7 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
|
||||
# matching sets, plus the two band-independent surfaces and the platform TU.
|
||||
editor_session.cpp
|
||||
editor_controls.cpp
|
||||
editor_models.cpp
|
||||
editor_paint.cpp
|
||||
editor_paint_chrome.cpp
|
||||
editor_paint_waveform.cpp
|
||||
@@ -85,7 +86,7 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
|
||||
capture_browser keyboard_strip sample_bands sample_chrome
|
||||
waveform_view bank_sync browser_scroll param_slider tooltip
|
||||
theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit
|
||||
knob_deck deck_groups curve_popup master_gain sample_usage file_bytes curve_law)
|
||||
knob_deck deck_groups curve_popup spline_edit master_gain sample_usage file_bytes curve_law)
|
||||
# SDK_INC gives the REAPER VST3 interfaces + API header for the bridge; WDL_INC gives
|
||||
# LICE for the editor. The VST3 SDK headers arrive via vst3_sdk PUBLIC.
|
||||
target_include_directories(reasampler_vst PRIVATE ${REASAMPLER_SRC_DIR} ${SDK_INC} ${WDL_INC})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// editor_controls.cpp — the ReaSamplerEditor's parameter plumbing: the band-stack layout
|
||||
// resolve every paint/hit-test path shares, the control-value domain maps (controlValue /
|
||||
// applyControl — seconds/fraction/frames <-> normalized 0..1), the control-id<->value binding
|
||||
// against the pure `deck_groups` module's descriptors, and the envelope pack/unpack (which
|
||||
// stored struct each overlay selection maps onto). Value logic only — no painting, no window
|
||||
// plumbing.
|
||||
// against the pure `deck_groups` module's descriptors, and the node-drag clamp bounds that must
|
||||
// match those domains. The orthogonal half — which stored struct each editor selection names —
|
||||
// is editor_models. Value logic only: no painting, no window plumbing.
|
||||
|
||||
#include "shell/instrument/reasampler_editor.h"
|
||||
|
||||
@@ -99,6 +99,12 @@ double ReaSamplerEditor::controlValue(int id, const PlaySeconds& play) const {
|
||||
const auto secToNorm = [](double s) { return clamp01(s / kEnvTimeMaxSeconds); };
|
||||
switch (static_cast<ParamControl>(id)) {
|
||||
case ParamControl::kPlayMode: return play.playMode == PlayMode::Trigger ? 1.0 : 0.0;
|
||||
case ParamControl::kAmpEnvMode:
|
||||
return play.ampSpline.mode == EnvMode::Spline ? 1.0 : 0.0;
|
||||
case ParamControl::kPitchEnvMode:
|
||||
return play.pitchSpline.mode == EnvMode::Spline ? 1.0 : 0.0;
|
||||
case ParamControl::kFilterEnvMode:
|
||||
return play.filterSpline.mode == EnvMode::Spline ? 1.0 : 0.0;
|
||||
case ParamControl::kPitchEngine: return play.pitchEngine == PitchEngine::Preserve ? 1.0 : 0.0;
|
||||
case ParamControl::kAttack: return secToNorm(play.adsr.attackSeconds);
|
||||
case ParamControl::kHold: return secToNorm(play.adsr.holdSeconds);
|
||||
@@ -164,8 +170,26 @@ void ReaSamplerEditor::applyControl(int id, PlaySeconds& play, double value,
|
||||
const auto normToSec = [](double v) { return clamp01(v) * kEnvTimeMaxSeconds; };
|
||||
switch (static_cast<ParamControl>(id)) {
|
||||
case ParamControl::kPlayMode:
|
||||
// Gate is refused while any EG is drawn — see splineActive (play_params.h). The
|
||||
// segment paints Disabled for the same reason, so the refusal is never a surprise.
|
||||
if (segment == 0 && splineActive(play)) break;
|
||||
play.playMode = (segment == 1) ? PlayMode::Trigger : PlayMode::Gate;
|
||||
break;
|
||||
// Switching TO Spline drops Gate, which the spline model has no place for. Switching
|
||||
// back does NOT restore it: the previous mode is not stored, and silently re-gating an
|
||||
// instrument the user has since heard as a one-shot is the worse surprise.
|
||||
case ParamControl::kAmpEnvMode:
|
||||
case ParamControl::kPitchEnvMode:
|
||||
case ParamControl::kFilterEnvMode: {
|
||||
const EnvMode m = (segment == 1) ? EnvMode::Spline : EnvMode::Staged;
|
||||
switch (static_cast<ParamControl>(id)) {
|
||||
case ParamControl::kAmpEnvMode: play.ampSpline.mode = m; break;
|
||||
case ParamControl::kPitchEnvMode: play.pitchSpline.mode = m; break;
|
||||
default: play.filterSpline.mode = m; break;
|
||||
}
|
||||
if (splineActive(play)) play.playMode = PlayMode::Trigger;
|
||||
break;
|
||||
}
|
||||
case ParamControl::kPitchEngine:
|
||||
play.pitchEngine = (segment == 1) ? PitchEngine::Preserve : PitchEngine::Varispeed;
|
||||
break;
|
||||
@@ -409,9 +433,9 @@ std::string ReaSamplerEditor::deckValueLabel(int id) const {
|
||||
}
|
||||
|
||||
EnvClampBounds ReaSamplerEditor::envClampBounds() const {
|
||||
// Match the deck knobs' own domains so a node drag can never produce a param a knob
|
||||
// couldn't. Every stage time caps at kEnvTimeMaxSeconds; the Hold fractions and the sustain
|
||||
// level are [0,1] by definition and need no bound here.
|
||||
// Lives here, beside controlValue/applyControl, because it must MATCH them: a node drag can
|
||||
// never produce a param a knob couldn't. Every stage time caps at kEnvTimeMaxSeconds; the
|
||||
// Hold fractions and the sustain level are [0,1] by definition and need no bound here.
|
||||
EnvClampBounds b;
|
||||
b.maxAttackSeconds = kEnvTimeMaxSeconds;
|
||||
b.maxHoldSeconds = kEnvTimeMaxSeconds;
|
||||
@@ -420,141 +444,6 @@ EnvClampBounds ReaSamplerEditor::envClampBounds() const {
|
||||
return b;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// The two directions of the AHDSR <-> StageEnvelope copy, so a field can only be forgotten in
|
||||
// one place rather than two.
|
||||
void packAhdsr(const AdsrSeconds& a, StageEnvelope& env) {
|
||||
env.kind = instrument::ui::EnvKind::Ahdsr;
|
||||
env.attackSeconds = a.attackSeconds;
|
||||
env.holdSeconds = a.holdSeconds;
|
||||
env.decaySeconds = a.decaySeconds;
|
||||
env.sustainLevel = a.sustainLevel;
|
||||
env.releaseSeconds = a.releaseSeconds;
|
||||
env.attackCurve = a.attackCurve;
|
||||
env.decayCurve = a.decayCurve;
|
||||
env.releaseCurve = a.releaseCurve;
|
||||
}
|
||||
void unpackAhdsr(const StageEnvelope& env, AdsrSeconds& a) {
|
||||
a.attackSeconds = env.attackSeconds;
|
||||
a.holdSeconds = env.holdSeconds;
|
||||
a.decaySeconds = env.decaySeconds;
|
||||
a.sustainLevel = env.sustainLevel;
|
||||
a.releaseSeconds = env.releaseSeconds;
|
||||
a.attackCurve = env.attackCurve;
|
||||
a.decayCurve = env.decayCurve;
|
||||
a.releaseCurve = env.releaseCurve;
|
||||
}
|
||||
void packAhd(const AhdSeconds& a, double originSeconds, double spanSeconds, StageEnvelope& env) {
|
||||
env.kind = instrument::ui::EnvKind::Ahd;
|
||||
env.attackSeconds = a.attackSeconds;
|
||||
env.decaySeconds = a.decaySeconds;
|
||||
env.holdFraction = a.holdFraction;
|
||||
env.attackCurve = a.attackCurve;
|
||||
env.decayCurve = a.decayCurve;
|
||||
env.originSeconds = originSeconds;
|
||||
env.spanSeconds = spanSeconds;
|
||||
}
|
||||
void unpackAhd(const StageEnvelope& env, AhdSeconds& a) {
|
||||
a.attackSeconds = env.attackSeconds;
|
||||
a.decaySeconds = env.decaySeconds;
|
||||
a.holdFraction = env.holdFraction;
|
||||
a.attackCurve = env.attackCurve;
|
||||
a.decayCurve = env.decayCurve;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
StageEnvelope ReaSamplerEditor::packEnvelope(OverlayEnv which, const PlaySeconds& play,
|
||||
std::int64_t frames,
|
||||
std::int64_t startFrame) const {
|
||||
StageEnvelope env;
|
||||
const double rate = liveSampleRate();
|
||||
const double t0 = rate > 0.0 ? static_cast<double>(startFrame) / rate : 0.0;
|
||||
// The Trigger amp and filter AHDs live over the PLAY span; the pitch AHD over the whole
|
||||
// post-start span, since it keeps running after a Trigger one-shot's amplitude has ended.
|
||||
const std::int64_t playLen =
|
||||
triggerPlayLength(play.trigger.lengthFraction, frames, startFrame);
|
||||
const double playSpan = rate > 0.0 ? static_cast<double>(playLen) / rate : 0.0;
|
||||
const double fullSpan =
|
||||
rate > 0.0 ? static_cast<double>((std::max)(std::int64_t{0}, frames - startFrame)) / rate
|
||||
: 0.0;
|
||||
const bool trigger = (play.playMode == PlayMode::Trigger);
|
||||
switch (which) {
|
||||
case OverlayEnv::kPitch:
|
||||
packAhd(play.pitchEnv.shape, t0, fullSpan, env);
|
||||
break;
|
||||
case OverlayEnv::kFilter:
|
||||
if (trigger) packAhd(play.filter.trigEnv, t0, playSpan, env);
|
||||
else packAhdsr(play.filter.env, env);
|
||||
break;
|
||||
case OverlayEnv::kAmp:
|
||||
if (trigger) packAhd(play.trigAhd, t0, playSpan, env);
|
||||
else packAhdsr(play.adsr, env);
|
||||
break;
|
||||
case OverlayEnv::kNone:
|
||||
break; // nothing is overlay-active; a default-constructed StageEnvelope, not amp
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::unpackEnvelope(OverlayEnv which, const StageEnvelope& env,
|
||||
PlaySeconds& play) const {
|
||||
const bool trigger = (play.playMode == PlayMode::Trigger);
|
||||
switch (which) {
|
||||
case OverlayEnv::kPitch:
|
||||
unpackAhd(env, play.pitchEnv.shape);
|
||||
break;
|
||||
case OverlayEnv::kFilter:
|
||||
if (trigger) unpackAhd(env, play.filter.trigEnv);
|
||||
else unpackAhdsr(env, play.filter.env);
|
||||
break;
|
||||
case OverlayEnv::kAmp:
|
||||
if (trigger) unpackAhd(env, play.trigAhd);
|
||||
else unpackAhdsr(env, play.adsr);
|
||||
break;
|
||||
case OverlayEnv::kNone:
|
||||
break; // nothing is overlay-active, so there is nothing a drag could have edited
|
||||
}
|
||||
}
|
||||
|
||||
const VelocityCurve& ReaSamplerEditor::curveFor(CurveTarget target) const {
|
||||
switch (target) {
|
||||
case CurveTarget::kPitch: return params_.play.pitchVelocityCurve;
|
||||
case CurveTarget::kFilter: return params_.play.filter.velocityCurve;
|
||||
case CurveTarget::kAmp:
|
||||
case CurveTarget::kNone:
|
||||
// kNone reads as amp — a valid, harmless choice for a target-agnostic caller (a
|
||||
// disabled deck cell still needs SOME curve to draw). editedCurve()'s mutable
|
||||
// overload below refuses kNone rather than relying on this fallback to protect amp.
|
||||
return params_.velocityCurve;
|
||||
}
|
||||
return params_.velocityCurve;
|
||||
}
|
||||
|
||||
VelocityCurve& ReaSamplerEditor::curveFor(CurveTarget target) {
|
||||
return const_cast<VelocityCurve&>(std::as_const(*this).curveFor(target));
|
||||
}
|
||||
|
||||
const VelocityCurve& ReaSamplerEditor::editedCurve() const { return curveFor(curvePopup_); }
|
||||
|
||||
VelocityCurve& ReaSamplerEditor::editedCurve() {
|
||||
// Refuses kNone rather than aliasing amp (see curvePopup_'s declaration for why); should be
|
||||
// unreachable now that every writer of kNone also cancels the drag, but this is the second,
|
||||
// independent line of defense.
|
||||
if (curvePopup_ == CurveTarget::kNone) {
|
||||
static VelocityCurve sink = VelocityCurve::flat();
|
||||
return sink;
|
||||
}
|
||||
return curveFor(curvePopup_);
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::closeCurvePopup() {
|
||||
curvePopup_ = CurveTarget::kNone;
|
||||
if (drag_ == DragKind::kCurveNode) {
|
||||
drag_ = DragKind::kNone;
|
||||
curvePointIndex_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::applyParamControl(int id, double value, int segment) {
|
||||
if (id == static_cast<int>(ParamControl::kKeyTrack)) {
|
||||
// keyTrack sits beside the play bundle (0..200% over kKeyTrackMax); the knob maps 0..1.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// editor_input_curve.cpp — the velocity-curve popup's input: the modal click routing,
|
||||
// node grab/add/Alt-delete inside the curve box, the live node drag, the right-click
|
||||
// delete, and the popup's hover. Band-independent (the sheet floats over the whole face).
|
||||
// node grab/add/toggle/delete inside the curve box, the live node drag, the right-click
|
||||
// delete on EITHER spline surface, and the popup's hover. Band-independent (the sheet floats
|
||||
// over the whole face).
|
||||
// Windows-only.
|
||||
|
||||
#include "shell/instrument/reasampler_editor.h"
|
||||
@@ -8,6 +9,8 @@
|
||||
#ifdef _WIN32
|
||||
|
||||
#include "core/instrument/ui/curve_popup.h" // computeCurvePopup / popupOutsideSheet
|
||||
#include "core/instrument/ui/spline_edit.h" // the shared point-editing grammar
|
||||
#include "core/instrument/ui/waveform_view.h" // waveformOverlayArea (the overlay right-click)
|
||||
#include "shell/instrument/editor_internal.h" // curveBoxFromRect
|
||||
#include "shell/instrument/reasampler_processor.h"
|
||||
|
||||
@@ -42,37 +45,37 @@ void ReaSamplerEditor::handleCurveMouseDown(const Rect& r, int x, int y) {
|
||||
const VelocityCurve::Box box = curveBoxFromRect(r);
|
||||
if (box.width <= 0 || box.height <= 1) return;
|
||||
|
||||
int idx = editedCurve().pointAtPixel(box, x, y);
|
||||
|
||||
// Modifier-click (Alt) deletes an interior node — a discrete, final edit committed at once
|
||||
// (deletePoint refuses the two endpoints, so an Alt-click on them is a safe no-op).
|
||||
if (idx >= 0 && (GetKeyState(VK_MENU) & 0x8000) != 0) {
|
||||
if (editedCurve().deletePoint(static_cast<std::size_t>(idx))) {
|
||||
// Alt-click delete predates the converged grammar and is kept as a landed alternate; every
|
||||
// other gesture routes through the shared resolver so both spline consumers stay one
|
||||
// grammar.
|
||||
if ((GetKeyState(VK_MENU) & 0x8000) != 0) {
|
||||
const int alt = editedCurve().pointAtPixel(box, x, y);
|
||||
if (alt >= 0 && editedCurve().deletePoint(static_cast<std::size_t>(alt))) {
|
||||
hover_ = HoverTarget{}; // a stale kCurveNode index would light a shifted node
|
||||
commitAndReload();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const bool ctrl = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
|
||||
const SplineEdit edit = resolveSplineEdit(
|
||||
editedCurve(), box, ctrl ? SplineGesture::kControlLeft : SplineGesture::kLeft, x, y);
|
||||
if (edit.kind == SplineEditKind::kToggleHard) {
|
||||
if (editedCurve().toggleHard(static_cast<std::size_t>(edit.index))) commitAndReload();
|
||||
return;
|
||||
}
|
||||
if (edit.kind == SplineEditKind::kNone) return; // ring click with no node hit
|
||||
|
||||
// Snapshot BEFORE any mutation so a capture-loss rollback also cancels an in-flight ADD
|
||||
// (mirror of the other parameter-editing drags' dragStartParams_ contract).
|
||||
dragStartParams_ = params_;
|
||||
|
||||
// Empty-space click inside the mapping box: add a control point via the pure inverse map,
|
||||
// then grab it. Box-gated (not just contains(r,x,y)) because the inset ring must not add a
|
||||
// point — it would clamp to velocity 0/127, stacking an undeletable duplicate on an
|
||||
// endpoint. A ring click can still grab an existing node (handled above).
|
||||
if (idx < 0) {
|
||||
const bool inBox = (x >= box.left && x < box.left + box.width &&
|
||||
y >= box.top && y < box.top + box.height);
|
||||
if (inBox) {
|
||||
const VelocityPoint p = editedCurve().pointFromPixel(box, x, y);
|
||||
idx = static_cast<int>(editedCurve().addPoint(p.velocity, p.value));
|
||||
}
|
||||
int idx = edit.index;
|
||||
if (edit.kind == SplineEditKind::kAdd) {
|
||||
const VelocityPoint p = editedCurve().pointFromPixel(box, x, y);
|
||||
idx = editedCurve().addPoint(p.velocity, p.value);
|
||||
if (idx < 0) return; // at the point ceiling: refused, curve untouched
|
||||
}
|
||||
|
||||
if (idx < 0) return; // ring click with no node hit — nothing to grab
|
||||
|
||||
drag_ = DragKind::kCurveNode;
|
||||
curvePointIndex_ = idx;
|
||||
dragStartCurve_ = editedCurve(); // AFTER the add — resolvePointDrag's delta base
|
||||
@@ -94,23 +97,30 @@ void ReaSamplerEditor::dragCurve(int x, int y) {
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::onMouseRDown(int x, int y) {
|
||||
// Right-click on a popup curve node deletes it — the primary delete affordance; Alt-click
|
||||
// and drag-off remain as landed alternates. Commits immediately through the same path as
|
||||
// Alt-click; deletePoint's endpoint guard makes an endpoint right-click a safe no-op.
|
||||
// Right-clicks act only while the popup is open, and never during an in-flight left drag.
|
||||
if (!processor_ || view_ == View::kBrowse || curvePopup_ == CurveTarget::kNone) return;
|
||||
// Right-click deletes a node on EITHER spline surface — the primary delete affordance;
|
||||
// Alt-click and drag-off remain as landed alternates in the popup. deletePoint's endpoint
|
||||
// guard makes an endpoint right-click a safe no-op. Never acts during an in-flight left
|
||||
// drag; the modal popup wins when it is open.
|
||||
if (!processor_ || view_ == View::kBrowse) return;
|
||||
if (drag_ != DragKind::kNone) return;
|
||||
RECT rc{};
|
||||
GetClientRect(childHwnd_, &rc);
|
||||
const CurvePopupLayout pl = computeCurvePopup(rc.right - rc.left, rc.bottom - rc.top);
|
||||
if (!contains(pl.curveBox, x, y)) return;
|
||||
const VelocityCurve::Box box = curveBoxFromRect(pl.curveBox);
|
||||
const int idx = editedCurve().pointAtPixel(box, x, y);
|
||||
if (idx < 0) return;
|
||||
if (editedCurve().deletePoint(static_cast<std::size_t>(idx))) {
|
||||
hover_ = HoverTarget{}; // a stale kCurveNode index would light a shifted node
|
||||
commitAndReload();
|
||||
if (curvePopup_ != CurveTarget::kNone) {
|
||||
const CurvePopupLayout pl = computeCurvePopup(rc.right - rc.left, rc.bottom - rc.top);
|
||||
if (!contains(pl.curveBox, x, y)) return;
|
||||
const SplineEdit edit = resolveSplineEdit(editedCurve(), curveBoxFromRect(pl.curveBox),
|
||||
SplineGesture::kRight, x, y);
|
||||
if (edit.kind != SplineEditKind::kDelete) return;
|
||||
if (editedCurve().deletePoint(static_cast<std::size_t>(edit.index))) {
|
||||
hover_ = HoverTarget{}; // a stale kCurveNode index would light a shifted node
|
||||
commitAndReload();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!overlayIsSpline() || !overlayEnvEnabled(overlayEnv_, deckEnableState())) return;
|
||||
const FaceLayout fl = faceLayout(rc.right - rc.left, rc.bottom - rc.top);
|
||||
splineOverlayClick(waveformOverlayArea(fl.bands.waveform), x, y, SplineGesture::kRight,
|
||||
/*addOnEmptySpace=*/false);
|
||||
}
|
||||
|
||||
ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverCurvePopup(int w, int h, int x,
|
||||
|
||||
@@ -19,8 +19,7 @@ using namespace reasampler::instrument::ui;
|
||||
bool ReaSamplerEditor::deckKnobDisabled(int id) const {
|
||||
// Thin int-id wrapper over the pure, CTest-covered predicate — see deckKnobInert
|
||||
// (deck_groups.h) for which cells go inert and why.
|
||||
return deckKnobInert(static_cast<DeckParam>(id), params_.play.pitchEnv.enabled,
|
||||
params_.play.filter.enabled);
|
||||
return deckKnobInert(static_cast<DeckParam>(id), deckEnableState());
|
||||
}
|
||||
|
||||
bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#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"
|
||||
@@ -30,13 +31,24 @@ bool ReaSamplerEditor::mouseDownWaveform(const FaceLayout& fl, int x, int y) {
|
||||
if (frames <= 0) return false;
|
||||
const OverlayArea overlay = waveformOverlayArea(fl.bands.waveform);
|
||||
|
||||
// A drawn EG's own point actions come first, for the same reason the staged nodes do —
|
||||
// they sit on top of the markers. Its ADD, which any empty-space click satisfies, is held
|
||||
// back until after the markers below, or a spline overlay would make them unreachable.
|
||||
const DeckEnableState gates = deckEnableState();
|
||||
const bool splineLive = overlayIsSpline() && overlayEnvEnabled(overlayEnv_, gates);
|
||||
const SplineGesture gesture = (GetKeyState(VK_CONTROL) & 0x8000) != 0
|
||||
? SplineGesture::kControlLeft
|
||||
: SplineGesture::kLeft;
|
||||
if (splineLive && splineOverlayClick(overlay, x, y, gesture, /*addOnEmptySpace=*/false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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_, params_.play.pitchEnv.enabled, params_.play.filter.enabled);
|
||||
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);
|
||||
@@ -71,9 +83,54 @@ bool ReaSamplerEditor::mouseDownWaveform(const FaceLayout& fl, int x, int y) {
|
||||
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_);
|
||||
const SplineEdit edit = resolveSplineEdit(contour, box, gesture, x, y);
|
||||
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
|
||||
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;
|
||||
@@ -88,6 +145,17 @@ 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
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// editor_models.cpp — the ReaSamplerEditor's model-selection half: which STORED struct each
|
||||
// transient editor selection names. The overlay selection maps onto a staged envelope
|
||||
// (pack/unpack) or a drawn contour; a curve-popup target maps onto one of the three velocity
|
||||
// curves. One switch per family, so a moved field or a fourth curve is a one-place edit.
|
||||
// Split from editor_controls, which owns the orthogonal half: the control-value domain maps.
|
||||
|
||||
#include "shell/instrument/reasampler_editor.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <utility> // std::as_const (the const/non-const accessor pairs)
|
||||
|
||||
#include "core/instrument/map/trigger_seam.h" // triggerPlayLength (the Trigger play span)
|
||||
#include "shell/instrument/reasampler_processor.h"
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
using namespace reasampler::instrument::map; // PlaySeconds vocabulary + trigger_seam
|
||||
|
||||
namespace {
|
||||
// The two directions of the AHDSR <-> StageEnvelope copy, so a field can only be forgotten in
|
||||
// one place rather than two.
|
||||
void packAhdsr(const AdsrSeconds& a, StageEnvelope& env) {
|
||||
env.kind = instrument::ui::EnvKind::Ahdsr;
|
||||
env.attackSeconds = a.attackSeconds;
|
||||
env.holdSeconds = a.holdSeconds;
|
||||
env.decaySeconds = a.decaySeconds;
|
||||
env.sustainLevel = a.sustainLevel;
|
||||
env.releaseSeconds = a.releaseSeconds;
|
||||
env.attackCurve = a.attackCurve;
|
||||
env.decayCurve = a.decayCurve;
|
||||
env.releaseCurve = a.releaseCurve;
|
||||
}
|
||||
void unpackAhdsr(const StageEnvelope& env, AdsrSeconds& a) {
|
||||
a.attackSeconds = env.attackSeconds;
|
||||
a.holdSeconds = env.holdSeconds;
|
||||
a.decaySeconds = env.decaySeconds;
|
||||
a.sustainLevel = env.sustainLevel;
|
||||
a.releaseSeconds = env.releaseSeconds;
|
||||
a.attackCurve = env.attackCurve;
|
||||
a.decayCurve = env.decayCurve;
|
||||
a.releaseCurve = env.releaseCurve;
|
||||
}
|
||||
void packAhd(const AhdSeconds& a, double originSeconds, double spanSeconds, StageEnvelope& env) {
|
||||
env.kind = instrument::ui::EnvKind::Ahd;
|
||||
env.attackSeconds = a.attackSeconds;
|
||||
env.decaySeconds = a.decaySeconds;
|
||||
env.holdFraction = a.holdFraction;
|
||||
env.attackCurve = a.attackCurve;
|
||||
env.decayCurve = a.decayCurve;
|
||||
env.originSeconds = originSeconds;
|
||||
env.spanSeconds = spanSeconds;
|
||||
}
|
||||
void unpackAhd(const StageEnvelope& env, AhdSeconds& a) {
|
||||
a.attackSeconds = env.attackSeconds;
|
||||
a.decaySeconds = env.decaySeconds;
|
||||
a.holdFraction = env.holdFraction;
|
||||
a.attackCurve = env.attackCurve;
|
||||
a.decayCurve = env.decayCurve;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
StageEnvelope ReaSamplerEditor::packEnvelope(OverlayEnv which, const PlaySeconds& play,
|
||||
std::int64_t frames,
|
||||
std::int64_t startFrame) const {
|
||||
StageEnvelope env;
|
||||
const double rate = liveSampleRate();
|
||||
const double t0 = rate > 0.0 ? static_cast<double>(startFrame) / rate : 0.0;
|
||||
// The Trigger amp and filter AHDs live over the PLAY span; the pitch AHD over the whole
|
||||
// post-start span, since it keeps running after a Trigger one-shot's amplitude has ended.
|
||||
const std::int64_t playLen =
|
||||
triggerPlayLength(play.trigger.lengthFraction, frames, startFrame);
|
||||
const double playSpan = rate > 0.0 ? static_cast<double>(playLen) / rate : 0.0;
|
||||
const double fullSpan =
|
||||
rate > 0.0 ? static_cast<double>((std::max)(std::int64_t{0}, frames - startFrame)) / rate
|
||||
: 0.0;
|
||||
const bool trigger = (play.playMode == PlayMode::Trigger);
|
||||
switch (which) {
|
||||
case OverlayEnv::kPitch:
|
||||
packAhd(play.pitchEnv.shape, t0, fullSpan, env);
|
||||
break;
|
||||
case OverlayEnv::kFilter:
|
||||
if (trigger) packAhd(play.filter.trigEnv, t0, playSpan, env);
|
||||
else packAhdsr(play.filter.env, env);
|
||||
break;
|
||||
case OverlayEnv::kAmp:
|
||||
if (trigger) packAhd(play.trigAhd, t0, playSpan, env);
|
||||
else packAhdsr(play.adsr, env);
|
||||
break;
|
||||
case OverlayEnv::kNone:
|
||||
break; // nothing is overlay-active; a default-constructed StageEnvelope, not amp
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::unpackEnvelope(OverlayEnv which, const StageEnvelope& env,
|
||||
PlaySeconds& play) const {
|
||||
const bool trigger = (play.playMode == PlayMode::Trigger);
|
||||
switch (which) {
|
||||
case OverlayEnv::kPitch:
|
||||
unpackAhd(env, play.pitchEnv.shape);
|
||||
break;
|
||||
case OverlayEnv::kFilter:
|
||||
if (trigger) unpackAhd(env, play.filter.trigEnv);
|
||||
else unpackAhdsr(env, play.filter.env);
|
||||
break;
|
||||
case OverlayEnv::kAmp:
|
||||
if (trigger) unpackAhd(env, play.trigAhd);
|
||||
else unpackAhdsr(env, play.adsr);
|
||||
break;
|
||||
case OverlayEnv::kNone:
|
||||
break; // nothing is overlay-active, so there is nothing a drag could have edited
|
||||
}
|
||||
}
|
||||
|
||||
instrument::ui::DeckEnableState ReaSamplerEditor::deckEnableState() const {
|
||||
const PlaySeconds& play = params_.play;
|
||||
return instrument::ui::DeckEnableState{
|
||||
play.pitchEnv.enabled, play.filter.enabled,
|
||||
play.ampSpline.mode == EnvMode::Spline,
|
||||
play.pitchSpline.mode == EnvMode::Spline,
|
||||
play.filterSpline.mode == EnvMode::Spline};
|
||||
}
|
||||
|
||||
const VelocityCurve& ReaSamplerEditor::splineFor(OverlayEnv which) const {
|
||||
switch (which) {
|
||||
case OverlayEnv::kPitch: return params_.play.pitchSpline.contour;
|
||||
case OverlayEnv::kFilter: return params_.play.filterSpline.contour;
|
||||
case OverlayEnv::kAmp:
|
||||
case OverlayEnv::kNone:
|
||||
break;
|
||||
}
|
||||
return params_.play.ampSpline.contour;
|
||||
}
|
||||
|
||||
VelocityCurve& ReaSamplerEditor::splineFor(OverlayEnv which) {
|
||||
return const_cast<VelocityCurve&>(std::as_const(*this).splineFor(which));
|
||||
}
|
||||
|
||||
bool ReaSamplerEditor::overlayIsSpline() const {
|
||||
switch (overlayEnv_) {
|
||||
case OverlayEnv::kAmp: return params_.play.ampSpline.mode == EnvMode::Spline;
|
||||
case OverlayEnv::kPitch: return params_.play.pitchSpline.mode == EnvMode::Spline;
|
||||
case OverlayEnv::kFilter: return params_.play.filterSpline.mode == EnvMode::Spline;
|
||||
case OverlayEnv::kNone: return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const VelocityCurve& ReaSamplerEditor::curveFor(CurveTarget target) const {
|
||||
switch (target) {
|
||||
case CurveTarget::kPitch: return params_.play.pitchVelocityCurve;
|
||||
case CurveTarget::kFilter: return params_.play.filter.velocityCurve;
|
||||
case CurveTarget::kAmp:
|
||||
case CurveTarget::kNone:
|
||||
// kNone reads as amp — a valid, harmless choice for a target-agnostic caller (a
|
||||
// disabled deck cell still needs SOME curve to draw). editedCurve()'s mutable
|
||||
// overload below refuses kNone rather than relying on this fallback to protect amp.
|
||||
return params_.velocityCurve;
|
||||
}
|
||||
return params_.velocityCurve;
|
||||
}
|
||||
|
||||
VelocityCurve& ReaSamplerEditor::curveFor(CurveTarget target) {
|
||||
return const_cast<VelocityCurve&>(std::as_const(*this).curveFor(target));
|
||||
}
|
||||
|
||||
const VelocityCurve& ReaSamplerEditor::editedCurve() const { return curveFor(curvePopup_); }
|
||||
|
||||
VelocityCurve& ReaSamplerEditor::editedCurve() {
|
||||
// Refuses kNone rather than aliasing amp (see curvePopup_'s declaration for why); should be
|
||||
// unreachable now that every writer of kNone also cancels the drag, but this is the second,
|
||||
// independent line of defense.
|
||||
if (curvePopup_ == CurveTarget::kNone) {
|
||||
static VelocityCurve sink = VelocityCurve::flat();
|
||||
return sink;
|
||||
}
|
||||
return curveFor(curvePopup_);
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::closeCurvePopup() {
|
||||
curvePopup_ = CurveTarget::kNone;
|
||||
if (drag_ == DragKind::kCurveNode) {
|
||||
drag_ = DragKind::kNone;
|
||||
curvePointIndex_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace reasampler::vst
|
||||
@@ -30,13 +30,16 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
|
||||
// One compact-toggle draw (the Mono/Stereo segment grammar at Micro scale). Disabled
|
||||
// segments draw inert so the dependency (Retrig|Legato needs Mono) reads at a glance.
|
||||
// `seg0Disabled` disables ONE segment: Gate is unselectable while an EG is drawn, but
|
||||
// Trigger — the mode it is stuck in — must still read as the live choice.
|
||||
const auto drawToggle = [&](const DeckToggleLayout& t, const char* s0, const char* s1,
|
||||
bool seg1Active, bool disabled) {
|
||||
bool seg1Active, bool disabled, bool seg0Disabled = false) {
|
||||
const bool hov = !disabled && isHovered(HoverKind::kControl, t.id);
|
||||
const bool d0 = disabled || seg0Disabled;
|
||||
const InteractionState st0 =
|
||||
disabled ? InteractionState::Disabled
|
||||
: (!seg1Active ? InteractionState::Active
|
||||
: (hov ? InteractionState::Hover : InteractionState::Rest));
|
||||
d0 ? InteractionState::Disabled
|
||||
: (!seg1Active ? InteractionState::Active
|
||||
: (hov ? InteractionState::Hover : InteractionState::Rest));
|
||||
const InteractionState st1 =
|
||||
disabled ? InteractionState::Disabled
|
||||
: (seg1Active ? InteractionState::Active
|
||||
@@ -44,12 +47,13 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
fillSurface(bmp, toKitBox(t.seg0), Role::BgCell, st0);
|
||||
fillSurface(bmp, toKitBox(t.seg1), Role::BgCell, st1);
|
||||
kitTextCentered(bmp, t.seg0, s0, Font::Micro,
|
||||
disabled ? Role::TextDim
|
||||
: (!seg1Active ? Role::BgBase : Role::TextPrimary));
|
||||
d0 ? Role::TextDim
|
||||
: (!seg1Active ? Role::BgBase : Role::TextPrimary));
|
||||
kitTextCentered(bmp, t.seg1, s1, Font::Micro,
|
||||
disabled ? Role::TextDim
|
||||
: (seg1Active ? Role::BgBase : Role::TextPrimary));
|
||||
};
|
||||
const bool anySpline = splineActive(play);
|
||||
|
||||
// The knob's short name label (swapped for the live value during hover/drag — no third
|
||||
// line, no permanent value clutter).
|
||||
@@ -132,25 +136,32 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
|
||||
}
|
||||
}
|
||||
|
||||
// The compact caption toggle (right-anchored in the caption row, never full-width).
|
||||
if (g.captionToggle.id >= 0) {
|
||||
switch (static_cast<ParamControl>(g.captionToggle.id)) {
|
||||
// The compact caption toggles (right-anchored in the caption row, never full-width).
|
||||
for (const DeckToggleLayout* tp : {&g.captionToggle, &g.captionToggle2}) {
|
||||
if (tp->id < 0) continue;
|
||||
const DeckToggleLayout& t = *tp;
|
||||
switch (static_cast<ParamControl>(t.id)) {
|
||||
case ParamControl::kPlayMode:
|
||||
drawToggle(g.captionToggle, "Gate", "Trigger",
|
||||
play.playMode == PlayMode::Trigger, false);
|
||||
drawToggle(t, "Gate", "Trigger", play.playMode == PlayMode::Trigger, false,
|
||||
/*seg0Disabled=*/anySpline);
|
||||
break;
|
||||
case ParamControl::kPitchEngine:
|
||||
drawToggle(g.captionToggle, "Varisp", "Presrv",
|
||||
drawToggle(t, "Varisp", "Presrv",
|
||||
play.pitchEngine == PitchEngine::Preserve, false);
|
||||
break;
|
||||
case ParamControl::kPitchEnvEnable:
|
||||
drawToggle(g.captionToggle, "Off", "On", play.pitchEnv.enabled, false);
|
||||
drawToggle(t, "Off", "On", play.pitchEnv.enabled, false);
|
||||
break;
|
||||
case ParamControl::kVoiceMode:
|
||||
drawToggle(g.captionToggle, "Poly", "Mono", isMono, false);
|
||||
drawToggle(t, "Poly", "Mono", isMono, false);
|
||||
break;
|
||||
case ParamControl::kFilterEnable:
|
||||
drawToggle(g.captionToggle, "Off", "On", play.filter.enabled, false);
|
||||
drawToggle(t, "Off", "On", play.filter.enabled, false);
|
||||
break;
|
||||
case ParamControl::kAmpEnvMode:
|
||||
case ParamControl::kPitchEnvMode:
|
||||
case ParamControl::kFilterEnvMode:
|
||||
drawToggle(t, "Stg", "Spl", deckControlNorm(t.id) > 0.5, false);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "core/audio/peaks.h" // computeEnvelope (waveform binning)
|
||||
#include "core/instrument/ui/spline_edit.h" // splineOverlayBox (the contour's mapping box)
|
||||
#include "core/instrument/ui/waveform_view.h" // waveformSurface / laneEnvelope / frameToX
|
||||
#include "shell/instrument/editor_internal.h" // kit adapters
|
||||
#include "shell/instrument/reasampler_processor.h"
|
||||
@@ -131,11 +132,59 @@ void ReaSamplerEditor::paintWaveform(LICE_IBitmap* bmp, const Rect& band) {
|
||||
paintEnvelopeOverlay(bmp, overlay, frames);
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::paintSplineOverlay(LICE_IBitmap* bmp, const OverlayArea& waveArea) {
|
||||
const VelocityCurve::Box box = splineOverlayBox(waveArea);
|
||||
if (box.width <= 0 || box.height <= 1) return;
|
||||
const VelocityCurve& curve = splineFor(overlayEnv_);
|
||||
|
||||
// One eval per drawn column, through the curve's own pixel maps, so the trace and the
|
||||
// handles share the coordinate system the hit-test resolves against.
|
||||
const LICE_pixel line = toLice(roleColor(Role::OverlayTrace));
|
||||
int prevX = 0, prevY = 0;
|
||||
for (int px = 0; px <= box.width; ++px) {
|
||||
const int cx = box.left + px;
|
||||
const double t = curve.pointFromPixel(box, cx, box.top).velocity;
|
||||
const int cy = curve.pixelFromPoint(box, {t, curve.eval(t)}).y;
|
||||
if (px > 0) LICE_Line(bmp, prevX, prevY, cx, cy, line, 1.0f, 0, true);
|
||||
prevX = cx;
|
||||
prevY = cy;
|
||||
}
|
||||
|
||||
// Handles carry two independent states on the same mark, so they use two independent
|
||||
// channels: SIZE is the grab (the staged painter's grammar — a hotter hue reads as lower
|
||||
// contrast over the lime, see its note), and HOLLOW is hard. The corner itself is the
|
||||
// primary hard cue, but a corner between two near-collinear segments has none to show.
|
||||
const LICE_pixel handle = toLice(roleColor(Role::OverlayTrace));
|
||||
const LICE_pixel core = toLice(roleColor(Role::BgBase));
|
||||
const std::vector<instrument::engine::VelocityPoint>& pts = curve.points();
|
||||
for (std::size_t i = 0; i < pts.size(); ++i) {
|
||||
const auto np = curve.pixelFromPoint(box, pts[i]);
|
||||
const bool grabbed =
|
||||
(drag_ == DragKind::kSplineNode && curvePointIndex_ == static_cast<int>(i));
|
||||
const int r = grabbed ? kEnvHandleGrabbedRadius : kEnvHandleRadius;
|
||||
const int ir = r - kEnvHandleRingPx;
|
||||
const int hx = (std::max)(box.left + r,
|
||||
(std::min)(box.left + box.width - 1 - r, np.x));
|
||||
const int hy = (std::max)(box.top + r,
|
||||
(std::min)(box.top + box.height - 1 - r, np.y));
|
||||
LICE_FillRect(bmp, hx - r, hy - r, 2 * r, 2 * r, handle, 1.0f, 0);
|
||||
if (pts[i].hard && ir > 0) {
|
||||
LICE_FillRect(bmp, hx - ir, hy - ir, 2 * ir, 2 * ir, core, 1.0f, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const OverlayArea& waveArea,
|
||||
std::int64_t frames) {
|
||||
if (overlayEnv_ == OverlayEnv::kNone) return; // no envelope selected is a resting state
|
||||
const Rect& area = waveArea.rect;
|
||||
if (frames <= 0 || area.width <= 0 || area.height <= 0) return;
|
||||
if (overlayIsSpline()) {
|
||||
// The contour is a pure function of normalized position, so it needs neither the frame
|
||||
// count nor the live rate the staged path below resolves its seconds against.
|
||||
paintSplineOverlay(bmp, waveArea);
|
||||
return;
|
||||
}
|
||||
const double rate = liveSampleRate();
|
||||
if (rate <= 0.0) return;
|
||||
const double totalSeconds = static_cast<double>(frames) / rate;
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "core/instrument/ui/envelope_overlay.h" // StageEnvelope / EnvNode (envelope overlay draw seam)
|
||||
#include "core/instrument/ui/knob_deck.h" // DeckGroupDesc / DeckLayout (the deck band)
|
||||
#include "core/instrument/ui/sample_bands.h" // SampleBands (the band-stack allocator)
|
||||
#include "core/instrument/ui/spline_edit.h" // the shared point-editing grammar
|
||||
#include "core/instrument/ui/sample_chrome.h" // ChromeRects (chrome-band interior)
|
||||
#include "core/audio/peaks.h" // Envelope (the cached peak thumbnail)
|
||||
#include "core/instrument/map/sample_map.h" // SampleChoice, BankChoice, InstrumentParams
|
||||
@@ -78,8 +79,11 @@ private:
|
||||
// What a mouse drag is currently editing. kWaveMarker/kEnvNode/kCurveNode track their
|
||||
// grabbed item in waveMarker_/envNode_/curvePointIndex_; kDeckKnob is a grab-anchored
|
||||
// knob drag (control in dragParamId_, grab value in dragKnobStartValue_).
|
||||
// kSplineNode is the overlay's peer of kCurveNode: the same VelocityCurve point drag, over
|
||||
// the waveform overlay's box and the overlay-active envelope's contour rather than the
|
||||
// popup's box and curve.
|
||||
enum class DragKind { kNone, kRootMarker, kWaveMarker, kScrollThumb, kEnvNode,
|
||||
kCurveNode, kDeckKnob };
|
||||
kCurveNode, kSplineNode, kDeckKnob };
|
||||
|
||||
// Which envelope the waveform overlay is drawing and editing. The selection type and its
|
||||
// whole state machine are the pure deck_groups module's; this alias keeps the shell's
|
||||
@@ -168,9 +172,20 @@ private:
|
||||
// The velocity transfer-curve editor (X = velocity 0-127, Y = the curve's own domain); its
|
||||
// only host is the popup sheet. `r` empty -> draws nothing.
|
||||
void paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r);
|
||||
// Traces the amp-envelope overlay + its draggable node handles over `waveArea`, ONCE at
|
||||
// full band height (never per lane).
|
||||
// Traces the overlay-active envelope + its draggable handles over `waveArea`, ONCE at
|
||||
// full band height (never per lane). Dispatches on the envelope's mode: the staged
|
||||
// polyline, or the drawn contour below.
|
||||
void paintEnvelopeOverlay(LICE_IBitmap* bmp, const OverlayArea& waveArea, std::int64_t frames);
|
||||
// The spline EG's contour + point handles, spanning the overlay 1:1 with the sample's time
|
||||
// axis. Hard points draw hollow so a corner is legible before it is steep.
|
||||
void paintSplineOverlay(LICE_IBitmap* bmp, const OverlayArea& waveArea);
|
||||
|
||||
// A click on the spline overlay, routed through the shared grammar; true when it consumed
|
||||
// the click. `addOnEmptySpace` false resolves node actions only — the waveform band calls
|
||||
// it that way BEFORE the start/loop markers and again after, since the contour's box is the
|
||||
// whole band and an unconditional add would make every marker unreachable.
|
||||
bool splineOverlayClick(const OverlayArea& waveArea, int x, int y,
|
||||
instrument::ui::SplineGesture gesture, bool addOnEmptySpace);
|
||||
|
||||
// --- Input: the mouse-down dispatch and its per-band branches ---
|
||||
void onMouseDown(int x, int y);
|
||||
@@ -194,13 +209,15 @@ private:
|
||||
|
||||
void onMouseMove(int x, int y);
|
||||
void onMouseUp(int x, int y);
|
||||
// Right-click is the curve popup's primary node-delete affordance; only acts while the
|
||||
// popup is open (deletePoint's endpoint guard makes an endpoint right-click a no-op).
|
||||
// Right-click is the primary node-delete affordance on both spline surfaces — the popup
|
||||
// while it is open, else the spline EG overlay (deletePoint's endpoint guard makes an
|
||||
// endpoint right-click a no-op).
|
||||
void onMouseRDown(int x, int y);
|
||||
|
||||
// Mouse-down inside curve-editor box `r`: a node grab starts a kCurveNode drag;
|
||||
// Alt-click on an interior node deletes it at once; an empty-space click adds a point
|
||||
// and grabs it.
|
||||
// Mouse-down inside curve-editor box `r`, resolved through the shared point-editing
|
||||
// grammar (spline_edit): a node grab starts a kCurveNode drag, an empty-space click adds a
|
||||
// point and grabs it, control-click toggles a node hard/smooth. Alt-click delete predates
|
||||
// that grammar and is kept as a landed alternate.
|
||||
void handleCurveMouseDown(const Rect& r, int x, int y);
|
||||
|
||||
// Left-click while the curve popup is open (modal over the Sample face): Close /
|
||||
@@ -466,6 +483,20 @@ private:
|
||||
CurveTarget curvePopup_ = CurveTarget::kNone;
|
||||
void closeCurvePopup();
|
||||
|
||||
// The group-gate state the two pure inert predicates read, built once from the parameter
|
||||
// set so paint and hit-test can never assemble it differently.
|
||||
instrument::ui::DeckEnableState deckEnableState() const;
|
||||
|
||||
// THE one switch from an overlay selection to the drawn contour it names, mirroring
|
||||
// curveFor. kNone reads as amp — harmless for a target-agnostic caller, and every mutating
|
||||
// path is gated on overlayIsSpline() first.
|
||||
const VelocityCurve& splineFor(OverlayEnv which) const;
|
||||
VelocityCurve& splineFor(OverlayEnv which);
|
||||
|
||||
// Whether the overlay-active envelope is in Spline mode — the branch every overlay paint,
|
||||
// hit-test and drag path takes before touching either model.
|
||||
bool overlayIsSpline() const;
|
||||
|
||||
// THE one switch from a CurveTarget to the parameter-set curve it names — paint (button
|
||||
// thumbnails, for every target) and edit (editedCurve, for curvePopup_ specifically) both
|
||||
// route through it, so a fourth curve or a moved field is a one-place edit.
|
||||
|
||||
Reference in New Issue
Block a user