Phase S Wave 2: three-view ReaSampler 9000 editor (Sample home / Browse modal / Zone)

Retire the flat toggle; Sample-home face wires the Wave-1 envelope overlay + draggable nodes, preview button + velocity knob, Mono/Stereo, key-track. Browse reduced to a confirm/cancel modal; Zone retained with piano-pattern strip. RT-safe off-thread preview-note mailbox added.
This commit is contained in:
2026-07-27 14:17:58 -04:00
parent 203005e6cf
commit c12a374b88
5 changed files with 1094 additions and 542 deletions
+103 -14
View File
@@ -31,6 +31,8 @@
#include "public.sdk/source/common/pluginview.h"
#include "editor_geometry.h" // Rect (the shell's sub-rect type, shared with the pure modules)
#include "envelope_edit.h" // EnvClampBounds / NodeHit (S-VIEW-3 envelope node hit-test/edit)
#include "envelope_overlay.h" // AmpEnvelope / EnvNode (S-VIEW-3 envelope overlay draw seam)
#include "param_slider.h" // ControlRow (the S12/S15/S16 control-surface geometry)
#include "peaks.h" // Envelope (the cached peak thumbnail)
#include "sample_map.h" // SampleChoice, BankChoice, PerformanceMap (the shell's snapshot)
@@ -64,16 +66,21 @@ protected:
Steinberg::tresult PLUGIN_API onSize(Steinberg::ViewRect* newSize) override;
private:
// Which face the editor shows. The browser is the default; the Zones panel is the
// demoted opt-in view reached by the toggle. Both draw over the same snapshotted bank.
enum class View { kBrowser, kZones };
// Which face the editor shows (S-VIEW-1, three-view model). Sample is the HOME/default
// face (the loaded capture). Browse is a full-window MODAL picker overlaid on Sample
// (select + confirm/cancel changes the loaded capture, then dismisses). Zone is the
// dedicated multi-zone keymap surface, button-summoned. All three draw over the same
// snapshotted bank; Browse + Zone return to Sample when dismissed.
enum class View { kSample, kBrowse, kZone };
// What a mouse drag is currently editing (the drag-state machine). kNone = no drag in
// flight. The zone-edit grabs mirror keyboard_strip::ZoneGrab; kRootMarker is the
// single-capture root drag on the setup strip; kWaveMarker is a draggable start/loop
// marker on the S11 waveform surface (which marker is in waveMarker_).
// marker on the S11 waveform surface (which marker is in waveMarker_); kEnvNode is a
// draggable envelope breakpoint on the Sample-view hero overlay (S-VIEW-3, which node in
// envNode_).
enum class DragKind { kNone, kRootMarker, kZoneLow, kZoneHigh, kZoneBody, kWaveMarker,
kScrollThumb, kParamSlider };
kScrollThumb, kParamSlider, kEnvNode };
// The parameter controls on the setup surface (S12 AHDSR + the S15/S16 control surfaces).
// The int value is the ControlDesc id the pure param_slider hit-test returns; the shell
@@ -93,6 +100,7 @@ private:
kPitchEnvAttack, // AD pitch attack (S16)
kPitchEnvDecay, // AD pitch decay (S16)
kPitchEnvDepth, // AD pitch depth in +/- semitones (S16)
kKeyTrack, // S-VIEW-6 key-tracking 0..200% (lives on PerformanceZone, not ZonePlaySeconds)
kCount
};
@@ -111,13 +119,17 @@ private:
// bank_panel's L2 hover model.
enum class HoverKind {
kNone,
kToggleBrowser, // the Browser toggle segment
kToggleZones, // the Zones toggle segment
kNavBrowse, // the Sample-view "Browse" title-band button (opens the Browse modal)
kNavZone, // the Sample-view "Zone" title-band button (opens the Zone surface)
kBack, // the Browse/Zone "back" affordance (returns to Sample)
kSearchBox, // the browser search box
kFilterTab, // a bank-filter tab (index = tab ordinal, 0 = All)
kCard, // a capture card (index = visible_ index)
kBrowseConfirm, // the Browse modal "Load" confirm button
kBrowseCancel, // the Browse modal "Cancel" button
kChanMono, // the mono channel-mode segment
kChanStereo, // the stereo channel-mode segment
kPreview, // the Sample-view preview-trigger button
kAddZone, // the "+ Add Zone" button
kDeleteZone, // the "Delete" zone button
kControl, // a param-panel control row (index = ControlDesc id)
@@ -131,16 +143,30 @@ private:
#ifdef _WIN32
void paint(HDC hdc);
void paintBrowser(LICE_IBitmap* bmp, int w, int h);
void paintSetup(LICE_IBitmap* bmp, const Rect& area);
void paintZones(LICE_IBitmap* bmp, int w, int h);
void paintSample(LICE_IBitmap* bmp, int w, int h); // S-VIEW-2 home face
void paintBrowse(LICE_IBitmap* bmp, int w, int h); // S-VIEW-5 modal picker overlay
void paintZone(LICE_IBitmap* bmp, int w, int h); // S-VIEW-8 zone surface
void paintEmptyState(LICE_IBitmap* bmp, const Rect& area);
void paintControls(LICE_IBitmap* bmp, const Rect& panel); // S12/S15/S16 param surface
void paintControls(LICE_IBitmap* bmp, const Rect& panel, const PerformanceZone& zone); // S12/S15/S16 + keyTrack
// Trace the S-VIEW-3 amp-envelope overlay + its draggable node handles over `waveArea` for
// `zone`'s play params, at the sample's wall-clock duration. Shared by the Sample hero band.
void paintEnvelopeOverlay(LICE_IBitmap* bmp, const Rect& waveArea, const PerformanceZone& zone,
std::int64_t frames);
void onMouseDown(int x, int y);
void onMouseMove(int x, int y);
void onMouseUp(int x, int y);
// Route a click at (x,y) into the param control panel `panel` editing map_.zones[zoneIndex]:
// a toggle segment commits immediately, a slider grab starts a live param-drag (kParamSlider),
// the keyTrack slider likewise (against the zone's keyTrack scalar). Returns true when the
// click landed on a control (handled). `zoneIndex` must be a valid index into map_.zones.
bool handleControlClick(int zoneIndex, const Rect& panel, int x, int y);
// Apply a slider/toggle interaction to map_.zones[zoneIndex] for control `id`: routes ordinary
// controls through applyControl against the zone's play struct, and kKeyTrack against the
// zone's keyTrack scalar (0..200% over the slider's 0..1). Used by both the click + drag paths.
void applyZoneControl(int zoneIndex, int id, double value, int segment);
// Resolve the interactive element under (x, y) into hover_ (Phase L, L3). Called from
// WM_MOUSEMOVE (also while a drag is in flight — the resolved element just isn't used
// for a hover repaint mid-drag). Repaints only when the hovered element changed, so an
@@ -244,6 +270,49 @@ private:
// into the control's stored domain) or a toggle's `segment` (0/1). Mutates `play` in place.
void applyControl(int id, ZonePlaySeconds& play, double value, int segment) const;
// --- S-VIEW-3 envelope overlay seam (frames <-> fraction converter) ----------
//
// envelope_overlay's AmpEnvelope is a DERIVED VIEW, not a TriggerParams copy: it stores the
// Trigger fades as FRACTIONS of the played span, while the zone stores them as SOURCE FRAMES.
// These two members own the non-trivial conversion on BOTH paths (documented in
// envelope_overlay.h's TRIGGER SEAM note). `frames` is the sample's total source frame count;
// `rate` is the live sample rate (the wall-clock AHDSR seconds are rate-free and copy 1-to-1,
// but the Trigger played-span math needs the frame count).
// PACK (draw): zone play params -> AmpEnvelope. Copies AHDSR seconds directly; derives the
// Trigger fade fractions from the source-frame fades over the played span.
AmpEnvelope packEnvelope(const ZonePlaySeconds& play, std::int64_t frames) const;
// UNPACK (commit): an edited AmpEnvelope -> the zone's play params. Copies AHDSR seconds
// directly; converts the Trigger fade fractions back to source frames over the played span.
// Mutates `play` in place; only the mode-relevant fields are written.
void unpackEnvelope(const AmpEnvelope& env, std::int64_t frames, ZonePlaySeconds& play) const;
// The clamp bounds envelope_edit uses, matching the control-panel sliders' own domains (so a
// node drag can never produce a param a slider couldn't — the S-VIEW-F2 invariant).
EnvClampBounds envClampBounds() const;
// --- Sample-view resolution helpers (the ONE storage site, S15-F2) -----------
//
// The single-capture Sample face reads/writes the same one-zone map site as the Zone surface.
// These resolve the effective values for the picked id: effectiveSampleZone returns the picked
// id's one-zone override (found in map_) or a product-default PerformanceZone bound to the
// picked id (not yet materialized — a control edit materializes it, mirroring the Zone path).
PerformanceZone effectiveSampleZone() const;
// The effective root: the picked id's rootOverride, else its bank intrinsic, else middle C.
int effectiveRoot() const;
// The live sample rate from the bridge (for the envelope overlay's seconds<->frames time base),
// or 0 when unavailable (the caller guards). Matches the voice engine's resolution rate.
double liveSampleRate() const;
// The persisted preview velocity as a 0..1 slider value (MIDI 1..127 mapped onto [0,1]).
double previewVelocity01() const;
// Find-or-materialize the one-zone override for the picked id and return a mutable index into
// map_.zones (appending a product-default zone if none exists). selectedId_ must be non-empty.
// The mirror of upsertPickedOverride for a control edit — used when a Sample-face control edit
// needs a concrete zone to write. Returns -1 if selectedId_ is empty.
int ensureSampleZone();
ReaSamplerProcessor* processor_ = nullptr;
// --- Snapshot of the live bank (drawn each paint; refreshed off the audio thread) ---
@@ -255,9 +324,22 @@ private:
ChannelMode channelMode_ = ChannelMode::Mono; // S7 mono/stereo toggle snapshot
// --- Transient UI state (not persisted; component state carries selection + zones) ---
View view_ = View::kBrowser; // default face is the browser
View view_ = View::kSample; // default face is the loaded-sample home (S-VIEW-1)
std::string activeFilterBankId_; // "" = All; else a bank id from banks_
int selectedZone_ = -1; // highlighted zone in the Zones panel; -1 = none
int selectedZone_ = -1; // highlighted zone in the Zone surface; -1 = none
// --- S-VIEW-5 Browse modal picker (a selection PENDING confirm) ---------------
// The Browse overlay is a select-then-confirm picker: a click marks a pending pick without
// loading it; Confirm (or double-click) commits it to selectedId_ + reloads and returns to
// Sample; Cancel discards it and returns to Sample unchanged. "" = nothing picked yet.
std::string browsePendingId_;
int lastBrowseClickCard_ = -1; // for double-click-to-load detection (visible_ index)
// --- S-VIEW-4 preview-trigger note (transient) -------------------------------
// The MIDI note the preview button is currently sounding (a held Gate voice), or -1 when the
// button is up. Set on preview-button press (note-on posted to the processor), cleared on
// release (note-off posted). One note at a time — a fresh press releases the prior.
int previewingNote_ = -1;
// --- S13 drop-to-load affordance (relay DEGRADED — transient, never persisted) ----
// S13's cross-artifact ingest relay (editor drop -> extension ingest) is NOT shipped: the
@@ -308,9 +390,16 @@ private:
// from it). S12/S15/S16 param-slider drag: which control id + the panel it lives in (the
// shell re-lays the panel each move to map x->value against the live control rect).
int dragStartScrollOffset_ = 0;
int dragParamId_ = -1;
int dragParamId_ = -1; // ControlDesc id under a kParamSlider drag; -2 = preview-vel knob
int dragParamZone_ = -1; // the zone index a kParamSlider drag edits (Sample or Zone face)
Rect dragParamPanel_{};
// S-VIEW-3 envelope-node drag: which node is grabbed + the AmpEnvelope snapshotted at grab
// (so the pixel delta is absolute, per envelope_edit's grabEnv contract). The overlay rect +
// sample frame count are re-derived at move time from the live Sample-view layout.
EnvNode envNode_ = EnvNode::Origin;
AmpEnvelope dragStartEnv_{};
// --- Peak-thumbnail cache (mirror of bank_panel; id -> envelope at a bin width) ------
// Keyed by "id|binCount" so a resize recomputes at the new width. Cleared on refresh so
// a bank edit (a re-captured or deleted sample) does not show a stale thumbnail.