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:
2026-07-26 19:00:00 -04:00
parent 83cd040f26
commit d23a3f9bb8
5 changed files with 740 additions and 50 deletions
+385 -49
View File
@@ -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