// deck_groups.h — WHICH groups the Sample face's knob deck carries and in what order, plus // the control-id space they are built from. Pure data: knob_deck lays out whatever descriptors // it is handed, and this module decides what those descriptors are, so the deck's signal-flow // ordering is provable without a host. #pragma once #include #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 { // Deck control ids. Opaque to knob_deck, resolved by the shell's hit-test and value binding. // Runtime-only — nothing persists them, so the ordering here is free to change. enum class DeckParam { kPlayMode = 0, // Gate | Trigger toggle kPitchEngine, // Varispeed | Preserve toggle 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. kFilterEnable, // filter on|off caption toggle kFilterMorph, // morph position: high-pass .. low-pass kFilterCutoff, // cutoff, log across the audio band kFilterQ, // resonance kFilterDrive, // in-loop drive depth kFilterModAmt, // filter envelope -> cutoff, +/-100% kFilterVel, // velocity -> cutoff, +/-100% kFilterKeyTrack, // note -> cutoff, 0..200% kFilterLaw, // morph law row toggle: HP-BP-LP | HP-notch-LP 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 kVoiceMode, // Poly | Mono caption toggle (VOICE group) kMonoTrigger, // Retrig | Legato row toggle (VOICE group; live only in Mono) kMasterGain, // post-mixer master gain knob (-inf..+24 dB taper, MASTER group) kCount }; // Deck group ids. Unscoped so the shell's caption switch reads against the plain `id` int // knob_deck carries. enum DeckGroupId { kGroupPitch = 0, kGroupPitchEnv, kGroupFilter, kGroupFilterEnv, kGroupAmpEnv, kGroupVoice, kGroupMaster, }; // The deck's groups, left to right, in SIGNAL-FLOW order: pitch -> filter -> amp, then the // 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 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. 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 // the decoded PCM; // - 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, 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 // shell's DragKind maps onto it) so the WHOLE routing decision — not just the predicate — is // testable without a host. 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 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); // Which envelope the waveform overlay draws and edits. Exclusive across the three envelope // decks, and kNone is a valid resting state — the editor opens there. Transient view state: // never persisted, never a parameter. enum class OverlayEnv { kNone, kAmp, kPitch, kFilter }; // The envelope a deck's overlay-select radio picks; kNone for any other control id. OverlayEnv overlayEnvForRadio(int radioId); // The selection a click on `radioId` produces from `current`. Two rules, provable here rather // than in the shell: picking another deck's radio switches to it (exclusivity), and clicking // the ACTIVE one clears back to kNone — "no envelope shown" is a state the user can get back // to, not an error. A non-radio id leaves the selection alone. OverlayEnv nextOverlaySelection(OverlayEnv current, int radioId); // Whether the overlay for `env` is INERT: its deck group's enable toggle is off, so its knobs // are drawn-but-dead and a node drag on the same params must be too — otherwise a drag reaches // a param a knob couldn't (envelope_edit.h). Amp has no enable toggle and is never inert. bool overlayEnvInert(OverlayEnv env, bool pitchEnvEnabled, bool filterEnabled); // 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 // centre can never persist a hair of modulation. Out-of-range norm clamps to the endpoints. double deckBipolarFromNorm(double norm); double deckNormFromBipolar(double value); } // namespace reasampler::instrument::ui