feat(capture): bindable capture family — master/tracks/items/razor (M7)
Four wet capture actions route to OfflineRenderBackend (snapshot/restore RENDER_*, 32-bit float), produce a Sample, add to the bank, persist. Pure render_settings maps source mode to RENDER_SETTINGS and parses P_RAZOREDITS (tested). No arrange insertion. True dry deferred to M10.
This commit is contained in:
+216
-36
@@ -20,12 +20,16 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "actions.h"
|
||||
#include "bank_model.h"
|
||||
#include "bank_panel.h"
|
||||
#include "capture.h"
|
||||
#include "insert.h"
|
||||
#include "persist.h"
|
||||
#include "render_settings.h"
|
||||
#include "track_guid.h"
|
||||
#include "view.h"
|
||||
|
||||
// Persistent action-id prefix for the ReaSampler action family.
|
||||
@@ -39,14 +43,18 @@
|
||||
REAPER_PLUGIN_HINSTANCE g_hInst = nullptr; // this module's instance handle
|
||||
reaper_plugin_info_t* g_rec = nullptr; // REAPER's dispatch struct
|
||||
|
||||
// ---- Action registration seam (M3 spike: capture master mix) ---------------
|
||||
// First live use of the action pattern the seam left templated. The full capture
|
||||
// action family (selected tracks/items/razor, wet/dry, tail) is M7; this is ONE
|
||||
// temporary action driving the M3 offline-render spike.
|
||||
|
||||
// Command id for "ReaSampler: capture master mix (spike)". FOREVER-STABLE string
|
||||
// (user keybindings key off it) — see the prefix note above.
|
||||
static int g_cmdCaptureMasterSpike = 0;
|
||||
// ---- M7 capture action family ----------------------------------------------
|
||||
// Four wet-only bindable actions from captureActionTable() (render_settings, pure):
|
||||
// master mix, selected tracks, selected items, razor area — all wet (post-FX).
|
||||
// Tail is OFF for every row (exact bounds); a tail-on variant is a later opt-in
|
||||
// (YAGNI). The M3 "capture master mix (spike)" action is RETIRED and replaced by
|
||||
// this family. Dry variants are deferred to M10 (null-test work).
|
||||
//
|
||||
// The minted command ids parallel the table rows 1:1 (same index). gaccel storage
|
||||
// must outlive registration (REAPER holds each pointer), so both vectors are file-
|
||||
// scope and sized to the table. FOREVER-STABLE id strings live in the table.
|
||||
static std::vector<int> g_captureCmdIds;
|
||||
static std::vector<gaccel_register_t> g_captureAccels;
|
||||
|
||||
// Command id for "ReaSampler: toggle bank panel" (M5). FOREVER-STABLE string.
|
||||
// The docked grid window is display-only this wave (Wave A) — the action just
|
||||
@@ -92,24 +100,169 @@ static void OnTimer()
|
||||
reasampler::bankPanelRefresh();
|
||||
}
|
||||
|
||||
// Runs the M3 spike: render the time-selection master mix, add the Sample, log.
|
||||
static void RunCaptureMasterSpike()
|
||||
// --- M7 source resolvers ----------------------------------------------------
|
||||
// Each resolves a source mode to (1) the exact render range in project seconds and
|
||||
// (2) the track GUIDs, when track-scoped. They ONLY READ DAW state (selection, time
|
||||
// selection, razor strings) — they never mutate it (non-destructive). Returning
|
||||
// false means "nothing to capture" (empty selection / no razor / empty range); the
|
||||
// caller reports it and writes nothing.
|
||||
|
||||
// The resolved source: exact bounds + optional track GUIDs.
|
||||
struct ResolvedSource
|
||||
{
|
||||
// Time selection -> exact render bounds (no rounding). GetSet_LoopTimeRange
|
||||
// with isSet=false reads the current time selection (isLoop=false).
|
||||
double start = 0.0, end = 0.0;
|
||||
double startSeconds = 0.0;
|
||||
double endSeconds = 0.0;
|
||||
std::vector<std::string> trackGuids; // populated only for SelectedTracks
|
||||
};
|
||||
|
||||
// Time selection -> exact bounds (no rounding). GetSet_LoopTimeRange(isSet=false,
|
||||
// isLoop=false) reads the current time selection. Used by master mix (the range is
|
||||
// the time selection) and as the time window for selected-track captures.
|
||||
static bool resolveTimeSelection(double& start, double& end)
|
||||
{
|
||||
start = 0.0; end = 0.0;
|
||||
GetSet_LoopTimeRange(false, false, &start, &end, false);
|
||||
return end > start;
|
||||
}
|
||||
|
||||
// Master mix / time selection: bounds = the time selection; no track GUIDs.
|
||||
static bool resolveMaster(ResolvedSource& out)
|
||||
{
|
||||
return resolveTimeSelection(out.startSeconds, out.endSeconds);
|
||||
}
|
||||
|
||||
// Selected tracks: the render time window is the time selection (RENDER_SETTINGS
|
||||
// selects WHICH tracks; the custom bounds select the WHEN). We also collect the
|
||||
// selected tracks' GUIDs for the Sample's provenance. Requires both a non-empty
|
||||
// track selection AND a time selection (the bounds come from the latter).
|
||||
static bool resolveSelectedTracks(ResolvedSource& out)
|
||||
{
|
||||
const int n = CountSelectedTracks(nullptr); // nullptr = active project
|
||||
if (n <= 0) return false;
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
MediaTrack* tr = GetSelectedTrack(nullptr, i);
|
||||
if (!tr) continue;
|
||||
std::string g = reasampler::guidString(tr);
|
||||
if (!g.empty()) out.trackGuids.push_back(std::move(g));
|
||||
}
|
||||
return resolveTimeSelection(out.startSeconds, out.endSeconds);
|
||||
}
|
||||
|
||||
// Selected items: bounds = the union [min position, max position+length] across
|
||||
// the selected items (D_POSITION / D_LENGTH — SDK header ~1990/1991). Exact, no
|
||||
// rounding. RENDER_SETTINGS selects the items; the bounds keep the render window
|
||||
// tight around them.
|
||||
static bool resolveSelectedItems(ResolvedSource& out)
|
||||
{
|
||||
const int n = CountSelectedMediaItems(nullptr);
|
||||
if (n <= 0) return false;
|
||||
bool any = false;
|
||||
double lo = 0.0, hi = 0.0;
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
MediaItem* it = GetSelectedMediaItem(nullptr, i);
|
||||
if (!it) continue;
|
||||
const double pos = GetMediaItemInfo_Value(it, "D_POSITION");
|
||||
const double len = GetMediaItemInfo_Value(it, "D_LENGTH");
|
||||
const double end = pos + len;
|
||||
if (!any) { lo = pos; hi = end; any = true; }
|
||||
else { if (pos < lo) lo = pos; if (end > hi) hi = end; }
|
||||
}
|
||||
if (!any) return false;
|
||||
out.startSeconds = lo;
|
||||
out.endSeconds = hi;
|
||||
return out.endSeconds > out.startSeconds;
|
||||
}
|
||||
|
||||
// Razor area: razor edits live PER TRACK (P_RAZOREDITS — SDK header ~2899:
|
||||
// space-separated triples of start, end, envGuidString). We read every track's
|
||||
// razor string, parse the track-audio areas (pure parseRazorEdits), and take the
|
||||
// union bound as the render window. RENDER_SETTINGS&4096 selects the razor content;
|
||||
// the bounds keep the window tight. Reads only — never clears the razor selection.
|
||||
static bool resolveRazorArea(ResolvedSource& out)
|
||||
{
|
||||
std::vector<reasampler::RazorRange> allRanges;
|
||||
const int n = CountTracks(nullptr);
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
MediaTrack* tr = GetTrack(nullptr, i);
|
||||
if (!tr) continue;
|
||||
// GetSetMediaTrackInfo_String(tr, "P_RAZOREDITS", buf, false) reads the
|
||||
// razor string into buf. Big buffer: many areas can accumulate.
|
||||
std::vector<char> buf(8192, '\0');
|
||||
if (!GetSetMediaTrackInfo_String(tr, "P_RAZOREDITS", buf.data(), false))
|
||||
continue;
|
||||
std::vector<reasampler::RazorRange> ranges =
|
||||
reasampler::parseRazorEdits(std::string(buf.data()));
|
||||
for (auto& r : ranges) allRanges.push_back(r);
|
||||
}
|
||||
if (allRanges.empty()) return false;
|
||||
reasampler::RazorRange u = reasampler::razorUnionBounds(allRanges);
|
||||
out.startSeconds = u.startSeconds;
|
||||
out.endSeconds = u.endSeconds;
|
||||
return out.endSeconds > out.startSeconds;
|
||||
}
|
||||
|
||||
// Dispatches to the right resolver for a source mode. Returns false with a reason
|
||||
// in `why` when there is nothing to capture (so the action can log precisely).
|
||||
static bool ResolveSource(reasampler::SourceMode mode, ResolvedSource& out,
|
||||
std::string& why)
|
||||
{
|
||||
using reasampler::SourceMode;
|
||||
switch (mode)
|
||||
{
|
||||
case SourceMode::MasterMix:
|
||||
case SourceMode::TimeSelection:
|
||||
if (resolveMaster(out)) return true;
|
||||
why = "no time selection (make a time selection first)";
|
||||
return false;
|
||||
case SourceMode::SelectedTracks:
|
||||
if (resolveSelectedTracks(out)) return true;
|
||||
why = "select at least one track AND make a time selection";
|
||||
return false;
|
||||
case SourceMode::SelectedItems:
|
||||
if (resolveSelectedItems(out)) return true;
|
||||
why = "select at least one media item";
|
||||
return false;
|
||||
case SourceMode::RazorArea:
|
||||
if (resolveRazorArea(out)) return true;
|
||||
why = "no razor edit area found on any track";
|
||||
return false;
|
||||
case SourceMode::Realtime:
|
||||
why = "realtime capture is the M8 backend, not offline render";
|
||||
return false;
|
||||
}
|
||||
why = "unknown source mode";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Runs one capture-action-table row: resolve its source, build a CaptureRequest,
|
||||
// hand it to 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.
|
||||
static void RunCapture(const reasampler::CaptureActionDef& def)
|
||||
{
|
||||
ResolvedSource src;
|
||||
std::string why;
|
||||
if (!ResolveSource(def.sourceMode, src, why))
|
||||
{
|
||||
ShowConsoleMsg(("ReaSampler capture: " + why + ".\n").c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
reasampler::CaptureRequest req;
|
||||
req.sourceMode = reasampler::SourceMode::TimeSelection;
|
||||
req.startSeconds = start;
|
||||
req.endSeconds = end;
|
||||
req.wetDry = 1.0; // wet master mix
|
||||
req.renderTail = false; // exact bounds, no tail
|
||||
req.sampleRate = 0; // follow project rate
|
||||
req.sourceMode = def.sourceMode;
|
||||
req.startSeconds = src.startSeconds; // exact bounds — no rounding
|
||||
req.endSeconds = src.endSeconds;
|
||||
req.wetDry = def.wetDry; // 1.0 wet (all M7 actions are wet-only)
|
||||
req.renderTail = false; // exact bounds, no tail (M7 default)
|
||||
req.tailMs = 0.0;
|
||||
req.sampleRate = 0; // follow project rate
|
||||
req.channelCount = 2;
|
||||
req.bitDepth = reasampler::WavBitDepth::Float32; // deterministic, no dither
|
||||
req.baseName = "master_mix";
|
||||
req.bitDepth = reasampler::WavBitDepth::Float32; // deterministic, no dither
|
||||
req.baseName = def.baseName;
|
||||
req.trackGuids = src.trackGuids; // recorded on the Sample (track captures)
|
||||
|
||||
reasampler::OfflineRenderBackend backend;
|
||||
reasampler::CaptureResult res = backend.capture(req);
|
||||
@@ -122,8 +275,8 @@ static void RunCaptureMasterSpike()
|
||||
|
||||
reasampler::AddResult added = g_session.bank().add(res.sample);
|
||||
// Persist the updated bank into the active project's ext state so the capture
|
||||
// survives Save / close+reopen (M4). Non-destructive: writes only our own
|
||||
// ext-state key. No-ops on an unsaved project (nothing to store into yet).
|
||||
// survives Save / close+reopen (M4) and travels with the .rpp. saveToActiveProject
|
||||
// also calls MarkProjectDirty. Non-destructive: writes only our own ext-state key.
|
||||
g_session.saveToActiveProject();
|
||||
|
||||
std::string log = "ReaSampler: " + res.message + "\n";
|
||||
@@ -179,7 +332,14 @@ static void RunInsertSelected(bool conform)
|
||||
static bool OnHookCommand(int command, int /*flag*/)
|
||||
{
|
||||
if (command == 0) return false;
|
||||
if (command == g_cmdCaptureMasterSpike) { RunCaptureMasterSpike(); return true; }
|
||||
// M7 capture family: command ids parallel captureActionTable() 1:1 by index.
|
||||
// Claim the fired id if it is one of ours and route to its table row.
|
||||
for (std::size_t i = 0; i < g_captureCmdIds.size(); ++i)
|
||||
if (command == g_captureCmdIds[i])
|
||||
{
|
||||
RunCapture(reasampler::captureActionTable()[i]);
|
||||
return true;
|
||||
}
|
||||
if (command == g_cmdToggleBankPanel) { reasampler::bankPanelToggle(); return true; }
|
||||
if (command == g_cmdInsertSelected) { RunInsertSelected(false); return true; }
|
||||
if (command == g_cmdInsertSelectedConform) { RunInsertSelected(true); return true; }
|
||||
@@ -199,7 +359,7 @@ static int OnToggleAction(int command)
|
||||
}
|
||||
|
||||
// gaccel storage must outlive registration — REAPER holds the pointer.
|
||||
static gaccel_register_t g_accelCaptureMaster{};
|
||||
// (The capture family's accels live in g_captureAccels, sized to the table.)
|
||||
static gaccel_register_t g_accelToggleBankPanel{};
|
||||
static gaccel_register_t g_accelInsertSelected{};
|
||||
static gaccel_register_t g_accelInsertSelectedConform{};
|
||||
@@ -228,9 +388,18 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
||||
g_rec->Register("-gaccel", (void*)&g_accelToggleBankPanel);
|
||||
g_rec->Register("-command_id",
|
||||
(void*)(REASAMPLER_ACTION_PREFIX "TOGGLE_BANK_PANEL"));
|
||||
g_rec->Register("-gaccel", (void*)&g_accelCaptureMaster);
|
||||
g_rec->Register("-command_id",
|
||||
(void*)(REASAMPLER_ACTION_PREFIX "CAPTURE_MASTER_SPIKE"));
|
||||
// Mirror-unregister the M7 capture family: gaccel + command_id per row,
|
||||
// with '-'-prefixed strings (per the contract). The FOREVER-STABLE id
|
||||
// strings come from the same table used to register them.
|
||||
{
|
||||
const auto& table = reasampler::captureActionTable();
|
||||
for (std::size_t i = 0; i < table.size(); ++i)
|
||||
{
|
||||
if (i < g_captureAccels.size())
|
||||
g_rec->Register("-gaccel", (void*)&g_captureAccels[i]);
|
||||
g_rec->Register("-command_id", (void*)table[i].commandString);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Destroy the docked window and release cached thumbnails before we drop
|
||||
// the API pointers (DockWindowRemove/DestroyWindow need them live).
|
||||
@@ -251,15 +420,26 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
||||
g_hInst = hInstance;
|
||||
g_rec = rec;
|
||||
|
||||
// Register the M3 spike action (command_id -> gaccel -> hookcommand).
|
||||
g_cmdCaptureMasterSpike = rec->Register(
|
||||
"command_id",
|
||||
(void*)(REASAMPLER_ACTION_PREFIX "CAPTURE_MASTER_SPIKE"));
|
||||
if (g_cmdCaptureMasterSpike)
|
||||
// Register the M7 capture action family (command_id -> gaccel per table row).
|
||||
// The single hookcommand below routes every fired id back to its row by index.
|
||||
// g_captureAccels must be sized BEFORE the loop and never reallocated after —
|
||||
// REAPER holds a pointer to each element until we mirror-unregister it.
|
||||
{
|
||||
g_accelCaptureMaster.accel.cmd = g_cmdCaptureMasterSpike;
|
||||
g_accelCaptureMaster.desc = "ReaSampler: capture master mix (spike)";
|
||||
rec->Register("gaccel", (void*)&g_accelCaptureMaster);
|
||||
const auto& table = reasampler::captureActionTable();
|
||||
g_captureCmdIds.assign(table.size(), 0);
|
||||
g_captureAccels.assign(table.size(), gaccel_register_t{});
|
||||
for (std::size_t i = 0; i < table.size(); ++i)
|
||||
{
|
||||
const int cmd =
|
||||
rec->Register("command_id", (void*)table[i].commandString);
|
||||
g_captureCmdIds[i] = cmd;
|
||||
if (cmd)
|
||||
{
|
||||
g_captureAccels[i].accel.cmd = cmd;
|
||||
g_captureAccels[i].desc = table[i].description;
|
||||
rec->Register("gaccel", (void*)&g_captureAccels[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Point the bank panel at the live session BEFORE registering its action, so
|
||||
|
||||
Reference in New Issue
Block a user