fix(D2-W2): correct manual-lane exemption, drop read-only item C_LANEPLAYS write

isOnManualLane predicate now correctly treats unnamed fixed-lane lanes as manual;
item-side C_LANEPLAYS loop removed (SDK read-only); explicit #include <set> in
view_mode_model.cpp; undo-block omission in detectNewContent documented as intentional.
This commit is contained in:
2026-07-23 17:51:55 -04:00
parent af34143107
commit ebdb4eae66
6 changed files with 96 additions and 28 deletions
+34 -9
View File
@@ -566,13 +566,22 @@ bool refreshFingerprint() {
// enumeration is READ-ONLY on the project; the only mutation is to the in-memory // enumeration is READ-ONLY on the project; the only mutation is to the in-memory
// membership index (persisted by persist on the next save, same as an action-driven tag). // membership index (persisted by persist on the next save, same as an action-driven tag).
// The durable name of item `it`'s fixed lane, or empty if the item's track is not in // True iff `tr` has I_FREEMODE==2 (fixed lanes enabled). The SDK value is verified
// fixed-lane mode (⇒ not a lane at all, treated as non-manual normal content). Mirrors // in view.cpp (kFreeModeFixedLanes=2); reproduced here as a local constant so
// view.cpp's laneName but item-side: read the item's I_FIXEDLANE ordinal, then that // bank_panel.cpp stays self-contained without pulling in view.cpp's private namespace.
// lane's P_LANENAME:n off the owning track. constexpr int kFreeModeFixedLanes = 2;
bool isFixedLaneTrack(MediaTrack* tr) {
return static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes;
}
// Reads the durable P_LANENAME for the lane that item `it` sits on. Returns empty if
// the lane is unnamed or P_LANENAME is unavailable. Callers must already know the track
// is a fixed-lane track (I_FREEMODE==2) before calling this — the manual/non-manual
// distinction only applies there. On a non-fixed-lane track `I_FIXEDLANE` is
// meaningless; the isOnManualLane predicate handles that case via its isFixedLaneTrack
// argument, so callers should not call this at all for non-fixed-lane tracks.
std::string itemLaneName(MediaTrack* tr, MediaItem* it) { std::string itemLaneName(MediaTrack* tr, MediaItem* it) {
if (static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) != 2)
return {}; // not a fixed-lane track ⇒ no lane name (normal timeline content)
const int laneIdx = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE")); const int laneIdx = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
char parm[32]; char parm[32];
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx); std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
@@ -592,6 +601,10 @@ std::string itemGuid(MediaItem* it) {
// Enumerates the live project's track + item GUIDs. Fills `allGuids` (the full live set, // Enumerates the live project's track + item GUIDs. Fills `allGuids` (the full live set,
// baseline input) and, for each item, records whether it sits on a manual lane so a // baseline input) and, for each item, records whether it sits on a manual lane so a
// newly-detected item can be exempted from auto-tag without a second project walk. // newly-detected item can be exempted from auto-tag without a second project walk.
//
// Manual-lane classification uses the single pure predicate isOnManualLane(isFixedLaneTrack,
// laneName) from lane_keys — the same predicate the apply path consults — so the exemption
// rule is defined in exactly one place and is unit-tested there.
void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids, void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
std::map<std::string, bool>& itemOnManualLane) { std::map<std::string, bool>& itemOnManualLane) {
const int trackCount = CountTracks(proj); const int trackCount = CountTracks(proj);
@@ -601,6 +614,10 @@ void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
std::string tg = guidString(tr); std::string tg = guidString(tr);
if (!tg.empty()) allGuids.insert(tg); if (!tg.empty()) allGuids.insert(tg);
// Compute the fixed-lane status once per track (not per item) — I_FREEMODE is a
// track-level attribute and is the same for every item on the track.
const bool fixedLane = isFixedLaneTrack(tr);
const int itemCount = CountTrackMediaItems(tr); const int itemCount = CountTrackMediaItems(tr);
for (int i = 0; i < itemCount; ++i) { for (int i = 0; i < itemCount; ++i) {
MediaItem* it = GetTrackMediaItem(tr, i); MediaItem* it = GetTrackMediaItem(tr, i);
@@ -608,9 +625,11 @@ void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
std::string ig = itemGuid(it); std::string ig = itemGuid(it);
if (ig.empty()) continue; if (ig.empty()) continue;
allGuids.insert(ig); allGuids.insert(ig);
// Manual iff the item is on a fixed lane whose name is NOT tool-managed. // Classify via the single shared predicate. For a fixed-lane track we read
const std::string ln = itemLaneName(tr, it); // the item's lane name; for a normal track we pass "" (isOnManualLane returns
itemOnManualLane[ig] = (!ln.empty() && !isManagedLaneName(ln)); // false immediately for non-fixed-lane tracks regardless of name).
const std::string ln = fixedLane ? itemLaneName(tr, it) : std::string{};
itemOnManualLane[ig] = isOnManualLane(fixedLane, ln);
} }
} }
} }
@@ -619,6 +638,12 @@ void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
// into the active mode. Runs every timer tick regardless of panel open/close (content // into the active mode. Runs every timer tick regardless of panel open/close (content
// is created in the arrange). READ-ONLY on the project; mutates only the in-memory // is created in the arrange). READ-ONLY on the project; mutates only the in-memory
// membership index. // membership index.
//
// INTENTIONAL: membership mutation happens OUTSIDE any Undo block. Auto-tag is a
// background metadata update (like setting a label), not a destructive project edit.
// persist.cpp writes it on the next project save alongside the bank and view state, the
// same way an action-driven tag is persisted. Wrapping this in an Undo block would flood
// the REAPER undo history with a new entry for every timer tick that sees new content.
void detectNewContent() { void detectNewContent() {
if (!g_panel.session) return; if (!g_panel.session) return;
+10
View File
@@ -31,4 +31,14 @@ std::string laneNameForMode(const std::string& modeId) {
return std::string(kManagedLanePrefix) + modeId; return std::string(kManagedLanePrefix) + modeId;
} }
bool isOnManualLane(bool isFixedLaneTrack, const std::string& laneName) {
// On a normal (non-fixed-lane) track there is no concept of a manual lane; the
// item follows the normal auto-tag rule.
if (!isFixedLaneTrack) return false;
// On a fixed-lane track: a managed lane (prefixed) is NOT manual; everything else
// — including the empty/unnamed lane that REAPER creates by default — IS manual
// (user-minted, off-limits to auto-tag and to the lane-drive path).
return !hasManagedPrefix(laneName);
}
} // namespace reasampler } // namespace reasampler
+16
View File
@@ -58,4 +58,20 @@ std::optional<std::string> managedLaneKey(const std::string& laneName);
// contract is asserted here so minting and reading cannot drift. // contract is asserted here so minting and reading cannot drift.
std::string laneNameForMode(const std::string& modeId); std::string laneNameForMode(const std::string& modeId);
// True iff an item on a fixed-lane track with the given lane name is on a MANUAL lane
// (i.e. exempt from auto-tag). The two inputs are:
// isFixedLaneTrack — whether the item's track has I_FREEMODE==2. On a normal
// (non-fixed-lane) track the concept of a "manual lane" does not
// apply; the item follows the normal auto-tag rule (return false).
// laneName — the durable P_LANENAME of the lane the item sits on. A lane
// that carries kManagedLanePrefix is a tool-minted managed lane
// (not manual); any other name — including empty (unnamed) — is
// a user-minted manual lane (exempt from auto-tag).
//
// This is the SINGLE predicate that governs BOTH the apply path (which lanes may be
// driven) and the auto-tag exemption path (which items are exempt). It is unit-tested
// here so both paths share exactly one definition; the shell supplies the two REAPER
// inputs (I_FREEMODE result, P_LANENAME string) and never re-derives this logic.
bool isOnManualLane(bool isFixedLaneTrack, const std::string& laneName);
} // namespace reasampler } // namespace reasampler
+11 -19
View File
@@ -31,10 +31,6 @@
#define REAPERAPI_WANT_TrackFX_GetCount #define REAPERAPI_WANT_TrackFX_GetCount
#define REAPERAPI_WANT_TrackFX_GetOffline #define REAPERAPI_WANT_TrackFX_GetOffline
#define REAPERAPI_WANT_TrackFX_SetOffline #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_BeginBlock2
#define REAPERAPI_WANT_Undo_EndBlock2 #define REAPERAPI_WANT_Undo_EndBlock2
#define REAPERAPI_WANT_TrackList_AdjustWindows #define REAPERAPI_WANT_TrackList_AdjustWindows
@@ -180,25 +176,21 @@ std::map<std::string, int> managedLaneOrdinals(MediaTrack* tr) {
return byKey; return byKey;
} }
// Drives one managed lane on `tr` to `lanePlays` (C_LANEPLAYS value): the track-side // Drives one managed lane on `tr` to `lanePlays` (C_LANEPLAYS value) via the
// C_LANEPLAYS:N for lane ordinal `laneIdx`, plus every ITEM on that lane (item-side // TRACK-SIDE C_LANEPLAYS:N write. Track-side C_LANEPLAYS:N alone produces the
// C_LANEPLAYS). Items are matched to the lane by their read-only I_FIXEDLANE ordinal. // hide+silence effect for all items on lane N — no per-item write is needed or
// B_FIXEDLANE_HIDDEN is READ-ONLY (SDK) — hide/show follows from C_LANEPLAYS=0/1, never // possible (item-side C_LANEPLAYS is marked read-only in the SDK).
// written directly. Non-destructive: only reversible play/show flags; no item is moved // B_FIXEDLANE_HIDDEN is READ-ONLY (SDK) — hide/show follows from C_LANEPLAYS=0/1,
// or deleted. // never written directly. Non-destructive: only reversible play/show flags; no item
// is moved or deleted.
//
// DAW-VERIFY: confirm that track-side C_LANEPLAYS:N alone hides+silences all items
// on lane N without a per-item write. (SDK marks item-side C_LANEPLAYS as read-only;
// the track-side write is the documented mechanism.)
void applyLanePlays(MediaTrack* tr, int laneIdx, int lanePlays) { void applyLanePlays(MediaTrack* tr, int laneIdx, int lanePlays) {
char parm[32]; char parm[32];
std::snprintf(parm, sizeof(parm), "C_LANEPLAYS:%d", laneIdx); std::snprintf(parm, sizeof(parm), "C_LANEPLAYS:%d", laneIdx);
SetMediaTrackInfo_Value(tr, parm, static_cast<double>(lanePlays)); 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 // Applies the plan's managed-lane ops. Groups ops by track, resolves each op's durable
+1
View File
@@ -6,6 +6,7 @@
#include <climits> #include <climits>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <set>
#include <utility> #include <utility>
// view_mode_model implementation. // view_mode_model implementation.
+24
View File
@@ -46,6 +46,29 @@ static void testManagedLaneKey() {
CHECK(!managedLaneKey("guitar-double").has_value()); CHECK(!managedLaneKey("guitar-double").has_value());
} }
static void testIsOnManualLane() {
// Non-fixed-lane track: concept does not apply regardless of name.
CHECK(!isOnManualLane(false, "")); // normal track, unnamed ⇒ not manual
CHECK(!isOnManualLane(false, "Comp 1")); // normal track, user name ⇒ not manual
CHECK(!isOnManualLane(false, "reasampler:design")); // normal track, managed name ⇒ not manual
// Fixed-lane track: managed lane (tool-prefixed) ⇒ NOT manual (tool drives it).
CHECK(!isOnManualLane(true, "reasampler:design"));
CHECK(!isOnManualLane(true, "reasampler:arrange"));
CHECK(!isOnManualLane(true, "reasampler:")); // prefix-only: still managed
// Fixed-lane track: unnamed lane (empty P_LANENAME) ⇒ manual.
// REAPER starts fixed lanes unnamed; an item on an unnamed fixed lane is a user
// comp lane and must be exempt from auto-tag.
CHECK(isOnManualLane(true, ""));
// Fixed-lane track: user-named but non-managed ⇒ manual.
CHECK(isOnManualLane(true, "Comp 1"));
CHECK(isOnManualLane(true, "Lead vocal"));
CHECK(isOnManualLane(true, "reasample")); // near-miss, no colon ⇒ manual
CHECK(isOnManualLane(true, "Reasampler:x")); // wrong case ⇒ manual
}
static void testRoundTrip() { static void testRoundTrip() {
// Minting then reading must agree: managedLaneKey(laneNameForMode(m)) recovers the // Minting then reading must agree: managedLaneKey(laneNameForMode(m)) recovers the
// prefixed name for every mode id. // prefixed name for every mode id.
@@ -62,6 +85,7 @@ static void testRoundTrip() {
int main() { int main() {
testIsManagedLaneName(); testIsManagedLaneName();
testManagedLaneKey(); testManagedLaneKey();
testIsOnManualLane();
testRoundTrip(); testRoundTrip();
if (g_fail == 0) std::printf("All tests passed.\n"); if (g_fail == 0) std::printf("All tests passed.\n");