Ξ-W2-T1 re-review cleanup: soften ordering claim, bound bake guard fields, dedup gain/play-mode rationale, quiet foreign WrongProject noise
This commit is contained in:
@@ -21,10 +21,8 @@ decision about what the render made obsolete.
|
||||
loop runs to `BakePlan::renderFrames()` and stops. That is why a Gate bake with a sustain
|
||||
loop active terminates: the gate is released at `noteOffFrame` so the tail is real, but
|
||||
even a pathological envelope cannot run past the window.
|
||||
- **The whole signal chain is printed, master gain included.** `renderBake` scales its
|
||||
output by the dialed post-mixer gain, because `resetAfterBake` hands that control back at
|
||||
unity. A render that summed voices alone would return every iteration shifted by 1/gain,
|
||||
and a gain dialed to silence would come back at full level.
|
||||
- **The whole signal chain is printed, master gain included** — the gain multiply in
|
||||
`bake_render.cpp` carries the argument for why.
|
||||
- **A degenerate or unholdable window is refused, not rendered.** `planBake` returns nullopt
|
||||
for a collapsed window, a non-positive rate, a window that rounds to no frames, and one
|
||||
past `kMaxBakeFrames` — an unbounded window is a `bad_alloc` inside a UI tick, and the
|
||||
@@ -35,11 +33,8 @@ decision about what the render made obsolete.
|
||||
under-resetting applies the same processing twice while over-resetting costs a re-dial.
|
||||
A new mapping fact must be added to the copy list explicitly.
|
||||
- **Play mode resets to TRIGGER, not to the value struct's Gate default** — the one
|
||||
classification this track made against the ratified rule rather than reading off it. The
|
||||
bake's product is a finished one-shot, and Trigger is the mode that plays a finished
|
||||
one-shot verbatim; Gate would re-gate the printed release tail and each iteration would
|
||||
truncate the previous one's. "Neutral" here means "adds no processing", not "the struct's
|
||||
own default". `bake_reset.cpp` carries the argument at the assignment.
|
||||
classification this track made against the ratified rule rather than reading off it.
|
||||
`bake_reset.cpp` carries the argument at the assignment.
|
||||
|
||||
## Modules
|
||||
|
||||
|
||||
@@ -21,7 +21,14 @@ constexpr std::int64_t kBlockFrames = 512;
|
||||
BakeAudio renderBake(SampleData sample, const BakePlan& plan, double masterGainLinear) {
|
||||
BakeAudio out;
|
||||
if (!sample.playable() || plan.totalFrames <= 0 || plan.sampleRate <= 0) return out;
|
||||
if (plan.leadInFrames < 0 || plan.renderFrames() > kMaxBakeFrames) return out;
|
||||
// Each field bounded BEFORE the sum: renderFrames() adds them, and a hand-built plan
|
||||
// (planBake already bounds both — bake_plan.cpp) could otherwise carry leadInFrames
|
||||
// near INT64_MAX and signed-overflow inside the guard meant to catch exactly that.
|
||||
if (plan.leadInFrames < 0 || plan.leadInFrames > kMaxBakeFrames ||
|
||||
plan.totalFrames > kMaxBakeFrames) {
|
||||
return out;
|
||||
}
|
||||
if (plan.renderFrames() > kMaxBakeFrames) return out;
|
||||
|
||||
// The live block is the audio thread's moving target; a render that observed it would
|
||||
// depend on what the user happened to be dragging. The dialed values are already in
|
||||
@@ -67,8 +74,11 @@ BakeAudio renderBake(SampleData sample, const BakePlan& plan, double masterGainL
|
||||
const auto lead = static_cast<std::size_t>(plan.leadInFrames);
|
||||
const auto total = static_cast<std::size_t>(plan.totalFrames);
|
||||
out.interleaved.resize(total * static_cast<std::size_t>(channels));
|
||||
// A flat multiply, not the processor's per-sample ramp: the gain is constant for the
|
||||
// whole render, which is exactly what that ramp exists to converge to.
|
||||
// Printed here rather than left for the processor: resetAfterBake hands master gain
|
||||
// back to unity, so a render that only summed voices would return every iteration
|
||||
// shifted by 1/gain, and a gain dialed to silence would come back at full level. A flat
|
||||
// multiply, not the processor's per-sample ramp: the gain is constant for the whole
|
||||
// render, which is exactly what that ramp exists to converge to.
|
||||
const auto gain = static_cast<AudioSample>(masterGainLinear);
|
||||
for (std::size_t f = 0; f < total; ++f) {
|
||||
out.interleaved[f * channels] = left[lead + f] * gain;
|
||||
|
||||
@@ -30,9 +30,10 @@ struct BakeAudio {
|
||||
};
|
||||
|
||||
// Renders `plan` through `sample`'s own voice path, scaled by `masterGainLinear` — the
|
||||
// post-mixer gain the processor applies after the engine, printed here because the bake's
|
||||
// reset hands that control back at unity. The result is the plan's captured window: the
|
||||
// lead-in frames are rendered and dropped. An unplayable sample yields an empty result.
|
||||
// post-mixer gain the processor applies after the engine; see the gain multiply in
|
||||
// bake_render.cpp for why it is printed here rather than left to the processor. The result
|
||||
// is the plan's captured window: the lead-in frames are rendered and dropped. An unplayable
|
||||
// sample yields an empty result.
|
||||
BakeAudio renderBake(SampleData sample, const BakePlan& plan, double masterGainLinear);
|
||||
|
||||
} // namespace reasampler::instrument::bake
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
namespace reasampler::instrument::bake {
|
||||
|
||||
// The two surfaces a bake resets. Master gain lives on the processor rather than in the
|
||||
// parameter set, but renderBake prints it into the file, so it belongs to the same decision
|
||||
// and is answered here rather than left to the shell.
|
||||
// parameter set; it is answered here because renderBake prints it into the file (see
|
||||
// bake_render.cpp's gain multiply) rather than left to the shell.
|
||||
struct BakeReset {
|
||||
map::InstrumentParams params;
|
||||
double masterGainLinear = 1.0; // unity — renderBake printed the dialed gain
|
||||
|
||||
@@ -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". 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. 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).
|
||||
|
||||
@@ -308,11 +308,19 @@ void RunResampleBake(ReaSamplerSession& session) {
|
||||
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"});
|
||||
// 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"
|
||||
// (see the three-projects comment above) — only that tab's own refusal is fresh
|
||||
// feedback to a user who just clicked bake; every other one is a rescan repeat.
|
||||
const bool ownRequest = static_cast<const void*>(open.proj) == active;
|
||||
std::string console;
|
||||
if (outcome.status != BakeStatus::Ok &&
|
||||
(outcome.status != BakeStatus::WrongProject || ownRequest)) {
|
||||
console = "ReaSampler resample: " + outcome.message + ".\n";
|
||||
}
|
||||
answers.push_back(
|
||||
Answer{open.proj, key, wire::encodeBakeOutcome(outcome), console});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -298,9 +298,12 @@ 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_).
|
||||
// capture it belongs to become audible to process() within one block of each other.
|
||||
// live_.exchange above and the gain store below are two independent relaxed atomics
|
||||
// with no ordering between them, so the honest bound is "within one block", not "the
|
||||
// same instant" — a tail still ringing out of the drain does take the new gain, one
|
||||
// gain sitting above every snapshot, the same shape as ONE BLOCK, ONE RATE (see
|
||||
// builtSampleRate_).
|
||||
if (gainAtNextPublish_) {
|
||||
setMasterGainLinear(*gainAtNextPublish_);
|
||||
gainAtNextPublish_.reset();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
|
||||
using namespace reasampler;
|
||||
using namespace reasampler::instrument::bake;
|
||||
@@ -242,6 +243,13 @@ int main() {
|
||||
// able to walk the render into an allocation it cannot hold.
|
||||
CHECK(renderBake(s, planOf(kMaxBakeFrames, 0, 0, /*leadIn=*/1), kUnity).empty());
|
||||
CHECK(renderBake(s, planOf(1000, 0, 500, /*leadIn=*/-1), kUnity).empty());
|
||||
// A hand-built plan can carry a lead-in near the int64 ceiling; the guard must trip
|
||||
// on that field alone rather than signed-overflowing inside renderFrames()'s sum.
|
||||
CHECK(renderBake(s,
|
||||
planOf(1000, 0, 500,
|
||||
/*leadIn=*/std::numeric_limits<std::int64_t>::max() - 10),
|
||||
kUnity)
|
||||
.empty());
|
||||
}
|
||||
|
||||
if (g_fail == 0) std::printf("bake_render: all tests passed\n");
|
||||
|
||||
Reference in New Issue
Block a user