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
+55 -28
View File
@@ -7,7 +7,7 @@
#include <vector>
#include "core/instrument/engine/play_params.h" // PlayMode (the AMP group's Gate/Trigger face)
#include "core/instrument/engine/play_params.h" // PlayMode (the mode-dependent group faces)
#include "core/instrument/ui/knob_deck.h" // DeckGroupDesc
namespace reasampler::instrument::ui {
@@ -17,18 +17,20 @@ namespace reasampler::instrument::ui {
enum class DeckParam {
kPlayMode = 0, // Gate | Trigger toggle
kPitchEngine, // Varispeed | Preserve toggle
kAttack, // AHDSR attack (Gate) / —
kHold, // AHDSR hold (Gate)
kDecay, // AHDSR decay (Gate)
kSustain, // AHDSR sustain (Gate)
kRelease, // AHDSR release (Gate)
kTrigLength, // Trigger %-length
kTrigFadeIn, // Trigger fade-in
kTrigFadeOut, // Trigger fade-out
kPitchEnvEnable, // AD pitch envelope on|off
kPitchEnvAttack, // AD pitch attack
kPitchEnvDecay, // AD pitch decay
kPitchEnvDepth, // AD pitch depth in +/- semitones
kAttack, // amp AHDSR attack (Gate)
kHold, // amp AHDSR hold (Gate)
kDecay, // amp AHDSR decay (Gate)
kSustain, // amp AHDSR sustain (Gate)
kRelease, // amp AHDSR release (Gate)
kTrigLength, // Trigger play span, % of the post-start length
kTrigAttack, // amp AHD attack (Trigger)
kTrigHold, // amp AHD hold, % of the span left after attack + decay
kTrigDecay, // amp AHD decay (Trigger)
kPitchEnvEnable, // AHD pitch envelope on|off
kPitchEnvAttack,
kPitchEnvHold, // % of the span left after attack + decay
kPitchEnvDecay,
kPitchEnvDepth, // AHD pitch depth in +/- semitones
kKeyTrack, // key-tracking 0..200% (lives on InstrumentParams, not PlaySeconds)
// Filter. The four control positions map through filter_params' own laws; the three
// depths are bipolar and centred at zero.
@@ -41,11 +43,32 @@ enum class DeckParam {
kFilterVel, // velocity -> cutoff, +/-100%
kFilterKeyTrack, // note -> cutoff, 0..200%
kFilterLaw, // morph law row toggle: HP-BP-LP | HP-notch-LP
kFilterEnvAttack,
kFilterEnvAttack, // filter AHDSR (Gate)
kFilterEnvHold,
kFilterEnvDecay,
kFilterEnvSustain,
kFilterEnvRelease,
kFilterTrigAttack, // filter AHD (Trigger)
kFilterTrigHold,
kFilterTrigDecay,
// Curve exponents. These never get a cell of their own — each is the INNER DIAL of the
// stage knob it shapes (see curveParamFor), which is why only sloped stages have one.
kAttackCurve,
kDecayCurve,
kReleaseCurve,
kTrigAttackCurve,
kTrigDecayCurve,
kPitchEnvAttackCurve,
kPitchEnvDecayCurve,
kFilterEnvAttackCurve,
kFilterEnvDecayCurve,
kFilterEnvReleaseCurve,
kFilterTrigAttackCurve,
kFilterTrigDecayCurve,
// Overlay selection radios — transient view state, not parameters.
kAmpEnvSelect,
kPitchEnvSelect,
kFilterEnvSelect,
// Deck-only controls: processor-side per-instance params — routed to the processor
// setters, never through the parameter set.
kVoiceCount, // polyphony bound (1..32) — a stepped knob in the VOICE group
@@ -68,17 +91,24 @@ enum DeckGroupId {
};
// The deck's groups, left to right, in SIGNAL-FLOW order: pitch -> filter -> amp, then the
// two instance-wide groups. `playMode` picks the AMP group's face, via knob_deck's blank-cell
// reservation (knob_deck.h) so a mode flip never reflows the neighbouring groups.
// two instance-wide groups. `playMode` picks the AMP and FILTER ENV groups' faces — AHDSR in
// Gate, AHD in Trigger — via knob_deck's blank-cell reservation (knob_deck.h) so a mode flip
// never reflows the neighbouring groups.
std::vector<DeckGroupDesc> sampleDeckGroups(PlayMode playMode);
// The curve-exponent control a stage knob's INNER DIAL edits, or kCount when the knob shapes
// no curve. THE one place the "every stage except Hold and Sustain is sloped" rule is written
// down: a knob with no entry here draws no inner dial and its inner region resolves as an
// ordinary knob grab.
DeckParam curveParamFor(DeckParam knob);
// Whether control `id` is delivered LIVE — straight to the voices that are already sounding —
// rather than through an instrument reload. The line is drawn at continuously-valued playback
// controls, so this is a routing decision at the editor's commit site rather than a property
// of any one knob; moving a control across the line is a change here and nowhere else.
//
// THE home for why each excluded control is excluded. Five continuous controls are outside the
// live set, plus every discrete toggle:
// THE home for why each excluded control is excluded. Three continuous controls are outside
// the live set, plus every discrete toggle and the overlay radios:
// - the discrete toggles (play mode, pitch engine, filter enable/law, pitch-envelope enable)
// name a different sound rather than a different setting of one;
// - the three capture-anchored overrides (root, loop span, start frame) name positions in
@@ -86,13 +116,10 @@ std::vector<DeckGroupDesc> sampleDeckGroups(PlayMode playMode);
// - kKeyTrack and kFilterVel feed values a voice latches at note-on by design (the pitch
// ratio and the velocity-curve result), so live delivery would retune or re-gain a note
// already struck;
// - kTrigLength resolves playEnd_, a fact about the note. kTrigFadeIn/kTrigFadeOut are pure
// amplitude shape and would be live-able in principle, but they live in `sample.play` and
// are baked into SampleData at build time — the engine rebuild copies that verbatim, so
// only a reload can deliver them without widening LiveValues. They fold into the AHD
// alongside Gate's, at which point they inherit its routing; until then they reload.
// Consequence, stated plainly: a Trigger-mode instance gets NO live delivery on its amplitude
// controls. Only the filter and pitch-envelope knobs move a sounding Trigger one-shot.
// - kTrigLength resolves playEnd_, a fact about the note, not a setting of it;
// - the overlay radios select what the editor DRAWS and reach no parameter at all.
// Both amp shapes are live: the Trigger fade pair that used to reload folded into the AHD and
// inherited its routing, so a Trigger-mode instance now tracks its amplitude knobs too.
bool isLiveDeckParam(DeckParam id);
// The editor drag kinds that can commit live, in this pure module's own vocabulary (the
@@ -102,9 +129,9 @@ enum class LiveDragKind { kOther, kDeckKnob, kEnvNode };
// Whether a drag of `kind` commits live. A deck knob is live per isLiveDeckParam (negative ids
// are the shell's processor-side sentinels and out-of-range ids are not controls, so neither
// reaches the enum); an envelope-node drag is live only in Gate, where it edits the AHDSR —
// in Trigger the same drag rewrites the play span, which is not a live control.
bool liveCommitFor(LiveDragKind kind, int paramId, PlayMode playMode);
// reaches the enum); an envelope-node drag is live in either mode, since every stage value it
// can reach — AHDSR or AHD, on any of the three envelopes — is itself live.
bool liveCommitFor(LiveDragKind kind, int paramId);
// The deck's BIPOLAR knob law: 0.5 of the knob's travel is zero depth, the ends are -1 and
// +1. Exact inverses, and exact at the centre detent (0.5 -> 0 -> 0.5), so a knob parked at