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 { namespace reasampler::instrument::map {
std::string ownProjectDir(const void* own, const std::vector<ProjectTab>& open) { std::string ownProjectDir(const void* own, const std::vector<ProjectTab>& open,
if (!own) return {}; const void* activeFallback) {
const void* target = own ? own : activeFallback;
if (!target) return {};
for (const ProjectTab& tab : open) { for (const ProjectTab& tab : open) {
if (tab.project == own) return capture::projectDirOfRpp(tab.rppPath); if (tab.project == target) return capture::projectDirOfRpp(tab.rppPath);
} }
return {}; return {};
} }
+11 -5
View File
@@ -19,11 +19,17 @@ struct ProjectTab {
std::string rppPath; std::string rppPath;
}; };
// The directory of the project whose identity is `own`; empty when `own` is null, is not // The directory of the project whose identity is `own`; empty when `own` is not among
// among `open`, or was never saved. Deliberately no fallback to any other project: another // `open` or was never saved. Deliberately no fallback to any other project for a
// project's bank folder can hold a same-named file, and playing it is the wrong-project // NON-NULL `own`: another project's bank folder can hold a same-named file, and playing
// bug this exists to close. // it is the wrong-project bug this exists to close.
std::string ownProjectDir(const void* own, const std::vector<ProjectTab>& open); //
// `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 // 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 // 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`, ordered root list; every instrument-side sample read (`reloadInstrument`, `bakeSnapshot`,
the editor's `channelPcmFor`/`monoPcmFor`) goes through `readFromFirstRoot` over it. The old 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 `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 folder — do not reintroduce it as the PRIMARY resolve. It survives ONE sanctioned use: when
or an unsaved one yields no roots, which plays nothing, exactly as an unsaved project always `getReaperParent(3)` itself comes back null (REAPER may not expose the parent during plugin
did. The ordered list is one element today; a later root is added after the project creation/setState), `ownProjectDir` falls back to the focused tab — because at that point there
directory, in `sampleRootCandidates`. `[verify — DAW]`: that `getReaperParent(3)` is non-null is no "own project" identity to prefer a wrong answer over. A NON-NULL parent that is an
and enumerable during `setState` on project load, and that a background-tab instance and unlisted project or an unsaved one still yields no roots, which plays nothing, exactly as an
two open projects each with an instance play their own project's sample. 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 - 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 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 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) { std::optional<std::string> ReaperBridge::readReasamplerExtState(const std::string& key) {
if (!getProjExtState_ || !hostApp_) return std::nullopt; if (!getProjExtState_ || !hostApp_) return std::nullopt;
// getReaperParent(3) reads the live "reasampler" ext-state against the active project // getReaperParent(3) reads the live "reasampler" ext-state against the project the
// the instrument was instantiated in, so it follows project switches for free. A null // 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 // 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. // rather than bailing; a fruitless read still yields nullopt to the caller.
auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_); auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
@@ -204,7 +204,6 @@ std::vector<std::string> ReaperBridge::sampleRoots() {
if (!enumProjects_ || !hostApp_) return {}; if (!enumProjects_ || !hostApp_) return {};
auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_); auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
const void* own = reaper->getReaperParent(3); const void* own = reaper->getReaperParent(3);
if (!own) return {};
std::vector<ProjectTab> open; std::vector<ProjectTab> open;
std::vector<char> buf(4096, '\0'); 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. // A duplicate of a listed tab is harmless: the first identity match wins.
constexpr int kRenderingProject = 0x40000000; constexpr int kRenderingProject = 0x40000000;
enumerate(kRenderingProject); 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 } // namespace reasampler::vst
+11 -6
View File
@@ -32,8 +32,9 @@ public:
bool isConnected() const { return getProjExtState_ != nullptr; } bool isConnected() const { return getProjExtState_ != nullptr; }
// Reads a "reasampler" ext-state value by key from the host's active project. // Reads a "reasampler" ext-state value by key from this instance's own project
// Returns nullopt when unconnected, unresolvable, or the key is absent. // (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 // 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 // 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 // The ordered roots this instance's bank-relative sample paths resolve against — its OWN
// project's directory (getReaperParent(3) matched among EnumProjects' open projects), // project's directory (getReaperParent(3) matched among EnumProjects' open projects),
// never the focused tab's; see core/instrument/map/sample_roots. Empty when unconnected, // never the focused tab's; see core/instrument/map/sample_roots. When getReaperParent(3)
// when the own project is not found, or when it was never saved. Not RT-safe. // 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(); std::vector<std::string> sampleRoots();
// The instrument's TWO sanctioned ext-state write surfaces, each accepting exactly one // 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, char* keyOut, int keyOut_sz, char* valOut,
int valOut_sz); int valOut_sz);
// EnumProjects(idx, projfnOut, sz) -> the idx'th open project + its .rpp path, null past // 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 // the last. Walked by index for the open-tabs list; idx=-1 (the focused tab) was the
// sampleRoots replaced. // 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); 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. // 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, 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()); 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 ----------------------------------------------------- // --- sampleRootCandidates -----------------------------------------------------
static void testProjectDirIsTheOnlyCandidate() { static void testProjectDirIsTheOnlyCandidate() {
@@ -141,6 +153,9 @@ int main() {
testUnlistedProjectResolvesNothingRatherThanAnotherTab(); testUnlistedProjectResolvesNothingRatherThanAnotherTab();
testUnsavedOwnProjectResolvesNothingEvenBesideSavedTabs(); testUnsavedOwnProjectResolvesNothingEvenBesideSavedTabs();
testNoOpenProjectsResolvesNothing(); testNoOpenProjectsResolvesNothing();
testNullOwnFallsBackToTheActiveTab();
testNullOwnAndNullFallbackResolvesNothing();
testNonNullOwnIgnoresTheFallback();
testProjectDirIsTheOnlyCandidate(); testProjectDirIsTheOnlyCandidate();
testNoProjectDirMeansNoCandidates(); testNoProjectDirMeansNoCandidates();