// reasampler_editor.h — VST3 IPlugView LICE editor for the ReaSampler 9000 UI. Thin shell: // hosts a LICE child window, routing host paint/mouse into the pure geometry modules // (sample_bands, sample_chrome, capture_browser, keyboard_strip, sample_map). The Sample // face is a three-band stack — chrome, waveform, decks — and the shell TUs split on that // same axis; Browse is a modal picker over it. All layout/hit-test/drag math lives in the // pure modules; every edit commits off the audio thread via reloadInstrument. #pragma once #include #include #include #include #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) #include "core/instrument/ui/knob_deck.h" // DeckGroupDesc / DeckLayout (the deck band) #include "core/instrument/ui/sample_bands.h" // SampleBands (the band-stack allocator) #include "core/instrument/ui/sample_chrome.h" // ChromeRects (chrome-band interior) #include "core/audio/peaks.h" // Envelope (the cached peak thumbnail) #include "core/instrument/map/sample_map.h" // SampleChoice, BankChoice, InstrumentParams #include "core/instrument/engine/velocity_curve.h" // VelocityCurve (transfer-curve editor state) #ifdef _WIN32 #include #endif class LICE_IBitmap; // fwd: the paint helpers take one; lice.h is included only in the .cpp namespace reasampler::vst { using audio::AudioSample; using audio::Envelope; using instrument::map::BankChoice; using instrument::map::InstrumentParams; using instrument::map::PlaySeconds; using instrument::map::SampleChoice; using instrument::map::SampleRefEntry; using instrument::map::SampleRefs; using instrument::ui::AmpEnvelope; using instrument::ui::ChromeRects; using instrument::ui::DeckGroupDesc; using instrument::ui::EnvClampBounds; using instrument::ui::EnvNode; using instrument::ui::OverlayArea; using instrument::ui::Rect; using instrument::ui::SampleBands; class ReaSamplerProcessor; class ReaSamplerEditor : public Steinberg::CPluginView { public: // `processor` outlives this editor; the editor reads the live bank through it and drives // selection/parameter 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: // Sample is the home/default face (the three-band stack). Browse is a full-window modal // picker overlaid on it. enum class View { kSample, kBrowse }; // What a mouse drag is currently editing. kWaveMarker/kEnvNode/kCurveNode track their // grabbed item in waveMarker_/envNode_/curvePointIndex_; kDeckKnob is a grab-anchored // knob drag (control in dragParamId_, grab value in dragKnobStartValue_). enum class DragKind { kNone, kRootMarker, kWaveMarker, kScrollThumb, kEnvNode, kCurveNode, kDeckKnob }; // 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. 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. enum class WaveMarker { kStart = 0, kLoopStart = 1, kLoopEnd = 2, kCount = 3 }; // The interactive element under the pointer, resolved live in WM_MOUSEMOVE. `index` // disambiguates within a kind (tab ordinal, visible-card index, control-row id); -1 when // not applicable. enum class HoverKind { kNone, kNavBrowse, // the chrome "Browse" toolbar button (opens the Browse modal) kBack, // the Browse "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 preview-trigger button kControl, // a knob-deck element (index = control id) kCurveNode, // a velocity-curve control point (index = point index) kVelKnob, // the chrome preview-velocity radial knob kStripKey, // a piano-strip key (index = MIDI note); carries the name tooltip kCurveButton, // the chrome mini curve-preview button (opens the popup) kPopupClose, // the curve popup's Close (x) button }; 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); } }; // The three-band stack for the current client size, plus the chrome interior. Every // paint/hit-test path derives both through this one call so draw and hit-test can never // disagree about where a band is. struct FaceLayout { SampleBands bands; ChromeRects chrome; std::vector deckDescs; }; FaceLayout faceLayout(int w, int h) const; #ifdef _WIN32 void paint(HDC hdc); void paintSample(LICE_IBitmap* bmp, int w, int h); // home face (band composition) void paintBrowse(LICE_IBitmap* bmp, int w, int h); // modal picker overlay void paintEmptyState(LICE_IBitmap* bmp, const Rect& area); // --- Band painters (one TU each, mirroring the input side) --- // Chrome: title band + Browse nav + the control row (root strip, preview, velocity knob, // curve button, channel toggle). void paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool empty); // The hovered piano key's note-name chip. Drawn after every band — it overhangs the // chrome into whatever is below it. void paintChromeTooltip(LICE_IBitmap* bmp, const FaceLayout& fl, int w, int h); // Waveform: the channel lane(s), the loop/start markers, and the envelope overlay. void paintWaveform(LICE_IBitmap* bmp, const Rect& band); // Decks: the group fence + caption + compact caption toggles + radial knobs with // label<->value swap on hover/drag. void paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl); // The mini curve-preview button (chrome) and the modal curve editor it summons. void paintCurveButton(LICE_IBitmap* bmp, const Rect& r); void paintCurvePopup(LICE_IBitmap* bmp, int w, int h); // The velocity->amp transfer-curve editor (X = velocity 0-127, Y = amp 0-1); its only // host is the popup sheet. `r` empty -> draws nothing. void paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r); // Traces the amp-envelope overlay + its draggable node handles over `waveArea`, ONCE at // full band height (never per lane). void paintEnvelopeOverlay(LICE_IBitmap* bmp, const OverlayArea& waveArea, std::int64_t frames); // --- Input: the mouse-down dispatch and its per-band branches --- void onMouseDown(int x, int y); // Each returns true when it consumed the click. Called in band order by onMouseDown. bool mouseDownChrome(const FaceLayout& fl, int x, int y); bool mouseDownWaveform(const FaceLayout& fl, int x, int y); 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 void dragWaveform(const FaceLayout& fl, int x, int y); // kEnvNode / kWaveMarker void dragDeck(int x, int y); // kDeckKnob void dragBrowse(int x, int y); // kScrollThumb void dragCurve(int x, int y); // kCurveNode void onMouseMove(int x, int y); void onMouseUp(int x, int y); // Right-click is the curve popup's primary node-delete affordance; only acts while the // popup is open (deletePoint's endpoint guard makes an endpoint right-click a no-op). void onMouseRDown(int x, int y); // Mouse-down inside curve-editor box `r`: a node grab starts a kCurveNode drag; // Alt-click on an interior node deletes it at once; an empty-space click adds a point // and grabs it. void handleCurveMouseDown(const Rect& r, int x, int y); // Left-click while the curve popup is open (modal over the Sample face): Close / // outside-wash dismiss, in-box clicks route to the curve machinery, else swallowed. // Returns true whenever the popup is open (it consumed the click). bool handlePopupMouseDown(int w, int h, int x, int y); // Resolves the interactive element under (x, y) into hover_, called from WM_MOUSEMOVE. // Repaints only on change, so an idle move is free. The per-band resolvers mirror the // mouse-down branches but are read-only. Windows-only. void resolveHover(int x, int y); HoverTarget hoverChrome(const FaceLayout& fl, int x, int y) const; HoverTarget hoverDeck(const FaceLayout& fl, int x, int y) const; HoverTarget hoverBrowse(int w, int h, int x, int y) const; HoverTarget hoverCurvePopup(int w, int h, int x, int y) const; bool isHovered(HoverKind kind, int index) const { return hover_.kind == kind && hover_.index == index; } void onMouseWheel(int delta); // browser scroll (wheel) void onSearchChar(unsigned int ch); // type-to-filter search keystroke // An OS file drop landed on the editor window. We do NOT ingest (read-only bank // consumer) — flash a "drop on the ReaSampler panel to add" affordance instead of // silently swallowing it. Never inserts a timeline item. void onFilesDropped(int droppedCount); // The change-detection tick (WM_TIMER, UI thread only): polls the processor's bank-sync // and re-snapshots + repaints when anything changed. Suppressed mid-drag so a reload // never yanks 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 + parameter set. Main/UI thread only. Called on attach and after any edit. void refreshFromBank(); // Publishes the edited selection + parameters to the processor, then rebuilds the // instrument off the audio thread. UI thread only. void commitAndReload(); // Commits `id` as the loaded capture. The one parameter set carries over — it governs // whatever is loaded, so a load swaps the sound, not the settings. void loadSelection(const std::string& id); // Recomputes the visible capture cards (samples_ narrowed by activeFilterBankId_ then // search) into visible_. Called on refresh + filter change. void rebuildVisible(); // The peak thumbnail for a bank sample id at `binCount` bins, cached by (id, binCount). // 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, cached by id — feeds both the binned // waveform envelope and the zero-crossing snap. Empty vector on decode failure. UI // thread only (file I/O); cleared with the thumbnail cache on refresh. const std::vector& monoPcmFor(const std::string& sampleId); // The interleaved source PCM behind the stereo waveform lanes. struct ChannelPcm { std::vector interleaved; // frame-interleaved source frames int channelCount = 0; // 0 = nothing decoded std::int64_t frameCount() const { return channelCount > 0 ? static_cast(interleaved.size()) / channelCount : 0; } }; // The interleaved PCM + channel count for a bank sample id. SINGLE-SLOT by design: the // waveform band draws one capture at a time, while monoPcmFor's cache spans every // browsed card — holding interleaved PCM there would pin a whole bank at multi-channel // size. A miss re-decodes (only on selection change or a bank refresh; an edit commit // does not clear it). UI thread only (file I/O). const ChannelPcm& channelPcmFor(const std::string& sampleId); // The project-relative WAV path for a bank sample id: the live bank blob first, the // instance's own SampleRefs as the self-contained fallback. "" when unresolvable. std::string samplePathFor(const std::string& sampleId) const; // The effective loop + start markers for the loaded capture: the parameter set's // override when one is set, else the bank's loop intrinsic / frame 0. Absent loop -> // loopStart==loopEnd==0. `frames` defaults 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; // Writes `m` into params_ as the loop/start override. Does NOT call commitAndReload — // callers decide live-drag vs final commit. void applyMarkers(const SetupMarkers& m); // Deck knobs edit the parameter set's PlaySeconds (play mode + AHDSR; pitch engine + AD // pitch envelope) — wall-clock seconds, rate-free; the build resolves to frames. // The normalized [0,1] display value for control `id` given `play` (seconds -> 0..1 over // a fixed ceiling, sustain 0..1 as-is, %-length/fade frames -> 0..1, semitone depth // centered at 0.5). double controlValue(int id, const PlaySeconds& play) const; // Applies a committed control interaction to `play`: a knob's normalized `value` or a // toggle's `segment` (0/1). Mutates `play` in place. void applyControl(int id, PlaySeconds& play, double value, int segment) const; // Applies a knob/toggle interaction to the ONE parameter set for control `id`: ordinary // controls route through applyControl; kKeyTrack writes the keyTrack scalar (0..200% // over the knob's 0..1). void applyParamControl(int id, double value, int segment); // The Trigger fade-in/out knob full-scale, in source frames: kFadeMaxSeconds resolved // against the live rate — never a baked-in rate. Returns 0 when the rate is unknown. double fadeMaxFrames() const; // envelope_overlay's AmpEnvelope stores Trigger fades as fractions of the played span, // while the parameter set stores source frames — pack/unpack own that conversion (see // envelope_overlay.h's trigger-seam note). `frames` is total source frames; AHDSR // seconds are rate-free and copy 1-to-1. // PACK (draw): play params -> AmpEnvelope. `startFrame` is the effective start point. AmpEnvelope packEnvelope(const PlaySeconds& play, std::int64_t frames, std::int64_t startFrame) const; // UNPACK (commit): an edited AmpEnvelope -> the play params, in place. void unpackEnvelope(const AmpEnvelope& env, std::int64_t frames, std::int64_t startFrame, PlaySeconds& play) const; // Clamp bounds envelope_edit uses, matching the sliders' own domains so a node drag can // never produce a param a slider couldn't. EnvClampBounds envClampBounds() const; // The effective root: params_.rootOverride, else the bank intrinsic, else middle C. int effectiveRoot() const; // The live sample rate from the bridge, or 0 when unavailable (caller guards). double liveSampleRate() const; // Persisted preview velocity as a 0..1 slider value (MIDI 1..127 -> [0,1]). double previewVelocity01() 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. double deckControlNorm(int id) const; // Applies a deck-knob value: parameter-set ids write params_ (commit on release); // processor params write through the processor setters immediately (transient — no // params edit, no reload). void applyDeckKnob(int id, double norm); // The knob's live value label shown during hover/drag: seconds, percents, source // frames, signed semitones, a voice count, or the master-gain dB. std::string deckValueLabel(int id) const; 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 loaded capture ("" = empty state) InstrumentParams params_; // the ONE parameter set governing it ChannelMode channelMode_ = ChannelMode::Mono; // mono/stereo toggle snapshot // Mirrors of the processor's persisted voice-system params, refreshed with the rest of the // live snapshot; every edit writes through the processor setters (which rebuild the engine // off-thread via the drain-slot swap). int voiceCount_ = kDefaultVoiceCount; VoiceMode voiceMode_ = VoiceMode::Poly; MonoTrigger monoTrigger_ = MonoTrigger::Retrigger; // Transient UI state (not persisted; component state carries selection + parameters). View view_ = View::kSample; // default face is the loaded-sample home std::string activeFilterBankId_; // "" = All; else a bank id from banks_ // The Browse overlay is a select-then-confirm picker: a click marks a pending pick; // Confirm/double-click commits it + reloads; Cancel discards it. "" = nothing picked. std::string browsePendingId_; int lastBrowseClickCard_ = -1; // for double-click-to-load detection (visible_ index) // The MIDI note the preview button is currently sounding (held Gate voice), or -1 when // up. One note at a time — a fresh press releases the prior. int previewingNote_ = -1; // The editor-drop -> extension-ingest relay is not shipped (the bridge is read-only): // an OS drop just flashes a "drop on the panel instead" banner (dropHintTicks_ counts // down via the sync tick). Never ingests, never inserts a timeline item. int dropHintTicks_ = 0; // remaining sync ticks to show the drop affordance // 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 // Hover state (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 dragCurX_ = 0; // live cursor x (px) during a drag — updated in onMouseMove int dragCurY_ = 0; // live cursor y (px) during a drag — updated in onMouseMove InstrumentParams dragStartParams_; // params_ snapshotted at grab; restored on capture-loss // Waveform-marker drag: which marker + the marker set snapshotted at grab time, so the // pixel-delta resolver shifts from the 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; // effective start point at grab time; for env-node drag // Scrollbar-thumb drag: the offset at grab time. kDeckKnob drag: which control id. int dragStartScrollOffset_ = 0; int dragParamId_ = -1; // control id under a kDeckKnob drag; -2 = preview-vel knob // Envelope-node drag: which node + the AmpEnvelope snapshotted at grab (absolute-delta // contract, per envelope_edit's grabEnv). EnvNode envNode_ = EnvNode::Origin; AmpEnvelope dragStartEnv_{}; // Velocity-curve node drag: which point, the curve snapshotted at grab // (resolvePointDrag's absolute-delta contract), and the grab-time box rect. int curvePointIndex_ = -1; VelocityCurve dragStartCurve_ = VelocityCurve::flat(); Rect dragCurveRect_{}; // Deck-knob drag: the normalized value at grab — knobDragValue maps the vertical pixel // delta from this anchor, so a grab never jumps the value. double dragKnobStartValue_ = 0.0; // Curve popup open flag, never persisted. bool curvePopupOpen_ = false; // Peak-thumbnail cache (mirror of bank_panel), keyed by "id|binCount" so a resize // recomputes at the new width. Cleared on refresh so a stale sample never shows. std::unordered_map thumbCache_; // Decoded mono-PCM cache, keyed by id (width-independent). Feeds the waveform envelope // binning + zero-crossing snap. Cleared alongside thumbCache_ on refresh. std::unordered_map> pcmCache_; // The single-slot interleaved-PCM cache behind channelPcmFor (see its note on why this // is not keyed into pcmCache_). Cleared alongside pcmCache_ on refresh. std::string channelPcmId_; ChannelPcm channelPcm_; }; } // namespace reasampler::vst