Ξ-W2-T1: the resample bake chain — instrument renders, extension banks, one click re-points and resets
This commit is contained in:
@@ -42,6 +42,10 @@ bool ReaperBridge::connect(Steinberg::FUnknown* context) {
|
||||
setProjExtState_ = nullptr;
|
||||
getTrackGuid_ = nullptr;
|
||||
guidToString_ = nullptr;
|
||||
namedCommandLookup_ = nullptr;
|
||||
mainOnCommandEx_ = nullptr;
|
||||
getCursorPositionEx_ = nullptr;
|
||||
timeMapGetTimeSigAtTime_ = nullptr;
|
||||
hostApp_ = nullptr;
|
||||
if (!context) return false;
|
||||
|
||||
@@ -69,6 +73,17 @@ bool ReaperBridge::connect(Steinberg::FUnknown* context) {
|
||||
reaper->getReaperApi("GetTrackGUID"));
|
||||
guidToString_ = reinterpret_cast<GuidToStringFn>(
|
||||
reaper->getReaperApi("guidToString"));
|
||||
// The bake crossing: resolve the extension's action id by name, then fire it against
|
||||
// this instance's own project. All degrade to null gracefully — an unresolvable pair
|
||||
// simply leaves the bake affordance disabled.
|
||||
namedCommandLookup_ = reinterpret_cast<NamedCommandLookupFn>(
|
||||
reaper->getReaperApi("NamedCommandLookup"));
|
||||
mainOnCommandEx_ = reinterpret_cast<MainOnCommandExFn>(
|
||||
reaper->getReaperApi("Main_OnCommandEx"));
|
||||
getCursorPositionEx_ = reinterpret_cast<GetCursorPositionExFn>(
|
||||
reaper->getReaperApi("GetCursorPositionEx"));
|
||||
timeMapGetTimeSigAtTime_ = reinterpret_cast<TimeMapGetTimeSigAtTimeFn>(
|
||||
reaper->getReaperApi("TimeMap_GetTimeSigAtTime"));
|
||||
|
||||
return getProjExtState_ != nullptr;
|
||||
}
|
||||
@@ -95,25 +110,70 @@ std::optional<std::string> ReaperBridge::readReasamplerExtState(const std::strin
|
||||
return decodeGetProjExtState(read.apiReturn, read.value);
|
||||
}
|
||||
|
||||
bool ReaperBridge::writeUsageExtState(const std::string& usageKey,
|
||||
const std::string& value) {
|
||||
bool ReaperBridge::writeGuarded(const std::string& key, const std::string& value,
|
||||
const char* requiredPrefix) {
|
||||
if (!setProjExtState_ || !hostApp_) return false;
|
||||
// Read-only-bank guard: this module writes usage keys and nothing else. A non-
|
||||
// "rsusage_" key is refused rather than widening the instrument's write surface
|
||||
// (banks/view/tail/assign stay extension-owned).
|
||||
const std::string prefix = kProjExtUsageKeyPrefix;
|
||||
if (usageKey.compare(0, prefix.size(), prefix) != 0) return false;
|
||||
// Read-only-BANK guard: this module writes the two sanctioned per-instance prefixes
|
||||
// and nothing else. Any other key is refused rather than widening the instrument's
|
||||
// write surface (banks/view/tail/assign stay extension-owned).
|
||||
const std::string prefix = requiredPrefix;
|
||||
if (key.compare(0, prefix.size(), prefix) != 0) return false;
|
||||
|
||||
auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
|
||||
void* proj = reaper->getReaperParent(3); // null = current project (same as reads)
|
||||
// SetProjExtState returns the size of the extname's state — after storing a
|
||||
// non-empty value that's necessarily > 0, so <= 0 means the write did not land (the
|
||||
// publish path retries next reload tick; a silent drop would leave holds unprotected).
|
||||
// A deliberate CLEAR (empty value) shrinks the state and can legitimately return 0,
|
||||
// so it is reported as landed.
|
||||
const int rv =
|
||||
setProjExtState_(proj, kProjExtNamespace(), usageKey.c_str(), value.c_str());
|
||||
// Deliberately NO MarkProjectDirty: a usage change always rides a component-state
|
||||
// change that already dirties the project.
|
||||
return rv > 0;
|
||||
setProjExtState_(proj, kProjExtNamespace(), key.c_str(), value.c_str());
|
||||
return value.empty() ? true : rv > 0;
|
||||
}
|
||||
|
||||
bool ReaperBridge::writeUsageExtState(const std::string& usageKey,
|
||||
const std::string& value) {
|
||||
return writeGuarded(usageKey, value, kProjExtUsageKeyPrefix);
|
||||
}
|
||||
|
||||
bool ReaperBridge::writeBakeExtState(const std::string& bakeKey,
|
||||
const std::string& value) {
|
||||
return writeGuarded(bakeKey, value, kProjExtBakeKeyPrefix);
|
||||
}
|
||||
|
||||
int ReaperBridge::lookupCommand(const std::string& commandName) {
|
||||
if (!namedCommandLookup_ || commandName.empty()) return 0;
|
||||
return namedCommandLookup_(commandName.c_str());
|
||||
}
|
||||
|
||||
bool ReaperBridge::extensionActionAvailable(const std::string& commandName) {
|
||||
return mainOnCommandEx_ != nullptr && lookupCommand(commandName) != 0;
|
||||
}
|
||||
|
||||
bool ReaperBridge::invokeExtensionAction(const std::string& commandName) {
|
||||
if (!mainOnCommandEx_ || !hostApp_) return false;
|
||||
const int command = lookupCommand(commandName);
|
||||
if (command == 0) return false;
|
||||
auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
|
||||
// getReaperParent(3), not the focused tab: an instance in a background tab must bake
|
||||
// into ITS OWN project's bank.
|
||||
void* proj = reaper->getReaperParent(3);
|
||||
// flag 0 is the conventional "no modifier" value; the header documents no other, and
|
||||
// the extension's hookcommand ignores it.
|
||||
mainOnCommandEx_(command, 0, proj);
|
||||
return true;
|
||||
}
|
||||
|
||||
double ReaperBridge::projectTempoBpm() {
|
||||
if (!timeMapGetTimeSigAtTime_ || !getCursorPositionEx_ || !hostApp_) return 0.0;
|
||||
auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
|
||||
void* proj = reaper->getReaperParent(3);
|
||||
int num = 0, denom = 0;
|
||||
double tempo = 0.0;
|
||||
timeMapGetTimeSigAtTime_(proj, getCursorPositionEx_(proj), &num, &denom, &tempo);
|
||||
// The meter is read but not applied: the note model's beat is a quarter note by
|
||||
// ruling (core/instrument/note/CLAUDE.md), so the undivided BPM is the right one.
|
||||
return tempo;
|
||||
}
|
||||
|
||||
std::string ReaperBridge::currentTrackGuid() {
|
||||
|
||||
Reference in New Issue
Block a user