One taper, one modifier law: extract param_taper, raise the stage ceiling to 10 s, and make the AHDSR schematic axis the taper itself

This commit is contained in:
2026-08-01 19:09:19 -04:00
parent e589addc54
commit 3eb72d01c4
23 changed files with 1208 additions and 193 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ two small identity/helper headers this directory owns outright
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`,
`param_slider`, `param_taper`, `trigger_seam`, `velocity_curve`, `embed_strip`, `knob_deck`,
`deck_groups`, `deck_values`, `bake_hold`, `curve_popup`, `spline_edit`, `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.
@@ -110,7 +110,7 @@ declared ahead of the instrument slots at that member in `reasampler_processor.h
- `editor_stroke` — the editor's LICE side of the analytic stroker: builds a coverage mask with the pure `core/ui/stroke_aa` and blends it into the bitmap ONCE, writing straight to the bitmap's bits (the arithmetic matches LICE's own mode-0 combine, so a stroke composites identically to every other kit draw). Every radial and spline stroke on the editor routes through `strokeArcAA` / `strokePolylineAA` / `strokeLineAA`. Holds the draw-thread-only scratch mask and arc point list — reuse, not a hidden dependency: threading a canvas through the eight paint sites would grow those signatures to carry an allocation detail. Deliberately does NOT touch `shell/panel/draw_kit`: the waveform stroke, the docked bank panel and the browse cards are out of this seam's blast radius.
- `instrument_bake` — the instrument's half of the resample chain, on the UI thread: render the dialed sound through the pure `core/instrument/bake` modules at the instance's PERSISTED PREVIEW VELOCITY (the velocity the user has been auditioning at — three velocity curves are live, so it is a property of the sound and not a render detail), stage the WAV OUTSIDE the bank folder, publish one `rsbake_<guid>` request, invoke the extension's landing action SYNCHRONOUSLY, read the outcome back over the same key, then adopt + reset in one act. What that key holds afterwards is classified by `core/wire`'s pure `classifyBakeAnswer`, and each of its five non-answers gets its OWN sentence — a silent no-answer stays a failure, but the user is told whether nothing wrote over the key, a stale generation was answered, the answer came in a wire this build cannot read, the request was cleared, or it was refused. All five name the key, because the extension prints one console line per key it scanned and the key is what correlates the two in a multi-instance session. None of them claims the landing never ran — nothing on this side can observe that. Two stack-RAII guards mirror `FxBypassGuard`'s discipline: the staged file and the request key are both cleared on every exit path, so a failed bake leaves no temp, no bank entry and no parameter reset. `bakeAvailable` is the affordance's paint gate. A cloned `instanceGuid` (two instances sharing one `rsbake_` key) is NOT handled here — the residual is contained by pre-existing tracking machinery instead: `planUsagePublish`'s sticky `unioned` poison plus `tiedUsageExists` (`core/tracking/tracking_authority.cpp`) force a clone's bake to `AddDistinct` rather than silently replacing a sibling's entry.
- `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, 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.
- `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, the velocity-curve box derivation, and `dragModifiers()` — THE modifier read for every drag surface and gesture resolver, so the editor cannot grow a second modifier grammar — 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
+3 -3
View File
@@ -47,9 +47,9 @@ void ReaSamplerEditor::handleCurveMouseDown(const Rect& r, int x, int y) {
// Alt-click delete is retired (the spec's right-click supersedes it — one grammar, no
// migration on either side): every gesture here routes through the shared resolver.
const bool ctrl = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
const SplineEdit edit = resolveSplineEdit(
editedCurve(), box, ctrl ? SplineGesture::kControlLeft : SplineGesture::kLeft, x, y);
const SplineGesture gesture =
dragModifiers().ctrl ? SplineGesture::kControlLeft : SplineGesture::kLeft;
const SplineEdit edit = resolveSplineEdit(editedCurve(), box, gesture, x, y);
if (edit.kind == SplineEditKind::kToggleHard) {
if (editedCurve().toggleHard(static_cast<std::size_t>(edit.index))) commitAndReload();
return;
+16 -1
View File
@@ -6,6 +6,7 @@
#ifdef _WIN32
#include "core/instrument/ui/deck_values.h" // snapDeckParamNorm (Shift's whole-unit table)
#include "core/instrument/ui/knob_deck.h" // hitTestDeck / layoutDeck
#include "core/instrument/ui/param_slider.h" // knobDragValue (grab-anchored drag)
#include "shell/instrument/editor_internal.h"
@@ -97,6 +98,7 @@ bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
dragParamId_ = inner ? static_cast<int>(curve) : hit.id;
dragInnerCellId_ = inner ? hit.id : -1;
dragKnobStartValue_ = deckControlNorm(dragParamId_);
dragMods_ = dragModifiers();
// Processor-side knobs (voice count / master gain) are transient live writes with no
// parameter-set mutation, so they need no rollback snapshot.
dragStartParams_ = params_;
@@ -150,7 +152,20 @@ void ReaSamplerEditor::dragDeck(int x, int y) {
// value at grab (up = increase), so the value tracks relative motion and never jumps on
// grab. Live feedback; parameter-set commits land on WM_LBUTTONUP.
(void)x;
applyDeckKnob(dragParamId_, knobDragValue(dragKnobStartValue_, y - dragStartY_));
const DragModifiers mods = dragModifiers();
if (mods != dragMods_) {
// Re-anchor (see dragMods_). Reading the anchor back off the control also means a Shift
// RELEASE re-anchors from the SNAPPED value, so the knob does not spring back.
dragKnobStartValue_ = deckControlNorm(dragParamId_);
dragStartY_ = y;
dragMods_ = mods;
}
double norm = knobDragValue(dragKnobStartValue_, y - dragStartY_, mods);
// The preview-velocity sentinel (-2) and the discrete controls have no whole unit to snap to.
if (mods.shift && dragParamId_ >= 0) {
norm = snapDeckParamNorm(static_cast<DeckParam>(dragParamId_), norm);
}
applyDeckKnob(dragParamId_, norm);
// A live control is delivered on every move, not only on release — that is the whole
// point: the note already sounding tracks the hand on the knob.
if (dragCommitsLive(DragKind::kDeckKnob, dragParamId_)) commitLive();
+17 -6
View File
@@ -35,9 +35,8 @@ bool ReaSamplerEditor::mouseDownWaveform(const FaceLayout& fl, int x, int y) {
const DeckEnableState gates = deckEnableState();
const bool splineLive = overlayIsSpline() && overlayEnvEnabled(overlayEnv_, gates);
const SplineGesture gesture = (GetKeyState(VK_CONTROL) & 0x8000) != 0
? SplineGesture::kControlLeft
: SplineGesture::kLeft;
const SplineGesture gesture =
dragModifiers().ctrl ? SplineGesture::kControlLeft : SplineGesture::kLeft;
// The staged envelope's draggable node and the drawn contour's node are mutually exclusive
// (overlayEnvInert flips the staged one inert exactly when its envelope is in Spline mode),
@@ -107,6 +106,7 @@ bool ReaSamplerEditor::mouseDownWaveform(const FaceLayout& fl, int x, int y) {
envNode_ = envNodeHit.node;
dragStartX_ = x;
dragStartY_ = y;
dragMods_ = dragModifiers();
dragStartEnv_ = env;
dragSampleFrames_ = frames;
dragStartParams_ = params_;
@@ -213,9 +213,20 @@ void ReaSamplerEditor::dragWaveform(const FaceLayout& fl, int x, int y) {
const double rate = liveSampleRate();
if (frames <= 0 || rate <= 0.0) return;
const double totalSeconds = static_cast<double>(frames) / rate;
const StageEnvelope edited = resolveNodeDrag(dragStartEnv_, envNode_, overlay,
totalSeconds, envClampBounds(), dx,
y - dragStartY_);
const DragModifiers mods = dragModifiers();
if (mods != dragMods_) {
// Re-anchor (see dragMods_): the node's CURRENT params and the cursor's current
// position become the origin, so the flip changes only the rate. Re-packing the
// grab envelope is what makes that true for the delta this resolver measures.
dragStartEnv_ =
packEnvelope(overlayEnv_, params_.play, frames, params_.startPoint.value_or(0));
dragStartX_ = x;
dragStartY_ = y;
dragMods_ = mods;
}
const StageEnvelope edited =
resolveNodeDrag(dragStartEnv_, envNode_, overlay, totalSeconds, envClampBounds(),
x - dragStartX_, y - dragStartY_, mods);
unpackEnvelope(overlayEnv_, edited, params_.play);
if (dragCommitsLive(DragKind::kEnvNode)) commitLive();
invalidate(); // live feedback; commit on WM_LBUTTONUP
+10
View File
@@ -15,6 +15,7 @@
#include "core/instrument/map/sample_map.h" // SampleChoice / SampleRefs (sampleLabel)
#include "core/instrument/ui/editor_geometry.h" // Rect (the shared sub-rect type)
#include "core/instrument/ui/keyboard_strip.h" // noteName (the one note-naming source)
#include "core/instrument/ui/param_taper.h" // DragModifiers (the shared interaction law)
#ifdef _WIN32
#include "wdltypes.h"
@@ -70,6 +71,15 @@ inline std::string sampleLabel(const std::vector<instrument::map::SampleChoice>&
#ifdef _WIN32
// THE modifier read, for every drag surface and every gesture resolver. One helper so the editor
// cannot grow a second modifier grammar. GetKeyState rather than WM_MOUSEMOVE's wParam because a
// modifier can be pressed or released with the mouse standing still, and the re-anchor has to see
// that on the next move it does get.
inline instrument::ui::DragModifiers dragModifiers() {
return instrument::ui::DragModifiers{(GetKeyState(VK_SHIFT) & 0x8000) != 0,
(GetKeyState(VK_CONTROL) & 0x8000) != 0};
}
// The editor's own sub-rect type is `Rect` (editor_geometry); the kit draws against
// `KitBox` (component_geometry). This is the single boundary that bridges them so every
// draw routes through the shared kit (theme roles + draw_kit).
+6
View File
@@ -519,6 +519,12 @@ private:
// delta from this anchor, so a grab never jumps the value.
double dragKnobStartValue_ = 0.0;
// The modifier state the in-flight drag is anchored to. Every transition of it — press OR
// release — RE-ANCHORS the drag: current value and current cursor become the new origin, so
// the value is continuous across the flip and only the rate changes. Without that, rescaling
// an accumulated absolute delta in place jumps by (1 - kFineDragScale) x the accumulation.
instrument::ui::DragModifiers dragMods_{};
// Which velocity curve the popup is editing; kNone = closed. Never persisted. Every writer
// of kNone must also cancel a live curve-node drag (closeCurvePopup does both) — an Esc
// mid-drag that closed the popup without cancelling the drag used to leave editedCurve()'s