diff --git a/src/shell/instrument/editor_controls.cpp b/src/shell/instrument/editor_controls.cpp index 7852597..a23d2a7 100644 --- a/src/shell/instrument/editor_controls.cpp +++ b/src/shell/instrument/editor_controls.cpp @@ -1,8 +1,7 @@ -// editor_controls.cpp — the ReaSamplerEditor's PARAMETER PLUMBING (Q-W2v split of -// reasampler_editor.cpp, T4-11): the control-value domain maps (controlValue / -// applyControl — seconds/fraction/frames <-> normalized 0..1), the r11 knob-deck -// group descriptors + control-id<->value binding, the S-VIEW-3 envelope pack/unpack -// (the TRIGGER SEAM converter), the curve-popup target resolution, and applyZoneControl. +// editor_controls.cpp — the ReaSamplerEditor's parameter plumbing: the control-value domain +// maps (controlValue / applyControl — seconds/fraction/frames <-> normalized 0..1), the +// knob-deck group descriptors + control-id<->value binding, the envelope pack/unpack +// (the trigger-seam converter), the curve-popup target resolution, and applyZoneControl. // Value logic only — no painting, no window plumbing. #include "shell/instrument/reasampler_editor.h" @@ -13,8 +12,8 @@ #include #include -#include "core/instrument/engine/master_gain.h" // r11 master-gain dB<->linear<->knob taper (FB1) -#include "core/instrument/map/trigger_seam.h" // triggerPlayLength / fade fraction converters (S-VIEW-3) +#include "core/instrument/engine/master_gain.h" // master-gain dB<->linear<->knob taper +#include "core/instrument/map/trigger_seam.h" // triggerPlayLength / fade fraction converters #include "core/util/clamp01.h" #include "shell/instrument/editor_internal.h" // DeckGroup ids #include "shell/instrument/reasampler_processor.h" @@ -22,35 +21,32 @@ namespace reasampler::vst { using namespace reasampler::instrument::map; // ZonePlaySeconds vocabulary + trigger_seam converters -using instrument::ui::EnvMode; // envelope_overlay's mode enum (Q-W6: shim retired) +using instrument::ui::EnvMode; // envelope_overlay's mode enum using instrument::engine::formatMasterGainLabel; using instrument::engine::masterGainLinearFromNorm; using instrument::engine::masterGainNormFromLinear; using util::clamp01; namespace { -// The S12/S15/S16 control-surface value DOMAINS (the shell owns these — param_slider is -// engine-free and maps only 0..1). WALL-CLOCK time sliders (AHDSR A/H/D/R, pitch env A/D) span -// [0, kEnvTimeMaxSeconds] SECONDS — rate-free, exactly what the zone stores; the keymap build -// resolves seconds->frames at the live rate. SOURCE-timeline fade sliders (Trigger fade-in/out) -// STORE source frames (PLAN.md §S15 — never a wall-clock second; the storage domain is -// settled-correct and unchanged), but the knob's FULL-SCALE THROW is a wall-clock intent — -// kFadeMaxSeconds resolved against the live rate at use (fadeMaxFrames(), Q-W0 T3-03; the -// prior 88200-frame constant baked 2 s x 44.1 kHz into src/, against the no-hardcoded-rate -// ruling). Build-time residual — one place to retune; not persisted. +// Control-surface value domains (the shell owns these — param_slider is engine-free and maps +// only 0..1). Wall-clock time sliders (AHDSR A/H/D/R, pitch env A/D) span [0, kEnvTimeMaxSeconds] +// seconds — rate-free, exactly what the zone stores; the keymap build resolves seconds->frames +// at the live rate. Source-timeline fade sliders (Trigger fade-in/out) store source frames +// (never a wall-clock second), but the knob's full-scale throw is a wall-clock intent — +// kFadeMaxSeconds resolved against the live rate at use (fadeMaxFrames()) rather than a baked-in +// rate constant, per the no-hardcoded-rate ruling. constexpr double kEnvTimeMaxSeconds = 2.0; // AHDSR A/H/D/R + pitch A/D throw ceiling (seconds) constexpr double kFadeMaxSeconds = 2.0; // Trigger fade throw ceiling (wall-clock) constexpr double kPitchDepthMaxSemis = 24.0; // AD pitch depth throw: +/-24 st, centered -constexpr double kKeyTrackMax = 2.0; // S-VIEW-6 key-track slider ceiling (0..200%) +constexpr double kKeyTrackMax = 2.0; // key-track slider ceiling (0..200%) } // namespace double ReaSamplerEditor::controlValue(int id, const ZonePlaySeconds& play) const { // Wall-clock seconds -> normalized over the seconds ceiling; source frames -> normalized over - // the rate-resolved frames ceiling (T3-03). Two domains, kept explicit so neither leaks a rate. - // A stored fade exceeding fadeMaxFrames() at the current host rate reads as norm 1.0 (clamp01 - // pins it) and gets rewritten down on the next knob touch — deliberate, matching the old - // fixed-ceiling clamp behavior in kind, just rate-dependent now instead of fixed at 88200. + // the rate-resolved frames ceiling. Two domains, kept explicit so neither leaks a rate. A + // stored fade exceeding fadeMaxFrames() at the current host rate reads as norm 1.0 (clamp01 + // pins it) and gets rewritten down on the next knob touch. const double fadeMax = fadeMaxFrames(); const auto secToNorm = [](double s) { return clamp01(s / kEnvTimeMaxSeconds); }; const auto framesToNorm = [fadeMax](std::int64_t f) { @@ -80,7 +76,7 @@ double ReaSamplerEditor::controlValue(int id, const ZonePlaySeconds& play) const void ReaSamplerEditor::applyControl(int id, ZonePlaySeconds& play, double value, int segment) const { - const double fadeMax = fadeMaxFrames(); // T3-03: rate-resolved knob full-scale + const double fadeMax = fadeMaxFrames(); // rate-resolved knob full-scale const auto normToSec = [](double v) { return clamp01(v) * kEnvTimeMaxSeconds; }; const auto normToFrames = [fadeMax](double v) -> std::int64_t { // Ceiling unavailable (rate not yet known): inert until fadeMaxFrames() resolves. @@ -122,14 +118,12 @@ double ReaSamplerEditor::liveSampleRate() const { } double ReaSamplerEditor::fadeMaxFrames() const { - // T3-03: the Trigger-fade knob's full-scale throw is kFadeMaxSeconds (2 s wall-clock) - // resolved against the live rate — the SAME time base the envelope overlay already uses - // to place these source-frame fades on screen (totalSeconds = frames / liveSampleRate()), - // and the rate captures are made at (the capture path renders at the project rate). - // Pre-setupProcessing the rate is still 0: rather than substitute a literal rate (the - // exact residue T3-03 removed), bail the same way paintEnvelopeOverlay does (~line 1396) — - // callers treat a <= 0 return as "ceiling unavailable yet" and degrade the knob to inert - // rather than guess a rate. Storage stays SOURCE FRAMES — this resolves the UI ceiling only. + // The Trigger-fade knob's full-scale throw is kFadeMaxSeconds (2 s wall-clock) resolved + // against the live rate — the same time base the envelope overlay already uses to place + // these source-frame fades on screen. Pre-setupProcessing the rate is still 0: rather than + // substitute a literal rate, callers treat a <= 0 return as "ceiling unavailable yet" and + // degrade the knob to inert rather than guess a rate. Storage stays source frames — this + // resolves the UI ceiling only. const double rate = liveSampleRate(); if (rate <= 0.0) return 0.0; return kFadeMaxSeconds * rate; @@ -141,11 +135,11 @@ double ReaSamplerEditor::previewVelocity01() const { } std::vector ReaSamplerEditor::zoneDeckGroupDescs(const ZonePlaySeconds& play) const { - // The PER-ZONE groups — the deck grammar both surfaces share (FB2: the Zone panel renders - // exactly these; the Sample face appends the per-instance groups in deckGroupDescs). - // Group widths are MODE-INDEPENDENT: AMP ENVELOPE reserves its 5-cell Gate width (Trigger - // leaves two blank cells), so a Gate<->Trigger flip repopulates in place and never reflows - // the neighbouring groups (r11). + // The per-zone groups — the deck grammar both surfaces share (the Zone panel renders + // exactly these; the Sample face appends the per-instance groups in deckGroupDescs). Group + // widths are mode-independent: AMP ENVELOPE reserves its 5-cell Gate width (Trigger leaves + // two blank cells), so a Gate<->Trigger flip repopulates in place and never reflows the + // neighbouring groups. std::vector out; { DeckGroupDesc amp; @@ -159,8 +153,8 @@ std::vector ReaSamplerEditor::zoneDeckGroupDescs(const ZonePlaySe static_cast(ParamControl::kSustain), static_cast(ParamControl::kRelease)}; } else { - // Trigger, TIME-ORDERED left-to-right (r11: Fade In · Length % · Fade Out — - // matches the drawn envelope), plus the two reserved blanks. + // Trigger, time-ordered left-to-right (Fade In / Length % / Fade Out — matches + // the drawn envelope), plus the two reserved blanks. amp.cellIds = {static_cast(ParamControl::kTrigFadeIn), static_cast(ParamControl::kTrigLength), static_cast(ParamControl::kTrigFadeOut), -1, -1}; @@ -190,9 +184,8 @@ std::vector ReaSamplerEditor::zoneDeckGroupDescs(const ZonePlaySe std::vector ReaSamplerEditor::deckGroupDescs(const ZonePlaySeconds& play) const { // The full Sample-face deck: the shared per-zone groups + the per-instance VOICE + MASTER - // groups. VOICE + MASTER are the FB1 homes for the provisional voice-deck controls and the - // post-mixer gain — the r11 spec predates both; per-instance state (ComponentState) stays - // OFF the Zone panel (FB2), so they are appended here, not in zoneDeckGroupDescs. + // groups. Per-instance state (ComponentState) stays off the Zone panel, so they are + // appended here, not in zoneDeckGroupDescs. std::vector out = zoneDeckGroupDescs(play); { DeckGroupDesc voice; @@ -303,8 +296,8 @@ std::string ReaSamplerEditor::deckValueLabel(int id, const PerformanceZone& zone EnvClampBounds ReaSamplerEditor::envClampBounds() const { // Match the control-panel sliders' own domains so a node drag can never produce a param a - // slider couldn't (the S-VIEW-F2 invariant). AHDSR seconds cap at kEnvTimeMaxSeconds; the - // Trigger fade/length fractions cap at 1.0 (the natural full-span bound the sliders use). + // slider couldn't. AHDSR seconds cap at kEnvTimeMaxSeconds; the Trigger fade/length + // fractions cap at 1.0 (the natural full-span bound the sliders use). EnvClampBounds b; b.maxAttackSeconds = kEnvTimeMaxSeconds; b.maxHoldSeconds = kEnvTimeMaxSeconds; @@ -326,8 +319,8 @@ AmpEnvelope ReaSamplerEditor::packEnvelope(const ZonePlaySeconds& play, std::int env.decaySeconds = play.adsr.decaySeconds; env.sustainLevel = play.adsr.sustainLevel; env.releaseSeconds = play.adsr.releaseSeconds; - // Trigger: lengthFraction copies 1-to-1; the fades are DERIVED — source frames over the played - // span (the TRIGGER SEAM converter, PACK direction). startFrame is the zone's effective start + // Trigger: lengthFraction copies 1-to-1; the fades are derived — source frames over the played + // span (the trigger-seam converter, pack direction). startFrame is the zone's effective start // point so the fraction denominator matches the voice's actual post-start span. A zero play // length yields 0 fractions. env.lengthFraction = play.trigger.lengthFraction; @@ -348,10 +341,10 @@ void ReaSamplerEditor::unpackEnvelope(const AmpEnvelope& env, std::int64_t frame play.adsr.releaseSeconds = env.releaseSeconds; } else { // Trigger: lengthFraction copies back; the fades convert fractions -> source frames over - // the played span (the TRIGGER SEAM converter, UNPACK direction). startFrame is the zone's - // effective start point so the frame denominator matches the voice's actual post-start span. - // Keep the same (0,1] floor on lengthFraction the slider path enforces so a zero-length - // trigger never plays nothing. + // the played span (the trigger-seam converter, unpack direction). startFrame is the + // zone's effective start point so the frame denominator matches the voice's actual + // post-start span. Keep the same (0,1] floor on lengthFraction the slider path enforces + // so a zero-length trigger never plays nothing. play.trigger.lengthFraction = (std::max)(0.01, env.lengthFraction); const std::int64_t playLen = triggerPlayLength(play.trigger.lengthFraction, frames, startFrame); @@ -361,8 +354,8 @@ void ReaSamplerEditor::unpackEnvelope(const AmpEnvelope& env, std::int64_t frame } PerformanceZone ReaSamplerEditor::popupZone() const { - // The zone the popup displays: the Zone surface's SELECTED zone (FB2), else the Sample - // face's one-zone site (a read-only resolve — an edit materializes via popupZoneIndex). + // The zone the popup displays: the Zone surface's selected zone, else the Sample face's + // one-zone site (a read-only resolve — an edit materializes via popupZoneIndex). if (view_ == View::kZone && selectedZone_ >= 0 && selectedZone_ < static_cast(map_.zones.size())) { return map_.zones[static_cast(selectedZone_)]; diff --git a/src/shell/instrument/editor_input_browse_zone.cpp b/src/shell/instrument/editor_input_browse_zone.cpp index 1587c23..7ee41cd 100644 --- a/src/shell/instrument/editor_input_browse_zone.cpp +++ b/src/shell/instrument/editor_input_browse_zone.cpp @@ -1,10 +1,9 @@ -// editor_input_browse_zone.cpp — the ReaSamplerEditor's BROWSE-MODAL and ZONE-SURFACE -// input + the hover resolver (Q-W2v split of reasampler_editor.cpp, T4-11): the L3 hover -// resolution across all three faces, the Browse picker's click branch (tabs, cards, -// select-then-confirm, scroll-thumb grab, search focus), the Zone surface's click branch -// (add/delete, strip drags, numeric-entry focus, per-zone deck + curve button), the -// browser wheel scroll, the type-to-filter / note-entry keystrokes, and the S13 degraded -// drop affordance. Windows-only (D5). +// editor_input_browse_zone.cpp — the ReaSamplerEditor's browse-modal and zone-surface +// input + the hover resolver: hover resolution across all three faces, the Browse picker's +// click branch (tabs, cards, select-then-confirm, scroll-thumb grab, search focus), the +// Zone surface's click branch (add/delete, strip drags, numeric-entry focus, per-zone deck +// + curve button), the browser wheel scroll, the type-to-filter / note-entry keystrokes, +// and the degraded drop affordance. Windows-only. #include "shell/instrument/reasampler_editor.h" @@ -15,10 +14,10 @@ #include #include -#include "core/instrument/ui/browser_scroll.h" // BrowseModal + scroll/search geometry (S12) +#include "core/instrument/ui/browser_scroll.h" // BrowseModal + scroll/search geometry #include "core/instrument/ui/curve_popup.h" // computeCurvePopup (popup hover) #include "core/instrument/ui/knob_deck.h" // hitTestDeck / kDeckKnobSize -#include "core/instrument/map/note_entry.h" // parseNoteEntry (S12 numeric entry) +#include "core/instrument/map/note_entry.h" // parseNoteEntry (numeric entry) #include "shell/instrument/editor_internal.h" // curveBoxFromRect (popup node hover) #include "shell/instrument/reasampler_processor.h" @@ -28,8 +27,6 @@ using namespace reasampler::ui; using namespace reasampler::instrument::ui; using namespace reasampler::instrument::map; -// --- Hover resolution (Phase L, L3) ------------------------------------------ -// // Resolve the interactive element under (x, y) into hover_ and repaint only on change (an // idle move is free). Mirrors onMouseDown's hit-test order, but read-only. Windows-only. void ReaSamplerEditor::resolveHover(int x, int y) { @@ -57,7 +54,7 @@ void ReaSamplerEditor::resolveHover(int x, int y) { if (tab >= 0) h = {HoverKind::kFilterTab, tab}; else if (card >= 0) h = {HoverKind::kCard, card}; } - } else if (curvePopupOpen_) { // the r11 curve popup — modal over Sample AND Zone (FB2) + } else if (curvePopupOpen_) { // the curve popup — modal over Sample and Zone const CurvePopupLayout pl = computeCurvePopup(w, hgt); if (contains(pl.close, x, y)) { h = {HoverKind::kPopupClose, -1}; @@ -79,8 +76,8 @@ void ReaSamplerEditor::resolveHover(int x, int y) { } else if (selectedZone_ >= 0 && contains(delR, x, y)) { h = {HoverKind::kDeleteZone, -1}; } else if (selectedZone_ >= 0 && selectedZone_ < static_cast(map_.zones.size())) { - // FB2: the per-zone knob deck + the mini curve-preview button (the Sample deck's - // hover grammar — knobs light + swap label->value). + // The per-zone knob deck + the mini curve-preview button (the Sample deck's hover + // grammar — knobs light + swap label->value). if (contains(zonesCurveButton(content), x, y)) { h = {HoverKind::kCurveButton, -1}; } else { @@ -93,7 +90,7 @@ void ReaSamplerEditor::resolveHover(int x, int y) { if (dh.kind != DeckHitKind::None) h = {HoverKind::kControl, dh.id}; } } - } else { // Sample view (home, r11 recomposition) + } else { // Sample view (home) const PerformanceZone zone = effectiveSampleZone(); const std::vector descs = deckGroupDescs(zone.play); const SampleBands bands = @@ -128,8 +125,8 @@ void ReaSamplerEditor::resolveHover(int x, int y) { } } -// The Browse-modal branch of the mouse-down dispatch (formerly inline in onMouseDown — -// behavior-identical; see editor_input_sample.cpp for the dispatch). +// The Browse-modal branch of the mouse-down dispatch (see editor_input_sample.cpp for the +// dispatch). void ReaSamplerEditor::mouseDownBrowse(int w, int h, int x, int y) { const BrowseModal bm = computeBrowseModal(w, h); if (contains(bm.back, x, y) || contains(bm.cancel, x, y)) { @@ -198,8 +195,8 @@ void ReaSamplerEditor::mouseDownBrowse(int w, int h, int x, int y) { return; } -// The Zone-surface branch of the mouse-down dispatch (formerly the tail of onMouseDown — -// behavior-identical; the curve popup is modal over the Zone surface too, FB2). +// The Zone-surface branch of the mouse-down dispatch (the curve popup is modal over the +// Zone surface too). void ReaSamplerEditor::mouseDownZone(int w, int h, int x, int y) { if (handlePopupMouseDown(w, h, x, y)) return; const Rect back = zoneBackRect(w, h); @@ -209,11 +206,11 @@ void ReaSamplerEditor::mouseDownZone(int w, int h, int x, int y) { if (contains(addR, x, y)) { // Add a narrow default zone for the picked capture (or the first visible sample as a // sensible seed). No pick -> nothing to add. If a full-keyboard zone for the seed id - // already exists (pre-fix bleed survivor), select it rather than appending a duplicate - // (mirrors the upsert the root-marker drag path already performs). - // NARROW DEFAULT: seed [root-6, root+5] (one octave centred on the bank root, clamped - // to [0,127]) so the new zone is immediately "authored" (narrow) and survives - // reconcileSingleCaptureZones without being treated as a Sample-face full-range zone. + // already exists, select it rather than appending a duplicate (mirrors the upsert the + // root-marker drag path already performs). Narrow default: seed [root-6, root+5] (one + // octave centred on the bank root, clamped to [0,127]) so the new zone is immediately + // "authored" (narrow) and survives reconcileSingleCaptureZones without being treated + // as a Sample-face full-range zone. std::string seed = !selectedId_.empty() ? selectedId_ : (!visible_.empty() ? visible_.front().id : std::string()); if (seed.empty()) return; @@ -290,7 +287,7 @@ void ReaSamplerEditor::mouseDownZone(int w, int h, int x, int y) { return; } - // S12 numeric-entry fields (low/high/root): a click focuses the field for typing. Only when a + // Numeric-entry fields (low/high/root): a click focuses the field for typing. Only when a // zone is selected. entryText_ starts empty (the user types the full value). if (selectedZone_ >= 0 && selectedZone_ < static_cast(map_.zones.size())) { const Rect fields = noteEntryFieldsArea(content); @@ -305,9 +302,9 @@ void ReaSamplerEditor::mouseDownZone(int w, int h, int x, int y) { } entryField_ = -1; // a click elsewhere in the Zone view cancels an in-progress entry - // The per-zone param surface (FB2): the knob deck + the mini curve-preview button — the - // SAME grammar and hit-test machinery as the Sample face. Only when a zone is selected - // (the Zone surface has no single-capture fallback — that lives on the Sample face). + // The per-zone param surface: the knob deck + the mini curve-preview button — the same + // grammar and hit-test machinery as the Sample face. Only when a zone is selected (the + // Zone surface has no single-capture fallback — that lives on the Sample face). if (selectedZone_ >= 0 && selectedZone_ < static_cast(map_.zones.size())) { if (contains(zonesCurveButton(content), x, y)) { curvePopupOpen_ = true; @@ -335,7 +332,7 @@ void ReaSamplerEditor::mouseDownZone(int w, int h, int x, int y) { hit.id == static_cast(ParamControl::kPitchEnvDecay) || hit.id == static_cast(ParamControl::kPitchEnvDepth); if (pitchEnvKnob && !play.pitchEnv.enabled) return; - // GRAB-ANCHORED vertical drag (FA4): live-drag the map, commit on release. + // Grab-anchored vertical drag: live-drag the map, commit on release. drag_ = DragKind::kDeckKnob; dragParamId_ = hit.id; dragParamZone_ = selectedZone_; @@ -351,8 +348,8 @@ void ReaSamplerEditor::mouseDownZone(int w, int h, int x, int y) { void ReaSamplerEditor::onMouseWheel(int delta) { // Browser scroll (only in the Browse modal — the sole card grid). One wheel notch - // (WHEEL_DELTA==120) scrolls roughly one card row; the offset is clamped at paint. A positive - // delta (wheel up) scrolls toward the top (smaller offset). + // (WHEEL_DELTA==120) scrolls roughly one card row; the offset is clamped at paint. A + // positive delta (wheel up) scrolls toward the top (smaller offset). if (view_ != View::kBrowse) return; const int rows = delta / 120; if (rows == 0) return; @@ -362,8 +359,8 @@ void ReaSamplerEditor::onMouseWheel(int delta) { } void ReaSamplerEditor::onSearchChar(unsigned int ch) { - // r11 curve popup: Esc dismisses (checked first — the popup is modal over the Sample face - // or the Zone surface, FB2; opening it clears any note-entry focus, and the Browse search + // The curve popup: Esc dismisses (checked first — the popup is modal over the Sample face + // or the Zone surface; opening it clears any note-entry focus, and the Browse search // cannot hold focus under it). if (curvePopupOpen_ && ch == 27) { curvePopupOpen_ = false; @@ -371,7 +368,7 @@ void ReaSamplerEditor::onSearchChar(unsigned int ch) { return; } - // S12 numeric note-entry (Zone surface): a focused low/high/root field accumulates keystrokes + // Numeric note-entry (Zone surface): a focused low/high/root field accumulates keystrokes // and commits via parseNoteEntry on Enter. Handled before the search box (a field, when // focused, owns the keystrokes). if (view_ == View::kZone && entryField_ >= 0) { @@ -402,8 +399,9 @@ void ReaSamplerEditor::onSearchChar(unsigned int ch) { return; } - // S12 type-to-filter search. Only when the search box has focus (a click focuses it). Backspace - // deletes; a printable ASCII char appends; the visible list recomposes (bank filter, then search). + // Type-to-filter search. Only when the search box has focus (a click focuses it). Backspace + // deletes; a printable ASCII char appends; the visible list recomposes (bank filter, then + // search). if (view_ != View::kBrowse || !searchFocused_) return; if (ch == 8) { // backspace if (!searchQuery_.empty()) searchQuery_.pop_back(); @@ -421,12 +419,12 @@ void ReaSamplerEditor::onSearchChar(unsigned int ch) { } void ReaSamplerEditor::onFilesDropped(int droppedCount) { - // S13 relay DEGRADED. The instrument is a read-only bank consumer and the cross-artifact - // ingest relay (editor drop -> extension) is not shipped (see the header note + the handoff - // decision point), so we do NOT ingest the dropped files and — load-bearing — NEVER insert a - // timeline item. Instead of silently swallowing the drop, flash a clear affordance pointing - // at the shipped ingest gesture. dropHintTicks_ counts sync ticks (kSyncTimerIntervalMs - // each); ~6 ticks keeps the banner up a few seconds, then onSyncTimer decays it to 0. + // The instrument is a read-only bank consumer and the cross-artifact ingest relay (editor + // drop -> extension) is not shipped, so we do not ingest the dropped files and — load- + // bearing — never insert a timeline item. Instead of silently swallowing the drop, flash a + // clear affordance pointing at the shipped ingest gesture. dropHintTicks_ counts sync ticks + // (kSyncTimerIntervalMs each); ~6 ticks keeps the banner up a few seconds, then onSyncTimer + // decays it to 0. (void)droppedCount; // count is informational; the banner text is drop-count-agnostic dropHintTicks_ = 6; #ifdef _WIN32 diff --git a/src/shell/instrument/editor_input_sample.cpp b/src/shell/instrument/editor_input_sample.cpp index b236d76..a973372 100644 --- a/src/shell/instrument/editor_input_sample.cpp +++ b/src/shell/instrument/editor_input_sample.cpp @@ -1,10 +1,10 @@ -// editor_input_sample.cpp — the ReaSamplerEditor's SAMPLE-FACE input + the drag-state -// machine (Q-W2v split of reasampler_editor.cpp, T4-11): the mouse-down dispatch (the -// Sample-face branch inline; Browse/Zone branches delegate to editor_input_browse_zone), -// the curve-popup/curve-box click machinery, the live drag resolution (onMouseMove — deck -// knobs, root marker, envelope nodes, curve nodes, wave markers, scroll thumb, zone -// edges), the release commit (onMouseUp), and the popup right-click delete. Windows-only -// (D5). All hit-test math is pure; this TU routes and mutates editor state only. +// editor_input_sample.cpp — the ReaSamplerEditor's sample-face input + the drag-state +// machine: the mouse-down dispatch (the Sample-face branch inline; Browse/Zone branches +// delegate to editor_input_browse_zone), the curve-popup/curve-box click machinery, the +// live drag resolution (onMouseMove — deck knobs, root marker, envelope nodes, curve +// nodes, wave markers, scroll thumb, zone edges), the release commit (onMouseUp), and the +// popup right-click delete. Windows-only. All hit-test math is pure; this TU routes and +// mutates editor state only. #include "shell/instrument/reasampler_editor.h" @@ -16,11 +16,11 @@ #include #include "core/instrument/ui/browser_scroll.h" // BrowseModal + thumbDragToOffset (scroll drag) -#include "core/instrument/ui/curve_popup.h" // computeCurvePopup / popupOutsideSheet (r11) -#include "core/instrument/ui/envelope_edit.h" // nodeAtPoint / resolveNodeDrag (S-VIEW-3) +#include "core/instrument/ui/curve_popup.h" // computeCurvePopup / popupOutsideSheet +#include "core/instrument/ui/envelope_edit.h" // nodeAtPoint / resolveNodeDrag #include "core/instrument/ui/knob_deck.h" // hitTestDeck / kDeckKnobSize -#include "core/instrument/ui/param_slider.h" // knobDragValue (FA4 grab-anchored drag) -#include "core/instrument/ui/waveform_view.h" // markerAtPoint / resolveDragFrame / snap (S11) +#include "core/instrument/ui/param_slider.h" // knobDragValue (grab-anchored drag) +#include "core/instrument/ui/waveform_view.h" // markerAtPoint / resolveDragFrame / snap #include "shell/instrument/editor_internal.h" // curveBoxFromRect + kCurveDragOffMargin #include "shell/instrument/reasampler_processor.h" @@ -31,11 +31,10 @@ using namespace reasampler::instrument::ui; using namespace reasampler::instrument::map; bool ReaSamplerEditor::handlePopupMouseDown(int w, int h, int x, int y) { - // The r11 curve popup: while open the sheet is MODAL over its host face — the Sample home - // (FB1) or the Zone surface (FB2) — it owns every left-click. Close click / outside-wash - // click dismiss (outside only when no drag is in flight, per the spec); in-box clicks - // route to the shared curve machinery against popupZoneIndex(); anything else on the - // sheet is swallowed. + // The curve popup: while open the sheet is modal over its host face — the Sample home or + // the Zone surface — it owns every left-click. Close click / outside-wash click dismiss + // (outside only when no drag is in flight); in-box clicks route to the shared curve + // machinery against popupZoneIndex(); anything else on the sheet is swallowed. if (!curvePopupOpen_) return false; const CurvePopupLayout pl = computeCurvePopup(w, h); if (contains(pl.close, x, y)) { @@ -77,13 +76,10 @@ void ReaSamplerEditor::handleCurveMouseDown(const Rect& r, int zoneIndex, int x, // ADD (mirror of the other map-editing drags' dragStartMap_ contract). dragStartMap_ = map_; - // Empty-space click inside the MAPPING BOX: add a control point at the cursor via the pure - // inverse map, then grab it — the click flows straight into a placing drag. Guard: the caller - // gates on contains(r, x, y) (the full border rect), but the 6+px inset ring — including the - // caption band — must not add a point; a click there would clamp to velocity 0/127 and - // produce an undeletable duplicate stacked on an endpoint. Clicks in the ring may still grab - // an existing node (pointAtPixel's pick radius legitimately extends into the ring), which is - // handled above; only the add path is box-gated here. + // Empty-space click inside the mapping box: add a control point via the pure inverse map, + // then grab it. Box-gated (not just contains(r,x,y)) because the inset ring must not add a + // point — it would clamp to velocity 0/127, stacking an undeletable duplicate on an endpoint. + // A ring click can still grab an existing node (handled above); only add is box-gated. if (idx < 0) { const bool inBox = (x >= box.left && x < box.left + box.width && y >= box.top && y < box.top + box.height); @@ -115,15 +111,15 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { const int w = cr.right - cr.left; const int h = cr.bottom - cr.top; - // ---- Browse modal (S-VIEW-5): the face branch lives in editor_input_browse_zone ---- + // Browse modal: the face branch lives in editor_input_browse_zone. if (view_ == View::kBrowse) { mouseDownBrowse(w, h, x, y); return; } - // ---- Sample home (S-VIEW-2 / r11) ---- + // Sample home. if (view_ == View::kSample) { - // r11 curve popup: while open the sheet is modal — it owns every left-click. + // The curve popup: while open the sheet is modal — it owns every left-click. if (handlePopupMouseDown(w, h, x, y)) return; const PerformanceZone probeZone = effectiveSampleZone(); @@ -155,8 +151,8 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { invalidate(); return; } - // Radial preview-velocity knob (r11): GRAB-ANCHORED vertical drag — the grab itself - // never jumps the value (FA4); the delta from the grab point maps via knobDragValue. + // Radial preview-velocity knob: grab-anchored vertical drag — the grab itself never + // jumps the value; the delta from the grab point maps via knobDragValue. if (contains(cr.velCell, x, y)) { drag_ = DragKind::kDeckKnob; dragParamId_ = -2; // sentinel: the preview velocity knob (a processor param) @@ -187,9 +183,9 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { return; } - // The knob deck (r11): toggles commit at once (a discrete, final edit — the slider - // precedent); knobs start a grab-anchored vertical drag. The deck band swallows its - // clicks (no fall-through to the hero/markers). + // The knob deck: toggles commit at once (a discrete, final edit); knobs start a + // grab-anchored vertical drag. The deck band swallows its clicks (no fall-through to + // the hero/markers). if (contains(bands.deck, x, y)) { const DeckLayout dl = layoutDeck(deckDescs, bands.deck.x, bands.deck.y, bands.deck.width); @@ -266,7 +262,7 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { return; } - // Hero waveform: envelope nodes (S-VIEW-3) first, then the S11 markers. + // Hero waveform: envelope nodes first, then the wave markers. const std::vector& pcm = monoPcmFor(selectedId_); const std::int64_t frames = static_cast(pcm.size()); const Rect waveArea = bands.hero; @@ -304,7 +300,7 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { } } - // Fenced root strip: grab the root marker (remainder-width since r11). + // Fenced root strip: grab the root marker (remainder-width). if (cr.rootStrip.width > 0) { const StripLayout sl = layoutStrip(cr.rootStrip.width, cr.rootStrip.height); const int note = keyAtPoint(sl, x - cr.rootStrip.x, y - cr.rootStrip.y); @@ -320,7 +316,7 @@ void ReaSamplerEditor::onMouseDown(int x, int y) { return; } - // ---- Zone surface (S-VIEW-8 / FB2): the face branch lives in editor_input_browse_zone ---- + // Zone surface: the face branch lives in editor_input_browse_zone. mouseDownZone(w, h, x, y); } @@ -335,24 +331,24 @@ void ReaSamplerEditor::onMouseMove(int x, int y) { const int dx = x - dragStartX_; if (drag_ == DragKind::kDeckKnob) { - // r11 radial knob: GRAB-ANCHORED vertical drag — knobDragValue maps the y delta from - // the value at grab (up = increase), so the value tracks relative motion and never - // jumps on grab (FA4). Live feedback; zone-param commits land on WM_LBUTTONUP. + // Radial knob: grab-anchored vertical drag — knobDragValue maps the y delta from the + // value at grab (up = increase), so the value tracks relative motion and never jumps + // on grab. Live feedback; zone-param commits land on WM_LBUTTONUP. const int dy = y - dragStartY_; applyDeckKnob(dragParamZone_, dragParamId_, knobDragValue(dragKnobStartValue_, dy)); invalidate(); return; } - // r11: the Sample bands derive from the deck height (mode-independent width math). Hoisted + // The Sample bands derive from the deck height (mode-independent width math). Hoisted // below the kDeckKnob early-return — that branch uses neither deckDescs nor bands. const std::vector deckDescs = deckGroupDescs(effectiveSampleZone().play); const SampleBands bands = computeSampleBands(w, h, deckHeight(deckDescs, w - 2 * kPad)); if (drag_ == DragKind::kRootMarker) { // The fenced root strip on the Sample cluster band. Setting the root materializes a - // full-keyboard zone carrying the override on the picked id (the D-B override vehicle) — - // upsert by id so a repeated drag edits the same zone rather than stacking duplicates. + // full-keyboard zone carrying the override on the picked id — upsert by id so a + // repeated drag edits the same zone rather than stacking duplicates. const ChannelToggleRects chan = channelToggleRects(bands.cluster); const Rect stripArea = clusterRects(bands.cluster, chan.mono, kDeckKnobSize).rootStrip; const StripLayout sl = layoutStrip(stripArea.width, stripArea.height); @@ -381,10 +377,11 @@ void ReaSamplerEditor::onMouseMove(int x, int y) { } if (drag_ == DragKind::kEnvNode) { - // S-VIEW-3: resolve the grabbed envelope node's new params from the pixel delta (through - // the pure envelope_edit inverse map, clamped + monotonic), then unpack them back onto the - // picked id's one-zone play params. The AmpEnvelope was snapshotted at grab (dragStartEnv_) - // so the delta is absolute. Materialize the zone if needed (mirror of the marker path). + // Resolve the grabbed envelope node's new params from the pixel delta (through the + // pure envelope_edit inverse map, clamped + monotonic), then unpack them back onto the + // picked id's one-zone play params. The AmpEnvelope was snapshotted at grab + // (dragStartEnv_) so the delta is absolute. Materialize the zone if needed (mirror of + // the marker path). const std::int64_t frames = dragSampleFrames_; const double rate = liveSampleRate(); if (frames <= 0 || rate <= 0.0) return; @@ -403,9 +400,9 @@ void ReaSamplerEditor::onMouseMove(int x, int y) { } if (drag_ == DragKind::kCurveNode) { - // S-VIEW-10: resolve the grabbed control point from the pixel delta through the pure - // inverse map (box + neighbour-X + endpoint-pin clamps), against the grab-time curve + - // box (absolute delta — the mirror of the envelope-node drag). Live feedback only; the + // Resolve the grabbed control point from the pixel delta through the pure inverse map + // (box + neighbour-X + endpoint-pin clamps), against the grab-time curve + box + // (absolute delta — the mirror of the envelope-node drag). Live feedback only; the // commit lands on WM_LBUTTONUP. if (dragCurveZone_ < 0 || dragCurveZone_ >= static_cast(map_.zones.size())) return; if (curvePointIndex_ < 0) return; @@ -419,8 +416,8 @@ void ReaSamplerEditor::onMouseMove(int x, int y) { } if (drag_ == DragKind::kWaveMarker) { - // S11: resolve the grabbed marker's new frame from the pixel delta, zero-crossing-snap - // it against the decoded PCM, apply the inter-marker clamps, and write the override live. + // Resolve the grabbed marker's new frame from the pixel delta, zero-crossing-snap it + // against the decoded PCM, apply the inter-marker clamps, and write the override live. const Rect waveArea = bands.hero; const std::int64_t frames = dragSampleFrames_; if (frames <= 0) return; @@ -431,8 +428,8 @@ void ReaSamplerEditor::onMouseMove(int x, int y) { dragStartMarkers_.loopEnd}; std::int64_t newFrame = resolveDragFrame(waveArea, frames, startVals[idx], dx); - // Snap to the nearest zero crossing in the decoded PCM (the S2 zero-crossing-aware - // requirement). Pure over the cached mono frames — no host types, no file I/O. + // Snap to the nearest zero crossing in the decoded PCM. Pure over the cached mono + // frames — no host types, no file I/O. const std::vector& pcm = monoPcmFor(selectedId_); if (!pcm.empty()) { newFrame = nearestZeroCrossing(pcm.data(), static_cast(pcm.size()), @@ -464,8 +461,8 @@ void ReaSamplerEditor::onMouseMove(int x, int y) { } if (drag_ == DragKind::kScrollThumb) { - // S12: map the thumb-drag pixel delta to a new (clamped) scroll offset. The scroll drag - // only happens in the Browse modal (the sole card grid). The visible-card window recomputes + // Map the thumb-drag pixel delta to a new (clamped) scroll offset. The scroll drag only + // happens in the Browse modal (the sole card grid). The visible-card window recomputes // at paint from scrollOffset_. const int dyThumb = y - dragStartY_; const BrowseModal bm = computeBrowseModal(w, h); @@ -535,9 +532,9 @@ void ReaSamplerEditor::onMouseUp(int x, int y) { invalidate(); return; } - // S-VIEW-10 drag-off delete: releasing a curve-node drag well OUTSIDE the box removes the - // dragged point (deletePoint refuses the two endpoints, so an endpoint drag-off is a plain - // move — its amp keeps the last clamped drag value). + // Drag-off delete: releasing a curve-node drag well outside the box removes the dragged + // point (deletePoint refuses the two endpoints, so an endpoint drag-off is a plain move — + // its amp keeps the last clamped drag value). if (kind == DragKind::kCurveNode && curveIdx >= 0 && curveZone >= 0 && curveZone < static_cast(map_.zones.size())) { const bool off = x < curveRect.x - kCurveDragOffMargin || @@ -554,12 +551,12 @@ void ReaSamplerEditor::onMouseUp(int x, int y) { } void ReaSamplerEditor::onMouseRDown(int x, int y) { - // r11 (issue 3c): right-click on a popup curve node deletes it — the PRIMARY delete - // affordance; Alt-click and drag-off remain as landed alternates. Commits immediately - // through the same path as Alt-click; deletePoint's endpoint guard makes an endpoint - // right-click a safe no-op. Right-clicks act ONLY while the popup is open — over the - // Sample face OR the Zone surface (FB2; nothing else in the editor consumes them) — - // and never during an in-flight left drag. + // Right-click on a popup curve node deletes it — the primary delete affordance; Alt-click + // and drag-off remain as landed alternates. Commits immediately through the same path as + // Alt-click; deletePoint's endpoint guard makes an endpoint right-click a safe no-op. + // Right-clicks act only while the popup is open — over the Sample face or the Zone + // surface (nothing else in the editor consumes them) — and never during an in-flight left + // drag. if (!processor_ || view_ == View::kBrowse || !curvePopupOpen_) return; if (drag_ != DragKind::kNone) return; RECT rc{}; diff --git a/src/shell/instrument/editor_internal.h b/src/shell/instrument/editor_internal.h index 438eadf..c3ea21d 100644 --- a/src/shell/instrument/editor_internal.h +++ b/src/shell/instrument/editor_internal.h @@ -1,11 +1,8 @@ -// editor_internal.h — INTERNAL shared helpers for the ReaSamplerEditor TU family -// (Q-W2v: the eight face-axis TUs split out of the former reasampler_editor.cpp). -// Included ONLY by the editor's own shell TUs (editor_session / editor_controls / -// editor_paint_* / editor_input_* / editor_platform) — never a public seam. Holds the -// former god-TU's anonymous-namespace helpers that more than one split TU needs: the -// Rect<->kit adapters, the small draw primitives (knob face / spectral strip / root -// marker / title band), the label helpers, the deck group ids, and the velocity-curve -// box derivation. All inline; behavior-identical to the pre-split definitions. +// editor_internal.h — shared helpers for the ReaSamplerEditor TU family. Included ONLY by +// the editor's own shell TUs (editor_session / editor_controls / editor_paint_* / +// editor_input_* / editor_platform) — never a public seam. Holds the Rect<->kit adapters, +// small draw primitives (knob face / spectral strip / root marker / title band), label +// helpers, deck group ids, and the velocity-curve box derivation. All inline. #pragma once @@ -24,7 +21,7 @@ #include "core/audio/peaks.h" // Envelope (drawEnvelope) #include "core/instrument/ui/capture_browser.h" // BrowserLayout / cardThumbnailRect (thumbBins) -#include "core/instrument/ui/param_slider.h" // KnobGeometry / KnobArc (drawKnobFace, FA4) +#include "core/instrument/ui/param_slider.h" // KnobGeometry / KnobArc (drawKnobFace) #include "core/instrument/ui/keyboard_strip.h" // StripLayout / keyRect / isNaturalKey (spectral strip) #include "core/ui/component_geometry.h" // KitBox / waveformColumnCount #include "core/ui/theme.h" // Role / InteractionState / KitColor / spectralColor @@ -33,8 +30,7 @@ namespace reasampler::vst { -// The deck group ids (shell-owned; knob_deck treats them opaquely). Left-to-right deck -// order. Shared by the deck-desc builders (editor_controls) and the deck painter. +// Deck group ids (shell-owned; knob_deck treats them opaquely), left-to-right order. enum DeckGroup { kGroupAmpEnv = 0, kGroupPitch, @@ -43,17 +39,14 @@ enum DeckGroup { kGroupMaster, }; -// The S-VIEW-10 velocity-curve editor box metrics. Since r11/FB2 BOTH surfaces host the -// curve in the POPUP (curve_popup), each summoned from its own mini preview button. The -// INSET keeps node handles + the pick radius inside the border so an endpoint at amp 0/1 -// stays grabbable — the ONE curveBoxFromRect grammar the popup derives its mapping box -// through. Drag-off: release beyond box+margin deletes the dragged node. +// Velocity-curve editor box metrics. The inset keeps node handles + the pick radius +// inside the border so an endpoint at amp 0/1 stays grabbable; drag-off beyond +// box+margin deletes the dragged node. inline constexpr int kVelCurveInset = 14; inline constexpr int kCurveDragOffMargin = 24; -// The pure-module mapping Box for a drawn curve rect: inset from the border so node -// handles and the pick radius stay inside the box. Every consumer (paint, hit-test, add, -// drag) derives the Box through this ONE formula, so drawn nodes and grabs never drift. +// The pure-module mapping Box for a drawn curve rect. Every consumer (paint, hit-test, +// add, drag) derives it through this ONE formula, so drawn nodes and grabs never drift. inline instrument::engine::VelocityCurve::Box curveBoxFromRect( const instrument::ui::Rect& r) { return instrument::engine::VelocityCurve::Box{ @@ -74,8 +67,8 @@ inline std::string noteLabel(int note) { } // A display name for a bank sample id: the snapshotted bank list first, then the -// instance-OWNED ref's displayName (pS — the label survives with the extension absent / -// bank unreadable). "?" only when neither source knows the id. +// instance-owned ref's displayName (survives with the extension absent). "?" if neither +// source knows the id. inline std::string sampleLabel(const std::vector& samples, const instrument::map::SampleRefs& refs, const std::string& id) { @@ -90,11 +83,9 @@ inline std::string sampleLabel(const std::vector& #ifdef _WIN32 -// --- Rect <-> kit adapters (Phase L, L3) ------------------------------------- -// // The editor's own sub-rect type is `Rect` (editor_geometry); the kit draws against // `KitBox` (component_geometry). This is the single boundary that bridges them so every -// draw routes through the L1 kit (theme roles + draw_kit). +// draw routes through the shared kit (theme roles + draw_kit). inline ui::KitBox toKitBox(const instrument::ui::Rect& r) { return ui::KitBox{r.x, r.y, r.width, r.height}; } @@ -110,23 +101,22 @@ inline void kitTextCentered(LICE_IBitmap* bmp, const instrument::ui::Rect& r, text(bmp, toKitBox(r), s, font, role, Align::Center); } -// Draw a peak envelope in `r` through the kit's shared waveform primitive (Phase L, L3). +// Draw a peak envelope in `r` through the kit's shared waveform primitive. inline void drawEnvelope(LICE_IBitmap* bmp, const instrument::ui::Rect& r, const audio::Envelope& env) { drawWaveform(bmp, toKitBox(r), env); } -// The bin count a card's thumbnail is computed at: one bin per drawn pixel column — the -// gap-free render comes from peaks::columnMinMax's exact partition, not from extra bins. -// thumbnailFor clamps the request to the decoded frame count. +// The bin count a card's thumbnail is computed at: one bin per drawn pixel column (the +// gap-free render comes from peaks::columnMinMax's exact partition). inline int thumbBins(const instrument::ui::BrowserLayout& layout) { return (std::max)(1, kWaveformOversample * ui::waveformColumnCount(toKitBox( instrument::ui::cardThumbnailRect(layout, 0)))); } -// Draw the title band with the live readout. Shared by the Sample face (nav visible) — -// Browse/Zone draw their own back button in place of the nav. +// Draws the title band with the live readout. Browse/Zone draw their own back button in +// place of the nav. inline void drawTitleBand(LICE_IBitmap* bmp, const instrument::ui::Rect& title, const std::string& readout) { fillSurface(bmp, toKitBox(title), ui::Role::BgPanel, ui::InteractionState::Rest); @@ -135,12 +125,10 @@ inline void drawTitleBand(LICE_IBitmap* bmp, const instrument::ui::Rect& title, kitText(bmp, titleText, readout.c_str(), Font::Title, ui::Role::TextPrimary); } -// Draw one radial knob face (r11): the FA4 param_slider primitive owns the value<->angle -// map; this turns it into LICE calls through the kit's palette roles. LICE's arc -// convention matches param_slider's (angle 0 = 12 o'clock, positive clockwise) — but LICE -// takes RADIANS, and drawing the 7->5 o'clock sweep THROUGH the top needs a continuous -// angle span, so the degrees convert as (deg - 360) * pi/180, mapping 210..510 onto -// -150..+150 degrees. One conversion, both arcs. +// Draws one radial knob face: param_slider owns the value<->angle map; this turns it into +// LICE calls. LICE takes radians, and drawing the 7->5 o'clock sweep through the top needs +// a continuous angle span, so degrees convert as (deg - 360) * pi/180, mapping 210..510 +// onto -150..+150 degrees. inline void drawKnobFace(LICE_IBitmap* bmp, const instrument::ui::Rect& knobRect, double value01, ui::InteractionState st) { using instrument::ui::KnobArc; @@ -149,14 +137,13 @@ inline void drawKnobFace(LICE_IBitmap* bmp, const instrument::ui::Rect& knobRect const KnobGeometry kg = instrument::ui::computeKnob(knobRect); if (kg.radius <= 1.0) return; constexpr double kDegToRad = 3.14159265358979323846 / 180.0; - const KnobArc arc{}; // the FA4 default 7->5 o'clock sweep + const KnobArc arc{}; // the default 7->5 o'clock sweep const float cx = static_cast(kg.centerX); const float cy = static_cast(kg.centerY); const float rOuter = static_cast(kg.radius) - 0.5f; const bool disabled = (st == ui::InteractionState::Disabled); const bool hot = (st == ui::InteractionState::Dragging || st == ui::InteractionState::Hover); - // Face: a filled circle in the cell surface color under the interaction state. LICE_FillCircle(bmp, cx, cy, rOuter - 1.f, toLice(ui::roleColorState(ui::Role::BgCell, st)), 1.0f, 0, true); // Track: the full sweep as a hairline arc (the dead 60-degree arc at the bottom stays bare). @@ -165,8 +152,6 @@ inline void drawKnobFace(LICE_IBitmap* bmp, const instrument::ui::Rect& knobRect (arc.startDeg + instrument::ui::knobSweepDeg(arc) - 360.0) * kDegToRad); LICE_Arc(bmp, cx, cy, rOuter, a0, a1, toLice(ui::roleColor(ui::Role::LineHairline)), 1.0f, 0, true); - // Value arc: start -> the value's angle, in the live accent (hot while under the pointer / - // dragging, dim when disabled). const double v = value01 < 0.0 ? 0.0 : (value01 > 1.0 ? 1.0 : value01); if (v > 0.0) { const float av = static_cast( @@ -185,11 +170,9 @@ inline void drawKnobFace(LICE_IBitmap* bmp, const instrument::ui::Rect& knobRect toLice(ui::roleColor(needleRole)), 1.0f, 0, true); } -// Draw the pastel spectral keyboard-strip background (Phase L, L3) — the signature -// surface. Fills each MIDI key column with its spectral hue, then draws faint per-octave -// hairline ticks. Shared by the setup face + the Zones strip so both read as the same -// spectrum. S-VIEW-7: accidentals get a dark bg/base wash over the hue (an OVERLAY, not -// a keyboard shape) so pitch position reads as a keyboard at a glance. +// Draws the pastel spectral keyboard-strip background: each MIDI key column filled with +// its spectral hue, accidentals darkened with an overlay wash so pitch position reads as +// a keyboard at a glance. Shared by the setup face + the Zones strip. inline void drawSpectralStrip(LICE_IBitmap* bmp, const instrument::ui::Rect& stripArea) { using instrument::ui::StripLayout; if (stripArea.width <= 0 || stripArea.height <= 0) return; @@ -218,8 +201,8 @@ inline void drawSpectralStrip(LICE_IBitmap* bmp, const instrument::ui::Rect& str } } -// Draw the single-capture root marker on the strip: an accent-primary bar with a soft -// STATIC glow (a wider, lower-alpha accent bar behind it) — the "this is live" mark. +// Draws the single-capture root marker: an accent-primary bar with a soft static glow — +// the "this is live" mark. inline void drawRootMarker(LICE_IBitmap* bmp, const instrument::ui::Rect& stripArea, const instrument::ui::StripLayout& sl, int root) { const int sx = stripArea.x; diff --git a/src/shell/instrument/editor_paint_browse_zone.cpp b/src/shell/instrument/editor_paint_browse_zone.cpp index 60c5d82..309eb05 100644 --- a/src/shell/instrument/editor_paint_browse_zone.cpp +++ b/src/shell/instrument/editor_paint_browse_zone.cpp @@ -1,10 +1,9 @@ -// editor_paint_browse_zone.cpp — the ReaSamplerEditor's BROWSE-MODAL and ZONE-SURFACE -// painting (Q-W2v split of reasampler_editor.cpp, T4-11): the full-window select-then- -// confirm picker (S-VIEW-5 — wash, search box, filter tabs, card grid, scrollbar, -// footer) and the Zone keymap surface (S-VIEW-8/FB2 — add/delete, the spectral zones -// strip, the numeric-entry legend, the per-zone knob deck + curve button). Windows-only -// (D5). Shares the Sample face's painters (title band / empty state / deck / curve -// button / popup) via the class + editor_internal.h. +// editor_paint_browse_zone.cpp — the ReaSamplerEditor's browse-modal and zone-surface +// painting: the full-window select-then-confirm picker (wash, search box, filter tabs, card +// grid, scrollbar, footer) and the Zone keymap surface (add/delete, the spectral zones +// strip, the numeric-entry legend, the per-zone knob deck + curve button). Windows-only. +// Shares the Sample face's painters (title band / empty state / deck / curve button / +// popup) via the class + editor_internal.h. #include "shell/instrument/reasampler_editor.h" @@ -15,8 +14,8 @@ #include #include -#include "core/instrument/ui/browser_scroll.h" // BrowseModal + scroll/search geometry (S12) -#include "core/instrument/ui/knob_deck.h" // the per-zone deck layout (FB2) +#include "core/instrument/ui/browser_scroll.h" // BrowseModal + scroll/search geometry +#include "core/instrument/ui/knob_deck.h" // the per-zone deck layout #include "shell/instrument/editor_internal.h" // kit adapters + spectral strip + labels #include "shell/instrument/reasampler_processor.h" @@ -27,8 +26,8 @@ using namespace reasampler::instrument::ui; // browser/strip/deck/zone-surface using namespace reasampler::instrument::map; // SampleChoice / BankChoice / SampleRefs void ReaSamplerEditor::paintBrowse(LICE_IBitmap* bmp, int w, int h) { - // A full-window modal sheet over the Sample face (F3: full-window overlay). Dim the underlying - // Sample face with a bg/base wash, then draw the picker opaque on top. + // A full-window modal sheet over the Sample face. Dim the underlying Sample face with a + // bg/base wash, then draw the picker opaque on top. LICE_FillRect(bmp, 0, 0, w, h, toLice(roleColor(Role::BgBase)), 0.82f, 0); const BrowseModal bm = computeBrowseModal(w, h); @@ -84,8 +83,9 @@ void ReaSamplerEditor::paintBrowse(LICE_IBitmap* bmp, int w, int h) { active ? Role::BgBase : Role::TextPrimary); } - // Cards (the S12 visible window at the current scroll offset). The PENDING pick (browsePendingId_) - // is marked with the accent-primary border; the currently-loaded id gets a faint tertiary border. + // Cards (the visible window at the current scroll offset). The pending pick + // (browsePendingId_) is marked with the accent-primary border; the currently-loaded id + // gets a faint tertiary border. const int bins = thumbBins(bl); const int cardCount = static_cast(visible_.size()); const VisibleRange vr = visibleCardRange(bl, cardCount, scrollOffset_); @@ -192,8 +192,8 @@ void ReaSamplerEditor::paintZone(LICE_IBitmap* bmp, int w, int h) { drawButton(bmp, box, "Delete", state, /*warn=*/false); } - // The zones strip — the same PASTEL SPECTRAL surface as the Sample face, with one bar per - // zone over the spectrum. The SELECTED zone lifts to accent-primary + a static glow ("which + // The zones strip — the same pastel spectral surface as the Sample face, with one bar per + // zone over the spectrum. The selected zone lifts to accent-primary + a static glow ("which // zone is live"); the rest take the categorical secondary hue at low alpha. const Rect stripArea = zonesStripArea(content); drawSpectralStrip(bmp, stripArea); @@ -218,8 +218,8 @@ void ReaSamplerEditor::paintZone(LICE_IBitmap* bmp, int w, int h) { } // A one-line legend of the selected zone below the strip, with three click-to-type numeric - // entry fields (low / high / root) — S12 direct numeric entry. Clicking a field focuses it - // (entryField_) and typed text commits via parseNoteEntry on Enter. + // entry fields (low / high / root). Clicking a field focuses it (entryField_) and typed + // text commits via parseNoteEntry on Enter. const int legendTop = stripArea.bottom() + 8; Rect infoR = Rect::ltrb(stripArea.x, legendTop, stripArea.right(), legendTop + 18); if (selectedZone_ >= 0 && selectedZone_ < static_cast(map_.zones.size())) { @@ -256,19 +256,18 @@ void ReaSamplerEditor::paintZone(LICE_IBitmap* bmp, int w, int h) { Font::Label, Role::TextDim); } - // The per-zone parameter surface for the selected zone. FB2 (R11-F2): the SAME knob deck + - // curve-preview-button/popup grammar as the Sample face — one control language over the one - // storage site (S15-F2) — replacing the retired param_slider rows + inline curve box. Only - // the per-zone groups render here; VOICE/MASTER are per-instance (ComponentState) and live - // on the Sample deck only. + // The per-zone parameter surface for the selected zone: the same knob deck + + // curve-preview-button/popup grammar as the Sample face — one control language over the + // one storage site. Only the per-zone groups render here; VOICE/MASTER are per-instance + // (ComponentState) and live on the Sample deck only. if (selectedZone_ >= 0 && selectedZone_ < static_cast(map_.zones.size())) { const PerformanceZone& z = map_.zones[static_cast(selectedZone_)]; paintKnobDeck(bmp, zonesDeckArea(content), z, zoneDeckGroupDescs(z.play)); paintCurveButton(bmp, zonesCurveButton(content), z); } - // The curve popup (FB2): a centered sheet over the whole Zone surface, drawn LAST — - // the same modal grammar as the Sample face. + // The curve popup: a centered sheet over the whole Zone surface, drawn last — the same + // modal grammar as the Sample face. if (curvePopupOpen_) paintCurvePopup(bmp, w, h); } diff --git a/src/shell/instrument/editor_paint_sample.cpp b/src/shell/instrument/editor_paint_sample.cpp index 6194f9d..ced83e8 100644 --- a/src/shell/instrument/editor_paint_sample.cpp +++ b/src/shell/instrument/editor_paint_sample.cpp @@ -1,10 +1,9 @@ -// editor_paint_sample.cpp — the ReaSamplerEditor's SAMPLE-FACE painting (Q-W2v split of -// reasampler_editor.cpp, T4-11): the WM_PAINT dispatch, the r11 Sample home face (title -// band + elastic hero waveform + root/preview cluster + bottom-anchored knob deck), the -// S-VIEW-3 envelope overlay, the velocity-curve editor + mini preview button + popup -// sheet (shared painters the Zone surface reuses, FB2), and the empty state. Windows-only -// (D5); draws through the L1 kit by palette role. All layout math is pure -// (editor_geometry / knob_deck / curve_popup) — this TU only draws. +// editor_paint_sample.cpp — the ReaSamplerEditor's sample-face painting: the WM_PAINT +// dispatch, the Sample home face (title band + elastic hero waveform + root/preview cluster +// + bottom-anchored knob deck), the envelope overlay, the velocity-curve editor + mini +// preview button + popup sheet (shared painters the Zone surface reuses), and the empty +// state. Windows-only; draws through the shared kit by palette role. All layout math is +// pure (editor_geometry / knob_deck / curve_popup) — this TU only draws. #include "shell/instrument/reasampler_editor.h" @@ -17,10 +16,10 @@ #include #include "core/audio/peaks.h" // computeEnvelope (hero waveform binning) -#include "core/instrument/ui/curve_popup.h" // r11 centered curve-popup sheet geometry (FB1) +#include "core/instrument/ui/curve_popup.h" // centered curve-popup sheet geometry #include "core/instrument/ui/knob_deck.h" // deck layout + kDeckKnobSize -#include "core/instrument/ui/waveform_view.h" // frameToX (S11 markers) -#include "core/version/app_version.h" // vstPluginName (channel-derived title band, S18) +#include "core/instrument/ui/waveform_view.h" // frameToX (waveform markers) +#include "core/version/app_version.h" // vstPluginName (channel-derived title band) #include "shell/instrument/editor_internal.h" // kit adapters + knob face/spectral strip/root marker #include "shell/instrument/reasampler_processor.h" @@ -32,8 +31,8 @@ using namespace reasampler::instrument::map; // SampleRefs / findRef (title read using audio::computeEnvelope; namespace { -// Marker roles (Phase L, L3) — semantic, drawn through the kit's palette: start = teal -// (secondary), loop start/end = purple (tertiary). The loop-span fill is a faint purple. +// Marker roles — semantic, drawn through the kit's palette: start = teal (secondary), loop +// start/end = purple (tertiary). The loop-span fill is a faint purple. constexpr Role kRoleStartMarker = Role::AccentSecondary; constexpr Role kRoleLoopMarker = Role::AccentTertiary; } // namespace @@ -48,9 +47,9 @@ void ReaSamplerEditor::paint(HDC hdc) { LICE_SysBitmap bmp(w, h); LICE_Clear(&bmp, toLice(roleColor(Role::BgBase))); - // S-VIEW-1 three-view dispatch. Sample is home; Browse is a full-window modal overlay drawn - // OVER Sample; Zone is the dedicated surface. In the Browse view we draw Sample first so the - // modal reads as a sheet layered over the home face (the "picker over the document" grammar). + // Three-view dispatch. Sample is home; Browse is a full-window modal overlay drawn over + // Sample; Zone is the dedicated surface. In the Browse view we draw Sample first so the + // modal reads as a sheet layered over the home face. if (view_ == View::kZone) { paintZone(&bmp, w, h); } else { @@ -58,9 +57,9 @@ void ReaSamplerEditor::paint(HDC hdc) { if (view_ == View::kBrowse) paintBrowse(&bmp, w, h); } - // S13 (relay degraded): a transient banner flashed after a file was dropped ON THIS window. - // It reiterates the shipped ingest gesture rather than swallowing the drop silently. Drawn - // LAST so it overlays whatever view is up; decays via onSyncTimer (dropHintTicks_). + // A transient banner flashed after a file was dropped on this window. It reiterates the + // shipped ingest gesture rather than swallowing the drop silently. Drawn last so it + // overlays whatever view is up; decays via onSyncTimer (dropHintTicks_). if (dropHintTicks_ > 0) { const int bannerTop = (std::min)(kTitleHeight, h); const int bannerH = (std::min)(kTitleHeight + 8, (std::max)(0, h - bannerTop)); @@ -77,21 +76,20 @@ void ReaSamplerEditor::paint(HDC hdc) { } void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) { - // r11: the deck height comes from the pure knob_deck wrap (mode-independent — the AMP - // ENVELOPE group reserves its 5-cell Gate width, so Gate<->Trigger never changes it). + // The deck height comes from the pure knob_deck wrap (mode-independent — the AMP ENVELOPE + // group reserves its 5-cell Gate width, so Gate<->Trigger never changes it). const PerformanceZone deckZone = effectiveSampleZone(); const std::vector deckDescs = deckGroupDescs(deckZone.play); const SampleBands bands = computeSampleBands(w, h, deckHeight(deckDescs, w - 2 * kPad)); - // Title: product name + live readout. Standard B palette — the beta channel gets NO distinct - // accent (settled 2026-07-27); the channel-derived vstPluginName is the only beta-vs-stable - // signal. - std::string title = version::vstPluginName(); // channel-derived (S18) + // Title: product name + live readout. The beta channel gets no distinct accent; the + // channel-derived vstPluginName is the only beta-vs-stable signal. + std::string title = version::vstPluginName(); if (processor_ && processor_->bridge().isConnected()) { - // The instance's OWN loaded state outranks bank availability (pS: the bank is a - // browser source, not the instrument's identity) — a self-contained instance names - // its sound (refs displayName fallback) even when the bank snapshot is empty. + // The instance's own loaded state outranks bank availability (the bank is a browser + // source, not the instrument's identity) — a self-contained instance names its sound + // (refs displayName fallback) even when the bank snapshot is empty. if (!map_.zones.empty()) title += " [" + std::to_string(map_.zones.size()) + " zone(s)]"; else if (!selectedId_.empty()) title += " [" + sampleLabel(samples_, processor_->sampleRefs(), selectedId_) + "]"; @@ -128,17 +126,17 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) { } // Resolve the effective single-capture zone: the picked id's one-zone override when present, - // else the product-default play params (S15-F2 — the single capture is a one-zone map). This - // is the ONE storage site both Sample and Zone edit. + // else the product-default play params (the single capture is a one-zone map). This is the + // one storage site both Sample and Zone edit. const PerformanceZone& zone = deckZone; - // --- Hero waveform band: envelope + S11 markers + S-VIEW-3 envelope overlay ----------- + // Hero waveform band: envelope + markers + envelope overlay. const std::vector& pcm = monoPcmFor(selectedId_); const std::int64_t frames = static_cast(pcm.size()); const Rect waveArea = bands.hero; fillSurface(bmp, toKitBox(waveArea), Role::BgBase, InteractionState::Rest); if (frames > 0 && waveArea.width > 0) { - // FA3 gap-free: one bin per drawn pixel column (kWaveformOversample == 1, so this + // Gap-free: one bin per drawn pixel column (kWaveformOversample == 1, so this // multiplies by 1). The gap-free draw comes from peaks::columnMinMax's exact // partition — extra bins produce no visible change. Clamped to frame count below. const std::int64_t wantBins = @@ -168,14 +166,14 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) { toLice(roleColor(markerRoles[i])), alpha, 0); } - // S-VIEW-3: trace the amp-envelope overlay + its draggable node handles over the hero. + // Trace the amp-envelope overlay + its draggable node handles over the hero. paintEnvelopeOverlay(bmp, waveArea, zone, frames); } else { kitTextCentered(bmp, waveArea, "(decoding...)", Font::Label, Role::TextDim); } - // --- Root + preview cluster (r11: remainder-width root strip, preview button, radial - // velocity knob, mini curve-preview button, channel toggle) ----------------------------- + // Root + preview cluster: remainder-width root strip, preview button, radial velocity + // knob, mini curve-preview button, channel toggle. fillSurface(bmp, toKitBox(bands.cluster), Role::BgPanel, InteractionState::Rest); const ChannelToggleRects chan = channelToggleRects(bands.cluster); const ClusterRects cr = clusterRects(bands.cluster, chan.mono, kDeckKnobSize); @@ -193,7 +191,7 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) { : (isHovered(HoverKind::kPreview, -1) ? InteractionState::Hover : InteractionState::Rest); drawButton(bmp, box, "Preview", st, /*warn=*/false); } - // Preview velocity: a RADIAL knob cell (r11 — the deck cell grammar), bound to the same + // Preview velocity: a radial knob cell (the deck cell grammar), bound to the same // persisted previewVelocity seam. Label swaps to the live value during hover/drag. { const bool dragging = (drag_ == DragKind::kDeckKnob && dragParamId_ == -2); @@ -211,8 +209,8 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) { kitTextCentered(bmp, cr.velLabel, "Vel", Font::Micro, Role::TextDim); } } - // The mini curve-preview button (r11): opens the popup editor. Shared painter with the - // Zone panel's button (FB2 — one grammar on both surfaces). + // The mini curve-preview button: opens the popup editor. Shared painter with the Zone + // panel's button — one grammar on both surfaces. paintCurveButton(bmp, cr.curveBtn, zone); // Mono | Stereo output-mode toggle. { @@ -227,10 +225,10 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) { kitTextCentered(bmp, chan.stereo, "Stereo", Font::Label, isStereo ? Role::BgBase : Role::TextPrimary); } - // --- The knob deck (r11: the fenced control groups, bottom-anchored) ------------------- + // The knob deck: the fenced control groups, bottom-anchored. paintKnobDeck(bmp, bands.deck, zone, deckDescs); - // --- The curve popup (r11): a centered sheet over the whole Sample face, drawn LAST ---- + // The curve popup: a centered sheet over the whole Sample face, drawn last. if (curvePopupOpen_) paintCurvePopup(bmp, w, h); } @@ -252,11 +250,11 @@ void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const Rect& waveA const int x1 = (std::max)(waveArea.x, (std::min)(waveArea.right() - 1, poly[i].x)); LICE_Line(bmp, x0, poly[i - 1].y, x1, poly[i].y, line, 1.0f, 0, true); } - // Draggable node handles: a small square per DRAGGABLE node (Origin + ReleaseStart are draw- - // only). Lit accent-hot when this node is the grabbed one. FA2 guarantees every vertex is - // in-bounds (the pre-FA2 right-edge clip is dead and removed — edge nodes like ReleaseEnd - // at area.right()-1 MUST get handles); the handle SQUARE is additionally clamped inside the - // hero rect so a 6px box on an edge node never overhangs into the neighbouring bands. + // Draggable node handles: a small square per draggable node (Origin + ReleaseStart are + // draw-only). Lit accent-hot when this node is the grabbed one. Every vertex is + // guaranteed in-bounds (edge nodes like ReleaseEnd at area.right()-1 must get handles); + // the handle square is additionally clamped inside the hero rect so a 6px box on an edge + // node never overhangs into the neighbouring bands. const LICE_pixel handle = toLice(roleColor(Role::AccentPrimary)); const LICE_pixel handleHot = toLice(roleColor(Role::AccentHot)); for (const EnvVertex& v : poly) { @@ -274,8 +272,8 @@ void ReaSamplerEditor::paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r, if (r.width <= 0 || r.height <= 0) return; // defensive (degenerate rect) // The bordered box: a panel surface + hairline border, drawn by palette role. No corner - // caption — the popup sheet's own "VELOCITY -> AMP" title labels this context (FB2: the - // popup is the only host). + // caption — the popup sheet's own "VELOCITY -> AMP" title labels this context (the popup + // is the only host). fillSurface(bmp, toKitBox(r), Role::BgPanel, InteractionState::Rest); LICE_DrawRect(bmp, r.x, r.y, r.width - 1, r.height - 1, toLice(roleColor(Role::LineHairline)), 1.0f, 0); @@ -357,8 +355,8 @@ void ReaSamplerEditor::paintKnobDeck(LICE_IBitmap* bmp, const Rect& deckArea, : (seg1Active ? Role::BgBase : Role::TextPrimary)); }; - // The knob's short name label (swapped for the live value during hover/drag — r11: no - // third line, no permanent value clutter). + // The knob's short name label (swapped for the live value during hover/drag — no third + // line, no permanent value clutter). const auto knobName = [](ParamControl c) -> const char* { switch (c) { case ParamControl::kAttack: return "Attack"; @@ -395,7 +393,7 @@ void ReaSamplerEditor::paintKnobDeck(LICE_IBitmap* bmp, const Rect& deckArea, } kitText(bmp, g.caption, caption, Font::Micro, Role::TextDim); - // The compact caption toggle (r11: right-anchored IN the caption row, never full-width). + // The compact caption toggle (right-anchored in the caption row, never full-width). if (g.captionToggle.id >= 0) { switch (static_cast(g.captionToggle.id)) { case ParamControl::kPlayMode: @@ -422,7 +420,7 @@ void ReaSamplerEditor::paintKnobDeck(LICE_IBitmap* bmp, const Rect& deckArea, } // The knobs. PITCH ENV knobs draw Disabled (not hidden) while the envelope is off — - // stable geometry (r11). + // stable geometry. for (const DeckCellLayout& c : g.cells) { if (c.id < 0) continue; // reserved blank cell (the Trigger face's two spares) const bool disabled = (g.id == kGroupPitchEnv && !play.pitchEnv.enabled); @@ -444,11 +442,11 @@ void ReaSamplerEditor::paintKnobDeck(LICE_IBitmap* bmp, const Rect& deckArea, void ReaSamplerEditor::paintCurveButton(LICE_IBitmap* bmp, const Rect& r, const PerformanceZone& zone) { if (r.width <= 0 || r.height <= 0) return; - // The mini curve-preview button (r11/FB2 — shared by the Sample cluster and the Zone - // panel): a hairline-bordered bg/cell square with the zone's live velocity curve traced - // in miniature (no node markers at this scale). Hover lifts it; it draws ACTIVE - // (accent-primary border) while its popup is open, and re-renders live as the popup - // edits the curve (same zone, re-read each paint). + // The mini curve-preview button (shared by the Sample cluster and the Zone panel): a + // hairline-bordered bg/cell square with the zone's live velocity curve traced in + // miniature (no node markers at this scale). Hover lifts it; it draws Active + // (accent-primary border) while its popup is open, and re-renders live as the popup edits + // the curve (same zone, re-read each paint). const bool hov = isHovered(HoverKind::kCurveButton, -1); fillSurface(bmp, toKitBox(r), Role::BgCell, hov ? InteractionState::Hover : InteractionState::Rest); @@ -489,10 +487,10 @@ void ReaSamplerEditor::paintCurvePopup(LICE_IBitmap* bmp, int w, int h) { : InteractionState::Rest; drawButton(bmp, box, "x", st, /*warn=*/false); } - // The full-size editor: ONE draw path + the one curveBoxFromRect mapping formula, so + // The full-size editor: one draw path + the one curveBoxFromRect mapping formula, so // trace/handles/drag-off cues cannot drift between hosts. The popup edits popupZone() — // the picked capture's one-zone site on the Sample face, the selected zone on the Zone - // surface (FB2). + // surface. paintVelocityCurve(bmp, pl.curveBox, popupZone()); } @@ -502,9 +500,9 @@ void ReaSamplerEditor::paintEmptyState(LICE_IBitmap* bmp, const Rect& area) { const char* msg = samples_.empty() ? "No captures in this project yet - capture audio into the bank to play it here." : "No captures in this bank filter. Choose another bank tab above."; - // Split the area so the primary line sits centered and the S13 ingest affordance sits just - // below it. The affordance is the SHIPPED ingest gesture (drop onto the docked panel) — kept - // discoverable here regardless of whether a drop ever lands on THIS window. + // Split the area so the primary line sits centered and the ingest affordance sits just + // below it. The affordance is the shipped ingest gesture (drop onto the docked panel) — + // kept discoverable here regardless of whether a drop ever lands on this window. Rect primary = Rect::ltrb(area.x, area.y, area.right(), area.y + area.height / 2); Rect hint = Rect::ltrb(area.x, primary.bottom(), area.right(), area.bottom()); kitTextCentered(bmp, primary, msg, Font::Label, Role::TextDim); diff --git a/src/shell/instrument/editor_platform.cpp b/src/shell/instrument/editor_platform.cpp index 3c569df..fa30067 100644 --- a/src/shell/instrument/editor_platform.cpp +++ b/src/shell/instrument/editor_platform.cpp @@ -1,15 +1,14 @@ -// editor_platform.cpp — the ReaSamplerEditor's IPlugView + Win32 window plumbing (Q-W2v -// split of reasampler_editor.cpp, T4-11): platform-type/resize negotiation, the child -// window class + creation/destruction, the S9/S8 sync timer lifetime, the WM_* dispatch -// (wndProc — paint, mouse, keyboard, capture-loss rollback, drop-accept, timer), and the -// non-Windows stubs (D5 makes Windows the only build target; the TU still compiles -// elsewhere). +// editor_platform.cpp — the ReaSamplerEditor's IPlugView + Win32 window plumbing: +// platform-type/resize negotiation, the child window class + creation/destruction, the +// sync timer lifetime, the WM_* dispatch (wndProc — paint, mouse, keyboard, capture-loss +// rollback, drop-accept, timer), and the non-Windows stubs (Windows is the only build +// target; the TU still compiles elsewhere). #include "shell/instrument/reasampler_editor.h" #ifdef _WIN32 #include // GET_X_LPARAM / GET_Y_LPARAM -#include // DragAcceptFiles / DragQueryFile / DragFinish — S13 drop-accept +#include // DragAcceptFiles / DragQueryFile / DragFinish — drop-accept #endif #include "shell/instrument/editor_internal.h" // (transitively: lice + the kit, Windows only) @@ -23,11 +22,11 @@ namespace reasampler::vst { namespace { constexpr const wchar_t* kChildClassName = L"ReaSampler9000VstEditor"; -// The S9/S8 change-detection poll (WM_TIMER on the child window). A low-frequency -// UI-thread timer: responsive enough that a recapture/ingest/assign refreshes "within a -// bounded cadence" (the S9 verify criterion) yet cheap — three small ext-state reads per -// tick, coalescing many bumps between ticks into one reload. 500 ms is a deliberate -// build-time residual. The id is a per-window SetTimer id (any nonzero). +// The change-detection poll (WM_TIMER on the child window). A low-frequency UI-thread +// timer: responsive enough that a recapture/ingest/assign refreshes within a bounded +// cadence, yet cheap — three small ext-state reads per tick, coalescing many bumps +// between ticks into one reload. 500 ms is a deliberate build-time residual. The id is a +// per-window SetTimer id (any nonzero). constexpr UINT_PTR kSyncTimerId = 1; constexpr UINT kSyncTimerIntervalMs = 500; } // namespace @@ -45,12 +44,12 @@ tresult PLUGIN_API ReaSamplerEditor::canResize() { } tresult PLUGIN_API ReaSamplerEditor::checkSizeConstraint(ViewRect* rect) { - // Enforce a minimum usable floor: at least 560 wide and 460 tall. The host calls this before - // every resize; clamp the proposed rect in place and return kResultTrue so the host applies the - // (possibly adjusted) rect rather than the raw user drag. 560×460 keeps the Sample face's title - // + hero waveform + cluster + a few control rows visible (the control strip clips gracefully - // below the panel bottom); anything smaller would clip essential UI. The default 840×620 is - // above this floor. + // Enforce a minimum usable floor: at least 560 wide and 460 tall. The host calls this + // before every resize; clamp the proposed rect in place and return kResultTrue so the host + // applies the (possibly adjusted) rect rather than the raw user drag. 560x460 keeps the + // Sample face's title + hero waveform + cluster + a few control rows visible (the control + // strip clips gracefully below the panel bottom); anything smaller would clip essential UI. + // The default 840x620 is above this floor. constexpr int kMinW = 560; constexpr int kMinH = 460; if (!rect) return kResultFalse; @@ -85,11 +84,11 @@ void ReaSamplerEditor::attachedToParent() { classRegistered = true; } - // Create the kit's cached AA fonts before the first paint (Phase L, L3). Idempotent, so a - // reopen (or a co-resident embed strip that also inits) is a cheap no-op. NOT torn down on - // editor close: the embed strip in the SAME binary shares the kit's process-global font - // set, so a per-view shutdown could free fonts still in use by the other view. The tiny - // static HFONT set is reclaimed by the OS at module unload. See the L3 handoff note. + // Create the kit's cached AA fonts before the first paint. Idempotent, so a reopen (or a + // co-resident embed strip that also inits) is a cheap no-op. Not torn down on editor close: + // the embed strip in the same binary shares the kit's process-global font set, so a + // per-view shutdown could free fonts still in use by the other view. The tiny static HFONT + // set is reclaimed by the OS at module unload. kitFontsInit(); refreshFromBank(); @@ -99,17 +98,17 @@ void ReaSamplerEditor::attachedToParent() { r.getWidth(), r.getHeight(), parent, nullptr, hInst, nullptr); if (childHwnd_) { SetWindowLongPtr(childHwnd_, GWLP_USERDATA, reinterpret_cast(this)); - // S13: accept OS file drops on the editor window (WM_DROPFILES). The drop is NOT - // ingested here (the relay is degraded — see onFilesDropped); accepting it lets us show - // the "drop on the panel" affordance instead of the OS bouncing the drop silently. + // Accept OS file drops on the editor window (WM_DROPFILES). The drop is not ingested + // here (the relay is degraded — see onFilesDropped); accepting it lets us show the + // "drop on the panel" affordance instead of the OS bouncing the drop silently. DragAcceptFiles(childHwnd_, TRUE); - // Start the S9/S8 change-detection poll (UI thread). Tied to the child window's - // lifetime — created here, killed in removedFromParent — so an instance whose editor - // is closed does NOT poll (the editor-open-only cadence; see the handoff limitation). + // Start the change-detection poll (UI thread). Tied to the child window's lifetime — + // created here, killed in removedFromParent — so an instance whose editor is closed + // does not poll. SetTimer(childHwnd_, kSyncTimerId, kSyncTimerIntervalMs, nullptr); - // Poll ONCE immediately so a pending assignment (an S8 ingest fired while this editor - // was closed) or a bank change applies the instant the editor opens, rather than waiting - // up to one timer interval. refreshFromBank above already primed the view; this folds in + // Poll once immediately so a pending assignment (an ingest fired while this editor was + // closed) or a bank change applies the instant the editor opens, rather than waiting up + // to one timer interval. refreshFromBank above already primed the view; this folds in // any pending assign/generation so the just-opened editor shows the assigned capture. onSyncTimer(); } @@ -147,7 +146,7 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam, case WM_LBUTTONDOWN: if (self) { SetCapture(hwnd); // keep receiving moves/up if the cursor leaves the child - SetFocus(hwnd); // take keyboard focus so WM_CHAR reaches the search box (S12) + SetFocus(hwnd); // take keyboard focus so WM_CHAR reaches the search box self->onMouseDown(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); } return 0; @@ -155,9 +154,9 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam, if (self) { const int mx = GET_X_LPARAM(lParam); const int my = GET_Y_LPARAM(lParam); - // Hover feedback (Phase L, L3): resolve the element under the pointer and - // repaint on change. Arm WM_MOUSELEAVE once per "over" cycle so the hover - // clears when the pointer leaves the child (TrackMouseEvent is one-shot). + // Hover feedback: resolve the element under the pointer and repaint on change. + // Arm WM_MOUSELEAVE once per "over" cycle so the hover clears when the pointer + // leaves the child (TrackMouseEvent is one-shot). if (!self->mouseTracking_) { TRACKMOUSEEVENT tme{}; tme.cbSize = sizeof(tme); @@ -182,15 +181,15 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam, } return 0; case WM_MOUSEWHEEL: - // S12 browser scroll. GET_WHEEL_DELTA_WPARAM is signed; positive == wheel up. + // Browser scroll. GET_WHEEL_DELTA_WPARAM is signed; positive == wheel up. if (self) self->onMouseWheel(GET_WHEEL_DELTA_WPARAM(wParam)); return 0; case WM_CHAR: - // S12 type-to-filter search keystrokes (only acted on when the search box is focused). + // Type-to-filter search keystrokes (only acted on when the search box is focused). if (self) self->onSearchChar(static_cast(wParam)); return 0; case WM_GETDLGCODE: - // Claim all keys (incl. chars) so the host doesn't eat them before WM_CHAR (S12 search). + // Claim all keys (incl. chars) so the host doesn't eat them before WM_CHAR (search). return DLGC_WANTCHARS | DLGC_WANTARROWS; case WM_LBUTTONUP: if (self) { @@ -199,8 +198,8 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam, } return 0; case WM_RBUTTONDOWN: - // r11: right-click — the curve popup's primary node-delete affordance (issue 3c). - // Routed explicitly (the child wndproc historically handled only left-button). + // Right-click — the curve popup's primary node-delete affordance. Routed + // explicitly (the child wndproc historically handled only left-button). if (self) self->onMouseRDown(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); return 0; case WM_RBUTTONUP: @@ -232,16 +231,16 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam, self->drag_ = DragKind::kNone; self->dragParamId_ = -1; self->dragParamZone_ = -1; - self->curvePointIndex_ = -1; // S-VIEW-10 curve-node drag state (peer reset) + self->curvePointIndex_ = -1; // curve-node drag state (peer reset) self->dragCurveZone_ = -1; self->invalidate(); } } return 0; case WM_DROPFILES: { - // S13 (relay degraded): count the dropped files and flash the affordance. We do NOT - // read/ingest the paths (the instrument never ingests — the relay to the extension is - // unshipped); DragQueryFile with 0xFFFFFFFF just returns the count for the banner. + // Count the dropped files and flash the affordance. We do not read/ingest the paths + // (the instrument never ingests — the relay to the extension is unshipped); + // DragQueryFile with 0xFFFFFFFF just returns the count for the banner. HDROP drop = reinterpret_cast(wParam); const UINT count = DragQueryFileW(drop, 0xFFFFFFFF, nullptr, 0); DragFinish(drop); @@ -258,7 +257,7 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam, } } -#else // non-Windows: not a build target (D5), but keep the TU compilable. +#else // non-Windows: not a build target, but keep the TU compilable. void ReaSamplerEditor::attachedToParent() {} void ReaSamplerEditor::removedFromParent() {} diff --git a/src/shell/instrument/editor_session.cpp b/src/shell/instrument/editor_session.cpp index 5042ac1..6fea63c 100644 --- a/src/shell/instrument/editor_session.cpp +++ b/src/shell/instrument/editor_session.cpp @@ -1,10 +1,8 @@ -// editor_session.cpp — the ReaSamplerEditor's SESSION/BRIDGE state (Q-W2v split of -// reasampler_editor.cpp, T4-11): construction, the live-bank snapshot (refreshFromBank / -// rebuildVisible), the S9/S8 sync tick, the commit-and-reload seam, selection loading, -// the picked-capture marker resolution/upsert helpers, and the decoded-PCM + peak -// thumbnail caches (the mirror of bank_panel's, keyed through the pure ThumbnailKey — -// T2-10 rider). UI thread only; every edit commits OFF the audio thread via the -// processor's reloadInstrument. +// editor_session.cpp — the ReaSamplerEditor's session/bridge state: construction, the +// live-bank snapshot (refreshFromBank / rebuildVisible), the sync tick, the +// commit-and-reload seam, selection loading, the picked-capture marker resolution/upsert +// helpers, and the decoded-PCM + peak thumbnail caches. UI thread only; every edit commits +// off the audio thread via the processor's reloadInstrument. #include "shell/instrument/reasampler_editor.h" @@ -14,12 +12,12 @@ #include #include "core/audio/peaks.h" // computeEnvelope (the cached peak thumbnail) -#include "core/capture/capture_paths.h" // resolveBankFile (shared M4 path resolution) +#include "core/capture/capture_paths.h" // resolveBankFile (shared path resolution) #include "core/capture/wav_codec.h" // parseWavLayout, extractFloatFrames -#include "core/ui/bank_grid.h" // ThumbnailKey / thumbnailKeyString (T2-10: the pure key) -#include "core/util/file_bytes.h" // shared whole-file loader (Q-W1, T2-03) +#include "core/ui/bank_grid.h" // ThumbnailKey / thumbnailKeyString (the pure key) +#include "core/util/file_bytes.h" // shared whole-file loader #include "ext_keys.h" -#include "core/instrument/ui/browser_scroll.h" // nameMatchesQuery (S12 type-to-filter) +#include "core/instrument/ui/browser_scroll.h" // nameMatchesQuery (type-to-filter) #include "shell/instrument/reaper_bridge.h" #include "shell/instrument/reasampler_processor.h" @@ -40,11 +38,8 @@ using util::readFileBytes; ReaSamplerEditor::ReaSamplerEditor(ReaSamplerProcessor* processor) : CPluginView(nullptr), processor_(processor) { - // Default view size (S-VIEW-SIZE-1 tuned to the concrete Sample-face band heights). The Sample - // home stacks: title (26) + hero waveform (150) + cluster (52) + the control strip, whose Gate - // mode shows 12 rows at ~26px ≈ 312px. 840×620 clears the full three-band face without scroll - // on a 1080p screen with headroom. Wide enough that the control strip's label + value columns - // read comfortably. + // Default view size, tuned to the Sample-face band heights: title + hero waveform + + // cluster + control strip. 840x620 clears the full face without scroll on 1080p. ViewRect r(0, 0, 840, 620); setRect(r); } @@ -52,7 +47,7 @@ ReaSamplerEditor::ReaSamplerEditor(ReaSamplerProcessor* processor) void ReaSamplerEditor::refreshFromBank() { // Main/UI thread only — reads the live bank over the bridge (allocates, calls REAPER). thumbCache_.clear(); // a bank edit may have re-captured/removed a sample; drop stale peaks - pcmCache_.clear(); // and its decoded PCM (the S11 waveform + snap source) + pcmCache_.clear(); // and its decoded PCM (the waveform + snap source) if (!processor_) { samples_.clear(); banks_.clear(); @@ -69,17 +64,15 @@ void ReaSamplerEditor::refreshFromBank() { const auto prevZoneCount = static_cast(map_.zones.size()); map_ = processor_->performanceMap(); channelMode_ = processor_->channelMode(); - voiceCount_ = processor_->voiceCount(); // Phase S voice-deck snapshot + voiceCount_ = processor_->voiceCount(); voiceMode_ = processor_->voiceMode(); monoTrigger_ = processor_->monoTrigger(); if (selectedZone_ >= static_cast(map_.zones.size())) selectedZone_ = -1; - // r11: a refresh that emptied the selection (a bank change on the sync tick) closes the - // curve popup — the empty-state Sample face no longer draws it, and an open-but-invisible - // modal would swallow clicks. + // A refresh that emptied the selection closes the curve popup — an open-but-invisible + // modal would otherwise swallow clicks on the empty state. if (selectedId_.empty() && map_.zones.empty()) curvePopupOpen_ = false; - // FB2: on the Zone surface the popup edits the SELECTED zone; close it if the zones list - // shrank (selectedZone_ past-end), OR if the zone count changed at all — a mid-list - // deletion leaves selectedZone_ in range but now naming a DIFFERENT zone (silent retarget). + // On the Zone surface, close the popup if the zone count changed at all — a mid-list + // deletion can leave selectedZone_ in range but silently naming a different zone. if (view_ == View::kZone && curvePopupOpen_) { const auto newZoneCount = static_cast(map_.zones.size()); if (selectedZone_ < 0 || newZoneCount != prevZoneCount) curvePopupOpen_ = false; @@ -94,8 +87,7 @@ void ReaSamplerEditor::refreshFromBank() { } void ReaSamplerEditor::rebuildVisible() { - // S12 composition: the bank filter picks the bank FIRST, then the type-to-filter search - // narrows the survivors by name substring (nameMatchesQuery — empty query is the identity). + // Bank filter first, then type-to-filter search narrows by name substring. visible_.clear(); for (const SampleChoice& s : samples_) { const bool inBank = activeFilterBankId_.empty() || s.bankId == activeFilterBankId_; @@ -103,39 +95,30 @@ void ReaSamplerEditor::rebuildVisible() { const std::string& name = s.displayName.empty() ? s.id : s.displayName; if (nameMatchesQuery(name, searchQuery_)) visible_.push_back(s); } - // NOTE: scrollOffset_ is clamped at paint + wheel time (where the browser layout / panel - // height is known); rebuildVisible runs cross-platform + on the sync-timer refresh, so it - // must not reset the user's scroll here. + // scrollOffset_ is clamped at paint/wheel time (where layout is known); this runs on + // the sync-timer refresh too, so it must not reset the user's scroll here. } #ifdef _WIN32 -// Windows-only (the WM_TIMER cadence + invalidate() are the Win32 child-window path). Declared -// under the same _WIN32 guard in the header; keep the definition guarded to match (D5 makes -// Windows the only build target, but the TU must still compile elsewhere). +// Windows-only (the WM_TIMER cadence + invalidate() are the Win32 child-window path). void ReaSamplerEditor::onSyncTimer() { - // UI thread (WM_TIMER). Poll the S9 bank generation + the S8 assignment request via the - // processor (off the audio thread — the poll itself never touches process()). NEVER while a - // drag is in flight: a reload mid-drag would rebuild the instrument and repaint under the - // user's cursor, yanking the edit. The next tick (500 ms) picks up the change after release. + // UI thread (WM_TIMER). Never while a drag is in flight: a reload mid-drag would + // rebuild the instrument and repaint under the cursor, yanking the edit — the next + // tick picks up the change after release. if (!processor_) return; if (drag_ != DragKind::kNone) return; // defer past the in-flight edit - // An open editor marks THIS instance the focused assignment target (the thundering-herd - // policy — only an editor-open instance applies a pending assign; see the handoff). Pass - // true so this instance consumes the request; instances with no editor open do not poll at - // all (the timer is bound to the child window), so they never contend for the request. + // An open editor is the focused assignment target (thundering-herd policy); instances + // with no editor open never poll (the timer is bound to the child window). const ReaSamplerProcessor::BankSyncResult r = processor_->pollBankSync(/*isFocusedTarget=*/true); - // Re-snapshot the editor's own view only when something changed (a reload from a bank - // content change, or an applied assignment). refreshFromBank re-reads the bank blob + the - // processor's (possibly just-updated) selection/map and drops the stale thumbnail/PCM - // caches, then repaints — so the browser + setup surface reflect the new bank hands-free. + // Re-snapshot only when something changed. if (r.reloaded || r.applied) { refreshFromBank(); invalidate(); } - // S13: decay the drop-affordance banner so it auto-dismisses a few ticks after a drop. + // Decay the drop-affordance banner so it auto-dismisses a few ticks after a drop. if (dropHintTicks_ > 0) { --dropHintTicks_; invalidate(); @@ -144,18 +127,16 @@ void ReaSamplerEditor::onSyncTimer() { #endif // _WIN32 void ReaSamplerEditor::commitAndReload() { - // UI thread only. Publish the edited selection + zones to the processor, then rebuild - // the instrument off the audio thread (reloadInstrument bakes them into the live Keymap). - // pS: the reload also COPIES the picked capture's file ref + intrinsics from the bank - // blob into the instance-owned refs table (refreshRefsFromBank) — a browser load is the - // moment the instance becomes self-contained for that sample. + // UI thread only. Publishes the edited selection + zones, then rebuilds off the audio + // thread. The reload also copies the picked capture's file ref + intrinsics into the + // instance-owned refs table — a browser load is the moment the instance becomes + // self-contained for that sample. if (!processor_) return; processor_->setSelectedSampleId(selectedId_); processor_->setPerformanceMap(map_); processor_->reloadInstrument(); - // GA: the reload may have AUTO-DEFAULTED the channel mode from the loaded capture's - // channel count (implicit mode only) — re-read so the Mono/Stereo toggle draws the mode - // the engine actually decoded with. + // The reload may have auto-defaulted the channel mode (implicit only) — re-read so the + // toggle draws what the engine actually decoded with. channelMode_ = processor_->channelMode(); #ifdef _WIN32 invalidate(); @@ -163,10 +144,9 @@ void ReaSamplerEditor::commitAndReload() { } void ReaSamplerEditor::loadSelection(const std::string& id) { - // Zone-bleed fix (3a): a Sample-face load REPLACES the loaded sound. The previous - // sample's materialized full-range zone must not linger — first-match resolve would - // keep playing it while the editor draws the new pick's zone (matched by sampleId, - // order-blind). Authored Zone-view maps (any narrow key range) are left untouched. + // A Sample-face load REPLACES the loaded sound: the previous sample's materialized + // full-range zone must not linger, or first-match resolve would keep playing it. + // Authored Zone-view maps (narrow key ranges) are left untouched. selectedId_ = id; if (reconcileSingleCaptureZones(map_, selectedId_)) { selectedZone_ = map_.zones.empty() ? -1 : 0; @@ -176,11 +156,11 @@ void ReaSamplerEditor::loadSelection(const std::string& id) { ReaSamplerEditor::SetupMarkers ReaSamplerEditor::pickedMarkers(std::int64_t frames) const { SetupMarkers m; - // Seed from the bank's S2 intrinsic loop (fact about the file), then let a per-zone override - // for the picked id win (the instrument's performance choice, D-B). Read the loop intrinsic - // from the live bank blob (the same path selectSample uses); when that is not readable - // (extension absent / not yet parsed) the instance-OWNED ref carries the same intrinsics - // (pS fallback). The override lives in map_. + // Seed from the bank's intrinsic loop (fact about the file), then let a per-zone override + // for the picked id win (the instrument's performance choice). Read the loop intrinsic from + // the live bank blob (the same path selectSample uses); when that is not readable (extension + // absent / not yet parsed) the instance-owned ref carries the same intrinsics. The override + // lives in map_. if (processor_) { std::optional sel; auto banksJson = @@ -215,10 +195,10 @@ ReaSamplerEditor::SetupMarkers ReaSamplerEditor::pickedMarkers(std::int64_t fram } int ReaSamplerEditor::upsertPickedOverride(const SetupMarkers& m) { - // Find-or-append the zone for selectedId_ and write the loop/start override fields. - // The bank intrinsic is NEVER written (read-only bank consumer, D-B). selectedId_ must - // be non-empty; callers are responsible for that guard. - // Returns the zone index (0-based) so callers can update selectedZone_. + // Find-or-append the zone for selectedId_ and write the loop/start override fields. The + // bank intrinsic is never written (read-only bank consumer). selectedId_ must be + // non-empty; callers are responsible for that guard. Returns the zone index (0-based) so + // callers can update selectedZone_. SampleLoop loop; loop.hasLoop = m.hasLoop; loop.start = m.loopStart; @@ -243,8 +223,8 @@ int ReaSamplerEditor::upsertPickedOverride(const SetupMarkers& m) { PerformanceZone ReaSamplerEditor::effectiveSampleZone() const { // The picked id's one-zone override, if the map already carries one; else a product-default - // zone bound to the picked id (NOT appended — a read-only resolve; a control edit materializes - // it via ensureSampleZone). Mirrors the S15-F2 single-storage-site lean. + // zone bound to the picked id (not appended — a read-only resolve; a control edit + // materializes it via ensureSampleZone). for (const PerformanceZone& z : map_.zones) { if (z.sampleId == selectedId_) return z; } @@ -280,11 +260,10 @@ int ReaSamplerEditor::ensureSampleZone() { } void ReaSamplerEditor::commitPickedMarkers(const SetupMarkers& m) { - // Materialize the edited markers as a per-zone loop/start override on the picked id (upsert, - // mirror of the root-marker path): a full-keyboard zone carrying the override. This plays - // identically to the un-zoned single capture (one chromatic zone) and round-trips through - // the component state; the zone becomes visible if the user opens the Zones panel. The bank - // intrinsic is NEVER written (read-only bank consumer, D-B). + // Materialize the edited markers as a per-zone loop/start override on the picked id (upsert): + // a full-keyboard zone carrying the override. This plays identically to the un-zoned single + // capture (one chromatic zone) and round-trips through the component state; the zone becomes + // visible if the user opens the Zones panel. The bank intrinsic is never written. if (selectedId_.empty()) return; upsertPickedOverride(m); commitAndReload(); @@ -294,11 +273,11 @@ const std::vector& ReaSamplerEditor::monoPcmFor(const std::string& auto it = pcmCache_.find(sampleId); if (it != pcmCache_.end()) return it->second; - // SampleChoice is the browser's metadata projection and does NOT carry the WAV path, so + // SampleChoice is the browser's metadata projection and does not carry the WAV path, so // resolve the path from the live bank blob (selectSample) and decode via the shared WAV - // parse — the mirror of the processor's decodeRelative. Every failure path caches an EMPTY - // vector so a broken/missing file is not re-decoded on every paint. Keyed by id (width- - // independent) — the thumbnail bins this at whatever width, the snap scans it directly. + // parse. Every failure path caches an empty vector so a broken/missing file is not + // re-decoded on every paint. Keyed by id (width-independent) — the thumbnail bins this at + // whatever width, the snap scans it directly. std::string relativePath; std::vector mono; if (processor_) { @@ -308,9 +287,9 @@ const std::vector& ReaSamplerEditor::monoPcmFor(const std::string& if (auto sel = selectSample(*banksJson, sampleId)) relativePath = sel->relativePath; } if (relativePath.empty()) { - // pS fallback: the bank blob is not readable (extension absent / not yet parsed) - // or the id went stale there — the instance-OWNED ref still carries the path, so - // a self-contained instance draws its loaded sound's waveform regardless. + // Fallback: the bank blob is not readable (extension absent / not yet parsed) or + // the id went stale there — the instance-owned ref still carries the path, so a + // self-contained instance draws its loaded sound's waveform regardless. const SampleRefs refs = processor_->sampleRefs(); if (const SelectedSample* r = findRef(refs, sampleId)) { relativePath = r->relativePath; @@ -319,8 +298,7 @@ const std::vector& ReaSamplerEditor::monoPcmFor(const std::string& if (!relativePath.empty()) { const std::string projectDir = processor_->bridge().activeProjectDir(); const std::string abs = resolveBankFile(projectDir, relativePath); - // Shared core/util whole-file loader (Q-W1, T2-03): empty on any failure. - const std::vector bytes = readFileBytes(abs); + const std::vector bytes = readFileBytes(abs); // empty on any failure const WavLayout layout = parseWavLayout(bytes); if (layout.valid) { std::vector interleaved = @@ -334,17 +312,16 @@ const std::vector& ReaSamplerEditor::monoPcmFor(const std::string& } const Envelope& ReaSamplerEditor::thumbnailFor(const std::string& sampleId, int binCount) { - // T2-10 rider: key through the PURE ThumbnailKey (bank_grid) instead of the former - // ad-hoc "id|binCount" concat, so both thumbnail pipelines share one tested key - // grammar (length-prefixed id — collision-proof). The editor invalidates by wholesale - // clear() on refresh/resize, so the bank generation carries no information here — 0. + // Key through the pure ThumbnailKey (bank_grid, length-prefixed id — collision-proof) so + // both thumbnail pipelines share one tested key grammar. The editor invalidates by + // wholesale clear() on refresh/resize, so the bank generation carries no information here. const std::string key = thumbnailKeyString(ThumbnailKey{sampleId, binCount, /*generation=*/0}); auto it = thumbCache_.find(key); if (it != thumbCache_.end()) return it->second; // Bin the (cached) decoded mono PCM at the requested width — one decode per id, reused by - // every thumbnail width AND the S11 waveform surface + snap. + // every thumbnail width AND the waveform surface + snap. const std::vector& mono = monoPcmFor(sampleId); Envelope env; if (!mono.empty()) { diff --git a/src/shell/instrument/processor_reload.cpp b/src/shell/instrument/processor_reload.cpp index 0aa5cff..5130173 100644 --- a/src/shell/instrument/processor_reload.cpp +++ b/src/shell/instrument/processor_reload.cpp @@ -1,11 +1,9 @@ -// processor_reload.cpp — the ReaSamplerProcessor's OFF-AUDIO-THREAD instrument -// lifecycle: reloadInstrument (self-contained refs resolve + WAV decode + keymap -// build), the safety-critical publishBuiltLocked drain-slot swap, the voice-param -// light rebuild, idle-drain retirement, the pre-v10 legacy-lift gate, the S9/S8 -// bank-sync poll, and the pS-usage publish. Split out of reasampler_processor.cpp -// (Q-W2v, T4-12). NOTHING here runs on the audio thread — process() (the lifecycle -// TU) only touches the atomics this family publishes; the atomic-pointer-swap -// pattern deliberately gains NO virtual seam (T4-29). +// processor_reload.cpp — ReaSamplerProcessor's off-audio-thread instrument lifecycle: +// reloadInstrument (self-contained refs resolve + WAV decode + keymap build), the +// safety-critical publishBuiltLocked drain-slot swap, the voice-param light rebuild, +// idle-drain retirement, the pre-v10 legacy-lift gate, the bank-sync poll, and the +// usage publish. Nothing here runs on the audio thread — process() only touches the +// atomics this family publishes; the atomic-pointer-swap pattern gains no virtual seam. #include "shell/instrument/reasampler_processor.h" @@ -18,20 +16,19 @@ #include #include -#include "core/capture/capture_paths.h" // resolveBankFile (shared M4 path resolution) +#include "core/capture/capture_paths.h" // resolveBankFile (shared path resolution) #include "core/capture/wav_codec.h" // parseWavLayout, extractFloatFrames (shared WAV parse) -#include "core/instrument/map/bank_sync.h" // S9/S8 pure decisions: parseBankGeneration, consumeDecision -#include "core/instrument/map/sample_map.h" // refs resolve, buildZonedKeymap (pS self-contained) -#include "core/util/file_bytes.h" // shared whole-file loader (Q-W1, T2-03) -#include "core/wire/assignment_request.h" // decodeAssignmentRequest (S8 request wire parse) -#include "core/wire/sample_usage.h" // pS-usage publish plan + wire (prune-protection seam) +#include "core/instrument/map/bank_sync.h" // pure decisions: parseBankGeneration, consumeDecision +#include "core/instrument/map/sample_map.h" // refs resolve, buildZonedKeymap (self-contained) +#include "core/util/file_bytes.h" // shared whole-file loader +#include "core/wire/assignment_request.h" // decodeAssignmentRequest (request wire parse) +#include "core/wire/sample_usage.h" // usage publish plan + wire (prune-protection seam) #include "ext_keys.h" // kProjExtBanksKey / kProjExtBankGenKey / kProjExtAssignKey namespace reasampler::vst { using namespace instrument::map; // resolution + bank-sync vocabulary this TU drives using namespace reasampler::wire; // assignment_request + sample_usage wire records -// Q-W6 (shim retired): the shared WAV parse + file loader by their real homes. using capture::extractFloatFrames; using capture::parseWavLayout; using capture::resolveBankFile; @@ -40,20 +37,15 @@ using util::readFileBytes; namespace { -// S16 Preserve-mode voice cap: a Preserve voice runs a per-voice OLA pitch shifter and is -// materially heavier than a Varispeed voice. A Preserve note-on past the cap is dropped rather -// than glitching (Varispeed notes are unaffected). Set from the measured/estimated per-voice -// cost — see the handoff CPU note. 8 is conservative pending DAW profiling. Phase S: the -// polyphony bound itself is now the USER-SET voiceCount (1..32, persisted) — this cap stays -// FIXED so raising the voice count never multiplies shifter CPU past the profiled budget. +// Preserve-mode voice cap: a Preserve voice runs a per-voice OLA pitch shifter and is +// materially heavier than a Varispeed voice, so a note-on past the cap is dropped rather +// than glitching. 8 is conservative pending DAW profiling; fixed regardless of the +// user-set voiceCount (1..32) so raising polyphony never multiplies shifter CPU past budget. constexpr std::size_t kPreserveVoiceCap = 8; -// pS-usage: mint a fresh publish identity — 32 lowercase hex chars from the OS entropy -// source. Used for BOTH the persisted per-instance key guid (instanceGuid_) and the -// in-memory per-LIFETIME owner nonce (usageNonce_). Uniqueness (not cryptographic -// strength) is the requirement: two instances sharing a key is the copy-collision -// planUsagePublish resolves fail-safe anyway; the mint just makes accidental collision -// vanishingly unlikely. Off-thread only. +// Mints a fresh publish identity (32 lowercase hex chars) for either the persisted +// instanceGuid_ or the in-memory usageNonce_. Uniqueness, not cryptographic strength, is +// the requirement — planUsagePublish resolves a collision fail-safe anyway. std::string mintUsageInstanceGuid() { std::random_device rd; std::mt19937_64 gen((static_cast(rd()) << 32) ^ rd()); @@ -65,17 +57,11 @@ std::string mintUsageInstanceGuid() { return std::string(buf); } -// Whole-file reads go through the shared core/util readFileBytes (Q-W1, T2-03). -// Off-thread only (blocking file I/O). Empty on any failure — the caller treats -// an unreadable WAV as "nothing to play". - -// Resolve a project-relative WAV path (the M4 way persist does), read + decode it (file -// I/O — off-thread only), and apply the S7 cross-mode channel policy for `mode`: mono mode -// downmixes to one channel (existing policy); stereo mode yields two channels (dual-mono for -// a mono source, L/R for a stereo source) — see decodeChannels. Returns nullopt when the path -// fails to resolve, the file is unreadable, the WAV is malformed, or the decode yields no -// frames — the caller drops the zone (zoned map) or plays silence (single capture). Shared by -// the zoned build and the single-capture path so both decode identically for the active mode. +// Resolves a project-relative WAV path, reads + decodes it (file I/O, off-thread only), +// and applies the cross-mode channel policy for `mode` (mono downmix; stereo -> dual-mono +// for a mono source, L/R for a stereo source — see decodeChannels). Returns nullopt on any +// resolve/read/decode failure — the caller drops the zone or plays silence. Shared by the +// zoned build and the single-capture path. std::optional decodeRelative(const std::string& projectDir, const std::string& relativePath, ChannelMode mode) { @@ -95,22 +81,19 @@ std::optional decodeRelative(const std::string& projectDir, } // namespace std::string ReaSamplerProcessor::reloadInstrument() { - // OFF THE AUDIO THREAD. Serialize concurrent reloads (editor click + setState) so - // the retired-slot free is single-writer. This mutex is NEVER taken on the audio - // thread — process() only touches the atomic. + // OFF THE AUDIO THREAD. Serializes concurrent reloads (editor click + setState) so the + // retired-slot free is single-writer; never taken on the audio thread. std::lock_guard lock(reloadMutex_); - // Mint this reload's generation number first so we can stamp the built instrument - // with it before publishing. Under reloadMutex_ no other reload races here. + // Mint this reload's generation number first so the built instrument is stamped + // before publishing. const std::uint64_t gen = reloadGeneration_.fetch_add(1, std::memory_order_relaxed) + 1; - // 1. SELF-CONTAINED RESOLUTION (pS). The instance-OWNED refs table is the source of - // truth for what to decode. The live bank blob, WHEN readable, is folded into the - // table first (refreshRefsFromBank) — that is the browser's copy-the-ref-in - // mechanism and the S9 recapture sync in one — but its absence changes NOTHING - // below: a project restored before the extension's PROJEXTSTATE parses (or with - // the extension absent entirely) resolves + plays from the persisted refs. The - // project dir comes from REAPER itself (EnumProjects), not from the extension. + // 1. Self-contained resolution: the instance-owned refs table is the source of truth. + // The live bank blob, when readable, is folded in first (refreshRefsFromBank — the + // browser's copy-the-ref-in + recapture-sync mechanism), but its absence changes + // nothing below — a project restored before PROJEXTSTATE parses (or with the + // extension absent) resolves + plays from the persisted refs. const std::string selId = selectedSampleId(); const PerformanceMap map = performanceMap(); const std::vector ids = referencedSampleIds(selId, map); @@ -120,20 +103,18 @@ std::string ReaSamplerProcessor::reloadInstrument() { bridge_.readReasamplerExtState(kProjExtBanksKey); std::lock_guard rl(refsMutex_); if (banksJson) refreshRefsFromBank(sampleRefs_, *banksJson, ids); - // The LOAD path never prunes the owned table: dropping entries here on a transient - // bank miss could destroy the owned intrinsics of the previous selection — the ONE - // copy that survives with the extension absent. Entries for de-referenced ids stay - // in memory (bounded by in-session browsing); hygiene lives at the PERSIST boundary, - // where getState filters its snapshot via retainRefs to what the instance plays. + // The LOAD path never prunes the owned table: dropping entries on a transient bank + // miss could destroy the owned intrinsics of the previous selection — the ONE copy + // that survives with the extension absent. Hygiene lives at the PERSIST boundary + // (getState filters via retainRefs to what the instance plays). refs = sampleRefs_; // snapshot for the decode below (outside the refs lock) } const std::string projectDir = bridge_.activeProjectDir(); - // The active channel mode (S7) governs how each WAV decodes (mono downmix vs 2-channel). - // Read once under its mutex, off the audio thread, before the decode loop. The single- - // capture branch below may auto-default it (GA) before its decode. + // Governs how each WAV decodes (mono downmix vs 2-channel); the single-capture branch + // below may auto-default it before its decode. ChannelMode mode = channelMode(); - // Phase S: snapshot the voice-system parameters once — they are baked into the built - // engine's construction (the engine's config is immutable; a later change rebuilds). + // Snapshot the voice-system parameters once — baked into the built engine's + // construction (immutable config; a later change rebuilds). int builtVoiceCount = kDefaultVoiceCount; VoiceMode builtVoiceMode = VoiceMode::Poly; MonoTrigger builtMonoTrigger = MonoTrigger::Retrigger; @@ -149,12 +130,10 @@ std::string ReaSamplerProcessor::reloadInstrument() { Keymap km; bool haveKeymap = false; - // 2. Tier 1 first: if the instrument's performance map is non-empty, resolve its - // zones against the OWNED refs (an id with no ref drops cleanly), decode each - // zone's WAV off-thread, and build the ZONED keymap. Each surviving zone plays - // its sample repitched from its effective root note (override > ref intrinsic > - // C4). A zone whose WAV fails to decode — a MISSING FILE included — is dropped - // (not the whole map): the defined no-play, no crash, no retry loop. + // 2. Zoned build: if the performance map is non-empty, resolve its zones against the + // owned refs (an id with no ref drops cleanly), decode each zone's WAV off-thread, + // and build the keymap. A zone whose WAV fails to decode is dropped, not the whole + // map — the defined no-play, no crash, no retry loop. if (!map.empty()) { const ResolvedPerformance resolved = resolvePerformanceFromRefs(refs, map); if (!resolved.zones.empty()) { @@ -174,19 +153,15 @@ std::string ReaSamplerProcessor::reloadInstrument() { } } - // 3. Single-capture fast path (S10): an empty performance map plays the ONE - // deliberately-selected capture chromatically across the whole keyboard, resolved - // against the OWNED refs. NO first-sample fallback: an EMPTY selection (or a - // selection with no ref) resolves to nothing, so an un-picked instrument stays - // SILENT (the editor shows its "pick a capture" empty state) rather than - // auto-playing sample #1 (S10 policy reversal of the S4 convenience default). + // 3. Single-capture fast path: an empty performance map plays the one selected capture + // chromatically across the whole keyboard. No first-sample fallback: an empty + // selection (or one with no ref) resolves to nothing, so an un-picked instrument + // stays silent rather than auto-playing sample #1. if (!haveKeymap) { if (const SelectedSample* sel = findRef(refs, selId)) { - // GA auto-default: channelModeFor computes the mode from the loaded capture's - // REQUESTED channel count (always 2 for extension captures; mono only for - // ingest-imported mono files). An unknown count (0) or explicit user choice - // returns the current mode unchanged. Decode-only: the output bus is fixed - // stereo, so no bus work follows a flip. + // Auto-default: channelModeFor computes the mode from the loaded capture's + // channel count (always 2 for extension captures; mono only for ingest-imported + // mono files). An unknown count (0) or explicit user choice keeps the mode. { std::lock_guard cm(channelModeMutex_); channelMode_ = channelModeFor(sel->channelCount, channelMode_, @@ -206,10 +181,9 @@ std::string ReaSamplerProcessor::reloadInstrument() { } if (haveKeymap) { - // Preserve OLA window in OUTPUT frames from the host sample rate (kPreserveWindowMs). - // Every voice's shifter is pre-sized to this off-thread here, so process()-time - // note-on never allocates. Floored at 2 so a valid window is always a real ring - // (which also covers a pathological host rate <= 0 — no rate literal needed). + // Preserve OLA window in output frames from the host rate (kPreserveWindowMs), + // pre-sized here so process()-time note-on never allocates. Floored at 2 so a + // valid window is always a real ring, covering a pathological host rate <= 0 too. std::int64_t preserveWindow = static_cast( kPreserveWindowMs * sampleRate_ / 1000.0 + 0.5); if (preserveWindow < 2) preserveWindow = 2; @@ -218,21 +192,14 @@ std::string ReaSamplerProcessor::reloadInstrument() { kPreserveVoiceCap, preserveWindow, builtVoiceMode, builtMonoTrigger); } - // 4. Publish. Atomically install the new instrument; the DISPLACED one moves into the - // DRAIN slot (FA1, bug 3b) where process() keeps rendering its ringing voices — - // a reload never cuts a sounding note; the next note-on plays the new state. The - // instrument evicted FROM the drain slot (two reloads old) goes to the graveyard - // (process may still be mid-block reading it). A null `built` (no ref / unreadable - // WAV) installs silence while the displaced tails still ring out via the drain. - // `built` is heap-owned; release() hands ownership to the atomic; the drain-evicted - // pointer is re-owned by the graveyard. + // 4. Publish: atomically install the new instrument via the drain-slot swap (see the + // header). A null `built` (no ref / unreadable WAV) installs silence while any + // displaced tails still ring out via the drain. publishBuiltLocked(std::move(built)); - // 5. pS-usage: publish this instance's held captures so the extension's prune can - // never reclaim them (see publishUsage). AFTER the instrument swap, still off the - // audio thread and under reloadMutex_. Publishes regardless of decode success: - // the holds are the refs the instance RETAINS (its play-set), not what decoded — - // a transiently unreadable WAV must stay protected. + // 5. Publish this instance's held captures so the extension's prune can never reclaim + // them. Regardless of decode success: the holds are the refs the instance retains + // (its play-set), not what decoded — a transiently unreadable WAV stays protected. publishUsage(refs, ids); return resolvedId; } @@ -252,15 +219,13 @@ void ReaSamplerProcessor::publishUsage(const SampleRefs& refs, } std::lock_guard lock(usageMutex_); - // A never-published instance with nothing held writes nothing — no key litter for - // fresh/empty instances. Once an identity exists, empties DO publish (they release - // holds the prune would otherwise keep protecting). + // A never-published instance with nothing held writes nothing (no key litter); once an + // identity exists, empties do publish (releasing protected holds). if (instanceGuid_.empty() && mine.holds.empty()) return; if (instanceGuid_.empty()) instanceGuid_ = mintUsageInstanceGuid(); - // The per-LIFETIME owner nonce rides INSIDE the wire (UsageRecord.ownerNonce) so - // planUsagePublish can prove "exactly this incarnation wrote the key" — a same-track - // sibling's byte-identical hold set can never pass as ours (its nonce differs), so - // siblings always union and never clean-replace over each other's held paths. + // The per-lifetime owner nonce (UsageRecord.ownerNonce) lets planUsagePublish prove + // "exactly this incarnation wrote the key" — a same-track sibling's byte-identical hold + // set can never pass as ours, so siblings always union rather than clean-replace. if (usageNonce_.empty()) usageNonce_ = mintUsageInstanceGuid(); mine.ownerNonce = usageNonce_; @@ -268,10 +233,9 @@ void ReaSamplerProcessor::publishUsage(const SampleRefs& refs, bridge_.readReasamplerExtState(usageKeyFor(instanceGuid_)); const UsagePublishPlan plan = planUsagePublish(existing, mine); if (plan.remint) { - // This state was cloned onto another track (FX copy / track duplication): take a - // fresh identity and leave the original's record untouched. The abandoned old - // identity's record dies by the extension's liveness rule when its track no - // longer hosts an instance. getState persists the new guid on the next save. + // Cloned onto another track (FX copy / track duplication): take a fresh identity; + // the abandoned old record dies by the extension's liveness rule once its track no + // longer hosts an instance. instanceGuid_ = mintUsageInstanceGuid(); } else if (plan.skipWrite) { return; // idle tick, or a union that adds nothing — no ext-state churn @@ -280,15 +244,8 @@ void ReaSamplerProcessor::publishUsage(const SampleRefs& refs, } void ReaSamplerProcessor::publishBuiltLocked(std::unique_ptr built) { - // REQUIRES reloadMutex_ held (single-writer over both slots + the graveyard). Shared by - // reloadInstrument and rebuildVoiceEngine — the one safety-critical swap dance. - // - // Bounded reclaim: free graveyard entries whose installedAt < seen, where seen is - // the minimum installedAt process() published over the pointers it holds. Both - // slots are monotone in installedAt, so seen is monotone and any future process() - // load yields installedAt >= seen — an entry below seen is provably unreachable - // (see the header proof). Remaining entries drain at setActive(false) / terminate() - // when process is guaranteed stopped. + // REQUIRES reloadMutex_ held. Shared by reloadInstrument and rebuildVoiceEngine — the + // one safety-critical swap dance (see the header's drain-slot proof). const std::uint64_t seen = processGeneration_.load(std::memory_order_acquire); graveyard_.erase( std::remove_if(graveyard_.begin(), graveyard_.end(), @@ -302,10 +259,9 @@ void ReaSamplerProcessor::publishBuiltLocked(std::unique_ptr b } void ReaSamplerProcessor::rebuildVoiceEngine() { - // OFF THE AUDIO THREAD (the editor's voice-deck click handlers). See the header contract: - // a voice-param change touches NO audio data, so this rebuilds the engine - // around a COPY of the live instrument's already-decoded keymap — no bridge, no disk — - // and publishes through the same drain-slot swap, so ringing tails survive. + // Off the audio thread. A voice-param change touches no audio data, so this rebuilds + // the engine around a copy of the live instrument's already-decoded keymap — no + // bridge, no disk — and publishes through the same drain-slot swap. std::lock_guard lock(reloadMutex_); LoadedInstrument* cur = live_.load(std::memory_order_acquire); if (!cur) return; // nothing loaded: the new params bake into the next real reload. @@ -321,13 +277,14 @@ void ReaSamplerProcessor::rebuildVoiceEngine() { } const std::uint64_t gen = reloadGeneration_.fetch_add(1, std::memory_order_relaxed) + 1; - // Same Preserve-window derivation as reloadInstrument (kPreserveWindowMs at the host rate). + // Same Preserve-window derivation as reloadInstrument. std::int64_t preserveWindow = static_cast( kPreserveWindowMs * sampleRate_ / 1000.0 + 0.5); if (preserveWindow < 2) preserveWindow = 2; - // Deep-copy the decoded PCM + zones. Safe to read concurrently with process(): the keymap - // is immutable after construction, and under reloadMutex_ nobody can free `cur`. + // Deep-copy the decoded PCM + zones: safe to read concurrently with process() because + // the keymap is immutable after construction and reloadMutex_ prevents `cur` from + // being freed. Keymap km = cur->keymap; auto built = std::make_unique( std::move(km), static_cast(builtVoiceCount), gen, @@ -336,23 +293,19 @@ void ReaSamplerProcessor::rebuildVoiceEngine() { } void ReaSamplerProcessor::retireIdleDrain() { - // Phase S (FA1-review Major #2). Cheap early-out BEFORE the lock: 0 means "no drain, or - // it still sounds" — the common case costs one relaxed load and no mutex. + // Cheap early-out before the lock: 0 means "no drain, or it still sounds". const std::uint64_t idleGen = drainIdleGeneration_.load(std::memory_order_acquire); if (idleGen == 0) return; std::lock_guard lock(reloadMutex_); LoadedInstrument* drain = draining_.load(std::memory_order_acquire); - // Retire ONLY if the publication names the drain currently in the slot. A stale value - // (about an already-evicted, older drain) can never match the newer occupant's - // installedAt — the slot is monotone in generation — so a mid-swap race is closed by - // this identity check, not by timing. + // Retire only if the publication names the drain currently in the slot — a stale value + // (an already-evicted, older drain) can never match the newer occupant's installedAt + // (monotone in generation), closing a mid-swap race by identity rather than timing. if (!drain || drain->installedAt != idleGen) return; draining_.store(nullptr, std::memory_order_release); graveyard_.push_back(std::unique_ptr(drain)); - // Prune what is now provably unreachable — the same monotone-generation proof as the - // reload path's reclaim (see reloadInstrument): an entry with installedAt < seen cannot be - // held by process() now or ever again. The just-parked drain frees here immediately when - // process() has already published past it; otherwise on the next reload/retire/deactivate. + // Prune what is now provably unreachable (same monotone-generation proof as + // reloadInstrument's reclaim). const std::uint64_t seen = processGeneration_.load(std::memory_order_acquire); graveyard_.erase( std::remove_if(graveyard_.begin(), graveyard_.end(), @@ -363,18 +316,16 @@ void ReaSamplerProcessor::retireIdleDrain() { } bool ReaSamplerProcessor::legacyLiftShouldRun() { - // #A terminating guard for the pre-v10 legacy lift. The caller has already established - // refs-empty + intent; this decides whether a lift attempt can MAKE PROGRESS before - // paying for a full reload. Once concluded, the steady state is this one relaxed load — - // no bank read, no parse, no reload churn. + // Terminating guard for the pre-v10 legacy lift (caller has already established + // refs-empty + intent). Once concluded, the steady state is one relaxed load — no bank + // read, no parse, no reload churn. if (legacyLiftConcluded_.load(std::memory_order_relaxed)) return false; const LegacyLiftDecision decision = legacyLiftDecision( bridge_.readReasamplerExtState(kProjExtBanksKey), referencedSampleIds(selectedSampleId(), performanceMap())); if (decision == LegacyLiftDecision::Stale) { - // Provably stale (the bank parses and knows none of the referenced ids): give up - // PERMANENTLY. A later bank change that re-introduces an id bumps the generation, - // and the genChanged reload refreshes the refs without consulting this latch. + // Provably stale: give up permanently. A later bank change that re-introduces an + // id bumps the generation, and genChanged refreshes the refs without this latch. legacyLiftConcluded_.store(true, std::memory_order_relaxed); return false; } @@ -383,21 +334,18 @@ bool ReaSamplerProcessor::legacyLiftShouldRun() { ReaSamplerProcessor::BankSyncResult ReaSamplerProcessor::pollBankSync(bool isFocusedTarget) { - // OFF THE AUDIO THREAD (the editor's UI timer calls this). Both reads allocate and call - // REAPER via the bridge — never invoked from process(). A disconnected bridge (non-REAPER - // host, or before connect) yields nullopt for both reads, so this no-ops cleanly. + // Off the audio thread (editor's UI timer only). A disconnected bridge yields nullopt + // for both reads, so this no-ops cleanly. BankSyncResult result; - // Phase S: park an idle drain snapshot in the graveyard (and prune) on the same UI-timer - // cadence that drives reloads — an edited-away instrument stops costing memory as soon - // as its tails die instead of squatting in the drain slot until the next reload. + // Park an idle drain snapshot in the graveyard on the same cadence that drives + // reloads, so an edited-away instrument stops costing memory as soon as tails die. retireIdleDrain(); - // --- S8: assignment-request consume FIRST ------------------------------------- - // Decode the pending assignment request (nullopt when absent/malformed). Resolve its - // (bankId, sampleId) against the live bank blob: selectSample returns non-nullopt only when - // the sampleId names an existing sample (the reader requirement — an unresolvable pair is - // dropped). Then run the pure consume decision against this instance's persisted marker. + // --- Assignment-request consume first ------------------------------------------- + // Decodes the pending assignment request (nullopt if absent/malformed), resolves its + // sampleId against the live bank blob (an unresolvable pair is dropped), then runs the + // pure consume decision against this instance's persisted marker. std::optional request; if (auto raw = bridge_.readReasamplerExtState(kProjExtAssignKey)) { request = decodeAssignmentRequest(*raw); @@ -405,26 +353,24 @@ ReaSamplerProcessor::pollBankSync(bool isFocusedTarget) { bool resolves = false; if (request) { - // Resolve the assigned sample against the CURRENT bank blob (a fresh read, so a request - // whose sample was rolled back by an extension undo resolves to nullopt -> dropped). + // Resolve against the CURRENT bank blob (a fresh read, so a request whose sample + // was rolled back by an extension undo resolves to nullopt -> dropped). if (auto banksJson = bridge_.readReasamplerExtState(kProjExtBanksKey)) { resolves = selectSample(*banksJson, request->sampleId).has_value(); } } - // Read lastConsumed and conditionally write it back under a single lock scope so there - // is no interleave window between the read and the write (a concurrent getState could - // otherwise observe a stale marker between the two separate lock acquisitions). + // Read + conditionally write lastConsumed under one lock scope so a concurrent + // getState cannot observe a stale marker between two separate acquisitions. std::int64_t lastConsumed = 0; const AssignConsumeDecision decision = [&] { std::lock_guard lock(assignMarkerMutex_); lastConsumed = lastConsumedAssignGeneration_; const AssignConsumeDecision d = consumeDecision(request, lastConsumed, resolves, isFocusedTarget); - // Advance the persisted consumed marker whenever the decision consumed the request - // (applied OR dropped-as-seen). getState will persist it on the next project save so - // a re-open does not re-apply. A non-target instance leaves the marker (decision - // returns it unchanged) so it stays eligible if focus later lands here. + // Advance the persisted marker whenever the decision consumed the request + // (applied or dropped-as-seen); a non-target instance leaves it unchanged so it + // stays eligible if focus later lands here. if (d.consumedGeneration != lastConsumed) { lastConsumedAssignGeneration_ = d.consumedGeneration; } @@ -432,13 +378,12 @@ ReaSamplerProcessor::pollBankSync(bool isFocusedTarget) { }(); if (decision.apply) { - // Apply the assignment as this instance's own selection (the same path a user card-pick - // takes) — the instrument updates its OWN state, never the bank. reloadInstrument below - // rebuilds against the new selection, so skip a redundant reload here. + // Apply as this instance's own selection (the instrument updates its own state, + // never the bank); reloadInstrument below rebuilds against it. setSelectedSampleId(decision.sampleId); - // Zone-bleed fix (3a), peer of the editor's Browse Load: a stale full-range zone - // materialized for the previously loaded sample would shadow the assigned pick under - // first-match resolve. Authored maps (any narrow key range) are untouched. + // Peer of the editor's Browse Load: a stale full-range zone from the previous + // sample would shadow the assigned pick under first-match resolve. Authored maps + // (narrow key ranges) are untouched. PerformanceMap reconciled = performanceMap(); if (reconcileSingleCaptureZones(reconciled, decision.sampleId)) { setPerformanceMap(reconciled); @@ -446,13 +391,10 @@ ReaSamplerProcessor::pollBankSync(bool isFocusedTarget) { result.applied = true; } - // --- S9: bank-generation change-detection ------------------------------------- - // Read the generation stamp; parse (absent/malformed -> 0, the pre-S9 default). FIRST poll - // (lastSeenBankGeneration_ == -1 sentinel): BASELINE the seen value without a reload — - // setState already loaded the instrument from its OWNED refs (pS), so a redundant reload - // on open would only churn. A later - // generation CHANGE (a recapture/ingest/remove, or an undo that lowers it) then drives the - // reload. An assignment we just applied also needs a reload; fold both into ONE (coalesced). + // --- Bank-generation change-detection ------------------------------------------- + // First poll (lastSeenBankGeneration_ == -1 sentinel) baselines without a reload — + // setState already loaded from owned refs, so a redundant reload on open would only + // churn. A later generation change (recapture/ingest/remove/undo) drives the reload. std::int64_t currentGen = kBankGenerationAbsent; if (auto rawGen = bridge_.readReasamplerExtState(kProjExtBankGenKey)) { currentGen = parseBankGeneration(*rawGen); @@ -462,18 +404,12 @@ ReaSamplerProcessor::pollBankSync(bool isFocusedTarget) { !firstPoll && bankGenerationChanged(lastSeenBankGeneration_, currentGen); lastSeenBankGeneration_ = currentGen; - // LEGACY LIFT (pre-v10 blob): the restored state carries intent (a selection or zones) - // but NO owned refs — a pre-pS blob had no path table, so the setState-time reload had - // nothing to decode unless the bank happened to be readable already. Reload on this - // editor tick until the lift lands: reloadInstrument folds the bank blob into the refs - // when readable, after which the table is non-empty and this never fires again (the - // next save is then self-contained). A deliberately-empty instance has no intent and - // never churns; a bank that is not readable YET retries a cheap null publish on the - // editor cadence only. TERMINATING GUARD (#A, legacyLiftShouldRun): once the bank blob - // PARSES and no referenced id resolves in it, the ids are provably stale — there is - // nothing to lift, so the lift concludes permanently instead of churning a full bank - // read + reload every tick forever. This is a MIGRATION convenience for old projects, - // NOT a playback dependency — a v10 blob plays from its refs with no poll at all (pS). + // Legacy lift (pre-v10 blob): restored state carries intent but no owned refs (old + // blobs had no path table). Reload on this tick until reloadInstrument folds the bank + // blob into the refs (after which this never fires again — the next save is + // self-contained). legacyLiftShouldRun concludes permanently once the bank parses and + // no referenced id resolves — a migration convenience only, never a playback + // dependency (a v10 blob plays from its refs with no poll at all). bool legacyLift = false; if (!genChanged && !result.applied && sampleRefs().empty()) { const bool hasIntent = !selectedSampleId().empty() || !performanceMap().empty(); @@ -481,10 +417,9 @@ ReaSamplerProcessor::pollBankSync(bool isFocusedTarget) { } if (genChanged || result.applied || legacyLift) { - reloadInstrument(); // atomic pointer-swap handoff — glitch-free mid-play (S4 graveyard) - // Report the reload distinctly from an S8 apply so the editor re-snapshots its bank - // view. A legacy lift counts only when it actually landed an instrument (otherwise - // every retry tick would churn the editor's caches for nothing). + reloadInstrument(); // atomic pointer-swap handoff — glitch-free mid-play + // Reported distinctly from an applied assignment so the editor re-snapshots its + // bank view; a legacy lift counts only when it actually landed an instrument. result.reloaded = genChanged || (legacyLift && live_.load(std::memory_order_acquire) != nullptr); diff --git a/src/shell/instrument/processor_state.cpp b/src/shell/instrument/processor_state.cpp index 5f8bbe1..ab45a2d 100644 --- a/src/shell/instrument/processor_state.cpp +++ b/src/shell/instrument/processor_state.cpp @@ -1,10 +1,9 @@ -// processor_state.cpp — the ReaSamplerProcessor's COMPONENT-STATE I/O (setState / -// getState against the component_state_io codec) and its UI-thread parameter -// accessors/setters (selection, performance map, channel mode, preview velocity, -// voice-system params, master gain, preview-note mailbox posts). Split out of -// reasampler_processor.cpp (Q-W2v, T4-12). Everything here runs OFF the audio -// thread (UI / host load-save); the setters hand work to the reload family -// (processor_reload.cpp) or store atomics process() picks up at block start. +// processor_state.cpp — ReaSamplerProcessor's component-state I/O (setState/getState +// against the component_state_io codec) and its UI-thread parameter accessors/setters +// (selection, performance map, channel mode, preview velocity, voice-system params, +// master gain, preview-note mailbox posts). Everything here runs off the audio thread; +// setters hand work to the reload family (processor_reload.cpp) or store atomics +// process() picks up at block start. #include "shell/instrument/reasampler_processor.h" @@ -14,8 +13,8 @@ #include "pluginterfaces/base/ibstream.h" -#include "core/instrument/engine/master_gain.h" // masterGainMaxLinear (FB1 post-mixer gain clamp) -#include "core/instrument/map/component_state_io.h" // the ComponentState codec (Q-W2v split) +#include "core/instrument/engine/master_gain.h" // masterGainMaxLinear (post-mixer gain clamp) +#include "core/instrument/map/component_state_io.h" // the ComponentState codec #include "core/instrument/map/sample_map.h" // reconcileSingleCaptureZones / retainRefs / referencedSampleIds using namespace Steinberg; @@ -24,135 +23,107 @@ using namespace Steinberg::Vst; namespace reasampler::vst { using namespace instrument::map; // the codec + resolution vocabulary this TU marshals -using instrument::engine::masterGainMaxLinear; // FB1 taper ceiling (Q-W6: shim retired) +using instrument::engine::masterGainMaxLinear; // taper ceiling tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) { if (!state) return kResultFalse; - // Read the whole component-state blob (the performance map, versioned). The blob is - // small; read in one shot into a growable buffer. + // The blob is small; read it in one shot into a growable buffer. std::vector bytes; std::uint8_t chunk[256]; int32 got = 0; while (state->read(chunk, sizeof(chunk), &got) == kResultOk && got > 0) { bytes.insert(bytes.end(), chunk, chunk + got); } - // Component state (v3, S10) is {single-capture selection id, opt-in zones}. The - // selection and the zones are DISTINCT — the default face is one picked capture, zones - // are a demoted overlay — so both are restored explicitly (no more inferring a selection - // from a lone zone). deserializeComponentState lifts older blobs cleanly: a v2 zones-only - // blob restores {"", zones}; a v1 S4 single-selection blob restores {id, one-zone map} so - // the old pick survives as both; an empty/unknown blob restores {"", no zones} — the S10 - // silent empty state (no first-sample fallback in reloadInstrument). - // Pass sampleRate_ as the project rate for legacy v3 blob conversion (frames -> seconds at - // the v3 read boundary). sampleRate_ is set by setupProcessing; REAPER calls setupProcessing - // before setState on project load, so sampleRate_ is the real host rate here. A v3 blob on a - // pre-setup call would assert inside readZonesPayload (a programming error, not a field case). + // Component state is {single-capture selection id, opt-in zones}, restored explicitly + // since they're distinct (default face vs. a demoted overlay). deserializeComponentState + // lifts older blobs cleanly (no first-sample fallback in reloadInstrument). sampleRate_ + // is the real host rate here — REAPER calls setupProcessing before setState on load. const ComponentState cs = deserializeComponentState(bytes, sampleRate_); setSelectedSampleId(cs.selectionId); - // Zone-bleed fix (3a) heal-on-load: a blob saved under the pre-fix editor may carry a - // pile of stale full-range zones (one per sample ever browsed), the oldest shadowing the - // saved selection under first-match resolve. Reconciling here restores "the sample the - // editor shows is the sample the engine plays" for already-affected projects; authored - // Zone-view maps (any narrow key range) pass through untouched. + // Heal-on-load: a blob saved under the pre-fix editor may carry stale full-range zones + // (one per sample ever browsed), the oldest shadowing the saved selection under + // first-match resolve. Authored Zone-view maps (narrow key ranges) pass through untouched. PerformanceMap restored = cs.map; - reconcileSingleCaptureZones(restored, cs.selectionId); // bool return ignored: setPerformanceMap + reloadInstrument run unconditionally on load + reconcileSingleCaptureZones(restored, cs.selectionId); // bool return ignored: reload below runs unconditionally setPerformanceMap(restored); - // S8: restore the last-consumed assignment generation so a re-open does not re-apply a - // stale assign_request (the user may have manually changed the selection after the assign). + // Restore the last-consumed assignment generation so a re-open does not re-apply a + // stale assign_request. { std::lock_guard lock(assignMarkerMutex_); lastConsumedAssignGeneration_ = cs.lastConsumedAssignGeneration; } - // Restore the S7 channel mode + the GA explicit flag. The output bus is FIXED stereo (see - // initialize) — the mode only governs how the reload below decodes, so no bus work here. + // The output bus is fixed stereo (see initialize) — the mode only governs decode below. { std::lock_guard lock(channelModeMutex_); channelMode_ = cs.channelMode; channelModeExplicit_ = cs.channelModeExplicit; } - // S-VIEW-4: restore the per-instance preview velocity. Guarded by previewMutex_ — since Wave 2 - // the editor's velocity knob is a concurrent UI-thread writer. { std::lock_guard lock(previewMutex_); previewVelocity_ = cs.previewVelocity; } - // Phase S: restore the voice-system parameters (v7; older blobs lift to {16, Poly, - // Retrigger} in deserializeComponentState — pre-Phase-S behavior). Restored BEFORE the - // reload below so the rebuilt engine is born with the saved polyphony/mode. + // Restore before the reload so the rebuilt engine is born with the saved polyphony/mode. { std::lock_guard lock(voiceParamsMutex_); voiceCount_ = cs.voiceCount; voiceMode_ = cs.voiceMode; monoTrigger_ = cs.monoTrigger; } - // FB1: restore the post-mixer master gain (v8; older blobs lift to unity in - // deserializeComponentState — pre-FB1 output). One atomic store; the audio thread picks - // it up at the next block start. setMasterGainLinear(cs.masterGainLinear); - // pS self-contained playback: restore the instance-OWNED sample refs (v10) BEFORE the - // reload so it decodes straight from them — no bank read required to play. A pre-v10 - // blob lifts to an EMPTY table; the reload then resolves nothing until the bank blob - // becomes readable (the reload's opportunistic refresh, or pollBankSync's legacy lift), - // after which the next save is self-contained. + // Restore the instance-owned sample refs before the reload so it decodes straight from + // them — no bank read required. A pre-v10 blob lifts to an empty table; the reload + // resolves nothing until the bank blob becomes readable (opportunistic refresh, or + // pollBankSync's legacy lift), after which the next save is self-contained. { std::lock_guard lock(refsMutex_); sampleRefs_ = cs.sampleRefs; } - // pS-usage: restore the persisted publish identity (v11; pre-v11 lifts to empty — - // minted on first publish). usageNonce_ resets: a restored blob is a NEW LIFETIME - // for the copy-collision analysis (the fresh nonce means this incarnation can never - // be mistaken for the previous one's writes — or for a copy-sibling's). + // Restore the publish identity (pre-v11 lifts to empty, minted on first publish). + // usageNonce_ resets: a restored blob is a new lifetime, so this incarnation can never + // be mistaken for the previous one's writes or a copy-sibling's. { std::lock_guard lock(usageMutex_); instanceGuid_ = cs.instanceGuid; usageNonce_.clear(); } - // A new blob is new facts: a staleness proof latched against the PREVIOUS state does - // not carry over (#A — the legacy lift gets one fresh run per restored state). + // A new blob is new facts — the legacy lift gets one fresh run per restored state. legacyLiftConcluded_.store(false, std::memory_order_relaxed); - // Rebuild from the restored state (off-thread — setState is a load-time call). reloadInstrument(); return kResultOk; } tresult PLUGIN_API ReaSamplerProcessor::getState(IBStream* state) { if (!state) return kResultFalse; - // Persist the full instance state (v3, S10): the single-capture selection id AND the - // opt-in zones — the instrument's own state (D-B), NEVER written to the "reasampler" - // bank ext-state. An instance with no pick and no zones serializes to {"", no zones} - // and restores as the S10 empty state (silence + "pick a capture"), never auto-playing - // sample #1. + // Persists the full instance state — never written to the "reasampler" bank ext-state. + // No pick + no zones serializes to {"", no zones}, restoring as silence (never + // auto-playing sample #1). ComponentState state_out; state_out.selectionId = selectedSampleId(); state_out.map = performanceMap(); { - // S7: persist the per-instance mono/stereo decode mode + the GA explicit flag (v9). std::lock_guard lock(channelModeMutex_); state_out.channelMode = channelMode_; state_out.channelModeExplicit = channelModeExplicit_; } { std::lock_guard lock(assignMarkerMutex_); - state_out.lastConsumedAssignGeneration = lastConsumedAssignGeneration_; // S8 reader marker + state_out.lastConsumedAssignGeneration = lastConsumedAssignGeneration_; } - state_out.previewVelocity = previewVelocity(); // S-VIEW-4: persist the preview strike velocity + state_out.previewVelocity = previewVelocity(); { - // Phase S: persist the voice-system parameters (component state v7). std::lock_guard lock(voiceParamsMutex_); state_out.voiceCount = voiceCount_; state_out.voiceMode = voiceMode_; state_out.monoTrigger = monoTrigger_; } - state_out.masterGainLinear = masterGainLinear(); // FB1: persist the post-mixer gain (v8) - // pS: persist the OWNED sample refs (v10) — the saved blob carries everything needed to - // decode + play with no extension present. Filtered (on the snapshot copy, the member is - // untouched) to exactly what the instance currently plays, so the table cannot grow with - // browsing history. + state_out.masterGainLinear = masterGainLinear(); + // Persist the owned sample refs — the saved blob decodes + plays with no extension + // present. Filtered (snapshot copy only) to what the instance currently plays, so the + // table cannot grow with browsing history. state_out.sampleRefs = sampleRefs(); retainRefs(state_out.sampleRefs, referencedSampleIds(state_out.selectionId, state_out.map)); - // pS-usage: persist the publish identity (v11) so the instance's usage key is - // stable across sessions (records do not proliferate per reopen). + // Persist the publish identity so the usage key is stable across sessions. { std::lock_guard lock(usageMutex_); state_out.instanceGuid = instanceGuid_; @@ -202,8 +173,7 @@ std::uint8_t ReaSamplerProcessor::previewVelocity() { } void ReaSamplerProcessor::setPreviewVelocity(std::uint8_t velocity) { - // Clamp to the MIDI-note range [1,127] (0 would be a note-off by convention — a preview - // strike must sound). The editor's knob maps its 0..1 domain into this range before calling. + // Clamp to [1,127] — 0 would be a note-off by convention, and a preview strike must sound. if (velocity < 1) velocity = 1; if (velocity > 127) velocity = 127; std::lock_guard lock(previewMutex_); @@ -216,8 +186,8 @@ int ReaSamplerProcessor::voiceCount() { } void ReaSamplerProcessor::setVoiceCount(int count) { - // Clamp to the shared pure-core range so the engine, the state bytes, and the editor's - // control can never disagree about the legal polyphony span. + // Clamp to the shared pure-core range so the engine, state bytes, and editor control + // can never disagree about the legal polyphony span. if (count < kMinVoiceCount) count = kMinVoiceCount; if (count > kMaxVoiceCount) count = kMaxVoiceCount; { @@ -225,11 +195,8 @@ void ReaSamplerProcessor::setVoiceCount(int count) { if (voiceCount_ == count) return; // no-op: don't churn a rebuild voiceCount_ = count; } - // LIGHT rebuild OFF-thread through the drain-slot swap: the engine is reconstructed from - // the already-decoded keymap (no bridge re-read, no WAV re-decode — a polyphony change - // touches no audio data) and the displaced instrument keeps rendering its ringing tails, - // so a voice-param change never cuts a sounding note NOR stalls the UI re-decoding every - // zone from disk. Same contract for the mode/trigger setters below. + // Light rebuild through the drain-slot swap (no bridge re-read, no WAV re-decode) so a + // voice-param change never cuts a sounding tail. Same contract below. rebuildVoiceEngine(); } @@ -262,9 +229,8 @@ void ReaSamplerProcessor::setMonoTrigger(MonoTrigger trigger) { } void ReaSamplerProcessor::setMasterGainLinear(double linear) { - // Clamp to the control's legal span (the master_gain taper: 0 = -inf/silence, cap = - // +24 dB). One relaxed atomic store — the audio thread reads it at the next block start; - // no rebuild, no lock (a post-sum output trim is not a keymap fact). + // Clamp to the master_gain taper (0 = silence, cap = +24 dB). One relaxed atomic + // store — no rebuild, no lock (a post-sum trim is not a keymap fact). if (!(linear >= 0.0)) linear = 0.0; // also catches NaN const double maxLin = masterGainMaxLinear(); if (linear > maxLin) linear = maxLin; @@ -275,9 +241,8 @@ void ReaSamplerProcessor::previewNoteOn(int note) { if (note < 0) note = 0; if (note > 127) note = 127; const std::uint8_t vel = previewVelocity(); // latch the current knob value into the request - // Advance the sequence (wrapping; process compares for inequality, so a wrap is harmless as - // long as we never land back on the exact value the audio thread last consumed in one step — - // 16 bits gives 65535 posts between collisions, unreachable at UI-click rates). + // Advance the sequence (wrapping; process compares for inequality — 16 bits gives 65535 + // posts between collisions, unreachable at UI-click rates). const std::uint16_t seq = ++previewOnSeq_ == 0 ? ++previewOnSeq_ : previewOnSeq_; const std::uint32_t packed = (static_cast(seq) << 16) | (static_cast(vel) << 8) | @@ -297,15 +262,14 @@ void ReaSamplerProcessor::previewNoteOff(int note) { void ReaSamplerProcessor::setChannelMode(ChannelMode mode) { { std::lock_guard lock(channelModeMutex_); - // The editor toggle is a DELIBERATE choice either way: latch explicit even on a - // same-mode click (the user confirmed the mode; the GA auto-default stops fighting it). + // A deliberate choice either way: latch explicit even on a same-mode click so + // auto-default stops fighting it. channelModeExplicit_ = true; if (channelMode_ == mode) return; // no decode change: don't churn a reload channelMode_ = mode; } - // The DECODE policy changed. The output bus is FIXED stereo (GA fix — no bus repoint, no - // restartComponent): reloading re-decodes the loaded WAV(s) under the new mode off-thread - // (mono = downmix, stereo = L/R split) and the RT path just keeps rendering. + // The output bus is fixed stereo (no bus repoint): reloading re-decodes off-thread + // under the new mode and the RT path just keeps rendering. reloadInstrument(); } diff --git a/src/shell/instrument/reaper_bridge.cpp b/src/shell/instrument/reaper_bridge.cpp index 92062df..6415d31 100644 --- a/src/shell/instrument/reaper_bridge.cpp +++ b/src/shell/instrument/reaper_bridge.cpp @@ -5,41 +5,33 @@ #include #include "core/instrument/map/bridge_marshal.h" -#include "core/wire/ext_state_read.h" // readProjExtStateGrowing (T2-04 grow-loop policy) -#include "core/capture/capture_paths.h" // projectDirOfRpp (shared M4 project-dir derivation) +#include "core/wire/ext_state_read.h" // readProjExtStateGrowing (grow-loop policy) +#include "core/capture/capture_paths.h" // projectDirOfRpp (shared project-dir derivation) #include "ext_keys.h" // kProjExtNamespace (shared wire contract) -// The VST3 base types must be included before REAPER's VST3 interface header, which -// uses FUnknown / CStringA / uint32 / DECLARE_CLASS_IID / PLUGIN_API from -// pluginterfaces/base — all in namespace Steinberg. +// VST3 base types must be included before REAPER's VST3 interface header, which uses +// unqualified Steinberg types (FUnknown, CStringA, uint32, DECLARE_CLASS_IID, PLUGIN_API). #include "pluginterfaces/base/funknown.h" #include "pluginterfaces/base/ftypes.h" -// REAPER's VST3-side bridge interface (vendored). IReaperHostApplication is what REAPER -// passes (as an IHostApplication) to IComponent::initialize; it exposes getReaperApi -// (resolve-by-name) and getReaperParent (host context). The header uses UNQUALIFIED -// Steinberg types (FUnknown, CStringA, uint32, FUID, DECLARE_CLASS_IID, PLUGIN_API), so -// it must be pulled into the Steinberg namespace — the same way REAPER's own VST3 -// examples include it. +// REAPER's VST3-side bridge interface (vendored): IReaperHostApplication is the +// IHostApplication REAPER passes to IComponent::initialize, exposing getReaperApi +// (resolve-by-name) and getReaperParent (host context). Pulled into namespace Steinberg +// (the header's unqualified types), the same way REAPER's own VST3 examples include it. namespace Steinberg { #include "reaper_vst3_interfaces.h" } // namespace Steinberg -// DECLARE_CLASS_IID in the REAPER header only DECLARES IReaperHostApplication::iid; some -// TU must DEFINE it. We do it here — this is the only place that queries for the -// interface (FUnknownPtr uses the iid), so the definition lives with its sole use. +// DECLARE_CLASS_IID in the REAPER header only declares the iid; this is the only TU that +// queries for the interface, so the DEFINE lives with its sole use. DEF_CLASS_IID(Steinberg::IReaperHostApplication) -// The ext-state namespace is the SHARED wire contract between the extension (writer) -// and this instrument (reader); it lives in ext_keys.h (pure, REAPER-free) — -// reasampler::kProjExtNamespace() — so the two artifacts read one symbol and cannot -// drift. Channel-derived (Phase V, V4): the accessor returns "reasampler" (stable) or -// "reasampler_beta" (beta), matching whatever the extension wrote. The S1 spike -// duplicated it locally; that duplication is retired. +// The ext-state namespace is the shared wire contract with the extension — ext_keys.h's +// kProjExtNamespace() (pure, REAPER-free), channel-derived so both artifacts read one +// symbol and cannot drift. namespace reasampler::vst { -// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired). using capture::projectDirOfRpp; using instrument::map::decodeGetProjExtState; @@ -53,27 +45,24 @@ bool ReaperBridge::connect(Steinberg::FUnknown* context) { hostApp_ = nullptr; if (!context) return false; - // Query the host context for REAPER's bridge interface. In a non-REAPER host this - // query fails and we stay unconnected — the instrument still loads. + // In a non-REAPER host this query fails and we stay unconnected — the instrument + // still loads. Steinberg::FUnknownPtr reaper(context); if (!reaper) return false; hostApp_ = reaper.get(); - // Resolve the ext-state functions by name. getReaperApi returns the same function - // pointers the extension resolves via rec->GetFunc; a null return means the symbol - // is unavailable (very old REAPER) — degrade gracefully. + // getReaperApi returns the same function pointers the extension resolves via + // rec->GetFunc; a null return means the symbol is unavailable (very old REAPER). getProjExtState_ = reinterpret_cast( reaper->getReaperApi("GetProjExtState")); enumProjExtState_ = reinterpret_cast( reaper->getReaperApi("EnumProjExtState")); - // EnumProjects(-1, ...) yields the active project AND its .rpp path — the same call - // the persist shell (ext_state_io.cpp) uses, so the instrument derives the project - // directory identically. + // EnumProjects(-1, ...) yields the active project + its .rpp path — same convention + // the persist shell uses, so the instrument derives the project directory identically. enumProjects_ = reinterpret_cast( reaper->getReaperApi("EnumProjects")); - // pS-usage: the (prefix-guarded) usage publish write + the track-identity pair the - // usage record stamps. All degrade to null gracefully — an old REAPER just never - // publishes usage (the extension then protects by bank references only). + // The (prefix-guarded) usage publish write + the track-identity pair it stamps. All + // degrade to null gracefully — an old REAPER never publishes usage. setProjExtState_ = reinterpret_cast( reaper->getReaperApi("SetProjExtState")); getTrackGuid_ = reinterpret_cast( @@ -87,23 +76,16 @@ bool ReaperBridge::connect(Steinberg::FUnknown* context) { std::optional ReaperBridge::readReasamplerExtState(const std::string& key) { if (!getProjExtState_ || !hostApp_) return std::nullopt; - // Fetch the host project (getReaperParent(3) — project). Reads that live "reasampler" - // ext-state against the ACTIVE project the instrument was instantiated in, so it - // follows project switches for free (D6). + // getReaperParent(3) reads the live "reasampler" ext-state against the active project + // the instrument was instantiated in, so it follows project switches for free. A null + // project is legitimate (REAPER treats it as the current project) — pass it through + // rather than bailing; a fruitless read still yields nullopt to the caller. auto* reaper = static_cast(hostApp_); void* proj = reaper->getReaperParent(3); - // A null project is legitimate (e.g. instantiated before a project context exists); - // REAPER treats null as the current project for these calls, so we pass it through - // rather than bailing — but if the read yields nothing the caller sees nullopt. - // GetProjExtState writes into a caller buffer; the bank blob can be large (many - // samples), so grow the buffer until the value fits rather than risk a silent - // truncation. The retry policy is the SHARED pure wire::readProjExtStateGrowing - // (T2-04 — one loop for the - // extension's persist/usage reads and this bridge read; the rules cannot drift): - // absent (rv <= 0) and the >16 MB ceiling both fold to nullopt here, and a - // complete value still runs through decodeGetProjExtState (the stale/empty-buffer - // guard) exactly as before. + // The bank blob can be large, so grow the buffer until it fits rather than risk a + // silent truncation. The shared wire::readProjExtStateGrowing loop keeps this bridge + // read and the extension's persist/usage reads from drifting. const auto read = wire::readProjExtStateGrowing( [&](char* buf, int cap) { return getProjExtState_(proj, kProjExtNamespace(), key.c_str(), buf, cap); @@ -116,24 +98,21 @@ std::optional ReaperBridge::readReasamplerExtState(const std::strin bool ReaperBridge::writeUsageExtState(const std::string& usageKey, const std::string& value) { if (!setProjExtState_ || !hostApp_) return false; - // STRUCTURAL read-only-bank guard: this module writes usage keys and nothing else. - // A non-"rsusage_" key is a programming error upstream — refuse rather than widen - // the instrument's write surface (banks/view/tail/assign stay extension-owned). + // Read-only-bank guard: this module writes usage keys and nothing else. A non- + // "rsusage_" key is refused rather than widening the instrument's write surface + // (banks/view/tail/assign stay extension-owned). const std::string prefix = kProjExtUsageKeyPrefix; if (usageKey.compare(0, prefix.size(), prefix) != 0) return false; auto* reaper = static_cast(hostApp_); void* proj = reaper->getReaperParent(3); // null = current project (same as reads) - // SetProjExtState returns "the size of the state for this extname" (SDK ~6288) — - // after storing our non-empty value the namespace state is necessarily > 0, so a - // <= 0 return means the write did not land. Reported to the caller (the publish - // path retries on the next reload tick); a silently-dropped record would leave the - // instance's holds unprotected. + // SetProjExtState returns the size of the extname's state — after storing a + // non-empty value that's necessarily > 0, so <= 0 means the write did not land (the + // publish path retries next reload tick; a silent drop would leave holds unprotected). const int rv = setProjExtState_(proj, kProjExtNamespace(), usageKey.c_str(), value.c_str()); - // Deliberately NO MarkProjectDirty: a usage change always accompanies a component- - // state change that already dirties the project; an idempotent load-time republish - // must not flag an untouched project as modified. + // Deliberately NO MarkProjectDirty: a usage change always rides a component-state + // change that already dirties the project. return rv > 0; } @@ -151,11 +130,8 @@ std::string ReaperBridge::currentTrackGuid() { std::string ReaperBridge::activeProjectDir() { if (!enumProjects_) return {}; - // idx=-1 is the current project tab; the out-buffer receives the full .rpp path, - // EMPTY for a never-saved project. Same call + convention as the persist shell; the pure - // projectDirOfRpp turns the .rpp path into the project directory (parent, forward- - // slashed) and keeps an unsaved project's empty path empty (no default-location - // fallback — the tool's invariant). + // idx=-1 is the current project tab; the out-buffer is empty for a never-saved + // project. projectDirOfRpp keeps that empty (no default-location fallback). std::vector buf(4096, '\0'); enumProjects_(-1, buf.data(), static_cast(buf.size())); return projectDirOfRpp(std::string(buf.data())); diff --git a/src/shell/instrument/reaper_bridge.h b/src/shell/instrument/reaper_bridge.h index 29bf057..5d14bc9 100644 --- a/src/shell/instrument/reaper_bridge.h +++ b/src/shell/instrument/reaper_bridge.h @@ -1,19 +1,11 @@ -// reaper_bridge.h — the REAPER VST-host bridge (Phase S1 read spike). THIN shell: -// resolves REAPER API functions by name over the host context and reads the live -// "reasampler" project ext-state. The fiddly decode lives in bridge_marshal (pure). +// reaper_bridge.h — the REAPER VST-host bridge. Thin shell: resolves REAPER API functions +// by name over the host context and reads the live "reasampler" project ext-state. The +// fiddly decode lives in bridge_marshal (pure). // -// VERIFIED BRIDGE MECHANISM (corrects §1a's estimate). §1a described the VST2-style -// hostcb opcode pattern (hostcb(&effect, 0xdeadbeef, 0xdeadf00d, ...)). That is the -// VST2 path (video_processor.h documents it for a VST2 aEffect). For a VST3 plugin the -// bridge is exposed differently and more cleanly: REAPER passes an IHostApplication as -// the `context` to IComponent::initialize(FUnknown* context); querying it for -// IReaperHostApplication (vendor/reaper-sdk/sdk/reaper_vst3_interfaces.h) yields: -// * getReaperApi(funcname) -> resolve a REAPER API function pointer by name -// (the VST3 equivalent of opcode 0xdeadf00d), and -// * getReaperParent(3) -> the host ReaProject* (the VST3 equivalent of the -// 0xdeadf00e host-context fetch; 1=track, 2=take, 3=project, 4=fxdsp, 5=trackchan). -// So a VST3 uses IReaperHostApplication, not the raw hostcb opcodes. Verified against -// reaper_vst3_interfaces.h + reaper_plugin_functions.h at the spike. +// Bridge mechanism: REAPER passes an IHostApplication as `context` to +// IComponent::initialize; querying it for IReaperHostApplication yields getReaperApi +// (resolve a REAPER API function pointer by name) and getReaperParent(3) (the host +// ReaProject*; 1=track, 2=take, 3=project, 4=fxdsp, 5=trackchan) — not VST2 hostcb opcodes. #pragma once @@ -32,48 +24,39 @@ class ReaperBridge { public: ReaperBridge() = default; - // Bind to the host. `context` is the FUnknown* REAPER hands IComponent::initialize. - // Returns true when the REAPER bridge is available (host is REAPER and the ext-state - // API resolved). Safe to call with a null or non-REAPER context — returns false. + // Binds to the host (`context` is the FUnknown* IComponent::initialize hands us). + // Returns true when the host is REAPER and the ext-state API resolved; safe to call + // with a null or non-REAPER context (returns false). bool connect(Steinberg::FUnknown* context); - // True once connect() found the REAPER host application AND resolved the ext-state - // functions. bool isConnected() const { return getProjExtState_ != nullptr; } - // Read a "reasampler" ext-state value by key from the host's active project. - // Returns nullopt when unconnected, when the project can't be resolved, or when the - // key is absent. This is the S1 read-spike entry point. + // Reads a "reasampler" ext-state value by key from the host's active project. + // Returns nullopt when unconnected, unresolvable, or the key is absent. // - // NOT REAL-TIME SAFE (it allocates a read buffer and calls into REAPER): callers on - // the audio thread MUST NOT invoke it. The S4 instrument reads on the main/UI thread - // and hands a snapshot to the process path (see reasampler_processor.cpp). + // NOT REAL-TIME SAFE (allocates + calls into REAPER): audio-thread callers MUST NOT + // invoke this. The instrument reads on the main/UI thread and hands a snapshot to + // the process path. std::optional readReasamplerExtState(const std::string& key); - // The active project's directory (the folder holding its .rpp), forward-slashed, - // no trailing slash — the M4 convention persist uses to place the bank alongside - // the .rpp. Empty for an unsaved project or when unconnected. The instrument - // resolves relative sample paths against this the SAME way persist does - // (capture_paths::projectDirOfRpp over EnumProjects(-1)'s .rpp path). Not RT-safe. + // The active project's directory (forward-slashed, no trailing slash) — the same + // convention persist uses to place the bank alongside the .rpp. Empty for an unsaved + // project or when unconnected. Not RT-safe. std::string activeProjectDir(); - // Write THIS INSTANCE's usage record (pS-usage): the ONE sanctioned instrument-side - // ext-state write. `usageKey` MUST carry the "rsusage_" prefix (ext_keys.h's - // usageKeyFor) — any other key is REFUSED here, so the read-only-BANK invariant is - // enforced structurally: this module can publish the instance's own usage and - // nothing else (banks/view/tail/assign remain unwritable from the instrument). - // Returns true iff written (the SetProjExtState return is checked — a dropped - // write must not silently claim protection). NOT RT-safe (calls into REAPER) — - // publish sites are the off-audio-thread reload path only. Deliberately does NOT - // mark the project dirty: a usage change always rides a component-state change - // that already does. + // Writes THIS INSTANCE's usage record: the ONE sanctioned instrument-side ext-state + // write. `usageKey` MUST carry the "rsusage_" prefix (ext_keys.h's usageKeyFor); any + // other key is refused, enforcing the read-only-bank invariant structurally (banks/ + // view/tail/assign stay unwritable from the instrument). Returns true iff written + // (the SetProjExtState return is checked). NOT RT-safe — publish sites are the + // off-audio-thread reload path only. Deliberately does NOT mark the project dirty: a + // usage change always rides a component-state change that already does. bool writeUsageExtState(const std::string& usageKey, const std::string& value); - // The canonical "{XXXXXXXX-...}" GUID string of the track hosting this FX instance - // (getReaperParent(1) -> GetTrackGUID -> guidToString — the same rendering as the - // extension's track_guid::guidString, so usage records and the extension's live-FX - // enumeration compare byte-equal). Empty when unconnected or no track context (the - // usage reader then falls back to any-instance liveness — fail-safe). Not RT-safe. + // The canonical GUID string of the track hosting this FX instance (same rendering as + // the extension's track_guid::guidString, so usage records compare byte-equal + // against its live-FX enumeration). Empty when unconnected or no track context (the + // usage reader then falls back to any-instance liveness). Not RT-safe. std::string currentTrackGuid(); private: @@ -84,18 +67,14 @@ private: using EnumProjExtStateFn = bool (*)(void* proj, const char* extname, int idx, char* keyOut, int keyOut_sz, char* valOut, int valOut_sz); - // EnumProjects(-1, projfnOut, sz) -> active project + its .rpp path (SDK line - // ~1264). The instrument uses idx=-1 (current tab) so it follows the active project, - // and reads the .rpp path from the out-buffer exactly as the persist shell - // (ext_state_io.cpp) does. + // EnumProjects(-1, projfnOut, sz) -> active project + its .rpp path. idx=-1 (current + // tab) follows the active project, same convention as the persist shell. using EnumProjectsFn = void* (*)(int idx, char* projfnOut, int projfnOut_sz); - // SetProjExtState(proj, extname, key, value) -> int (SDK line ~6290). Used ONLY by - // writeUsageExtState (prefix-guarded) — see the read-only-bank note there. + // Used ONLY by writeUsageExtState (prefix-guarded) — see the read-only-bank note there. using SetProjExtStateFn = int (*)(void* proj, const char* extname, const char* key, const char* value); - // GetTrackGUID(MediaTrack*) -> GUID* (SDK ~3562) + guidToString(const GUID*, char* - // destNeed64) (SDK ~3848). Both held as opaque-pointer signatures so the header - // stays SDK-type-free; the GUID* is passed straight through, never dereferenced here. + // Opaque-pointer signatures so the header stays SDK-type-free; the GUID* is passed + // straight through, never dereferenced here. using GetTrackGuidFn = void* (*)(void* tr); using GuidToStringFn = void (*)(const void* g, char* destNeed64); diff --git a/src/shell/instrument/reasampler_editor.h b/src/shell/instrument/reasampler_editor.h index 3de98fd..73b2416 100644 --- a/src/shell/instrument/reasampler_editor.h +++ b/src/shell/instrument/reasampler_editor.h @@ -1,25 +1,8 @@ -// 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 reloadInstrument (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. +// 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 +// (capture_browser, keyboard_strip, sample_map) — default face is the capture browser, then +// single-capture setup, with an opt-in zones panel. All layout/hit-test/drag math lives in +// the pure modules; every edit commits off the audio thread via reloadInstrument. #pragma once @@ -30,13 +13,13 @@ #include "public.sdk/source/common/pluginview.h" -#include "core/instrument/ui/editor_geometry.h" // Rect (the shell's sub-rect type, shared with the pure modules) -#include "core/instrument/ui/envelope_edit.h" // EnvClampBounds / NodeHit (S-VIEW-3 envelope node hit-test/edit) -#include "core/instrument/ui/envelope_overlay.h" // AmpEnvelope / EnvNode (S-VIEW-3 envelope overlay draw seam) -#include "core/instrument/ui/knob_deck.h" // DeckGroupDesc / DeckLayout (r11 knob deck — Sample FB1, Zone FB2) +#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 (Sample + Zone knob deck) #include "core/audio/peaks.h" // Envelope (the cached peak thumbnail) #include "core/instrument/map/sample_map.h" // SampleChoice, BankChoice, PerformanceMap (the shell's snapshot) -#include "core/instrument/engine/velocity_curve.h" // VelocityCurve (S-VIEW-10 transfer-curve editor state) +#include "core/instrument/engine/velocity_curve.h" // VelocityCurve (transfer-curve editor state) #ifdef _WIN32 #include @@ -46,10 +29,6 @@ class LICE_IBitmap; // fwd: the paint helpers take one; lice.h is included only namespace reasampler::vst { -// Cross-subsystem deps by their real namespace homes (Q-W2v: the core/namespaces.h shim -// is retired from the editor family; engine symbols — ChannelMode, VoiceMode, MonoTrigger, -// the voice-count constants, VelocityCurve via the engine re-export — stay in flat -// `reasampler` and resolve via the enclosing namespace). using audio::AudioSample; using audio::Envelope; using instrument::map::BankChoice; @@ -69,9 +48,9 @@ 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). + // `processor` outlives this editor; 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; @@ -86,67 +65,51 @@ protected: 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. + // Sample is the home/default face. Browse is a full-window modal picker overlaid on + // Sample. Zone is the dedicated multi-zone keymap surface, button-summoned. 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_); kCurveNode is a draggable velocity-curve control point in the S-VIEW-10 - // transfer-curve editor (which point in curvePointIndex_); kDeckKnob is a GRAB-ANCHORED - // vertical radial-knob drag on an r11 knob deck — the Sample face's deck/cluster (FB1) - // or the Zone panel's per-zone deck (FB2) — (which control in dragParamId_; the value at - // grab in dragKnobStartValue_ — no jump on grab, FA4). + // 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, kZoneLow, kZoneHigh, kZoneBody, kWaveMarker, kScrollThumb, kEnvNode, kCurveNode, kDeckKnob }; - // The parameter controls on the setup surface (S12 AHDSR + the S15/S16 control surfaces). - // The int value is the opaque control id the pure knob_deck hit-test returns; the shell - // maps it to the picked zone's play params (or a processor-side per-instance setter). + // 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 zone's play params or a + // processor-side per-instance setter. enum class ParamControl { - kPlayMode = 0, // Gate | Trigger toggle (S15) - kPitchEngine, // Varispeed | Preserve toggle (S16) + kPlayMode = 0, // Gate | Trigger toggle + kPitchEngine, // Varispeed | Preserve toggle kAttack, // AHDSR attack (Gate) / — - kHold, // AHDSR hold (Gate, S15) + kHold, // AHDSR hold (Gate) 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) - // r11 deck-only controls (FB1): processor-side per-instance params, NOT zone params — - // routed to the processor setters, never through applyZoneControl / the map. - kVoiceCount, // Phase S polyphony bound (1..32) — a stepped knob in the VOICE group + kTrigLength, // Trigger %-length + kTrigFadeIn, // Trigger fade-in + kTrigFadeOut, // Trigger fade-out + kPitchEnvEnable, // AD pitch envelope on|off + kPitchEnvAttack, // AD pitch attack + kPitchEnvDecay, // AD pitch decay + kPitchEnvDepth, // AD pitch depth in +/- semitones + kKeyTrack, // key-tracking 0..200% (lives on PerformanceZone, not ZonePlaySeconds) + // Deck-only controls: processor-side per-instance params, NOT zone params — routed to + // the processor setters, never through applyZoneControl / the map. + kVoiceCount, // polyphony bound (1..32) — a stepped knob in the VOICE group kVoiceMode, // Poly | Mono caption toggle (VOICE group) kMonoTrigger, // Retrig | Legato row toggle (VOICE group; live only in Mono) - kMasterGain, // FB1 post-mixer master gain knob (-inf..+24 dB taper, MASTER group) + kMasterGain, // post-mixer master gain knob (-inf..+24 dB taper, MASTER group) 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. + // The waveform markers on the single-capture setup surface: start-point + the sustain + // loop's two ends, in draw + hit order. 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. + // 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 Sample-view "Browse" title-band button (opens the Browse modal) @@ -163,10 +126,10 @@ private: kAddZone, // the "+ Add Zone" button kDeleteZone, // the "Delete" zone button kControl, // a knob-deck element (index = control id) - kCurveNode, // a velocity-curve control point (index = point index, S-VIEW-10) - kVelKnob, // the cluster preview-velocity radial knob (r11) - kCurveButton, // the cluster mini curve-preview button (r11 — opens the popup) - kPopupClose, // the curve popup's Close (x) button (r11) + kCurveNode, // a velocity-curve control point (index = point index) + kVelKnob, // the cluster preview-velocity radial knob + kCurveButton, // the cluster mini curve-preview button (opens the popup) + kPopupClose, // the curve popup's Close (x) button }; struct HoverTarget { HoverKind kind = HoverKind::kNone; @@ -177,96 +140,72 @@ private: #ifdef _WIN32 void paint(HDC hdc); - void paintSample(LICE_IBitmap* bmp, int w, int h); // S-VIEW-2/r11 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 paintSample(LICE_IBitmap* bmp, int w, int h); // home face + void paintBrowse(LICE_IBitmap* bmp, int w, int h); // modal picker overlay + void paintZone(LICE_IBitmap* bmp, int w, int h); // zone surface void paintEmptyState(LICE_IBitmap* bmp, const Rect& area); - // --- r11 knob-deck rendering (FB1 Sample face; FB2 Zone panel) ------------------- - // The knob deck: the fenced task groups drawn through the L1 kit — group fence + caption + - // compact caption toggles + radial knobs (param_slider's FA4 primitive) with label<->value - // swap on hover/drag. `descs` picks the group set: the full Sample deck (deckGroupDescs) - // or the Zone panel's per-zone groups (zoneDeckGroupDescs). Lays out from deckArea's - // top-left; the caller anchors (Sample bottom-anchors, Zone top-anchors). + // The knob deck: group fence + caption + compact caption toggles + radial knobs with + // label<->value swap on hover/drag. `descs` picks the group set (Sample's deckGroupDescs + // or the Zone panel's zoneDeckGroupDescs); caller anchors (Sample bottom, Zone top). void paintKnobDeck(LICE_IBitmap* bmp, const Rect& deckArea, const PerformanceZone& zone, const std::vector& descs); - // The mini curve-preview button (shared by the Sample cluster + the Zone panel, FB2): a - // hairline bg/cell square tracing the zone's live curve; Active border while the popup is up. + // The mini curve-preview button shared by the Sample cluster + the Zone panel. void paintCurveButton(LICE_IBitmap* bmp, const Rect& r, const PerformanceZone& zone); - // The centered curve-popup sheet (wash + title + close + full-size curve editor). Edits - // popupZone() — the Sample face's one-zone site or the Zone surface's selected zone (FB2). + // The centered curve-popup sheet. Edits popupZone() — the Sample face's one-zone site + // or the Zone surface's selected zone. void paintCurvePopup(LICE_IBitmap* bmp, int w, int h); - // 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. + // Traces the amp-envelope overlay + its draggable node handles over `waveArea`. void paintEnvelopeOverlay(LICE_IBitmap* bmp, const Rect& waveArea, const PerformanceZone& zone, std::int64_t frames); - // S-VIEW-10: the velocity->amp transfer-curve editor — a bordered box (X = velocity 0-127, - // Y = amp 0-1), the monotone spline traced by eval, one draggable node handle per control - // point. Since FB2 its ONLY host is the r11 popup sheet (both surfaces summon it via the - // mini preview button); all mapping / hit-test / clamp math lives in the pure - // velocity_curve module. `r` empty -> draws nothing. + // 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, const PerformanceZone& zone); - // Route a mouse-down inside curve-editor box `r` editing map_.zones[zoneIndex]: a node grab - // starts a kCurveNode drag; Alt-click on an interior node deletes it (committed at once); - // an empty-space click ADDS a point at the cursor and grabs it for an immediate drag. - // `zoneIndex` must be a valid index into map_.zones (callers materialize first). + // Mouse-down inside curve-editor box `r` editing map_.zones[zoneIndex]: 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. `zoneIndex` must be valid (callers + // materialize first). void handleCurveMouseDown(const Rect& r, int zoneIndex, int x, int y); - // Route a left-click while the curve popup is open (the popup is MODAL over the Sample - // face AND the Zone surface, FB2): Close / outside-wash dismiss, in-box clicks into the - // shared curve machinery against popupZoneIndex(), everything else on the sheet swallowed. - // Returns true when the popup consumed the click (i.e. whenever it is open). + // Left-click while the curve popup is open (modal over both faces): 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); void onMouseDown(int x, int y); - // The Browse-modal and Zone-surface halves of the mouse-down dispatch (Q-W2v: the - // input TUs split along the face axis — onMouseDown keeps the Sample-face branch and - // delegates these two; bodies in editor_input_browse_zone.cpp). Behavior-identical - // to the former inline branches. + // The Browse-modal and Zone-surface halves of the mouse-down dispatch (bodies in + // editor_input_browse_zone.cpp). void mouseDownBrowse(int w, int h, int x, int y); void mouseDownZone(int w, int h, int x, int y); void onMouseMove(int x, int y); void onMouseUp(int x, int y); - // r11: right-click — the curve popup's PRIMARY node-delete affordance (issue 3c). Only - // acts while the popup is open (over the Sample face OR the Zone surface, FB2); a - // right-click on a popup curve node deletes it through the same commit path as Alt-click - // (deletePoint's endpoint guard makes endpoint right-clicks a safe no-op). Everything - // else ignores right-clicks. + // 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); - // Apply a knob/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 knob's 0..1). Used by both the click + drag paths. + // Applies a knob/toggle interaction to map_.zones[zoneIndex] for control `id`: ordinary + // controls route through applyControl; kKeyTrack writes the zone's keyTrack scalar + // (0..200% over the knob's 0..1). 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). + // Resolves the interactive element under (x, y) into hover_, called from WM_MOUSEMOVE. + // Repaints only on change, so an idle move is free. Windows-only. 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 + void onMouseWheel(int delta); // browser scroll (wheel) + void onSearchChar(unsigned int ch); // 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. + // 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 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. + // 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); @@ -279,37 +218,31 @@ private: // 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. + // Publishes the edited zones/selection to the processor, then rebuilds the instrument + // off the audio thread. UI thread only. void commitAndReload(); - // Commit `id` as the loaded single-capture selection (the Browse Load confirm and the - // double-click accelerator both route here). Runs reconcileSingleCaptureZones first so - // the previous sample's materialized full-range zone cannot linger and shadow the new - // pick under first-match resolve (the zone-bleed fix, issue 3a), then publishes + reloads. + // Commits `id` as the loaded single-capture selection. Runs reconcileSingleCaptureZones + // first so the previous sample's materialized full-range zone cannot linger and shadow + // the new pick under first-match resolve, then publishes + reloads. void loadSelection(const std::string& id); - // Recompute the capture cards visible under the current bank filter (samples_ narrowed by - // activeFilterBankId_; "" = All) into visible_. Called on refresh + filter change. + // 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, 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). + // 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, 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. + // 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 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). + // The effective loop + start markers for the picked capture: the per-zone override when + // one exists in map_, 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; @@ -318,125 +251,93 @@ private: }; 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. + // Commits an edited marker set for the picked capture as a per-zone loop/start override + // (upsert on the picked id), then reloads 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_. + // Writes `m` as a loop/start override upsert into map_ for selectedId_ (find-or-append). + // Does NOT call commitAndReload — callers decide live-drag vs final commit. selectedId_ + // must be non-empty. Returns the updated/appended zone index. int upsertPickedOverride(const SetupMarkers& m); - // --- S12/S15/S16 parameter value domains (both deck surfaces) ------------------ - // - // The deck knobs edit a 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. + // Deck knobs edit a zone's ZonePlaySeconds (play mode + AHDSR; pitch engine + AD pitch + // envelope) — wall-clock seconds, rate-free; the keymap build resolves to frames. - // 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). + // 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 ZonePlaySeconds& play) const; - // Apply a committed control interaction to `play`: a knob's normalized `value` (mapped back - // into the control's stored domain) or a toggle's `segment` (0/1). Mutates `play` in place. + // 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, ZonePlaySeconds& play, double value, int segment) const; - // The Trigger fade-in/out knob full-scale, in SOURCE frames: kFadeMaxSeconds (2 s - // wall-clock) resolved against the live rate at use (Q-W0 T3-03 — never a baked-in - // rate). 44.1 kHz fallback before setupProcessing has run. Storage stays frames. + // The Trigger fade-in/out knob full-scale, in source frames: kFadeMaxSeconds resolved + // against the live rate — never a baked-in rate. 44.1 kHz fallback pre-setupProcessing. double fadeMaxFrames() 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). + // envelope_overlay's AmpEnvelope stores Trigger fades as fractions of the played span, + // while the zone 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): 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)). + // PACK (draw): zone play params -> AmpEnvelope. `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. + // UNPACK (commit): an edited AmpEnvelope -> the zone's play params, in place. 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). + // 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; - // --- 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). + // The Sample face and the Zone surface read/write the same one-zone map site. + // effectiveSampleZone returns the picked id's override if present in map_, else a + // product-default zone (not yet materialized — a control edit does that). PerformanceZone effectiveSampleZone() const; - // The effective root: the picked id's rootOverride, else its bank intrinsic, else middle C. + // The effective root: rootOverride, else the 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. + // The live sample rate from the bridge, or 0 when unavailable (caller guards). double liveSampleRate() const; - // The persisted preview velocity as a 0..1 slider value (MIDI 1..127 mapped onto [0,1]). + // Persisted preview velocity as a 0..1 slider value (MIDI 1..127 -> [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. + // Find-or-materializes the one-zone override for the picked id, appending a + // product-default zone if none exists. Mirror of upsertPickedOverride for a control + // edit. Returns -1 if selectedId_ is empty. int ensureSampleZone(); - // --- Curve-popup target resolution (r11 FB1 + FB2) ----------------------------- - // - // The popup edits ONE zone per open: the Zone surface's SELECTED zone (FB2) or the Sample - // face's picked one-zone site. popupZone is the read-only resolve (paint/hover/right-click - // hit-test); popupZoneIndex is the edit target — it materializes the Sample-face zone via - // ensureSampleZone but NEVER materializes on the Zone surface (the button only shows for - // an explicit selection). Returns -1 when there is no valid target (callers guard). + // The popup edits ONE zone per open: the Zone surface's selected zone or the Sample + // face's picked site. popupZone is the read-only resolve; popupZoneIndex is the edit + // target — materializes on the Sample face via ensureSampleZone, never on the Zone + // surface (button only shows for an explicit selection). -1 = no valid target. PerformanceZone popupZone() const; int popupZoneIndex(); - // --- r11 knob-deck plumbing (FB1 Sample face; FB2 Zone panel) ------------------- - // - // The deck is the r11 replacement for the slider control strips on BOTH surfaces: the pure - // knob_deck module lays out the fenced groups, param_slider's FA4 primitive owns the - // value<->needle map, and these members own the control-id <-> value binding. - - // The PER-ZONE deck groups (FB2 — the set both surfaces share): AMP ENVELOPE (Gate: - // A/H/D/S/R; Trigger: Fade In / Length % / Fade Out + two RESERVED blanks so a mode flip - // never reflows the neighbours) / PITCH (Key Track) / PITCH ENV (P.Attack/P.Decay/P.Depth). - // The Zone panel renders exactly these — per-instance state stays off it. + // The per-zone deck groups both surfaces share: AMP ENVELOPE (Gate A/H/D/S/R; Trigger + // Fade In/Length %/Fade Out + two reserved blanks so a mode flip never reflows + // neighbours) / PITCH (Key Track) / PITCH ENV (P.Attack/P.Decay/P.Depth). std::vector zoneDeckGroupDescs(const ZonePlaySeconds& play) const; - // The full Sample-face deck: the shared per-zone groups + the per-instance VOICE (Voices - // knob + Poly|Mono caption toggle + Retrig|Legato row toggle) and MASTER (the FB1 - // post-mixer Gain knob) groups. + // The full Sample-face deck: the shared groups + the per-instance VOICE (Voices knob + + // Poly|Mono + Retrig|Legato) and MASTER (Gain knob) groups. std::vector deckGroupDescs(const ZonePlaySeconds& play) const; // The normalized [0,1] value a deck knob shows for `zone` — zone params route through - // controlValue/keyTrack; the processor-side ids (voice count, master gain, and the - // cluster's preview velocity via the -2 sentinel) read the processor's live value, so - // the knob and its storage are two views on one model (re-read each paint). + // 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 PerformanceZone& zone) const; - // Apply a deck-knob value: zone params write map_.zones[zoneIndex] (live-drag semantics, - // commit on release); processor params (voice count / master gain / preview velocity) - // write through the processor setters immediately (transient — no map edit, no reload). - // zoneIndex is ignored for processor-side ids. + // Applies a deck-knob value: zone params write map_.zones[zoneIndex] (commit on + // release); processor params write through the processor setters immediately + // (transient — no map edit, no reload). zoneIndex ignored for processor-side ids. void applyDeckKnob(int zoneIndex, int id, double norm); - // The knob's live value label (shown in place of the name label during hover/drag): - // seconds ("0.123s"), percents ("85%"), source frames ("8820f"), signed semitones - // ("+3.5st"), a voice count ("16"), or the master-gain dB ("-inf"/"+2.4dB"). + // 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 PerformanceZone& zone) const; ReaSamplerProcessor* processor_ = nullptr; @@ -447,64 +348,51 @@ private: 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 + ChannelMode channelMode_ = ChannelMode::Mono; // mono/stereo toggle snapshot - // --- Phase S voice-deck snapshot (PROVISIONAL controls — the Wave B recompose owns the - // final deck). 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). + // 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 + zones) --- - View view_ = View::kSample; // default face is the loaded-sample home (S-VIEW-1) + // Transient UI state (not persisted; component state carries selection + zones). + View view_ = View::kSample; // default face is the loaded-sample home 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. + // 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) - // --- 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. + // 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; - // --- 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. + // 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 - // --- S12 browser scroll + search (transient UI state, never persisted) -------- + // 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). + // When >= 0, a low/high/root field is being typed (0=low,1=high,2=root); entryText_ + // accumulates keystrokes and commits via parseNoteEntry on Enter. -1 = no field editing. int entryField_ = -1; std::string entryText_; - // --- Hover state (Phase L, L3; transient, never persisted) -------------------- + // 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 ------------------------------------------------------ + // 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 @@ -515,54 +403,46 @@ private: 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). + // 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; // 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). kDeckKnob drag: which control id + the zone it edits. + // Scrollbar-thumb drag: the offset at grab time. kDeckKnob drag: which control id + zone. int dragStartScrollOffset_ = 0; int dragParamId_ = -1; // control id under a kDeckKnob drag; -2 = preview-vel knob int dragParamZone_ = -1; // the zone index a kDeckKnob drag edits; -1 = processor-side - // 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. + // Envelope-node drag: which node + the AmpEnvelope snapshotted at grab (absolute-delta + // contract, per envelope_edit's grabEnv). EnvNode envNode_ = EnvNode::Origin; AmpEnvelope dragStartEnv_{}; - // S-VIEW-10 velocity-curve node drag: which point is grabbed, the curve snapshotted at grab - // (resolvePointDrag's absolute-delta contract), the box rect the grab happened in (the Sample - // and Zone views place the editor differently — the drag resolves against the grab-time box), - // and which zone the edit lands on. Mirror of the envelope-node drag state. + // Velocity-curve node drag: which point, the curve snapshotted at grab + // (resolvePointDrag's absolute-delta contract), the grab-time box rect (Sample and Zone + // place the editor differently), and which zone the edit lands on. int curvePointIndex_ = -1; VelocityCurve dragStartCurve_ = VelocityCurve::flat(); Rect dragCurveRect_{}; int dragCurveZone_ = -1; - // r11 deck-knob drag (FB1): the control's normalized value AT GRAB — knobDragValue maps - // the vertical pixel delta from this anchor, so a grab never jumps the value (FA4). + // 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; - // r11 curve popup (FB1 + FB2): open flag — editor-local, never persisted. The popup edits - // popupZone() — the picked capture's one-zone site on the Sample face, the SELECTED zone - // on the Zone surface — re-resolved each paint so a sync-tick refresh mid-open stays - // coherent (a refresh that drops the target closes it; see refreshFromBank). + // Curve popup open flag, never persisted. Edits popupZone(), re-resolved each paint so a + // sync-tick refresh mid-open stays coherent (a refresh that drops the target closes it). bool curvePopupOpen_ = false; - // --- 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. + // 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 (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. + // 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_; }; diff --git a/src/shell/instrument/reasampler_embed.cpp b/src/shell/instrument/reasampler_embed.cpp index 53e9d8d..7b7a1b6 100644 --- a/src/shell/instrument/reasampler_embed.cpp +++ b/src/shell/instrument/reasampler_embed.cpp @@ -1,29 +1,29 @@ // reasampler_embed.cpp — see reasampler_embed.h. The IReaperUIEmbedInterface shell. -// Windows-only (D5); guarded so a non-Windows build degrades to a stub that reports -// "not supported" and draws nothing. +// Windows-only; guarded so a non-Windows build degrades to a stub that reports "not +// supported" and draws nothing. #include "shell/instrument/reasampler_embed.h" #include #include -#include "core/version/app_version.h" // vstPluginName (channel-derived embed label, S18) -#include "core/instrument/map/bank_sync.h" // parseBankGeneration (S9 dirty-guard over the per-paint refresh) -#include "core/ui/component_geometry.h" // KitBox — the kit text/fill draw box (Phase L, L3) -#include "shell/panel/draw_kit.h" // the L1 draw kit: fillSurface/text (L3) +#include "core/version/app_version.h" // vstPluginName (channel-derived embed label) +#include "core/instrument/map/bank_sync.h" // parseBankGeneration (dirty-guard over the per-paint refresh) +#include "core/ui/component_geometry.h" // KitBox — the kit text/fill draw box +#include "shell/panel/draw_kit.h" // the shared draw kit: fillSurface/text #include "core/instrument/ui/editor_geometry.h" // Rect (shared with embed_strip) #include "core/instrument/ui/embed_strip.h" // the pure strip layout + hit-test #include "ext_keys.h" // kProjExtBanksKey / kProjExtBankGenKey #include "shell/instrument/reaper_bridge.h" #include "shell/instrument/reasampler_processor.h" -#include "core/ui/theme.h" // Role / InteractionState / spectralColor (L3) +#include "core/ui/theme.h" // Role / InteractionState / spectralColor -// wdltypes.h first: it defines INT_PTR portably (and pulls on Windows), which -// reaper_plugin_fx_embed.h's REAPER_FXEMBED_IBitmap::Extended needs as its return type. +// wdltypes.h first: it defines INT_PTR portably (needed by REAPER_FXEMBED_IBitmap::Extended's +// return type in the header below). #include "wdltypes.h" -// REAPER's embed message/bitmap contract (vendored). REAPER_FXEMBED_IBitmap is an alias of -// LICE_IBitmap, and the WM_* / DrawInfo / SizeHints definitions live here. +// REAPER's embed message/bitmap contract (vendored): REAPER_FXEMBED_IBitmap aliases +// LICE_IBitmap; WM_* / DrawInfo / SizeHints live here. #include "reaper_plugin_fx_embed.h" #ifdef _WIN32 @@ -34,17 +34,12 @@ using namespace Steinberg; -// DECLARE_CLASS_IID in the REAPER header only DECLARES IReaperUIEmbedInterface::iid; some -// TU must DEFINE it. This is the only place that answers queryInterface for it, so the -// definition lives with its sole use (mirrors reaper_bridge.cpp doing this for -// IReaperHostApplication). +// This is the only TU that answers queryInterface for IReaperUIEmbedInterface, so the +// DEFINE lives here (mirrors reaper_bridge.cpp's IReaperHostApplication). DEF_CLASS_IID(Steinberg::IReaperUIEmbedInterface) namespace reasampler::vst { -// Real-namespace-home using-directives (Q-W6: the namespaces.h shim is retired): -// the embed strip speaks the map vocabulary (listSamples / parseBankGeneration) and -// the pure UI layout (embed_strip / editor_geometry Rect) wholesale. using namespace reasampler::instrument::map; using namespace reasampler::instrument::ui; using reasampler::ui::spectralColor; @@ -52,15 +47,13 @@ using version::vstPluginName; namespace { #ifdef _WIN32 -// Kit adapter (Phase L, L3): the embed shell's Rect (editor_geometry) -> the kit's KitBox -// (component_geometry). Every embed surface now draws by palette ROLE via the L1 kit, retiring -// the local pre-L1 forest-green palette + raw GDI DrawTextA. +// Kit adapter: the embed shell's Rect -> the kit's KitBox. KitBox toKitBox(const Rect& r) { return KitBox{r.x, r.y, r.width, r.height}; } -// A short display name for a bank sample id, from the snapshotted list (the editor's helper, -// duplicated small rather than shared across the shell/pure boundary). +// A short display name for a bank sample id (small duplicate of the editor's helper +// rather than shared across the shell/pure boundary). std::string sampleLabel(const std::vector& samples, const std::string& id) { for (const SampleChoice& c : samples) { if (c.id == id) return c.displayName.empty() ? c.id : c.displayName; @@ -69,9 +62,8 @@ std::string sampleLabel(const std::vector& samples, const std::str } #endif -// Project the instrument's performance map into the strip's minimal zone shape (key ranges -// only). Pure projection — kept here (shell side) because it reads PerformanceMap, a shell -// type; embed_strip stays free of it. +// Projects the performance map into the strip's minimal zone shape (key ranges only). +// Kept shell-side because it reads PerformanceMap; embed_strip stays free of it. std::vector toEmbedZones(const PerformanceMap& map) { std::vector out; out.reserve(map.zones.size()); @@ -104,28 +96,24 @@ void ReaSamplerEmbed::refresh() { void ReaSamplerEmbed::maybeRefresh() { if (!processor_) { refresh(); return; } // clears state; cheap - // The performance map is a cheap in-process accessor (mutex + copy), and the editor may - // have edited zones with NO bank-content change — always re-snapshot it so a zone edit - // reflects immediately. + // The performance map is a cheap in-process accessor, and the editor may edit zones + // with no bank-content change — always re-snapshot it so an edit reflects immediately. map_ = processor_->performanceMap(); if (selectedZone_ >= static_cast(map_.zones.size())) selectedZone_ = -1; - // The EXPENSIVE part is the bank-blob bridge read (samples_). Gate it on the S9 bank- - // generation stamp (a small ext-state read): only re-read the bank when the generation - // changed since the last paint (a recapture / ingest / remove), or on the first paint - // (lastSeenBankGeneration_ == -1). A pre-S9 project reads generation 0; the first paint - // folds it and subsequent idle paints skip the bank read entirely. + // The expensive part is the bank-blob bridge read: gate it on the bank-generation + // stamp, re-reading only when it changed (or on the first paint). A project with no + // stamp reads generation 0; the first paint folds it and idle paints skip the read. std::int64_t currentGen = lastSeenBankGeneration_; if (auto rawGen = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBankGenKey)) { currentGen = parseBankGeneration(*rawGen); } else if (lastSeenBankGeneration_ < 0) { - currentGen = 0; // unprimed + no stamp (pre-S9): treat as generation 0 for the first read + currentGen = 0; // unprimed + no stamp: treat as generation 0 for the first read } - // Intentional asymmetry: a TRANSIENT bridge failure (readReasamplerExtState returned - // nullopt after we were already primed) leaves currentGen == lastSeenBankGeneration_, - // so the bank-blob read is skipped and the editor keeps its last-known sample list. - // A stale-but-intact list is better than clearing samples_ on every transient hiccup. + // Intentional asymmetry: a transient bridge failure after priming leaves currentGen + // unchanged, skipping the read — a stale-but-intact list beats clearing samples_ on + // every hiccup. if (lastSeenBankGeneration_ < 0 || currentGen != lastSeenBankGeneration_) { auto banks = @@ -145,9 +133,8 @@ TPtrInt ReaSamplerEmbed::embed_message(int msg, TPtrInt parm2, TPtrInt parm3) { #endif case REAPER_FXEMBED_WM_CREATE: #ifdef _WIN32 - // Create the kit's cached AA fonts before the first paint (Phase L, L3). - // Idempotent + process-global (shared with the editor in this binary); NOT torn - // down per-view — the OS reclaims the tiny static HFONT set at module unload. + // Idempotent + process-global (shared with the editor); not torn down per-view + // — the OS reclaims the tiny static HFONT set at module unload. kitFontsInit(); #endif refresh(); // prime the first paint's snapshot @@ -157,8 +144,7 @@ TPtrInt ReaSamplerEmbed::embed_message(int msg, TPtrInt parm2, TPtrInt parm3) { case REAPER_FXEMBED_WM_GETMINMAXINFO: { auto* hints = reinterpret_cast(parm3); if (!hints) return 0; - // Minimum usable strip height: the keymap must not collapse below its floor - // (kEmbedKeymapMinHeight) plus the level band. + // The keymap must not collapse below its floor plus the level band. hints->min_width = 64; hints->max_width = 0; // 0 = unconstrained hints->min_height = kEmbedKeymapMinHeight + kEmbedLevelBandHeight; @@ -172,7 +158,7 @@ TPtrInt ReaSamplerEmbed::embed_message(int msg, TPtrInt parm2, TPtrInt parm3) { case REAPER_FXEMBED_WM_PAINT: return paint(parm2, parm3) ? 1 : 0; case REAPER_FXEMBED_WM_LBUTTONDOWN: - // Selection at most (S6): map the click to a zone; force a redraw if it changed. + // Selection at most: map the click to a zone; force a redraw if it changed. return onMouseDown(parm3) ? REAPER_FXEMBED_RETNOTIFY_INVALIDATE : 0; #endif default: @@ -190,35 +176,30 @@ bool ReaSamplerEmbed::paint(TPtrInt bitmap, TPtrInt drawInfo) { const int h = di->height; if (w <= 0 || h <= 0) return false; - // Re-read live state each paint (UI thread) so the strip reflects keymap edits + bank - // changes without its own timer — REAPER repaints the embed surface on its cadence. S9 - // dirty-guard: maybeRefresh does the EXPENSIVE bank-blob read only when the bank generation - // changed (the flagged S6 follow-up), always refreshing the cheap performance map. + // Re-read live state each paint (no own timer) — REAPER repaints the embed surface on + // its own cadence. maybeRefresh(); - // REAPER hands us its own bitmap sized to the embed area; draw directly into it (unlike - // the editor, which owns a LICE_SysBitmap and BitBlt's). Origin is the bitmap's (0,0). - // Base canvas through the kit (bg/base + micro-gradient), Phase L L3. + // REAPER hands us its own bitmap sized to the embed area; draw directly into it + // (unlike the editor, which owns a LICE_SysBitmap and BitBlt's). fillSurface(bmp, KitBox{0, 0, w, h}, Role::BgBase, InteractionState::Rest); const EmbedLayout layout = layoutEmbed(w, h); if (map_.zones.empty()) { - // No opt-in zones authored: a faint bg/cell band spanning the keymap area so the strip - // reads as "present, no zones" — the default single-capture face lives in the editor. + // No opt-in zones authored: a faint band so the strip reads as "present, no zones" + // — the default single-capture face lives in the editor. LICE_FillRect(bmp, layout.keymap.x, layout.keymap.y, layout.keymap.width, layout.keymap.height, toLice(roleColor(Role::BgCell)), 0.5f, 0); - const std::string label = version::vstPluginName() + // channel-derived (S18) + const std::string label = version::vstPluginName() + // channel-derived (samples_.empty() ? " (bank empty)" : " (no zones)"); const Rect labelR = Rect::ltrb(layout.keymap.x + 4, layout.keymap.y, layout.keymap.right(), layout.keymap.bottom()); text(bmp, toKitBox(labelR), label.c_str(), Font::Label, Role::TextPrimary, Align::Left); } else { - // Draw each zone as a segment across the keymap span, first-match order (so the painted - // order matches selection + playback). Each segment takes its PASTEL SPECTRAL hue from - // the center of its key span (spectralColor — §4), so the strip reads as the same - // spectrum as the editor's keyboard strip. The SELECTED zone lifts to accent-primary - // + a static glow ("which zone is live", never a pulse — §3.5). + // Each zone draws as a segment (first-match order, matching selection/playback), + // colored by its key span's spectral hue so it reads as the same spectrum as the + // editor's keyboard strip. The selected zone lifts to accent-primary + a static glow. for (int i = 0; i < static_cast(map_.zones.size()); ++i) { const PerformanceZone& z = map_.zones[i]; const Rect r = zoneSegmentRect(layout, z.lowNote, z.highNote); @@ -237,9 +218,8 @@ bool ReaSamplerEmbed::paint(TPtrInt bitmap, TPtrInt drawInfo) { } LICE_DrawRect(bmp, r.x, r.y, r.width - 1, r.height - 1, toLice(roleColor(Role::LineHairline)), 1.0f, 0); - // Label the segment with the sample name when it is wide enough to read. The - // selected (accent-fill) segment draws its label in bg/base for contrast (the - // tight text-on-pastel pair, §4); the rest in text/primary. + // Label when wide enough to read; the selected (accent-fill) segment labels in + // bg/base for contrast, the rest in text/primary. if (r.width >= 24) { const Rect lr = Rect::ltrb(r.x + 3, r.y, r.right() - 2, r.bottom()); text(bmp, toKitBox(lr), sampleLabel(samples_, z.sampleId).c_str(), @@ -248,8 +228,8 @@ bool ReaSamplerEmbed::paint(TPtrInt bitmap, TPtrInt drawInfo) { } } - // The level band: a recessed bg/cell channel with an accent-primary fill following the - // live activity level (a direct level follow — the one permitted "motion", §3.5). + // The level band: a recessed channel with an accent-primary fill tracking the live + // activity level (the one permitted "motion"). if (layout.levelBand.height > 0) { fillSurface(bmp, toKitBox(layout.levelBand), Role::BgCell, InteractionState::Pressed); const double level = processor_ ? processor_->embedActivityLevel() : 0.0; diff --git a/src/shell/instrument/reasampler_embed.h b/src/shell/instrument/reasampler_embed.h index bcc31c1..8e7342d 100644 --- a/src/shell/instrument/reasampler_embed.h +++ b/src/shell/instrument/reasampler_embed.h @@ -1,34 +1,9 @@ -// reasampler_embed.h — the S6 embedded TCP/MCP UI shell. Implements REAPER's -// IReaperUIEmbedInterface (vendor/reaper-sdk/sdk/reaper_plugin_fx_embed.h + -// reaper_vst3_interfaces.h) so the instrument draws a compact keymap/level strip INLINE in -// the track/mixer control panel — the same Cockos surface REAPER's own embedded FX use. -// -// VERIFIED CONTRACT (against reaper_plugin_fx_embed.h + reaper_vst3_interfaces.h): -// * VST3 exposes this by having the IEditController answer queryInterface for -// IReaperUIEmbedInterface (iid {0x049bf9e7,0xbc74ead0,0xc4101e86,0x7f725981}). Our -// SingleComponentEffect IS the edit controller, so the processor's queryInterface hands -// REAPER a reference to this object. -// * The single method is embed_message(int msg, TPtrInt parm2, TPtrInt parm3). msg is a -// REAPER_FXEMBED_WM_* value (aliased to Win32 WM_*): -// - WM_IS_SUPPORTED (0x0000): return 1 (supported+available), -1, or 0. -// - WM_CREATE (0x0001) / WM_DESTROY (0x0002): embed begin/end; return ignored. -// - WM_PAINT (0x000F): parm2 = REAPER_FXEMBED_IBitmap* (alias LICE_IBitmap) to draw -// into; parm3 = REAPER_FXEMBED_DrawInfo* (context TCP=1/MCP=2, width/height, mouse, -// flags). Return 1 if drawing occurred, 0 otherwise. -// - WM_GETMINMAXINFO (0x0024): parm3 = SizeHints*; return 1 if filled. -// - mouse WM_* (0x0200..0x020A): parm3 = DrawInfo*; return RETNOTIFY_INVALIDATE -// (0x1000000) to force a redraw. Capture is auto-managed by the host. -// * There is NO plugin-owned window/HWND here (unlike the IPlugView editor): REAPER hands -// a LICE bitmap per paint; we only draw into it and read mouse coords from DrawInfo. -// -// RT DISCIPLINE (S6 constraint): all embed messages arrive on REAPER's UI thread; nothing -// here runs in process(). It reads the same live state the editor reads (bank over the -// bridge + the processor's performance map) with the same off-audio-thread accessors — no -// new locks visible to process, read-only over the bank. Windows-only (D5), guarded so a -// non-Windows build stays compilable. -// -// The strip's LAYOUT + HIT-TEST is pure (embed_strip.h, unit-tested); this shell marshals -// REAPER's messages to/from it and draws with the same LICE idiom as reasampler_editor. +// reasampler_embed.h — the embedded TCP/MCP UI shell. Implements REAPER's +// IReaperUIEmbedInterface so the instrument draws a compact keymap/level strip inline in +// the track/mixer control panel. All embed messages arrive on REAPER's UI thread; nothing +// here runs in process(). Windows-only, guarded so a non-Windows build stays compilable. +// The strip's layout + hit-test is pure (embed_strip.h, unit-tested); this shell marshals +// REAPER's messages to/from it. #pragma once @@ -51,27 +26,30 @@ namespace reasampler::vst { class ReaSamplerProcessor; -// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired). using instrument::map::PerformanceMap; using instrument::map::SampleChoice; -// Implements IReaperUIEmbedInterface. Lifetime is OWNED by the processor (the processor -// holds the sole unique_ptr and hands out AddRef'd references from queryInterface); the -// back-pointer to the processor is therefore always valid while this lives. +// Implements IReaperUIEmbedInterface. Lifetime is owned by the processor (sole unique_ptr, +// hands out AddRef'd references from queryInterface); the back-pointer to the processor is +// therefore always valid while this lives. class ReaSamplerEmbed : public Steinberg::IReaperUIEmbedInterface { public: explicit ReaSamplerEmbed(ReaSamplerProcessor* processor) : processor_(processor) {} - // The one embed entry point. Routes each REAPER_FXEMBED_WM_* message; see the header - // note above for the per-message contract. UI thread only. + // The one embed entry point, verified against reaper_plugin_fx_embed.h + + // reaper_vst3_interfaces.h: our IEditController answers queryInterface for + // IReaperUIEmbedInterface. msg is a REAPER_FXEMBED_WM_* value (aliased to Win32 WM_*) + // — WM_IS_SUPPORTED, WM_CREATE/WM_DESTROY, WM_PAINT (parm2 = IBitmap*, parm3 = + // DrawInfo*), WM_GETMINMAXINFO (parm3 = SizeHints*), mouse WM_* (return + // RETNOTIFY_INVALIDATE to force a redraw). No plugin-owned HWND here (unlike the + // IPlugView editor): REAPER hands a LICE bitmap per paint. UI thread only. Steinberg::TPtrInt embed_message(int msg, Steinberg::TPtrInt parm2, Steinberg::TPtrInt parm3) override; - // FUnknown: this object's lifetime is owned by the processor, not the host refcount, so - // AddRef/release are no-ops (the processor's unique_ptr governs destruction) and - // queryInterface answers only FUnknown + IReaperUIEmbedInterface. This mirrors how the - // SDK's OBJ refcount would otherwise churn; here the owning processor guarantees the - // object outlives every borrowed reference REAPER holds during embedding. + // FUnknown: lifetime is owned by the processor, not the host refcount, so + // AddRef/release are no-ops and queryInterface answers only FUnknown + + // IReaperUIEmbedInterface — the owning processor guarantees this outlives every + // borrowed reference REAPER holds during embedding. Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid, void** obj) override; Steinberg::uint32 PLUGIN_API addRef() override { return 1000; } @@ -79,37 +57,29 @@ public: private: #ifdef _WIN32 - // Draw the current strip into REAPER's supplied LICE bitmap. Returns true if it drew. + // Draws the current strip into REAPER's supplied LICE bitmap. Returns true if it drew. bool paint(Steinberg::TPtrInt bitmap, Steinberg::TPtrInt drawInfo); - // Handle a mouse-down inside the strip: map to a zone and select it (S6: selection at - // most — no new editing semantics). Returns true if the selection changed (the caller - // then asks REAPER to invalidate). + // A mouse-down inside the strip: maps to a zone and selects it (no new editing + // semantics). Returns true if the selection changed (caller then invalidates). bool onMouseDown(Steinberg::TPtrInt drawInfo); #endif - // Snapshot the live bank + the instrument's performance map for the next paint, exactly - // as the editor's refreshSampleList does (bridge read + processor accessors, UI thread). + // Snapshots the live bank + the instrument's performance map for the next paint. void refresh(); - // The S9 dirty-guard over refresh() (the S6 flagged follow-up): read the cheap bank- - // generation stamp; do the EXPENSIVE bank-blob bridge read (refresh()) only when the - // generation changed since the last paint (or on the first paint) — the strip re-read - // per paint was wasteful now that a generation counter exists. The performance map (a - // cheap in-process accessor, edited by the editor independently of bank content) is - // ALWAYS refreshed so a zone edit still reflects immediately. UI thread only. + // Dirty-guard over refresh(): re-reads the bank blob only when the (cheap) generation + // stamp changed since the last paint. The performance map is always refreshed (cheap + // in-process accessor) so a zone edit reflects immediately. UI thread only. void maybeRefresh(); ReaSamplerProcessor* processor_ = nullptr; - // The bank generation last folded into samples_ (S9 dirty-guard). -1 forces the first - // maybeRefresh() to do a full read (no generation can be negative — parseBankGeneration - // yields >= 0 — so -1 is an "unprimed" sentinel distinct from a real generation 0). + // The bank generation last folded into samples_. -1 is an "unprimed" sentinel distinct + // from a real generation 0, forcing the first maybeRefresh() to do a full read. std::int64_t lastSeenBankGeneration_ = -1; - // Snapshotted for the current paint (refreshed each paint off the audio thread). std::vector samples_; PerformanceMap map_; - // The zone the last click selected (local/visual only — S6 selection constraint; the - // processor's editor-shared selection is NOT updated from here); -1 = none. - // Drives the strip's highlight. + // The zone the last click selected (local/visual only); -1 = none. Drives the strip's + // highlight. int selectedZone_ = -1; }; diff --git a/src/shell/instrument/reasampler_processor.cpp b/src/shell/instrument/reasampler_processor.cpp index 8f11876..bf5f14c 100644 --- a/src/shell/instrument/reasampler_processor.cpp +++ b/src/shell/instrument/reasampler_processor.cpp @@ -1,10 +1,9 @@ -// reasampler_processor.cpp — see reasampler_processor.h. Since Q-W2v (T4-12) this TU is -// the VST3 LIFECYCLE + the REAL-TIME process() path ONLY: factory/queryInterface, -// initialize/terminate/setActive, bus setup, and the block render (MIDI marshal, preview -// mailbox drain, engine + drain sum, master-gain ramp). Component-state I/O + parameter -// accessors live in processor_state.cpp; the off-thread reload/publish family lives in -// processor_reload.cpp. process() and its per-block work stay ONE TU (T4-29): no virtual -// seam, no cross-TU call on the per-sample path. +// reasampler_processor.cpp — see reasampler_processor.h. This TU is the VST3 lifecycle + +// the real-time process() path only: factory/queryInterface, initialize/terminate/ +// setActive, bus setup, and the block render. Component-state I/O + parameter accessors +// live in processor_state.cpp; the off-thread reload/publish family lives in +// processor_reload.cpp. process() and its per-block work stay one TU on purpose — no +// virtual seam, no cross-TU call on the per-sample path. #include "shell/instrument/reasampler_processor.h" @@ -17,8 +16,8 @@ #include "pluginterfaces/vst/ivstmidicontrollers.h" // kCtrlAllNotesOff / kCtrlAllSoundsOff (panic) #include "pluginterfaces/vst/vstspeaker.h" -#include "shell/instrument/reasampler_editor.h" // createView hands the host our IPlugView editor -#include "shell/instrument/reasampler_embed.h" // S6 embed shell + IReaperUIEmbedInterface (its iid DEF'd there) +#include "shell/instrument/reasampler_editor.h" // createView hands the host our IPlugView editor +#include "shell/instrument/reasampler_embed.h" // embed shell + IReaperUIEmbedInterface (its iid DEF'd there) using namespace Steinberg; using namespace Steinberg::Vst; @@ -27,20 +26,15 @@ namespace reasampler::vst { namespace { -// FB1 post-mixer gain ramp TIME (wall-clock). gainCurrent_ converges to masterGain_ by a -// linear per-sample step derived from this at setupProcessing (gainRampStep_ = -// 1 / (kGainRampSeconds * sampleRate_)) — the kPreserveWindowMs pattern, per the standing -// no-hardcoded-rate ruling (Q-W0 T3-01; the prior constant baked 20 ms x 48 kHz in as -// 1/960, silently shortening the ramp at higher host rates). A full 0-to-unity ramp is -// ~20 ms at EVERY host rate; the snap threshold (half a step, below which gainCurrent_ -// jumps to the target) avoids long sub-LSB creep and the ramp loop on idle blocks. +// Post-mixer gain ramp time (wall-clock): gainRampStep_ = 1/(kGainRampSeconds * +// sampleRate_), per the no-hardcoded-rate ruling — ~20 ms full ramp at every host rate. constexpr double kGainRampSeconds = 0.020; } // namespace FUnknown* ReaSamplerProcessor::createInstance(void* /*context*/) { - // The host owns the returned reference. Cast up to the combined interface the SDK - // exposes (IAudioProcessor) so the FUnknown refcount is correctly rooted. + // The host owns the returned reference; cast to IAudioProcessor so the FUnknown + // refcount is correctly rooted. return static_cast(new ReaSamplerProcessor()); } @@ -48,10 +42,8 @@ FUnknown* ReaSamplerProcessor::createInstance(void* /*context*/) { ReaSamplerProcessor::~ReaSamplerProcessor() = default; tresult PLUGIN_API ReaSamplerProcessor::queryInterface(const TUID iid, void** obj) { - // S6: expose REAPER's inline-embed interface. REAPER queries the IEditController for - // IReaperUIEmbedInterface (reaper_vst3_interfaces.h); hand it our lazily-created embed - // shell. We own the shell (unique_ptr); the borrowed reference is valid because the - // processor outlives it. All other iids fall through to the SDK's queryInterface. + // REAPER queries the IEditController for IReaperUIEmbedInterface; hand it our + // lazily-created embed shell (the processor outlives the borrowed reference). if (FUnknownPrivate::iidEqual(iid, IReaperUIEmbedInterface::iid)) { if (!embed_) embed_ = std::make_unique(this); embed_->addRef(); @@ -69,17 +61,11 @@ tresult PLUGIN_API ReaSamplerProcessor::initialize(FUnknown* context) { // instrument still loads, it just has no live bank to play. bridge_.connect(context); - // Instrument bus topology: one event input (MIDI in, 16 channels), one audio output, no - // audio input. GA fix (hard-right pan): the output bus is a FIXED STEREO bus regardless of - // the channel mode. The mode is a DECODE policy (downmix vs L/R split) — mono mode renders - // dual-mono through the stereo bus (both channels equal, centered), which is audibly - // identical to a mono bus but never asks the host to re-map a live instance's pins. The - // prior design flipped the bus kMono<->kStereo via restartComponent(kIoChanged) on every - // mode change/restore; in the DAW that flip panned a dual-mono capture hard RIGHT. The - // in-plugin path is provably symmetric (decode, per-voice stereo render, engine sum, buffer - // write — see testDualMonoStereoSampleRendersCentered), so the asymmetry sat in the host's - // re-routing of the live instance's pins across the arrangement change. A fixed arrangement - // is the maximally-standard VSTi shape and removes that whole negotiation surface. + // One event input (MIDI, 16 channels), one audio output, no audio input. The output + // bus is fixed stereo regardless of channel mode (mono renders dual-mono, centered). + // Do not reintroduce per-mode bus renegotiation: flipping kMono<->kStereo via + // restartComponent previously panned a dual-mono capture hard right in the host's pin + // re-routing (see testDualMonoStereoSampleRendersCentered). addEventInput(STR16("MIDI In"), 16); addAudioOutput(STR16("Audio Out"), SpeakerArr::kStereo); @@ -87,9 +73,8 @@ tresult PLUGIN_API ReaSamplerProcessor::initialize(FUnknown* context) { } tresult PLUGIN_API ReaSamplerProcessor::terminate() { - // process() is not running at terminate: free the live + draining instruments and - // drain the graveyard. Take the pointers out of the atomics first so nothing else - // races them. + // process() is guaranteed stopped at terminate: free the live + draining instruments + // and drain the graveyard. std::lock_guard lock(reloadMutex_); delete live_.exchange(nullptr); delete draining_.exchange(nullptr); @@ -98,33 +83,24 @@ tresult PLUGIN_API ReaSamplerProcessor::terminate() { } tresult PLUGIN_API ReaSamplerProcessor::setActive(TBool state) { - // Activating: build the instrument from the currently-selected sample so the first - // block after activation can play. Deactivating: process is now GUARANTEED stopped by - // the host, so this is the safe point to reclaim the graveyard (the displaced engines - // no reload could free while active). The build/drain are off the audio thread — - // setActive is a main/UI-thread call. + // Activating: build from the currently-selected sample so the first block after + // activation can play. Deactivating: process is now guaranteed stopped, so this is + // the safe point to reclaim the graveyard. Main/UI-thread call. if (state) { - // Self-contained (pS): this rebuild resolves + decodes from the instance-OWNED - // sample refs — it needs no bank read, so it plays regardless of whether the - // extension's PROJEXTSTATE has parsed yet (or the extension exists at all). - // - // #B: this unconditional rebuild is ALSO the NON-editor legacy trigger for a - // pre-v10 blob (refs empty + intent): reloadInstrument's opportunistic - // refreshRefsFromBank copies the refs in when the bank blob is readable by - // activation time, so an upgraded project plays on load without the instrument - // ever being opened (and the next save is self-contained). Residual load-order - // race, DAW-verifiable only: if the host activates this instance BEFORE the - // project's ext-state lines parse, the lift misses here and — with no editor open — - // nothing retries until the next activation or editor tick. MIGRATION NOTE: open a - // pre-v10 instrument once after upgrading if it restores silent. + // Resolves + decodes from the instance-owned refs — no bank read needed, so it + // plays regardless of PROJEXTSTATE parse state. Also doubles as the non-editor + // legacy-lift trigger for a pre-v10 blob: reloadInstrument's opportunistic + // refreshRefsFromBank copies refs in when the bank blob is readable by now. + // Residual load-order race (DAW-verifiable only): if the host activates before the + // project's ext-state parses, nothing retries until the next activation or editor + // tick — open a pre-v10 instrument once after upgrading if it restores silent. reloadInstrument(); } else { std::lock_guard lock(reloadMutex_); - // process is guaranteed stopped: free EVERYTHING. The live instrument too — its - // voices are frozen mid-flight, and if it survived deactivation the reactivate - // reload would displace it into the DRAIN slot, resurrecting stale sustained - // voices as ghosts. Reactivation rebuilds from scratch (reloadInstrument above), - // so nothing is lost by clearing here. + // Free EVERYTHING, including live_: its voices are frozen mid-flight, and if it + // survived deactivation the reactivate reload would displace it into the drain + // slot, resurrecting stale sustained voices as ghosts. Reactivation rebuilds from + // scratch above, so nothing is lost. delete live_.exchange(nullptr); delete draining_.exchange(nullptr); graveyard_.clear(); @@ -135,9 +111,8 @@ tresult PLUGIN_API ReaSamplerProcessor::setActive(TBool state) { tresult PLUGIN_API ReaSamplerProcessor::setupProcessing(ProcessSetup& setup) { sampleRate_ = setup.sampleRate; maxBlockSize_ = setup.maxSamplesPerBlock; - // T3-01: resolve the FB1 gain-ramp step against the live host rate (20 ms wall-clock at - // every rate). At 48 kHz this is exactly the former 1/960 constant. Written here (host - // guarantees setupProcessing never overlaps process), read on the audio thread only. + // Resolve the gain-ramp step against the live host rate (host guarantees + // setupProcessing never overlaps process). if (sampleRate_ > 0.0) { gainRampStep_ = static_cast(1.0 / (kGainRampSeconds * sampleRate_)); } @@ -147,11 +122,10 @@ tresult PLUGIN_API ReaSamplerProcessor::setupProcessing(ProcessSetup& setup) { tresult PLUGIN_API ReaSamplerProcessor::setBusArrangements( SpeakerArrangement* inputs, int32 numIns, SpeakerArrangement* outputs, int32 numOuts) { - // ONE canonical arrangement: the fixed stereo output bus (GA fix — the channel mode is a - // decode policy, never a bus fact). We take NO audio input, so any inputs are rejected. - // Accept (kResultTrue) only a single stereo output proposal; otherwise reject (kResultFalse) - // and keep our stereo arrangement (per the VST3 contract, a plug-in that can't honor a - // proposal keeps a valid arrangement of its own) — the host adapts its routing to us. + // Fixed stereo output bus (channel mode is a decode policy, never a bus fact); no audio + // input, so any inputs are rejected. Accept only a single stereo output proposal; + // otherwise reject and keep stereo (per the VST3 contract, a plug-in that can't honor a + // proposal keeps a valid arrangement of its own) — the host adapts to us. if (numIns < 0 || numOuts < 0) return kInvalidArgument; if (numIns > 0) return kResultFalse; // no audio input bus to arrange if (numOuts == 1 && outputs && outputs[0] == SpeakerArr::kStereo) return kResultTrue; @@ -159,23 +133,19 @@ tresult PLUGIN_API ReaSamplerProcessor::setBusArrangements( } tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) { - // REAL-TIME: no allocation, no IO, no locks. Load the live AND draining instruments - // once for the whole block (two atomic acquires), then publish the MINIMUM installedAt - // over the pointers held so the off-thread graveyard pruner knows exactly which - // generations this block is holding (see the header proof). + // Real-time: no allocation, no IO, no locks. Load live + draining once for the whole + // block (two atomic acquires), then publish the minimum installedAt over the pointers + // held so the off-thread graveyard pruner knows which generations this block holds (see + // the header's drain-slot proof). We publish installedAt rather than re-reading + // reloadGeneration_ to close an ordering race: a fresh read could observe a generation + // newer than the pointers actually held, letting the pruner free an instrument still + // in use. // - // We publish installedAt — not a fresh re-read of reloadGeneration_ — to close an - // ordering race: reading reloadGeneration_ after the slots could observe a generation - // newer than the pointers we actually hold, causing the pruner to free an instrument - // process is still reading. installedAt was set on the reload path before the atomic - // exchange that made the instrument visible. - // - // The DRAIN instrument (FA1, bug 3b) is the previously-live snapshot displaced by the - // last reload: its already-sounding voices keep rendering (and receive note-offs) so a - // curve/param edit or bank refresh never cuts a ringing note. It receives NO note-ons. - // A racing reload can briefly leave the same pointer in both slots (live_ was loaded - // before the swap, draining_ after); collapse that to live-only so one engine is never - // advanced twice per frame. + // The drain instrument is the previously-live snapshot displaced by the last reload: + // its already-sounding voices keep rendering (and receive note-offs) so an edit never + // cuts a ringing note; it receives no note-ons. A racing reload can briefly leave the + // same pointer in both slots (live_ loaded before the swap, draining_ after); collapse + // that to live-only so one engine is never advanced twice per frame. LoadedInstrument* inst = live_.load(std::memory_order_acquire); LoadedInstrument* drain = draining_.load(std::memory_order_acquire); if (drain == inst) drain = nullptr; @@ -190,20 +160,16 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) { } processGeneration_.store(heldGen, std::memory_order_release); - // Phase S drain retirement: publish whether the drain snapshot is FULLY idle (every engine - // voice silent) by naming its OWN installedAt (0 = no drain / still - // sounding). Evaluated at block START — idleness is monotone for a drain (it receives no - // note-ons), so a snapshot observed idle here stays idle; a tail that dies mid-block simply - // publishes one block later. Bounded scan (<= maxVoices), relaxed store — RT-safe. + // Publish whether the drain snapshot is fully idle, naming its own installedAt (0 = no + // drain / still sounding). Idleness is monotone for a drain (no note-ons), so a + // snapshot observed idle here stays idle. Bounded scan, relaxed store — RT-safe. drainIdleGeneration_.store( (drain && drain->fullyIdle()) ? drain->installedAt : 0, std::memory_order_relaxed); - // Marshal MIDI note-on/off from the event input into the voice engine. Tier 0 maps - // events at block granularity (no per-event sample-offset split) — audible timing is - // within one block, adequate for Tier 0; sample-accurate scheduling is a later tier. - // Note-offs also route to the DRAIN engine so a note held across a reload releases - // its old-snapshot voice too (otherwise it would sustain until the next reload). + // Marshal MIDI note-on/off at block granularity (no per-event sample-offset split; + // sample-accurate scheduling is a later tier). Note-offs also route to the drain + // engine so a note held across a reload releases its old-snapshot voice too. if (data.inputEvents) { const int32 count = data.inputEvents->getEventCount(); for (int32 i = 0; i < count; ++i) { @@ -222,18 +188,11 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) { if (inst) inst->engine.noteOff(e.noteOff.pitch); if (drain) drain->engine.noteOff(e.noteOff.pitch); } else if (e.type == Event::kLegacyMIDICCOutEvent) { - // PANIC (Phase S voice-review Major #2): REAPER delivers raw input MIDI CC to a - // VST3 instrument as kLegacyMIDICCOut events on the INPUT event list (a REAPER-ism - // — the type is nominally an output event; DAW-verify, see handoff). - // CC 123 (All Notes Off): release semantics — Gate voices enter their AHDSR - // release tail; Trigger one-shots play through their bounded play length. - // CC 120 (All Sounds Off): hard-stop semantics — immediate silence regardless - // of play mode, including Trigger one-shots that ignore CC 123. This is the - // true "panic" for a ringing one-shot (e.g. a full-length capture). - // Both clear the mono held stack. Both apply to live AND drain. A ringing - // preview note is a real engine voice since the PreviewCard retirement, so - // the panics cover it with no separate routing. allNotesOff / allSoundsOff - // are RT-safe (no allocation, bounded scans). + // Panic: REAPER delivers raw input MIDI CC as kLegacyMIDICCOut events on the + // INPUT event list (a REAPER-ism, DAW-verified). CC 123 (All Notes Off): + // release semantics (Gate -> release tail; Trigger plays through). CC 120 + // (All Sounds Off): immediate hard silence, including Trigger. Both apply to + // live + drain and cover a ringing preview note. const auto cc = static_cast(e.midiCCOut.controlNumber); if (cc == kCtrlAllSoundsOff) { if (inst) inst->engine.allSoundsOff(); @@ -246,16 +205,11 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) { } } - // S-VIEW-4 preview mailbox: drain the off-thread preview-trigger requests (a single relaxed - // atomic load each — RT-safe). A request is NEW when its packed sequence differs from the last - // one we consumed; fire it once, then latch the sequence so the same request never re-fires. - // Preview redesign: the drained requests drive the MAIN VoiceEngine — the exact - // noteOn/noteOff calls the host MIDI marshal above makes — so a preview note is a real - // voice: it counts against the voice count, can steal / be stolen, and respects - // Poly/Mono + Retrigger/Legato (deliberate reversal of the retired PreviewCard's - // isolation). The editor posts the root note, so it plays at unity. - // Consume (advance the sequence) even when inst is null so a note-on posted while no instrument - // is loaded does not re-fire stale on the next instrument load. + // Preview mailbox: drain off-thread preview-trigger requests (one relaxed atomic load + // each). A request is new when its packed sequence differs from the last consumed; fire + // once, then latch the sequence. Drives the main VoiceEngine — same noteOn/noteOff as + // host MIDI, so a preview note is a real voice. Consume even when inst is null so a + // note-on posted while nothing is loaded does not re-fire stale later. { const std::uint32_t on = previewOnRequest_.load(std::memory_order_acquire); const std::uint16_t onSeq = static_cast(on >> 16); @@ -272,16 +226,12 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) { const std::uint32_t off = previewOffRequest_.load(std::memory_order_acquire); const std::uint16_t offSeq = static_cast(off >> 16); if (offSeq != 0 && offSeq != previewOffConsumed_) { - // Consume UNCONDITIONALLY (mirror of the on path): a stale off left pending - // while nothing was loaded would otherwise survive until a (heal) reload lands - // and release the NEXT preview press in the same block. - previewOffConsumed_ = offSeq; - // Route the preview note-off to BOTH engines (mirror of the host note-off): a - // preview held across a reload — e.g. a curve edit committed mid-press — must - // release the old-snapshot voice now draining, not just the (fresh) live one. - // NOTE: preview shares the host-MIDI note space — noteOff releases the newest - // voice at that pitch, so a preview release can release a host-held note at + // Consume unconditionally (mirror of the on path) so a stale off does not + // survive to release the NEXT preview press. Routes to both engines: a preview + // held across a reload must release the old-snapshot voice too. NOTE: preview + // shares the host-MIDI note space, so a release can release a host-held note at // the same pitch (inherent to routing preview through the real note path). + previewOffConsumed_ = offSeq; if (inst) inst->engine.noteOff(static_cast(off & 0xFF)); if (drain) drain->engine.noteOff(static_cast(off & 0xFF)); } @@ -309,27 +259,22 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) { return kResultOk; } - // Render per the host's NEGOTIATED output channel count (S7). The channel mode was baked - // into the LoadedInstrument's decode + negotiated onto the output bus off-thread, so here - // we simply match the buffers the host handed us: >=2 channels -> true stereo render into - // ch0/ch1 (then replicate any extra channels); exactly 1 -> the mono render. Either way the - // render ADDS into a cleared buffer — RT-safe (no alloc/IO/lock). NEVER reads the mode here. + // Render per the host's negotiated channel count (mode was baked into the decode + // off-thread, so the mode itself is never read here): >=2 channels -> stereo into + // ch0/ch1 (then mirror extras); exactly 1 -> mono. Adds into a cleared buffer. float* ch0 = out.numChannels > 0 ? out.channelBuffers32[0] : nullptr; float* ch1 = out.numChannels > 1 ? out.channelBuffers32[1] : nullptr; if (ch0 && ch1) { - // Stereo: clear both, render L/R. A mono sample plays dual-mono via the engine's stereo - // path (both channels equal), so a mono capture in stereo mode is centered, not silent. - // The DRAIN engine's ringing tails ADD on top (render mixes into the cleared buffer). + // A mono sample plays dual-mono via the engine's stereo path, so a mono capture in + // stereo mode is centered, not silent. The drain engine's ringing tails add on top. for (int32 i = 0; i < frames; ++i) { ch0[i] = 0.f; ch1[i] = 0.f; } if (inst) inst->engine.render(ch0, ch1, static_cast(frames)); if (drain) drain->engine.render(ch0, ch1, static_cast(frames)); - // FB1 post-mixer master gain: ramp gainCurrent_ toward the atomic target per-sample so - // continuous knob drags produce no zipper noise and the true-zero bottom causes no click. - // Applied AFTER the voice sum and BEFORE the extra-channel mirror + peak so both see the - // actual output. Branch-free inner loop; early-out when already at target. RT-safe. + // Post-mixer master gain: ramp gainCurrent_ toward the atomic target per-sample so + // knob drags produce no zipper noise. Early-out when already at target. { const float gTarget = masterGain_.load(std::memory_order_relaxed); - const float gStep = gainRampStep_; // T3-01: rate-derived per-sample step + const float gStep = gainRampStep_; // rate-derived per-sample step const float gSnap = 0.5f * gStep; const float diff = gTarget - gainCurrent_; if (diff < -gSnap || diff > gSnap) { @@ -349,13 +294,13 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) { } } } - // Any channels beyond the first two mirror ch0 (defensive — REAPER negotiates 1 or 2). + // Channels beyond the first two mirror ch0 (defensive — REAPER negotiates 1 or 2). for (int32 ch = 2; ch < out.numChannels; ++ch) { if (float* buf = out.channelBuffers32[ch]) { for (int32 i = 0; i < frames; ++i) buf[i] = ch0[i]; } } - // Block peak (max across L/R) for the embed strip's level indicator; RT-safe. + // Block peak (max across L/R) for the embed strip's level indicator. float peak = 0.f; for (int32 i = 0; i < frames; ++i) { const float a0 = ch0[i] < 0.f ? -ch0[i] : ch0[i]; @@ -365,16 +310,14 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) { } embedPeak_.store(peak, std::memory_order_relaxed); } else if (ch0) { - // Mono: render into channel 0, replicate to any extra channels (mono bus is 1 channel; - // the replicate is defensive for a host that still hands >1 channel on a mono bus). + // Mono: render into channel 0, replicate to any extra channels (defensive). for (int32 i = 0; i < frames; ++i) ch0[i] = 0.f; if (inst) inst->engine.render(ch0, static_cast(frames)); if (drain) drain->engine.render(ch0, static_cast(frames)); - // FB1 post-mixer master gain (mono path) — same ramp contract as the stereo branch: - // post-sum, pre-peak/replicate, per-sample gainCurrent_ ramp toward target, RT-safe. + // Same gain-ramp contract as the stereo branch above. { const float gTarget = masterGain_.load(std::memory_order_relaxed); - const float gStep = gainRampStep_; // T3-01: rate-derived per-sample step + const float gStep = gainRampStep_; // rate-derived per-sample step const float gSnap = 0.5f * gStep; const float diff = gTarget - gainCurrent_; if (diff < -gSnap || diff > gSnap) { @@ -405,9 +348,8 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) { } } - // Report silence only when nothing is loaded (lets the host optimize when idle). - // With an instrument loaded — or a drain snapshot still ringing out — we clear the - // flag so a ringing voice is not skipped. + // Report silence only when nothing is loaded (lets the host optimize when idle); with + // a drain snapshot still ringing out, clear the flag so it is not skipped. out.silenceFlags = (inst || drain) ? 0 : ((out.numChannels >= 64) ? ~0ULL diff --git a/src/shell/instrument/reasampler_processor.h b/src/shell/instrument/reasampler_processor.h index 3c01c6c..d5bb9da 100644 --- a/src/shell/instrument/reasampler_processor.h +++ b/src/shell/instrument/reasampler_processor.h @@ -1,28 +1,9 @@ -// reasampler_processor.h — the VST3 SingleComponentEffect (Phase S4, Tier 0). Wires the -// pure S3 sampler core into a real VSTi: it declares an event-input bus + a stereo audio -// output bus, marshals host MIDI note-on/off into the VoiceEngine, and renders the -// engine's audio into the output bus — so a chosen bank sample plays chromatically from -// its root note in REAPER's routing/record/render path. -// -// SingleComponentEffect is the SDK's combined processor+controller base — sanctioned -// for a non-distributable, REAPER-only plugin under D5/D6. It gives us -// addAudioOutput/addEventInput, IComponent setState/getState for the instance's own -// state (the selected sample), and the IEditController seat so createView() can hand the -// host our IPlugView LICE editor. -// -// SELF-CONTAINED PLAYBACK (pS architecture correction). The instance OWNS its sample: the -// component state persists, per referenced bank sample, the project-relative WAV path + -// decode intrinsics (SampleRefs), and reloadInstrument decodes straight from that table. -// The extension's bank blob is a BROWSER SOURCE that opportunistically refreshes the refs -// when readable — NEVER a runtime requirement for playback. A project restored before the -// extension's PROJEXTSTATE parses (or with the extension absent) plays on load; the old -// reopen-heal timer + poll-to-play machinery that papered over the bank dependency is gone. -// -// REAL-TIME DISCIPLINE (S4 hard constraint). The audio thread (process) does NO -// allocation, NO file I/O, NO bridge calls, NO locks. Sample loading — ref resolve, WAV -// decode, keymap build, VoiceEngine construction — all happens OFF the audio thread -// (reloadInstrument, driven from the main/UI thread) and is handed to process via a -// single atomic pointer swap. See the LoadedInstrument handoff below. +// reasampler_processor.h — VST3 SingleComponentEffect wiring the pure sampler core into +// a playable instrument: event-input + stereo output bus, MIDI -> VoiceEngine, render. +// Self-contained playback: component state owns per-sample WAV path + decode intrinsics +// (SampleRefs); the bank blob is an opportunistic browser source, never a playback +// dependency. Audio thread (process()) does no allocation/file-IO/bridge calls/locks; +// loading happens off-thread (reloadInstrument) and hands off via one atomic pointer swap. #pragma once @@ -42,37 +23,24 @@ namespace reasampler::vst { -// Cross-subsystem deps by their real namespace homes (Q-W2v: the core/namespaces.h shim -// is retired from the processor family; the engine family's symbols — Keymap, VoiceEngine, -// ChannelMode, VoiceMode, MonoTrigger, the voice-count constants — still live in flat -// `reasampler` and resolve via the enclosing namespace). using instrument::map::ComponentState; using instrument::map::PerformanceMap; using instrument::map::SampleRefs; using instrument::map::kPreviewVelocityDefault; -class ReaSamplerEmbed; // S6 embedded TCP/MCP UI shell (owned below; see queryInterface) +class ReaSamplerEmbed; // embedded TCP/MCP UI shell (owned below; see queryInterface) -// One fully-built, ready-to-play instrument snapshot: the decoded keymap and the voice -// engine that plays it. The engine holds references into the keymap, so the two MUST live -// and die together at a STABLE address — hence this is heap-allocated and neither copyable -// nor movable. The audio thread only ever reads it through an atomic pointer; it is built -// and destroyed off the audio thread. -// -// installedAt: the reloadGeneration_ value at which this instrument was atomically -// installed into live_. Set on the reload path before the exchange. process() publishes -// this field (not a fresh re-read of reloadGeneration_) so the published generation is -// exactly the generation of the instrument actually in hand for the block. +// Decoded keymap + the voice engine playing it. The engine holds references into the +// keymap, so both must live/die together at a stable address — heap-allocated, +// non-copyable, non-movable. process() only ever reads this through an atomic pointer. struct LoadedInstrument { Keymap keymap; VoiceEngine engine; - std::uint64_t installedAt = 0; // reload generation at which this was installed + std::uint64_t installedAt = 0; // reloadGeneration_ at which this was installed into live_ - // The takeover declick (GA fix, rev 2) is opted IN here — the PRODUCT default: any - // restart of a sounding voice (mono Retrigger takeover/fallback, cross-sample legato - // restart, POLY at-cap steal — the preview note included, now that it is a real pool - // voice) smooths the cut via the difference-seeded ramp instead of clicking. The pure - // core defaults it off (regression baseline) — same layering as kDefaultPitchEngine. + // Takeover declick is on by default here (product default; the pure core defaults it + // off): any voice restart (mono retrigger, legato, poly steal, preview) ramps instead + // of clicking. LoadedInstrument(Keymap km, std::size_t maxVoices, std::uint64_t gen, std::size_t preserveVoiceCap = 0, std::int64_t preserveWindowFrames = 0, @@ -83,9 +51,8 @@ struct LoadedInstrument { voiceMode, monoTrigger, /*takeoverDeclick=*/true), installedAt(gen) {} - // True when nothing in this snapshot is sounding. process() publishes this for the - // drain slot so the off-thread retirer can park an idle drain in the graveyard early - // (FA1-review Major #2). Bounded scan (<= maxVoices). + // True when nothing in this snapshot is sounding; lets the off-thread retirer park an + // idle drain early. Bounded scan (<= maxVoices). bool fullyIdle() const { return engine.activeVoiceCount() == 0; } LoadedInstrument(const LoadedInstrument&) = delete; @@ -95,8 +62,8 @@ struct LoadedInstrument { class ReaSamplerProcessor : public Steinberg::Vst::SingleComponentEffect { public: ReaSamplerProcessor() = default; - // Out-of-line so the owned ReaSamplerEmbed (held by unique_ptr, forward-declared here) - // is a complete type at the destruction point (defined in the .cpp). + // Out-of-line so the owned ReaSamplerEmbed (unique_ptr, forward-declared here) is + // complete at the destruction point (defined in the .cpp). ~ReaSamplerProcessor() override; // The factory create function (registered in vst_entry.cpp). @@ -109,9 +76,9 @@ public: Steinberg::tresult PLUGIN_API terminate() override; Steinberg::tresult PLUGIN_API setActive(Steinberg::TBool state) override; - // Instance state = the selected bank sample id (D-B: a performance choice the - // instrument owns; NEVER written back to the bank). Component-state, so a saved - // REAPER project restores which sample each instance plays. + // Instance state = the selected bank sample id (a performance choice the instrument + // owns; never written back to the bank). Component-state, so a saved project restores + // which sample each instance plays. Steinberg::tresult PLUGIN_API setState(Steinberg::IBStream* state) override; Steinberg::tresult PLUGIN_API getState(Steinberg::IBStream* state) override; @@ -122,11 +89,9 @@ public: Steinberg::tresult PLUGIN_API process( Steinberg::Vst::ProcessData& data) override; - // Output-bus negotiation. The instrument has ONE canonical output arrangement: a FIXED - // stereo bus (GA fix — the channel mode is a decode policy, never a bus fact; mono mode - // renders dual-mono through it). We accept the host's proposal only when it is a single - // stereo output; otherwise we reject (kResultFalse) but keep our stereo arrangement, so - // getBusArrangement / getBusInfo always report 2 channels and the host routes accordingly. + // Fixed stereo output bus — channel mode is a decode policy, never a bus fact; mono + // renders dual-mono through it. Do not reintroduce per-instance bus renegotiation. + // Accepts only a single stereo output proposal; otherwise rejects and keeps stereo. Steinberg::tresult PLUGIN_API setBusArrangements( Steinberg::Vst::SpeakerArrangement* inputs, Steinberg::int32 numIns, Steinberg::Vst::SpeakerArrangement* outputs, Steinberg::int32 numOuts) override; @@ -135,107 +100,73 @@ public: // Hands the host our LICE IPlugView editor. Steinberg::IPlugView* PLUGIN_API createView(Steinberg::FIDString name) override; - // Override queryInterface to additionally expose REAPER's IReaperUIEmbedInterface (S6): - // REAPER queries the IEditController for it to drive the inline TCP/MCP embed surface. - // All other iids delegate to SingleComponentEffect's implementation unchanged. + // Additionally exposes REAPER's IReaperUIEmbedInterface (queried by REAPER to drive the + // inline TCP/MCP embed); all other iids delegate to SingleComponentEffect unchanged. Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid, void** obj) override; - // The embedded-strip activity level (0..1), read by the S6 embed shell on the UI thread. - // Backed by embedPeak_, the per-block mono peak the audio thread stores relaxed — a - // lock-free advisory readout, never touched with a lock the audio thread could contend. + // The embedded-strip activity level (0..1) for the embed shell, UI thread. Backed by + // embedPeak_, a lock-free relaxed atomic the audio thread writes each block. double embedActivityLevel() const { return static_cast(embedPeak_.load(std::memory_order_relaxed)); } - // Called by the editor (main/UI thread) when the user picks a sample, and internally - // on load. SELF-CONTAINED (pS): resolves the selection/zones against the instance-OWNED - // SampleRefs table, decodes each WAV OFF the audio thread, and publishes the built - // instrument to process() via an atomic swap — NO bank read is required for playback. - // When the live bank blob IS readable it is first folded into the refs table - // (refreshRefsFromBank), which is both the browser's copy-the-ref-in mechanism and the - // S9 live-recapture sync. A missing/unreadable WAV is the defined no-play (silence, no - // retry). Returns the resolved selection id ("" if nothing was loaded) for the editor. + // Resolves selection/zones against the instance-owned SampleRefs, decodes each WAV + // off-thread, and publishes the built instrument via atomic swap — no bank read + // required. When the bank blob is readable it's first folded into the refs table + // (refreshRefsFromBank; the browser's copy-the-ref-in + recapture-sync mechanism). A + // missing/unreadable WAV is the defined no-play (silence, no retry). Returns the + // resolved selection id ("" if nothing loaded). std::string reloadInstrument(); - // The result of a bank-sync poll (S9/S8): what pollBankSync did this tick, so the editor - // can react (repaint / re-snapshot its own view) only when something actually changed. + // What pollBankSync did this tick, so the editor can react only when something changed. struct BankSyncResult { - // The bank generation changed (or a pre-v10 legacy lift landed an instrument) -> - // reloadInstrument ran and the editor should re-snapshot its bank view. - bool reloaded = false; + bool reloaded = false; // bank generation changed (or a legacy lift landed) -> reloaded bool applied = false; // a new assignment request was applied -> selection changed }; - // Poll the S9 bank-generation counter and the S8 assignment request over the bridge, OFF - // THE AUDIO THREAD (the editor's UI timer drives this — NEVER process()). This is an - // EDITOR/BROWSER sync path — playback never depends on it (pS). Semantics: - // * S9: if the bank generation differs from what we last saw, call reloadInstrument() so - // a recapture/ingest refreshes playback hands-free (atomic swap, glitch-free). - // * S8: if a NEW (generation > last consumed) assignment request names a resolvable - // sample AND this instance is the target (isFocusedTarget), apply it as the selection - // and reload; an unresolvable request is DROPPED silently (marker advanced, no change); - // a non-target instance neither applies nor advances its marker. - // * LEGACY LIFT: a pre-v10 blob restored with intent but no refs retries the (cheap) - // bank read until the blob is parseable, then reloads ONCE to copy the refs in. - // TERMINATING: once the blob parses and NO referenced id resolves, the ids are - // provably stale — the lift concludes permanently (legacyLiftShouldRun) instead of - // churning a full bank read + reload every tick forever. - // The consumed marker advances in component state (marked dirty via the host handler) so a - // re-open does not re-apply. `isFocusedTarget` is the shell's thundering-herd policy input - // (the editor passes true only for the instance whose editor is open — see the handoff). - // Idempotent on an idle tick (generation unchanged + no new request -> no work). + // Off-thread poll (editor's UI timer only) of the bank generation + assignment request; + // playback never depends on it. Generation change -> reload; a resolvable NEW assignment + // targeting this instance (isFocusedTarget) -> apply as selection + reload (unresolvable + // ones drop silently, marker still advances); pre-v10 legacy blobs retry the bank read + // until the refs lift in, then stop (legacyLiftShouldRun). The consumed marker persists + // so a re-open does not re-apply. Idempotent on an idle tick. BankSyncResult pollBankSync(bool isFocusedTarget); // The bridge, for the editor's live-state readout + sample list. Owned here; the // editor borrows it (outlives the editor). ReaperBridge& bridge() { return bridge_; } - // The live host sample rate latched from setupProcessing (the SAME rate reloadInstrument - // resolves seconds->frames against). The editor's S-VIEW-3 envelope overlay reads it to place - // its wall-clock seconds on the same time base the voice engine plays them over. 0.0 before - // setupProcessing runs (the editor guards). Read on the UI thread; a plain load — sampleRate_ - // is set once by setupProcessing before any audio and does not change under the editor. + // The live host sample rate latched from setupProcessing; the editor's envelope overlay + // shares this time base. 0.0 before setupProcessing runs. double sampleRate() const { return sampleRate_; } - // The current single-capture selection id (main/UI thread reads for the editor). Guarded - // by selectionMutex_ — never touched on the audio thread. Since S10 this is the ONE picked - // capture the default face plays chromatically when the performance map is empty; an EMPTY - // id resolves to SILENCE (no first-sample fallback). A non-empty zoned map supersedes it. + // The single-capture selection id (guarded by selectionMutex_, never read on the audio + // thread): the default face's pick when the performance map is empty; a non-empty map + // supersedes it. Empty id -> silence, no first-sample fallback. std::string selectedSampleId(); void setSelectedSampleId(const std::string& id); - // The performance map (Tier 1: the zoned keymap the instrument owns; D-B). Read/written - // by the editor on the UI thread; snapshotted under performanceMutex_. NEVER read on the - // audio thread — reloadInstrument bakes it into the LoadedInstrument's Keymap off-thread. + // The performance map (zoned keymap). UI thread, guarded by performanceMutex_; never + // read on the audio thread — reloadInstrument bakes it into the Keymap off-thread. PerformanceMap performanceMap(); void setPerformanceMap(const PerformanceMap& map); - // The per-instance channel mode (S7, D-E: mono | stereo). Read/written on the UI thread - // (the editor toggle) and read off-thread by getState/reloadInstrument; guarded by - // channelModeMutex_. NEVER read on the audio thread — process() renders against the host's - // negotiated output channel count, and reloadInstrument bakes the mode into the decode. - // GA fix: the mode is a DECODE policy only (downmix vs L/R split). The output bus is a - // FIXED stereo bus — mono mode renders dual-mono through it (centered) — so a mode change - // never renegotiates host I/O (the mono<->stereo bus flip's live pin remap was the - // hard-right-pan defect). + // Per-instance channel mode (mono | stereo), guarded by channelModeMutex_, never read + // on the audio thread. Decode policy only (downmix vs L/R split) — the output bus is + // fixed stereo, so a mode change never renegotiates host I/O. ChannelMode channelMode(); - // Sets the mode from the EDITOR TOGGLE (a deliberate user choice): latches the mode - // EXPLICIT (the GA auto-default stops fighting it), and on a CHANGE reloads the instrument - // so the next block decodes the new channel count. UI thread only. + // Editor toggle: latches the mode explicit (auto-default stops fighting it) and + // reloads so the next block decodes the new channel count. UI thread only. void setChannelMode(ChannelMode mode); - // The per-instance preview-trigger velocity (S-VIEW-4, MIDI 1..127). Read/written on the - // UI thread (the Sample-view velocity knob) and by getState/setState (host load-save thread); - // guarded by previewMutex_. Persisted in component state (v6). NOT read on the audio thread. + // Per-instance preview-trigger velocity (MIDI 1..127), guarded by previewMutex_, not + // read on the audio thread. std::uint8_t previewVelocity(); void setPreviewVelocity(std::uint8_t velocity); - // --- Phase S voice-system parameters (per-instance, persisted in component state v7) --- - // Read/written on the UI thread (the editor's voice deck) and by getState/setState; guarded - // by voiceParamsMutex_. NOT read on the audio thread — each setter rebuilds the VoiceEngine - // OFF-thread via rebuildVoiceEngine (a LIGHT rebuild around the already-decoded keymap; no - // bridge read, no WAV re-decode) published through the same tail-preserving drain-slot swap, - // so changing polyphony / mode / the retrigger toggle never cuts a ringing tail. + // Voice-system parameters (per-instance), guarded by voiceParamsMutex_, not read on the + // audio thread — each setter rebuilds via rebuildVoiceEngine (already-decoded keymap, no + // bridge/WAV re-read) through the same drain-slot swap, so a change never cuts a tail. int voiceCount(); void setVoiceCount(int count); // clamped to kMinVoiceCount..kMaxVoiceCount VoiceMode voiceMode(); @@ -243,251 +174,160 @@ public: MonoTrigger monoTrigger(); void setMonoTrigger(MonoTrigger trigger); - // --- FB1 post-mixer master gain (per-instance, persisted in component state v8) --------- - // LINEAR gain in [0, masterGainMaxLinear()] (0.0 = -inf/true silence, 1.0 = unity, cap = - // +24 dB; the pure master_gain module owns the dB knob taper). Held in an atomic so the - // audio thread applies it with ONE relaxed load per block as a post-sum multiply over the - // rendered output (engine + drain + preview) — no lock, no rebuild, no per-voice cost. - // Written by the editor's Gain knob (UI thread) and setState; read by getState + process(). + // Post-mixer master gain, linear in [0, masterGainMaxLinear()] (0 = true silence, 1 = + // unity, cap +24 dB). Atomic — the audio thread applies it as a per-block post-sum + // multiply, no lock, no rebuild. double masterGainLinear() const { return static_cast(masterGain_.load(std::memory_order_relaxed)); } void setMasterGainLinear(double linear); // clamped to [0, masterGainMaxLinear()] - // Fire a one-shot PREVIEW note-on / note-off through the live instrument's MAIN - // VoiceEngine — the SAME noteOn/noteOff calls host MIDI takes, so a preview is a REAL - // voice: it counts against the voice count, can steal / be stolen, and respects - // Poly/Mono + Retrigger/Legato (deliberate reversal of the retired PreviewCard's - // isolation — preview must obey voicing). The editor posts the loaded capture's / - // selected zone's ROOT note (plays at unity); previewNoteOn plays it at the current - // previewVelocity() (the velocity curve applies); previewNoteOff releases it (Gate) — - // Trigger zones ignore note-off and play through. OFF the audio thread (the editor's - // preview-trigger button, UI thread); the request is handed to process() via a - // lock-free single-slot mailbox drained at block start — no allocation, no lock on the - // audio thread. A momentary button (down = on, up = off) reads as a natural key press. - // This is PLAYBACK ONLY: it never captures, never inserts a timeline item. + // Fires a one-shot preview note-on/off through the live VoiceEngine — the same + // noteOn/noteOff host MIDI uses, so a preview is a real voice (counts against voice + // count, can steal/be stolen, respects Poly/Mono + Retrigger/Legato). Off the audio + // thread; handed to process() via a lock-free single-slot mailbox drained at block + // start. Never captures, never inserts a timeline item. void previewNoteOn(int note); void previewNoteOff(int note); - // The instance-owned sample refs (pS self-contained playback): a snapshot copy for the - // editor (waveform/loop-intrinsic fallback when the bank blob is not readable). UI - // thread; guarded by refsMutex_. + // Snapshot copy of the instance-owned sample refs, for the editor's waveform/loop + // fallback when the bank blob is unreadable. Guarded by refsMutex_. SampleRefs sampleRefs(); private: - // Phase S drain retirement (FA1-review Major #2): if process() has published that the - // CURRENT drain instrument is fully idle (every engine voice silent), - // move it out of the drain slot into the graveyard and prune — so an edited-away snapshot - // stops costing resident memory as soon as its tails die, instead of squatting in the slot - // until the NEXT reload. Off the audio thread only (takes reloadMutex_); driven from - // pollBankSync's UI-timer tick (the same cadence that drives reloads — an idle drain with - // no editor open simply waits for the next reload/deactivate, exactly the pre-fix bound). - // Safe against a racing process(): idleness is monotone (the drain receives no note-ons) - // and the published value names the drain's OWN installedAt, so a stale publication about - // an OLDER drain can never retire a newer one; the graveyard prune's monotone-generation - // proof (see below) covers the free. + // If process() published that the drain instrument is fully idle, move it into the + // graveyard and prune — so an edited-away snapshot stops costing memory as soon as its + // tails die. Off the audio thread only (driven by pollBankSync); safe against a racing + // process() because idleness is monotone and the publication names the drain's own + // installedAt (a stale value can never retire a newer occupant). void retireIdleDrain(); - // Phase S voice-param LIGHT rebuild (voice-review Major #3): rebuild the engine - // around a COPY of the LIVE instrument's already-decoded Keymap — no bridge read, no - // filesystem, no WAV re-decode — and publish through the same tail-preserving drain-slot - // swap as a full reload. A polyphony/mode/trigger change touches no audio data, so the - // full reloadInstrument (which re-decodes every zone WAV from disk on the UI thread) was - // pure waste — a visible UI stall on a many-zone instrument. Copying the keymap is safe: - // it is immutable after construction and, under reloadMutex_, the live instrument can - // neither be swapped nor freed while we read it. When nothing is loaded this is a no-op — - // the new params bake into the next real reload. Off the audio thread only. + // Light voice-param rebuild: rebuilds the engine around a copy of the live instrument's + // already-decoded Keymap (no bridge/disk) and publishes through the same drain-slot + // swap as a full reload. No-op when nothing is loaded. Off the audio thread only. void rebuildVoiceEngine(); - // The pre-v10 LEGACY LIFT gate (#A): true when a lift attempt this tick could make - // progress. Latches legacyLiftConcluded_ on a Stale proof (see the member below); the - // pure decision itself is sample_map's legacyLiftDecision. Off the audio thread only - // (bridge read + bank parse). + // Pre-v10 legacy-lift gate: true when a lift attempt this tick could make progress + // (see legacyLiftConcluded_). Off the audio thread only (bridge read + bank parse). bool legacyLiftShouldRun(); - // Publish `built` (null = install silence) into live_: prune the graveyard by the last - // process()-published generation, swap `built` into live_, displace the previous live into - // the drain slot, and park the drain-evicted instrument in the graveyard. REQUIRES - // reloadMutex_ held — factored out so reloadInstrument and rebuildVoiceEngine share the ONE - // safety-critical swap dance (see the handoff proof below). + // Publishes `built` (null = install silence) into live_: prunes the graveyard by the + // last process()-published generation, swaps `built` into live_, displaces the previous + // live into the drain slot, and parks the evicted drain instrument in the graveyard. + // Requires reloadMutex_ held — shared by reloadInstrument and rebuildVoiceEngine. void publishBuiltLocked(std::unique_ptr built); - // pS-usage: publish this instance's held captures to its per-instance ext-state key - // ("rsusage_") so the extension's prune counts them as referenced — a - // capture a live instance holds can never be pruned. Called at the end of every - // reloadInstrument (the ONE choke point every play-set change funnels through: - // selection change, zone edits, assignment consume, bank refresh, setState load), so - // publishing is EAGER and needs no timer — a closed-editor instance's record is - // already in ext-state from its last change/load. OFF THE AUDIO THREAD only (bridge - // calls). Mints instanceGuid_ on first need; RE-mints when planUsagePublish detects - // this state was cloned onto another track (FX copy / track duplication). Idempotent - // on an unchanged play-set (skipWrite). `refs`/`ids` are reloadInstrument's own - // snapshot — the refs table and the id set the instance currently plays. + // Publishes this instance's held captures to its per-instance ext-state key + // ("rsusage_") so the extension's prune can never reclaim them. Called at + // the tail of every reloadInstrument, off the audio thread. Mints instanceGuid_ on + // first need; re-mints on a detected clone (FX copy / track duplication). void publishUsage(const SampleRefs& refs, const std::vector& ids); ReaperBridge bridge_; - // --- The audio-thread handoff (S4 real-time discipline, FA1 drain slot) -- - // process() atomically loads `live_` AND `draining_` at block start and marshals/renders - // against them — two atomic acquires, no lock, no free on the audio thread. + // --- The audio-thread handoff (drain slot) --- + // process() atomically loads live_ + draining_ at block start (two acquires, no lock). + // reloadInstrument() (off-thread, serialized by reloadMutex_) swaps a new build into + // live_; the displaced instrument moves to draining_, where process() keeps rendering + // its already-sounding voices (and routes note-offs to it) so a reload never cuts a + // ringing note — new note-ons go only to live_. The instrument evicted from draining_ + // (two reloads old) parks in graveyard_ for reclaim. // - // reloadInstrument() (off-thread, serialized by reloadMutex_) builds a new - // LoadedInstrument and atomically swaps it into `live_`. The DISPLACED instrument is - // NOT freed and NOT silenced: it moves into `draining_`, where process() keeps - // rendering its already-sounding voices (and routes note-offs to it) so a reload — - // a curve/param edit, a bank-generation refresh, an applied assignment — never cuts a - // ringing note (FA1, bug 3b). New note-ons go ONLY to the live instrument, so the next - // trigger plays the new state. The instrument evicted FROM the drain slot (two reloads - // old) is parked in `graveyard_` for reclaim — a rapid second reload hard-cuts only the - // oldest edit's tails (bounded compromise, documented). + // Reclaim: process() publishes the minimum installedAt it holds via processGeneration_ + // (one relaxed store); the reload path frees graveyard entries older than that. Safe + // because both slots are monotone in installedAt, so the published minimum is monotone + // and an entry only reaches the graveyard after leaving both slots under reloadMutex_ — + // an entry below the published minimum can never be loaded again. // - // Bounded reclaim: process() publishes the MINIMUM installedAt over the (non-null) - // pointers it holds this block via processGeneration_ — a single atomic store, RT-safe. - // The reload path frees graveyard entries whose installedAt < seen (the last published - // value). - // - // Safety argument: both slots are monotone in installedAt over time (live_ receives - // successively newer builds; draining_ receives successively newer displaced lives), so - // the published minimum is monotone across blocks, and any future process() load yields - // installedAt >= seen. An entry only reaches the graveyard by leaving BOTH slots - // (single-writer under reloadMutex_), so a graveyard entry with installedAt < seen can - // never again be loaded and is not currently held — freeing it is safe. process() - // publishes BEFORE rendering, so the pointers it renders with are covered by the value - // the pruner reads (a stale lower read is merely conservative). - // - // The graveyard's upper bound is the number of reloads since process last ran - // (typically 0–1 in normal use). Remaining entries drain at setActive(false) / - // terminate(), when the host guarantees process is stopped. + // Graveyard upper bound: reloads since process last ran (typically 0-1). Remaining + // entries drain at setActive(false) / terminate(), when process is guaranteed stopped. std::atomic live_{nullptr}; std::atomic draining_{nullptr}; // displaced instrument still rendering its tails std::atomic reloadGeneration_{0}; // incremented by each reload (off-thread, under reloadMutex_; read atomically by process) std::atomic processGeneration_{0}; // min installedAt held by process (written on audio thread, read off-thread) - // Phase S: the installedAt of the drain instrument process() last observed FULLY IDLE - // (every engine voice silent; 0 = none / the current drain still sounds). Written relaxed on the audio thread each - // block; read by retireIdleDrain() off-thread. Naming the generation (not a bool) closes - // the swap race: a publication about an old drain can never retire its successor. + // The installedAt of the drain instrument process() last observed fully idle (0 = none / + // still sounds). Written relaxed on the audio thread each block; read by retireIdleDrain() + // off-thread. Naming the generation (not a bool) closes the swap race: a publication about + // an old drain can never retire its successor. std::atomic drainIdleGeneration_{0}; std::vector> graveyard_; // drained on reclaim + setActive(false) + terminate std::mutex reloadMutex_; // serializes off-thread reloads + graveyard access - // The single-capture selection id (S10: the ONE picked capture; "" = no pick -> silence). - // Off-thread only; a small mutex guards the string against a getState/editor race. NOT - // read on the audio thread. + // The single-capture selection id ("" = no pick -> silence). Off-thread only, not read + // on the audio thread. std::mutex selectionMutex_; std::string selectedSampleId_; - // The performance map (Tier 1: the instrument's owned zoned keymap). Off-thread only; - // guarded against a getState/editor race. NOT read on the audio thread — reloadInstrument - // bakes it into the LoadedInstrument's Keymap under the reload lock. + // The performance map (zoned keymap). Off-thread only; reloadInstrument bakes it into + // the Keymap under the reload lock, never read directly on the audio thread. std::mutex performanceMutex_; PerformanceMap performanceMap_; - // The instance-OWNED sample refs (pS self-contained playback): the path + intrinsics - // per referenced bank sample that setState restores, reloadInstrument resolves/decodes - // from, and getState persists (v10). Refreshed opportunistically from the bank blob - // when it is readable; NEVER a bank dependency for playback. Off-thread only (UI + - // load/save + reload); guarded against a getState/reload race. NOT read on the audio - // thread. + // Instance-owned sample refs: path + intrinsics per referenced sample. Refreshed + // opportunistically from the bank blob when readable; never a bank dependency for + // playback. Off-thread only. std::mutex refsMutex_; SampleRefs sampleRefs_; - // pS-usage publish identity + lifetime nonce (see publishUsage). instanceGuid_ is - // the persisted per-instance identity (ComponentState v11; empty until first - // publish); usageNonce_ is THIS incarnation's per-LIFETIME owner nonce, carried - // INSIDE the published wire (UsageRecord.ownerNonce) — planUsagePublish's exact - // ownership discriminator between "my own write" (clean replace) and "a foreign - // writer" (union / re-mint). NEVER persisted: a persisted nonce would clone with - // the state on FX copy, and two same-track copies converging on byte-identical - // wires is exactly the ambiguity the nonce exists to break (a wire-equality - // discriminator let sibling A clean-replace over sibling B's still-held paths — - // the delete direction). Minted lazily on first publish; cleared on setState (a - // restored blob is a new lifetime). Guarded by usageMutex_ (publish runs under - // reloadMutex_ but getState/setState do not). + // Usage-publish identity (see publishUsage). instanceGuid_ is the persisted per-instance + // identity; usageNonce_ is this incarnation's per-lifetime owner nonce (never persisted — + // a persisted nonce would clone with the state on FX copy, letting a sibling clean- + // replace over another's held paths). Minted lazily; cleared on setState. std::mutex usageMutex_; std::string instanceGuid_; std::string usageNonce_; - // The per-instance channel mode (S7). Off-thread only (UI + getState + reloadInstrument); - // guarded against a getState/editor race. Default Mono preserves pre-S7 behavior. NOT read - // on the audio thread — process renders against the host's negotiated output channel count. - // channelModeExplicit_ (GA, persisted v9): false = the mode is an un-touched default that - // reloadInstrument may auto-default from the loaded capture's channel count; true = the user - // deliberately toggled the mode (setChannelMode latches it) and it is never fought. + // Per-instance channel mode, default Mono; not read on the audio thread (process + // renders against the host's negotiated channel count). channelModeExplicit_: false = + // reloadInstrument may auto-default the mode from the loaded capture; true = the user + // deliberately toggled it (never fought thereafter). std::mutex channelModeMutex_; ChannelMode channelMode_ = ChannelMode::Mono; bool channelModeExplicit_ = false; - // The last assignment-request generation this instance CONSUMED (S8 reader). Persisted in - // component state (v5) so a re-open does not re-apply a request the user already got and - // then changed away from. Written by pollBankSync (UI/timer thread) and getState; read by - // pollBankSync + getState; seeded by setState. Guarded against a getState/poll race. NEVER - // read on the audio thread. Default 0 -> a genuinely new first assign (gen >= 1) applies. + // The last assignment-request generation consumed, persisted so a re-open does not + // re-apply a stale request. Default 0 -> a genuinely new first assign (gen >= 1) applies. std::mutex assignMarkerMutex_; std::int64_t lastConsumedAssignGeneration_ = 0; - // The bank generation this instance last SAW (S9 reader). UI/timer-thread only (pollBankSync - // is the sole reader/writer) — no mutex needed, and it is NOT persisted. Initialized to a - // -1 SENTINEL (no real generation can be negative — parseBankGeneration yields >= 0) so the - // FIRST poll after an editor open BASELINES the seen value without a redundant reload - // (setState already loaded the instrument from the OWNED refs); a subsequent generation - // CHANGE then drives the reload. Since pS there is NO reopen-heal here: playback never - // depends on this poll — a v10 blob plays from its own refs at setState time. Besides a - // generation change, pollBankSync reloads only for an APPLIED S8 assignment and for the - // pre-v10 LEGACY LIFT. NOT read on the audio thread. + // The bank generation this instance last saw. UI/timer-thread only (pollBankSync's sole + // reader/writer), not persisted. -1 sentinel baselines the first poll without a + // redundant reload; a later generation change then drives the reload. std::int64_t lastSeenBankGeneration_ = -1; - // The pre-v10 LEGACY LIFT's terminating latch (#A): set once legacyLiftShouldRun proves - // the referenced ids STALE against a readable bank blob (LegacyLiftDecision::Stale) — - // there is nothing to lift, so the lift stops re-firing (the steady state is one relaxed - // load per tick, no bank read). Reset by setState (a new blob = new facts). NOT consulted - // by the genChanged/applied reload paths, so a later bank change that re-introduces an id - // (e.g. an extension-side undo) still refreshes the refs — the latch only gates the lift. - // Atomic: written on the UI-timer thread (pollBankSync) and the host load thread (setState). + // Legacy-lift terminating latch: set once legacyLiftShouldRun proves the referenced ids + // stale against a readable bank blob, so the lift stops re-firing every tick. Reset by + // setState (a new blob = new facts). std::atomic legacyLiftConcluded_{false}; - // S-VIEW-4 preview-trigger velocity (MIDI 1..127). Persisted in component state (v6) so the - // user's chosen strike velocity survives a project save/reload. Since Wave 2 the Sample-view - // velocity knob writes it on the UI thread, so it is guarded by previewMutex_; setState and - // getState (load/save thread) share the same guard. Default kPreviewVelocityDefault (64). NOT - // read on the audio thread. + // Preview-trigger velocity (MIDI 1..127, persisted). Default kPreviewVelocityDefault + // (64). Not read on the audio thread. std::mutex previewMutex_; std::uint8_t previewVelocity_ = kPreviewVelocityDefault; - // Phase S voice-system parameters (per-instance, persisted in component state v7). Off-thread - // only (UI voice deck + getState/setState + reloadInstrument); guarded against a getState/editor - // race. Defaults {16, Poly, Retrigger} reproduce pre-Phase-S behavior. NOT read on the audio - // thread — reloadInstrument bakes them into the LoadedInstrument's engine off-thread. + // Voice-system parameters (per-instance, persisted). Defaults {16, Poly, Retrigger}. + // Not read on the audio thread — reloadInstrument bakes them into the engine off-thread. std::mutex voiceParamsMutex_; int voiceCount_ = kDefaultVoiceCount; VoiceMode voiceMode_ = VoiceMode::Poly; MonoTrigger monoTrigger_ = MonoTrigger::Retrigger; - // FB1 post-mixer master gain (LINEAR; persisted in component state v8). A lock-free - // atomic — the target the UI thread writes; the audio thread ramps gainCurrent_ toward - // it per-sample each block (linear interpolation, ~20 ms wall-clock at every host rate) - // so sudden knob moves produce no zipper noise and the true-zero bottom causes no click. + // Post-mixer master gain (linear, persisted). Lock-free atomic target; the audio thread + // ramps gainCurrent_ toward it per-sample (~20 ms wall-clock at every host rate) so + // knob moves produce no zipper noise. std::atomic masterGain_{1.0f}; - // The audio-thread running gain value: tracks masterGain_ across blocks, stepping at - // most gainRampStep_ per sample toward the target. Starts at unity (pre-FB1 default). - // Written and read exclusively on the audio thread — no atomics needed. + // Audio-thread running gain value, stepping at most gainRampStep_ per sample toward the + // target. Written/read exclusively on the audio thread — no atomics needed. float gainCurrent_ = 1.0f; - // T3-01: the per-sample ramp step, derived from kGainRampSeconds (20 ms wall-clock) - // against the live host rate in setupProcessing — never a baked-in rate. The default is - // the 48 kHz value so behavior before the first setupProcessing is unchanged. Written in - // setupProcessing (host-serialized against process), read on the audio thread. + // Per-sample ramp step derived from kGainRampSeconds against the live host rate in + // setupProcessing — never a baked-in rate. Default is the 48 kHz value. float gainRampStep_ = 1.0f / 960.0f; - // --- S-VIEW-4 preview-trigger mailbox (off-thread -> audio thread, lock-free) --------- - // The editor's preview-trigger button posts a note-on/off request from the UI thread; process() - // drains it at block start and drives the live instrument's MAIN VoiceEngine — the same - // noteOn/noteOff host MIDI takes, so the preview obeys voicing. ONE slot per direction, each a packed - // request whose high bits are a monotonically-incrementing sequence so process() detects a NEW - // request by comparing against the last sequence it consumed (never re-firing a stale one). The - // low 8 bits carry the note (on) / note (off); the on request also carries the velocity in the - // next 8 bits, latched at post time so the audio thread reads no shared velocity field. A single - // relaxed atomic load per block on the audio thread — RT-safe (no alloc, no lock). - // packed = (seq << 16) | (velocity << 8) | note [note-on] - // packed = (seq << 16) | note [note-off] + // --- Preview-trigger mailbox (off-thread -> audio thread, lock-free) ----------------- + // One slot per direction, packed as (seq << 16) | (velocity << 8) | note [on] or + // (seq << 16) | note [off]. process() detects a new request by comparing the packed + // sequence against the last one consumed — a single relaxed atomic load per block, + // RT-safe (no alloc, no lock). std::atomic previewOnRequest_{0}; // 0 = no request posted yet std::atomic previewOffRequest_{0}; std::uint16_t previewOnSeq_ = 0; // UI-thread post counter (never 0 after first post) @@ -495,22 +335,17 @@ private: std::uint16_t previewOnConsumed_ = 0; // audio-thread: last on-seq fired std::uint16_t previewOffConsumed_ = 0; // audio-thread: last off-seq fired - // Latched from setupProcessing so setActive/reload can size against it. Read - // off-thread only. 0.0 is explicitly invalid — setupProcessing sets the real host rate - // before any audio, and reloadInstrument guards on it before use. + // Latched from setupProcessing; 0.0 is explicitly invalid (reloadInstrument guards on it). double sampleRate_ = 0.0; Steinberg::int32 maxBlockSize_ = 4096; - // --- S6 embedded TCP/MCP UI --------------------------------------------- - // The embed shell (IReaperUIEmbedInterface), created lazily on the first queryInterface - // and owned here for the processor's lifetime. REAPER borrows AddRef'd references from - // queryInterface; the shell's refcount is a no-op because THIS unique_ptr governs its - // destruction (the processor always outlives the borrowed references). + // The embed shell, created lazily on the first queryInterface and owned here for the + // processor's lifetime; REAPER's borrowed AddRef'd references are outlived by this + // unique_ptr, so its own refcount is a no-op. std::unique_ptr embed_; - // The per-block mono peak (0..1+) the audio thread stores relaxed; the embed strip's - // level indicator reads it via embedActivityLevel(). Advisory only — a plain atomic, - // no ordering coupling, never guarded by a lock the audio thread touches. + // Per-block mono peak the audio thread stores relaxed; embedActivityLevel() reads it + // for the embed strip's level indicator. Advisory only. std::atomic embedPeak_{0.f}; }; diff --git a/src/shell/instrument/reasampler_vst.h b/src/shell/instrument/reasampler_vst.h index 3993279..6bdd0af 100644 --- a/src/shell/instrument/reasampler_vst.h +++ b/src/shell/instrument/reasampler_vst.h @@ -1,21 +1,8 @@ -// reasampler_vst.h — shared identity constants for the ReaSampler VST3 instrument -// (Phase S). One place for the plugin's class UID, name, vendor, and version so the -// processor, factory, and editor agree. -// -// A class UID is FOREVER-STABLE once shipped: a REAPER project that instantiates this -// instrument records the UID, so changing it orphans every saved instance. Minted once; -// do not regenerate. -// -// CHANNEL ISOLATION (S18, beta-in-isolation — the instrument-side companion to V4). Just -// as V4 gave the extension a per-channel ext-state namespace / command-id family / dock -// ident, S18 gives the VST3 instrument a per-channel PLUGIN IDENTITY: its class UID, its -// on-disk filename, and its display name all fork by the ONE channel bit -// (REASAMPLER_CHANNEL_IS_BETA, from version_generated.h). ONE class per binary — the bit -// selects which UID compiles into the single DEF_CLASS2, so a beta build carries only the -// beta identity and can never present the stable one (mirrors V4's fully-isolated-binary -// philosophy). The two UIDs below are BOTH frozen forever; the filename + display name -// derive from app_version's vstOutputName()/vstPluginName() (this header owns only the -// binary UID identity — the string identity lives in the pure module). +// reasampler_vst.h — shared identity constants for the ReaSampler VST3 instrument: the +// plugin's class UID, vendor name/URL/email, so the processor, factory, and editor agree. +// A class UID is FOREVER-STABLE once shipped (see this directory's CLAUDE.md) — minted +// once, never regenerated. Filename + display name are channel-derived from app_version's +// string accessors; this header owns only the binary UID identity. #pragma once @@ -25,23 +12,16 @@ namespace reasampler::vst { -// Vendor identity (S-NAME-1, SETTLED 2026-07-26). Shared across channels — V4 kept the -// lane-name prefix shared, so shared-where-V4-shares is the default (the channel is carried -// by the UID + filename + display fork, not the vendor block). +// Vendor identity, shared across channels — the channel is carried by the UID + filename + +// display fork, not the vendor block. inline constexpr const char* kVendorName = "ReaSampler"; inline constexpr const char* kVendorUrl = "https://github.com/daniel-c-harvey/reasampler"; inline constexpr const char* kVendorEmail = "mailto:the.real.daniel.harvey@gmail.com"; -// ----------------------------------------------------------------------------------------- // The two FOREVER-FROZEN VST3 class UIDs — one per channel — live in reasampler_uid.h // (SDK-free, so the extension's pure instrument_drop can render the .vstpreset class-ID -// string from the SAME constants without pulling the VST3 SDK). A saved REAPER project -// records the UID of the instance it instantiated and rebinds by it on reopen, so each is -// a permanent commitment. The channel bit selects which one this binary's factory registers -// — one class per binary, never both. The UID selection is the ONLY channel #ifdef in the -// VST shell (an INLINE_UID needs literal brace-init tokens, so it cannot route through -// app_version's runtime string accessors — reasampler_uid.h owns the binary UID fork, -// app_version owns the string fork). +// string from the same constants without pulling the VST3 SDK). The channel bit selects +// which one this binary's factory registers — one class per binary, never both. // The runtime FUID for the class this binary registers — the channel-selected UID. static const Steinberg::FUID kReaSamplerProcessorUID(REASAMPLER_ACTIVE_UID_1, diff --git a/src/shell/instrument/vst_entry.cpp b/src/shell/instrument/vst_entry.cpp index 0ca6ed3..7cfe745 100644 --- a/src/shell/instrument/vst_entry.cpp +++ b/src/shell/instrument/vst_entry.cpp @@ -1,21 +1,9 @@ -// vst_entry.cpp — the VST3 module class factory (Phase S1). Enumerates the one class -// this module offers (the ReaSampler instrument) via the SDK's factory macros. The -// Windows module exports — GetPluginFactory (here, via BEGIN_FACTORY) and -// InitDll/ExitDll (from the SDK's dllmain.cpp) — are how REAPER discovers and loads a -// VST3. -// -// VERIFIED (corrects §1a's "experienced estimate" flags on export names + macros, -// against vendor/vst3sdk/public.sdk/source/main/): -// * Windows exports: InitDll / ExitDll (SMTG_EXPORT_SYMBOL, in dllmain.cpp) + -// GetPluginFactory (SMTG_EXPORT_SYMBOL IPluginFactory* PLUGIN_API, emitted by the -// BEGIN_FACTORY macro). The plug-in must provide InitModule/DeinitModule — supplied -// here by linking moduleinit.cpp (the SDK's default one-time init/term). -// * Factory macros: BEGIN_FACTORY(vendor,url,email,flags) / DEF_CLASS2(...) / -// END_FACTORY — exact spellings from pluginfactory.h. -// * Instrument subcategory string: "Instrument|Synth|Sampler" -// (PlugType::kInstrumentSynthSampler, ivstaudioprocessor.h). -// * classFlags = 0 for a SingleComponentEffect (non-distributable), matching the -// AGain example. +// vst_entry.cpp — the VST3 module class factory. Enumerates the one class this module +// offers via the SDK's factory macros. Windows module exports — GetPluginFactory (here, +// via BEGIN_FACTORY) and InitDll/ExitDll (SDK's dllmain.cpp) — are how REAPER discovers +// and loads a VST3. Verified against vendor/vst3sdk/public.sdk/source/main/: the plug-in +// must supply InitModule/DeinitModule (linked here via moduleinit.cpp). classFlags = 0 for +// a SingleComponentEffect (non-distributable), matching the AGain example. #include "public.sdk/source/main/pluginfactory.h" @@ -26,23 +14,16 @@ #include "shell/instrument/reasampler_processor.h" #include "shell/instrument/reasampler_vst.h" // channel-selected class UID (REASAMPLER_ACTIVE_UID_*) -// CHANNEL PAIRING INVARIANT (S18). The instrument's PLUGIN identity forks by the ONE channel -// bit (REASAMPLER_CHANNEL_IS_BETA — the class UID selected in reasampler_vst.h, the filename -// + display name in app_version). Its DATA identity forks by the SAME bit, one layer down: -// ext_keys.h's kProjExtNamespace() delegates to app_version::extStateNamespace(), so a beta -// binary reads "reasampler_beta". Both derive from that one bit, so a beta VST can only ever -// talk to the beta extension. +// The instrument's plugin identity (UID + filename + display) and its data identity +// (ext_keys.h's kProjExtNamespace(), delegating to app_version::extStateNamespace()) both +// fork from the one REASAMPLER_CHANNEL_IS_BETA bit, so a beta VST can only ever talk to +// the beta extension. // -// The guard below pins the two forks together so a refactor cannot split them. It asserts -// that the CLASS UID this factory registers (REASAMPLER_ACTIVE_UID_1, selected by the #if in -// reasampler_vst.h) is the UID that matches THIS binary's channel bit. If someone edited that -// #if to pick the wrong branch — registering the stable UID in a beta build, or vice versa — -// the instrument's identity would diverge from the namespace ext_keys reads (a beta-named -// plugin presenting the stable UID, or reading the stable banks under a beta identity). That -// is exactly the silent split the invariant forbids, and it breaks the build here instead. -// (The namespace itself is a runtime accessor — .c_str() on a channel-selected string — so -// the couplable compile-time fact is the UID selection, not the namespace value; the -// app_version_tests pin the namespace string per channel.) +// The guard below pins the two forks together so a refactor cannot split them: it asserts +// the class UID this factory registers matches this binary's channel bit. If the #if in +// reasampler_vst.h picked the wrong branch, the instrument's identity would diverge from +// the namespace ext_keys reads (a beta-named plugin presenting the stable UID, or vice +// versa) — this breaks the build instead of shipping that silent split. #if REASAMPLER_CHANNEL_IS_BETA static_assert(REASAMPLER_ACTIVE_UID_1 == REASAMPLER_PROC_UID_BETA_1 && REASAMPLER_ACTIVE_UID_2 == REASAMPLER_PROC_UID_BETA_2 && @@ -62,13 +43,9 @@ static_assert(REASAMPLER_ACTIVE_UID_1 == REASAMPLER_PROC_UID_1 && BEGIN_FACTORY(reasampler::vst::kVendorName, reasampler::vst::kVendorUrl, reasampler::vst::kVendorEmail, Steinberg::PFactoryInfo::kNoFlags) -// The display name and version are channel-derived from app_version — sourced here, not -// as literals. DEF_CLASS2 expands inside GetPluginFactory() and PClassInfo2's constructor -// copies the char* into its own fixed buffer at that runtime call, so .c_str() on the -// accessors' static-storage strings is valid (no dangling — the refs outlive the copy). -// vstPluginName(): "ReaSampler 9000" / "ReaSampler 9000 beta" (live literals in -// app_version.cpp). appVersion(): the configured version string / that string plus -// "-beta" (the -beta render V4 already yields on beta). +// Display name + version are channel-derived from app_version, not literals. DEF_CLASS2 +// expands inside GetPluginFactory(); PClassInfo2's constructor copies the char* into its +// own buffer at that call, so .c_str() on the accessors' static-storage strings is valid. DEF_CLASS2(INLINE_UID(REASAMPLER_ACTIVE_UID_1, REASAMPLER_ACTIVE_UID_2, REASAMPLER_ACTIVE_UID_3, REASAMPLER_ACTIVE_UID_4), Steinberg::PClassInfo::kManyInstances, // cardinality