// reasampler_editor.h — the VST3 IPlugView LICE editor for the ReaSampler 9000 // capture-first UI (Phase S10). THIN shell: hosts a LICE-drawn child window inside the // host's IPlugView seat and routes host paint/mouse into the pure geometry modules // (capture_browser, keyboard_strip) + the pure mapping (sample_map). Windows-only (D5). // // The default face is the CAPTURE BROWSER: a bank-filter tab strip over a grid of // scannable capture cards (peak thumbnail + name + root/key badge). A fresh instance with // no pick shows a "pick a capture" EMPTY STATE and plays silence (the S10 policy reversal // of the S4 first-sample auto-play). Picking a card loads that one capture and reveals a // guided SINGLE-CAPTURE SETUP surface (a keyboard strip with the capture's root marker + // a level readout). Multi-zone keymap editing is a demoted, opt-in ZONES panel (S10-Z), // reached by a toggle and driven by the same keyboard_strip drag machine. // // All layout/hit-test/drag math lives in the pure modules; this shell only draws + routes // (a LICE_SysBitmap blitted in WM_PAINT, a WM_LBUTTONDOWN/WM_MOUSEMOVE/WM_LBUTTONUP // drag-state machine hit-testing via the pure resolvers). Peak thumbnails are computed // shell-side from the decoded WAV (bank_model's Sample carries no envelope) and cached — // the mirror of bank_panel::thumbnailFor. Every edit commits OFF the audio thread via the // processor's reloadFromBank (RT path untouched). // // Subclasses CPluginView for the IPlugView boilerplate; overrides the attach/remove hooks // to create/destroy the child window and onSize to resize it. #pragma once #include #include #include #include #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) #ifdef _WIN32 #include #endif class LICE_IBitmap; // fwd: the paint helpers take one; lice.h is included only in the .cpp namespace reasampler::vst { class ReaSamplerProcessor; class ReaSamplerEditor : public Steinberg::CPluginView { public: // `processor` owns this editor's lifetime domain and outlives it; the editor reads the // live bank through it and drives selection/zone edits + reload on user input. May be // null (defensive — a real host always supplies one). explicit ReaSamplerEditor(ReaSamplerProcessor* processor); ~ReaSamplerEditor() override; Steinberg::tresult PLUGIN_API isPlatformTypeSupported( Steinberg::FIDString type) override; Steinberg::tresult PLUGIN_API canResize() override; Steinberg::tresult PLUGIN_API checkSizeConstraint(Steinberg::ViewRect* rect) override; protected: void attachedToParent() override; void removedFromParent() override; Steinberg::tresult PLUGIN_API onSize(Steinberg::ViewRect* newSize) override; private: // 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_); 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, 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 // maps it to the picked zone's play params. Order here is the panel's top-down stack order. enum class ParamControl { kPlayMode = 0, // Gate | Trigger toggle (S15) kPitchEngine, // Varispeed | Preserve toggle (S16) kAttack, // AHDSR attack (Gate) / — kHold, // AHDSR hold (Gate, S15) kDecay, // AHDSR decay (Gate) kSustain, // AHDSR sustain (Gate) kRelease, // AHDSR release (Gate) kTrigLength, // Trigger %-length (Trigger, S15) kTrigFadeIn, // Trigger fade-in (Trigger, S15) kTrigFadeOut, // Trigger fade-out (Trigger, S15) kPitchEnvEnable, // AD pitch envelope on|off (S16) 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 }; // The waveform markers on the single-capture setup surface (S11). Order is the draw + hit // order (start first). Named generically per the spec so S15 can repurpose the surface with // a different marker set; here it is start-point + the sustain loop's two ends. enum class WaveMarker { kStart = 0, kLoopStart = 1, kLoopEnd = 2, kCount = 3 }; // --- Hover model (Phase L, L3) ------------------------------------------------ // // The interactive element under the pointer, resolved live in WM_MOUSEMOVE so the kit // draws its hover state on that element only ("hover on every interactive element" + // "sub-frame feedback = the perception of speed", §3.3/§3.5). Cleared to kNone on // WM_MOUSELEAVE (tracked via TrackMouseEvent). `index` disambiguates within a kind // (tab ordinal, visible-card index, control-row id); -1 when not applicable. Mirror of // bank_panel's L2 hover model. enum class HoverKind { kNone, 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) }; struct HoverTarget { HoverKind kind = HoverKind::kNone; int index = -1; bool operator==(const HoverTarget& o) const { return kind == o.kind && index == o.index; } bool operator!=(const HoverTarget& o) const { return !(*this == o); } }; #ifdef _WIN32 void paint(HDC hdc); 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, 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 // idle mouse-move is free. Windows-only (the hit-tests use the shell's Win32 client rect). void resolveHover(int x, int y); // True iff element (kind, index) is the live hover_ target — the shell maps this to the // kit's Hover interaction state when the element has no more-specific state (Active, etc.). bool isHovered(HoverKind kind, int index) const { return hover_.kind == kind && hover_.index == index; } void onMouseWheel(int delta); // S12 browser scroll (wheel) void onSearchChar(unsigned int ch); // S12 type-to-filter search keystroke // S13 (relay degraded): an OS file drop landed on the editor window. We do NOT ingest (the // instrument is a read-only bank consumer and the relay is unshipped) — we flash the "drop // on the ReaSampler panel to add" affordance so the drop is never silently swallowed and the // shipped ingest gesture stays discoverable. `droppedCount` is how many files were dropped // (drawn into the banner). NEVER inserts a timeline item / never touches the bank. void onFilesDropped(int droppedCount); // The S9/S8 change-detection tick (WM_TIMER on the child window — the UI thread, NEVER the // audio thread). Polls the processor's bank-sync (generation change -> hands-free reload; // a new assignment request -> apply as this instance's selection) and, when anything // changed, re-snapshots the editor's own view (refreshFromBank) + repaints so the browser / // setup surface reflect the new bank. An open editor means THIS instance is the focused // assignment target (the thundering-herd policy — see the handoff), so it passes true. // Suppressed WHILE A DRAG IS IN FLIGHT so a mid-drag reload does not yank the edit surface. void onSyncTimer(); static LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); void invalidate(); HWND childHwnd_ = nullptr; #endif // Re-read the bank (samples + banks) from the live bridge and snapshot the instrument's // selection + performance map. Main/UI thread only. Called on attach and after any edit. void refreshFromBank(); // Publish the edited zones/selection to the processor, then rebuild the instrument OFF // the audio thread. UI thread only. One place so every edit commits identically. void commitAndReload(); // Recompute the capture cards visible under the current bank filter (samples_ narrowed by // activeFilterBankId_; "" = All) into visible_. Called on refresh + filter change. void rebuildVisible(); // The peak thumbnail for a bank sample id at `binCount` bins, computed once from the // decoded WAV (mirror of bank_panel::thumbnailFor) and cached by (id, binCount). Returns // an empty envelope when the WAV can't be resolved/decoded. UI thread only (file I/O). const Envelope& thumbnailFor(const std::string& sampleId, int binCount); // The decoded MONO PCM for a bank sample id, decoded once from the WAV and cached by id. // Feeds the S11 waveform surface: the full-res envelope binned at view width AND the // zero-crossing snap (both need the raw frames, not the binned thumbnail). Returns an empty // vector when the WAV can't be resolved/decoded. UI thread only (file I/O). Reuses the same // decode path as thumbnailFor (no new WAV reader), keyed by id (not width — snap is width- // independent). Cleared with the thumbnail cache on refresh. const std::vector& monoPcmFor(const std::string& sampleId); // The effective loop + start markers for the picked single capture (S11): the per-zone // OVERRIDE for the picked id when one exists in map_, else the bank's S2 loop intrinsic // (loop) / frame 0 (start). Absent loop -> loopStart==loopEnd==0 (the "no loop" state). // frames is the decoded length (for defaulting loopEnd when the bank left the loop empty). struct SetupMarkers { std::int64_t start = 0; std::int64_t loopStart = 0; std::int64_t loopEnd = 0; bool hasLoop = false; // whether a sustain loop is set (drives the "no loop" affordance) }; SetupMarkers pickedMarkers(std::int64_t frames) const; // Commit an edited marker set for the picked capture as a per-zone loop/start override // (upsert on the picked id — mirror of the root-marker path), then reload off-thread. void commitPickedMarkers(const SetupMarkers& m); // Write `m` as a loop/start override upsert into map_ for selectedId_ (find-or-append). // Does NOT call commitAndReload — callers decide whether this is a live-drag update or a // final commit. selectedId_ must be non-empty before calling. Returns the zone index // (0-based) that was updated or appended, so callers can set selectedZone_. int upsertPickedOverride(const SetupMarkers& m); // --- S12/S15/S16 parameter surface (Zones panel, keyed to selectedZone_) ------ // // The control panel edits the SELECTED zone's ZonePlaySeconds (S15 play mode + AHDSR; S16 // pitch engine + AD pitch envelope). Wall-clock times are SECONDS (rate-free); the keymap // build resolves them to frames at the live rate. Instrument-owned (D-B), never a bank fact. // The control descriptors the panel shows for `play`'s CURRENT play mode: the two toggles + // the mode-relevant sliders (AHDSR for Gate, %-length/fades for Trigger) + the pitch-envelope // controls. The pure param_slider lays these out; this only picks the set. Static (a free // choice of set from the mode) — kept a member for the ParamControl enum access. std::vector controlDescs(const ZonePlaySeconds& play) const; // The normalized [0,1] display value for control `id` given `play` (the shell's domain // mapping: seconds->0..1 over a fixed seconds ceiling, sustain 0..1 as-is, %-length/fade // frames->0..1, semitone depth centered at 0.5). double controlValue(int id, const ZonePlaySeconds& play) const; // Apply a committed control interaction to `play`: a slider's normalized `value` (mapped back // 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. // `startFrame` is the zone's effective start point (zone.startPoint.value_or(0)). AmpEnvelope packEnvelope(const ZonePlaySeconds& play, std::int64_t frames, std::int64_t startFrame) 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. // `startFrame` is the zone's effective start point (zone.startPoint.value_or(0)). // Mutates `play` in place; only the mode-relevant fields are written. void unpackEnvelope(const AmpEnvelope& env, std::int64_t frames, std::int64_t startFrame, 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) --- std::vector samples_; // every bank sample, bank order std::vector banks_; // the named banks, for the filter tab strip std::vector visible_; // samples_ narrowed by the active bank filter std::string selectedId_; // the single-capture pick ("" = empty state) PerformanceMap map_; // the opt-in zones (empty = no zones) ChannelMode channelMode_ = ChannelMode::Mono; // S7 mono/stereo toggle snapshot // --- Transient UI state (not persisted; component state carries selection + zones) --- 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 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 // instrument's REAPER bridge is deliberately READ-ONLY (it never writes the bank / ext // state), so an editor drop cannot relay a bank-ingest request without a new write seam + // an extension-side poller (surfaced as a decision, not crossed here). The DEGRADE path per // the spec: the editor ACCEPTS the drop (WM_DROPFILES) and, rather than silently swallowing // it, flashes a clear affordance pointing at the shipped ingest gesture (drop onto the // docked ReaSampler panel). When > 0, the affordance banner is shown; each sync tick decays // it so it auto-dismisses. No file is ingested, no timeline item is ever inserted. int dropHintTicks_ = 0; // remaining sync ticks to show the drop affordance // --- S12 browser scroll + search (transient UI state, never persisted) -------- int scrollOffset_ = 0; // vertical px offset into the card grid (clamped) std::string searchQuery_; // type-to-filter narrow; "" = no search bool searchFocused_ = false; // whether the search box has keyboard focus // --- S12 numeric note entry (LICE text-entry idiom, transient) ---------------- // When >= 0, a low/high/root field is being typed; entryText_ accumulates the keystrokes // and commits (parseNoteEntry) on Enter. -1 = no field editing. The field id is a // ParamControl-independent small enum encoded inline (see the .cpp: 0=low,1=high,2=root). int entryField_ = -1; std::string entryText_; // --- Hover state (Phase L, L3; transient, never persisted) -------------------- HoverTarget hover_; // the interactive element under the pointer #ifdef _WIN32 bool mouseTracking_ = false; // TrackMouseEvent armed for WM_MOUSELEAVE this "over" cycle #endif // --- Drag-state machine ------------------------------------------------------ DragKind drag_ = DragKind::kNone; int dragStartX_ = 0; // grab x (px), for the pixel-delta resolver int dragStartY_ = 0; // grab y (px), for the vertical scrollbar-thumb drag int dragStartLow_ = 0; // the grabbed field's note at grab time int dragStartHigh_ = 0; int dragStartRoot_ = 60; PerformanceMap dragStartMap_; // map_ snapshotted at grab; restored on capture-loss // S11 waveform-marker drag: which marker + the marker set snapshotted at grab time (so the // pixel-delta resolver shifts the grabbed frame from its grab-time value, and inter-marker // clamps use the sibling markers). WaveMarker waveMarker_ = WaveMarker::kStart; SetupMarkers dragStartMarkers_; std::int64_t dragSampleFrames_ = 0; // decoded length of the sample under the drag std::int64_t dragStartFrame_ = 0; // zone startPoint at grab time (0 if absent); for env-node drag // S12 scrollbar-thumb drag: the offset held at grab time (the pixel-delta resolver shifts // 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; // 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. std::unordered_map thumbCache_; // --- Decoded mono-PCM cache (S11; id -> full-res frames) ------------------------------ // Keyed by id (width-independent, unlike thumbCache_). Feeds the waveform envelope binning // + the zero-crossing snap. Cleared alongside thumbCache_ on refresh so a re-captured or // deleted sample does not show/snap against stale PCM. std::unordered_map> pcmCache_; }; } // namespace reasampler::vst