feat(view): D2 Wave 2 — apply managed-lane ops + timer-diff auto-tag new content

Drives fixed-lane C_LANEPLAYS for managed lanes only (name-keyed, ordinal-renumber
safe) in the view shell; diffs live track/item GUIDs on the panel timer to auto-tag
new content to the active mode, first-poll-guarded. Pure guid_diff + lane_keys
modules unit-tested; folds in Wave-1 polish.
This commit is contained in:
2026-07-23 17:32:22 -04:00
parent af5f7ee912
commit af34143107
11 changed files with 773 additions and 4 deletions
+128
View File
@@ -10,10 +10,15 @@
#include "view.h"
#include <cstdio>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "lane_keys.h"
#include "track_guid.h"
#include "view_tree.h"
@@ -22,19 +27,29 @@
#define REAPERAPI_WANT_GetTrack
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
#define REAPERAPI_WANT_TrackFX_GetCount
#define REAPERAPI_WANT_TrackFX_GetOffline
#define REAPERAPI_WANT_TrackFX_SetOffline
#define REAPERAPI_WANT_CountTrackMediaItems
#define REAPERAPI_WANT_GetTrackMediaItem
#define REAPERAPI_WANT_GetMediaItemInfo_Value
#define REAPERAPI_WANT_SetMediaItemInfo_Value
#define REAPERAPI_WANT_Undo_BeginBlock2
#define REAPERAPI_WANT_Undo_EndBlock2
#define REAPERAPI_WANT_TrackList_AdjustWindows
#define REAPERAPI_WANT_UpdateArrange
#define REAPERAPI_WANT_UpdateTimeline
#include "reaper_plugin_functions.h"
namespace reasampler {
namespace {
// Track fixed-lane mode value (I_FREEMODE=2). See SDK: 0=normal, 1=free item
// positioning, 2=fixed lanes.
constexpr int kFreeModeFixedLanes = 2;
// The parmname for each planner Flag. All four are documented bool*/int* track
// info params driven through the double-valued Get/SetMediaTrackInfo_Value API.
const char* flagParm(Flag f) {
@@ -132,6 +147,103 @@ void restoreFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& fxOffline)
}
}
// -- Managed-lane application (D2 Wave 2) ------------------------------------
//
// The pure planner emits LanePlayOps keyed by (trackGuid, laneKey) where laneKey is
// the lane's DURABLE name (lane_keys convention: "reasampler:<mode>"). REAPER's
// C_LANEPLAYS:N is keyed by the lane's CURRENT ORDINAL, which renumbers on reorder.
// So before applying, we build the ordinal<->key reconcile for a track by reading each
// lane's P_LANENAME:n; the write then targets the correct current ordinal for a given
// durable key even after a reorder (design point #2). A lane whose name lacks the
// managed prefix is manual and never appears in this map, so it can never be driven.
// Reads lane index `laneIdx`'s durable name off track `tr` (P_LANENAME:n). Empty if
// the lane is unnamed or the param is unavailable (non-fixed-lane track).
std::string laneName(MediaTrack* tr, int laneIdx) {
char parm[32];
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
char buf[512] = {0};
if (!GetSetMediaTrackInfo_String(tr, parm, buf, false)) return {};
return std::string(buf);
}
// Maps each MANAGED lane's durable key -> its current ordinal on `tr`, by walking the
// track's I_NUMFIXEDLANES lanes and reading each name. Manual (unprefixed/unnamed)
// lanes are omitted, so a key absent from the map is a lane the tool must not drive.
std::map<std::string, int> managedLaneOrdinals(MediaTrack* tr) {
std::map<std::string, int> byKey;
const int numLanes = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
for (int lane = 0; lane < numLanes; ++lane) {
std::optional<std::string> key = managedLaneKey(laneName(tr, lane));
if (key) byKey.emplace(*key, lane); // first ordinal wins if names collide
}
return byKey;
}
// Drives one managed lane on `tr` to `lanePlays` (C_LANEPLAYS value): the track-side
// C_LANEPLAYS:N for lane ordinal `laneIdx`, plus every ITEM on that lane (item-side
// C_LANEPLAYS). Items are matched to the lane by their read-only I_FIXEDLANE ordinal.
// B_FIXEDLANE_HIDDEN is READ-ONLY (SDK) — hide/show follows from C_LANEPLAYS=0/1, never
// written directly. Non-destructive: only reversible play/show flags; no item is moved
// or deleted.
void applyLanePlays(MediaTrack* tr, int laneIdx, int lanePlays) {
char parm[32];
std::snprintf(parm, sizeof(parm), "C_LANEPLAYS:%d", laneIdx);
SetMediaTrackInfo_Value(tr, parm, static_cast<double>(lanePlays));
const int itemCount = CountTrackMediaItems(tr);
for (int i = 0; i < itemCount; ++i) {
MediaItem* it = GetTrackMediaItem(tr, i);
if (!it) continue;
const int itemLane = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
if (itemLane != laneIdx) continue; // item is on a different lane
SetMediaItemInfo_Value(it, "C_LANEPLAYS", static_cast<double>(lanePlays));
}
}
// Applies the plan's managed-lane ops. Groups ops by track, resolves each op's durable
// laneKey to the track's current ordinal (skipping any key not present on the live
// track — a stale/renamed/deleted managed lane is pruned, never mis-driven), enables
// fixed-lane mode on any track that carries a managed lane, and drives C_LANEPLAYS.
// UpdateTimeline() is called ONCE at the end (SDK: required after I_FREEMODE changes).
// Returns true if any track's I_FREEMODE was (re)set to fixed lanes (⇒ needs timeline
// refresh). MANAGED lanes only — plan.lanes never contains a manual lane (pure planner
// gates on the ownership index), and a manual lane's name never resolves to a key here,
// so the invariant is enforced twice.
bool applyLaneOps(const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid,
const std::vector<LanePlayOp>& lanes) {
if (lanes.empty()) return false;
// Group op indices by track guid so we read each track's lane map once.
std::map<std::string, std::vector<const LanePlayOp*>> byTrack;
for (const LanePlayOp& op : lanes) byTrack[op.trackGuid].push_back(&op);
bool touchedFreeMode = false;
for (const auto& [guid, ops] : byTrack) {
MediaTrack* tr = resolve(handleByGuid, guid);
if (!tr) continue; // stale GUID — prune
// Ensure fixed-lane mode is on before driving lane play state. A track carrying
// a managed lane must be in I_FREEMODE=2; set it only if not already, and flag
// that a timeline refresh is owed.
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
if (freeMode != kFreeModeFixedLanes) {
SetMediaTrackInfo_Value(tr, "I_FREEMODE",
static_cast<double>(kFreeModeFixedLanes));
touchedFreeMode = true;
}
// Reconcile durable keys -> current ordinals on THIS track, then drive each op.
const std::map<std::string, int> ordinals = managedLaneOrdinals(tr);
for (const LanePlayOp* op : ops) {
auto it = ordinals.find(op->laneKey);
if (it == ordinals.end()) continue; // key not live on this track — prune
applyLanePlays(tr, it->second, op->lanePlays);
}
}
return touchedFreeMode;
}
} // namespace
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
@@ -194,6 +306,16 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
model.clearSnapshot(guid);
}
// MANAGED LANES (D2 item-level projection): drive C_LANEPLAYS so the active mode's
// managed lane plays+shows and every inactive-mode managed lane is silenced+hidden.
// plan.lanes carries MANAGED lanes only (the pure planner gates on the ownership
// index); applyLaneOps additionally resolves each op's durable key against the live
// track's lane names, so a manual lane — which never carries the managed prefix —
// can never be driven. Empty for a D1-only project (no fixed lanes), leaving D1
// behavior byte-identical. UpdateTimeline() is owed only if a track's I_FREEMODE
// was (re)set to fixed lanes (SDK requirement); deferred to the refresh block below.
const bool laneModeChanged = applyLaneOps(handleByGuid, plan.lanes);
// PARENT VISIBILITY (never parked): visibleTracks() marks a parent visible when
// a descendant leaf is visible in the target mode OR the parent belongs to the
// mode by its own membership (untagged folder → Arrange default). Recomputed
@@ -228,6 +350,12 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
TrackList_AdjustWindows(false);
UpdateArrange();
// A fixed-lane mode change (I_FREEMODE -> 2) requires UpdateTimeline() to take
// visible effect (SDK). Call it only when we actually toggled a track into fixed
// lanes this apply; the C_LANEPLAYS writes themselves are picked up by the arrange
// refresh above.
if (laneModeChanged) UpdateTimeline();
Undo_EndBlock2(proj, undoLabel.c_str(), -1);
return true;
}