Rework capture into three FX-scope actions with inferred range
Replace the four capture modes with item/track/master scope actions. Each infers its range (razor-else-time) and enforces FX scope via non-destructive FX-bypass-around-render (RAII I_FXEN snapshot/restore over ancestors + master). Corrects the defect of items captured through parent FX.
This commit is contained in:
+209
-118
@@ -43,12 +43,17 @@
|
||||
REAPER_PLUGIN_HINSTANCE g_hInst = nullptr; // this module's instance handle
|
||||
reaper_plugin_info_t* g_rec = nullptr; // REAPER's dispatch struct
|
||||
|
||||
// ---- 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).
|
||||
// ---- Capture action family (three FX scopes) -------------------------------
|
||||
// Three bindable SCOPE actions from captureActionTable() (render_settings, pure):
|
||||
// capture item / track / master. Each infers its range (razor-else-time) and
|
||||
// enforces the FX-scope invariant via FX-bypass-around-render (FxBypassGuard):
|
||||
// Item -> take/item FX only (bypass the item's track + ancestors + master).
|
||||
// Track -> item FX + track's own FX (bypass ancestors + master).
|
||||
// Master -> whole chain (bypass nothing).
|
||||
// This REPLACES the retired M7 four-mode family (master / tracks / items / razor).
|
||||
// The retired CAPTURE_TRACKS_WET / CAPTURE_ITEMS_WET / CAPTURE_RAZOR_WET ids are
|
||||
// mirror-unregistered on unload so old keybindings clear cleanly; CAPTURE_MASTER's
|
||||
// id string is preserved.
|
||||
//
|
||||
// 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-
|
||||
@@ -56,6 +61,16 @@ reaper_plugin_info_t* g_rec = nullptr; // REAPER's dispatch struct
|
||||
static std::vector<int> g_captureCmdIds;
|
||||
static std::vector<gaccel_register_t> g_captureAccels;
|
||||
|
||||
// Retired capture-action command-id strings (M7 four-mode family). Kept ONLY to
|
||||
// mirror-unregister them on unload so a user's stale keybindings are cleaned up.
|
||||
// Never re-register these. CAPTURE_MASTER is NOT here — its id string carries over
|
||||
// to the new master scope action unchanged.
|
||||
static const char* const kRetiredCaptureCmdStrings[] = {
|
||||
"CEREBELLUM_REASAMPLER_CAPTURE_TRACKS_WET",
|
||||
"CEREBELLUM_REASAMPLER_CAPTURE_ITEMS_WET",
|
||||
"CEREBELLUM_REASAMPLER_CAPTURE_RAZOR_WET",
|
||||
};
|
||||
|
||||
// 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
|
||||
// shows/hides it; it never captures, inserts, or mutates the bank.
|
||||
@@ -100,24 +115,26 @@ static void OnTimer()
|
||||
reasampler::bankPanelRefresh();
|
||||
}
|
||||
|
||||
// --- 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.
|
||||
// --- Scope-action source resolution -----------------------------------------
|
||||
// The three scope actions (item / track / master) each resolve to (1) an exact
|
||||
// render range in project seconds — razor-else-time, inferred here — and (2) the
|
||||
// set of source TRACKS whose ancestor chains drive the FX-bypass plan. All reads
|
||||
// are non-destructive: selection, razor, and time selection are read, never
|
||||
// mutated. Returning false means "nothing to capture" (empty selection / no
|
||||
// range); the caller reports it and writes nothing.
|
||||
|
||||
// The resolved source: exact bounds + optional track GUIDs.
|
||||
// The resolved source: exact bounds + the source tracks (for FX-bypass + Sample
|
||||
// provenance GUIDs). `sourceTracks` is empty for Master scope.
|
||||
struct ResolvedSource
|
||||
{
|
||||
double startSeconds = 0.0;
|
||||
double endSeconds = 0.0;
|
||||
std::vector<std::string> trackGuids; // populated only for SelectedTracks
|
||||
double startSeconds = 0.0;
|
||||
double endSeconds = 0.0;
|
||||
std::vector<MediaTrack*> sourceTracks; // item's/selected tracks; empty for master
|
||||
std::vector<std::string> trackGuids; // canonical GUIDs of sourceTracks
|
||||
};
|
||||
|
||||
// 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.
|
||||
// isLoop=false) reads the current time selection.
|
||||
static bool resolveTimeSelection(double& start, double& end)
|
||||
{
|
||||
start = 0.0; end = 0.0;
|
||||
@@ -125,62 +142,11 @@ static bool resolveTimeSelection(double& start, double& end)
|
||||
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)
|
||||
// Reads every track's P_RAZOREDITS (SDK header ~2899: space-separated triples of
|
||||
// start, end, envGuidString), parses the track-audio areas (pure parseRazorEdits),
|
||||
// and returns the union bound. Reads only — never clears the razor selection.
|
||||
// Returns false when no track-audio razor area exists on any track.
|
||||
static bool resolveRazorRange(double& start, double& end)
|
||||
{
|
||||
std::vector<reasampler::RazorRange> allRanges;
|
||||
const int n = CountTracks(nullptr);
|
||||
@@ -188,8 +154,6 @@ static bool resolveRazorArea(ResolvedSource& out)
|
||||
{
|
||||
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;
|
||||
@@ -199,70 +163,193 @@ static bool resolveRazorArea(ResolvedSource& out)
|
||||
}
|
||||
if (allRanges.empty()) return false;
|
||||
reasampler::RazorRange u = reasampler::razorUnionBounds(allRanges);
|
||||
out.startSeconds = u.startSeconds;
|
||||
out.endSeconds = u.endSeconds;
|
||||
return out.endSeconds > out.startSeconds;
|
||||
start = u.startSeconds;
|
||||
end = u.endSeconds;
|
||||
return end > start;
|
||||
}
|
||||
|
||||
// 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)
|
||||
// Infers the render RANGE for any scope: razor union when a razor area is present,
|
||||
// else the time selection (pure inferRangeSource decides which). Orthogonal to
|
||||
// scope. Returns false (with a reason) when neither yields a non-empty range.
|
||||
static bool resolveRange(double& start, double& end, std::string& why)
|
||||
{
|
||||
using reasampler::SourceMode;
|
||||
switch (mode)
|
||||
double rzStart = 0.0, rzEnd = 0.0;
|
||||
const bool hasRazor = resolveRazorRange(rzStart, rzEnd);
|
||||
if (reasampler::inferRangeSource(hasRazor) == reasampler::RangeSource::Razor)
|
||||
{
|
||||
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;
|
||||
start = rzStart; end = rzEnd;
|
||||
return true; // resolveRazorRange already verified end > start
|
||||
}
|
||||
why = "unknown source mode";
|
||||
if (resolveTimeSelection(start, end)) return true;
|
||||
why = "make a razor area or a time selection first";
|
||||
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.
|
||||
// Collects the selected tracks (Track scope) into out.sourceTracks + GUIDs.
|
||||
static bool collectSelectedTracks(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;
|
||||
out.sourceTracks.push_back(tr);
|
||||
std::string g = reasampler::guidString(tr);
|
||||
if (!g.empty()) out.trackGuids.push_back(std::move(g));
|
||||
}
|
||||
return !out.sourceTracks.empty();
|
||||
}
|
||||
|
||||
// Collects the tracks that own the selected items (Item scope) into
|
||||
// out.sourceTracks (deduped) — these are the tracks whose FX must be bypassed so an
|
||||
// item capture hears take/item FX only. GetMediaItem_Track(item) gives the owning
|
||||
// track (SDK header, verify). GUIDs recorded for provenance.
|
||||
static bool collectSelectedItemTracks(ResolvedSource& out)
|
||||
{
|
||||
const int n = CountSelectedMediaItems(nullptr);
|
||||
if (n <= 0) return false;
|
||||
for (int i = 0; i < n; ++i)
|
||||
{
|
||||
MediaItem* it = GetSelectedMediaItem(nullptr, i);
|
||||
if (!it) continue;
|
||||
MediaTrack* tr = GetMediaItem_Track(it);
|
||||
if (!tr) continue;
|
||||
// Dedup: several selected items can share a track.
|
||||
bool seen = false;
|
||||
for (MediaTrack* t : out.sourceTracks) if (t == tr) { seen = true; break; }
|
||||
if (seen) continue;
|
||||
out.sourceTracks.push_back(tr);
|
||||
std::string g = reasampler::guidString(tr);
|
||||
if (!g.empty()) out.trackGuids.push_back(std::move(g));
|
||||
}
|
||||
return !out.sourceTracks.empty();
|
||||
}
|
||||
|
||||
// Resolves the source for a scope: the selection tracks (item/track) or none
|
||||
// (master), plus the inferred range. Returns false with a reason on nothing to do.
|
||||
static bool ResolveScopeSource(reasampler::CaptureScope scope,
|
||||
ResolvedSource& out, std::string& why)
|
||||
{
|
||||
using reasampler::CaptureScope;
|
||||
switch (scope)
|
||||
{
|
||||
case CaptureScope::Item:
|
||||
if (!collectSelectedItemTracks(out)) {
|
||||
why = "select at least one media item"; return false;
|
||||
}
|
||||
break;
|
||||
case CaptureScope::Track:
|
||||
if (!collectSelectedTracks(out)) {
|
||||
why = "select at least one track"; return false;
|
||||
}
|
||||
break;
|
||||
case CaptureScope::Master:
|
||||
break; // whole chain — no source-track collection
|
||||
}
|
||||
return resolveRange(out.startSeconds, out.endSeconds, why);
|
||||
}
|
||||
|
||||
// --- FX-bypass-around-render (RAII, non-destructive) ------------------------
|
||||
// Snapshots and clears I_FXEN on the tracks a scope must NOT hear the FX of, then
|
||||
// restores every snapshotted value on EVERY exit path (including the render's).
|
||||
// I_FXEN bypasses a track's FX plugins only — NOT its volume/pan/routing (so a
|
||||
// Track capture rendered via master still carries parent/master GAIN; documented
|
||||
// boundary, DAW-confirm). Structurally non-destructive: no takes, no items, no
|
||||
// project restructuring — only a transient FX-enable toggle, always restored.
|
||||
class FxBypassGuard
|
||||
{
|
||||
public:
|
||||
// scope drives fxBypassPlanFor; sourceTracks are the captured tracks whose
|
||||
// ancestor chains (walked via GetParentTrack) + the master are bypassed per the
|
||||
// plan. proj is the active project (for GetMasterTrack).
|
||||
FxBypassGuard(reasampler::CaptureScope scope,
|
||||
const std::vector<MediaTrack*>& sourceTracks,
|
||||
ReaProject* proj)
|
||||
{
|
||||
const reasampler::FxBypassPlan plan = reasampler::fxBypassPlanFor(scope);
|
||||
|
||||
for (MediaTrack* tr : sourceTracks)
|
||||
{
|
||||
if (!tr) continue;
|
||||
if (plan.bypassSelfFx) bypass(tr);
|
||||
if (plan.bypassAncestorFx)
|
||||
{
|
||||
// Walk parents to the top: GetParentTrack returns the immediate
|
||||
// parent (folder) track, nullptr at the outermost level (SDK
|
||||
// header ~2407). The master is NOT returned here — handled below.
|
||||
for (MediaTrack* p = GetParentTrack(tr); p; p = GetParentTrack(p))
|
||||
bypass(p);
|
||||
}
|
||||
}
|
||||
if (plan.bypassMaster)
|
||||
{
|
||||
// GetMasterTrack(proj) -> the master track (SDK header ~1925); I_FXEN
|
||||
// on it bypasses the master FX chain, leaving master gain/routing live.
|
||||
if (MediaTrack* master = GetMasterTrack(proj)) bypass(master);
|
||||
}
|
||||
}
|
||||
|
||||
~FxBypassGuard()
|
||||
{
|
||||
// Restore in reverse for symmetry (order is not load-bearing — each track
|
||||
// appears once, snapshots are independent).
|
||||
for (auto it = snapshots_.rbegin(); it != snapshots_.rend(); ++it)
|
||||
SetMediaTrackInfo_Value(it->track, "I_FXEN", it->fxen);
|
||||
}
|
||||
|
||||
FxBypassGuard(const FxBypassGuard&) = delete;
|
||||
FxBypassGuard& operator=(const FxBypassGuard&) = delete;
|
||||
|
||||
private:
|
||||
struct Snap { MediaTrack* track; double fxen; };
|
||||
std::vector<Snap> snapshots_;
|
||||
|
||||
// Snapshot I_FXEN once per track (dedup: an ancestor shared by two selected
|
||||
// tracks must be restored to its ORIGINAL value, not a re-snapshot of the
|
||||
// already-bypassed 0), then clear it.
|
||||
void bypass(MediaTrack* tr)
|
||||
{
|
||||
for (const Snap& s : snapshots_) if (s.track == tr) return; // already done
|
||||
const double fxen = GetMediaTrackInfo_Value(tr, "I_FXEN");
|
||||
snapshots_.push_back({tr, fxen});
|
||||
SetMediaTrackInfo_Value(tr, "I_FXEN", 0.0); // 0 = bypassed (SDK ~2194)
|
||||
}
|
||||
};
|
||||
|
||||
// Runs one capture-action-table row: resolve its scope source + range, snapshot &
|
||||
// clear the out-of-scope FX (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 is fully restored on
|
||||
// every path by FxBypassGuard, and the backend restores every RENDER_* setting.
|
||||
static void RunCapture(const reasampler::CaptureActionDef& def)
|
||||
{
|
||||
ResolvedSource src;
|
||||
std::string why;
|
||||
if (!ResolveSource(def.sourceMode, src, why))
|
||||
if (!ResolveScopeSource(def.scope, src, why))
|
||||
{
|
||||
ShowConsoleMsg(("ReaSampler capture: " + why + ".\n").c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
reasampler::CaptureRequest req;
|
||||
req.sourceMode = def.sourceMode;
|
||||
req.sourceMode = reasampler::sourceModeForScope(def.scope);
|
||||
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.wetDry = 1.0; // wet post the FX left enabled by the scope
|
||||
req.renderTail = false; // exact bounds, no tail (default)
|
||||
req.tailMs = 0.0;
|
||||
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 (track captures)
|
||||
req.trackGuids = src.trackGuids; // recorded on the Sample (provenance)
|
||||
|
||||
// Bypass the out-of-scope FX for the duration of the render. Restored on EVERY
|
||||
// exit path below (RAII), including backend failures. proj = active project.
|
||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||
FxBypassGuard fxGuard(def.scope, src.sourceTracks, proj);
|
||||
|
||||
reasampler::OfflineRenderBackend backend;
|
||||
reasampler::CaptureResult res = backend.capture(req);
|
||||
@@ -332,7 +419,7 @@ static void RunInsertSelected(bool conform)
|
||||
static bool OnHookCommand(int command, int /*flag*/)
|
||||
{
|
||||
if (command == 0) return false;
|
||||
// M7 capture family: command ids parallel captureActionTable() 1:1 by index.
|
||||
// Three-scope 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])
|
||||
@@ -388,7 +475,7 @@ 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"));
|
||||
// Mirror-unregister the M7 capture family: gaccel + command_id per row,
|
||||
// Mirror-unregister the 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.
|
||||
{
|
||||
@@ -400,6 +487,10 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
||||
g_rec->Register("-command_id", (void*)table[i].commandString);
|
||||
}
|
||||
}
|
||||
// Retire the removed M7 command ids (command_id only — we never held a
|
||||
// gaccel for them this session). Clears stale user keybindings on unload.
|
||||
for (const char* id : kRetiredCaptureCmdStrings)
|
||||
g_rec->Register("-command_id", (void*)id);
|
||||
}
|
||||
// Destroy the docked window and release cached thumbnails before we drop
|
||||
// the API pointers (DockWindowRemove/DestroyWindow need them live).
|
||||
@@ -420,7 +511,7 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
||||
g_hInst = hInstance;
|
||||
g_rec = rec;
|
||||
|
||||
// Register the M7 capture action family (command_id -> gaccel per table row).
|
||||
// Register the three-scope 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.
|
||||
|
||||
Reference in New Issue
Block a user