// 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 AMP group's Gate/Trigger face) #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, // 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 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, kFilterEnvHold, kFilterEnvDecay, kFilterEnvSustain, kFilterEnvRelease, // 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 group's face, via knob_deck's blank-cell // reservation (knob_deck.h) so a mode flip never reflows the neighbouring groups. std::vector sampleDeckGroups(PlayMode playMode); // 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 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. 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. 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 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); // 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