Merge the bank-root seam: every extension bank-directory lookup goes through bankRootOf
This commit is contained in:
@@ -21,6 +21,7 @@ add_library(reaper_reasampler MODULE
|
||||
${REASAMPLER_SRC_DIR}/shell/capture/capture_realtime_shell.cpp
|
||||
${REASAMPLER_SRC_DIR}/shell/capture/capture_realtime_finalize.cpp
|
||||
${REASAMPLER_SRC_DIR}/shell/persist/session.cpp
|
||||
${REASAMPLER_SRC_DIR}/shell/persist/bank_root.cpp
|
||||
${REASAMPLER_SRC_DIR}/shell/persist/ext_state_io.cpp
|
||||
${REASAMPLER_SRC_DIR}/shell/persist/prune_fs.cpp
|
||||
${REASAMPLER_SRC_DIR}/shell/bank_ops/bank_ops.cpp
|
||||
|
||||
@@ -48,7 +48,7 @@ Detail specific to these pure modules:
|
||||
- `wav_codec` — chunk walker + layout parse + float32 build + size-field patch + the lossless mono collapse + content hashes; the single pure RIFF/WAV owner (`wav_trim` is retired; `wav_codec` is the sole owner).
|
||||
- `capture_realtime` (`core/capture`, **renamed from `realtime_record` in Q-W3** — the Q-9 naming rider: pure module takes the stem, the shell takes the suffix, matching `drag_out`/`drag_out_win`) — the M8 realtime-record pure logic: capture scope + FX-tap point → `I_RECMODE`/`I_RECMODE_FLAGS` values, wet/dry → tap point, the recorded-file → `Sample` mapping, and the async record-phase state machine. Depends on `bank_model` for the plain `Sample`/`SourceMode` types. The transport/temp-track/send recipe lives in the shell (`shell/capture/capture_realtime_shell.cpp` + `capture_realtime_finalize.cpp`).
|
||||
- `batch_capture` — pure batch-capture planner: maps source ranges to capture units and aggregates results.
|
||||
- `capture_paths` — the REAPER-free path arithmetic behind offline capture: bank-subfolder + unique-filename derivation (`deriveBankPaths`, forward-slash form, no filesystem touch), the absolute-render-dir vs. project-relative-index-path split (`BankPaths`), the persist-side inverse (`resolveBankFile`, `projectDirOfRpp`), the Save-As bank-relocation plan (`deriveRelocationPlan`), and the GUID-primary project-identity classifier (`classifyProjectTransition` → `NoOp`/`Load`/`SaveAsRelocate`) the persist-poll timer drives.
|
||||
- `capture_paths` — the REAPER-free path arithmetic behind offline capture: bank-subfolder + unique-filename derivation (`deriveBankPaths`, forward-slash form, no filesystem touch), the absolute-render-dir vs. project-relative-index-path split (`BankPaths`), the persist-side inverse (`resolveBankFile`, `projectDirOfRpp`), the bank-root rule (`bankRootFor(rppPath, unsavedRoot)` — the `.rpp`'s directory, else the unsaved root, else empty, both branches in `normalizeSlashes` form; reached from the extension only through `shell/persist`'s `bankRootOf` seam, while the VST3 instrument still calls `projectDirOfRpp` directly), the Save-As bank-relocation plan (`deriveRelocationPlan`), and the GUID-primary project-identity classifier (`classifyProjectTransition` → `NoOp`/`Load`/`SaveAsRelocate`) the persist-poll timer drives.
|
||||
- `capture_name` — the REAPER-free composition of one capture's label + file-stem base from its source-track name(s), a local-calendar discriminator (`MM-DD HHMM`, from the shell's clock read), and an optional batch ordinal. The label and the stem deliberately diverge: the stem still passes through `capture_paths::sanitizeStem` (so a name that sanitizes to nothing files as `capture`), while the label keeps the source name verbatim. Stem uniqueness stays entirely `makeUniqueTag`'s — this module never disambiguates.
|
||||
- `insert_plan` — the REAPER-free logic behind the `insert` shell (M6): computes the `InsertMedia` `mode` bitmask from an `InsertOptions` struct (placement target, tempo-conform ratio, preserve-pitch flag), guaranteeing the &4 stretch-to-time-selection bit is never set and that no tempo bits are set when `conform == None`.
|
||||
- `render_settings` — the REAPER-free logic behind the capture action family: `SourceMode` → `RENDER_SETTINGS` bit mapping, `P_RAZOREDITS` string parsing + range-union bounds, razor-else-time range inference, the FX-scope bypass plan (`fxBypassPlanFor`), the one bounds mode a capture hands its window over on (`kRenderBoundsTimeSelection`) and the tail bit paired with it (`kTailFlagTimeSelection`), the tail-mode → `RENDER_TAILFLAG`/`RENDER_NORMALIZE`/`RENDER_TRIMEND` mapping (`tailRenderSettingsFor`) and its realtime-window analog (`realtimeRecordWindowEnd`), the capture-action taxonomy table (`captureActionTable`) `main.cpp` iterates to register the CAPTURE_ITEM/CAPTURE_TRACK family, and `renderSourceLabel` (the source named in the offline backend's bounds refusal).
|
||||
|
||||
@@ -106,6 +106,12 @@ std::string projectDirOfRpp(const std::string& rppPath) {
|
||||
return normalizeSlashes(dir);
|
||||
}
|
||||
|
||||
std::string bankRootFor(const std::string& rppPath, const std::string& unsavedRoot) {
|
||||
const std::string projectDir = projectDirOfRpp(rppPath);
|
||||
if (!projectDir.empty()) return projectDir;
|
||||
return normalizeSlashes(unsavedRoot);
|
||||
}
|
||||
|
||||
BankRelocation deriveRelocationPlan(const std::string& oldProjectDir,
|
||||
const std::string& newProjectDir) {
|
||||
BankRelocation r;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
// capture_paths — the REAPER-free path arithmetic behind offline capture. The
|
||||
// capture shell resolves the current project directory via REAPER APIs, then
|
||||
// hands the raw strings here. Forward-slash form throughout, no filesystem
|
||||
// shell reads the .rpp path via REAPER APIs and resolves the bank root through
|
||||
// bankRootFor, then hands the raw strings here. Forward-slash form throughout, no filesystem
|
||||
// access; the bank subfolder name is a fixed constant.
|
||||
|
||||
#include <cstddef>
|
||||
@@ -87,6 +87,12 @@ std::string resolveBankFile(const std::string& projectDir,
|
||||
// same way persist does.
|
||||
std::string projectDirOfRpp(const std::string& rppPath);
|
||||
|
||||
// The directory a project's bank lives under: the .rpp's directory when it has one,
|
||||
// else `unsavedRoot` (where a never-saved project banks), else empty — never a
|
||||
// default-location fallback. Either branch comes back in normalizeSlashes form, so a
|
||||
// root compares the same whichever branch produced it.
|
||||
std::string bankRootFor(const std::string& rppPath, const std::string& unsavedRoot);
|
||||
|
||||
// A relocation plan for the physical bank folder on Save-As to a new project
|
||||
// location. The index's relative paths do NOT change (they are relative to the
|
||||
// project dir, which moved with the .rpp), so relocation is purely a folder
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
#include "core/model/bank_book.h" // BankBook, Bank, activeBankId / activeIndex
|
||||
#include "core/model/bank_model.h" // Sample, AddResult, findByHash
|
||||
#include "shell/panel/panel_input.h" // bankPanelRefresh
|
||||
#include "core/capture/capture_paths.h" // deriveBankPaths / projectDirOfRpp
|
||||
#include "core/capture/capture_paths.h" // deriveBankPaths
|
||||
#include "core/util/file_bytes.h" // shared whole-file loader
|
||||
#include "core/wire/instrument_drop.h" // pure buildInstrumentDropPreset (.vstpreset image for a sampleId)
|
||||
#include "shell/actions/instrument_drop_win.h" // shell loadInstrumentOntoTrack (FX add+apply, no own undo block)
|
||||
#include "shell/persist/bank_root.h"
|
||||
#include "shell/persist/session.h" // ReaSamplerSession
|
||||
#include "shell/undo/undo_block.h" // the ONE undo-block opener
|
||||
|
||||
@@ -29,7 +30,6 @@
|
||||
#include "reaper_plugin.h" // reaper_plugin_info_t, gaccel_register_t (full defs)
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_ShowConsoleMsg
|
||||
#define REAPERAPI_WANT_MediaExplorerGetLastPlayedFileInfo
|
||||
#define REAPERAPI_WANT_PCM_Source_CreateFromFile
|
||||
@@ -47,7 +47,6 @@ using capture::buildFloat32Wav;
|
||||
using capture::deriveBankPaths;
|
||||
using capture::hashWavContent;
|
||||
using capture::parseWavLayout;
|
||||
using capture::projectDirOfRpp;
|
||||
using capture::WavLayout;
|
||||
using util::readFileBytes;
|
||||
using version::channelActionName;
|
||||
@@ -79,14 +78,6 @@ std::string g_idImportStr;
|
||||
std::string g_idImportMxStr;
|
||||
std::string g_labelImportStr;
|
||||
|
||||
// Forward-slashed, no trailing slash. Empty for an unsaved/no-active project, which
|
||||
// makes the import refuse to place a file (relative-paths invariant, no fallback).
|
||||
std::string currentProjectDir() {
|
||||
std::vector<char> buf(4096, '\0');
|
||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||
return projectDirOfRpp(std::string(buf.data()));
|
||||
}
|
||||
|
||||
// Writes a byte buffer to a file. Returns true on success. The caller is responsible for
|
||||
// ensuring the directory exists before calling.
|
||||
bool writeFileBytes(const std::string& path, const std::vector<std::uint8_t>& bytes) {
|
||||
@@ -170,7 +161,8 @@ ImportResult importFileIntoActiveBank(const std::string& absoluteSourcePath) {
|
||||
return out;
|
||||
}
|
||||
|
||||
const std::string projectDir = currentProjectDir();
|
||||
// Empty refuses to place a file: relative-paths invariant, no fallback.
|
||||
const std::string projectDir = bankRootOf(nullptr);
|
||||
if (projectDir.empty()) {
|
||||
out.message = "no saved project, so the bank has no location -- save the "
|
||||
"project first";
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/capture/capture_paths.h" // projectDirOfRpp, sanitizeStem
|
||||
#include "core/capture/capture_paths.h" // sanitizeStem
|
||||
#include "shell/package/export_bank.h"
|
||||
#include "shell/package/package_pickers.h"
|
||||
#include "shell/persist/bank_root.h"
|
||||
#include "shell/persist/session.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_ShowConsoleMsg
|
||||
#define REAPERAPI_WANT_ShowMessageBox
|
||||
#include "reaper_plugin_functions.h"
|
||||
@@ -28,12 +28,6 @@ constexpr const char* kUnsavedProjectMsg =
|
||||
"ReaSampler export: save the project first -- an unsaved project has no bank folder "
|
||||
"to read from.\n";
|
||||
|
||||
std::string currentProjectDir() {
|
||||
std::vector<char> buf(4096, '\0');
|
||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||
return capture::projectDirOfRpp(std::string(buf.data()));
|
||||
}
|
||||
|
||||
// 6 == YES; anything else cancels (SDK ~6544).
|
||||
bool confirmed(const std::string& msg, const char* title) {
|
||||
return ShowMessageBox(msg.c_str(), title, 4) == 6;
|
||||
@@ -118,7 +112,7 @@ void reportOutcome(const ExportOutcome& out, const std::string& destPath) {
|
||||
} // namespace
|
||||
|
||||
void doBankPackageExport(const ReaSamplerSession& session, const std::string& bankId) {
|
||||
const std::string projectDir = currentProjectDir();
|
||||
const std::string projectDir = bankRootOf(nullptr);
|
||||
if (projectDir.empty()) {
|
||||
ShowConsoleMsg(kUnsavedProjectMsg);
|
||||
return;
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "core/capture/capture_paths.h" // projectDirOfRpp
|
||||
#include "core/wire/bake_wire.h"
|
||||
#include "core/wire/ext_state_read.h" // readProjExtStateGrowing (the shared grow loop)
|
||||
#include "ext_keys.h"
|
||||
#include "shell/capture/bake_landing.h"
|
||||
#include "shell/panel/panel_input.h" // bankPanelRefresh
|
||||
#include "shell/persist/bank_root.h"
|
||||
#include "shell/persist/session.h"
|
||||
#include "shell/undo/undo_block.h" // the ONE undo-block opener
|
||||
|
||||
@@ -77,19 +77,17 @@ KeyRead readKey(ReaProject* proj, const std::string& key) {
|
||||
|
||||
struct OpenProject {
|
||||
ReaProject* proj = nullptr;
|
||||
std::string dir; // the project's own directory; empty for a never-saved project
|
||||
std::string dir; // the project's bank root; empty when it has no bank location
|
||||
};
|
||||
|
||||
// Every open project tab. The request key lives in the INSTANCE's project, which is not
|
||||
// necessarily the focused one, so the scan cannot assume the current tab holds it.
|
||||
std::vector<OpenProject> openProjects() {
|
||||
std::vector<OpenProject> out;
|
||||
std::vector<char> buf(4096, '\0');
|
||||
for (int idx = 0;; ++idx) {
|
||||
buf[0] = '\0';
|
||||
ReaProject* proj = EnumProjects(idx, buf.data(), static_cast<int>(buf.size()));
|
||||
ReaProject* proj = EnumProjects(idx, nullptr, 0);
|
||||
if (!proj) break;
|
||||
out.push_back(OpenProject{proj, projectDirOfRpp(std::string(buf.data()))});
|
||||
out.push_back(OpenProject{proj, bankRootOf(proj)});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#include "core/util/file_bytes.h"
|
||||
#include "core/capture/render_settings.h"
|
||||
#include "shell/capture/render_bounds_gate.h" // the exact-bounds verdict on the landed render
|
||||
#include "shell/persist/bank_root.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
@@ -391,43 +392,23 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Unsaved-project detection via EnumProjects(-1, buf, bufsz): the .rpp path
|
||||
// out-param is empty for a project that has never been saved — a reliable
|
||||
// unsaved sentinel. NOT GetProjectPathEx: that returns the recording path, not
|
||||
// the .rpp location, and is never empty even when unsaved (the original bug —
|
||||
// captures landed in REAPER's default media location instead of by the .rpp).
|
||||
//
|
||||
// Flow: read .rpp path; if empty, Main_SaveProject(proj, true) — true =
|
||||
// No bank root (a never-saved project): Main_SaveProject(proj, true) — true =
|
||||
// forceSaveAsIn — prompts Save-As and (per SDK header) blocks until dismissed,
|
||||
// though that blocking behaviour cannot be verified from the header itself;
|
||||
// re-read; if still empty (cancelled), refuse with NoProject and write nothing.
|
||||
auto readRppPath = [&]() -> std::string {
|
||||
std::vector<char> buf(4096, '\0');
|
||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||
return std::string(buf.data());
|
||||
};
|
||||
|
||||
std::string rppPath = readRppPath();
|
||||
if (rppPath.empty()) {
|
||||
// re-resolve; if still empty (cancelled), refuse with NoProject and write nothing.
|
||||
// Never GetProjectPathEx: that is the recording path, never empty even when
|
||||
// unsaved (the original bug — captures landed in REAPER's default media location).
|
||||
std::string projectDir = bankRootOf(proj);
|
||||
if (projectDir.empty()) {
|
||||
Main_SaveProject(proj, true);
|
||||
rppPath = readRppPath();
|
||||
projectDir = bankRootOf(proj);
|
||||
}
|
||||
if (rppPath.empty()) {
|
||||
if (projectDir.empty()) {
|
||||
result.status = CaptureStatus::NoProject;
|
||||
result.message = "Project must be saved before capture — nothing captured.";
|
||||
return result;
|
||||
}
|
||||
|
||||
// Project dir = parent of the .rpp; forward-slash-normalized so the rest of
|
||||
// the capture pipeline (deriveBankPaths, RENDER_FILE) sees a clean path.
|
||||
const std::string projectDir = [&]() -> std::string {
|
||||
namespace fs = std::filesystem;
|
||||
std::string dir = fs::path(rppPath).parent_path().string();
|
||||
for (char& c : dir) { if (c == '\\') c = '/'; }
|
||||
if (dir.size() > 1 && dir.back() == '/') dir.pop_back();
|
||||
return dir;
|
||||
}();
|
||||
|
||||
// Compute the tag ONCE — calling makeUniqueTag() twice would let the file
|
||||
// stem and Sample.id diverge (the counter advances per call).
|
||||
const std::string uniqueTag = makeUniqueTag("");
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
#include "shell/panel/panel_bank_ops.h" // bankPanelSelectedSampleIds / SourceBankId
|
||||
#include "shell/panel/panel_input.h" // bankPanelRefresh
|
||||
#include "core/capture/batch_capture.h" // planCaptureUnits / BatchOutcome
|
||||
#include "core/capture/capture_paths.h" // projectDirOfRpp
|
||||
#include "core/model/bank_book.h" // BankBook / Bank
|
||||
#include "core/model/provenance.h" // recipe parse/build, fingerprint
|
||||
#include "shell/persist/bank_root.h"
|
||||
#include "shell/persist/session.h" // ReaSamplerSession
|
||||
#include "shell/capture/capture_orchestrator.h" // captureAndIndexOne / renderOffline
|
||||
#include "shell/capture/provenance_shell.h" // fxChainIdentity* / trackByGuid
|
||||
@@ -31,7 +31,6 @@
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_CountSelectedMediaItems
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_GetSelectedMediaItem
|
||||
#define REAPERAPI_WANT_GetMediaItem_Track
|
||||
#define REAPERAPI_WANT_GetMediaItemInfo_Value
|
||||
@@ -104,10 +103,8 @@ private:
|
||||
// at all -- the batch summary reports ordinals only.
|
||||
std::string withQuarantineNote(std::string line, int quarantinedCount) {
|
||||
if (quarantinedCount <= 0) return line;
|
||||
std::vector<char> buf(4096, '\0');
|
||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||
const std::string dir = projectDirOfRpp(std::string(buf.data()));
|
||||
if (dir.empty()) return line; // unreachable: a quarantine implies a saved project
|
||||
const std::string dir = bankRootOf(nullptr);
|
||||
if (dir.empty()) return line; // unreachable: a quarantine implies a bank root
|
||||
line += " " + std::to_string(quarantinedCount) + " refused render" +
|
||||
(quarantinedCount == 1 ? " was" : "s were") + " retained for diagnosis, "
|
||||
"normally at " + refusedRenderFolder(dir) + " (delete when done) -- one "
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
#include "core/capture/capture_realtime.h" // RecordPhase machine, record-mode plan (pure)
|
||||
#include "core/capture/render_settings.h" // realtimeRecordWindowEnd
|
||||
#include "shell/capture/capture_realtime_finalize.h" // recordedFilePath, finalizeRecording
|
||||
#include "shell/persist/bank_root.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
@@ -84,20 +85,6 @@ namespace reasampler::capture {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string normSlashes(std::string s) {
|
||||
for (char& c : s) if (c == '\\') c = '/';
|
||||
if (s.size() > 1 && s.back() == '/') s.pop_back();
|
||||
return s;
|
||||
}
|
||||
|
||||
// Reads the ACTIVE project's .rpp path (empty if unsaved). Only needed at begin()
|
||||
// time, when the record's project IS the active project.
|
||||
std::string readRppPath() {
|
||||
std::vector<char> buf(4096, '\0');
|
||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||
return std::string(buf.data());
|
||||
}
|
||||
|
||||
// -1 if unresolved yet. Used by the flush wait to detect stability (size
|
||||
// unchanged across a tick) before moving the file — a take REAPER is still
|
||||
// flushing grows tick over tick.
|
||||
@@ -290,20 +277,18 @@ RealtimeRecordBackend::begin(const CaptureRequest& request,
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Saved-project gate (same as offline): the bank folder resolves against the
|
||||
// .rpp parent. Prompt Save-As once when unsaved; refuse if still unsaved.
|
||||
std::string rppPath = readRppPath();
|
||||
if (rppPath.empty()) {
|
||||
// Bank-root gate (same as offline): prompt Save-As once when there is no bank
|
||||
// root; refuse if there still is none.
|
||||
std::string projectDir = bankRootOf(proj);
|
||||
if (projectDir.empty()) {
|
||||
Main_SaveProject(proj, true);
|
||||
rppPath = readRppPath();
|
||||
projectDir = bankRootOf(proj);
|
||||
}
|
||||
if (rppPath.empty()) {
|
||||
if (projectDir.empty()) {
|
||||
outFailure.status = CaptureStatus::NoProject;
|
||||
outFailure.message = "Project must be saved before capture — nothing captured.";
|
||||
return nullptr;
|
||||
}
|
||||
const std::string projectDir =
|
||||
normSlashes(std::filesystem::path(rppPath).parent_path().string());
|
||||
|
||||
RealtimeCaptureHandle st(new RealtimeCaptureState());
|
||||
st->proj_ = proj;
|
||||
|
||||
@@ -32,12 +32,12 @@
|
||||
#include "core/model/bank_model.h"
|
||||
#include "shell/panel/panel_bank_ops.h" // bankPanelSelectedSampleIds / SourceBankId
|
||||
#include "core/capture/capture_paths.h"
|
||||
#include "shell/persist/bank_root.h"
|
||||
#include "shell/persist/session.h"
|
||||
#include "shell/undo/undo_block.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_CountSelectedTracks
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_GetCursorPosition
|
||||
#define REAPERAPI_WANT_GetSelectedTrack
|
||||
#define REAPERAPI_WANT_InsertMedia
|
||||
@@ -50,7 +50,6 @@
|
||||
namespace reasampler {
|
||||
|
||||
using capture::computeInsertMode;
|
||||
using capture::normalizeSlashes;
|
||||
using capture::resolveBankFile;
|
||||
using capture::TempoConform;
|
||||
|
||||
@@ -58,18 +57,6 @@ namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Mirrors bank_panel/capture/persist's own derivation; the bank index stores
|
||||
// relative paths, so resolving a file needs the current .rpp dir. A shared "current
|
||||
// project dir" helper would be a clean small refactor now that a fourth consumer
|
||||
// exists (also noted in panel_bank_ops.cpp) — out of scope here.
|
||||
std::string currentProjectDir() {
|
||||
std::vector<char> buf(4096, '\0');
|
||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||
std::string rpp(buf.data());
|
||||
if (rpp.empty()) return {}; // unsaved project: no resolvable bank
|
||||
return normalizeSlashes(fs::path(rpp).parent_path().string());
|
||||
}
|
||||
|
||||
// Snapshot of the currently-selected track set (master is skipped, matching
|
||||
// CountSelectedTracks/GetSelectedTrack), in selection order, for restore later.
|
||||
std::vector<MediaTrack*> snapshotSelectedTracks() {
|
||||
@@ -111,9 +98,9 @@ InsertResult runInsert(ReaSamplerSession* session, const InsertRequest& request)
|
||||
if (ids.empty()) { result.status = InsertStatus::NoSelection; return result; }
|
||||
const std::string& id = ids.front(); // focused / first selected — single sample
|
||||
|
||||
// WHERE: an unsaved project has no resolvable bank dir; no-op rather than
|
||||
// WHERE: no bank root means no resolvable bank file; no-op rather than
|
||||
// resolving against CWD.
|
||||
const std::string projectDir = currentProjectDir();
|
||||
const std::string projectDir = bankRootOf(nullptr);
|
||||
if (projectDir.empty()) { result.status = InsertStatus::NoProject; return result; }
|
||||
|
||||
// Resolve against the bank the selection came from — it may be the pool or a
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
#include "shell/capture/scope_resolve.h"
|
||||
|
||||
#include <cstring> // strnlen — bounded read of GetTrackName's buffer
|
||||
#include <filesystem> // project-dir derivation for provenance parent resolution
|
||||
#include <utility>
|
||||
|
||||
#include "core/capture/render_window.h" // itemExtentPrintsWindow
|
||||
#include "shell/capture/provenance_shell.h" // fxChainIdentity* / *SourceFiles / bankFileRefs
|
||||
#include "shell/capture/track_guid.h" // guidString
|
||||
#include "shell/persist/bank_root.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_GetSet_LoopTimeRange
|
||||
@@ -195,28 +195,14 @@ bool ResolveScopeSource(CaptureScope scope, ResolvedSource& out, std::string& wh
|
||||
return true;
|
||||
}
|
||||
|
||||
// Empty for an unsaved project (EnumProjects writes an empty .rpp path), which
|
||||
// makes every bank file resolve empty -> no false parentage.
|
||||
std::string currentProjectDir()
|
||||
{
|
||||
std::vector<char> buf(4096, '\0');
|
||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||
const std::string rpp(buf.data());
|
||||
if (rpp.empty()) return {};
|
||||
namespace fs = std::filesystem;
|
||||
std::string dir = fs::path(rpp).parent_path().string();
|
||||
for (char& c : dir) if (c == '\\') c = '/';
|
||||
if (dir.size() > 1 && dir.back() == '/') dir.pop_back();
|
||||
return dir;
|
||||
}
|
||||
|
||||
// Detection rule: the capture's source item media file(s) must all resolve, by
|
||||
// exact normalized absolute path, to one bank sample's file.
|
||||
// exact normalized absolute path, to one bank sample's file. No bank root makes every
|
||||
// bank file resolve empty -> no false parentage.
|
||||
std::optional<model::Provenance> buildCaptureProvenance(
|
||||
const BankBook& book, const CaptureRequest& req,
|
||||
CaptureScope scope, const ResolvedSource& src)
|
||||
{
|
||||
const std::string projectDir = currentProjectDir();
|
||||
const std::string projectDir = bankRootOf(nullptr);
|
||||
const std::vector<model::BankFileRef> bankFiles = bankFileRefs(book, projectDir);
|
||||
|
||||
// What "the source audio" means depends on scope: item scope uses the selected
|
||||
|
||||
@@ -72,10 +72,6 @@ std::string trackName(MediaTrack* tr);
|
||||
// inferred range. Returns false with a reason on nothing to do.
|
||||
bool ResolveScopeSource(CaptureScope scope, ResolvedSource& out, std::string& why);
|
||||
|
||||
// Current project's directory (parent of its .rpp), forward-slashed, no trailing
|
||||
// slash. Empty for an unsaved project (no false parentage). Read-only.
|
||||
std::string currentProjectDir();
|
||||
|
||||
// Builds the provenance for a capture if it genuinely resamples from a bank sample
|
||||
// (detectParent over `book`'s resolved file refs), else returns nullopt (the common,
|
||||
// non-resample case). Must run BEFORE the FxBypassGuard neutralizes the in-scope
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// import_bank.cpp — see import_bank.h for the contract. The REAPER-facing half of the
|
||||
// import: the project directory, the minted bank id, the birth records, and the one
|
||||
// import: the bank root, the minted bank id, the birth records, and the one
|
||||
// undo-batched persist. Every decision it makes is in import_landing / import_plan.
|
||||
//
|
||||
// main.cpp owns the API pointers; this TU gets them extern. DAW-verified, not unit tested.
|
||||
@@ -11,12 +11,11 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/capture/capture_paths.h" // projectDirOfRpp
|
||||
#include "shell/bank_ops/bank_ops.h" // persistBankOp — one bank op is one Ctrl-Z
|
||||
#include "shell/persist/bank_root.h"
|
||||
#include "shell/persist/session.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_genGuid
|
||||
#define REAPERAPI_WANT_guidToString
|
||||
#include "reaper_plugin_functions.h"
|
||||
@@ -25,12 +24,6 @@ namespace reasampler {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string activeProjectDir() {
|
||||
std::vector<char> buf(4096, '\0');
|
||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||
return capture::projectDirOfRpp(std::string(buf.data()));
|
||||
}
|
||||
|
||||
// The model mints no ids (it stays pure and deterministic), so the shell does — the
|
||||
// same GUID pair bankOpCreate uses.
|
||||
std::string mintBankId() {
|
||||
@@ -62,7 +55,7 @@ ImportBankResult importBankPackage(ReaSamplerSession& session,
|
||||
std::to_string(static_cast<std::int64_t>(std::time(nullptr)));
|
||||
|
||||
LandedFileJournal journal;
|
||||
const ImportLanding landing = landPackage(packageAbsPath, activeProjectDir(),
|
||||
const ImportLanding landing = landPackage(packageAbsPath, bankRootOf(nullptr),
|
||||
session.book(), uniqueTag, journal);
|
||||
out.outcome = landing.outcome;
|
||||
out.header = landing.header;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "shell/panel/panel_state.h"
|
||||
#include "shell/persist/bank_root.h"
|
||||
|
||||
// PlayPreview/StopPreview (stock, not SWS-only) drive a caller-owned preview_register_t.
|
||||
#define REAPERAPI_MINIMAL
|
||||
@@ -75,7 +76,7 @@ void startAudition(int idx) {
|
||||
const Sample* s = index->query(disp.orderedIds[static_cast<std::size_t>(idx)]);
|
||||
if (!s) return;
|
||||
|
||||
const std::string projectDir = currentProjectDir();
|
||||
const std::string projectDir = bankRootOf(nullptr);
|
||||
const std::string abs = resolveBankFile(projectDir, s->relativePath);
|
||||
if (abs.empty()) return;
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
#include "shell/actions/package_export_action.h" // doBankPackageExport — the export skin
|
||||
#include "shell/actions/package_import_action.h" // doImportBankPackage — the menu's import row
|
||||
#include "shell/bank_ops/bank_ops.h" // bankOp* promptless verbs
|
||||
#include "shell/persist/bank_root.h"
|
||||
#include "shell/persist/session.h" // ReaSamplerSession — the live session the ops mutate
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_GetUserInputs
|
||||
#define REAPERAPI_WANT_ShowMessageBox
|
||||
#define REAPERAPI_WANT_Main_OnCommand
|
||||
@@ -32,15 +32,6 @@ namespace reasampler::panel {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Mirrors the persist shell's derivation (ext_state_io.cpp).
|
||||
std::string currentProjectDir() {
|
||||
std::vector<char> buf(4096, '\0');
|
||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||
std::string rpp(buf.data());
|
||||
if (rpp.empty()) return {};
|
||||
return normalizeSlashes(fs::path(rpp).parent_path().string());
|
||||
}
|
||||
|
||||
BankBook* book() { return g_panel.session ? &g_panel.session->book() : nullptr; }
|
||||
|
||||
// The BankModel a region currently displays. Pool region -> the pool; banks region ->
|
||||
@@ -206,7 +197,7 @@ std::vector<std::string> resolveDragPathsForOs() {
|
||||
const BankModel* idx = b->index(g_panel.dragSourceBankId);
|
||||
if (!idx) return {};
|
||||
|
||||
const std::string projectDir = currentProjectDir();
|
||||
const std::string projectDir = bankRootOf(nullptr);
|
||||
resolved.reserve(g_panel.dragSampleIds.size());
|
||||
for (const std::string& sid : g_panel.dragSampleIds) {
|
||||
const Sample* s = idx->query(sid);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "shell/panel/panel_state.h"
|
||||
|
||||
#include "shell/panel/draw_kit.h"
|
||||
#include "shell/persist/bank_root.h"
|
||||
#include "shell/persist/session.h"
|
||||
#include "core/view/view_mode_model.h"
|
||||
|
||||
@@ -488,7 +489,7 @@ void paintPanel(HWND hwnd, HDC hdc) {
|
||||
LICE_SysBitmap bmp(w, h);
|
||||
LICE_Clear(&bmp, toLice(roleColor(Role::BgBase)));
|
||||
|
||||
const std::string projectDir = currentProjectDir();
|
||||
const std::string projectDir = bankRootOf(nullptr);
|
||||
const std::string activeName = activeBankName();
|
||||
|
||||
if (poolShown()) {
|
||||
|
||||
@@ -460,7 +460,6 @@ BankBook* book();
|
||||
const BankModel* indexForRegion(Region r);
|
||||
std::string bankIdForRegion(Region r);
|
||||
std::vector<const Bank*> namedBanks();
|
||||
std::string currentProjectDir();
|
||||
void doCreateBank();
|
||||
void transferSamples(const std::vector<std::string>& sampleIds,
|
||||
const std::string& srcBankId, const std::string& destBankId,
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
## Scope
|
||||
|
||||
The persist seam: project ext-state read/write (`session` / `ext_state_io`), the
|
||||
prune path's filesystem half (`prune_fs`), and the extension-side instance-usage
|
||||
scan (`usage_scan`). Internal helpers shared only within the persist TU family live
|
||||
extension's one bank-root resolver (`bank_root`), the prune path's filesystem half
|
||||
(`prune_fs`), and the extension-side instance-usage scan (`usage_scan`). Internal helpers shared only within the persist TU family live
|
||||
in `persist_internal.h`. The pure orphan computation is owned elsewhere
|
||||
(`core/reclaim`), the pure usage wire elsewhere again (`core/wire`), and every
|
||||
tracking *decision* by `core/tracking`'s authority — this directory is the
|
||||
@@ -54,8 +54,9 @@ REAPER/filesystem-facing half only, and it gathers rather than decides.
|
||||
## Modules
|
||||
|
||||
- `shell/persist` (`session` / `ext_state_io` / `prune_fs`) — the persist seam, split by responsibility (Q-W5; the former `persist.cpp` god-TU and its `persist.h` compatibility umbrella are both retired — callers include `shell/persist/session.h` / `ext_state_io.h` directly). `session` owns the `ReaSamplerSession` lifecycle: the poll identity-transition detection (load / Save-As / forked sibling / recycled pointer) and the `projectconfig`-driven deferred undo/redo reload. `ext_state_io` owns project ext state (`SetProjExtState`/`GetProjExtState`, namespace `"reasampler"`) ↔ `BankBook` JSON, `ViewModeModel` JSON, `TailSetting` JSON, the tracking ledger JSON, the writing-version stamp, GUID minting, and bank-folder relocation. `saveToActiveProject` returns whether the writes were ISSUED — false means no active/saved project and NOTHING was written, which is the only reading its callers' discard-the-undo-point branch is safe under; it must never grow an observational third failure mode (the contract lives at its declaration in `session.h`). `saveViewStateOnly` is its narrowed sibling for a caller that changed only the Design-View model — `view_state` plus the dirty mark, no other key — and returns on that same rule; the full save stays the default and narrowing is opt-in per call site. `session` additionally owns `recordCreated` — **the one writer of a birth record**, called at the same point the `Sample` is added, deriving lineage from that `Sample`'s own provenance. `prune_fs` hosts the prune dry-run / full-set orphan queries (gathering `referencedPaths()` plus `tracking::pruneProtection`'s two inputs for the `prune_reconcile` pure core) — and, beside them, `tiedUsageFor`, the resample's replace-vs-add input, deliberately co-located so "both answers come out of one `TrackingState`" is structural rather than a rule two files must remember. It is also **the single file-deletion authority over user files in the bank folder** (`deleteOrphanFile` via `SHFileOperationW`); nothing else in the system deletes bank-folder bytes. Dry-run / orphan-set / reclaim each independently abort (delete nothing) when the authority reports a block.
|
||||
- `bank_root` — **the extension's one bank-root resolver**, `bankRootOf(ReaProject*)` (nullptr = the active project): reads the project's `.rpp` path via `EnumProjects` and answers with the pure `capture::bankRootFor`. Every extension site that resolves or writes bank files — capture (offline + realtime), batch, bake landing, insert, provenance, the panel's audition/thumbnails/drag-out, ingest, package import/export, prune, and the session's Save-As relocation — asks here; none derives a directory from an `.rpp` path itself. Empty means "no bank location" and every caller refuses on it exactly as before. No unsaved root is supplied yet, so a never-saved project still has none. It answers WHERE the bank is, not whether the project is saved: the ext-state write gates (`activeSavedProject`, `ensureProjectGuid`), Design View's persist prompt and prune's own saved-project gate stay keyed on the `.rpp` path. The session's Save-As relocation reads its old root from `lastBankRoot_` (why: `session.h`). The VST3 instrument does not use this seam (`shell/instrument` resolves its own paths).
|
||||
- `usage_scan` — extension-side prune-scan shell: enumerates every `rsusage_*` ext-state key, decodes each `sample_usage` wire record, enumerates every ReaSampler 9000 FX instance across all tracks + master / normal + record chains / containers (recursive) / take FX, and returns the pure `sample_usage::foldUsageRecords` result verbatim. One of the two inputs `tracking::pruneProtection` reads; it decides nothing itself. Read-only: writes no ext-state.
|
||||
- `persist_internal.h` — internal-only shared helpers for the persist TU family (`session` / `ext_state_io` / `prune_fs`); included only by those three TUs, never a public seam (mirror of the panel's `panel_state.h` / the editor's `editor_internal.h` precedent). Holds the former anonymous-namespace helpers more than one split TU needs (active-project + `.rpp` path lookup, project-dir derivation, growing `GetProjExtState` read, project-GUID minting, bank-folder relocation) — all definitions live in `ext_state_io.cpp`. REAPER-free header: the project handle crosses this seam as the same opaque `void*` the public `session` header already uses.
|
||||
- `persist_internal.h` — internal-only shared helpers for the persist TU family (`session` / `ext_state_io` / `prune_fs`); included only by those three TUs, never a public seam (mirror of the panel's `panel_state.h` / the editor's `editor_internal.h` precedent). Holds the former anonymous-namespace helpers more than one split TU needs (active-project + `.rpp` path lookup, growing `GetProjExtState` read, project-GUID minting, bank-folder relocation) — all definitions live in `ext_state_io.cpp`. REAPER-free header: the project handle crosses this seam as the same opaque `void*` the public `session` header already uses.
|
||||
|
||||
## Gotchas
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// bank_root.cpp — see bank_root.h. main.cpp owns the API pointers; this TU gets them
|
||||
// extern.
|
||||
|
||||
#include "shell/persist/bank_root.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "core/capture/capture_paths.h" // bankRootFor
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace {
|
||||
|
||||
// EnumProjects is the only SDK read of a project's .rpp path — GetProjectPathEx is the
|
||||
// recording path, GetProjectName the bare file name. Empty for a never-saved project.
|
||||
// The active project is read through idx -1 first, so the common case is one call.
|
||||
std::string rppPathOf(ReaProject* proj) {
|
||||
std::vector<char> buf(4096, '\0');
|
||||
ReaProject* const active = EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||
if (!proj || proj == active) return std::string(buf.data());
|
||||
|
||||
for (int idx = 0;; ++idx) {
|
||||
buf[0] = '\0';
|
||||
ReaProject* const tab = EnumProjects(idx, buf.data(), static_cast<int>(buf.size()));
|
||||
if (!tab) return {}; // not an open tab: no location to answer with
|
||||
if (tab == proj) return std::string(buf.data());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string bankRootOf(ReaProject* proj) {
|
||||
// No unsaved root is supplied: a never-saved project has no bank location.
|
||||
return capture::bankRootFor(rppPathOf(proj), std::string{});
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
// bank_root — the extension's ONE answer to "which directory is this project's bank
|
||||
// under" (the rule is this directory's CLAUDE.md). The index still stores only
|
||||
// "reasampler_bank/<file>" relative to this root.
|
||||
|
||||
#include <string>
|
||||
|
||||
class ReaProject;
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// `proj`'s bank root per capture::bankRootFor: forward-slashed, no trailing slash, and
|
||||
// EMPTY when the project has no bank location or is not an open tab — callers refuse on
|
||||
// empty, never fall back. nullptr = the active project (REAPER's own convention).
|
||||
std::string bankRootOf(ReaProject* proj);
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -30,7 +30,6 @@
|
||||
#include "shell/persist/persist_internal.h"
|
||||
#include "shell/persist/session.h"
|
||||
|
||||
#include "core/capture/capture_paths.h" // projectDirOfRpp (pure path arithmetic)
|
||||
#include "core/instrument/map/bank_sync.h" // parseBankGeneration / formatBankGeneration (SHARED with the instrument reader)
|
||||
#include "core/wire/ext_state_read.h" // readProjExtStateGrowing (T2-04: the ONE grow-loop policy)
|
||||
#include "core/version/app_version.h"
|
||||
@@ -59,13 +58,6 @@ void* readActiveProject(std::string& rppPathOut) {
|
||||
return proj;
|
||||
}
|
||||
|
||||
// NOT GetProjectPathEx, which returns the recording path, not the .rpp's own
|
||||
// directory. Delegates to the pure projectDirOfRpp so both artifacts share one
|
||||
// implementation.
|
||||
std::string projectDirOf(const std::string& rppPath) {
|
||||
return capture::projectDirOfRpp(rppPath);
|
||||
}
|
||||
|
||||
std::string getProjExtStateString(void* proj, const char* ns, const char* key) {
|
||||
using wire::GrowingExtStateRead;
|
||||
const GrowingExtStateRead read = wire::readProjExtStateGrowing(
|
||||
|
||||
@@ -16,11 +16,6 @@ namespace reasampler::persist_detail {
|
||||
// reliable unsaved sentinel). Returns nullptr only when there is no active project.
|
||||
void* readActiveProject(std::string& rppPathOut);
|
||||
|
||||
// Parent directory of the .rpp, forward-slashed, no trailing slash. Empty in ->
|
||||
// empty out. Delegates to the pure capture::projectDirOfRpp — the SAME convention
|
||||
// the VST3 instrument resolves audio paths by.
|
||||
std::string projectDirOf(const std::string& rppPath);
|
||||
|
||||
// Growing GetProjExtState read for `key` in namespace `ns` against `proj`. Returns
|
||||
// "" when the key is absent (a valid empty bank, not an error) and warns on the
|
||||
// console for a value exceeding the 16 MB read ceiling (unreadable whole, ignored).
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
// confirm — executes.
|
||||
//
|
||||
// Compiled into the reaper_reasampler module. REAPER-facing only through the
|
||||
// persist_detail helpers and usage_scan; this TU itself calls no REAPER API directly.
|
||||
// persist_detail helpers, bank_root and usage_scan; this TU itself calls no REAPER API
|
||||
// directly.
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
@@ -37,6 +38,7 @@
|
||||
#include <shellapi.h>
|
||||
#endif
|
||||
|
||||
#include "shell/persist/bank_root.h"
|
||||
#include "shell/persist/persist_internal.h"
|
||||
#include "shell/persist/session.h"
|
||||
#include "shell/persist/usage_scan.h" // scanInstanceUsage — one of the authority's two inputs
|
||||
@@ -51,7 +53,6 @@ namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
using persist_detail::projectDirOf;
|
||||
using persist_detail::readActiveProject;
|
||||
|
||||
// The dry-run file-list display cap: count and size are always exact
|
||||
@@ -94,10 +95,10 @@ PruneScan scanPruneOrphans(const BankBook& book, const tracking::OriginLedger& l
|
||||
|
||||
// Resolve the current bank folder the same way the index does — never a
|
||||
// stored absolute path, so a Save-As relocation is followed automatically.
|
||||
const std::string projectDir = projectDirOf(rppPath);
|
||||
const std::string projectDir = bankRootOf(static_cast<ReaProject*>(proj));
|
||||
const std::string bankDir =
|
||||
capture::resolveBankFile(projectDir, capture::kBankSubfolder);
|
||||
if (bankDir.empty()) return scan; // unresolvable (no project dir) -> empty scan
|
||||
if (bankDir.empty()) return scan; // unresolvable (no bank root) -> empty scan
|
||||
|
||||
std::error_code ec;
|
||||
if (!fs::exists(bankDir, ec) || !fs::is_directory(bankDir, ec)) {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "shell/persist/bank_root.h"
|
||||
#include "shell/persist/ext_state_io.h"
|
||||
#include "shell/persist/persist_internal.h"
|
||||
|
||||
@@ -43,7 +44,6 @@ namespace reasampler {
|
||||
using persist_detail::ensureProjectGuid;
|
||||
using persist_detail::genProjectGuidString;
|
||||
using persist_detail::getProjExtStateString;
|
||||
using persist_detail::projectDirOf;
|
||||
using persist_detail::readActiveProject;
|
||||
using persist_detail::relocateBankFolder;
|
||||
|
||||
@@ -96,10 +96,12 @@ void ReaSamplerSession::poll() {
|
||||
// First observation: adopt current identity and load its index, without
|
||||
// treating it as a "change" (avoids a spurious relocation on startup).
|
||||
primed_ = true;
|
||||
loadFromProject(proj, projectDirOf(rppPath));
|
||||
lastProject_ = proj;
|
||||
lastGuid_ = ensureProjectGuid(proj, rppPath, currentGuid);
|
||||
lastRppPath_ = rppPath;
|
||||
const std::string root = bankRootOf(static_cast<ReaProject*>(proj));
|
||||
loadFromProject(proj, root);
|
||||
lastProject_ = proj;
|
||||
lastGuid_ = ensureProjectGuid(proj, rppPath, currentGuid);
|
||||
lastRppPath_ = rppPath;
|
||||
lastBankRoot_ = root;
|
||||
reloadRequested_ = false; // priming already loaded — a co-tick request is moot
|
||||
return;
|
||||
}
|
||||
@@ -113,10 +115,12 @@ void ReaSamplerSession::poll() {
|
||||
// path — the timer never polls ext-state content to detect an undo.
|
||||
if (reloadRequested_) {
|
||||
reloadRequested_ = false;
|
||||
loadFromProject(proj, projectDirOf(rppPath));
|
||||
lastProject_ = proj;
|
||||
lastGuid_ = ensureProjectGuid(proj, rppPath, currentGuid);
|
||||
lastRppPath_ = rppPath;
|
||||
const std::string root = bankRootOf(static_cast<ReaProject*>(proj));
|
||||
loadFromProject(proj, root);
|
||||
lastProject_ = proj;
|
||||
lastGuid_ = ensureProjectGuid(proj, rppPath, currentGuid);
|
||||
lastRppPath_ = rppPath;
|
||||
lastBankRoot_ = root;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -133,6 +137,7 @@ void ReaSamplerSession::poll() {
|
||||
|
||||
case capture::ProjectTransition::Load: {
|
||||
// A different project of record is active. Load its index; never relocate.
|
||||
const std::string root = bankRootOf(static_cast<ReaProject*>(proj));
|
||||
//
|
||||
// Forked-sibling re-GUID: gate on `!sameProjectObject` so this fires
|
||||
// only for the fork case (same GUID, different object) — a Save-As
|
||||
@@ -146,18 +151,20 @@ void ReaSamplerSession::poll() {
|
||||
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
|
||||
kProjExtGuidKey, fresh.c_str());
|
||||
MarkProjectDirty(static_cast<ReaProject*>(proj));
|
||||
loadFromProject(proj, projectDirOf(rppPath));
|
||||
lastProject_ = proj;
|
||||
lastGuid_ = fresh;
|
||||
lastRppPath_ = rppPath;
|
||||
loadFromProject(proj, root);
|
||||
lastProject_ = proj;
|
||||
lastGuid_ = fresh;
|
||||
lastRppPath_ = rppPath;
|
||||
lastBankRoot_ = root;
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal load: establish identity the same way prime does.
|
||||
loadFromProject(proj, projectDirOf(rppPath));
|
||||
lastProject_ = proj;
|
||||
lastGuid_ = ensureProjectGuid(proj, rppPath, currentGuid);
|
||||
lastRppPath_ = rppPath;
|
||||
loadFromProject(proj, root);
|
||||
lastProject_ = proj;
|
||||
lastGuid_ = ensureProjectGuid(proj, rppPath, currentGuid);
|
||||
lastRppPath_ = rppPath;
|
||||
lastBankRoot_ = root;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -166,10 +173,9 @@ void ReaSamplerSession::poll() {
|
||||
// the bank folder so the wavs sit under the new .rpp and the
|
||||
// index's relative paths still resolve. Keep the in-memory bank
|
||||
// as-is (Save-As copied our ext state) — do NOT reload.
|
||||
const std::string oldDir = projectDirOf(lastRppPath_);
|
||||
const std::string newDir = projectDirOf(rppPath);
|
||||
const std::string newRoot = bankRootOf(static_cast<ReaProject*>(proj));
|
||||
const capture::BankRelocation plan =
|
||||
capture::deriveRelocationPlan(oldDir, newDir);
|
||||
capture::deriveRelocationPlan(lastBankRoot_, newRoot);
|
||||
if (plan.needed) {
|
||||
relocateBankFolder(plan.oldBankDir, plan.newBankDir);
|
||||
}
|
||||
@@ -183,9 +189,10 @@ void ReaSamplerSession::poll() {
|
||||
kProjExtGuidKey, fresh.c_str());
|
||||
MarkProjectDirty(static_cast<ReaProject*>(proj));
|
||||
}
|
||||
lastProject_ = proj; // unchanged (same object) — set for symmetry
|
||||
lastGuid_ = fresh;
|
||||
lastRppPath_ = rppPath;
|
||||
lastProject_ = proj; // unchanged (same object) — set for symmetry
|
||||
lastGuid_ = fresh;
|
||||
lastRppPath_ = rppPath;
|
||||
lastBankRoot_ = newRoot;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +229,10 @@ private:
|
||||
void* lastProject_ = nullptr;
|
||||
std::string lastGuid_; // "" until the first saved project is seen
|
||||
std::string lastRppPath_;
|
||||
bool primed_ = false; // false until the first poll() observes state
|
||||
// Adopted with lastRppPath_. A Save-As relocates FROM this: by then the project
|
||||
// object reports only its new location, so the old root cannot be re-asked.
|
||||
std::string lastBankRoot_;
|
||||
bool primed_ = false; // false until the first poll() observes state
|
||||
bool loadPending_ = false; // raised by loadFromProject; drained by consumeLoadSignal
|
||||
bool reloadRequested_ = false; // raised by requestReload; drained by poll
|
||||
|
||||
|
||||
@@ -402,6 +402,49 @@ static void testBankRelativeForNameConventionAndEdge() {
|
||||
CHECK(bankRelativeForName("").empty());
|
||||
}
|
||||
|
||||
// --- bankRootFor --------------------------------------------------------------
|
||||
|
||||
static void testBankRootForSavedProjectIsTheRppDirectory() {
|
||||
CHECK(bankRootFor("/home/d/proj/song.rpp", "") == "/home/d/proj");
|
||||
#ifdef _WIN32
|
||||
CHECK(bankRootFor("C:\\Users\\D\\Proj\\song.rpp", "") == "c:/users/d/proj");
|
||||
#endif
|
||||
}
|
||||
|
||||
static void testBankRootForSavedProjectIgnoresTheUnsavedRoot() {
|
||||
// Once the project has a directory, a supplied unsaved root must not leak through.
|
||||
CHECK(bankRootFor("/home/d/proj/song.rpp", "/tmp/unsaved") == "/home/d/proj");
|
||||
}
|
||||
|
||||
static void testBankRootForUnsavedProjectFallsBackToTheUnsavedRoot() {
|
||||
CHECK(bankRootFor("", "/tmp/unsaved") == "/tmp/unsaved");
|
||||
// Normalized like the saved branch: trailing slash stripped, backslashes flipped.
|
||||
CHECK(bankRootFor("", "/tmp/unsaved/") == "/tmp/unsaved");
|
||||
#ifdef _WIN32
|
||||
CHECK(bankRootFor("", "C:\\Temp\\Unsaved\\") == "c:/temp/unsaved");
|
||||
#endif
|
||||
}
|
||||
|
||||
static void testBankRootForUnsavedProjectWithNoUnsavedRootIsEmpty() {
|
||||
// No CWD / default-location fallback: an unsaved project with nowhere to bank
|
||||
// yields empty, which every caller treats as "no bank location".
|
||||
CHECK(bankRootFor("", "").empty());
|
||||
}
|
||||
|
||||
static void testBankRootForRppWithNoDirectoryIsTreatedAsUnsaved() {
|
||||
CHECK(bankRootFor("song.rpp", "/tmp/unsaved") == "/tmp/unsaved");
|
||||
CHECK(bankRootFor("song.rpp", "").empty());
|
||||
}
|
||||
|
||||
static void testBankRootForBothBranchesProduceComparableRoots() {
|
||||
// The same directory reached through either branch is the same string, so a
|
||||
// root recorded under one branch compares equal to the other's.
|
||||
CHECK(bankRootFor("/a/b/song.rpp", "") == bankRootFor("", "/a/b/"));
|
||||
#ifdef _WIN32
|
||||
CHECK(bankRootFor("C:\\A\\B\\song.rpp", "") == bankRootFor("", "c:/a/b"));
|
||||
#endif
|
||||
}
|
||||
|
||||
int main() {
|
||||
testNormalizeSlashes();
|
||||
testNormalizeSlashesCaseFolding();
|
||||
@@ -433,6 +476,12 @@ int main() {
|
||||
testRenderPathsTakeTheirDirectoryVerbatim();
|
||||
testRenderPathsEmptyDirectoryStaysEmpty();
|
||||
testBankRelativeForNameConventionAndEdge();
|
||||
testBankRootForSavedProjectIsTheRppDirectory();
|
||||
testBankRootForSavedProjectIgnoresTheUnsavedRoot();
|
||||
testBankRootForUnsavedProjectFallsBackToTheUnsavedRoot();
|
||||
testBankRootForUnsavedProjectWithNoUnsavedRootIsEmpty();
|
||||
testBankRootForRppWithNoDirectoryIsTreatedAsUnsaved();
|
||||
testBankRootForBothBranchesProduceComparableRoots();
|
||||
|
||||
if (g_fail == 0) std::printf("capture_paths: all tests passed\n");
|
||||
else std::printf("capture_paths: %d CHECK(s) FAILED\n", g_fail);
|
||||
|
||||
Reference in New Issue
Block a user