Fix the bake scan report's unsound absence-as-signal claim
Gate the summary on any unanswered key and on SetProjExtState's return, print one line per scanned key, and make the undo block stack RAII.
This commit is contained in:
+168
-80
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
@@ -222,12 +223,22 @@ std::vector<std::string> pendingBakeKeys(ReaProject* proj) {
|
||||
return keys;
|
||||
}
|
||||
|
||||
std::optional<std::string> readKey(ReaProject* proj, const std::string& key) {
|
||||
// Whole value, or the reason there isn't one. Absent and Overflow both leave the scan with
|
||||
// nothing to decode — one verdict — but they are different faults to go fix, so the reason
|
||||
// survives to the per-key console line rather than collapsing here.
|
||||
struct KeyRead {
|
||||
std::optional<std::string> value;
|
||||
bool oversized = false; // the value never fit under the grow loop's ceiling
|
||||
};
|
||||
|
||||
KeyRead readKey(ReaProject* proj, const std::string& key) {
|
||||
const auto read = wire::readProjExtStateGrowing([&](char* buf, int cap) {
|
||||
return GetProjExtState(proj, kProjExtNamespace(), key.c_str(), buf, cap);
|
||||
});
|
||||
if (read.status != wire::GrowingExtStateRead::Status::Complete) return std::nullopt;
|
||||
return read.value;
|
||||
KeyRead out;
|
||||
out.oversized = read.status == wire::GrowingExtStateRead::Status::Overflow;
|
||||
if (read.status == wire::GrowingExtStateRead::Status::Complete) out.value = read.value;
|
||||
return out;
|
||||
}
|
||||
|
||||
struct OpenProject {
|
||||
@@ -249,21 +260,49 @@ std::vector<OpenProject> openProjects() {
|
||||
return out;
|
||||
}
|
||||
|
||||
// One answer to write back after the undo block closes.
|
||||
struct Answer {
|
||||
// One scanned key: what the pass decided about it, and the answer to write back after the
|
||||
// undo block closes.
|
||||
struct ScannedKey {
|
||||
ReaProject* proj = nullptr;
|
||||
std::string key;
|
||||
std::string wire; // empty = clear the key instead of answering it
|
||||
std::string console; // empty = nothing to print
|
||||
bool writesAnswer = false; // false = an Ignore verdict, nothing to write
|
||||
std::string answerWire; // with writesAnswer: empty = clear the key instead
|
||||
std::string console; // the outcome's own sentence; empty = nothing to print
|
||||
wire::BakeKeyOutcome report;
|
||||
};
|
||||
|
||||
// Undo_BeginBlock2/EndBlock2 as stack RAII, the FxBypassGuard discipline: landOne reads a
|
||||
// whole WAV and hashes it, so a bad_alloc between the two calls would otherwise leave an
|
||||
// open undo block in the user's project. Closing with an empty description records no undo
|
||||
// point, which is what every path that landed nothing wants anyway.
|
||||
class UndoBlock {
|
||||
public:
|
||||
UndoBlock() { Undo_BeginBlock2(nullptr); }
|
||||
~UndoBlock() { Undo_EndBlock2(nullptr, description_.c_str(), flags_); }
|
||||
void recordPoint(std::string description, int flags) {
|
||||
description_ = std::move(description);
|
||||
flags_ = flags;
|
||||
}
|
||||
UndoBlock(const UndoBlock&) = delete;
|
||||
UndoBlock& operator=(const UndoBlock&) = delete;
|
||||
|
||||
private:
|
||||
std::string description_;
|
||||
int flags_ = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void RunResampleBake(ReaSamplerSession& session) {
|
||||
// Whether REAPER makes Main_OnCommandEx's `proj` current for the action's duration is
|
||||
// unverified in the DAW; the per-key verdict holds either way, and a request it cannot
|
||||
// land is told why rather than silently ignored.
|
||||
const void* loaded = session.loadedProject();
|
||||
// REAPER's CURRENT tab, which is all this is. The action was invoked with the asking
|
||||
// instance's own project as `proj` — getReaperParent(3), documented as the parent
|
||||
// project in reaper_vst3_interfaces.h, and already paired with an EnumProjects-derived
|
||||
// enumeration by the shipped rsusage_* path prune's protection depends on. What is
|
||||
// `[verify — DAW]` is only whether REAPER makes that `proj` CURRENT for the action's
|
||||
// duration, so nothing below may call this "the tab the bake was fired against". The
|
||||
// per-key verdict holds either way, and a request this pass cannot land is told why
|
||||
// rather than silently ignored.
|
||||
const void* active = EnumProjects(-1, nullptr, 0);
|
||||
// `loaded == active` alone is enough: every OpenProject::proj enumerated below is a
|
||||
// real, non-null tab, so an unloaded `loaded` (nullptr) can never equal one and
|
||||
@@ -271,88 +310,137 @@ void RunResampleBake(ReaSamplerSession& session) {
|
||||
const wire::BakeScanContext scanContext{loaded == active};
|
||||
const std::int64_t nowSec = static_cast<std::int64_t>(std::time(nullptr));
|
||||
|
||||
std::vector<Answer> answers;
|
||||
std::vector<ScannedKey> scanned;
|
||||
wire::BakeScanTally tally; // every verdict below is counted, skips included
|
||||
Undo_BeginBlock2(nullptr);
|
||||
for (const OpenProject& open : openProjects()) {
|
||||
++tally.tabsScanned;
|
||||
const bool activeTab = static_cast<const void*>(open.proj) == active;
|
||||
for (const std::string& key : pendingBakeKeys(open.proj)) {
|
||||
++tally.keysFound;
|
||||
if (activeTab) ++tally.activeTabKeys;
|
||||
const std::optional<std::string> raw = readKey(open.proj, key);
|
||||
const std::optional<BakeRequest> request =
|
||||
raw ? wire::decodeBakeRequest(*raw) : std::nullopt;
|
||||
const wire::BakeScanKey scanKey{
|
||||
raw.has_value(), request.has_value(), request ? request->generation : 0,
|
||||
static_cast<const void*>(open.proj) == loaded};
|
||||
const wire::BakeScanVerdict verdict =
|
||||
wire::classifyBakeScan(scanContext, scanKey, nowSec);
|
||||
std::string aborted; // set only when the scan itself threw
|
||||
// The scan allocates outside landOne's own guard too (readKey grows a buffer toward
|
||||
// 16 MB, every container here allocates), and a throw that escaped would discard the
|
||||
// answers already buffered — the exact no-answer-with-a-silent-console this action
|
||||
// exists to make impossible. UndoBlock is inside the try, so unwinding still closes it.
|
||||
try {
|
||||
UndoBlock undo;
|
||||
for (const OpenProject& open : openProjects()) {
|
||||
++tally.tabsScanned;
|
||||
const bool activeTab = static_cast<const void*>(open.proj) == active;
|
||||
for (const std::string& key : pendingBakeKeys(open.proj)) {
|
||||
++tally.keysFound;
|
||||
if (activeTab) ++tally.activeTabKeys;
|
||||
const KeyRead read = readKey(open.proj, key);
|
||||
const std::optional<BakeRequest> request =
|
||||
read.value ? wire::decodeBakeRequest(*read.value) : std::nullopt;
|
||||
const wire::BakeScanKey scanKey{
|
||||
read.value.has_value(), request.has_value(),
|
||||
request ? request->generation : 0,
|
||||
static_cast<const void*>(open.proj) == loaded};
|
||||
const wire::BakeScanVerdict verdict =
|
||||
wire::classifyBakeScan(scanContext, scanKey, nowSec);
|
||||
|
||||
// Leave it — the writing instance owns clearing its own key.
|
||||
if (verdict == wire::BakeScanVerdict::IgnoreUnreadable) {
|
||||
++tally.unreadable;
|
||||
continue;
|
||||
}
|
||||
if (verdict == wire::BakeScanVerdict::IgnoreNotARequest) {
|
||||
++tally.notARequest;
|
||||
continue;
|
||||
}
|
||||
if (verdict == wire::BakeScanVerdict::ClearStale) {
|
||||
++tally.staleCleared;
|
||||
answers.push_back(Answer{open.proj, key, std::string{}, std::string{}});
|
||||
continue;
|
||||
}
|
||||
ScannedKey entry;
|
||||
entry.proj = open.proj;
|
||||
entry.key = key;
|
||||
entry.report.verdict = verdict;
|
||||
entry.report.oversized = read.oversized;
|
||||
|
||||
BakeOutcome outcome;
|
||||
if (verdict == wire::BakeScanVerdict::RefuseWrongProject) {
|
||||
outcome = refuse(BakeStatus::WrongProject,
|
||||
"this bake's project tab is not the one the extension has "
|
||||
"loaded -- focus that tab and try again",
|
||||
request->generation);
|
||||
} else {
|
||||
outcome = landOne(session, open.dir, *request);
|
||||
if (outcome.status == BakeStatus::Ok) ++tally.landed;
|
||||
// Leave it — the writing instance owns clearing its own key.
|
||||
if (verdict == wire::BakeScanVerdict::IgnoreUnreadable) {
|
||||
++tally.unreadable;
|
||||
scanned.push_back(std::move(entry));
|
||||
continue;
|
||||
}
|
||||
// `!request` is folded in here rather than dereferenced below: today
|
||||
// classifyBakeScan filters every undecoded key into this verdict, but a
|
||||
// future verdict that did not would otherwise reach *request unguarded.
|
||||
if (verdict == wire::BakeScanVerdict::IgnoreNotARequest || !request) {
|
||||
++tally.notARequest;
|
||||
entry.report.verdict = wire::BakeScanVerdict::IgnoreNotARequest;
|
||||
scanned.push_back(std::move(entry));
|
||||
continue;
|
||||
}
|
||||
if (verdict == wire::BakeScanVerdict::ClearStale) {
|
||||
++tally.staleCleared;
|
||||
entry.writesAnswer = true; // empty answerWire = clear the key
|
||||
scanned.push_back(std::move(entry));
|
||||
continue;
|
||||
}
|
||||
|
||||
BakeOutcome outcome;
|
||||
if (verdict == wire::BakeScanVerdict::RefuseWrongProject) {
|
||||
outcome =
|
||||
refuse(BakeStatus::WrongProject,
|
||||
"this bake's project tab is not the one the extension has "
|
||||
"loaded -- focus that tab and try again",
|
||||
request->generation);
|
||||
} else {
|
||||
// landOne reads and hashes a whole WAV. A throw here would otherwise
|
||||
// discard every answer buffered so far and leave each asking instance
|
||||
// with a no-answer it cannot explain; converting it to a refusal keeps
|
||||
// the pass and its report intact.
|
||||
try {
|
||||
outcome = landOne(session, open.dir, *request);
|
||||
} catch (const std::exception& e) {
|
||||
outcome = refuse(BakeStatus::Failed,
|
||||
std::string("the landing failed: ") + e.what(),
|
||||
request->generation);
|
||||
} catch (...) {
|
||||
outcome = refuse(BakeStatus::Failed,
|
||||
"the landing failed for an unknown reason",
|
||||
request->generation);
|
||||
}
|
||||
if (outcome.status == BakeStatus::Ok) ++tally.landed;
|
||||
}
|
||||
++tally.answered; // QUEUED, not written — the write loop below judges that
|
||||
entry.report.landed = outcome.status == BakeStatus::Ok;
|
||||
// A WrongProject request left sitting in a tab this call did not come from
|
||||
// prints again on every OTHER tab's bake, since the scan revisits every
|
||||
// open project each time. Only a refusal in REAPER's active tab is likely
|
||||
// to be fresh feedback to a user who just clicked bake; every other one is
|
||||
// a rescan repeat. The per-key line below is printed either way.
|
||||
if (outcome.status != BakeStatus::Ok &&
|
||||
(outcome.status != BakeStatus::WrongProject || activeTab)) {
|
||||
entry.console = "ReaSampler resample: " + outcome.message + ".\n";
|
||||
}
|
||||
entry.writesAnswer = true;
|
||||
entry.answerWire = wire::encodeBakeOutcome(outcome);
|
||||
scanned.push_back(std::move(entry));
|
||||
}
|
||||
++tally.answered;
|
||||
// A WrongProject request left sitting in a tab this call did not come from
|
||||
// prints again on every OTHER tab's bake, since the scan revisits every open
|
||||
// project each time. `active` is this call's own proxy for "the invoking tab" —
|
||||
// only that tab's own refusal is fresh feedback to a user who just clicked
|
||||
// bake; every other one is a rescan repeat.
|
||||
std::string console;
|
||||
if (outcome.status != BakeStatus::Ok &&
|
||||
(outcome.status != BakeStatus::WrongProject || activeTab)) {
|
||||
console = "ReaSampler resample: " + outcome.message + ".\n";
|
||||
}
|
||||
answers.push_back(
|
||||
Answer{open.proj, key, wire::encodeBakeOutcome(outcome), console});
|
||||
}
|
||||
}
|
||||
|
||||
if (tally.landed > 0) {
|
||||
// A bake changes what a live instance would play, so the generation bump rides the
|
||||
// persist — every other open instance refreshes hands-free.
|
||||
session.bumpBankGeneration();
|
||||
const bool persisted = session.saveToActiveProject();
|
||||
Undo_EndBlock2(nullptr,
|
||||
persisted ? "ReaSampler: resample bake into bank" : "",
|
||||
persisted ? UNDO_STATE_MISCCFG : 0);
|
||||
} else {
|
||||
Undo_EndBlock2(nullptr, "", 0); // nothing landed — record no empty undo point
|
||||
if (tally.landed > 0) {
|
||||
// A bake changes what a live instance would play, so the generation bump rides
|
||||
// the persist — every other open instance refreshes hands-free.
|
||||
session.bumpBankGeneration();
|
||||
if (session.saveToActiveProject())
|
||||
undo.recordPoint("ReaSampler: resample bake into bank", UNDO_STATE_MISCCFG);
|
||||
}
|
||||
} catch (...) {
|
||||
aborted =
|
||||
"ReaSampler resample: the landing pass stopped early on an internal failure. "
|
||||
"The answers it had already prepared are written below; any key it had not "
|
||||
"reached yet went unanswered and is not named at all.\n";
|
||||
}
|
||||
|
||||
// OUTSIDE the undo block on purpose: the answer is transient handshake state, and an
|
||||
// undo point that captured it could restore the consumed request on the next Ctrl-Z —
|
||||
// which the following bake would then re-land against a temp file that is long gone.
|
||||
for (const Answer& answer : answers) {
|
||||
SetProjExtState(answer.proj, kProjExtNamespace(), answer.key.c_str(),
|
||||
answer.wire.c_str());
|
||||
if (!answer.console.empty()) ShowConsoleMsg(answer.console.c_str());
|
||||
std::string console = aborted;
|
||||
for (ScannedKey& entry : scanned) {
|
||||
if (entry.writesAnswer) {
|
||||
// SetProjExtState returns the size of the extname's state, so storing a
|
||||
// non-empty value necessarily returns > 0 and <= 0 means the write did not
|
||||
// land. A deliberate clear shrinks the state and can legitimately return 0 —
|
||||
// the same reading reaper_bridge's writeGuarded applies to the other end.
|
||||
const int rv = SetProjExtState(entry.proj, kProjExtNamespace(),
|
||||
entry.key.c_str(), entry.answerWire.c_str());
|
||||
entry.report.answerWritten = entry.answerWire.empty() || rv > 0;
|
||||
if (!entry.report.answerWritten) ++tally.writeFailed;
|
||||
}
|
||||
// Unconditional, including on success: the tally counts keys but cannot name them,
|
||||
// and the key name is the only thing that tells an instance which line is its own.
|
||||
console += wire::describeBakeKey(entry.key, entry.report);
|
||||
console += entry.console;
|
||||
}
|
||||
// Empty unless the pass answered nobody, so a bake that spoke for itself stays quiet.
|
||||
const std::string report = wire::describeBakeScan(tally);
|
||||
if (!report.empty()) ShowConsoleMsg(report.c_str());
|
||||
console += wire::describeBakeScan(tally);
|
||||
if (!console.empty()) ShowConsoleMsg(console.c_str());
|
||||
if (tally.landed > 0) bankPanelRefresh();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user