Fall back to focused tab when getReaperParent(3) is null

REAPER may not expose the parent project pointer during plugin creation/setState, so the instrument would silently resolve no sample roots. Only that null-parent case now falls back to EnumProjects(-1); a resolved-but-unlisted or unsaved project still resolves nothing.
This commit is contained in:
2026-09-25 14:59:07 -04:00
parent 0976ea13a8
commit cb6b63cc88
6 changed files with 62 additions and 24 deletions
+5 -3
View File
@@ -6,10 +6,12 @@
namespace reasampler::instrument::map {
std::string ownProjectDir(const void* own, const std::vector<ProjectTab>& open) {
if (!own) return {};
std::string ownProjectDir(const void* own, const std::vector<ProjectTab>& open,
const void* activeFallback) {
const void* target = own ? own : activeFallback;
if (!target) return {};
for (const ProjectTab& tab : open) {
if (tab.project == own) return capture::projectDirOfRpp(tab.rppPath);
if (tab.project == target) return capture::projectDirOfRpp(tab.rppPath);
}
return {};
}
+11 -5
View File
@@ -19,11 +19,17 @@ struct ProjectTab {
std::string rppPath;
};
// The directory of the project whose identity is `own`; empty when `own` is null, is not
// among `open`, or was never saved. Deliberately no fallback to any other project: another
// project's bank folder can hold a same-named file, and playing it is the wrong-project
// bug this exists to close.
std::string ownProjectDir(const void* own, const std::vector<ProjectTab>& open);
// The directory of the project whose identity is `own`; empty when `own` is not among
// `open` or was never saved. Deliberately no fallback to any other project for a
// NON-NULL `own`: another project's bank folder can hold a same-named file, and playing
// it is the wrong-project bug this exists to close.
//
// `activeFallback` is used ONLY when `own` is null (REAPER may not expose
// getReaperParent(3) yet during plugin creation/setState) — then it stands in for `own`
// unchanged, so a still-null fallback (or one absent from `open`) resolves nothing exactly
// as a null `own` always did.
std::string ownProjectDir(const void* own, const std::vector<ProjectTab>& open,
const void* activeFallback = nullptr);
// The roots a bank-relative path is tried against, in order. Only the project directory
// today — the ordered list is the seam a later root (tried after it) extends. Empty when
+9 -6
View File
@@ -320,12 +320,15 @@ against a performance budget — they are there because `VoiceEngine::applyLiveT
ordered root list; every instrument-side sample read (`reloadInstrument`, `bakeSnapshot`,
the editor's `channelPcmFor`/`monoPcmFor`) goes through `readFromFirstRoot` over it. The old
`EnumProjects(-1)` lookup resolved a background tab's instance against the FOCUSED project's
folder — do not reintroduce it, not even as a fallback. A null parent, an unlisted project
or an unsaved one yields no roots, which plays nothing, exactly as an unsaved project always
did. The ordered list is one element today; a later root is added after the project
directory, in `sampleRootCandidates`. `[verify — DAW]`: that `getReaperParent(3)` is non-null
and enumerable during `setState` on project load, and that a background-tab instance and
two open projects each with an instance play their own project's sample.
folder — do not reintroduce it as the PRIMARY resolve. It survives ONE sanctioned use: when
`getReaperParent(3)` itself comes back null (REAPER may not expose the parent during plugin
creation/setState), `ownProjectDir` falls back to the focused tab — because at that point there
is no "own project" identity to prefer a wrong answer over. A NON-NULL parent that is an
unlisted project or an unsaved one still yields no roots, which plays nothing, exactly as an
unsaved project always did. The ordered list is one element today; a later root is added after
the project directory, in `sampleRootCandidates`. `[verify — DAW]`: that `getReaperParent(3)` is
non-null and enumerable during `setState` on project load, and that a background-tab instance
and two open projects each with an instance play their own project's sample.
- The bake's availability probe runs on the SAME tick that paints the button, so the
control can never be enabled on one tick and refuse on the next. The bake Hold control's
applicability (`resolveBakeHoldNeeded`) rides the same tick for the same reason, and
+11 -4
View File
@@ -94,8 +94,8 @@ bool ReaperBridge::connect(Steinberg::FUnknown* context) {
std::optional<std::string> ReaperBridge::readReasamplerExtState(const std::string& key) {
if (!getProjExtState_ || !hostApp_) return std::nullopt;
// 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
// getReaperParent(3) reads the live "reasampler" ext-state against the 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<Steinberg::IReaperHostApplication*>(hostApp_);
@@ -204,7 +204,6 @@ std::vector<std::string> ReaperBridge::sampleRoots() {
if (!enumProjects_ || !hostApp_) return {};
auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
const void* own = reaper->getReaperParent(3);
if (!own) return {};
std::vector<ProjectTab> open;
std::vector<char> buf(4096, '\0');
@@ -220,7 +219,15 @@ std::vector<std::string> ReaperBridge::sampleRoots() {
// A duplicate of a listed tab is harmless: the first identity match wins.
constexpr int kRenderingProject = 0x40000000;
enumerate(kRenderingProject);
return sampleRootCandidates(ownProjectDir(own, open));
// getReaperParent(3) can come back null while REAPER has not yet exposed the parent
// (plugin creation/setState) — the focused tab is the one sanctioned fallback, used
// ONLY then; see ownProjectDir. A non-null own that isn't in `open`, or is unsaved,
// still resolves nothing.
const void* activeFallback =
own ? nullptr
: enumProjects_(-1, buf.data(), static_cast<int>(buf.size()));
return sampleRootCandidates(ownProjectDir(own, open, activeFallback));
}
} // namespace reasampler::vst
+11 -6
View File
@@ -32,8 +32,9 @@ public:
bool isConnected() const { return getProjExtState_ != nullptr; }
// Reads a "reasampler" ext-state value by key from the host's active project.
// Returns nullopt when unconnected, unresolvable, or the key is absent.
// Reads a "reasampler" ext-state value by key from this instance's own project
// (getReaperParent(3)). Returns nullopt when unconnected, unresolvable, or the key is
// absent.
//
// 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
@@ -42,8 +43,11 @@ public:
// The ordered roots this instance's bank-relative sample paths resolve against — its OWN
// project's directory (getReaperParent(3) matched among EnumProjects' open projects),
// never the focused tab's; see core/instrument/map/sample_roots. Empty when unconnected,
// when the own project is not found, or when it was never saved. Not RT-safe.
// never the focused tab's; see core/instrument/map/sample_roots. When getReaperParent(3)
// is null (REAPER may not expose it yet during plugin creation/setState), falls back to
// the focused tab (EnumProjects(-1)) — the one sanctioned use of it. Empty when
// unconnected, when neither resolves to an open project, or when it was never saved.
// Not RT-safe.
std::vector<std::string> sampleRoots();
// The instrument's TWO sanctioned ext-state write surfaces, each accepting exactly one
@@ -110,8 +114,9 @@ private:
char* keyOut, int keyOut_sz, char* valOut,
int valOut_sz);
// EnumProjects(idx, projfnOut, sz) -> the idx'th open project + its .rpp path, null past
// the last. Walked by index only: idx=-1 (the focused tab) is the wrong-project lookup
// sampleRoots replaced.
// the last. Walked by index for the open-tabs list; idx=-1 (the focused tab) was the
// wrong-project lookup sampleRoots replaced as the PRIMARY resolve — it survives only as
// the fallback used when getReaperParent(3) is null (see sampleRoots).
using EnumProjectsFn = void* (*)(int idx, char* projfnOut, int projfnOut_sz);
// Used ONLY by the two prefix-guarded writers — see the read-only-bank note there.
using SetProjExtStateFn = int (*)(void* proj, const char* extname, const char* key,
+15
View File
@@ -64,6 +64,18 @@ static void testNoOpenProjectsResolvesNothing() {
CHECK(ownProjectDir(&projA, {}).empty());
}
static void testNullOwnFallsBackToTheActiveTab() {
CHECK(ownProjectDir(nullptr, threeTabs(), &projB) == "c:/projects/b");
}
static void testNullOwnAndNullFallbackResolvesNothing() {
CHECK(ownProjectDir(nullptr, threeTabs(), nullptr).empty());
}
static void testNonNullOwnIgnoresTheFallback() {
CHECK(ownProjectDir(&stranger, threeTabs(), &projB).empty());
}
// --- sampleRootCandidates -----------------------------------------------------
static void testProjectDirIsTheOnlyCandidate() {
@@ -141,6 +153,9 @@ int main() {
testUnlistedProjectResolvesNothingRatherThanAnotherTab();
testUnsavedOwnProjectResolvesNothingEvenBesideSavedTabs();
testNoOpenProjectsResolvesNothing();
testNullOwnFallsBackToTheActiveTab();
testNullOwnAndNullFallbackResolvesNothing();
testNonNullOwnIgnoresTheFallback();
testProjectDirIsTheOnlyCandidate();
testNoProjectDirMeansNoCandidates();