instrument: one staged-envelope system — per-segment curves, the sustain-less AHD, and a shared overlay for all three envelopes

Trigger's fade pair folds into the AHD (and goes live); the release anchors right;
Preserve rings its synthetic tail out instead of cutting it. Payload v10.
This commit is contained in:
2026-07-31 08:37:57 -04:00
parent 87d7ceb066
commit 13e8c5c4d9
51 changed files with 3406 additions and 1812 deletions
+25 -2
View File
@@ -19,6 +19,7 @@ using namespace reasampler::instrument::ui;
bool ReaSamplerEditor::deckKnobDisabled(int id) const {
switch (static_cast<ParamControl>(id)) {
case ParamControl::kPitchEnvAttack:
case ParamControl::kPitchEnvHold:
case ParamControl::kPitchEnvDecay:
case ParamControl::kPitchEnvDepth:
return !params_.play.pitchEnv.enabled;
@@ -34,6 +35,9 @@ bool ReaSamplerEditor::deckKnobDisabled(int id) const {
case ParamControl::kFilterEnvDecay:
case ParamControl::kFilterEnvSustain:
case ParamControl::kFilterEnvRelease:
case ParamControl::kFilterTrigAttack:
case ParamControl::kFilterTrigHold:
case ParamControl::kFilterTrigDecay:
return !params_.play.filter.enabled;
default:
return false;
@@ -46,6 +50,14 @@ bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
const DeckLayout dl = layoutDeck(fl.deckDescs, band.x, band.y, band.width);
const DeckHit hit = hitTestDeck(dl, x, y);
if (hit.kind == DeckHitKind::CaptionRadio) {
// Exclusive across the three envelope decks, and clicking the active one clears it —
// "no envelope shown" is a state the user can get back to, not an error.
const OverlayEnv picked = overlayEnvForRadio(hit.id);
overlayEnv_ = (overlayEnv_ == picked) ? OverlayEnv::kNone : picked;
invalidate(); // view state only: no parameter write, no reload
return true;
}
if (hit.kind == DeckHitKind::CaptionToggle || hit.kind == DeckHitKind::RowToggle) {
switch (static_cast<ParamControl>(hit.id)) {
case ParamControl::kVoiceMode: {
@@ -87,9 +99,14 @@ 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 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));
const bool inner = hit.inner && curve != ParamControl::kCount;
drag_ = DragKind::kDeckKnob;
dragParamId_ = hit.id;
dragKnobStartValue_ = deckControlNorm(hit.id);
dragParamId_ = inner ? static_cast<int>(curve) : hit.id;
dragInnerCellId_ = inner ? hit.id : -1;
dragKnobStartValue_ = deckControlNorm(dragParamId_);
// Processor-side knobs (voice count / master gain) are transient live writes with no
// parameter-set mutation, so they need no rollback snapshot.
dragStartParams_ = params_;
@@ -120,6 +137,12 @@ ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverDeck(const FaceLayout& fl,
const DeckLayout dl = layoutDeck(fl.deckDescs, band.x, band.y, band.width);
const DeckHit dh = hitTestDeck(dl, x, y);
if (dh.kind == DeckHitKind::None) return {};
if (dh.kind == DeckHitKind::CaptionRadio) return {HoverKind::kEnvRadio, dh.id};
if (dh.kind == DeckHitKind::Knob && dh.inner &&
curveParamFor(static_cast<ParamControl>(dh.id)) != ParamControl::kCount) {
// Indexed by the OUTER cell id so the paint side can find the cell it belongs to.
return {HoverKind::kInnerDial, dh.id};
}
return {HoverKind::kControl, dh.id};
}