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:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user