327 lines
19 KiB
C++
327 lines
19 KiB
C++
// 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 <optional>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
#include "public.sdk/source/common/pluginview.h"
|
|
|
|
#include "editor_geometry.h" // Rect (the shell's sub-rect type, shared with the pure modules)
|
|
#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 <windows.h>
|
|
#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. 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 };
|
|
|
|
// 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_).
|
|
enum class DragKind { kNone, kRootMarker, kZoneLow, kZoneHigh, kZoneBody, kWaveMarker,
|
|
kScrollThumb, kParamSlider };
|
|
|
|
// 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)
|
|
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,
|
|
kToggleBrowser, // the Browser toggle segment
|
|
kToggleZones, // the Zones toggle segment
|
|
kSearchBox, // the browser search box
|
|
kFilterTab, // a bank-filter tab (index = tab ordinal, 0 = All)
|
|
kCard, // a capture card (index = visible_ index)
|
|
kChanMono, // the mono channel-mode segment
|
|
kChanStereo, // the stereo channel-mode segment
|
|
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 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 paintEmptyState(LICE_IBitmap* bmp, const Rect& area);
|
|
void paintControls(LICE_IBitmap* bmp, const Rect& panel); // S12/S15/S16 param surface
|
|
|
|
void onMouseDown(int x, int y);
|
|
void onMouseMove(int x, int y);
|
|
void onMouseUp(int x, int y);
|
|
|
|
// 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<AudioSample>& 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<ControlDesc> 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;
|
|
|
|
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 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::kBrowser; // default face is the browser
|
|
std::string activeFilterBankId_; // "" = All; else a bank id from banks_
|
|
int selectedZone_ = -1; // highlighted zone in the Zones panel; -1 = none
|
|
|
|
// --- 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
|
|
|
|
// 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;
|
|
Rect dragParamPanel_{};
|
|
|
|
// --- 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<std::string, Envelope> 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<std::string, std::vector<AudioSample>> pcmCache_;
|
|
};
|
|
|
|
} // namespace reasampler::vst
|