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:
@@ -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, and printing `wire::describeBakeScan` when the pass answered nobody. Each key is materialized before any answer is written, so no `SetProjExtState` in this action mutates a set the enumerator is still walking. 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 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.
|
||||
- `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).
|
||||
|
||||
+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();
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ declared ahead of the instrument slots at that member in `reasampler_processor.h
|
||||
- `reasampler_editor` — VST3 `IPlugView` LICE editor shell: hosts a LICE-drawn child window; the Sample face is home and Browse is a modal picker over it. Split on the Sample face's BAND axis, mirroring the pure `sample_bands` allocator: `editor_session` (session/bridge state, caches, commit-and-reload), `editor_controls` (the ONE `faceLayout` band resolve every paint and hit-test path shares, the node-drag bounds, the value labels, and the per-instance controls the parameter set does not carry — the parameter-set binding itself is the pure `core/instrument/ui/deck_values` module this only adapts int ids onto), `editor_models` (the orthogonal half: which stored struct each transient editor selection names — the staged-envelope pack/unpack, the drawn contour, and the three velocity curves), then matching paint and input sets — `editor_paint`/`editor_input` (dispatch + drag router + hover dispatch), `_chrome`, `_waveform`, `_deck` — plus the two band-independent surfaces (`_browse` for the modal picker, `_curve` for the velocity-curve popup) and `editor_platform` (IPlugView/Win32 window plumbing). Shared internals in `editor_internal.h`, no TU of its own. Drop-onto-editor ingest is NOT shipped (deferred).
|
||||
- `reasampler_embed` — implements `IReaperUIEmbedInterface` so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout to `embed_strip`. A read-only readout: the loaded capture across the keyboard span with its root marked, plus the activity level. It takes no mouse input (there is nothing on the strip to select).
|
||||
- `editor_stroke` — the editor's LICE side of the analytic stroker: builds a coverage mask with the pure `core/ui/stroke_aa` and blends it into the bitmap ONCE, writing straight to the bitmap's bits (the arithmetic matches LICE's own mode-0 combine, so a stroke composites identically to every other kit draw). Every radial and spline stroke on the editor routes through `strokeArcAA` / `strokePolylineAA` / `strokeLineAA`. Holds the draw-thread-only scratch mask and arc point list — reuse, not a hidden dependency: threading a canvas through the eight paint sites would grow those signatures to carry an allocation detail. Deliberately does NOT touch `shell/panel/draw_kit`: the waveform stroke, the docked bank panel and the browse cards are out of this seam's blast radius.
|
||||
- `instrument_bake` — the instrument's half of the resample chain, on the UI thread: render the dialed sound through the pure `core/instrument/bake` modules at the instance's PERSISTED PREVIEW VELOCITY (the velocity the user has been auditioning at — three velocity curves are live, so it is a property of the sound and not a render detail), stage the WAV OUTSIDE the bank folder, publish one `rsbake_<guid>` request, invoke the extension's landing action SYNCHRONOUSLY, read the outcome back over the same key, then adopt + reset in one act. What that key holds afterwards is classified by `core/wire`'s pure `classifyBakeAnswer`, and each of its five non-answers gets its OWN sentence — a silent no-answer stays a failure, but the user is told whether the extension never ran the landing, answered a stale generation, answered in a wire this build cannot read, cleared the request, or refused it. Two stack-RAII guards mirror `FxBypassGuard`'s discipline: the staged file and the request key are both cleared on every exit path, so a failed bake leaves no temp, no bank entry and no parameter reset. `bakeAvailable` is the affordance's paint gate. A cloned `instanceGuid` (two instances sharing one `rsbake_` key) is NOT handled here — the residual is contained by pre-existing tracking machinery instead: `planUsagePublish`'s sticky `unioned` poison plus `tiedUsageExists` (`core/tracking/tracking_authority.cpp`) force a clone's bake to `AddDistinct` rather than silently replacing a sibling's entry.
|
||||
- `instrument_bake` — the instrument's half of the resample chain, on the UI thread: render the dialed sound through the pure `core/instrument/bake` modules at the instance's PERSISTED PREVIEW VELOCITY (the velocity the user has been auditioning at — three velocity curves are live, so it is a property of the sound and not a render detail), stage the WAV OUTSIDE the bank folder, publish one `rsbake_<guid>` request, invoke the extension's landing action SYNCHRONOUSLY, read the outcome back over the same key, then adopt + reset in one act. What that key holds afterwards is classified by `core/wire`'s pure `classifyBakeAnswer`, and each of its five non-answers gets its OWN sentence — a silent no-answer stays a failure, but the user is told whether nothing wrote over the key, a stale generation was answered, the answer came in a wire this build cannot read, the request was cleared, or it was refused. The three sentences that are ABOUT the key name it, because the extension prints one console line per key it scanned and the key is what correlates the two in a multi-instance session. None of them claims the landing never ran — nothing on this side can observe that. Two stack-RAII guards mirror `FxBypassGuard`'s discipline: the staged file and the request key are both cleared on every exit path, so a failed bake leaves no temp, no bank entry and no parameter reset. `bakeAvailable` is the affordance's paint gate. A cloned `instanceGuid` (two instances sharing one `rsbake_` key) is NOT handled here — the residual is contained by pre-existing tracking machinery instead: `planUsagePublish`'s sticky `unioned` poison plus `tiedUsageExists` (`core/tracking/tracking_authority.cpp`) force a clone's bake to `AddDistinct` rather than silently replacing a sibling's entry.
|
||||
- `vst_entry` — VST3 entry point: `GetPluginFactory` export, class registration, channel-forked class UIDs.
|
||||
- `editor_internal.h` — INTERNAL shared helpers for the `reasampler_editor` TU family, included only by the editor's own shell TUs (`editor_session` / `editor_controls` / `editor_paint_*` / `editor_input_*` / `editor_platform`), never a public seam: the `Rect`↔kit adapters, small draw primitives (knob face / title band), label helpers, and the velocity-curve box derivation — the helpers more than one band TU needs. The deck's control ids, group ids and group composition are the pure `deck_groups` module's, not this file's. The piano-strip and root-key draws live in `editor_paint_chrome`, their only consumer, not here.
|
||||
- `reasampler_vst.h` — shared identity constants for the ReaSampler VST3 instrument (Phase S): the plugin's class UID (the channel-selected `Steinberg::FUID`, built from the FOREVER-FROZEN macros in `core/wire/reasampler_uid.h`), vendor name/URL/email, so the processor, factory, and editor agree. A class UID is FOREVER-STABLE once shipped — minted once, never regenerated. *(Newly authored per this dispatch's brief — no existing root-CLAUDE.md bullet; verified by reading `src/shell/instrument/reasampler_vst.h` directly.)*
|
||||
|
||||
@@ -196,32 +196,34 @@ 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.
|
||||
// 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.
|
||||
const wire::BakeAnswer answer =
|
||||
wire::classifyBakeAnswer(bridge.readReasamplerExtState(key), request);
|
||||
switch (answer.kind) {
|
||||
case wire::BakeAnswerKind::Answered:
|
||||
break;
|
||||
case wire::BakeAnswerKind::Unanswered:
|
||||
return fail(
|
||||
"the ReaSampler extension did not answer -- the request key still holds "
|
||||
"this exact request, untouched. Check the REAPER console: if the landing "
|
||||
"action printed a scan report just now, it DID run and that line says what "
|
||||
"it saw; if the console is silent, the action never ran at all");
|
||||
return fail("the ReaSampler extension did not answer " + key +
|
||||
" -- that key still holds this exact request, untouched. The "
|
||||
"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");
|
||||
case wire::BakeAnswerKind::Cleared:
|
||||
return fail(
|
||||
"the bake key came back empty -- either the request was cleared before "
|
||||
"an answer was written, or this build could not read whatever was there. "
|
||||
"The landing action's console scan report, if one appeared, names which");
|
||||
return fail("the bake key " + key +
|
||||
" came back empty -- either the request was cleared before an "
|
||||
"answer was written, or this build could not read whatever was "
|
||||
"there. Look for that key name in the landing action's console "
|
||||
"lines");
|
||||
case wire::BakeAnswerKind::ForeignRequest:
|
||||
return fail(
|
||||
"the bake key held a different pending request instead of an answer -- "
|
||||
"unexpected given the bake's single-threaded call flow; if this recurs, "
|
||||
"note the exact steps and file it");
|
||||
return fail("the bake key " + key +
|
||||
" held a different pending request instead of an answer -- "
|
||||
"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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user