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:
@@ -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 {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user