Ξ-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:
@@ -37,7 +37,7 @@ detail not covered there:
|
||||
- `capture` — two CONCRETE backends with deliberately different lifecycles (no shared interface — the former `ICaptureBackend` was deleted in Q-W3, T4-26: one deriver, zero polymorphic call sites): `OfflineRenderBackend` (deterministic default, synchronous) and `RealtimeRecordBackend` (async begin/tick/abort). Input: `CaptureRequest`. Output: finished file + populated `Sample` handed to `bank_model`.
|
||||
- `scope_resolve` (`shell/capture`) — scope/source resolution shared by every capture entry point (Q-W3 hoist out of `main.cpp`): razor-else-time range inference, selected-track/selected-item-owning-track collection with canonical GUIDs, and the M10 provenance-assembly inputs (read BEFORE the FX-bypass guard neutralizes the in-scope chain).
|
||||
- `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: lands every pending `rsbake_*` request in the active project and answers over the same key, inside the invoking instance's synchronous action call. 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".
|
||||
- `bake_land` (`shell/capture`) — the EXTENSION's half of the resample chain: lands every pending `rsbake_*` request in the active project and answers over the same key, inside the invoking instance's synchronous action call. 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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,10 +16,12 @@ class ReaSamplerSession;
|
||||
|
||||
namespace reasampler::capture {
|
||||
|
||||
// The bake action's body: lands every pending "rsbake_*" request in the active project,
|
||||
// one undo point for the batch. Normally there is exactly one — the instance that just
|
||||
// invoked us. Runs synchronously inside the invoking instance's Main_OnCommandEx call, so
|
||||
// the answer is available to it the moment this returns.
|
||||
// The bake action's body: scans EVERY open project tab for pending "rsbake_*" requests and
|
||||
// lands the ones belonging to the project this session has loaded, one undo point for the
|
||||
// batch. Normally there is exactly one — the instance that just invoked us. Runs
|
||||
// synchronously inside that instance's Main_OnCommandEx call, so the answer is available to
|
||||
// it the moment this returns. A request from another tab is refused, and one too old to
|
||||
// have a reader left is cleared rather than answered.
|
||||
void RunResampleBake(ReaSamplerSession& session);
|
||||
|
||||
} // namespace reasampler::capture
|
||||
|
||||
@@ -103,7 +103,7 @@ declared ahead of the instrument slots at that member in `reasampler_processor.h
|
||||
|
||||
## Modules
|
||||
|
||||
- `reaper_bridge` — READ-ONLY bank consumer: receives bank snapshots from the extension and exposes them as a read-only view. **Never writes to the extension's bank** — this is a load-bearing invariant; no mutation path exists in this module. It owns TWO prefix-guarded ext-state write entry points, `writeUsageExtState` (`rsusage_`) and `writeBakeExtState` (`rsbake_`), each refusing every other key; neither weakens the read-only-*bank* invariant, because neither payload is bank state and `banks`/`view`/`tail`/`assign` stay structurally unwritable. It also owns the bake crossing — `extensionActionAvailable` / `invokeExtensionAction` (`NamedCommandLookup` + `Main_OnCommandEx` against `getReaperParent(3)`, the instance's OWN project tab, never the focused one) and `projectTempoBpm`.
|
||||
- `reaper_bridge` — READ-ONLY bank consumer: receives bank snapshots from the extension and exposes them as a read-only view. **Never writes to the extension's bank** — this is a load-bearing invariant; no mutation path exists in this module. It owns TWO prefix-guarded ext-state write entry points, `writeUsageExtState` (`rsusage_`) and `writeBakeExtState` (`rsbake_`), each refusing every other key; neither weakens the read-only-*bank* invariant, because neither payload is bank state and `banks`/`view`/`tail`/`assign` stay structurally unwritable. It also owns the bake crossing — `extensionActionAvailable` / `invokeExtensionAction` (`NamedCommandLookup` + `Main_OnCommandEx` with `getReaperParent(3)`, the instance's OWN project tab, as `proj` — a request, not a DAW-verified guarantee; see the header) and `projectTempoBpm`.
|
||||
- `reasampler_processor` (`shell/instrument/`: `reasampler_processor.cpp` lifecycle + `process()`, `processor_state.cpp` component-state I/O + UI-thread parameter accessors, `processor_reload.cpp` the off-audio-thread `reloadInstrument`/publish family — Q-W2v, T4-12 split; `process()` and its per-block work stay ONE TU on purpose, no cross-TU call on the per-sample path) — VST3 `SingleComponentEffect` shell: declares event-input bus + **permanently stereo** output (GA fix: dynamic mono↔stereo bus renegotiation deleted; `ChannelMode` is now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread `reloadInstrument` + atomic pointer swap so `process()` does no allocation, no file I/O, no bridge calls. The instance state is `{loaded capture id, one InstrumentParams}`, and `reloadInstrument` resolves + decodes exactly that one capture into the `SampleData` the engine plays. **Self-contained playback (pS):** `ComponentState` v10 adds a `SampleRefs` table — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName); `reloadInstrument` decodes directly from `SampleRefs`, bank-free (plays with the extension absent). The bank/bridge is a browser source: loading a capture copies its reference in; the reopen-heal timer + poll-to-play apparatus are removed. `retireIdleDrain()` retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (`setVoiceCount`/`setVoiceMode`/`setMonoTrigger`) rebuild the engine from the already-decoded `SampleData` via the drain-slot swap — no bank re-read, no WAV re-decode, no audible cut to ringing tails. **FB1:** applies the post-mixer `masterGainLinear` (from `ComponentState` v8) as a per-sample ramp over the summed output — no zipper noise. **GA v9:** `channelModeExplicit_` flag persisted; `channelModeFor()` auto-defaults the mode from the loaded capture's channel count when the flag is not set. **pS:** `ComponentState` bumped v9→v10 (`SampleRefs` table); pre-v10 blobs lift to empty refs and re-save self-contained. **pS-usage:** publishes instance usage (held `SampleRefs` paths) to `rsusage_<instanceGuid>` at the tail of `reloadInstrument` (off audio thread) via `reaper_bridge::writeUsageExtState`; `ComponentState` bumped v10→**v11** (`instanceGuid` field); pre-v11 blobs mint guid on first publish.
|
||||
- `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).
|
||||
|
||||
@@ -121,14 +121,19 @@ BakeChainResult runBake(ReaSamplerProcessor& processor) {
|
||||
|
||||
const InstrumentParams dialed = processor.instrumentParams();
|
||||
const int rootNote = dialed.rootOverride ? *dialed.rootOverride : source->rootNote;
|
||||
const auto plan = planBake(resolveNote(defaultBakeProgram(), *tempo), sampleRate,
|
||||
rootNote);
|
||||
if (!plan) return fail("the programmed capture window is empty");
|
||||
|
||||
// The snapshot comes first: the default program's window is derived from the sound it
|
||||
// carries (a Gate release, a Trigger play span), not from a constant.
|
||||
std::optional<SampleData> snapshot = processor.bakeSnapshot();
|
||||
if (!snapshot) return fail("the loaded capture could not be decoded for the render");
|
||||
|
||||
const instrument::bake::BakeAudio audio = renderBake(std::move(*snapshot), *plan);
|
||||
const auto plan = planBake(
|
||||
resolveNote(defaultBakeProgram(*snapshot, sampleRate, *tempo), *tempo), sampleRate,
|
||||
rootNote);
|
||||
if (!plan) return fail("the programmed capture window is empty");
|
||||
|
||||
const instrument::bake::BakeAudio audio =
|
||||
renderBake(std::move(*snapshot), *plan, processor.masterGainLinear());
|
||||
if (audio.empty()) return fail("the offline pass produced no audio");
|
||||
|
||||
// buildFloat32Wav takes doubles and narrows; the narrowing back to float is the bank's
|
||||
|
||||
@@ -15,8 +15,9 @@ class ReaSamplerProcessor;
|
||||
class ReaperBridge;
|
||||
|
||||
// Whether a bake can run at all right now: a REAPER host, and the extension's bake action
|
||||
// registered. Cheap enough for the editor's sync tick, which is where the button's paint
|
||||
// state is decided — the control must never be enabled and then refuse.
|
||||
// registered. Runs one NamedCommandLookup and builds one std::string per call — cost
|
||||
// unmeasured. It is on the editor's sync tick because that is where the button's paint
|
||||
// state is decided, and the control must never be enabled and then refuse.
|
||||
bool bakeAvailable(ReaperBridge& bridge);
|
||||
|
||||
struct BakeChainResult {
|
||||
|
||||
@@ -235,10 +235,15 @@ void ReaSamplerProcessor::adoptBakedCapture(const SampleRefEntry& entry,
|
||||
}
|
||||
setSelectedSampleId(entry.sampleId);
|
||||
setInstrumentParams(reset);
|
||||
setMasterGainLinear(masterGainLinear);
|
||||
{
|
||||
// ARMED, not stored: publishBuiltLocked applies it inside the same locked section as
|
||||
// the pointer swap. Storing it here instead would put the reset gain under the OLD
|
||||
// capture for the whole bridge-read-plus-WAV-decode the reload below runs first.
|
||||
std::lock_guard<std::mutex> lock(reloadMutex_);
|
||||
gainAtNextPublish_ = masterGainLinear;
|
||||
}
|
||||
// ONE reload for the re-point and the reset together: it decodes the new file and
|
||||
// publishes the neutral parameters in the same swap, so no block is ever rendered with
|
||||
// one of the two applied and not the other.
|
||||
// publishes the neutral parameters in the same swap.
|
||||
reloadInstrument();
|
||||
}
|
||||
|
||||
@@ -292,6 +297,14 @@ void ReaSamplerProcessor::publishBuiltLocked(std::unique_ptr<LoadedInstrument> b
|
||||
}),
|
||||
graveyard_.end());
|
||||
LoadedInstrument* prev = live_.exchange(built.release());
|
||||
// A bake's reset gain lands here rather than at its call site, so the gain and the
|
||||
// capture it belongs to become audible to process() in the same instant. A tail still
|
||||
// ringing out of the drain does take the new gain — one gain sits above every snapshot,
|
||||
// the same shape as ONE BLOCK, ONE RATE (see builtSampleRate_).
|
||||
if (gainAtNextPublish_) {
|
||||
setMasterGainLinear(*gainAtNextPublish_);
|
||||
gainAtNextPublish_.reset();
|
||||
}
|
||||
LoadedInstrument* evicted = draining_.exchange(prev);
|
||||
if (evicted) graveyard_.push_back(std::unique_ptr<LoadedInstrument>(evicted));
|
||||
}
|
||||
|
||||
@@ -155,8 +155,9 @@ bool ReaperBridge::invokeExtensionAction(const std::string& commandName) {
|
||||
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.
|
||||
// getReaperParent(3), not the focused tab — the ask is that an instance in a background
|
||||
// tab bakes into ITS OWN project's bank (see the header on what that does and does not
|
||||
// guarantee).
|
||||
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.
|
||||
|
||||
@@ -71,11 +71,15 @@ public:
|
||||
// affordance: an unavailable action must read Disabled, never enabled-then-refusing.
|
||||
bool extensionActionAvailable(const std::string& commandName);
|
||||
|
||||
// Fires the action against THIS INSTANCE's own project tab (getReaperParent(3)), not
|
||||
// whichever tab is focused. Returns false when the action is unregistered — the
|
||||
// invocation itself reports nothing, so a caller learns the result from the state the
|
||||
// action wrote, never from here. Must NOT be called from a mouse handler: it runs the
|
||||
// extension's whole bake landing synchronously, and the action re-points this instance.
|
||||
// Fires the action with THIS INSTANCE's own project tab (getReaperParent(3)) as
|
||||
// Main_OnCommandEx's `proj`, rather than leaving it to whichever tab is focused. What
|
||||
// REAPER then makes current for the action's duration is NOT verified in the DAW, so
|
||||
// this is a request, not a guarantee — the extension side re-derives which project a
|
||||
// request came from and refuses one it cannot safely land. Returns false when the action
|
||||
// is unregistered; the invocation itself reports nothing, so a caller learns the result
|
||||
// from the state the action wrote, never from here. Must NOT be called from a mouse
|
||||
// handler: it runs the extension's whole bake landing synchronously, and the action
|
||||
// re-points this instance.
|
||||
bool invokeExtensionAction(const std::string& commandName);
|
||||
|
||||
// The effective project tempo (BPM, quarter notes per minute) at the edit cursor of
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -130,9 +131,9 @@ public:
|
||||
|
||||
// Adopt a landed bake in ONE act: the new capture becomes this instance's ref and
|
||||
// selection, the parameter set and master gain go neutral, and a single reload
|
||||
// publishes both together. The ordering is the whole point — a reset published ahead
|
||||
// of the re-point would put neutral parameters under the OLD capture for a block.
|
||||
// UI thread only.
|
||||
// publishes all three together. The ordering is the whole point — anything published
|
||||
// ahead of the re-point would apply neutral settings to the OLD capture, which is the
|
||||
// one thing they are meaningless against. UI thread only.
|
||||
void adoptBakedCapture(const SampleRefEntry& entry, const InstrumentParams& reset,
|
||||
double masterGainLinear);
|
||||
|
||||
@@ -304,6 +305,10 @@ private:
|
||||
std::atomic<std::uint64_t> drainIdleGeneration_{0};
|
||||
std::vector<std::unique_ptr<LoadedInstrument>> graveyard_; // drained on reclaim + setActive(false) + terminate
|
||||
std::mutex reloadMutex_; // serializes off-thread reloads + graveyard access
|
||||
// A master gain that must reach the audio thread in the SAME swap as the next publish —
|
||||
// the bake's reset, which the old capture would be the wrong thing to apply it to.
|
||||
// Guarded by reloadMutex_, consumed by publishBuiltLocked.
|
||||
std::optional<double> gainAtNextPublish_;
|
||||
|
||||
// The loaded capture's id ("" = no pick -> silence). Off-thread only, not read on the
|
||||
// audio thread.
|
||||
|
||||
@@ -14,7 +14,9 @@ REAPER/filesystem-facing half only, and it gathers rather than decides.
|
||||
|
||||
- **Prune is the single, exclusive file-deletion authority.** No bank op, no
|
||||
capture op, no Design View op deletes a file; if any path other than prune
|
||||
deletes a bank file, reject it in review.
|
||||
deletes a bank file, reject it in review. The ONE carve-out — a shell removing
|
||||
a file it wrote itself moments earlier that no index ever referenced, wherever
|
||||
it sits — is stated in full at `prune_fs.cpp`'s header and nowhere else.
|
||||
- **Dry-run first, always; no silent deletion.** Prune reports before it deletes
|
||||
(orphan count, reclaimed size, and — for a small set — the files); actual
|
||||
deletion is a confirmed second step. No periodic/background sweep.
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
//
|
||||
// deleteOrphanFile below (SHFileOperationW on Windows, std::filesystem::remove
|
||||
// on SWELL platforms) is the ONLY code in the system that deletes USER files —
|
||||
// the sole deletion authority over the bank folder's bytes (a shell removing a
|
||||
// transient scratch file it just created, e.g. the drop path's temp
|
||||
// .vstpreset, is self-cleanup, not authority over user data). Deliberately
|
||||
// file-local (anonymous namespace): nothing outside this TU can reach it, and
|
||||
// this concentration must never spread. The safety-critical "which files are
|
||||
// orphans" decision stays in the pure core (prune_reconcile); this TU only
|
||||
// enumerates, resolves, stats, and — after the confirm — executes.
|
||||
// the sole deletion authority over the bank folder's bytes. THE carve-out, and
|
||||
// its one home: a shell removing a file it wrote itself moments earlier and
|
||||
// that no index ever referenced is self-cleanup, not authority over user data.
|
||||
// It covers a transient scratch file outside the bank (the drop path's temp
|
||||
// .vstpreset) AND a bank-folder write whose index entry was then refused
|
||||
// (bake_land) — the discriminator is "did this call create it, and did anything
|
||||
// ever reference it", not where it sits.
|
||||
//
|
||||
// deleteOrphanFile is deliberately file-local (anonymous namespace): nothing
|
||||
// outside this TU can reach it, and this concentration must never spread. The
|
||||
// safety-critical "which files are orphans" decision stays in the pure core
|
||||
// (prune_reconcile); this TU only enumerates, resolves, stats, and — after the
|
||||
// confirm — executes.
|
||||
//
|
||||
// Compiled into the reaper_reasampler module. REAPER-facing only through the
|
||||
// persist_detail helpers and usage_scan; this TU itself calls no REAPER API directly.
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
// JSON bridge, GUID minting, bank-folder relocation (see ext_state_io.h).
|
||||
// * prune_fs.cpp — pruneDryRun/pruneOrphanSet/pruneReclaim: the prune
|
||||
// scan and THE SINGLE FILE-DELETION AUTHORITY over user files in the bank
|
||||
// folder. Nothing else in the system deletes bank-folder bytes (a shell's
|
||||
// self-cleanup of its own transient scratch file is not this authority).
|
||||
// folder, plus the one self-cleanup carve-out from it, both stated there.
|
||||
//
|
||||
// Save: BankModel JSON -> SetProjExtState under namespace "reasampler" (ext
|
||||
// state lives inside the .rpp, so the index travels with the project for
|
||||
@@ -64,6 +63,13 @@ public:
|
||||
model::BankModel& bank() { return book_.activeIndex(); }
|
||||
const model::BankModel& bank() const { return book_.activeIndex(); }
|
||||
|
||||
// The project whose book/view/tail/ledger poll() last loaded — compare-only, never
|
||||
// dereferenced; nullptr before the first poll. An action that can be fired against a
|
||||
// project other than the one currently loaded here (the VST bake, which targets its own
|
||||
// instance's tab) MUST check this before mutating: the book in memory belongs to one
|
||||
// project, and landing another tab's request would write its capture into this bank.
|
||||
const void* loadedProject() const { return lastProject_; }
|
||||
|
||||
// Design-View model; persists MODEL STATE only (visibility on open is the view shell's job).
|
||||
ViewModeModel& view() { return view_; }
|
||||
const ViewModeModel& view() const { return view_; }
|
||||
|
||||
Reference in New Issue
Block a user