Cut shell/capture comment bloat ~33% (comments only, zero code change)
This commit is contained in:
@@ -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" : "",
|
||||
|
||||
Reference in New Issue
Block a user