feat: run the per-voice filter between the pitch and amp stages, with its own deck

Params ride the one parameter set; payload v8 -> v9, off by default.
Deck composition moves to a pure deck_groups module in pitch -> filter -> amp order.
This commit is contained in:
2026-07-30 15:26:14 -04:00
parent c9c708a338
commit 67215509cb
25 changed files with 1354 additions and 191 deletions
+2 -2
View File
@@ -11,7 +11,7 @@ The pure engine/geometry core this shell wraps (`sampler_core`, `pitch_shift`,
`sample_map`, `component_state_io`, `play_params.h`, `editor_geometry`, `sample_bands`,
`sample_chrome`, `keyboard_strip`, `waveform_view`, `capture_browser`, `browser_scroll`,
`param_slider`, `trigger_seam`, `velocity_curve`, `embed_strip`, `knob_deck`,
`curve_popup`, `master_gain`, `reasampler_uid.h`) lives in `core/instrument/*` and
`deck_groups`, `curve_popup`, `master_gain`, `reasampler_uid.h`) lives in `core/instrument/*` and
`core/wire` and is documented there — this directory consumes it but does not own it.
## Invariants
@@ -90,7 +90,7 @@ scattered `#ifdef`s in the VST shell, except the one described below).
- `reasampler_editor` — VST3 `IPlugView` LICE editor shell: hosts a LICE-drawn child window; the Sample face is home and Browse is a modal picker over it. Split on the Sample face's BAND axis, mirroring the pure `sample_bands` allocator: `editor_session` (session/bridge state, caches, commit-and-reload), `editor_controls` (parameter plumbing + the ONE `faceLayout` band resolve every paint and hit-test path shares), then matching paint and input sets — `editor_paint`/`editor_input` (dispatch + drag router + hover dispatch), `_chrome`, `_waveform`, `_deck` — plus the two band-independent surfaces (`_browse` for the modal picker, `_curve` for the velocity-curve popup) and `editor_platform` (IPlugView/Win32 window plumbing). Shared internals in `editor_internal.h`, no TU of its own. Drop-onto-editor ingest is NOT shipped (deferred).
- `reasampler_embed` — implements `IReaperUIEmbedInterface` so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout to `embed_strip`. A read-only readout: the loaded capture across the keyboard span with its root marked, plus the activity level. It takes no mouse input (there is nothing on the strip to select).
- `vst_entry` — VST3 entry point: `GetPluginFactory` export, class registration, channel-forked class UIDs.
- `editor_internal.h` — INTERNAL shared helpers for the `reasampler_editor` TU family, included only by the editor's own shell TUs (`editor_session` / `editor_controls` / `editor_paint_*` / `editor_input_*` / `editor_platform`), never a public seam: the `Rect`↔kit adapters, small draw primitives (knob face / title band), label helpers, deck group ids, and the velocity-curve box derivation — the helpers more than one band TU needs. The piano-strip and root-key draws live in `editor_paint_chrome`, their only consumer, not here.
- `editor_internal.h` — INTERNAL shared helpers for the `reasampler_editor` TU family, included only by the editor's own shell TUs (`editor_session` / `editor_controls` / `editor_paint_*` / `editor_input_*` / `editor_platform`), never a public seam: the `Rect`↔kit adapters, small draw primitives (knob face / title band), label helpers, and the velocity-curve box derivation — the helpers more than one band TU needs. The deck's control ids, group ids and group composition are the pure `deck_groups` module's, not this file's. The piano-strip and root-key draws live in `editor_paint_chrome`, their only consumer, not here.
- `reasampler_vst.h` — shared identity constants for the ReaSampler VST3 instrument (Phase S): the plugin's class UID (the channel-selected `Steinberg::FUID`, built from the FOREVER-FROZEN macros in `core/wire/reasampler_uid.h`), vendor name/URL/email, so the processor, factory, and editor agree. A class UID is FOREVER-STABLE once shipped — minted once, never regenerated. *(Newly authored per this dispatch's brief — no existing root-CLAUDE.md bullet; verified by reading `src/shell/instrument/reasampler_vst.h` directly.)*
## Gotchas
+1 -1
View File
@@ -85,7 +85,7 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
capture_browser keyboard_strip sample_bands sample_chrome
waveform_view bank_sync browser_scroll param_slider tooltip
theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit
knob_deck curve_popup master_gain sample_usage file_bytes)
knob_deck deck_groups curve_popup master_gain sample_usage file_bytes)
# SDK_INC gives the REAPER VST3 interfaces + API header for the bridge; WDL_INC gives
# LICE for the editor. The VST3 SDK headers arrive via vst3_sdk PUBLIC.
target_include_directories(reasampler_vst PRIVATE ${REASAMPLER_SRC_DIR} ${SDK_INC} ${WDL_INC})
+92 -64
View File
@@ -12,11 +12,13 @@
#include <string>
#include <vector>
#include "core/instrument/engine/filter/filter_params.h" // the filter's own control laws
#include "core/instrument/engine/master_gain.h" // master-gain dB<->linear<->knob taper
#include "core/instrument/map/trigger_seam.h" // triggerPlayLength / fade fraction converters
#include "core/instrument/ui/deck_groups.h" // sampleDeckGroups (the deck's composition)
#include "core/instrument/ui/knob_deck.h" // deckHeight / kDeckKnobSize (the band's own height)
#include "core/util/clamp01.h"
#include "shell/instrument/editor_internal.h" // DeckGroup ids
#include "shell/instrument/editor_internal.h"
#include "shell/instrument/reasampler_processor.h"
namespace reasampler::vst {
@@ -28,9 +30,16 @@ using instrument::ui::chromeRects;
using instrument::ui::deckHeight;
using instrument::ui::kDeckKnobSize;
using instrument::ui::kPad;
using instrument::ui::deckBipolarFromNorm;
using instrument::ui::deckNormFromBipolar;
using instrument::ui::sampleDeckGroups;
using instrument::engine::formatMasterGainLabel;
using instrument::engine::masterGainLinearFromNorm;
using instrument::engine::masterGainNormFromLinear;
using instrument::engine::filter::MorphLaw;
using instrument::engine::filter::filterCutoffHzFromNorm;
using instrument::engine::filter::filterDriveDepthFromNorm;
using instrument::engine::filter::filterQFromNorm;
using util::clamp01;
namespace {
@@ -53,7 +62,7 @@ ReaSamplerEditor::FaceLayout ReaSamplerEditor::faceLayout(int w, int h) const {
// chrome interior, and the deck descriptors can never be derived three different ways.
// The deck's own wrapped height is the only interior measurement the allocator needs.
FaceLayout fl;
fl.deckDescs = deckGroupDescs(params_.play);
fl.deckDescs = sampleDeckGroups(params_.play.playMode);
fl.bands = computeSampleBands(w, h, deckHeight(fl.deckDescs, w - 2 * kPad));
fl.chrome = chromeRects(fl.bands.chrome, kDeckKnobSize);
return fl;
@@ -87,6 +96,23 @@ double ReaSamplerEditor::controlValue(int id, const PlaySeconds& play) const {
case ParamControl::kPitchEnvDepth:
// Signed depth centered at 0.5 (0.5 == 0 semitones).
return clamp01(0.5 + play.pitchEnv.peakSemitones / (2.0 * kPitchDepthMaxSemis));
// Filter. The four tone controls ARE the module's normalized positions — stored and
// shown as-is, so the knob travel is exactly filter_params' own law.
case ParamControl::kFilterEnable: return play.filter.enabled ? 1.0 : 0.0;
case ParamControl::kFilterLaw:
return play.filter.settings.morphLaw == MorphLaw::HighNotchLow ? 1.0 : 0.0;
case ParamControl::kFilterMorph: return clamp01(play.filter.settings.morphNorm);
case ParamControl::kFilterCutoff: return clamp01(play.filter.settings.cutoffNorm);
case ParamControl::kFilterQ: return clamp01(play.filter.settings.resonanceNorm);
case ParamControl::kFilterDrive: return clamp01(play.filter.settings.driveNorm);
case ParamControl::kFilterModAmt: return deckNormFromBipolar(play.filter.modAmount);
case ParamControl::kFilterVel: return deckNormFromBipolar(play.filter.velAmount);
case ParamControl::kFilterKeyTrack:return clamp01(play.filter.keyTrack / kKeyTrackMax);
case ParamControl::kFilterEnvAttack: return secToNorm(play.filter.env.attackSeconds);
case ParamControl::kFilterEnvHold: return secToNorm(play.filter.env.holdSeconds);
case ParamControl::kFilterEnvDecay: return secToNorm(play.filter.env.decaySeconds);
case ParamControl::kFilterEnvSustain: return clamp01(play.filter.env.sustainLevel);
case ParamControl::kFilterEnvRelease: return secToNorm(play.filter.env.releaseSeconds);
default: return 0.0;
}
}
@@ -126,6 +152,33 @@ void ReaSamplerEditor::applyControl(int id, PlaySeconds& play, double value,
case ParamControl::kPitchEnvDepth:
play.pitchEnv.peakSemitones = (clamp01(value) - 0.5) * 2.0 * kPitchDepthMaxSemis;
break;
case ParamControl::kFilterEnable: play.filter.enabled = (segment == 1); break;
case ParamControl::kFilterLaw:
play.filter.settings.morphLaw =
(segment == 1) ? MorphLaw::HighNotchLow : MorphLaw::HighBandLow;
break;
case ParamControl::kFilterMorph:
play.filter.settings.morphNorm = static_cast<float>(clamp01(value)); break;
case ParamControl::kFilterCutoff:
play.filter.settings.cutoffNorm = static_cast<float>(clamp01(value)); break;
case ParamControl::kFilterQ:
play.filter.settings.resonanceNorm = static_cast<float>(clamp01(value)); break;
case ParamControl::kFilterDrive:
play.filter.settings.driveNorm = static_cast<float>(clamp01(value)); break;
case ParamControl::kFilterModAmt: play.filter.modAmount = deckBipolarFromNorm(value); break;
case ParamControl::kFilterVel: play.filter.velAmount = deckBipolarFromNorm(value); break;
case ParamControl::kFilterKeyTrack:
play.filter.keyTrack = clamp01(value) * kKeyTrackMax; break;
case ParamControl::kFilterEnvAttack:
play.filter.env.attackSeconds = normToSec(value); break;
case ParamControl::kFilterEnvHold:
play.filter.env.holdSeconds = normToSec(value); break;
case ParamControl::kFilterEnvDecay:
play.filter.env.decaySeconds = normToSec(value); break;
case ParamControl::kFilterEnvSustain:
play.filter.env.sustainLevel = clamp01(value); break;
case ParamControl::kFilterEnvRelease:
play.filter.env.releaseSeconds = normToSec(value); break;
default: break;
}
}
@@ -151,68 +204,6 @@ double ReaSamplerEditor::previewVelocity01() const {
return static_cast<double>(processor_->previewVelocity()) / 127.0;
}
std::vector<DeckGroupDesc> ReaSamplerEditor::deckGroupDescs(const PlaySeconds& play) const {
// The deck band's groups, left to right. Group widths are mode-independent: AMP ENVELOPE
// reserves its 5-cell Gate width (Trigger leaves two blank cells), so a Gate<->Trigger
// flip repopulates in place and never reflows the neighbouring groups.
std::vector<DeckGroupDesc> out;
{
DeckGroupDesc amp;
amp.id = kGroupAmpEnv;
amp.captionWidth = 78;
amp.captionToggle = {static_cast<int>(ParamControl::kPlayMode), 44};
if (play.playMode == PlayMode::Gate) {
amp.cellIds = {static_cast<int>(ParamControl::kAttack),
static_cast<int>(ParamControl::kHold),
static_cast<int>(ParamControl::kDecay),
static_cast<int>(ParamControl::kSustain),
static_cast<int>(ParamControl::kRelease)};
} else {
// Trigger, time-ordered left-to-right (Fade In / Length % / Fade Out — matches
// the drawn envelope), plus the two reserved blanks.
amp.cellIds = {static_cast<int>(ParamControl::kTrigFadeIn),
static_cast<int>(ParamControl::kTrigLength),
static_cast<int>(ParamControl::kTrigFadeOut), -1, -1};
}
out.push_back(std::move(amp));
}
{
DeckGroupDesc pitch;
pitch.id = kGroupPitch;
pitch.captionWidth = 38;
pitch.captionToggle = {static_cast<int>(ParamControl::kPitchEngine), 48};
pitch.cellIds = {static_cast<int>(ParamControl::kKeyTrack)};
out.push_back(std::move(pitch));
}
{
DeckGroupDesc penv;
penv.id = kGroupPitchEnv;
penv.captionWidth = 58;
penv.captionToggle = {static_cast<int>(ParamControl::kPitchEnvEnable), 32};
penv.cellIds = {static_cast<int>(ParamControl::kPitchEnvAttack),
static_cast<int>(ParamControl::kPitchEnvDecay),
static_cast<int>(ParamControl::kPitchEnvDepth)};
out.push_back(std::move(penv));
}
{
DeckGroupDesc voice;
voice.id = kGroupVoice;
voice.captionWidth = 38;
voice.captionToggle = {static_cast<int>(ParamControl::kVoiceMode), 40};
voice.cellIds = {static_cast<int>(ParamControl::kVoiceCount)};
voice.rowToggle = {static_cast<int>(ParamControl::kMonoTrigger), 44};
out.push_back(std::move(voice));
}
{
DeckGroupDesc master;
master.id = kGroupMaster;
master.captionWidth = 46;
master.cellIds = {static_cast<int>(ParamControl::kMasterGain)};
out.push_back(std::move(master));
}
return out;
}
double ReaSamplerEditor::deckControlNorm(int id) const {
if (id == -2) return previewVelocity01(); // the chrome preview-velocity knob
switch (static_cast<ParamControl>(id)) {
@@ -294,6 +285,43 @@ std::string ReaSamplerEditor::deckValueLabel(int id) const {
snprintf(buf, sizeof(buf), "%d", voiceCount_); break;
case ParamControl::kMasterGain:
formatMasterGainLabel(deckControlNorm(id), buf, sizeof(buf)); break;
// Filter readouts run the stored normalized positions back through the module's OWN
// laws, so what the label says is what the kernel is solved for.
case ParamControl::kFilterMorph: {
const double m = play.filter.settings.morphNorm;
snprintf(buf, sizeof(buf), "%.0f%%", m * 100.0);
break;
}
case ParamControl::kFilterCutoff: {
const float hz = filterCutoffHzFromNorm(play.filter.settings.cutoffNorm);
if (hz >= 1000.0f) snprintf(buf, sizeof(buf), "%.2fk", hz / 1000.0f);
else snprintf(buf, sizeof(buf), "%.0fHz", hz);
break;
}
case ParamControl::kFilterQ:
snprintf(buf, sizeof(buf), "%.2f",
static_cast<double>(filterQFromNorm(play.filter.settings.resonanceNorm)));
break;
case ParamControl::kFilterDrive:
snprintf(buf, sizeof(buf), "%.2f",
static_cast<double>(filterDriveDepthFromNorm(play.filter.settings.driveNorm)));
break;
case ParamControl::kFilterModAmt:
snprintf(buf, sizeof(buf), "%+.0f%%", play.filter.modAmount * 100.0); break;
case ParamControl::kFilterVel:
snprintf(buf, sizeof(buf), "%+.0f%%", play.filter.velAmount * 100.0); break;
case ParamControl::kFilterKeyTrack:
snprintf(buf, sizeof(buf), "%.0f%%", play.filter.keyTrack * 100.0); break;
case ParamControl::kFilterEnvAttack:
snprintf(buf, sizeof(buf), "%.3fs", play.filter.env.attackSeconds); break;
case ParamControl::kFilterEnvHold:
snprintf(buf, sizeof(buf), "%.3fs", play.filter.env.holdSeconds); break;
case ParamControl::kFilterEnvDecay:
snprintf(buf, sizeof(buf), "%.3fs", play.filter.env.decaySeconds); break;
case ParamControl::kFilterEnvSustain:
snprintf(buf, sizeof(buf), "%.0f%%", play.filter.env.sustainLevel * 100.0); break;
case ParamControl::kFilterEnvRelease:
snprintf(buf, sizeof(buf), "%.3fs", play.filter.env.releaseSeconds); break;
default:
// -2 (preview velocity) is labeled at its chrome call site; nothing else here.
break;
+33 -7
View File
@@ -16,6 +16,30 @@ namespace reasampler::vst {
using namespace reasampler::ui;
using namespace reasampler::instrument::ui;
bool ReaSamplerEditor::deckKnobDisabled(int id) const {
switch (static_cast<ParamControl>(id)) {
case ParamControl::kPitchEnvAttack:
case ParamControl::kPitchEnvDecay:
case ParamControl::kPitchEnvDepth:
return !params_.play.pitchEnv.enabled;
case ParamControl::kFilterMorph:
case ParamControl::kFilterCutoff:
case ParamControl::kFilterQ:
case ParamControl::kFilterDrive:
case ParamControl::kFilterModAmt:
case ParamControl::kFilterVel:
case ParamControl::kFilterKeyTrack:
case ParamControl::kFilterEnvAttack:
case ParamControl::kFilterEnvHold:
case ParamControl::kFilterEnvDecay:
case ParamControl::kFilterEnvSustain:
case ParamControl::kFilterEnvRelease:
return !params_.play.filter.enabled;
default:
return false;
}
}
bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
const Rect& band = fl.bands.decks;
if (!contains(band, x, y)) return false;
@@ -46,8 +70,14 @@ bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
invalidate();
break;
}
case ParamControl::kFilterLaw:
// Inert while the filter is off, matching its Disabled paint.
if (!params_.play.filter.enabled) break;
applyParamControl(hit.id, 0.0, hit.segment);
commitAndReload();
break;
default:
// Parameter-set toggles (play mode / pitch engine / pitch-env enable).
// Parameter-set toggles (play mode / pitch engine / pitch-env + filter enable).
applyParamControl(hit.id, 0.0, hit.segment);
commitAndReload();
break;
@@ -55,12 +85,8 @@ bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
return true;
}
if (hit.kind == DeckHitKind::Knob) {
// PITCH ENV knobs are Disabled (drawn, inert) while the envelope is off.
const bool pitchEnvKnob =
hit.id == static_cast<int>(ParamControl::kPitchEnvAttack) ||
hit.id == static_cast<int>(ParamControl::kPitchEnvDecay) ||
hit.id == static_cast<int>(ParamControl::kPitchEnvDepth);
if (pitchEnvKnob && !params_.play.pitchEnv.enabled) return true;
// Knobs of a disabled group are drawn but inert.
if (deckKnobDisabled(hit.id)) return true;
drag_ = DragKind::kDeckKnob;
dragParamId_ = hit.id;
dragKnobStartValue_ = deckControlNorm(hit.id);
+2 -11
View File
@@ -1,8 +1,8 @@
// editor_internal.h — shared helpers for the ReaSamplerEditor TU family. Included ONLY by
// the editor's own shell TUs (editor_session / editor_controls / editor_paint_* /
// editor_input_* / editor_platform) — never a public seam. Holds the Rect<->kit adapters,
// small draw primitives (knob face / title band), label helpers, deck group ids, and the
// velocity-curve box derivation. All inline.
// small draw primitives (knob face / title band), label helpers, and the velocity-curve box
// derivation. All inline.
#pragma once
@@ -30,15 +30,6 @@
namespace reasampler::vst {
// Deck group ids (shell-owned; knob_deck treats them opaquely), left-to-right order.
enum DeckGroup {
kGroupAmpEnv = 0,
kGroupPitch,
kGroupPitchEnv,
kGroupVoice,
kGroupMaster,
};
// Velocity-curve editor box metrics. The inset keeps node handles + the pick radius
// inside the border so an endpoint at amp 0/1 stays grabbable; drag-off beyond
// box+margin deletes the dragged node.
+41 -16
View File
@@ -1,7 +1,7 @@
// editor_paint_deck.cpp — the DECKS band's painter: the fenced control groups (AMP
// ENVELOPE / PITCH / PITCH ENV / VOICE / MASTER), their captions, the compact caption and
// row toggles, and the radial knobs with the label<->value swap on hover/drag.
// Windows-only; the deck's cell geometry is the pure knob_deck layout.
// editor_paint_deck.cpp — the DECKS band's painter: the fenced control groups, their
// captions, the compact caption and row toggles, and the radial knobs with the label<->value
// swap on hover/drag. Windows-only; the deck's cell geometry is the pure knob_deck layout and
// its group composition the pure deck_groups list.
#include "shell/instrument/reasampler_editor.h"
@@ -10,8 +10,9 @@
#include <string>
#include <vector>
#include "core/instrument/engine/filter/filter_morph.h" // MorphLaw (the law toggle's state)
#include "core/instrument/ui/knob_deck.h" // deck layout + kDeckKnobSize
#include "shell/instrument/editor_internal.h" // kit adapters + knob face + DeckGroup ids
#include "shell/instrument/editor_internal.h" // kit adapters + knob face
#include "shell/instrument/reasampler_processor.h"
namespace reasampler::vst {
@@ -68,6 +69,18 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
case ParamControl::kPitchEnvDepth: return "P.Depth";
case ParamControl::kVoiceCount: return "Voices";
case ParamControl::kMasterGain: return "Gain";
case ParamControl::kFilterMorph: return "Mode";
case ParamControl::kFilterCutoff: return "Cutoff";
case ParamControl::kFilterQ: return "Res";
case ParamControl::kFilterDrive: return "Drive";
case ParamControl::kFilterModAmt: return "Mod";
case ParamControl::kFilterVel: return "Vel";
case ParamControl::kFilterKeyTrack: return "Key Trk";
case ParamControl::kFilterEnvAttack: return "F.Att";
case ParamControl::kFilterEnvHold: return "F.Hold";
case ParamControl::kFilterEnvDecay: return "F.Dec";
case ParamControl::kFilterEnvSustain: return "F.Sus";
case ParamControl::kFilterEnvRelease: return "F.Rel";
default: return "";
}
};
@@ -79,11 +92,13 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
hairline, 1.0f, 0);
const char* caption = "";
switch (g.id) {
case kGroupAmpEnv: caption = "AMP ENVELOPE"; break;
case kGroupPitch: caption = "PITCH"; break;
case kGroupPitchEnv: caption = "PITCH ENV"; break;
case kGroupVoice: caption = "VOICE"; break;
case kGroupMaster: caption = "MASTER"; break;
case kGroupAmpEnv: caption = "AMP ENVELOPE"; break;
case kGroupPitch: caption = "PITCH"; break;
case kGroupPitchEnv: caption = "PITCH ENV"; break;
case kGroupFilter: caption = "FILTER"; break;
case kGroupFilterEnv: caption = "FILTER ENV"; break;
case kGroupVoice: caption = "VOICE"; break;
case kGroupMaster: caption = "MASTER"; break;
default: break;
}
kitText(bmp, g.caption, caption, Font::Micro, Role::TextDim);
@@ -105,20 +120,30 @@ void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
case ParamControl::kVoiceMode:
drawToggle(g.captionToggle, "Poly", "Mono", isMono, false);
break;
case ParamControl::kFilterEnable:
drawToggle(g.captionToggle, "Off", "On", play.filter.enabled, false);
break;
default: break;
}
}
// The row toggle (VOICE group's Retrig|Legato) — live only in Mono.
// Row toggles: VOICE's Retrig|Legato (live only in Mono) and FILTER's morph law.
if (g.rowToggle.id >= 0) {
drawToggle(g.rowToggle, "Retrig", "Legato",
monoTrigger_ == MonoTrigger::Legato, !isMono);
if (static_cast<ParamControl>(g.rowToggle.id) == ParamControl::kFilterLaw) {
drawToggle(g.rowToggle, "Band", "Notch",
play.filter.settings.morphLaw ==
instrument::engine::filter::MorphLaw::HighNotchLow,
!play.filter.enabled);
} else {
drawToggle(g.rowToggle, "Retrig", "Legato",
monoTrigger_ == MonoTrigger::Legato, !isMono);
}
}
// The knobs. PITCH ENV knobs draw Disabled (not hidden) while the envelope is off —
// stable geometry.
// The knobs. A dependent group's knobs draw Disabled (not hidden) — stable geometry.
// The predicate is the input side's, so the drawn state and the inert grab agree.
for (const DeckCellLayout& c : g.cells) {
if (c.id < 0) continue; // reserved blank cell (the Trigger face's two spares)
const bool disabled = (g.id == kGroupPitchEnv && !play.pitchEnv.enabled);
const bool disabled = deckKnobDisabled(c.id);
const bool dragging = (drag_ == DragKind::kDeckKnob && dragParamId_ == c.id);
const bool hov = !disabled && isHovered(HoverKind::kControl, c.id);
const InteractionState st =
+8 -31
View File
@@ -14,6 +14,7 @@
#include "public.sdk/source/common/pluginview.h"
#include "core/instrument/ui/deck_groups.h" // DeckParam / DeckGroupId / sampleDeckGroups
#include "core/instrument/ui/editor_geometry.h" // Rect (shared sub-rect type)
#include "core/instrument/ui/envelope_edit.h" // EnvClampBounds / NodeHit (envelope node hit-test/edit)
#include "core/instrument/ui/envelope_overlay.h" // AmpEnvelope / EnvNode (envelope overlay draw seam)
@@ -82,31 +83,9 @@ private:
// Controls on the setup surface. The int value is the opaque control id the pure
// knob_deck hit-test returns; the shell maps it to the one parameter set or a
// processor-side per-instance setter.
enum class ParamControl {
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)
// Deck-only controls: processor-side per-instance params — routed to the processor
// setters, never through applyParamControl.
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
};
// processor-side per-instance setter. The id space and the deck's group composition are
// the pure deck_groups module's — this alias keeps the shell's spelling.
using ParamControl = instrument::ui::DeckParam;
// The waveform markers on the waveform band: start-point + the sustain loop's two ends,
// in draw + hit order.
@@ -188,6 +167,10 @@ private:
bool mouseDownDeck(const FaceLayout& fl, int x, int y);
void mouseDownBrowse(int w, int h, int x, int y);
// Whether deck knob `id` belongs to a group whose enable toggle is off. The ONE predicate
// behind both the Disabled paint and the inert grab, so they cannot disagree.
bool deckKnobDisabled(int id) const;
// Live drag resolution, split on the same axis; each handles only its own DragKind
// values and is called from onMouseMove's router.
void dragChrome(const FaceLayout& fl, int x, int y); // kRootMarker
@@ -349,12 +332,6 @@ private:
// Persisted preview velocity as a 0..1 slider value (MIDI 1..127 -> [0,1]).
double previewVelocity01() const;
// The deck groups: AMP ENVELOPE (Gate A/H/D/S/R; Trigger Fade In/Length %/Fade Out + two
// reserved blanks so a mode flip never reflows neighbours) / PITCH (Key Track) / PITCH
// ENV (P.Attack/P.Decay/P.Depth) / VOICE (Voices knob + Poly|Mono + Retrig|Legato) /
// MASTER (Gain knob).
std::vector<DeckGroupDesc> deckGroupDescs(const PlaySeconds& play) const;
// The normalized [0,1] value a deck knob shows — parameter-set ids route through
// controlValue/keyTrack; processor-side ids (voice count, master gain, preview velocity
// via the -2 sentinel) read the processor's live value.