feat(capture): M11 batch capture - one bank sample per selected item / razor area
Pure batch_capture module (plan + mixed-result aggregation, unit-tested); CAPTURE_BATCH_ITEMS/RAZOR actions with transient per-unit selection restored on all paths, per-unit invariants + provenance, single persist. Conform-on-insert already shipped.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// batch_capture.cpp — pure logic for M11 batch capture. See header.
|
||||
// NO REAPER types; unit-tested by tests/test_batch_capture.cpp.
|
||||
|
||||
#include "batch_capture.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
std::vector<CaptureUnit> planCaptureUnits(const std::vector<BatchRange>& ranges) {
|
||||
std::vector<CaptureUnit> units;
|
||||
units.reserve(ranges.size());
|
||||
int ordinal = 0;
|
||||
for (const BatchRange& r : ranges) {
|
||||
// Drop empty/inverted ranges — the offline backend refuses end<=start too, so
|
||||
// planning one would only manufacture a guaranteed per-unit failure. Ordinals
|
||||
// count kept units so the reported numbering is contiguous.
|
||||
if (!(r.endSeconds > r.startSeconds)) continue;
|
||||
++ordinal;
|
||||
units.push_back({ordinal, r.startSeconds, r.endSeconds});
|
||||
}
|
||||
return units;
|
||||
}
|
||||
|
||||
void BatchOutcome::record(int ordinal, bool ok, std::string detail) {
|
||||
results_.push_back({ordinal, ok, std::move(detail)});
|
||||
}
|
||||
|
||||
std::size_t BatchOutcome::succeeded() const {
|
||||
return static_cast<std::size_t>(
|
||||
std::count_if(results_.begin(), results_.end(),
|
||||
[](const BatchUnitResult& r) { return r.ok; }));
|
||||
}
|
||||
|
||||
std::size_t BatchOutcome::failed() const {
|
||||
return results_.size() - succeeded();
|
||||
}
|
||||
|
||||
std::vector<BatchUnitResult> BatchOutcome::failures() const {
|
||||
std::vector<BatchUnitResult> out;
|
||||
for (const BatchUnitResult& r : results_)
|
||||
if (!r.ok) out.push_back(r);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string BatchOutcome::summaryLine(const std::string& noun) const {
|
||||
const std::size_t n = total();
|
||||
const std::size_t ok = succeeded();
|
||||
|
||||
if (n == 0)
|
||||
return "ReaSampler batch capture: nothing to capture.";
|
||||
|
||||
const std::string plural = (n == 1) ? noun : noun + "s";
|
||||
|
||||
if (ok == n)
|
||||
return "ReaSampler batch capture: " + std::to_string(ok) + " " +
|
||||
plural + " captured.";
|
||||
|
||||
// Mixed / all-failed: report the ratio and enumerate the failed ordinals so the
|
||||
// user knows exactly which units to retry. No partial corruption is implied —
|
||||
// each captured unit is a complete, independent bank sample.
|
||||
std::string line = "ReaSampler batch capture: " + std::to_string(ok) + " of " +
|
||||
std::to_string(n) + " " + plural + " captured (" +
|
||||
std::to_string(n - ok) + " failed: ";
|
||||
bool first = true;
|
||||
for (const BatchUnitResult& f : results_) {
|
||||
if (f.ok) continue;
|
||||
if (!first) line += ", ";
|
||||
line += "#" + std::to_string(f.ordinal);
|
||||
first = false;
|
||||
}
|
||||
line += ").";
|
||||
return line;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
// batch_capture — the REAPER-free logic behind M11 batch capture (one action fires
|
||||
// N captures: one bank sample per selected item / per razor area).
|
||||
//
|
||||
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
|
||||
// vendor/ includes. Standard library only. The batch shell (main.cpp) reads the DAW
|
||||
// state (selected items -> their exact bounds; every track's P_RAZOREDITS -> areas)
|
||||
// and hands the raw ranges here so the genuinely-pure, easy-to-get-wrong pieces are
|
||||
// unit-tested outside the DAW:
|
||||
//
|
||||
// 1. planCaptureUnits: an ordered list of (start,end) source ranges -> an ordered
|
||||
// list of CaptureUnit, each carrying its 1-based ordinal and validated bounds.
|
||||
// Empty/inverted ranges are DROPPED (mirrors the offline backend's own
|
||||
// end>start guard) so a zero-length item/area never produces a stray render.
|
||||
// Order is preserved: unit ordinals count only the KEPT units, so a batch of
|
||||
// three valid items yields ordinals 1,2,3 regardless of dropped neighbors.
|
||||
// 2. BatchOutcome: order-preserving aggregation of per-unit results into a summary
|
||||
// (succeeded / failed counts + the ordered list of failures) so the shell can
|
||||
// report a mixed result with one console line and no partial-corruption
|
||||
// ambiguity. The AGGREGATION is pure; the render loop that feeds it is shell.
|
||||
//
|
||||
// Range is the ONLY thing that varies per unit here. FX scope (item vs track) is a
|
||||
// per-ACTION constant the shell already owns (fxBypassPlanFor); it is not a
|
||||
// per-unit field. Item-batch uses item scope; razor-batch uses track scope — the
|
||||
// shell passes the scope straight through to each render, unchanged from the
|
||||
// single-capture path.
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// One capture in a batch: an exact source range plus its 1-based ordinal within the
|
||||
// KEPT set. The ordinal disambiguates per-unit file stems (the offline backend's
|
||||
// unique tag is 1-second-granular, so a fast batch could otherwise collide N files
|
||||
// onto one name) and labels a failure in the summary.
|
||||
struct CaptureUnit {
|
||||
int ordinal = 0; // 1-based, counts kept units only
|
||||
double startSeconds = 0.0; // exact — no rounding
|
||||
double endSeconds = 0.0;
|
||||
};
|
||||
|
||||
// A source range handed in by the shell (a selected item's [pos, pos+len] or one
|
||||
// razor area's [start, end]). Kept as a distinct type from CaptureUnit so the input
|
||||
// (raw, possibly-invalid) and the output (validated, ordinal-assigned) do not share
|
||||
// a shape by accident. Named BatchRange (not SourceRange) to avoid collision with
|
||||
// bank_model's SourceRange, which carries PPQ fields this planner does not need.
|
||||
struct BatchRange {
|
||||
double startSeconds = 0.0;
|
||||
double endSeconds = 0.0;
|
||||
};
|
||||
|
||||
// Validates + orders a batch's source ranges into capture units. Preserves input
|
||||
// order; DROPS every range with end <= start (empty/inverted) so no stray render is
|
||||
// planned; assigns 1-based ordinals over the KEPT units. An empty input (no selected
|
||||
// item / no razor area) yields an empty plan — the shell reports "nothing to batch"
|
||||
// and writes nothing (the same no-op posture the single-capture path takes).
|
||||
std::vector<CaptureUnit> planCaptureUnits(const std::vector<BatchRange>& ranges);
|
||||
|
||||
// The per-unit verdict the shell records after each render attempt, in unit order.
|
||||
struct BatchUnitResult {
|
||||
int ordinal = 0; // the CaptureUnit's ordinal this result is for
|
||||
bool ok = false; // true iff the render + bank-add succeeded
|
||||
std::string detail; // failure reason (empty on success) — for the summary
|
||||
};
|
||||
|
||||
// Order-preserving aggregation of a batch's per-unit results. Built incrementally by
|
||||
// the shell (record() after each unit) so a mid-batch failure is captured without
|
||||
// aborting the remaining units (no partial corruption: each unit is independent, and
|
||||
// the selection is restored on every exit path by the shell's RAII guard).
|
||||
class BatchOutcome {
|
||||
public:
|
||||
// Records one unit's verdict. Order of calls IS the reported order.
|
||||
void record(int ordinal, bool ok, std::string detail = {});
|
||||
|
||||
std::size_t total() const { return results_.size(); }
|
||||
std::size_t succeeded() const;
|
||||
std::size_t failed() const;
|
||||
|
||||
const std::vector<BatchUnitResult>& results() const { return results_; }
|
||||
|
||||
// The ordered subset of results that failed (ok == false). For the summary line.
|
||||
std::vector<BatchUnitResult> failures() const;
|
||||
|
||||
// A single human summary line for the console (explicit-action response — allowed
|
||||
// by the console policy; a batch-completion summary with failure counts qualifies,
|
||||
// per-unit success spam does not). `noun` is the unit word ("item" / "razor area").
|
||||
// Examples:
|
||||
// all-success, 3 items : "ReaSampler batch capture: 3 items captured."
|
||||
// partial, 3 of 5 : "ReaSampler batch capture: 3 of 5 items captured "
|
||||
// "(2 failed: #2, #4)."
|
||||
// empty plan : "ReaSampler batch capture: nothing to capture."
|
||||
std::string summaryLine(const std::string& noun) const;
|
||||
|
||||
private:
|
||||
std::vector<BatchUnitResult> results_;
|
||||
};
|
||||
|
||||
} // namespace reasampler
|
||||
+385
-49
@@ -18,9 +18,11 @@
|
||||
#include "reaper_plugin.h"
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <vector>
|
||||
|
||||
@@ -28,6 +30,7 @@
|
||||
#include "app_version.h"
|
||||
#include "bank_model.h"
|
||||
#include "bank_panel.h"
|
||||
#include "batch_capture.h"
|
||||
#include "capture.h"
|
||||
#include "insert.h"
|
||||
#include "persist.h"
|
||||
@@ -124,6 +127,14 @@ static int g_cmdToggleBankPanel = 0;
|
||||
static int g_cmdInsertSelected = 0;
|
||||
static int g_cmdInsertSelectedConform = 0;
|
||||
|
||||
// Command ids for the M11 batch-capture actions. NEW FOREVER-STABLE strings. One action
|
||||
// fires N captures: CAPTURE_BATCH_ITEMS -> one bank sample per selected item (item scope);
|
||||
// CAPTURE_BATCH_RAZOR -> one bank sample per razor area (track scope, each area's range).
|
||||
// Each unit honors every precision invariant; the original selection is restored on every
|
||||
// exit path. Bank-only, never places on the timeline (load-bearing principle).
|
||||
static int g_cmdCaptureBatchItems = 0;
|
||||
static int g_cmdCaptureBatchRazor = 0;
|
||||
|
||||
// Command id for the "capture selected track (realtime)" action. NEW FOREVER-STABLE
|
||||
// string. Records the selected track's OWN output in realtime (transport-driven) into
|
||||
// a hidden temp track via RealtimeRecordBackend, then moves the recorded file into the
|
||||
@@ -714,13 +725,75 @@ static reasampler::CaptureResult renderOffline(
|
||||
return backend.capture(req);
|
||||
}
|
||||
|
||||
// Runs one capture-action-table row: resolve its scope source + range, snapshot &
|
||||
// clear the out-of-scope FX AND neutralize their fader gain + pan chain (RAII),
|
||||
// render via the offline backend, add the Sample to the bank, persist + mark dirty.
|
||||
// The load-bearing principle holds structurally — this path writes a file + a bank
|
||||
// index entry ONLY; it never calls InsertMedia or touches the arrange/timeline.
|
||||
// Non-destructive: FX-enable + fader gain + pan/width/law/mode are fully restored
|
||||
// on every path by FxBypassGuard, and the backend restores every RENDER_* setting.
|
||||
// Renders ONE capture request under the scope's FX-bypass guard, stamps provenance,
|
||||
// and adds the resulting Sample to the ACTIVE bank + records the created file in the
|
||||
// owned-file manifest — WITHOUT persisting. The caller persists once (single-capture:
|
||||
// right after; batch: once at the end) so a batch does not write ext state N times.
|
||||
//
|
||||
// Provenance is read from the LIVE selection here, so a batch that transiently
|
||||
// selects exactly one item per unit gets per-unit-correct provenance. `src` supplies
|
||||
// the source tracks (FX bypass + Sample GUIDs); `scope` drives the bypass plan and
|
||||
// provenance scope. Returns the backend's CaptureResult (status + message) so the
|
||||
// caller can report success/failure. Load-bearing principle holds: writes a file +
|
||||
// a bank index entry ONLY; never touches the arrange/timeline. Non-destructive: the
|
||||
// out-of-scope FX/fader/pan chain is fully restored on every path (FxBypassGuard),
|
||||
// and the backend restores every RENDER_* setting.
|
||||
static reasampler::CaptureResult captureAndIndexOne(
|
||||
reasampler::CaptureScope scope,
|
||||
const ResolvedSource& src,
|
||||
const std::string& baseName,
|
||||
double startSeconds,
|
||||
double endSeconds)
|
||||
{
|
||||
// The tail mode is a PANEL SETTING (docked bank panel's toggle), not a per-action
|
||||
// variant: the capture actions apply whatever the panel is set to. Default is None
|
||||
// (exact bounds / byte-identical to today) until the user opts in via the toggle.
|
||||
const reasampler::TailSetting tail = reasampler::bankPanelTailSetting();
|
||||
|
||||
reasampler::CaptureRequest req;
|
||||
req.sourceMode = reasampler::sourceModeForScope(scope);
|
||||
req.startSeconds = startSeconds; // exact bounds — no rounding
|
||||
req.endSeconds = endSeconds;
|
||||
req.wetDry = 1.0; // wet post the FX left enabled by the scope
|
||||
req.tailMode = tail.mode; // None / Auto / Manual, from the panel toggle
|
||||
req.tailMs = tail.manualMs; // Manual-only (clamped); ignored for None/Auto
|
||||
req.sampleRate = 0; // follow project rate
|
||||
req.channelCount = 2;
|
||||
req.bitDepth = reasampler::WavBitDepth::Float32; // deterministic, no dither
|
||||
req.baseName = baseName;
|
||||
req.trackGuids = src.trackGuids; // recorded on the Sample (provenance)
|
||||
|
||||
// M10: compute provenance BEFORE the FxBypassGuard neutralizes the in-scope chain —
|
||||
// the source FX-chain identity must be read from the LIVE (un-bypassed) chain, and
|
||||
// the source selection is still live here. Returns nullopt unless this capture
|
||||
// genuinely resamples from a bank sample (detectParent). Read-only.
|
||||
const std::optional<reasampler::Provenance> prov =
|
||||
buildCaptureProvenance(req, scope, src);
|
||||
|
||||
// Render under the scope's FX-bypass guard (out-of-scope FX / fader / pan chain
|
||||
// neutralized for the render, fully restored on every path). Writes a file only.
|
||||
reasampler::CaptureResult res = renderOffline(scope, src.sourceTracks, req);
|
||||
if (res.status != reasampler::CaptureStatus::Ok)
|
||||
return res;
|
||||
|
||||
// Stamp provenance onto the captured Sample (only set when this was a genuine
|
||||
// resample-from-sample; otherwise the optional stays empty, per M1's contract).
|
||||
res.sample.provenance = prov;
|
||||
|
||||
// Add to the ACTIVE bank: g_session.bank() resolves to book.activeIndex() (B2).
|
||||
g_session.bank().add(res.sample);
|
||||
// B-cap: record the created file in the owned-file manifest, at the same point the
|
||||
// Sample is added. Recorded regardless of the index AddResult — even a hash-collapse
|
||||
// still WROTE a file the tool owns, and the manifest dedups a repeat path itself
|
||||
// (Phase R prune reconciles manifest vs index later).
|
||||
g_session.owned().add(res.sample.relativePath);
|
||||
return res;
|
||||
}
|
||||
|
||||
// Runs one capture-action-table row: resolve its scope source + range, render + add +
|
||||
// record via captureAndIndexOne, then persist + mark dirty. The load-bearing principle
|
||||
// holds structurally — this path writes a file + a bank index entry ONLY; it never
|
||||
// calls InsertMedia or touches the arrange/timeline.
|
||||
static void RunCapture(const reasampler::CaptureActionDef& def)
|
||||
{
|
||||
ResolvedSource src;
|
||||
@@ -731,54 +804,14 @@ static void RunCapture(const reasampler::CaptureActionDef& def)
|
||||
return;
|
||||
}
|
||||
|
||||
// The tail mode is a PANEL SETTING (docked bank panel's toggle), not a per-action
|
||||
// variant: the plain capture actions apply whatever the panel is set to. Default
|
||||
// is None (exact bounds / byte-identical to today) until the user opts in via the
|
||||
// toggle. tailMs is meaningful only for Manual and is pre-clamped by the panel.
|
||||
const reasampler::TailSetting tail = reasampler::bankPanelTailSetting();
|
||||
|
||||
reasampler::CaptureRequest req;
|
||||
req.sourceMode = reasampler::sourceModeForScope(def.scope);
|
||||
req.startSeconds = src.startSeconds; // exact bounds — no rounding
|
||||
req.endSeconds = src.endSeconds;
|
||||
req.wetDry = 1.0; // wet post the FX left enabled by the scope
|
||||
req.tailMode = tail.mode; // None / Auto / Manual, from the panel toggle
|
||||
req.tailMs = tail.manualMs; // Manual-only (clamped); ignored for None/Auto
|
||||
req.sampleRate = 0; // follow project rate
|
||||
req.channelCount = 2;
|
||||
req.bitDepth = reasampler::WavBitDepth::Float32; // deterministic, no dither
|
||||
req.baseName = def.baseName;
|
||||
req.trackGuids = src.trackGuids; // recorded on the Sample (provenance)
|
||||
|
||||
// M10: compute provenance BEFORE the FxBypassGuard neutralizes the in-scope chain —
|
||||
// the source FX-chain identity must be read from the LIVE (un-bypassed) chain, and
|
||||
// the source selection is still live here. Returns nullopt unless this capture
|
||||
// genuinely resamples from a bank sample (detectParent). Read-only.
|
||||
const std::optional<reasampler::Provenance> prov =
|
||||
buildCaptureProvenance(req, def.scope, src);
|
||||
|
||||
// Render under the scope's FX-bypass guard (out-of-scope FX / fader / pan chain
|
||||
// neutralized for the render, fully restored on every path — see renderOffline
|
||||
// and the FxBypassGuard header comment). Non-destructive; writes a file only.
|
||||
reasampler::CaptureResult res = renderOffline(def.scope, src.sourceTracks, req);
|
||||
|
||||
reasampler::CaptureResult res =
|
||||
captureAndIndexOne(def.scope, src, def.baseName, src.startSeconds, src.endSeconds);
|
||||
if (res.status != reasampler::CaptureStatus::Ok)
|
||||
{
|
||||
ShowConsoleMsg(("ReaSampler capture failed: " + res.message + "\n").c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Stamp provenance onto the captured Sample (only set when this was a genuine
|
||||
// resample-from-sample; otherwise the optional stays empty, per M1's contract).
|
||||
res.sample.provenance = prov;
|
||||
|
||||
// Add to the ACTIVE bank: g_session.bank() resolves to book.activeIndex() (B2).
|
||||
g_session.bank().add(res.sample);
|
||||
// B-cap: record the created file in the owned-file manifest, at the same point the
|
||||
// Sample is added and before the same persist. Recorded regardless of the index
|
||||
// AddResult — even a hash-collapse still WROTE a file the tool owns, and the manifest
|
||||
// dedups a repeat path itself (Phase R prune reconciles manifest vs index later).
|
||||
g_session.owned().add(res.sample.relativePath);
|
||||
// Persist the updated book AND manifest into the active project's ext state (the
|
||||
// `banks` + `owned_files` keys) so the capture survives Save / close+reopen (M4) and
|
||||
// travels with the .rpp. saveToActiveProject also clears the retired legacy key and
|
||||
@@ -786,6 +819,271 @@ static void RunCapture(const reasampler::CaptureActionDef& def)
|
||||
g_session.saveToActiveProject();
|
||||
}
|
||||
|
||||
// --- M11: batch capture (per selected item / per razor area) ----------------
|
||||
//
|
||||
// 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: the offline backend's unique tag is 1-second-granular. The
|
||||
// per-item render already takes real wall-clock time (REAPER's offline-render dialog per
|
||||
// unit), so consecutive units naturally land in distinct seconds; belt-and-braces, each
|
||||
// unit's baseName also carries its ordinal ("item-1", "item-2", ...) so two units are
|
||||
// never asked to write the same stem within one batch. (Residual, DAW-verify: two BATCHES
|
||||
// fired within the same wall-clock second with identical ordinals could still collide —
|
||||
// unreachable in practice given the per-unit render latency, noted for completeness.)
|
||||
|
||||
// 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).
|
||||
class ItemSelectionGuard
|
||||
{
|
||||
public:
|
||||
ItemSelectionGuard()
|
||||
{
|
||||
const int n = CountSelectedMediaItems(nullptr);
|
||||
for (int i = 0; i < n; ++i)
|
||||
if (MediaItem* it = GetSelectedMediaItem(nullptr, i))
|
||||
selected_.push_back(it);
|
||||
}
|
||||
|
||||
~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.
|
||||
const int total = CountMediaItems(nullptr);
|
||||
for (int i = 0; i < total; ++i)
|
||||
if (MediaItem* it = GetMediaItem(nullptr, i))
|
||||
SetMediaItemSelected(it, false);
|
||||
for (MediaItem* it : selected_)
|
||||
SetMediaItemSelected(it, true);
|
||||
UpdateArrange(); // reflect the restored selection in the arrange view
|
||||
}
|
||||
|
||||
ItemSelectionGuard(const ItemSelectionGuard&) = delete;
|
||||
ItemSelectionGuard& operator=(const ItemSelectionGuard&) = delete;
|
||||
|
||||
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.
|
||||
static void selectOnlyItem(MediaItem* item)
|
||||
{
|
||||
const int total = CountMediaItems(nullptr);
|
||||
for (int i = 0; i < total; ++i)
|
||||
if (MediaItem* it = GetMediaItem(nullptr, i))
|
||||
SetMediaItemSelected(it, it == item);
|
||||
}
|
||||
|
||||
// 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).
|
||||
static void RunBatchCaptureItems()
|
||||
{
|
||||
// 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.
|
||||
struct ItemUnit { MediaItem* item; MediaTrack* track; double start; double end; };
|
||||
std::vector<ItemUnit> itemUnits;
|
||||
{
|
||||
const int n = CountSelectedMediaItems(nullptr);
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
MediaItem* it = GetSelectedMediaItem(nullptr, i);
|
||||
if (!it) continue;
|
||||
MediaTrack* tr = GetMediaItem_Track(it);
|
||||
if (!tr) continue;
|
||||
const double pos = GetMediaItemInfo_Value(it, "D_POSITION");
|
||||
const double len = GetMediaItemInfo_Value(it, "D_LENGTH");
|
||||
itemUnits.push_back({it, tr, pos, pos + len});
|
||||
}
|
||||
}
|
||||
if (itemUnits.empty())
|
||||
{
|
||||
ShowConsoleMsg("ReaSampler batch capture: select at least one media item.\n");
|
||||
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.
|
||||
std::vector<reasampler::BatchRange> ranges;
|
||||
ranges.reserve(itemUnits.size());
|
||||
for (const ItemUnit& u : itemUnits)
|
||||
ranges.push_back({u.start, u.end});
|
||||
const std::vector<reasampler::CaptureUnit> plan = reasampler::planCaptureUnits(ranges);
|
||||
|
||||
reasampler::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.
|
||||
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.
|
||||
std::size_t planIdx = 0;
|
||||
for (const ItemUnit& u : itemUnits)
|
||||
{
|
||||
if (!(u.end > u.start)) continue; // dropped by planCaptureUnits — skip in lockstep
|
||||
const reasampler::CaptureUnit& unit = plan[planIdx++];
|
||||
|
||||
// Transiently select ONLY this item so the item-scope render captures exactly it.
|
||||
selectOnlyItem(u.item);
|
||||
|
||||
ResolvedSource src;
|
||||
src.startSeconds = unit.startSeconds;
|
||||
src.endSeconds = unit.endSeconds;
|
||||
src.sourceTracks.push_back(u.track);
|
||||
if (std::string g = reasampler::guidString(u.track); !g.empty())
|
||||
src.trackGuids.push_back(std::move(g));
|
||||
|
||||
const std::string baseName = "item-" + std::to_string(unit.ordinal);
|
||||
reasampler::CaptureResult res = captureAndIndexOne(
|
||||
reasampler::CaptureScope::Item, src, baseName,
|
||||
unit.startSeconds, unit.endSeconds);
|
||||
|
||||
const bool ok = (res.status == reasampler::CaptureStatus::Ok);
|
||||
outcome.record(unit.ordinal, ok, ok ? std::string{} : res.message);
|
||||
if (ok) anyAdded = true;
|
||||
}
|
||||
} // selGuard restores the original selection here, on every path
|
||||
|
||||
// Persist ONCE for the whole batch (one ext-state write) — only if something landed.
|
||||
if (anyAdded)
|
||||
g_session.saveToActiveProject();
|
||||
|
||||
ShowConsoleMsg((outcome.summaryLine("item") + "\n").c_str());
|
||||
}
|
||||
|
||||
// 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.
|
||||
static std::vector<std::pair<MediaTrack*, reasampler::RazorRange>> collectRazorAreas()
|
||||
{
|
||||
std::vector<std::pair<MediaTrack*, reasampler::RazorRange>> areas;
|
||||
const int n = CountTracks(nullptr);
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
MediaTrack* tr = GetTrack(nullptr, i);
|
||||
if (!tr) continue;
|
||||
std::vector<char> buf(8192, '\0');
|
||||
if (!GetSetMediaTrackInfo_String(tr, "P_RAZOREDITS", buf.data(), false))
|
||||
continue;
|
||||
for (const reasampler::RazorRange& r :
|
||||
reasampler::parseRazorEdits(std::string(buf.data())))
|
||||
areas.push_back({tr, r});
|
||||
}
|
||||
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.
|
||||
class TrackSelectionGuard
|
||||
{
|
||||
public:
|
||||
TrackSelectionGuard()
|
||||
{
|
||||
const int n = CountSelectedTracks(nullptr);
|
||||
for (int i = 0; i < n; ++i)
|
||||
if (MediaTrack* tr = GetSelectedTrack(nullptr, i))
|
||||
selected_.push_back(tr);
|
||||
}
|
||||
|
||||
~TrackSelectionGuard()
|
||||
{
|
||||
// Deselect every track, then re-select the snapshot — the exact original set.
|
||||
const int total = CountTracks(nullptr);
|
||||
for (int i = 0; i < total; ++i)
|
||||
if (MediaTrack* tr = GetTrack(nullptr, i))
|
||||
SetTrackSelected(tr, false);
|
||||
for (MediaTrack* tr : selected_)
|
||||
SetTrackSelected(tr, true);
|
||||
}
|
||||
|
||||
TrackSelectionGuard(const TrackSelectionGuard&) = delete;
|
||||
TrackSelectionGuard& operator=(const TrackSelectionGuard&) = delete;
|
||||
|
||||
private:
|
||||
std::vector<MediaTrack*> selected_;
|
||||
};
|
||||
|
||||
// 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.
|
||||
static void RunBatchCaptureRazor()
|
||||
{
|
||||
const std::vector<std::pair<MediaTrack*, reasampler::RazorRange>> areas =
|
||||
collectRazorAreas();
|
||||
if (areas.empty())
|
||||
{
|
||||
ShowConsoleMsg("ReaSampler batch capture: make at least one razor area first.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<reasampler::BatchRange> ranges;
|
||||
ranges.reserve(areas.size());
|
||||
for (const auto& a : areas)
|
||||
ranges.push_back({a.second.startSeconds, a.second.endSeconds});
|
||||
const std::vector<reasampler::CaptureUnit> plan = reasampler::planCaptureUnits(ranges);
|
||||
|
||||
reasampler::BatchOutcome outcome;
|
||||
bool anyAdded = false;
|
||||
{
|
||||
// Restore the user's ORIGINAL track selection on every exit path.
|
||||
TrackSelectionGuard selGuard;
|
||||
|
||||
std::size_t planIdx = 0;
|
||||
for (const auto& a : areas)
|
||||
{
|
||||
if (!(a.second.endSeconds > a.second.startSeconds)) continue; // dropped — lockstep
|
||||
const reasampler::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).
|
||||
SetOnlyTrackSelected(tr);
|
||||
|
||||
ResolvedSource src;
|
||||
src.startSeconds = unit.startSeconds;
|
||||
src.endSeconds = unit.endSeconds;
|
||||
src.sourceTracks.push_back(tr);
|
||||
if (std::string g = reasampler::guidString(tr); !g.empty())
|
||||
src.trackGuids.push_back(std::move(g));
|
||||
|
||||
const std::string baseName = "razor-" + std::to_string(unit.ordinal);
|
||||
reasampler::CaptureResult res = captureAndIndexOne(
|
||||
reasampler::CaptureScope::Track, src, baseName,
|
||||
unit.startSeconds, unit.endSeconds);
|
||||
|
||||
const bool ok = (res.status == reasampler::CaptureStatus::Ok);
|
||||
outcome.record(unit.ordinal, ok, ok ? std::string{} : res.message);
|
||||
if (ok) anyAdded = true;
|
||||
}
|
||||
} // selGuard restores the original track selection here, on every path
|
||||
|
||||
if (anyAdded)
|
||||
g_session.saveToActiveProject();
|
||||
|
||||
ShowConsoleMsg((outcome.summaryLine("razor area") + "\n").c_str());
|
||||
}
|
||||
|
||||
// --- M10: re-capture from source --------------------------------------------
|
||||
//
|
||||
// Regenerates a PROVENANCED bank sample's file from its recorded source's CURRENT
|
||||
@@ -1124,6 +1422,8 @@ static bool OnHookCommand(int command, int /*flag*/)
|
||||
if (command == g_cmdToggleBankPanel) { reasampler::bankPanelToggle(); return true; }
|
||||
if (command == g_cmdInsertSelected) { RunInsertSelected(false); return true; }
|
||||
if (command == g_cmdInsertSelectedConform) { RunInsertSelected(true); return true; }
|
||||
if (command == g_cmdCaptureBatchItems) { RunBatchCaptureItems(); return true; }
|
||||
if (command == g_cmdCaptureBatchRazor) { RunBatchCaptureRazor(); return true; }
|
||||
if (command == g_cmdCaptureTrackRealtime) { RunCaptureRealtimeTrack(); return true; }
|
||||
if (command == g_cmdCancelRealtime) { RunCancelRealtime(); return true; }
|
||||
if (command == g_cmdRecaptureFromSource) { RunRecaptureFromSource(); return true; }
|
||||
@@ -1155,6 +1455,8 @@ static int OnToggleAction(int command)
|
||||
static gaccel_register_t g_accelToggleBankPanel{};
|
||||
static gaccel_register_t g_accelInsertSelected{};
|
||||
static gaccel_register_t g_accelInsertSelectedConform{};
|
||||
static gaccel_register_t g_accelCaptureBatchItems{};
|
||||
static gaccel_register_t g_accelCaptureBatchRazor{};
|
||||
static gaccel_register_t g_accelCaptureTrackRealtime{};
|
||||
static gaccel_register_t g_accelCancelRealtime{};
|
||||
static gaccel_register_t g_accelRecaptureFromSource{};
|
||||
@@ -1166,6 +1468,8 @@ static gaccel_register_t g_accelShowVersion{};
|
||||
static std::string g_descToggleBankPanel;
|
||||
static std::string g_descInsertSelected;
|
||||
static std::string g_descInsertSelectedConform;
|
||||
static std::string g_descCaptureBatchItems;
|
||||
static std::string g_descCaptureBatchRazor;
|
||||
static std::string g_descCaptureTrackRealtime;
|
||||
static std::string g_descCancelRealtime;
|
||||
static std::string g_descRecaptureFromSource;
|
||||
@@ -1176,6 +1480,8 @@ static std::string g_descShowVersion;
|
||||
static const char* g_idToggleBankPanel = nullptr;
|
||||
static const char* g_idInsertSelected = nullptr;
|
||||
static const char* g_idInsertSelectedConform = nullptr;
|
||||
static const char* g_idCaptureBatchItems = nullptr;
|
||||
static const char* g_idCaptureBatchRazor = nullptr;
|
||||
static const char* g_idCaptureTrackRealtime = nullptr;
|
||||
static const char* g_idCancelRealtime = nullptr;
|
||||
static const char* g_idRecaptureFromSource = nullptr;
|
||||
@@ -1221,6 +1527,10 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
||||
g_rec->Register("-command_id", (void*)g_idCancelRealtime);
|
||||
g_rec->Register("-gaccel", (void*)&g_accelCaptureTrackRealtime);
|
||||
g_rec->Register("-command_id", (void*)g_idCaptureTrackRealtime);
|
||||
g_rec->Register("-gaccel", (void*)&g_accelCaptureBatchRazor);
|
||||
g_rec->Register("-command_id", (void*)g_idCaptureBatchRazor);
|
||||
g_rec->Register("-gaccel", (void*)&g_accelCaptureBatchItems);
|
||||
g_rec->Register("-command_id", (void*)g_idCaptureBatchItems);
|
||||
g_rec->Register("-gaccel", (void*)&g_accelInsertSelectedConform);
|
||||
g_rec->Register("-command_id", (void*)g_idInsertSelectedConform);
|
||||
g_rec->Register("-gaccel", (void*)&g_accelInsertSelected);
|
||||
@@ -1340,6 +1650,32 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
||||
rec->Register("gaccel", (void*)&g_accelInsertSelectedConform);
|
||||
}
|
||||
|
||||
// Register the M11 batch-capture actions (command_id -> gaccel -> hookcommand). Each
|
||||
// fires N captures (one bank sample per selected item / per razor area), honoring every
|
||||
// precision invariant per unit and restoring the original selection on every path.
|
||||
// Channel-qualified FOREVER-STABLE ids.
|
||||
g_idCaptureBatchItems = internCmdId("CAPTURE_BATCH_ITEMS");
|
||||
g_cmdCaptureBatchItems = rec->Register("command_id", (void*)g_idCaptureBatchItems);
|
||||
if (g_cmdCaptureBatchItems)
|
||||
{
|
||||
g_descCaptureBatchItems =
|
||||
reasampler::channelActionName("batch capture selected items (one per item)");
|
||||
g_accelCaptureBatchItems.accel.cmd = g_cmdCaptureBatchItems;
|
||||
g_accelCaptureBatchItems.desc = g_descCaptureBatchItems.c_str();
|
||||
rec->Register("gaccel", (void*)&g_accelCaptureBatchItems);
|
||||
}
|
||||
|
||||
g_idCaptureBatchRazor = internCmdId("CAPTURE_BATCH_RAZOR");
|
||||
g_cmdCaptureBatchRazor = rec->Register("command_id", (void*)g_idCaptureBatchRazor);
|
||||
if (g_cmdCaptureBatchRazor)
|
||||
{
|
||||
g_descCaptureBatchRazor =
|
||||
reasampler::channelActionName("batch capture razor areas (one per area)");
|
||||
g_accelCaptureBatchRazor.accel.cmd = g_cmdCaptureBatchRazor;
|
||||
g_accelCaptureBatchRazor.desc = g_descCaptureBatchRazor.c_str();
|
||||
rec->Register("gaccel", (void*)&g_accelCaptureBatchRazor);
|
||||
}
|
||||
|
||||
// Register the "capture selected track (realtime)" action (command_id -> gaccel ->
|
||||
// hookcommand). Realtime sibling of the offline CAPTURE_TRACK scope: records the
|
||||
// selected track's own output in realtime into a hidden temp track, moves it into
|
||||
|
||||
Reference in New Issue
Block a user