Q-W2v: split VST god-modules — editor 8 face-axis TUs (+pure layout hoist), processor 3 TUs, component_state_io codec split (extension drops the voice engine), zone_params.h, core/wire putLE; formats frozen, 61/61 green
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
// 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.
|
||||
|
||||
#include "shell/instrument/reasampler_editor.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/audio/peaks.h" // computeEnvelope (the cached peak thumbnail)
|
||||
#include "core/capture/capture_paths.h" // resolveBankFile (shared M4 path resolution)
|
||||
#include "core/capture/wav_trim.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 "ext_keys.h"
|
||||
#include "core/instrument/ui/browser_scroll.h" // nameMatchesQuery (S12 type-to-filter)
|
||||
#include "shell/instrument/reaper_bridge.h"
|
||||
#include "shell/instrument/reasampler_processor.h"
|
||||
|
||||
using namespace Steinberg;
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
using namespace reasampler::instrument::map; // sample_map vocabulary (selectSample / listSamples / …)
|
||||
using audio::computeEnvelope;
|
||||
using capture::WavLayout;
|
||||
using capture::extractFloatFrames;
|
||||
using capture::parseWavLayout;
|
||||
using capture::resolveBankFile;
|
||||
using instrument::ui::nameMatchesQuery;
|
||||
using ui::ThumbnailKey;
|
||||
using ui::thumbnailKeyString;
|
||||
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.
|
||||
ViewRect r(0, 0, 840, 620);
|
||||
setRect(r);
|
||||
}
|
||||
|
||||
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)
|
||||
if (!processor_) {
|
||||
samples_.clear();
|
||||
banks_.clear();
|
||||
visible_.clear();
|
||||
selectedId_.clear();
|
||||
map_.zones.clear();
|
||||
selectedZone_ = -1;
|
||||
return;
|
||||
}
|
||||
auto banksJson = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey);
|
||||
samples_ = banksJson ? listSamples(*banksJson) : std::vector<SampleChoice>{};
|
||||
banks_ = banksJson ? listBanks(*banksJson) : std::vector<BankChoice>{};
|
||||
selectedId_ = processor_->selectedSampleId();
|
||||
const auto prevZoneCount = static_cast<int>(map_.zones.size());
|
||||
map_ = processor_->performanceMap();
|
||||
channelMode_ = processor_->channelMode();
|
||||
voiceCount_ = processor_->voiceCount(); // Phase S voice-deck snapshot
|
||||
voiceMode_ = processor_->voiceMode();
|
||||
monoTrigger_ = processor_->monoTrigger();
|
||||
if (selectedZone_ >= static_cast<int>(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.
|
||||
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).
|
||||
if (view_ == View::kZone && curvePopupOpen_) {
|
||||
const auto newZoneCount = static_cast<int>(map_.zones.size());
|
||||
if (selectedZone_ < 0 || newZoneCount != prevZoneCount) curvePopupOpen_ = false;
|
||||
}
|
||||
// Drop a filter that names a bank no longer present.
|
||||
if (!activeFilterBankId_.empty()) {
|
||||
bool found = false;
|
||||
for (const BankChoice& b : banks_) if (b.id == activeFilterBankId_) found = true;
|
||||
if (!found) activeFilterBankId_.clear();
|
||||
}
|
||||
rebuildVisible();
|
||||
}
|
||||
|
||||
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).
|
||||
visible_.clear();
|
||||
for (const SampleChoice& s : samples_) {
|
||||
const bool inBank = activeFilterBankId_.empty() || s.bankId == activeFilterBankId_;
|
||||
if (!inBank) continue;
|
||||
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.
|
||||
}
|
||||
|
||||
#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).
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
if (r.reloaded || r.applied) {
|
||||
refreshFromBank();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
// S13: decay the drop-affordance banner so it auto-dismisses a few ticks after a drop.
|
||||
if (dropHintTicks_ > 0) {
|
||||
--dropHintTicks_;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
#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.
|
||||
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.
|
||||
channelMode_ = processor_->channelMode();
|
||||
#ifdef _WIN32
|
||||
invalidate();
|
||||
#endif
|
||||
}
|
||||
|
||||
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.
|
||||
selectedId_ = id;
|
||||
if (reconcileSingleCaptureZones(map_, selectedId_)) {
|
||||
selectedZone_ = map_.zones.empty() ? -1 : 0;
|
||||
}
|
||||
commitAndReload();
|
||||
}
|
||||
|
||||
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_.
|
||||
if (processor_) {
|
||||
std::optional<SelectedSample> sel;
|
||||
auto banksJson =
|
||||
processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey);
|
||||
if (banksJson) sel = selectSample(*banksJson, selectedId_);
|
||||
if (!sel) {
|
||||
const SampleRefs refs = processor_->sampleRefs();
|
||||
if (const SelectedSample* r = findRef(refs, selectedId_)) sel = *r;
|
||||
}
|
||||
if (sel && sel->loop.hasLoop) {
|
||||
m.hasLoop = true;
|
||||
m.loopStart = sel->loop.start;
|
||||
m.loopEnd = sel->loop.end;
|
||||
}
|
||||
}
|
||||
// The override (loop + start) on a zone for the picked id supersedes the intrinsic.
|
||||
for (const PerformanceZone& z : map_.zones) {
|
||||
if (z.sampleId != selectedId_) continue;
|
||||
if (z.loopOverride) {
|
||||
m.hasLoop = z.loopOverride->hasLoop;
|
||||
m.loopStart = z.loopOverride->start;
|
||||
m.loopEnd = z.loopOverride->end;
|
||||
}
|
||||
if (z.startPoint) m.start = *z.startPoint;
|
||||
break;
|
||||
}
|
||||
// Default an unset loop's end to the sample length so the loop markers have somewhere sane
|
||||
// to sit before the user drags (loopStart stays 0). The "no loop" state is m.hasLoop==false;
|
||||
// the markers are still drawn (drag one to CREATE a loop).
|
||||
if (!m.hasLoop && m.loopEnd == 0) m.loopEnd = frames > 0 ? frames : 0;
|
||||
return m;
|
||||
}
|
||||
|
||||
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_.
|
||||
SampleLoop loop;
|
||||
loop.hasLoop = m.hasLoop;
|
||||
loop.start = m.loopStart;
|
||||
loop.end = m.loopEnd;
|
||||
for (int i = 0; i < static_cast<int>(map_.zones.size()); ++i) {
|
||||
PerformanceZone& z = map_.zones[static_cast<std::size_t>(i)];
|
||||
if (z.sampleId == selectedId_) {
|
||||
z.loopOverride = loop;
|
||||
z.startPoint = m.start;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
PerformanceZone z;
|
||||
z.sampleId = selectedId_;
|
||||
z.lowNote = 0;
|
||||
z.highNote = 127;
|
||||
z.loopOverride = loop;
|
||||
z.startPoint = m.start;
|
||||
map_.zones.push_back(z);
|
||||
return static_cast<int>(map_.zones.size()) - 1;
|
||||
}
|
||||
|
||||
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.
|
||||
for (const PerformanceZone& z : map_.zones) {
|
||||
if (z.sampleId == selectedId_) return z;
|
||||
}
|
||||
PerformanceZone z;
|
||||
z.sampleId = selectedId_;
|
||||
z.lowNote = 0;
|
||||
z.highNote = 127;
|
||||
return z;
|
||||
}
|
||||
|
||||
int ReaSamplerEditor::effectiveRoot() const {
|
||||
int root = 60;
|
||||
for (const SampleChoice& s : samples_) {
|
||||
if (s.id == selectedId_ && s.rootNote) root = *s.rootNote;
|
||||
}
|
||||
for (const PerformanceZone& z : map_.zones) {
|
||||
if (z.sampleId == selectedId_ && z.rootOverride) root = *z.rootOverride;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
int ReaSamplerEditor::ensureSampleZone() {
|
||||
if (selectedId_.empty()) return -1;
|
||||
for (int i = 0; i < static_cast<int>(map_.zones.size()); ++i) {
|
||||
if (map_.zones[static_cast<std::size_t>(i)].sampleId == selectedId_) return i;
|
||||
}
|
||||
PerformanceZone z;
|
||||
z.sampleId = selectedId_;
|
||||
z.lowNote = 0;
|
||||
z.highNote = 127;
|
||||
map_.zones.push_back(z);
|
||||
return static_cast<int>(map_.zones.size()) - 1;
|
||||
}
|
||||
|
||||
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).
|
||||
if (selectedId_.empty()) return;
|
||||
upsertPickedOverride(m);
|
||||
commitAndReload();
|
||||
}
|
||||
|
||||
const std::vector<AudioSample>& ReaSamplerEditor::monoPcmFor(const std::string& sampleId) {
|
||||
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
|
||||
// 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.
|
||||
std::string relativePath;
|
||||
std::vector<AudioSample> mono;
|
||||
if (processor_) {
|
||||
auto banksJson =
|
||||
processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey);
|
||||
if (banksJson) {
|
||||
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.
|
||||
const SampleRefs refs = processor_->sampleRefs();
|
||||
if (const SelectedSample* r = findRef(refs, sampleId)) {
|
||||
relativePath = r->relativePath;
|
||||
}
|
||||
}
|
||||
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<std::uint8_t> bytes = readFileBytes(abs);
|
||||
const WavLayout layout = parseWavLayout(bytes);
|
||||
if (layout.valid) {
|
||||
std::vector<AudioSample> interleaved =
|
||||
extractFloatFrames(bytes, layout, 0, layout.frameCount());
|
||||
mono = downmixToMono(interleaved, layout.channelCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
auto ins = pcmCache_.emplace(sampleId, std::move(mono));
|
||||
return ins.first->second;
|
||||
}
|
||||
|
||||
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.
|
||||
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.
|
||||
const std::vector<AudioSample>& mono = monoPcmFor(sampleId);
|
||||
Envelope env;
|
||||
if (!mono.empty()) {
|
||||
// Clamp bins to the frame count: computeEnvelope pads binCount > frameCount with
|
||||
// trailing empty {0,0} bins, which would render a very short sample as a comb of
|
||||
// spikes over flat gaps.
|
||||
const std::size_t bins =
|
||||
(std::min)(static_cast<std::size_t>((std::max)(1, binCount)), mono.size());
|
||||
env = computeEnvelope(mono, 1, mono.size(), bins);
|
||||
}
|
||||
auto ins = thumbCache_.emplace(key, std::move(env));
|
||||
return ins.first->second;
|
||||
}
|
||||
|
||||
ReaSamplerEditor::~ReaSamplerEditor() {
|
||||
#ifdef _WIN32
|
||||
if (childHwnd_) {
|
||||
DestroyWindow(childHwnd_);
|
||||
childHwnd_ = nullptr;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace reasampler::vst
|
||||
Reference in New Issue
Block a user