Ξ-W2-T1 remediation: print master gain into the bake, derive the window from the dialed sound, reset play mode to Trigger

This commit is contained in:
2026-08-01 17:05:28 -04:00
parent 60308a3655
commit 39c2d1cdb4
31 changed files with 679 additions and 201 deletions
+98 -31
View File
@@ -6,11 +6,13 @@
#include "shell/capture/bake_land.h"
#include <cstdint>
#include <cstdlib>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "core/capture/capture_paths.h" // deriveBankPaths / projectDirOfRpp
@@ -51,6 +53,13 @@ using wire::BakeOutcome;
using wire::BakeRequest;
using wire::BakeStatus;
// The whole chain is a call and a return inside ONE editor tick, so a request older than
// this has no reader left: it is a crash leftover, and it is CLEARED rather than landed.
// Without the guard a stranded request would be banked on the next unrelated instance's
// click, and the outcome written back to a key nobody will collect would persist into the
// .rpp forever.
constexpr std::int64_t kMaxRequestAgeSeconds = 30;
BakeOutcome refuse(BakeStatus status, std::string message, std::int64_t generation) {
BakeOutcome out;
out.status = status;
@@ -59,12 +68,6 @@ BakeOutcome refuse(BakeStatus status, std::string message, std::int64_t generati
return out;
}
std::string currentProjectDir() {
std::vector<char> buf(4096, '\0');
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
return projectDirOfRpp(std::string(buf.data()));
}
bool writeFileBytes(const std::string& path, const std::vector<std::uint8_t>& bytes) {
std::ofstream f(path, std::ios::binary | std::ios::trunc);
if (!f) return false;
@@ -85,11 +88,12 @@ const Sample* findSourceSample(const BankBook& book, const std::string& sampleId
return nullptr;
}
// Lands ONE request. Mutates the book + ledger without persisting; the caller persists
// once for the batch. Every refusal path leaves the book untouched and writes no file, so
// a failed bake is invisible to the project.
BakeOutcome landOne(ReaSamplerSession& session, const BakeRequest& request) {
const std::string projectDir = currentProjectDir();
// Lands ONE request into `session`'s book, which must already be the book of the project
// `projectDir` names. Mutates book + ledger without persisting; the caller persists once for
// the batch. Every refusal path leaves the book untouched and writes no file, so a failed
// bake is invisible to the project.
BakeOutcome landOne(ReaSamplerSession& session, const std::string& projectDir,
const BakeRequest& request) {
if (projectDir.empty())
return refuse(BakeStatus::NoProject,
"no saved project, so the bank has no location", request.generation);
@@ -190,9 +194,9 @@ BakeOutcome landOne(ReaSamplerSession& session, const BakeRequest& request) {
: (book.index(bankId) &&
book.index(bankId)->add(landed) == AddResult::Added);
if (!indexed) {
// Self-cleanup of a file this call wrote seconds ago and never indexed — not the
// prune's deletion authority, which governs files the bank knows about. Leaving it
// would deposit an untracked orphan for every refused bake.
// Self-cleanup of a file this call wrote seconds ago and never indexed — the
// carve-out shell/persist/CLAUDE.md states, not prune's authority over the bank's
// known bytes. Leaving it would deposit an untracked orphan per refused bake.
fs::remove(destPath, ec);
return refuse(BakeStatus::IndexRejected,
replace ? "the bank refused the replacement"
@@ -209,7 +213,7 @@ BakeOutcome landOne(ReaSamplerSession& session, const BakeRequest& request) {
return out;
}
// Every "rsbake_*" key in the project, keys only — the value can outgrow
// Every "rsbake_*" key in one project, keys only — the value can outgrow
// EnumProjExtState's fixed buffer, so it is read separately by the growing reader.
std::vector<std::string> pendingBakeKeys(ReaProject* proj) {
std::vector<std::string> keys;
@@ -234,28 +238,82 @@ std::optional<std::string> readKey(ReaProject* proj, const std::string& key) {
return read.value;
}
struct OpenProject {
ReaProject* proj = nullptr;
std::string dir; // the project's own directory; empty for a never-saved project
};
// Every open project tab. The request key lives in the INSTANCE's project, which is not
// necessarily the focused one, so the scan cannot assume the current tab holds it.
std::vector<OpenProject> openProjects() {
std::vector<OpenProject> out;
std::vector<char> buf(4096, '\0');
for (int idx = 0;; ++idx) {
buf[0] = '\0';
ReaProject* proj = EnumProjects(idx, buf.data(), static_cast<int>(buf.size()));
if (!proj) break;
out.push_back(OpenProject{proj, projectDirOfRpp(std::string(buf.data()))});
}
return out;
}
// One answer to write back after the undo block closes.
struct Answer {
ReaProject* proj = nullptr;
std::string key;
std::string wire; // empty = clear the key instead of answering it
std::string console; // empty = nothing to print
};
} // namespace
void RunResampleBake(ReaSamplerSession& session) {
ReaProject* proj = EnumProjects(-1, nullptr, 0);
if (!proj) return;
// Three projects have to agree before anything may land: the tab the request was found
// in, the tab whose book/ledger poll() last loaded, and the tab saveToActiveProject
// will persist into. Land on a disagreement and one tab's bake is written into
// another's bank. Whether REAPER makes Main_OnCommandEx's `proj` current for the
// action's duration is unverified in the DAW; this holds either way, and a request it
// cannot land is told why rather than silently ignored.
const void* loaded = session.loadedProject();
const void* active = EnumProjects(-1, nullptr, 0);
const bool sessionUsable = loaded != nullptr && loaded == active;
const std::int64_t nowSec = static_cast<std::int64_t>(std::time(nullptr));
std::vector<Answer> answers;
int landedCount = 0;
Undo_BeginBlock2(nullptr);
for (const std::string& key : pendingBakeKeys(proj)) {
const std::optional<std::string> raw = readKey(proj, key);
if (!raw) continue;
const std::optional<BakeRequest> request = wire::decodeBakeRequest(*raw);
// Not a request: an outcome this instance has not yet collected, or a value from a
// build we do not read. Leave it — the writing instance owns clearing its own key.
if (!request) continue;
for (const OpenProject& open : openProjects()) {
for (const std::string& key : pendingBakeKeys(open.proj)) {
const std::optional<std::string> raw = readKey(open.proj, key);
if (!raw) continue;
const std::optional<BakeRequest> request = wire::decodeBakeRequest(*raw);
// Not a request: an outcome this instance has not yet collected, or a value from
// a build we do not read. Leave it — the writing instance owns clearing its key.
if (!request) continue;
const BakeOutcome outcome = landOne(session, *request);
if (outcome.status == BakeStatus::Ok) ++landedCount;
SetProjExtState(proj, kProjExtNamespace(), key.c_str(),
wire::encodeBakeOutcome(outcome).c_str());
if (outcome.status != BakeStatus::Ok)
ShowConsoleMsg(("ReaSampler resample: " + outcome.message + ".\n").c_str());
// Stale (either direction, so a clock moved backwards is caught too): clear the
// key, never answer it. The instance that could read an answer is gone.
if (std::llabs(nowSec - request->generation) > kMaxRequestAgeSeconds) {
answers.push_back(Answer{open.proj, key, std::string{}, std::string{}});
continue;
}
BakeOutcome outcome;
if (!sessionUsable || static_cast<const void*>(open.proj) != loaded) {
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) ++landedCount;
}
answers.push_back(Answer{
open.proj, key, wire::encodeBakeOutcome(outcome),
outcome.status == BakeStatus::Ok
? std::string{}
: "ReaSampler resample: " + outcome.message + ".\n"});
}
}
if (landedCount > 0) {
@@ -266,10 +324,19 @@ void RunResampleBake(ReaSamplerSession& session) {
Undo_EndBlock2(nullptr,
persisted ? "ReaSampler: resample bake into bank" : "",
persisted ? UNDO_STATE_MISCCFG : 0);
bankPanelRefresh();
} else {
Undo_EndBlock2(nullptr, "", 0); // nothing landed — record no empty undo point
}
// 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());
}
if (landedCount > 0) bankPanelRefresh();
}
} // namespace reasampler::capture