instrument: spline EGs — hard points on the one shared spline, a drawn contour per envelope beside its staged state, payload v13

This commit is contained in:
2026-07-31 21:33:59 -04:00
parent f115904e4f
commit e44bd42dd9
33 changed files with 1739 additions and 364 deletions
+63 -14
View File
@@ -9,6 +9,11 @@ namespace reasampler::instrument::ui {
namespace {
int id(DeckParam p) { return static_cast<int>(p); }
double clamp(double v, double lo, double hi) { return v < lo ? lo : (v > hi ? hi : v); }
// Segment width of the three Staged|Spline toggles. Sized so each env group's caption row stays
// no wider than its knob row — the ceiling is PITCH ENV's, whose caption row lands exactly on
// its four-cell knob row at 23. Raising it reflows the deck's first row.
constexpr int kEnvModeSegW = 23;
} // namespace
double deckBipolarFromNorm(double norm) { return clamp(norm, 0.0, 1.0) * 2.0 - 1.0; }
@@ -31,6 +36,10 @@ std::vector<DeckGroupDesc> sampleDeckGroups(PlayMode playMode) {
penv.captionWidth = 58;
penv.captionRadio = {id(DeckParam::kPitchEnvSelect)};
penv.captionToggle = {id(DeckParam::kPitchEnvEnable), 32};
// The mode toggle rides the caption slack rather than the knob row: every env group's
// knob row is wider than its caption row, so this costs no group width — and the deck
// has six pixels of headroom on its first row at the editor's floor width.
penv.captionToggle2 = {id(DeckParam::kPitchEnvMode), kEnvModeSegW};
penv.cellIds = {id(DeckParam::kPitchEnvAttack),
id(DeckParam::kPitchEnvHold),
id(DeckParam::kPitchEnvDecay),
@@ -58,6 +67,7 @@ std::vector<DeckGroupDesc> sampleDeckGroups(PlayMode playMode) {
fenv.id = kGroupFilterEnv;
fenv.captionWidth = 66;
fenv.captionRadio = {id(DeckParam::kFilterEnvSelect)};
fenv.captionToggle2 = {id(DeckParam::kFilterEnvMode), kEnvModeSegW};
if (trigger) {
fenv.cellIds = {id(DeckParam::kFilterTrigAttack), id(DeckParam::kFilterTrigHold),
id(DeckParam::kFilterTrigDecay), -1, -1};
@@ -76,6 +86,7 @@ std::vector<DeckGroupDesc> sampleDeckGroups(PlayMode playMode) {
amp.captionWidth = 78;
amp.captionRadio = {id(DeckParam::kAmpEnvSelect)};
amp.captionToggle = {id(DeckParam::kPlayMode), 44};
amp.captionToggle2 = {id(DeckParam::kAmpEnvMode), kEnvModeSegW};
if (trigger) {
// The play span first, then the AHD that shapes it, time-ordered left-to-right so
// the row reads like the drawn envelope. One blank keeps the group's width — and
@@ -204,6 +215,11 @@ bool isLiveDeckParam(DeckParam id) {
case DeckParam::kAmpEnvSelect:
case DeckParam::kPitchEnvSelect:
case DeckParam::kFilterEnvSelect:
// A mode toggle names a different envelope, not a different setting of one — the same
// reason every other discrete toggle above is excluded.
case DeckParam::kAmpEnvMode:
case DeckParam::kPitchEnvMode:
case DeckParam::kFilterEnvMode:
case DeckParam::kVoiceCount:
case DeckParam::kVoiceMode:
case DeckParam::kMonoTrigger:
@@ -229,24 +245,65 @@ OverlayEnv nextOverlaySelection(OverlayEnv current, int radioId) {
return (current == picked) ? OverlayEnv::kNone : picked;
}
bool overlayEnvInert(OverlayEnv env, bool pitchEnvEnabled, bool filterEnabled) {
OverlayEnv overlayEnvForModeToggle(int toggleId) {
switch (static_cast<DeckParam>(toggleId)) {
case DeckParam::kAmpEnvMode: return OverlayEnv::kAmp;
case DeckParam::kPitchEnvMode: return OverlayEnv::kPitch;
case DeckParam::kFilterEnvMode: return OverlayEnv::kFilter;
default: return OverlayEnv::kNone;
}
}
bool overlayEnvEnabled(OverlayEnv env, const DeckEnableState& state) {
switch (env) {
case OverlayEnv::kPitch: return !pitchEnvEnabled;
case OverlayEnv::kFilter: return !filterEnabled;
case OverlayEnv::kPitch: return state.pitchEnvEnabled;
case OverlayEnv::kFilter: return state.filterEnabled;
case OverlayEnv::kAmp:
case OverlayEnv::kNone:
return true;
}
return true; // unreachable for a valid enumerator; silences a warning.
}
bool overlayEnvInert(OverlayEnv env, const DeckEnableState& state) {
if (env == OverlayEnv::kNone) return false;
if (!overlayEnvEnabled(env, state)) return true;
switch (env) {
case OverlayEnv::kPitch: return state.pitchSpline;
case OverlayEnv::kFilter: return state.filterSpline;
case OverlayEnv::kAmp: return state.ampSpline;
case OverlayEnv::kNone:
return false;
}
return false; // unreachable for a valid enumerator; silences a warning.
}
bool deckKnobInert(DeckParam id, bool pitchEnvEnabled, bool filterEnabled) {
bool deckKnobInert(DeckParam id, const DeckEnableState& state) {
switch (id) {
case DeckParam::kAttack:
case DeckParam::kHold:
case DeckParam::kDecay:
case DeckParam::kSustain:
case DeckParam::kRelease:
case DeckParam::kTrigAttack:
case DeckParam::kTrigHold:
case DeckParam::kTrigDecay:
return state.ampSpline;
case DeckParam::kPitchEnvAttack:
case DeckParam::kPitchEnvHold:
case DeckParam::kPitchEnvDecay:
return !state.pitchEnvEnabled || state.pitchSpline;
case DeckParam::kPitchEnvDepth:
return !pitchEnvEnabled;
return !state.pitchEnvEnabled;
case DeckParam::kFilterEnvAttack:
case DeckParam::kFilterEnvHold:
case DeckParam::kFilterEnvDecay:
case DeckParam::kFilterEnvSustain:
case DeckParam::kFilterEnvRelease:
case DeckParam::kFilterTrigAttack:
case DeckParam::kFilterTrigHold:
case DeckParam::kFilterTrigDecay:
return !state.filterEnabled || state.filterSpline;
case DeckParam::kFilterMorph:
case DeckParam::kFilterCutoff:
case DeckParam::kFilterQ:
@@ -257,15 +314,7 @@ bool deckKnobInert(DeckParam id, bool pitchEnvEnabled, bool filterEnabled) {
// 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 DeckParam::kFilterVelCurve:
case DeckParam::kFilterEnvAttack:
case DeckParam::kFilterEnvHold:
case DeckParam::kFilterEnvDecay:
case DeckParam::kFilterEnvSustain:
case DeckParam::kFilterEnvRelease:
case DeckParam::kFilterTrigAttack:
case DeckParam::kFilterTrigHold:
case DeckParam::kFilterTrigDecay:
return !filterEnabled;
return !state.filterEnabled;
default:
return false;
}