576 lines
31 KiB
C++
576 lines
31 KiB
C++
// 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 <optional>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
#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" // StageEnvelope / 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/spline_edit.h" // the shared point-editing grammar
|
|
#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 <windows.h>
|
|
#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::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;
|
|
using instrument::ui::StageEnvelope;
|
|
|
|
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_).
|
|
// kSplineNode is the overlay's peer of kCurveNode: the same VelocityCurve point drag, over
|
|
// the waveform overlay's box and the overlay-active envelope's contour rather than the
|
|
// popup's box and curve.
|
|
enum class DragKind { kNone, kRootMarker, kWaveMarker, kScrollThumb, kEnvNode,
|
|
kCurveNode, kSplineNode, kDeckKnob };
|
|
|
|
// Which envelope the waveform overlay is drawing and editing. The selection type and its
|
|
// whole state machine are the pure deck_groups module's; this alias keeps the shell's
|
|
// spelling.
|
|
using OverlayEnv = instrument::ui::OverlayEnv;
|
|
|
|
// Which of the three velocity curves a deck cell edits — also the popup's open state.
|
|
using CurveTarget = instrument::ui::CurveTarget;
|
|
|
|
// 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 chrome knobs are drags of kind kDeckKnob with no place in the deck's id space, so
|
|
// they take negative sentinels (-2 is the preview velocity). Being outside
|
|
// [0, DeckParam::kCount) is what keeps liveCommitFor answering "not a live control".
|
|
static constexpr int kBakeHoldKnobId = -3;
|
|
|
|
// The waveform markers on the waveform band: start-point + the sustain loop's two ends,
|
|
// in draw + hit order, then the crossfade handle. The crossfade is NOT part of the
|
|
// full-height column hit-test — it answers only in its top-strip handle (waveform_view's
|
|
// markerHandleRect), because at a zero fade it sits exactly on the loop start.
|
|
enum class WaveMarker { kStart = 0, kLoopStart = 1, kLoopEnd = 2, kLoopXfade = 3,
|
|
kCount = 4 };
|
|
|
|
// 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
|
|
kBake, // the resample-bake trigger
|
|
kControl, // a knob-deck element (index = control id)
|
|
kInnerDial, // a knob cell's inner curve dial (index = the OUTER control id)
|
|
kEnvRadio, // an envelope deck's overlay-select radio (index = radio control id)
|
|
kCurveNode, // a velocity-curve control point (index = point index)
|
|
kVelKnob, // the chrome preview-velocity radial knob
|
|
kHoldKnob, // the chrome bake-Hold radial knob
|
|
kStripKey, // a piano-strip key (index = MIDI note); carries the name tooltip
|
|
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<DeckGroupDesc> 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,
|
|
// 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);
|
|
|
|
// A deck cell's mini curve thumbnail (the VELOCITY group) and the modal editor it summons.
|
|
void paintCurveButton(LICE_IBitmap* bmp, const Rect& r, CurveTarget target, bool disabled,
|
|
bool hovered);
|
|
void paintCurvePopup(LICE_IBitmap* bmp, int w, int h);
|
|
// The velocity transfer-curve editor (X = velocity 0-127, Y = the curve's own domain); its
|
|
// only host is the popup sheet. `r` empty -> draws nothing.
|
|
void paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r);
|
|
// Traces the overlay-active envelope + its draggable handles over `waveArea`, ONCE at
|
|
// full band height (never per lane). Dispatches on the envelope's mode: the staged
|
|
// polyline, or the drawn contour below.
|
|
void paintEnvelopeOverlay(LICE_IBitmap* bmp, const OverlayArea& waveArea, std::int64_t frames);
|
|
// The spline EG's contour + point handles, spanning the overlay 1:1 with the sample's time
|
|
// axis. Hard points draw hollow so a corner is legible before it is steep.
|
|
void paintSplineOverlay(LICE_IBitmap* bmp, const OverlayArea& waveArea);
|
|
|
|
// A click on the spline overlay, routed through the shared grammar; true when it consumed
|
|
// the click. `addOnEmptySpace` false resolves node actions only — the waveform band calls
|
|
// it that way BEFORE the start/loop markers and again after, since the contour's box is the
|
|
// whole band and an unconditional add would make every marker unreachable.
|
|
bool splineOverlayClick(const OverlayArea& waveArea, int x, int y,
|
|
instrument::ui::SplineGesture gesture, bool addOnEmptySpace);
|
|
|
|
// --- 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);
|
|
|
|
// Double-click = reset the radial knob under the pointer to its default. Returns false when
|
|
// no knob claims the point, and the caller then replays it as an ordinary mouse-down (the
|
|
// platform note at the window class explains why that fall-through is load-bearing).
|
|
bool onMouseDoubleClick(int x, int y);
|
|
bool doubleClickChrome(const FaceLayout& fl, int x, int y);
|
|
bool doubleClickDeck(const FaceLayout& fl, 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 primary node-delete affordance on both spline surfaces — the popup
|
|
// while it is open, else the spline EG overlay (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`, resolved through the shared point-editing
|
|
// grammar (spline_edit): a node grab starts a kCurveNode drag, an empty-space click adds a
|
|
// point and grabs it, control-click toggles a node hard/smooth.
|
|
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();
|
|
|
|
// The live peer of commitAndReload for a continuously-valued control (deckParamCommit):
|
|
// the same parameter-set write — so a saved project carries the edit exactly as before —
|
|
// followed by a live publish instead of a rebuild, so the note already sounding follows
|
|
// the knob. Does not repaint; callers already do. UI thread only.
|
|
void commitLive();
|
|
|
|
// Whether an in-flight drag commits live rather than through a reload. A deck knob is
|
|
// live per deckParamCommit; an envelope-node drag is live in EITHER mode — see
|
|
// liveCommitFor (deck_groups.h) for why.
|
|
bool dragCommitsLive(DragKind kind, int paramId = -1) const;
|
|
|
|
// 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<AudioSample>& monoPcmFor(const std::string& sampleId);
|
|
|
|
// The interleaved source PCM behind the stereo waveform lanes.
|
|
struct ChannelPcm {
|
|
std::vector<AudioSample> interleaved; // frame-interleaved source frames
|
|
int channelCount = 0; // 0 = nothing decoded
|
|
std::int64_t frameCount() const {
|
|
return channelCount > 0
|
|
? static_cast<std::int64_t>(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. With no loop set,
|
|
// the loop handles park on loop_span's defaultLoopBounds so both stay grabbable — the
|
|
// frame-0 default they replace put loopStart under the start marker, where nothing could
|
|
// reach it.
|
|
struct SetupMarkers {
|
|
std::int64_t start = 0;
|
|
std::int64_t loopStart = 0;
|
|
std::int64_t loopEnd = 0;
|
|
std::int64_t crossfade = 0; // pre-seam fade, SOURCE frames; handle at loopStart - this
|
|
bool hasLoop = false; // whether a sustain loop is set (drives the "no loop" affordance)
|
|
};
|
|
SetupMarkers pickedMarkers(std::int64_t frames) const;
|
|
|
|
// Whether the loaded sound's bake window needs the user's Hold — the pure predicate
|
|
// (bake_plan.h) answered against the markers this face is showing. Decodes and reads the
|
|
// bank, so it is called on the sync tick, not per paint, and memoized against the inputs
|
|
// below on top of that.
|
|
bool resolveBakeHoldNeeded();
|
|
|
|
// What that answer was last computed against. Invalidated wholesale by refreshFromBank,
|
|
// which is where the bank half of the input changes.
|
|
struct HoldNeedKey {
|
|
std::string sampleId;
|
|
std::optional<SampleLoop> loopOverride;
|
|
std::int64_t crossfade = 0;
|
|
bool operator==(const HoldNeedKey& other) const;
|
|
};
|
|
HoldNeedKey holdNeedKey_;
|
|
bool holdNeedValid_ = false;
|
|
bool holdNeedAnswer_ = false;
|
|
|
|
// 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);
|
|
|
|
// Arms a waveform-marker drag: the grabbed marker plus the snapshot the pixel-delta
|
|
// resolver and the inter-marker clamps measure from.
|
|
void beginMarkerDrag(WaveMarker which, const SetupMarkers& m, std::int64_t frames, int x);
|
|
|
|
// 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 three
|
|
// adapters below are thin int-id wrappers over the pure `deck_values` module, which owns
|
|
// what each control's value means; see its header rather than restating the domains here.
|
|
double controlValue(int id, const PlaySeconds& play) const;
|
|
void applyControl(int id, PlaySeconds& play, double value, int segment) const;
|
|
void resetParamControl(int id);
|
|
|
|
// 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 overlay speaks one StageEnvelope whichever envelope is active; pack/unpack are the
|
|
// only place that knows which stored struct each `which` maps onto, so the drawn shape and
|
|
// a committed node drag can never disagree about it. AHDSR seconds are rate-free and copy
|
|
// 1-to-1; an AHD additionally needs the wall-clock span its Hold fraction is taken against,
|
|
// which is where `frames`/`startFrame` and the live rate come in.
|
|
|
|
// PACK (draw): play params -> StageEnvelope. `startFrame` is the effective start point.
|
|
StageEnvelope packEnvelope(OverlayEnv which, const PlaySeconds& play, std::int64_t frames,
|
|
std::int64_t startFrame) const;
|
|
|
|
// UNPACK (commit): an edited StageEnvelope -> the play params, in place.
|
|
void unpackEnvelope(OverlayEnv which, const StageEnvelope& env, 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 bake Hold division as a 0..1 knob position, via the pure bake_hold map.
|
|
double bakeHoldNorm() 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: milliseconds, percents, Hz, signed
|
|
// semitones, a curve exponent, 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<SampleChoice> samples_; // every bank sample, bank order
|
|
std::vector<BankChoice> banks_; // the named banks, for the filter tab strip
|
|
std::vector<SampleChoice> 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;
|
|
|
|
// Resample bake. The click only ARMS it; the sync tick runs it. Running it inline
|
|
// would nest a synchronous REAPER action — which re-points this very instance — inside
|
|
// a mouse handler with the capture held.
|
|
bool bakePending_ = false;
|
|
// Whether the extension's bake action is registered, resolved on the same tick that
|
|
// governs the button's paint, so the control is never enabled and then refusing.
|
|
bool bakeAvailable_ = false;
|
|
// Whether the bake's window still needs the Hold control (bake_plan.h's
|
|
// bakeWindowNeedsHold). Resolved on the same tick as bakeAvailable_ rather than per paint:
|
|
// answering it costs a bridge read + bank parse whenever no loop override is set.
|
|
bool bakeHoldNeeded_ = false;
|
|
std::string bakeMessage_; // last outcome, shown in the title band
|
|
int bakeMessageTicks_ = 0; // sync ticks the message survives
|
|
|
|
// 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
|
|
|
|
// 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
|
|
// The cell whose INNER dial is under a kDeckKnob drag (dragParamId_ then holds the curve
|
|
// control), so the paint side can light the right ring. -1 when the grab was the outer knob.
|
|
int dragInnerCellId_ = -1;
|
|
|
|
// Which envelope the overlay draws and edits (kNone = none, the opening state).
|
|
OverlayEnv overlayEnv_ = OverlayEnv::kNone;
|
|
|
|
// Envelope-node drag: which node + the StageEnvelope snapshotted at grab (absolute-delta
|
|
// contract, per envelope_edit's grabEnv).
|
|
EnvNode envNode_ = EnvNode::Origin;
|
|
StageEnvelope 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;
|
|
|
|
// 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
|
|
// mutable overload aliasing the amp curve underneath an in-flight pitch/filter drag.
|
|
CurveTarget curvePopup_ = CurveTarget::kNone;
|
|
void closeCurvePopup();
|
|
|
|
// The group-gate state the two pure inert predicates read, built once from the parameter
|
|
// set so paint and hit-test can never assemble it differently.
|
|
instrument::ui::DeckEnableState deckEnableState() const;
|
|
|
|
// THE one switch from an overlay selection to the drawn contour it names, mirroring
|
|
// curveFor. kNone reads as amp — harmless for a target-agnostic caller, and every mutating
|
|
// path is gated on overlayIsSpline() first.
|
|
const VelocityCurve& splineFor(OverlayEnv which) const;
|
|
VelocityCurve& splineFor(OverlayEnv which);
|
|
|
|
// Whether the overlay-active envelope is in Spline mode — the branch every overlay paint,
|
|
// hit-test and drag path takes before touching either model.
|
|
bool overlayIsSpline() const;
|
|
|
|
// THE one switch from a CurveTarget to the parameter-set curve it names — paint (button
|
|
// thumbnails, for every target) and edit (editedCurve, for curvePopup_ specifically) both
|
|
// route through it, so a fourth curve or a moved field is a one-place edit.
|
|
const VelocityCurve& curveFor(CurveTarget target) const;
|
|
VelocityCurve& curveFor(CurveTarget target);
|
|
|
|
// The curve curvePopup_ names — curveFor(curvePopup_), typed as its own pair because every
|
|
// edit path needs the mutable overload and paint needs the const one. The mutable overload
|
|
// refuses kNone (a closed popup has nothing open to edit) rather than aliasing amp.
|
|
VelocityCurve& editedCurve();
|
|
const VelocityCurve& editedCurve() const;
|
|
|
|
// 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<std::string, Envelope> 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<std::string, std::vector<AudioSample>> 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
|