instrument: fix AHD node-tracking/tie-break/live-latch defects and close staged-envelope-curve test gaps

This commit is contained in:
2026-07-31 09:52:17 -04:00
parent d60ab1524a
commit 2fa1405b06
27 changed files with 360 additions and 86 deletions
+1 -1
View File
@@ -85,7 +85,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)
knob_deck deck_groups curve_popup 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})
+26 -2
View File
@@ -54,6 +54,29 @@ namespace {
constexpr double kEnvTimeMaxSeconds = instrument::ui::kGateStageMaxSeconds;
constexpr double kPitchDepthMaxSemis = 24.0; // pitch depth throw: +/-24 st, centered
constexpr double kKeyTrackMax = 2.0; // key-track slider ceiling (0..200%)
// The raw stored curve exponent for a curve-dial control id, read DIRECTLY off the field —
// never round-tripped through curveFromKnobNorm(knobNormFromCurve(x)): the knob-norm law's
// centre detent (curve_law.h) snaps anything near-neutral back to exactly 1.0, so a round trip
// can misreport a stored exponent that isn't neutral as "^1.00".
double curveExponentFor(int id, const PlaySeconds& play) {
using DeckParam = instrument::ui::DeckParam; // ReaSamplerEditor::ParamControl is an alias
switch (static_cast<DeckParam>(id)) {
case DeckParam::kAttackCurve: return play.adsr.attackCurve;
case DeckParam::kDecayCurve: return play.adsr.decayCurve;
case DeckParam::kReleaseCurve: return play.adsr.releaseCurve;
case DeckParam::kTrigAttackCurve: return play.trigAhd.attackCurve;
case DeckParam::kTrigDecayCurve: return play.trigAhd.decayCurve;
case DeckParam::kPitchEnvAttackCurve: return play.pitchEnv.shape.attackCurve;
case DeckParam::kPitchEnvDecayCurve: return play.pitchEnv.shape.decayCurve;
case DeckParam::kFilterEnvAttackCurve: return play.filter.env.attackCurve;
case DeckParam::kFilterEnvDecayCurve: return play.filter.env.decayCurve;
case DeckParam::kFilterEnvReleaseCurve: return play.filter.env.releaseCurve;
case DeckParam::kFilterTrigAttackCurve: return play.filter.trigEnv.attackCurve;
case DeckParam::kFilterTrigDecayCurve: return play.filter.trigEnv.decayCurve;
default: return util::kCurveNeutral;
}
}
} // namespace
ReaSamplerEditor::FaceLayout ReaSamplerEditor::faceLayout(int w, int h) const {
@@ -373,7 +396,7 @@ std::string ReaSamplerEditor::deckValueLabel(int id) const {
case ParamControl::kFilterEnvReleaseCurve:
case ParamControl::kFilterTrigAttackCurve:
case ParamControl::kFilterTrigDecayCurve:
snprintf(buf, sizeof(buf), "^%.2f", util::curveFromKnobNorm(controlValue(id, play)));
snprintf(buf, sizeof(buf), "^%.2f", curveExponentFor(id, play));
break;
default:
// -2 (preview velocity) is labeled at its chrome call site; nothing else here.
@@ -461,10 +484,11 @@ StageEnvelope ReaSamplerEditor::packEnvelope(OverlayEnv which, const PlaySeconds
else packAhdsr(play.filter.env, env);
break;
case OverlayEnv::kAmp:
case OverlayEnv::kNone:
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;
}
@@ -44,6 +44,7 @@ bool ReaSamplerEditor::mouseDownChrome(const FaceLayout& fl, int x, int y) {
if (contains(cr.velCell, x, y)) {
drag_ = DragKind::kDeckKnob;
dragParamId_ = -2; // sentinel: the preview velocity knob (a processor param)
dragInnerCellId_ = -1; // peer of mouseDownDeck's own grab-time init, for symmetry
dragKnobStartValue_ = previewVelocity01();
dragStartX_ = x;
dragStartY_ = y;
+2 -2
View File
@@ -158,8 +158,8 @@ inline void drawKnobFace(LICE_IBitmap* bmp, const instrument::ui::Rect& knobRect
// The concentric INNER dial: a second value on the same cell, drawn in the categorical
// tertiary accent so it reads as a different KIND of control rather than a louder one — the
// same purple the overlay traces the envelope in, which is what ties a segment's knot to its
// dial by eye. Shares the outer knob's value<->angle map (param_slider's), so both needles
// same purple the overlay traces the envelope in (curve_law.h owns the knot/dial pairing this
// ties into). Shares the outer knob's value<->angle map (param_slider's), so both needles
// point the same way for the same normalized value.
inline void drawInnerDial(LICE_IBitmap* bmp, const instrument::ui::Rect& innerRect,
double value01, ui::InteractionState st) {
+5 -1
View File
@@ -111,7 +111,11 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
// The overlay-select radio: filled in the tertiary accent (the colour the overlay
// traces in) when this group's envelope is the one on the waveform, hollow otherwise.
if (g.captionRadio.id >= 0) {
const bool on = (overlayEnv_ == overlayEnvForRadio(g.captionRadio.id));
// overlayEnvForRadio returns kNone for BOTH "not a radio id" and "no selection" —
// a non-radio id must never read as lit just because nothing is selected, so the
// picked env has to be checked against kNone itself, not just matched by equality.
const OverlayEnv picked = overlayEnvForRadio(g.captionRadio.id);
const bool on = picked != OverlayEnv::kNone && overlayEnv_ == picked;
const bool hov = isHovered(HoverKind::kEnvRadio, g.captionRadio.id);
const Rect& rb = g.captionRadio.box;
LICE_DrawRect(bmp, rb.x, rb.y, rb.width - 1, rb.height - 1,