Prove the bake's answer writes by reading the key back
SetProjExtState's return covers the whole extname, so it never saw one key. The persist verdict now reaches the report, and a throw mid-write no longer claims the landing left nothing behind.
This commit is contained in:
@@ -57,7 +57,7 @@ detail not covered there:
|
||||
- `render_selection` (`shell/capture`) — the transient track selection a selected-tracks render (`&128`) requires, as a stack RAII guard: REAPER prints whatever tracks are selected, so `renderOffline` makes the request's own tracks BE the selection for the render's duration and restores the user's set on every exit path. Engaged ONLY for that source mode, which leaves a stated residual: a `&32` selected-items render still prints whatever ITEMS the user has selected. Live captures are unaffected (that selection is the source), but a recipe replay of a `SelectedItems` capture renders against whatever happens to be selected then — the recipe stores tracks and a range, never item GUIDs, so this guard cannot close it. Filed in `docs/TODO.md`.
|
||||
- `render_isolation` (`shell/capture`) — the transient upstream silencing a ranged ITEM render needs, as a stack RAII guard alongside the two above: the selected-tracks source prints everything flowing INTO the track, so each direct folder child's `B_MAINSEND` and each of the track's receives' `B_MUTE` are cut for the render and restored on every exit path. Direct children only — a grandchild reaches the track through the child that owns it. The child-set walk is pure (`core/capture/track_topology`).
|
||||
- `capture_orchestrator` (`shell/capture`) — single-capture orchestration + the realtime/insert action bodies (Q-W3 hoist, T4-02): `renderOffline` (one offline render under the scope's FX-bypass guard), `captureAndIndexOne` (render + provenance stamp + bank add + tracking-ledger record, unpersisted), `RunCapture`/`RunCaptureItemAssign`, `RunCaptureRealtimeTrack`/`RunCancelRealtime` (the realtime action bodies — the in-flight state lives in `realtime_lifecycle`), and `RunInsertSelected` (the ONE deliberate exception to capture-never-places).
|
||||
- `bake_land` (`shell/capture`) — the EXTENSION's half of the resample chain: scans every open project tab for pending `rsbake_*` requests, lands the ones belonging to the project this session has loaded, and refuses the rest with `WrongProject` — one undo point for the batch, each answered over its own key inside the invoking instance's synchronous action call. The per-key verdict itself is NOT this TU's: it is `core/wire`'s pure `classifyBakeScan`, so this shell only enumerates, reads, and applies — counting every verdict into a `wire::BakeScanTally` as it goes, printing `wire::describeBakeKey` for EVERY enumerated key (the only thing that names which key is whose) plus `wire::describeBakeScan` whenever any key went unanswered or any answer's write was rejected, in one `ShowConsoleMsg`. It checks `SetProjExtState`'s return on every non-empty answer — an answer that did not land is the one no-answer the tally alone cannot show. Each key is materialized before any answer is written, so no `SetProjExtState` in this action mutates a set the enumerator is still walking. The undo block is stack RAII (`UndoBlock`), and `landOne`'s whole-WAV read/hash runs under a catch that converts a throw into a refusal — neither an exception nor an early return can leave an undo block open or discard the buffered answers. It RENDERS NOTHING — the instrument already did, through its own engine in its own process, which is what makes the baked audio the sound the user approved and what keeps the voice engine out of the extension's link graph. Replace-vs-add comes from `tracking::resampleLanding`; a replace keeps the entry's id and slot and never deletes the superseded file. Hash-dedup applies on the add path only, before the disk write, matching `updateSampleInPlace`'s "an in-place refresh is not an insert". A refused index withdraws the bytes this call had just written — the self-cleanup carve-out from prune's deletion authority, stated in `prune_fs.cpp`'s header.
|
||||
- `bake_land` (`shell/capture`) — the EXTENSION's half of the resample chain: scans every open project tab for pending `rsbake_*` requests, lands the ones belonging to the project this session has loaded, and refuses the rest with `WrongProject` — one undo point for the batch, each answered over its own key inside the invoking instance's synchronous action call. The per-key verdict itself is NOT this TU's: it is `core/wire`'s pure `classifyBakeScan`, so this shell only enumerates, reads, and applies — counting every verdict into a `wire::BakeScanTally` as it goes, printing `wire::describeBakeKey` for EVERY enumerated key (the only thing that names which key is whose) plus `wire::describeBakeScan` whenever any key went unanswered or any answer's write was rejected, in one `ShowConsoleMsg`. It PROVES every write — answer or stale-clear — by reading the key back (`wire::bakeWriteLanded`), because `SetProjExtState`'s return describes the whole extname's state and cannot speak for one key; an answer that did not land is the one no-answer the tally alone cannot show. Each key is materialized before any answer is written, so no `SetProjExtState` in this action mutates a set the enumerator is still walking. Answers are held UNENCODED until after the pass's single persist, so a landing the project would not take is answered as a failure rather than as an `Ok` no reload would honour. The undo block is stack RAII (`UndoBlock`), and the landing is split into a non-mutating `prepareLanding` and a mutating `commitLanding` under separate catches — a throw before anything was written is a clean refusal, a throw after it is reported as possibly partial, and neither an exception nor an early return can leave an undo block open or discard the buffered answers. It RENDERS NOTHING — the instrument already did, through its own engine in its own process, which is what makes the baked audio the sound the user approved and what keeps the voice engine out of the extension's link graph. Replace-vs-add comes from `tracking::resampleLanding`; a replace keeps the entry's id and slot and never deletes the superseded file. Hash-dedup applies on the add path only, before the disk write, matching `updateSampleInPlace`'s "an in-place refresh is not an insert". A refused index withdraws the bytes this call had just written — the self-cleanup carve-out from prune's deletion authority, stated in `prune_fs.cpp`'s header.
|
||||
- `capture_batch` (`shell/capture`) — the batch-capture family + re-capture-from-source (Q-W3 hoist, T4-02): `RunBatchCaptureItems` (one sample per selected item), `RunBatchCaptureRazor` (one sample per razor area), `RunRecaptureFromSource` (regenerate a provenanced sample from its recorded source's current state, bank-only). Every unit routes through `capture_orchestrator` so every precision invariant holds; persist is batched to one ext-state write per action.
|
||||
- `realtime_lifecycle` (`shell/capture`) — the in-flight realtime-capture state machine + globals (Q-W3 hoist): the action starts it, `OnTimer` drives it per tick via `DriveRealtimeCapture` (a single-pointer-test idle fast path — load-bearing hot-path guardrail), `CommitRealtimeResult` lands a finished capture in the bank, `AbortRealtimeCaptureForUnload` tears down cleanly on extension unload.
|
||||
- `capture_realtime_shell` (`shell/capture`) — the async realtime-record backend surface (Q-W6 split of the former fat `capture.h`): `RealtimeRecordBackend::begin`/`tick`/`abort`, transport-driven across timer ticks (a realtime record cannot block REAPER's UI for its own duration). Deliberately shares NO interface with the offline backend — the lifecycles genuinely differ (the former `ICaptureBackend` interface was deleted in Q-W3, T4-26).
|
||||
|
||||
+221
-120
@@ -81,61 +81,82 @@ const Sample* findSourceSample(const BankBook& book, const std::string& sampleId
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Everything a landing DECIDES, before it touches disk or book: the staged WAV read and
|
||||
// hash, the source lookup, replace-vs-add, and the dedup short-circuit. Split from the
|
||||
// commit below so the two halves' throws can be told apart — a prepare that threw left the
|
||||
// project untouched, and only a commit that threw may have written something.
|
||||
struct PreparedLanding {
|
||||
std::optional<BakeOutcome> settled; // a refusal or a dedup hit: nothing left to commit
|
||||
BakeOutcome outcome; // the shape a successful commit completes
|
||||
Sample landed; // the entry the commit indexes
|
||||
std::vector<std::uint8_t> bytes; // the staged file, already read
|
||||
std::string bankId;
|
||||
std::string destDir;
|
||||
std::string destPath;
|
||||
bool replace = false;
|
||||
};
|
||||
|
||||
const std::vector<std::uint8_t> bytes = util::readFileBytes(request.stagedFilePath);
|
||||
if (bytes.empty())
|
||||
return refuse(BakeStatus::StagedMissing, "the staged render was unreadable",
|
||||
request.generation);
|
||||
const WavLayout layout = parseWavLayout(bytes);
|
||||
if (!layout.valid || layout.frameCount() == 0)
|
||||
return refuse(BakeStatus::StagedMissing, "the staged render is not a usable WAV",
|
||||
request.generation);
|
||||
PreparedLanding prepareLanding(ReaSamplerSession& session, const std::string& projectDir,
|
||||
const BakeRequest& request) {
|
||||
PreparedLanding prep;
|
||||
if (projectDir.empty()) {
|
||||
prep.settled = refuse(BakeStatus::NoProject,
|
||||
"no saved project, so the bank has no location",
|
||||
request.generation);
|
||||
return prep;
|
||||
}
|
||||
|
||||
prep.bytes = util::readFileBytes(request.stagedFilePath);
|
||||
if (prep.bytes.empty()) {
|
||||
prep.settled = refuse(BakeStatus::StagedMissing, "the staged render was unreadable",
|
||||
request.generation);
|
||||
return prep;
|
||||
}
|
||||
const WavLayout layout = parseWavLayout(prep.bytes);
|
||||
if (!layout.valid || layout.frameCount() == 0) {
|
||||
prep.settled = refuse(BakeStatus::StagedMissing,
|
||||
"the staged render is not a usable WAV", request.generation);
|
||||
return prep;
|
||||
}
|
||||
|
||||
BankBook& book = session.book();
|
||||
std::string bankId;
|
||||
const Sample* source = findSourceSample(book, request.sourceSampleId, bankId);
|
||||
if (!source)
|
||||
return refuse(BakeStatus::NoSource, "the resampled capture is not in any bank",
|
||||
request.generation);
|
||||
// Copied, not aliased: every mutation below invalidates the book's pointers.
|
||||
const Sample* source = findSourceSample(book, request.sourceSampleId, prep.bankId);
|
||||
if (!source) {
|
||||
prep.settled = refuse(BakeStatus::NoSource,
|
||||
"the resampled capture is not in any bank", request.generation);
|
||||
return prep;
|
||||
}
|
||||
// Copied, not aliased: every mutation the commit makes invalidates the book's pointers.
|
||||
const Sample sourceCopy = *source;
|
||||
|
||||
const tracking::Landing landing = tracking::resampleLanding(
|
||||
session.tiedUsageFor(request.sourceRelativePath, request.ownUsageKey));
|
||||
const bool replace = (landing == tracking::Landing::Replace);
|
||||
prep.replace = (landing == tracking::Landing::Replace);
|
||||
|
||||
// WAV-aware hash: the bank's dedup key, and — on the add path only — the reason a
|
||||
// byte-identical bake yields no second entry. Replace never dedups, matching
|
||||
// updateSampleInPlace's own contract: an in-place refresh is not an insert.
|
||||
const std::string contentHash = hashWavContent(bytes);
|
||||
const std::string contentHash = hashWavContent(prep.bytes);
|
||||
|
||||
BakeOutcome out;
|
||||
out.generation = request.generation;
|
||||
out.rootNote = request.rootNote;
|
||||
out.channelCount = static_cast<int>(layout.channelCount);
|
||||
out.replaced = replace;
|
||||
prep.outcome.generation = request.generation;
|
||||
prep.outcome.rootNote = request.rootNote;
|
||||
prep.outcome.channelCount = static_cast<int>(layout.channelCount);
|
||||
prep.outcome.replaced = prep.replace;
|
||||
|
||||
if (!replace && !contentHash.empty()) {
|
||||
if (const model::BankModel* index = book.index(bankId)) {
|
||||
if (!prep.replace && !contentHash.empty()) {
|
||||
if (const model::BankModel* index = book.index(prep.bankId)) {
|
||||
if (const Sample* existing = index->findByHash(contentHash)) {
|
||||
// Dedup BEFORE the disk write: this bake is bytes the bank already holds,
|
||||
// so it re-points at that entry rather than depositing an unreferenced
|
||||
// twin for the prune to reclaim later.
|
||||
BakeOutcome out = prep.outcome;
|
||||
out.status = BakeStatus::Ok;
|
||||
out.sampleId = existing->id;
|
||||
out.relativePath = existing->relativePath;
|
||||
out.displayName = existing->displayName;
|
||||
out.message = "identical to an existing capture -- pointed at it";
|
||||
return out;
|
||||
prep.settled = std::move(out);
|
||||
return prep;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,61 +172,128 @@ BakeOutcome landOne(ReaSamplerSession& session, const std::string& projectDir,
|
||||
const std::string stem =
|
||||
sourceCopy.displayName.empty() ? std::string("resample") : sourceCopy.displayName;
|
||||
const BankPaths paths = deriveBankPaths(projectDir, stem, uniqueTag);
|
||||
prep.destDir = paths.absoluteDir;
|
||||
prep.destPath = paths.absoluteDir + "/" + paths.fileName;
|
||||
|
||||
std::error_code ec;
|
||||
fs::create_directories(paths.absoluteDir, ec); // idempotent; the write reports failure
|
||||
const std::string destPath = paths.absoluteDir + "/" + paths.fileName;
|
||||
if (!writeFileBytes(destPath, bytes))
|
||||
return refuse(BakeStatus::Failed, "could not write the bake into the bank folder",
|
||||
request.generation);
|
||||
|
||||
Sample landed;
|
||||
// Replace keeps the entry's identity and its slot — the sound iterated, it did not
|
||||
// become a different capture. The superseded FILE is untouched: it stays on disk,
|
||||
// unreferenced, until a prune reclaims it, which is the iterate loop's recovery floor.
|
||||
landed.id = replace ? sourceCopy.id
|
||||
: ("bake-" + uniqueTag + "-" + paths.fileName);
|
||||
landed.displayName =
|
||||
replace ? sourceCopy.displayName : nextIterationName(sourceCopy.displayName);
|
||||
landed.relativePath = paths.relativePath; // project-relative (invariant)
|
||||
landed.channelCount = static_cast<int>(layout.channelCount);
|
||||
landed.sampleRate = static_cast<int>(layout.sampleRate);
|
||||
landed.lengthSeconds =
|
||||
prep.landed.id =
|
||||
prep.replace ? sourceCopy.id : ("bake-" + uniqueTag + "-" + paths.fileName);
|
||||
prep.landed.displayName =
|
||||
prep.replace ? sourceCopy.displayName : nextIterationName(sourceCopy.displayName);
|
||||
prep.landed.relativePath = paths.relativePath; // project-relative (invariant)
|
||||
prep.landed.channelCount = static_cast<int>(layout.channelCount);
|
||||
prep.landed.sampleRate = static_cast<int>(layout.sampleRate);
|
||||
prep.landed.lengthSeconds =
|
||||
layout.sampleRate ? static_cast<double>(layout.frameCount()) /
|
||||
static_cast<double>(layout.sampleRate)
|
||||
: 0.0;
|
||||
landed.rootNote = request.rootNote; // rendered AT root — that is what makes it survive
|
||||
landed.tier = model::Tier::Scratch;
|
||||
landed.contentHash = contentHash;
|
||||
landed.createdTimestamp = nowSec;
|
||||
// Rendered AT root — that is what makes it survive.
|
||||
prep.landed.rootNote = request.rootNote;
|
||||
prep.landed.tier = model::Tier::Scratch;
|
||||
prep.landed.contentHash = contentHash;
|
||||
prep.landed.createdTimestamp = nowSec;
|
||||
// The lineage seed recordCreated reads: the ledger's parent chain is what makes a
|
||||
// repeated bake readable as one iteration chain.
|
||||
landed.provenance = model::Provenance{sourceCopy.id, std::string{}};
|
||||
prep.landed.provenance = model::Provenance{sourceCopy.id, std::string{}};
|
||||
return prep;
|
||||
}
|
||||
|
||||
const bool indexed = replace
|
||||
? book.updateSampleInPlace(sourceCopy.id, landed)
|
||||
: (book.index(bankId) &&
|
||||
book.index(bankId)->add(landed) == AddResult::Added);
|
||||
// The mutating half. From its first successful write a file exists and the book can change,
|
||||
// so a throw out of THIS function is not invisible to the project — which is why its caller
|
||||
// reports it differently from a prepare that threw. Its own refusals withdraw the file they
|
||||
// wrote, best-effort. Mutates book + ledger without persisting; the caller persists once for
|
||||
// the batch.
|
||||
BakeOutcome commitLanding(ReaSamplerSession& session, PreparedLanding& prep) {
|
||||
std::error_code ec;
|
||||
fs::create_directories(prep.destDir, ec); // idempotent; the write reports failure
|
||||
if (!writeFileBytes(prep.destPath, prep.bytes)) {
|
||||
// A failed write can still have created the file before failing; withdraw it for
|
||||
// the same reason the refused index below does.
|
||||
fs::remove(prep.destPath, ec);
|
||||
return refuse(BakeStatus::Failed, "could not write the bake into the bank folder",
|
||||
prep.outcome.generation);
|
||||
}
|
||||
|
||||
BankBook& book = session.book();
|
||||
// On the replace path prepareLanding set `landed.id` to the source's own id, so this is
|
||||
// the entry being refreshed in place.
|
||||
const bool indexed =
|
||||
prep.replace ? book.updateSampleInPlace(prep.landed.id, prep.landed)
|
||||
: (book.index(prep.bankId) &&
|
||||
book.index(prep.bankId)->add(prep.landed) == AddResult::Added);
|
||||
if (!indexed) {
|
||||
// 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);
|
||||
fs::remove(prep.destPath, ec);
|
||||
return refuse(BakeStatus::IndexRejected,
|
||||
replace ? "the bank refused the replacement"
|
||||
: "the bank refused the new capture",
|
||||
request.generation);
|
||||
prep.replace ? "the bank refused the replacement"
|
||||
: "the bank refused the new capture",
|
||||
prep.outcome.generation);
|
||||
}
|
||||
session.recordCreated(landed, tracking::OriginKind::Capture);
|
||||
session.recordCreated(prep.landed, tracking::OriginKind::Capture);
|
||||
|
||||
BakeOutcome out = prep.outcome;
|
||||
out.status = BakeStatus::Ok;
|
||||
out.sampleId = landed.id;
|
||||
out.relativePath = landed.relativePath;
|
||||
out.displayName = landed.displayName;
|
||||
out.message = replace ? "replaced the bank entry" : "added as a distinct capture";
|
||||
out.sampleId = prep.landed.id;
|
||||
out.relativePath = prep.landed.relativePath;
|
||||
out.displayName = prep.landed.displayName;
|
||||
out.message = prep.replace ? "replaced the bank entry" : "added as a distinct capture";
|
||||
return out;
|
||||
}
|
||||
|
||||
// One request landed, with the two failure truths kept apart: a throw out of the prepare
|
||||
// half is a refusal that left the project untouched, while a throw out of the commit half
|
||||
// may have written a file and changed the book — calling that one "refused" would tell the
|
||||
// user nothing happened when something did.
|
||||
struct LandingAttempt {
|
||||
BakeOutcome outcome;
|
||||
bool partial = false; // the commit threw: a file and/or an entry may exist
|
||||
bool changedBook = false; // the book gained or refreshed an entry, unpersisted so far
|
||||
};
|
||||
|
||||
LandingAttempt attemptLanding(ReaSamplerSession& session, const std::string& projectDir,
|
||||
const BakeRequest& request) {
|
||||
LandingAttempt attempt;
|
||||
PreparedLanding prep;
|
||||
try {
|
||||
prep = prepareLanding(session, projectDir, request);
|
||||
} catch (const std::exception& e) {
|
||||
attempt.outcome =
|
||||
refuse(BakeStatus::Failed,
|
||||
std::string("the landing failed before it wrote anything: ") + e.what(),
|
||||
request.generation);
|
||||
return attempt;
|
||||
} catch (...) {
|
||||
attempt.outcome =
|
||||
refuse(BakeStatus::Failed,
|
||||
"the landing failed before it wrote anything, for an unknown reason",
|
||||
request.generation);
|
||||
return attempt;
|
||||
}
|
||||
if (prep.settled) {
|
||||
attempt.outcome = std::move(*prep.settled);
|
||||
return attempt; // a refusal, or a dedup hit that changed nothing
|
||||
}
|
||||
try {
|
||||
attempt.outcome = commitLanding(session, prep);
|
||||
attempt.changedBook = attempt.outcome.status == BakeStatus::Ok;
|
||||
} catch (const std::exception& e) {
|
||||
attempt.partial = true;
|
||||
attempt.outcome = refuse(BakeStatus::Failed,
|
||||
std::string("the landing failed while writing: ") + e.what(),
|
||||
request.generation);
|
||||
} catch (...) {
|
||||
attempt.partial = true;
|
||||
attempt.outcome = refuse(BakeStatus::Failed,
|
||||
"the landing failed while writing, for an unknown reason",
|
||||
request.generation);
|
||||
}
|
||||
return attempt;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -241,6 +329,15 @@ KeyRead readKey(ReaProject* proj, const std::string& key) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Writes one value under `key` and PROVES it by reading the key back. SetProjExtState's own
|
||||
// return cannot do that: it is the size of the whole extname's state, which `banks` alone
|
||||
// keeps non-zero in every case a bake can reach. One extra read per written key, on a cold
|
||||
// path. An empty `value` is a clear.
|
||||
bool writeKeyVerified(ReaProject* proj, const std::string& key, const std::string& value) {
|
||||
SetProjExtState(proj, kProjExtNamespace(), key.c_str(), value.c_str());
|
||||
return wire::bakeWriteLanded(value, readKey(proj, key).value);
|
||||
}
|
||||
|
||||
struct OpenProject {
|
||||
ReaProject* proj = nullptr;
|
||||
std::string dir; // the project's own directory; empty for a never-saved project
|
||||
@@ -260,18 +357,20 @@ std::vector<OpenProject> openProjects() {
|
||||
return out;
|
||||
}
|
||||
|
||||
// One scanned key: what the pass decided about it, and the answer to write back after the
|
||||
// undo block closes.
|
||||
// One scanned key: what the pass decided about it, and what to write back after the undo
|
||||
// block closes. The outcome is kept UNENCODED until then, because the pass's persist runs
|
||||
// after the whole scan and an Ok the project never took must not be sent as one.
|
||||
struct ScannedKey {
|
||||
enum class Write { None, Clear, Answer };
|
||||
|
||||
ReaProject* proj = nullptr;
|
||||
std::string key;
|
||||
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
|
||||
Write write = Write::None; // None = an Ignore verdict, nothing to write
|
||||
BakeOutcome outcome; // Write::Answer only
|
||||
wire::BakeKeyOutcome report;
|
||||
};
|
||||
|
||||
// Undo_BeginBlock2/EndBlock2 as stack RAII, the FxBypassGuard discipline: landOne reads a
|
||||
// Undo_BeginBlock2/EndBlock2 as stack RAII, the FxBypassGuard discipline: a landing 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.
|
||||
@@ -313,9 +412,10 @@ void RunResampleBake(ReaSamplerSession& session) {
|
||||
std::vector<ScannedKey> scanned;
|
||||
wire::BakeScanTally tally; // every verdict below is counted, skips included
|
||||
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
|
||||
bool persisted = false; // the pass's ONE persist actually happened
|
||||
// The scan allocates outside attemptLanding's own guards 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;
|
||||
@@ -358,49 +458,36 @@ void RunResampleBake(ReaSamplerSession& session) {
|
||||
}
|
||||
if (verdict == wire::BakeScanVerdict::ClearStale) {
|
||||
++tally.staleCleared;
|
||||
entry.writesAnswer = true; // empty answerWire = clear the key
|
||||
entry.write = ScannedKey::Write::Clear;
|
||||
scanned.push_back(std::move(entry));
|
||||
continue;
|
||||
}
|
||||
|
||||
BakeOutcome outcome;
|
||||
if (verdict == wire::BakeScanVerdict::RefuseWrongProject) {
|
||||
outcome =
|
||||
entry.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;
|
||||
LandingAttempt attempt = attemptLanding(session, open.dir, *request);
|
||||
if (attempt.outcome.status == BakeStatus::Ok) ++tally.landed;
|
||||
// Unpersisted rather than Banked for anything that changed the book: the
|
||||
// pass's persist has not run yet, and the write loop is where the
|
||||
// upgrade is earned. A dedup hit changed nothing, so its landing is not
|
||||
// this pass's to lose.
|
||||
if (attempt.partial)
|
||||
entry.report.landing = wire::BakeLanding::Partial;
|
||||
else if (attempt.outcome.status != BakeStatus::Ok)
|
||||
entry.report.landing = wire::BakeLanding::Refused;
|
||||
else
|
||||
entry.report.landing = attempt.changedBook
|
||||
? wire::BakeLanding::Unpersisted
|
||||
: wire::BakeLanding::Banked;
|
||||
entry.outcome = std::move(attempt.outcome);
|
||||
}
|
||||
++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);
|
||||
entry.write = ScannedKey::Write::Answer;
|
||||
scanned.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
@@ -409,14 +496,15 @@ void RunResampleBake(ReaSamplerSession& session) {
|
||||
// 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())
|
||||
persisted = session.saveToActiveProject();
|
||||
if (persisted)
|
||||
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";
|
||||
"The answers it had already prepared are written below; any key whose answer it "
|
||||
"had not prepared yet went unanswered and is not named at all.\n";
|
||||
}
|
||||
|
||||
// OUTSIDE the undo block on purpose: the answer is transient handshake state, and an
|
||||
@@ -424,20 +512,33 @@ void RunResampleBake(ReaSamplerSession& session) {
|
||||
// which the following bake would then re-land against a temp file that is long gone.
|
||||
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;
|
||||
if (entry.write != ScannedKey::Write::None) {
|
||||
std::string value; // empty = clear the key
|
||||
if (entry.write == ScannedKey::Write::Answer) {
|
||||
// Where a landing earns the word: the persist ran after the whole scan, so
|
||||
// until here nothing had observed whether the project took it. Without it
|
||||
// the entry lives in memory alone, which is a failed bake to anyone who
|
||||
// reloads — so it is answered as one rather than as an Ok.
|
||||
if (entry.report.landing == wire::BakeLanding::Unpersisted) {
|
||||
if (persisted)
|
||||
entry.report.landing = wire::BakeLanding::Banked;
|
||||
else
|
||||
entry.outcome = refuse(BakeStatus::Failed,
|
||||
"the bake reached the bank in memory, but "
|
||||
"this pass could not persist it, so the "
|
||||
"project's saved bank state does not carry it",
|
||||
entry.outcome.generation);
|
||||
}
|
||||
value = wire::encodeBakeOutcome(entry.outcome);
|
||||
entry.report.detail = entry.outcome.message;
|
||||
}
|
||||
entry.report.writeConfirmed = writeKeyVerified(entry.proj, entry.key, value);
|
||||
if (!entry.report.writeConfirmed && entry.write == ScannedKey::Write::Answer)
|
||||
++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;
|
||||
}
|
||||
console += wire::describeBakeScan(tally);
|
||||
if (!console.empty()) ShowConsoleMsg(console.c_str());
|
||||
|
||||
@@ -196,9 +196,9 @@ BakeChainResult runBake(ReaSamplerProcessor& processor) {
|
||||
|
||||
// What the key holds now is the only evidence this side gets, and each of the five
|
||||
// non-answers is a different thing to go fix — collapsing them into one sentence is
|
||||
// what made a stale install indistinguishable from a refusal. The three that are about
|
||||
// the KEY name it, because the landing prints one console line per key it scanned and
|
||||
// the key is what correlates the two in a multi-instance session.
|
||||
// what made a stale install indistinguishable from a refusal. All five name the KEY,
|
||||
// because the landing prints one console line per key it scanned and the key is what
|
||||
// correlates the two in a multi-instance session.
|
||||
const wire::BakeAnswer answer =
|
||||
wire::classifyBakeAnswer(bridge.readReasamplerExtState(key), request);
|
||||
switch (answer.kind) {
|
||||
@@ -210,9 +210,9 @@ BakeChainResult runBake(ReaSamplerProcessor& processor) {
|
||||
"landing action prints one REAPER console line per key it "
|
||||
"scanned; look for that key name there");
|
||||
case wire::BakeAnswerKind::Undecodable:
|
||||
return fail(
|
||||
"the extension answered in a format this plugin does not read -- the "
|
||||
"extension and ReaSampler 9000 are from different builds");
|
||||
return fail("the value under " + key +
|
||||
" is neither a request nor an answer this build can read -- the "
|
||||
"extension and ReaSampler 9000 may be from different builds");
|
||||
case wire::BakeAnswerKind::Cleared:
|
||||
return fail("the bake key " + key +
|
||||
" came back empty -- either the request was cleared before an "
|
||||
@@ -225,7 +225,8 @@ BakeChainResult runBake(ReaSamplerProcessor& processor) {
|
||||
"unexpected given the bake's single-threaded call flow; if this "
|
||||
"recurs, note the exact steps and file it");
|
||||
case wire::BakeAnswerKind::ForeignOutcome:
|
||||
return fail("the extension answered a different bake request");
|
||||
return fail("the answer under " + key +
|
||||
" is for a different bake request than this one");
|
||||
}
|
||||
// wire::answeredOutcome is the guard, not switch exhaustiveness alone: the switch
|
||||
// above has no `default`, so a future BakeAnswerKind enumerator it doesn't yet handle
|
||||
|
||||
Reference in New Issue
Block a user