// 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 loaded capture's marker resolution, 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" #include #include #include #include #include "core/audio/peaks.h" // computeEnvelope (the cached peak thumbnail) #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 (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 (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, tuned to the three band heights: chrome + two-lane waveform + // deck row. 840x620 clears the full face without scroll on 1080p. 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 waveform + snap source) if (!processor_) { samples_.clear(); banks_.clear(); visible_.clear(); selectedId_.clear(); params_ = InstrumentParams{}; return; } auto banksJson = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey); samples_ = banksJson ? listSamples(*banksJson) : std::vector{}; banks_ = banksJson ? listBanks(*banksJson) : std::vector{}; selectedId_ = processor_->selectedSampleId(); params_ = processor_->instrumentParams(); channelMode_ = processor_->channelMode(); voiceCount_ = processor_->voiceCount(); voiceMode_ = processor_->voiceMode(); monoTrigger_ = processor_->monoTrigger(); // 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()) 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() { // 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_; if (!inBank) continue; const std::string& name = s.displayName.empty() ? s.id : s.displayName; if (nameMatchesQuery(name, searchQuery_)) visible_.push_back(s); } // 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). void ReaSamplerEditor::onSyncTimer() { // 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 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 only when something changed. if (r.reloaded || r.applied) { refreshFromBank(); invalidate(); } // 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. Publishes the edited selection + parameter set, then rebuilds off the // audio thread. The reload also copies the loaded 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_->setInstrumentParams(params_); processor_->reloadInstrument(); // 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(); #endif } void ReaSamplerEditor::loadSelection(const std::string& id) { // A load REPLACES the loaded sound. The shaping parameters (play mode, envelopes, pitch // engine, key-track, velocity curve) are NOT reset — the one set governs whatever is // loaded, so a load swaps the sound and keeps the settings. The three CAPTURE-ANCHORED // overrides are: a root, a loop span and a start frame all name positions in the // OUTGOING capture and mean nothing in the new one, so they clear and the new capture // plays from its own bank intrinsics. selectedId_ = id; params_.rootOverride.reset(); params_.loopOverride.reset(); params_.startPoint.reset(); commitAndReload(); } ReaSamplerEditor::SetupMarkers ReaSamplerEditor::pickedMarkers(std::int64_t frames) const { SetupMarkers m; // Seed from the bank's intrinsic loop (fact about the file), then let the parameter set's // override 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. if (processor_) { std::optional 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 parameter set's override (loop + start) supersedes the intrinsic. if (params_.loopOverride) { m.hasLoop = params_.loopOverride->hasLoop; m.loopStart = params_.loopOverride->start; m.loopEnd = params_.loopOverride->end; } if (params_.startPoint) m.start = *params_.startPoint; // 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; } void ReaSamplerEditor::applyMarkers(const SetupMarkers& m) { // Write the edited markers into the parameter set as the loop/start override. The bank // intrinsic is never written (read-only bank consumer). SampleLoop loop; loop.hasLoop = m.hasLoop; loop.start = m.loopStart; loop.end = m.loopEnd; params_.loopOverride = loop; params_.startPoint = m.start; } int ReaSamplerEditor::effectiveRoot() const { if (params_.rootOverride) return *params_.rootOverride; for (const SampleChoice& s : samples_) { if (s.id == selectedId_ && s.rootNote) return *s.rootNote; } return 60; } const std::vector& 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. 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_) { auto banksJson = processor_->bridge().readReasamplerExtState(reasampler::kProjExtBanksKey); if (banksJson) { if (auto sel = selectSample(*banksJson, sampleId)) relativePath = sel->relativePath; } if (relativePath.empty()) { // 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); const std::vector bytes = readFileBytes(abs); // empty on any failure const WavLayout layout = parseWavLayout(bytes); if (layout.valid) { std::vector 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) { // 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 waveform surface + snap. const std::vector& 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::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