0627398bbb
Row membership is now the group's own property, not a wrap outcome. FILTER's Band|Notch moves to its caption slack, which is what makes the sound row fit. MASTER gains the limiter toggle, the output meter and the GR lamp.
395 lines
17 KiB
C++
395 lines
17 KiB
C++
// deck_groups.cpp — see deck_groups.h. Pure data; no host types.
|
|
|
|
#include "core/instrument/ui/deck_groups.h"
|
|
|
|
#include <utility>
|
|
|
|
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 binding group is PITCH ENV, which reaches its four-cell knob
|
|
// row at 47 (AMP, the next tightest, at 55). Well inside the ceiling — raising it would widen
|
|
// the CONTOUR row, which has 144px of slack, not the SOUND row.
|
|
constexpr int kEnvModeSegW = 23;
|
|
|
|
// The output meter's column, right of MASTER's cell slots. 62 + the 60px cell + the gap + the
|
|
// group's own padding is exactly kDeckSpanningW.
|
|
constexpr int kMasterMeterW = 62;
|
|
} // namespace
|
|
|
|
double deckBipolarFromNorm(double norm) { return clamp(norm, 0.0, 1.0) * 2.0 - 1.0; }
|
|
double deckNormFromBipolar(double value) { return clamp(value, -1.0, 1.0) * 0.5 + 0.5; }
|
|
|
|
std::vector<DeckGroupDesc> sampleDeckGroups(PlayMode playMode) {
|
|
const bool trigger = (playMode == PlayMode::Trigger);
|
|
std::vector<DeckGroupDesc> out;
|
|
{
|
|
// PITCH/RATE. The three cells make the knob row 180, which is what the group measures
|
|
// from; the caption row (caption + gap + two 48px segments) must stay under it, so the
|
|
// caption reserve has a hard ceiling of 80 — past that the caption row overtakes the knob
|
|
// row and the group grows past 192. Widening the group is not the answer if the text ever
|
|
// outgrows 80: narrow the Varisp|Presrv segments to 44 instead.
|
|
DeckGroupDesc pitch;
|
|
pitch.id = kGroupPitch;
|
|
pitch.captionWidth = 70;
|
|
pitch.captionToggle = {id(DeckParam::kPitchEngine), 48};
|
|
pitch.cellIds = {id(DeckParam::kKeyTrack), id(DeckParam::kRate), id(DeckParam::kPitch)};
|
|
out.push_back(std::move(pitch));
|
|
}
|
|
{
|
|
DeckGroupDesc penv;
|
|
penv.id = kGroupPitchEnv;
|
|
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 — costs no group
|
|
// width; see this module's CLAUDE.md bullet (knob_deck) for the headroom this relies on.
|
|
penv.captionToggle2 = {id(DeckParam::kPitchEnvMode), kEnvModeSegW};
|
|
penv.cellIds = {id(DeckParam::kPitchEnvAttack),
|
|
id(DeckParam::kPitchEnvHold),
|
|
id(DeckParam::kPitchEnvDecay),
|
|
id(DeckParam::kPitchEnvDepth)};
|
|
out.push_back(std::move(penv));
|
|
}
|
|
{
|
|
// Tone shaping left-to-right, then the three modulation depths that all target cutoff.
|
|
DeckGroupDesc filter;
|
|
filter.id = kGroupFilter;
|
|
filter.captionWidth = 46;
|
|
filter.captionToggle = {id(DeckParam::kFilterEnable), 32};
|
|
filter.cellIds = {id(DeckParam::kFilterMorph),
|
|
id(DeckParam::kFilterCutoff),
|
|
id(DeckParam::kFilterQ),
|
|
id(DeckParam::kFilterDrive),
|
|
id(DeckParam::kFilterModAmt),
|
|
id(DeckParam::kFilterVel),
|
|
id(DeckParam::kFilterKeyTrack)};
|
|
// The morph law rides the caption slack. Moving it back to the knob row costs the
|
|
// group 92px and the SOUND row stops fitting its block.
|
|
filter.captionToggle2 = {id(DeckParam::kFilterLaw), 44};
|
|
out.push_back(std::move(filter));
|
|
}
|
|
{
|
|
DeckGroupDesc fenv;
|
|
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};
|
|
} else {
|
|
fenv.cellIds = {id(DeckParam::kFilterEnvAttack),
|
|
id(DeckParam::kFilterEnvHold),
|
|
id(DeckParam::kFilterEnvDecay),
|
|
id(DeckParam::kFilterEnvSustain),
|
|
id(DeckParam::kFilterEnvRelease)};
|
|
}
|
|
out.push_back(std::move(fenv));
|
|
}
|
|
{
|
|
DeckGroupDesc amp;
|
|
amp.id = kGroupAmpEnv;
|
|
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 reserve (-1) keeps the group's width —
|
|
// and therefore its neighbours' placement — identical across a mode flip; its
|
|
// pixels go to the four cells that remain (knob_deck.h).
|
|
amp.cellIds = {id(DeckParam::kTrigLength), id(DeckParam::kTrigAttack),
|
|
id(DeckParam::kTrigHold), id(DeckParam::kTrigDecay), -1};
|
|
} else {
|
|
amp.cellIds = {id(DeckParam::kAttack), id(DeckParam::kHold),
|
|
id(DeckParam::kDecay), id(DeckParam::kSustain),
|
|
id(DeckParam::kRelease)};
|
|
}
|
|
out.push_back(std::move(amp));
|
|
}
|
|
{
|
|
// The three velocity transfer curves share one home, immediately left of VOICE: they
|
|
// shape three different destinations but are one gesture, and MASTER is reserved for
|
|
// post-voice-mixer concerns.
|
|
DeckGroupDesc vel;
|
|
vel.id = kGroupVelocity;
|
|
vel.captionWidth = 54;
|
|
vel.cellIds = {id(DeckParam::kAmpVelCurve), id(DeckParam::kPitchVelCurve),
|
|
id(DeckParam::kFilterVelCurve)};
|
|
out.push_back(std::move(vel));
|
|
}
|
|
{
|
|
DeckGroupDesc voice;
|
|
voice.id = kGroupVoice;
|
|
voice.captionWidth = 38;
|
|
voice.captionToggle = {id(DeckParam::kVoiceMode), 40};
|
|
voice.cellIds = {id(DeckParam::kVoiceCount)};
|
|
voice.rowToggle = {id(DeckParam::kMonoTrigger), 44};
|
|
out.push_back(std::move(voice));
|
|
}
|
|
{
|
|
// The lower slot is reserved and draws NOTHING: blank reads as breathing room where a
|
|
// dashed placeholder would read as unfinished. It is one cell, not two — a second
|
|
// would spend 60 of the layout's whole 90px budget on a control nobody has named.
|
|
DeckGroupDesc master;
|
|
master.id = kGroupMaster;
|
|
master.captionWidth = 46;
|
|
master.captionRadio = {id(DeckParam::kMasterGr), /*passive=*/true};
|
|
master.captionToggle = {id(DeckParam::kLimiterEnable), 32};
|
|
master.cellIds = {id(DeckParam::kMasterGain), -1};
|
|
master.column = {id(DeckParam::kMasterMeter), kMasterMeterW};
|
|
out.push_back(std::move(master));
|
|
}
|
|
for (DeckGroupDesc& d : out) d.row = deckRowFor(static_cast<DeckGroupId>(d.id));
|
|
return out;
|
|
}
|
|
|
|
DeckRow deckRowFor(DeckGroupId group) {
|
|
// Every enumerator listed and no default, on the same gate deckParamCommit below relies on.
|
|
switch (group) {
|
|
case kGroupPitch:
|
|
case kGroupFilter:
|
|
case kGroupVelocity:
|
|
case kGroupVoice:
|
|
return DeckRow::Sound;
|
|
case kGroupPitchEnv:
|
|
case kGroupFilterEnv:
|
|
case kGroupAmpEnv:
|
|
return DeckRow::Contour;
|
|
case kGroupMaster:
|
|
return DeckRow::Spanning;
|
|
}
|
|
// Unreachable for a valid enumerator, and Spanning rather than Sound ON PURPOSE: the
|
|
// -Wswitch gate is compiler-dependent, so on a toolchain that does not raise it a dropped
|
|
// case arm falls here instead. Sound is what a new group most plausibly IS, which would
|
|
// make the fall-through invisible; Spanning is the one row nothing may silently join, so
|
|
// the tests' partition count catches it.
|
|
return DeckRow::Spanning;
|
|
}
|
|
|
|
CurveTarget curveTargetFor(int controlId) {
|
|
switch (static_cast<DeckParam>(controlId)) {
|
|
case DeckParam::kAmpVelCurve: return CurveTarget::kAmp;
|
|
case DeckParam::kPitchVelCurve: return CurveTarget::kPitch;
|
|
case DeckParam::kFilterVelCurve: return CurveTarget::kFilter;
|
|
default: return CurveTarget::kNone;
|
|
}
|
|
}
|
|
|
|
DeckParam curveParamFor(DeckParam knob) {
|
|
switch (knob) {
|
|
case DeckParam::kAttack: return DeckParam::kAttackCurve;
|
|
case DeckParam::kDecay: return DeckParam::kDecayCurve;
|
|
case DeckParam::kRelease: return DeckParam::kReleaseCurve;
|
|
case DeckParam::kTrigAttack: return DeckParam::kTrigAttackCurve;
|
|
case DeckParam::kTrigDecay: return DeckParam::kTrigDecayCurve;
|
|
case DeckParam::kPitchEnvAttack: return DeckParam::kPitchEnvAttackCurve;
|
|
case DeckParam::kPitchEnvDecay: return DeckParam::kPitchEnvDecayCurve;
|
|
case DeckParam::kFilterEnvAttack: return DeckParam::kFilterEnvAttackCurve;
|
|
case DeckParam::kFilterEnvDecay: return DeckParam::kFilterEnvDecayCurve;
|
|
case DeckParam::kFilterEnvRelease: return DeckParam::kFilterEnvReleaseCurve;
|
|
case DeckParam::kFilterTrigAttack: return DeckParam::kFilterTrigAttackCurve;
|
|
case DeckParam::kFilterTrigDecay: return DeckParam::kFilterTrigDecayCurve;
|
|
default: return DeckParam::kCount;
|
|
}
|
|
}
|
|
|
|
LiveCommit deckParamCommit(DeckParam id) {
|
|
switch (id) {
|
|
// The one note-on-latched control; the header owns why.
|
|
case DeckParam::kRate:
|
|
return LiveCommit::NoteOnLatched;
|
|
case DeckParam::kPitch:
|
|
case DeckParam::kAttack:
|
|
case DeckParam::kHold:
|
|
case DeckParam::kDecay:
|
|
case DeckParam::kSustain:
|
|
case DeckParam::kRelease:
|
|
case DeckParam::kTrigAttack:
|
|
case DeckParam::kTrigHold:
|
|
case DeckParam::kTrigDecay:
|
|
case DeckParam::kPitchEnvAttack:
|
|
case DeckParam::kPitchEnvHold:
|
|
case DeckParam::kPitchEnvDecay:
|
|
case DeckParam::kPitchEnvDepth:
|
|
case DeckParam::kFilterMorph:
|
|
case DeckParam::kFilterCutoff:
|
|
case DeckParam::kFilterQ:
|
|
case DeckParam::kFilterDrive:
|
|
case DeckParam::kFilterModAmt:
|
|
case DeckParam::kFilterVel:
|
|
case DeckParam::kFilterKeyTrack:
|
|
case DeckParam::kFilterEnvAttack:
|
|
case DeckParam::kFilterEnvHold:
|
|
case DeckParam::kFilterEnvDecay:
|
|
case DeckParam::kFilterEnvSustain:
|
|
case DeckParam::kFilterEnvRelease:
|
|
case DeckParam::kFilterTrigAttack:
|
|
case DeckParam::kFilterTrigHold:
|
|
case DeckParam::kFilterTrigDecay:
|
|
case DeckParam::kAttackCurve:
|
|
case DeckParam::kDecayCurve:
|
|
case DeckParam::kReleaseCurve:
|
|
case DeckParam::kTrigAttackCurve:
|
|
case DeckParam::kTrigDecayCurve:
|
|
case DeckParam::kPitchEnvAttackCurve:
|
|
case DeckParam::kPitchEnvDecayCurve:
|
|
case DeckParam::kFilterEnvAttackCurve:
|
|
case DeckParam::kFilterEnvDecayCurve:
|
|
case DeckParam::kFilterEnvReleaseCurve:
|
|
case DeckParam::kFilterTrigAttackCurve:
|
|
case DeckParam::kFilterTrigDecayCurve:
|
|
return LiveCommit::Live;
|
|
// Listed rather than defaulted so a newly added control is a COMPILE error here on
|
|
// every toolchain — /we4062 on MSVC, -Werror=switch on GCC/Clang, both set on this
|
|
// library alone in cmake/reasampler_targets.cmake — instead of silently defaulting
|
|
// to non-live. Reasons live in the header.
|
|
case DeckParam::kPlayMode:
|
|
case DeckParam::kPitchEngine:
|
|
case DeckParam::kTrigLength:
|
|
case DeckParam::kPitchEnvEnable:
|
|
case DeckParam::kKeyTrack:
|
|
case DeckParam::kFilterEnable:
|
|
case DeckParam::kAmpVelCurve:
|
|
case DeckParam::kPitchVelCurve:
|
|
case DeckParam::kFilterVelCurve:
|
|
case DeckParam::kFilterLaw:
|
|
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:
|
|
case DeckParam::kMasterGain:
|
|
case DeckParam::kLimiterEnable:
|
|
// MASTER's two readouts reach no parameter at all — the same footing as the overlay
|
|
// radios above.
|
|
case DeckParam::kMasterMeter:
|
|
case DeckParam::kMasterGr:
|
|
case DeckParam::kCount: // not a control
|
|
return LiveCommit::Reload;
|
|
}
|
|
return LiveCommit::Reload; // unreachable for a valid enumerator; silences a warning.
|
|
}
|
|
|
|
OverlayEnv overlayEnvForRadio(int radioId) {
|
|
switch (static_cast<DeckParam>(radioId)) {
|
|
case DeckParam::kAmpEnvSelect: return OverlayEnv::kAmp;
|
|
case DeckParam::kPitchEnvSelect: return OverlayEnv::kPitch;
|
|
case DeckParam::kFilterEnvSelect: return OverlayEnv::kFilter;
|
|
default: return OverlayEnv::kNone;
|
|
}
|
|
}
|
|
|
|
OverlayEnv nextOverlaySelection(OverlayEnv current, int radioId) {
|
|
const OverlayEnv picked = overlayEnvForRadio(radioId);
|
|
if (picked == OverlayEnv::kNone) return current; // not a radio: nothing selects
|
|
return (current == picked) ? OverlayEnv::kNone : picked;
|
|
}
|
|
|
|
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 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, 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;
|
|
// The Trigger %-length knob is inert whenever ANY spline is active (not just the amp's):
|
|
// a drawn contour always covers the full sample length (splineActive, play_params.h), so
|
|
// the engine ignores lengthFraction in that case regardless of which EG is drawn.
|
|
case DeckParam::kTrigLength:
|
|
return state.ampSpline || state.pitchSpline || state.filterSpline;
|
|
case DeckParam::kPitchEnvAttack:
|
|
case DeckParam::kPitchEnvHold:
|
|
case DeckParam::kPitchEnvDecay:
|
|
return !state.pitchEnvEnabled || state.pitchSpline;
|
|
case DeckParam::kPitchEnvDepth:
|
|
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:
|
|
case DeckParam::kFilterDrive:
|
|
case DeckParam::kFilterModAmt:
|
|
case DeckParam::kFilterVel:
|
|
case DeckParam::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 DeckParam::kFilterVelCurve:
|
|
return !state.filterEnabled;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
LiveCommit liveCommitFor(LiveDragKind kind, int paramId) {
|
|
switch (kind) {
|
|
case LiveDragKind::kDeckKnob:
|
|
return (paramId >= 0 && paramId < static_cast<int>(DeckParam::kCount))
|
|
? deckParamCommit(static_cast<DeckParam>(paramId))
|
|
: LiveCommit::Reload;
|
|
case LiveDragKind::kEnvNode:
|
|
return LiveCommit::Live;
|
|
case LiveDragKind::kOther:
|
|
return LiveCommit::Reload;
|
|
}
|
|
return LiveCommit::Reload;
|
|
}
|
|
|
|
} // namespace reasampler::instrument::ui
|