instrument: one VELOCITY deck for all three velocity curves, bipolar and off by default for pitch and filter

Payload v12 appends the new velocity->pitch curve and folds the retired filter velAmount into its now-bipolar curve, so pre-v12 projects reopen sounding identical. Preview button takes a drawn play triangle.
This commit is contained in:
2026-07-31 19:15:17 -04:00
parent 4fecb58c0a
commit 9d38f87a2d
37 changed files with 1020 additions and 320 deletions
+18 -4
View File
@@ -11,6 +11,7 @@
#include <cstdint>
#include <cstdio> // snprintf (deck value labels)
#include <string>
#include <utility> // std::as_const (the const/non-const editedCurve pair)
#include <vector>
#include "core/instrument/engine/filter/filter_params.h" // the filter's own control laws
@@ -132,7 +133,6 @@ double ReaSamplerEditor::controlValue(int id, const PlaySeconds& play) const {
case ParamControl::kFilterQ: return clamp01(play.filter.settings.resonanceNorm);
case ParamControl::kFilterDrive: return clamp01(play.filter.settings.driveNorm);
case ParamControl::kFilterModAmt: return deckNormFromBipolar(play.filter.modAmount);
case ParamControl::kFilterVel: return deckNormFromBipolar(play.filter.velAmount);
case ParamControl::kFilterKeyTrack:return clamp01(play.filter.keyTrack / kKeyTrackMax);
case ParamControl::kFilterEnvAttack: return secToNorm(play.filter.env.attackSeconds);
case ParamControl::kFilterEnvHold: return secToNorm(play.filter.env.holdSeconds);
@@ -215,7 +215,6 @@ void ReaSamplerEditor::applyControl(int id, PlaySeconds& play, double value,
case ParamControl::kFilterDrive:
play.filter.settings.driveNorm = static_cast<float>(clamp01(value)); break;
case ParamControl::kFilterModAmt: play.filter.modAmount = deckBipolarFromNorm(value); break;
case ParamControl::kFilterVel: play.filter.velAmount = deckBipolarFromNorm(value); break;
case ParamControl::kFilterKeyTrack:
play.filter.keyTrack = clamp01(value) * kKeyTrackMax; break;
case ParamControl::kFilterEnvAttack:
@@ -363,8 +362,6 @@ std::string ReaSamplerEditor::deckValueLabel(int id) const {
break;
case ParamControl::kFilterModAmt:
snprintf(buf, sizeof(buf), "%+.0f%%", play.filter.modAmount * 100.0); break;
case ParamControl::kFilterVel:
snprintf(buf, sizeof(buf), "%+.0f%%", play.filter.velAmount * 100.0); break;
case ParamControl::kFilterKeyTrack:
snprintf(buf, sizeof(buf), "%.0f%%", play.filter.keyTrack * 100.0); break;
case ParamControl::kFilterEnvAttack:
@@ -513,6 +510,23 @@ void ReaSamplerEditor::unpackEnvelope(OverlayEnv which, const StageEnvelope& env
}
}
const VelocityCurve& ReaSamplerEditor::editedCurve() const {
switch (curvePopup_) {
case CurveTarget::kPitch: return params_.play.pitchVelocityCurve;
case CurveTarget::kFilter: return params_.play.filter.velocityCurve;
case CurveTarget::kAmp:
case CurveTarget::kNone:
// kNone only reaches here from a paint/hover racing the close; the amp curve is a
// valid, harmless read rather than a branch every caller would have to repeat.
return params_.velocityCurve;
}
return params_.velocityCurve;
}
VelocityCurve& ReaSamplerEditor::editedCurve() {
return const_cast<VelocityCurve&>(std::as_const(*this).editedCurve());
}
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.
+2 -2
View File
@@ -121,7 +121,7 @@ void ReaSamplerEditor::onMouseUp(int x, int y) {
y < curveRect.y - kCurveDragOffMargin ||
y > curveRect.bottom() + kCurveDragOffMargin;
if (off) {
params_.velocityCurve.deletePoint(static_cast<std::size_t>(curveIdx));
editedCurve().deletePoint(static_cast<std::size_t>(curveIdx));
hover_ = HoverTarget{}; // stale kCurveNode index would light a shifted node
}
}
@@ -139,7 +139,7 @@ void ReaSamplerEditor::resolveHover(int x, int y) {
HoverTarget h; // kNone by default
if (view_ == View::kBrowse) {
h = hoverBrowse(w, hgt, x, y);
} else if (curvePopupOpen_) { // modal over the face
} else if (curvePopup_ != CurveTarget::kNone) { // modal over the face
h = hoverCurvePopup(w, hgt, x, y);
} else {
const FaceLayout fl = faceLayout(w, hgt);
+2 -2
View File
@@ -132,8 +132,8 @@ void ReaSamplerEditor::onMouseWheel(int delta) {
void ReaSamplerEditor::onSearchChar(unsigned int ch) {
// The curve popup: Esc dismisses (checked first — the popup is modal over the face, and
// the Browse search cannot hold focus under it).
if (curvePopupOpen_ && ch == 27) {
curvePopupOpen_ = false;
if (curvePopup_ != CurveTarget::kNone && ch == 27) {
curvePopup_ = CurveTarget::kNone;
invalidate();
return;
}
+2 -9
View File
@@ -1,6 +1,6 @@
// editor_input_chrome.cpp — the CHROME band's input: the Browse nav, the preview trigger,
// the preview-velocity knob grab, the curve-button summon, the channel toggle, and the
// piano strip's root grab plus its live drag. Windows-only.
// the preview-velocity knob grab, the channel toggle, and the piano strip's root grab plus
// its live drag. Windows-only.
#include "shell/instrument/reasampler_editor.h"
@@ -51,12 +51,6 @@ bool ReaSamplerEditor::mouseDownChrome(const FaceLayout& fl, int x, int y) {
invalidate();
return true;
}
// The mini curve-preview button: summon the popup editor.
if (contains(cr.curveBtn, x, y)) {
curvePopupOpen_ = true;
invalidate();
return true;
}
if (contains(cr.chanMono, x, y)) {
channelMode_ = ChannelMode::Mono;
processor_->setChannelMode(ChannelMode::Mono);
@@ -105,7 +99,6 @@ ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverChrome(const FaceLayout& fl
if (selectedId_.empty()) return {}; // empty state — no interactive surfaces beyond nav
if (contains(cr.preview, x, y)) return {HoverKind::kPreview, -1};
if (contains(cr.velCell, x, y)) return {HoverKind::kVelKnob, -1};
if (contains(cr.curveBtn, x, y)) return {HoverKind::kCurveButton, -1};
if (contains(cr.chanMono, x, y)) return {HoverKind::kChanMono, -1};
if (contains(cr.chanStereo, x, y)) return {HoverKind::kChanStereo, -1};
if (!cr.rootStrip.empty()) {
+13 -13
View File
@@ -20,10 +20,10 @@ bool ReaSamplerEditor::handlePopupMouseDown(int w, int h, int x, int y) {
// While open the sheet is modal over the face — it owns every left-click. Close click /
// outside-wash click dismiss (outside only when no drag is in flight); in-box clicks
// route to the curve machinery; anything else on the sheet is swallowed.
if (!curvePopupOpen_) return false;
if (curvePopup_ == CurveTarget::kNone) return false;
const CurvePopupLayout pl = computeCurvePopup(w, h);
if (contains(pl.close, x, y)) {
curvePopupOpen_ = false;
curvePopup_ = CurveTarget::kNone;
invalidate();
return true;
}
@@ -32,7 +32,7 @@ bool ReaSamplerEditor::handlePopupMouseDown(int w, int h, int x, int y) {
return true;
}
if (popupOutsideSheet(pl, x, y) && drag_ == DragKind::kNone) {
curvePopupOpen_ = false;
curvePopup_ = CurveTarget::kNone;
invalidate();
}
return true;
@@ -42,12 +42,12 @@ 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 = params_.velocityCurve.pointAtPixel(box, x, y);
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 (params_.velocityCurve.deletePoint(static_cast<std::size_t>(idx))) {
if (editedCurve().deletePoint(static_cast<std::size_t>(idx))) {
hover_ = HoverTarget{}; // a stale kCurveNode index would light a shifted node
commitAndReload();
}
@@ -66,8 +66,8 @@ void ReaSamplerEditor::handleCurveMouseDown(const Rect& r, int x, int y) {
const bool inBox = (x >= box.left && x < box.left + box.width &&
y >= box.top && y < box.top + box.height);
if (inBox) {
const VelocityPoint p = VelocityCurve::pointFromPixel(box, x, y);
idx = static_cast<int>(params_.velocityCurve.addPoint(p.velocity, p.amp));
const VelocityPoint p = editedCurve().pointFromPixel(box, x, y);
idx = static_cast<int>(editedCurve().addPoint(p.velocity, p.value));
}
}
@@ -75,7 +75,7 @@ void ReaSamplerEditor::handleCurveMouseDown(const Rect& r, int x, int y) {
drag_ = DragKind::kCurveNode;
curvePointIndex_ = idx;
dragStartCurve_ = params_.velocityCurve; // AFTER the add — resolvePointDrag's delta base
dragStartCurve_ = editedCurve(); // AFTER the add — resolvePointDrag's delta base
dragCurveRect_ = r;
dragStartX_ = x;
dragStartY_ = y;
@@ -87,7 +87,7 @@ void ReaSamplerEditor::dragCurve(int x, int y) {
// (box + neighbour-X + endpoint-pin clamps), against the grab-time curve + box (absolute
// delta — the mirror of the envelope-node drag). Live feedback only; commit on release.
if (curvePointIndex_ < 0) return;
params_.velocityCurve = VelocityCurve::resolvePointDrag(
editedCurve() = VelocityCurve::resolvePointDrag(
dragStartCurve_, static_cast<std::size_t>(curvePointIndex_),
curveBoxFromRect(dragCurveRect_), x - dragStartX_, y - dragStartY_);
invalidate();
@@ -98,16 +98,16 @@ void ReaSamplerEditor::onMouseRDown(int x, int y) {
// 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 || !curvePopupOpen_) return;
if (!processor_ || view_ == View::kBrowse || curvePopup_ == CurveTarget::kNone) 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 = params_.velocityCurve.pointAtPixel(box, x, y);
const int idx = editedCurve().pointAtPixel(box, x, y);
if (idx < 0) return;
if (params_.velocityCurve.deletePoint(static_cast<std::size_t>(idx))) {
if (editedCurve().deletePoint(static_cast<std::size_t>(idx))) {
hover_ = HoverTarget{}; // a stale kCurveNode index would light a shifted node
commitAndReload();
}
@@ -120,7 +120,7 @@ ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverCurvePopup(int w, int h, in
if (!contains(pl.curveBox, x, y)) return {};
// A curve node under the pointer lights accent-hot.
const int idx =
params_.velocityCurve.pointAtPixel(curveBoxFromRect(pl.curveBox), x, y);
editedCurve().pointAtPixel(curveBoxFromRect(pl.curveBox), x, y);
if (idx < 0) return {};
return {HoverKind::kCurveNode, idx};
}
+10 -1
View File
@@ -28,8 +28,10 @@ bool ReaSamplerEditor::deckKnobDisabled(int id) const {
case ParamControl::kFilterQ:
case ParamControl::kFilterDrive:
case ParamControl::kFilterModAmt:
case ParamControl::kFilterVel:
case ParamControl::kFilterKeyTrack:
// The filter's velocity curve sits in the VELOCITY group but is a filter parameter:
// it goes inert with every other one, so no surface can reach a param the knobs can't.
case ParamControl::kFilterVelCurve:
case ParamControl::kFilterEnvAttack:
case ParamControl::kFilterEnvHold:
case ParamControl::kFilterEnvDecay:
@@ -96,6 +98,13 @@ bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
if (hit.kind == DeckHitKind::Knob) {
// Knobs of a disabled group are drawn but inert.
if (deckKnobDisabled(hit.id)) return true;
// A VELOCITY cell summons the popup editor instead of starting a knob drag.
const CurveTarget curveCell = curveTargetFor(hit.id);
if (curveCell != CurveTarget::kNone) {
curvePopup_ = curveCell;
invalidate();
return true;
}
// A grab on the inner disc drags the CURVE control instead, but only where the stage
// is sloped; on a Hold or Sustain cell the inner region is just more of the knob.
const ParamControl curve = curveParamFor(static_cast<ParamControl>(hit.id));
+1 -1
View File
@@ -70,7 +70,7 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) {
paintDeck(bmp, fl);
// The curve popup: a centered sheet over the whole face, drawn last.
if (curvePopupOpen_) paintCurvePopup(bmp, w, h);
if (curvePopup_ != CurveTarget::kNone) paintCurvePopup(bmp, w, h);
// The piano strip's note-name chip overhangs its band, so it goes on top of everything.
paintChromeTooltip(bmp, fl, w, h);
+12 -5
View File
@@ -129,12 +129,22 @@ void ReaSamplerEditor::paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool
if (empty) return;
// Preview-trigger button (fires the loaded capture at root through the live voice engine).
// A drawn play triangle rather than a label or an embedded image: it inherits the button's
// own foreground role, so it stays legible in every interaction state at no build cost.
{
const KitButtonBox box{toKitBox(cr.preview)};
const InteractionState st = (previewingNote_ >= 0) ? InteractionState::Active
const bool active = (previewingNote_ >= 0);
const InteractionState st = active ? InteractionState::Active
: (isHovered(HoverKind::kPreview, -1) ? InteractionState::Hover
: InteractionState::Rest);
drawButton(bmp, box, "Preview", st, /*warn=*/false);
drawButton(bmp, box, nullptr, st, /*warn=*/false);
const PreviewGlyph g = previewGlyph(cr.preview);
if (!g.empty()) {
const LICE_pixel ink =
toLice(roleColor(active ? Role::BgBase : Role::TextPrimary));
LICE_FillTriangle(bmp, g.leftX, g.topY, g.leftX, g.bottomY, g.apexX, g.apexY,
ink, 1.0f, 0);
}
}
// Preview velocity: a radial knob cell (the deck cell grammar), bound to the same
@@ -156,9 +166,6 @@ void ReaSamplerEditor::paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool
}
}
// The mini curve-preview button: opens the popup editor.
paintCurveButton(bmp, cr.curveBtn);
// Mono | Stereo output-mode toggle.
{
const bool isStereo = (channelMode_ == ChannelMode::Stereo);
+65 -20
View File
@@ -1,6 +1,6 @@
// editor_paint_curve.cpp — the velocity->amp curve surfaces: the chrome band's mini
// preview button and the modal popup sheet that hosts the full editor. Band-independent
// (the popup floats over the whole face). Windows-only.
// editor_paint_curve.cpp — the velocity-curve surfaces: the VELOCITY deck group's mini
// thumbnails and the modal popup sheet that hosts the full editor. Band-independent (the
// popup floats over the whole face). Windows-only.
#include "shell/instrument/reasampler_editor.h"
@@ -15,28 +15,66 @@ namespace reasampler::vst {
using namespace reasampler::ui; // kit vocabulary
using namespace reasampler::instrument::ui; // popup geometry
void ReaSamplerEditor::paintCurveButton(LICE_IBitmap* bmp, const Rect& r) {
namespace {
// The curve a VELOCITY cell shows. Mirrors editedCurve's routing for the popup's own target;
// a cell draws whichever curve it opens, whether or not the popup is up.
const VelocityCurve& curveFor(const InstrumentParams& p, CurveTarget target) {
switch (target) {
case CurveTarget::kPitch: return p.play.pitchVelocityCurve;
case CurveTarget::kFilter: return p.play.filter.velocityCurve;
case CurveTarget::kAmp:
case CurveTarget::kNone:
return p.velocityCurve;
}
return p.velocityCurve;
}
const char* curveTitle(CurveTarget target) {
switch (target) {
case CurveTarget::kPitch: return "VELOCITY -> PITCH";
case CurveTarget::kFilter: return "VELOCITY -> FILTER";
case CurveTarget::kAmp:
case CurveTarget::kNone:
return "VELOCITY -> AMP";
}
return "VELOCITY -> AMP";
}
} // namespace
void ReaSamplerEditor::paintCurveButton(LICE_IBitmap* bmp, const Rect& r, CurveTarget target,
bool disabled, bool hovered) {
if (r.width <= 0 || r.height <= 0) return;
// A hairline-bordered bg/cell square with the live velocity curve traced in miniature
// (no node markers at this scale). Hover lifts it; it draws Active (accent-primary
// border) while its popup is open, and re-renders live as the popup edits the curve.
const bool hov = isHovered(HoverKind::kCurveButton, -1);
fillSurface(bmp, toKitBox(r), Role::BgCell,
hov ? InteractionState::Hover : InteractionState::Rest);
const KitColor border = curvePopupOpen_ ? roleColor(Role::AccentPrimary)
: roleColor(Role::LineHairline);
disabled ? InteractionState::Disabled
: (hovered ? InteractionState::Hover : InteractionState::Rest));
const KitColor border = (!disabled && curvePopup_ == target)
? roleColor(Role::AccentPrimary)
: roleColor(Role::LineHairline);
LICE_DrawRect(bmp, r.x, r.y, r.width - 1, r.height - 1, toLice(border), 1.0f, 0);
const VelocityCurve& curve = params_.velocityCurve;
const VelocityCurve& curve = curveFor(params_, target);
const int inset = 3;
const VelocityCurve::Box mini{r.x + inset, r.y + inset, r.width - 2 * inset,
r.height - 2 * inset};
if (mini.width > 1 && mini.height > 1) {
const LICE_pixel trace = toLice(roleColor(Role::AccentSecondary));
// A bipolar thumbnail gets its zero line, without which a flat-at-zero curve and a
// flat-at-minimum one would draw identically at this scale.
if (curve.domain() == instrument::engine::CurveDomain::Bipolar) {
const int zy = curve.pixelFromPoint(mini, {0.0, 0.0}).y;
LICE_Line(bmp, mini.left, zy, mini.left + mini.width, zy,
toLice(roleColor(Role::LineHairline)), 1.0f, 0, false);
}
const LICE_pixel trace =
toLice(roleColor(disabled ? Role::LineHairline : Role::AccentSecondary));
int prevX = 0, prevY = 0;
for (int px = 0; px <= mini.width; ++px) {
const int mx = mini.left + px;
const double vel = VelocityCurve::pointFromPixel(mini, mx, mini.top).velocity;
const int my = VelocityCurve::pixelFromPoint(mini, {vel, curve.eval(vel)}).y;
const double vel = curve.pointFromPixel(mini, mx, mini.top).velocity;
const int my = curve.pixelFromPoint(mini, {vel, curve.eval(vel)}).y;
if (px > 0) LICE_Line(bmp, prevX, prevY, mx, my, trace, 1.0f, 0, true);
prevX = mx;
prevY = my;
@@ -52,7 +90,7 @@ void ReaSamplerEditor::paintCurvePopup(LICE_IBitmap* bmp, int w, int h) {
fillSurface(bmp, toKitBox(pl.sheet), Role::BgPanel, InteractionState::Rest);
LICE_DrawRect(bmp, pl.sheet.x, pl.sheet.y, pl.sheet.width - 1,
pl.sheet.height - 1, toLice(roleColor(Role::LineHairline)), 1.0f, 0);
kitText(bmp, pl.title, "VELOCITY -> AMP", Font::Micro, Role::TextDim);
kitText(bmp, pl.title, curveTitle(curvePopup_), Font::Micro, Role::TextDim);
{
const KitButtonBox box{toKitBox(pl.close)};
const InteractionState st = isHovered(HoverKind::kPopupClose, -1)
@@ -69,26 +107,33 @@ void ReaSamplerEditor::paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r) {
if (r.width <= 0 || r.height <= 0) return; // defensive (degenerate rect)
// The bordered box: a panel surface + hairline border, drawn by palette role. No corner
// caption — the popup sheet's own "VELOCITY -> AMP" title labels this context (the popup
// is the only host).
// caption — the popup sheet's own title labels this context (the popup is the only host).
fillSurface(bmp, toKitBox(r), Role::BgPanel, InteractionState::Rest);
LICE_DrawRect(bmp, r.x, r.y, r.width - 1, r.height - 1,
toLice(roleColor(Role::LineHairline)), 1.0f, 0);
const VelocityCurve::Box box = curveBoxFromRect(r);
if (box.width <= 0 || box.height <= 1) return;
const VelocityCurve& curve = params_.velocityCurve;
const VelocityCurve& curve = editedCurve();
// A bipolar editor needs its zero axis drawn: it is the whole "off" reading, and without
// it the default flat curve is indistinguishable from any other flat one.
if (curve.domain() == instrument::engine::CurveDomain::Bipolar) {
const int zy = curve.pixelFromPoint(box, {0.0, 0.0}).y;
LICE_Line(bmp, box.left, zy, box.left + box.width, zy,
toLice(roleColor(Role::LineHairline)), 1.0f, 0, false);
}
// Trace the monotone spline — ONE eval per x column over the mapping box, in the categorical
// secondary accent (the same grammar as the envelope trace over the waveform). The x ->
// velocity and amp -> y mappings both go through the pure module so the trace, the node
// velocity and value -> y mappings both go through the pure module so the trace, the node
// handles, and the hit-test all share one coordinate system.
const LICE_pixel line = toLice(roleColor(Role::AccentSecondary));
int prevX = 0, prevY = 0;
for (int px = 0; px <= box.width; ++px) {
const int cx = box.left + px;
const double vel = VelocityCurve::pointFromPixel(box, cx, box.top).velocity;
const int cy = VelocityCurve::pixelFromPoint(box, {vel, curve.eval(vel)}).y;
const double vel = curve.pointFromPixel(box, cx, box.top).velocity;
const int cy = curve.pixelFromPoint(box, {vel, curve.eval(vel)}).y;
if (px > 0) LICE_Line(bmp, prevX, prevY, cx, cy, line, 1.0f, 0, true);
prevX = cx;
prevY = cy;
@@ -108,7 +153,7 @@ void ReaSamplerEditor::paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r) {
dragCurY_ < r.y - kCurveDragOffMargin ||
dragCurY_ > r.bottom() + kCurveDragOffMargin);
for (std::size_t i = 0; i < curve.points().size(); ++i) {
const auto np = VelocityCurve::pixelFromPoint(box, curve.points()[i]);
const auto np = curve.pixelFromPoint(box, curve.points()[i]);
const bool grabbed = (drag_ == DragKind::kCurveNode &&
curvePointIndex_ == static_cast<int>(i));
const bool hot = grabbed || isHovered(HoverKind::kCurveNode, static_cast<int>(i));
+14 -1
View File
@@ -76,8 +76,10 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
case ParamControl::kFilterQ: return "Res";
case ParamControl::kFilterDrive: return "Drive";
case ParamControl::kFilterModAmt: return "Mod";
case ParamControl::kFilterVel: return "Vel";
case ParamControl::kFilterKeyTrack: return "Key Trk";
case ParamControl::kAmpVelCurve: return "Amp";
case ParamControl::kPitchVelCurve: return "Pitch";
case ParamControl::kFilterVelCurve: return "Filter";
case ParamControl::kFilterEnvAttack: return "F.Att";
case ParamControl::kFilterEnvHold: return "F.Hold";
case ParamControl::kFilterEnvDecay: return "F.Dec";
@@ -102,6 +104,7 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
case kGroupPitchEnv: caption = "PITCH ENV"; break;
case kGroupFilter: caption = "FILTER"; break;
case kGroupFilterEnv: caption = "FILTER ENV"; break;
case kGroupVelocity: caption = "VELOCITY"; break;
case kGroupVoice: caption = "VOICE"; break;
case kGroupMaster: caption = "MASTER"; break;
default: break;
@@ -169,6 +172,16 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
for (const DeckCellLayout& c : g.cells) {
if (c.id < 0) continue; // reserved blank cell (the Trigger face's spare)
const bool disabled = deckKnobDisabled(c.id);
// A VELOCITY cell is a popup opener, not a dial: it shows its curve in miniature
// where a knob face would be, and its whole cell is the click target.
const CurveTarget curveCell = curveTargetFor(c.id);
if (curveCell != CurveTarget::kNone) {
paintCurveButton(bmp, c.knob, curveCell, disabled,
!disabled && isHovered(HoverKind::kControl, c.id));
kitTextCentered(bmp, c.label, knobName(static_cast<ParamControl>(c.id)),
Font::Micro, disabled ? Role::LineHairline : Role::TextDim);
continue;
}
const bool dragging = (drag_ == DragKind::kDeckKnob && dragParamId_ == c.id);
const bool hov = !disabled && isHovered(HoverKind::kControl, c.id);
const InteractionState st =
+1 -1
View File
@@ -72,7 +72,7 @@ void ReaSamplerEditor::refreshFromBank() {
monoTrigger_ = processor_->monoTrigger();
// A refresh that emptied the selection closes the curve popup — an open-but-invisible
// modal would otherwise swallow clicks on the empty state.
if (selectedId_.empty()) curvePopupOpen_ = false;
if (selectedId_.empty()) curvePopup_ = CurveTarget::kNone;
// Drop a filter that names a bank no longer present.
if (!activeFilterBankId_.empty()) {
bool found = false;
+15 -7
View File
@@ -86,6 +86,9 @@ private:
// spelling.
using OverlayEnv = instrument::ui::OverlayEnv;
// Which of the three velocity curves a deck cell edits — also the popup's open state.
using CurveTarget = instrument::ui::CurveTarget;
// Controls on the setup surface. The int value is the opaque control id the pure
// knob_deck hit-test returns; the shell maps it to the one parameter set or a
// processor-side per-instance setter. The id space and the deck's group composition are
@@ -120,7 +123,6 @@ private:
kCurveNode, // a velocity-curve control point (index = point index)
kVelKnob, // the chrome preview-velocity radial knob
kStripKey, // a piano-strip key (index = MIDI note); carries the name tooltip
kCurveButton, // the chrome mini curve-preview button (opens the popup)
kPopupClose, // the curve popup's Close (x) button
};
struct HoverTarget {
@@ -159,11 +161,12 @@ private:
// label<->value swap on hover/drag.
void paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl);
// The mini curve-preview button (chrome) and the modal curve editor it summons.
void paintCurveButton(LICE_IBitmap* bmp, const Rect& r);
// A deck cell's mini curve thumbnail (the VELOCITY group) and the modal editor it summons.
void paintCurveButton(LICE_IBitmap* bmp, const Rect& r, CurveTarget target, bool disabled,
bool hovered);
void paintCurvePopup(LICE_IBitmap* bmp, int w, int h);
// The velocity->amp transfer-curve editor (X = velocity 0-127, Y = amp 0-1); its only
// host is the popup sheet. `r` empty -> draws nothing.
// 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).
@@ -456,8 +459,13 @@ private:
// delta from this anchor, so a grab never jumps the value.
double dragKnobStartValue_ = 0.0;
// Curve popup open flag, never persisted.
bool curvePopupOpen_ = false;
// Which velocity curve the popup is editing; kNone = closed. Never persisted.
CurveTarget curvePopup_ = CurveTarget::kNone;
// The parameter-set curve `curvePopup_` names. Both overloads exist because every edit
// path needs the mutable one and paint needs the const one; routing through this ONE
// switch is what keeps the three curves on a single popup/draw/hit-test code path.
VelocityCurve& editedCurve();
const VelocityCurve& editedCurve() const;
// Peak-thumbnail cache (mirror of bank_panel), keyed by "id|binCount" so a resize
// recomputes at the new width. Cleared on refresh so a stale sample never shows.