Cut shell/capture comment bloat ~33% (comments only, zero code change)

This commit is contained in:
2026-07-29 20:48:59 -04:00
parent 1f24c4b095
commit 54f5f24506
22 changed files with 699 additions and 1215 deletions
+76 -115
View File
@@ -1,10 +1,9 @@
// capture_batch.cpp — the M11 batch-capture family + the M10 re-capture-from-source
// action (Q-W3 hoist out of main.cpp; the code moved verbatim, the session threaded
// as a parameter). See the header.
// capture_batch.cpp — the batch-capture family + re-capture-from-source. See the
// header.
//
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
// Compiled into the reaper_reasampler module. Includes reaper_plugin_functions.h
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
// pointers; here they are extern (CLAUDE.md §contract).
// pointers; here they are extern.
#include "shell/capture/capture_batch.h"
@@ -50,28 +49,21 @@
namespace reasampler::capture {
// --- M11: batch capture (per selected item / per razor area) ----------------
// One action fires N captures — one sample per selected item (item scope) or per
// razor area (track scope, each area's own range). Each unit routes through
// captureAndIndexOne so every precision invariant holds; nothing lands in the
// arrange (load-bearing principle).
//
// One action fires N captures — one bank sample per selected item (item scope) or per
// razor area (track scope, each area's own range). Each individual capture honors every
// precision invariant via captureAndIndexOne (exact bounds, non-destructive FX/fader/pan
// neutralize, relative paths, channel preservation) and M10 provenance stamping applies
// per capture where its detection rule matches. The load-bearing principle holds: each
// unit writes a file + a bank index entry ONLY; nothing lands in the arrange.
//
// Per-unit FILE NAMING: each unit's baseName carries its ordinal ("item-1",
// "item-2", ...) so two units are never asked to write the same stem within one
// batch, and the shared makeUniqueTag now appends a per-session monotonic counter
// (T1-11 fix) so even same-second units across batches cannot collide.
// Each unit's baseName carries its ordinal ("item-1", "item-2", ...) so two units
// in one batch never share a stem, and makeUniqueTag's per-session monotonic
// counter keeps same-second units across batches from colliding too.
namespace {
// RAII snapshot/restore of the project's media-item selection. Batch item capture must
// transiently select exactly one item per render (RENDER_SETTINGS &32 renders whatever is
// selected); the user's ORIGINAL selection must be restored on EVERY exit path — including
// a mid-batch failure or early return — because selection restoration is part of the
// non-destructive invariant. Snapshot on construct (the currently-selected item set),
// restore on destruct (deselect everything, then re-select exactly the snapshot).
// RAII snapshot/restore of the item selection. Batch item capture must transiently
// select exactly one item per render (RENDER_SETTINGS &32 renders whatever is
// selected); the original selection is restored on every exit path — including a
// mid-batch failure — as part of the non-destructive invariant.
class ItemSelectionGuard
{
public:
@@ -85,9 +77,8 @@ public:
~ItemSelectionGuard()
{
// Deselect every item in the project, then re-select the snapshot — restoring the
// exact original set regardless of what the batch selected in between. Iterate ALL
// items (not just the currently-selected) so any transient selection is cleared.
// Deselect everything first (not just currently-selected) so any transient
// selection is cleared, then re-select exactly the snapshot.
const int total = CountMediaItems(nullptr);
for (int i = 0; i < total; ++i)
if (MediaItem* it = GetMediaItem(nullptr, i))
@@ -104,9 +95,8 @@ private:
std::vector<MediaItem*> selected_;
};
// Selects exactly `item` (deselect-all then select-one) so the offline render's
// selected-items bit (&32) captures a single item. Used inside the batch loop under the
// ItemSelectionGuard, which restores the user's original selection afterward.
// Deselect-all then select-one so the offline render's &32 bit captures exactly
// this item. Called inside ItemSelectionGuard, which restores the original selection.
void selectOnlyItem(MediaItem* item)
{
const int total = CountMediaItems(nullptr);
@@ -115,9 +105,8 @@ void selectOnlyItem(MediaItem* item)
SetMediaItemSelected(it, it == item);
}
// Collects every track's razor AUDIO areas as (owning track, range) pairs, preserving
// track order then area order — the batch analog of resolveRazorRange, which unions them.
// Read-only (never clears the razor selection). Reuses the pure parseRazorEdits parser.
// Collects every track's razor areas as (owning track, range) pairs, track order
// then area order. Read-only — never clears the razor selection.
std::vector<std::pair<MediaTrack*, RazorRange>> collectRazorAreas()
{
std::vector<std::pair<MediaTrack*, RazorRange>> areas;
@@ -135,10 +124,9 @@ std::vector<std::pair<MediaTrack*, RazorRange>> collectRazorAreas()
return areas;
}
// RAII snapshot/restore of the project's TRACK selection. Batch razor capture must
// transiently select exactly the area's owning track per render (track scope's &128 bit
// renders whatever TRACKS are selected); the user's original track selection is restored
// on EVERY exit path (part of the non-destructive invariant). Mirror of ItemSelectionGuard.
// Mirror of ItemSelectionGuard for track selection: batch razor capture transiently
// selects the area's owning track per render (&128 renders selected tracks), restoring
// the original selection on every exit path (non-destructive invariant).
class TrackSelectionGuard
{
public:
@@ -170,16 +158,13 @@ private:
} // namespace
// Batch item capture: one bank sample per SELECTED item, item scope. Snapshots the
// selection (RAII restore on every path), then for each selected item transiently selects
// only it, renders its exact [pos, pos+len] range under item-scope FX neutralize, adds the
// Sample, and records a per-unit verdict. Persists ONCE at the end (one ext-state write for
// the whole batch). Reports a mixed-result summary (explicit-action response — allowed).
// One sample per selected item. Snapshots the selection (RAII-restored), transiently
// selects each item in turn, renders its exact range under item-scope FX neutralize,
// and persists once at the end for the whole batch.
void RunBatchCaptureItems(ReaSamplerSession& session)
{
// Read the selected items up front (pointers stay valid — batch mutates only selection
// flags, never adds/removes items). Also capture each item's exact bounds and owning
// track NOW, while the full selection is live, before any transient re-selection.
// Read bounds + owning track now, while the full selection is live and before any
// transient re-selection (batch only mutates selection flags, never adds/removes items).
struct ItemUnit { MediaItem* item; MediaTrack* track; double start; double end; };
std::vector<ItemUnit> itemUnits;
{
@@ -201,8 +186,8 @@ void RunBatchCaptureItems(ReaSamplerSession& session)
return;
}
// Plan the exact source ranges -> validated, ordinal-assigned units (pure). Empty/
// inverted item ranges (a zero-length item) are dropped here so no stray render runs.
// Plan exact ranges -> validated, ordinal-assigned units; zero-length items are
// dropped here so no stray render runs.
std::vector<BatchRange> ranges;
ranges.reserve(itemUnits.size());
for (const ItemUnit& u : itemUnits)
@@ -212,19 +197,17 @@ void RunBatchCaptureItems(ReaSamplerSession& session)
BatchOutcome outcome;
bool anyAdded = false;
{
// Restore the user's ORIGINAL item selection on every exit path (incl. early
// return / mid-batch failure) — non-destructive invariant.
// selGuard restores the original item selection on every exit path.
ItemSelectionGuard selGuard;
// The plan and itemUnits are parallel over the KEPT units. Walk itemUnits, but only
// for those whose range survived planning (same drop rule), matching by ordinal.
// plan and itemUnits are parallel over kept units; skip dropped ranges in lockstep.
std::size_t planIdx = 0;
for (const ItemUnit& u : itemUnits)
{
if (!(u.end > u.start)) continue; // dropped by planCaptureUnits — skip in lockstep
const CaptureUnit& unit = plan[planIdx++];
// Transiently select ONLY this item so the item-scope render captures exactly it.
// select only this item so the item-scope render captures exactly it.
selectOnlyItem(u.item);
ResolvedSource src;
@@ -245,9 +228,9 @@ void RunBatchCaptureItems(ReaSamplerSession& session)
}
} // selGuard restores the original selection here, on every path
// Persist ONCE for the whole batch (one ext-state write) — only if something landed.
// S9: one bump for the whole batch (coalesced) — the counter is monotonic, not per-sample,
// so a single increment past the last-seen value is enough to trigger one instance reload.
// One ext-state write for the whole batch, only if something landed. The generation
// bump is monotonic, so one increment past the last-seen value triggers reload in
// any listening instance.
if (anyAdded) {
session.bumpBankGeneration();
session.saveToActiveProject();
@@ -256,12 +239,10 @@ void RunBatchCaptureItems(ReaSamplerSession& session)
ShowConsoleMsg((outcome.summaryLine("item") + "\n").c_str());
}
// Batch razor capture: one bank sample per razor AREA, track scope over that area's own
// range (the area's owning track is the source track). Track scope renders the selected
// TRACKS via master (&128), so each unit transiently selects ONLY its owning track
// (SetOnlyTrackSelected) under the TrackSelectionGuard, which restores the user's original
// track selection on every path. The razor selection itself is read-only and left intact.
// Persists ONCE at the end. Reports a mixed-result summary.
// One sample per razor area, track scope over that area's own range. Track scope
// renders selected tracks via master (&128), so each unit selects only its owning
// track under TrackSelectionGuard; the razor selection itself is read-only. Persists
// once at the end.
void RunBatchCaptureRazor(ReaSamplerSession& session)
{
const std::vector<std::pair<MediaTrack*, RazorRange>> areas = collectRazorAreas();
@@ -280,7 +261,7 @@ void RunBatchCaptureRazor(ReaSamplerSession& session)
BatchOutcome outcome;
bool anyAdded = false;
{
// Restore the user's ORIGINAL track selection on every exit path.
// selGuard restores the original track selection on every exit path.
TrackSelectionGuard selGuard;
std::size_t planIdx = 0;
@@ -290,8 +271,7 @@ void RunBatchCaptureRazor(ReaSamplerSession& session)
const CaptureUnit& unit = plan[planIdx++];
MediaTrack* tr = a.first;
// Transiently select ONLY this track so the track-scope render (&128) captures
// exactly it via master (over the custom time bounds we set per unit).
// select only this track so track-scope render (&128) captures it via master.
SetOnlyTrackSelected(tr);
ResolvedSource src;
@@ -312,7 +292,7 @@ void RunBatchCaptureRazor(ReaSamplerSession& session)
}
} // selGuard restores the original track selection here, on every path
// S9: one coalesced bump for the whole razor batch (see the item-batch note above).
// One coalesced generation bump for the whole batch (see the item-batch note above).
if (anyAdded) {
session.bumpBankGeneration();
session.saveToActiveProject();
@@ -321,25 +301,15 @@ void RunBatchCaptureRazor(ReaSamplerSession& session)
ShowConsoleMsg((outcome.summaryLine("razor area") + "\n").c_str());
}
// --- M10: re-capture from source --------------------------------------------
// Regenerates a provenanced sample's file from its recorded source's current state
// and updates the bank Sample in place. Bank-only — never calls InsertMedia; the
// user re-places manually if they want the new version on the timeline.
// Non-destructive to the source (FxBypassGuard snapshot/restore via renderOffline).
//
// Regenerates a PROVENANCED bank sample's file from its recorded source's CURRENT
// state, then updates the bank Sample IN PLACE. BANK-ONLY — it renders a file and
// refreshes the index entry; it NEVER calls InsertMedia / touches the timeline (the
// load-bearing capture-never-places line, structurally visible: this function has no
// insert path at all). Non-destructive to the source (FxBypassGuard snapshot/restore
// via renderOffline). Fork P2=a: refresh the bank entry only; the user re-places
// manually if they want the new version on the timeline.
//
// Failure modes are handled explicitly and reported to the user (a direct response
// to an explicit action is allowed by the console policy):
// * the selected sample has no provenance (not a resample) -> reported, no-op.
// * the recorded fingerprint is unparseable (legacy/corrupt) -> reported, no-op.
// * the recorded source track(s) no longer exist -> reported, no-op.
// * the render itself fails to satisfy the recorded request -> reported, no-op.
// On success, if the source FX chain drifted since capture (recorded vs current
// identity differ) the user is told — the re-capture still reflects the source AS IT
// IS NOW (P1=a: the fingerprint detects drift, it does not freeze the source).
// Failure modes are explicit and reported, each a no-op: no provenance, unparseable
// fingerprint, a missing recorded source track, or a failed render. On success, if
// the source FX chain drifted since capture, the user is told — the re-capture still
// reflects the source as it is now (drift is detected, not frozen against).
void RunRecaptureFromSource(ReaSamplerSession& session)
{
const std::vector<std::string> selected = bankPanelSelectedSampleIds();
@@ -371,8 +341,8 @@ void RunRecaptureFromSource(ReaSamplerSession& session)
return;
}
// Parse the recorded capture recipe from the fingerprint. A legacy / corrupt
// string fails gracefully — never a partial re-capture.
// Parse the recorded recipe; legacy/corrupt fingerprints fail gracefully, never
// a partial re-capture.
const std::string recordedParentId = orig->provenance->parentSampleId;
const std::string recordedFingerprint = orig->provenance->fxChainSnapshot;
const std::optional<model::CaptureRecipe> recipe =
@@ -384,8 +354,7 @@ void RunRecaptureFromSource(ReaSamplerSession& session)
return;
}
// Resolve the recorded source track GUID(s) to live tracks. Any missing track is a
// hard failure — we will not silently re-capture a different source.
// Missing recorded track = hard failure; never silently re-capture a different source.
std::vector<MediaTrack*> sourceTracks;
for (const std::string& g : recipe->trackGuids)
{
@@ -400,8 +369,7 @@ void RunRecaptureFromSource(ReaSamplerSession& session)
}
if (sourceTracks.empty())
{
// The recipe recorded no source tracks (e.g. an item-scope capture whose source
// tracks were not track-scoped). Without a resolvable source we cannot re-run.
// e.g. an item-scope capture with no track-scoped source — nothing to resolve.
ShowConsoleMsg("ReaSampler re-capture: no resolvable recorded source for this "
"sample; cannot re-capture from source.\n");
return;
@@ -411,10 +379,9 @@ void RunRecaptureFromSource(ReaSamplerSession& session)
recipe->scope == model::ProvenanceScope::Item ? CaptureScope::Item
: CaptureScope::Track;
// Rebuild the capture request verbatim from the recorded recipe — the SAME request,
// re-run against the source's CURRENT state (P1=a). Exact bounds, tail, rate,
// channels, bit depth all match the original so an unchanged source produces a
// byte-identical file (bit-identical-repeats invariant, consumed as a feature).
// Rebuild the request verbatim from the recorded recipe, re-run against the
// source's current state: an unchanged source reproduces a byte-identical file
// (bit-identical-repeats invariant).
CaptureRequest req;
req.sourceMode = static_cast<SourceMode>(recipe->sourceMode);
req.startSeconds = recipe->startSeconds;
@@ -428,10 +395,9 @@ void RunRecaptureFromSource(ReaSamplerSession& session)
req.baseName = orig->displayName.empty() ? "recapture" : orig->displayName;
req.trackGuids = recipe->trackGuids;
// Read the CURRENT source FX-chain identity BEFORE the render bypasses it, to
// compare against the recorded identity for drift reporting. Mirror the same
// scope split as buildCaptureProvenance: item scope reads take FX via TakeFX_*;
// track scope reads the track FX chain via TrackFX_*.
// Read the current FX-chain identity BEFORE the render bypasses it, for drift
// comparison against the recorded identity (item scope via TakeFX_*, track scope
// via TrackFX_*).
std::string currentIdentity;
if (scope == CaptureScope::Item) {
const int n = CountSelectedMediaItems(nullptr);
@@ -459,11 +425,10 @@ void RunRecaptureFromSource(ReaSamplerSession& session)
return;
}
// Update the Sample IN PLACE: keep its identity (id) and its provenance thread
// (same parent + a REFRESHED fingerprint reflecting the source as re-captured), but
// adopt the regenerated file's path / hash / length / rate / timestamp. The
// fingerprint is rebuilt from the recipe with the CURRENT FX identity so a
// subsequent re-capture measures drift from this point, not the original.
// Update in place: keep identity (id) + provenance parent, adopt the regenerated
// file's path/hash/length/rate/timestamp, and rebuild the fingerprint with the
// current FX identity so the next re-capture measures drift from here, not the
// original.
model::CaptureRecipe refreshed = *recipe;
refreshed.fxChainIdentity = currentIdentity;
@@ -476,33 +441,29 @@ void RunRecaptureFromSource(ReaSamplerSession& session)
updated.sampleRate = res.sample.sampleRate;
updated.lengthSeconds = res.sample.lengthSeconds;
updated.captureTempo = res.sample.captureTempo;
updated.captureTimeSigNum = res.sample.captureTimeSigNum; // L7 F1: refresh meter stamp
updated.captureTimeSigNum = res.sample.captureTimeSigNum; // refresh meter stamp
updated.captureTimeSigDenom = res.sample.captureTimeSigDenom; // to the re-capture's meter
updated.trackGuids = res.sample.trackGuids;
updated.createdTimestamp = res.sample.createdTimestamp;
// NOTE: levels, clipped, and lengthBeats are carried from the original (via the
// *orig copy above) because the offline backend does not populate them today
// (res.sample leaves them at defaults). If a later milestone populates these
// fields at capture time, refresh them here from res.sample instead.
// levels/clipped/lengthBeats carry from *orig — the offline backend doesn't
// populate them; refresh from res.sample here if that ever changes.
model::Provenance prov;
prov.parentSampleId = recordedParentId;
prov.fxChainSnapshot = model::buildFingerprint(refreshed);
updated.provenance = prov;
// Single batched undo point around the in-place bank mutation (mirrors the bank
// action family's R-B pattern). The mutation is index-only ext-state; the render
// wrote a new file but placed nothing on the timeline.
// One batched undo point around the in-place mutation; index-only ext-state,
// nothing placed on the timeline.
Undo_BeginBlock2(nullptr);
const bool changed = session.book().updateSampleInPlace(sampleId, updated);
if (changed)
{
// Record the regenerated file in the owned manifest (a new file the tool wrote);
// the superseded old file becomes an orphan reclaimed by Phase R prune.
// Record the regenerated file in the owned manifest; the superseded file
// becomes an orphan for prune to reclaim.
session.owned().add(updated.relativePath);
// S9: re-capture-in-place regenerates the SAME id's audio — the exact case the
// hands-free refresh exists for (an instance referencing this id keeps playing the
// OLD audio until it reloads). Bump inside the undo block so undo rolls back the
// generation with the rest of the blob.
// Regenerating the same id's audio is exactly why instances need the generation
// bump — they'd otherwise keep playing stale audio until reload. Bumped inside
// the undo block so undo rolls back the generation with the rest of the blob.
session.bumpBankGeneration();
const bool persisted = session.saveToActiveProject(); // book + manifest + generation + MarkProjectDirty
Undo_EndBlock2(nullptr, persisted ? "ReaSampler: re-capture from source" : "",