// See view.h. Compiled into the reaper_reasampler module; includes // reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that). // Tree arithmetic lives in view_tree (pure) and one track's park/restore in // view_fx_park; this file owns the plan iteration, lanes, parents and the one // undo block they all ride. #include "shell/view/view.h" #include #include #include #include #include #include #include #include #include "shell/capture/item_read.h" #include "core/view/fx_offline.h" #include "core/view/lane_keys.h" #include "core/view/solo_cache.h" #include "shell/capture/track_guid.h" #include "shell/view/view_fx_park.h" #include "shell/view/view_solo.h" #include "core/view/view_tree.h" #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_CountTracks #define REAPERAPI_WANT_EnumProjects #define REAPERAPI_WANT_GetPlayStateEx #define REAPERAPI_WANT_GetTrack #define REAPERAPI_WANT_GetMediaTrackInfo_Value #define REAPERAPI_WANT_SetMediaTrackInfo_Value #define REAPERAPI_WANT_GetSetMediaTrackInfo_String #define REAPERAPI_WANT_PreventUIRefresh #define REAPERAPI_WANT_Undo_BeginBlock2 #define REAPERAPI_WANT_Undo_EndBlock2 #define REAPERAPI_WANT_TrackList_AdjustWindows #define REAPERAPI_WANT_UpdateArrange #define REAPERAPI_WANT_UpdateTimeline #define REAPERAPI_WANT_ValidatePtr2 // Lane minting: item-side lane reads/writes assigning each item to its mode's lane. #define REAPERAPI_WANT_CountTrackMediaItems #define REAPERAPI_WANT_GetTrackMediaItem #define REAPERAPI_WANT_GetMediaItemInfo_Value #define REAPERAPI_WANT_SetMediaItemInfo_Value #include "reaper_plugin_functions.h" namespace reasampler { using view::buildFolderTree; using view::isOnManualLane; using view::managedLaneKey; using view::modeIdFromLaneName; using view::TrackFolderEntry; namespace { // I_FREEMODE value for fixed lanes. SDK: 0=normal, 1=free item positioning, 2=fixed lanes. constexpr int kFreeModeFixedLanes = 2; // GetPlayStateEx bitmask. SDK: &1 playing, &2 paused, &4 recording — paused is // deliberately excluded, nothing is moving there. constexpr int kTransportMoving = 1 | 4; // The domains one apply actually writes: track config/routing (visibility, // B_MAINSEND, I_SOLO), item lane assignment, and the ext-state block // (UNDO_STATE_MISCCFG covers extension state). NOT UNDO_STATE_ALL, which // includes UNDO_STATE_FX and so makes every undo record carry every track's FX // state — a cost that scales with the project's plugin count rather than with // what the switch changed. FX is OR'd back in per apply, only when I_FXEN or a // per-FX offline state really moved (TrackApplyResult::fxMoved). // [verify — DAW] INFERRED, not documented: UNDO_STATE_TRACKCFG reads // "track/master vol/pan/routing" (reaper_plugin.h:1541) and names neither // B_SHOWINTCP/B_SHOWINMIXER nor the fixed-lane properties (assumed to ride // UNDO_STATE_ITEMS, :1543). Wrong ⇒ a Ctrl-Z restores less than the switch did. constexpr int kApplyUndoMask = UNDO_STATE_TRACKCFG | UNDO_STATE_ITEMS | UNDO_STATE_MISCCFG; // Holds REAPER's UI refresh off for the write phase; the deliberate rebuild // (TrackList_AdjustWindows + UpdateArrange) runs after it releases. RAII because // an unbalanced pair leaves the user's UI frozen with no way back — the SDK's // own warning at reaper_plugin_functions.h:5581. struct UiRefreshHold { UiRefreshHold() { PreventUIRefresh(1); } ~UiRefreshHold() { PreventUIRefresh(-1); } UiRefreshHold(const UiRefreshHold&) = delete; UiRefreshHold& operator=(const UiRefreshHold&) = delete; }; // applyMode is reachable from the load tick, the actions and render_in_place, and its // TrackFX_SetOffline calls are assumed to pump the message loop — so any of them can // re-enter it mid-apply, planning against the same model the outer loop is iterating. // An inner restoreTrack consuming a snapshot the outer sits mid-park over leaves that // track flags-restored, chain-offline and snapshotless: refused forever. Fail closed. bool g_applying = false; struct ApplyLatch { ApplyLatch() { g_applying = true; } ~ApplyLatch() { g_applying = false; } ApplyLatch(const ApplyLatch&) = delete; ApplyLatch& operator=(const ApplyLatch&) = delete; }; // C_LANESCOLLAPSED=2: render a tool-split track like a normal single-lane // track showing only the playing lane (SDK: 1=collapsed, 2=hidden-lanes-exist // but displays as non-fixed-lane). constexpr int kLanesDisplayAsNormal = 2; // C_LANESETTINGS &32 = hide per-lane buttons; OR'd in, never clobbering the // mask. Deliberately NOT setting &1 (auto-remove empty lanes): the lazy-mint // decision never mints an empty lane, so &1 buys nothing and risks REAPER // silently removing a managed lane out from under the ownership index. constexpr int kLaneSettingsHideButtons = 32; // Makes a tool-split track's display read as an ordinary track. Gated by every // caller on the tool-driven transition INTO fixed lanes (freeMode != 2 before // the flip) — a track already in fixed-lane mode (the user's own) never // reaches this, so a user's comp-lane display prefs are never stomped. void applyTransparentLaneDisplay(MediaTrack* tr) { SetMediaTrackInfo_Value(tr, "C_LANESCOLLAPSED", static_cast(kLanesDisplayAsNormal)); const int settings = static_cast(GetMediaTrackInfo_Value(tr, "C_LANESETTINGS")); SetMediaTrackInfo_Value(tr, "C_LANESETTINGS", static_cast(settings | kLaneSettingsHideButtons)); } // The master track is not enumerated by GetTrack (index space excludes it), // so it can never enter the tree — the master-untouched invariant holds by // construction. std::vector readFolderEntries( ReaProject* proj, TrackHandles& handleByGuid) { std::vector entries; int count = CountTracks(proj); entries.reserve(static_cast(count)); handleByGuid.reserve(static_cast(count)); for (int i = 0; i < count; ++i) { MediaTrack* tr = GetTrack(proj, i); if (!tr) continue; std::string guid = guidString(tr); if (guid.empty()) continue; int depth = static_cast(GetMediaTrackInfo_Value(tr, "I_FOLDERDEPTH")); entries.push_back(TrackFolderEntry{guid, depth}); handleByGuid.emplace_back(guid, tr); } return entries; } // Every park, restore, parent and lane op resolves its GUID; a linear scan made // that O(T²) on the one path whose cost the user waits on. The vector form // survives beside it because view_solo's writers take one pass over it. using TrackByGuid = std::unordered_map; TrackByGuid indexHandles(const TrackHandles& handleByGuid) { TrackByGuid byGuid; byGuid.reserve(handleByGuid.size()); for (const auto& kv : handleByGuid) byGuid.emplace(kv.first, kv.second); return byGuid; } MediaTrack* resolve(const TrackByGuid& byGuid, const std::string& guid) { auto it = byGuid.find(guid); return it == byGuid.end() ? nullptr : it->second; // stale/deleted GUID — pruned by being skipped } // Lane `laneIdx`'s durable name (P_LANENAME:n) on `tr`, or empty if unnamed / // 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); } // Managed lane durable key -> current ordinal on `tr`. Manual lanes are // omitted, so a key absent from the map must not be driven. std::map managedLaneOrdinals(MediaTrack* tr) { std::map byKey; const int numLanes = static_cast(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES")); for (int lane = 0; lane < numLanes; ++lane) { std::optional key = managedLaneKey(laneName(tr, lane)); if (key) byKey.emplace(*key, lane); // first ordinal wins if names collide } return byKey; } // Track-side C_LANEPLAYS:N alone hides+silences every item on lane N (SDK: // item-side C_LANEPLAYS is read-only, so no per-item write exists or is // needed). B_FIXEDLANE_HIDDEN is also read-only — hide/show follows from // C_LANEPLAYS=0/1, never written directly. bool applyLanePlays(MediaTrack* tr, int laneIdx, int lanePlays) { char parm[32]; std::snprintf(parm, sizeof(parm), "C_LANEPLAYS:%d", laneIdx); return writeIfChanged(tr, parm, static_cast(lanePlays)); } // `freeMode` alone decides whether UpdateTimeline is owed; `wrote` is any project // write at all, which is what the apply's undo-point decision reads. struct LaneApplyResult { bool freeMode = false; bool wrote = false; }; // Groups ops by track, reconciles each op's durable laneKey to the track's // current ordinal (a stale/renamed/deleted key is pruned, never mis-driven), // enables fixed-lane mode on any track carrying a managed lane, and drives // C_LANEPLAYS. UpdateTimeline() is the caller's job when `freeMode` comes back // true (SDK: required after an I_FREEMODE change). LaneApplyResult applyLaneOps(const TrackByGuid& handleByGuid, const std::vector& lanes) { LaneApplyResult out; if (lanes.empty()) return out; std::map> byTrack; for (const LanePlayOp& op : lanes) byTrack[op.trackGuid].push_back(&op); for (const auto& [guid, ops] : byTrack) { MediaTrack* tr = resolve(handleByGuid, guid); if (!tr) continue; // stale GUID — prune // Every track reaching here already owns a managed lane (planToggle // only emits ops for managed lanes), so re-asserting fixed-lane mode // is always a tool-driven (re)split — never a user's untouched // manual-fixed-lane track — and gets the same transparent display. const int freeMode = static_cast(GetMediaTrackInfo_Value(tr, "I_FREEMODE")); if (freeMode != kFreeModeFixedLanes) { SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast(kFreeModeFixedLanes)); applyTransparentLaneDisplay(tr); out.freeMode = true; out.wrote = true; } const std::map 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 if (applyLanePlays(tr, it->second, op->lanePlays)) out.wrote = true; } } return out; } // One pass, so the assign loop needs no per-item re-scan. std::map itemHandlesByGuid(MediaTrack* tr) { std::map byGuid; const int itemCount = CountTrackMediaItems(tr); for (int i = 0; i < itemCount; ++i) { MediaItem* it = GetTrackMediaItem(tr, i); if (!it) continue; std::string ig = itemGuid(it); if (!ig.empty()) byGuid.emplace(std::move(ig), it); } return byGuid; } // An untagged item is Arrange by default (mirrors leafBelongsToMode). A // show-both/multi-mode item resolves to its first mode id — unusual for lane // content, and any one mode is sufficient for the decision. std::string itemModeFromMembership(const ViewModeModel& model, const std::string& itemGuid) { const std::set modes = model.membership().modesOf(itemGuid); if (modes.empty()) return kArrangeModeId; return *modes.begin(); } // The per-track LaneItem picture the pure decision consumes. Manual-lane reads // are skipped on a non-fixed-lane track (isOnManualLane is false there anyway). std::vector readLaneTracks( const ViewModeModel& model, const TrackHandles& handleByGuid) { std::vector tracks; tracks.reserve(handleByGuid.size()); for (const auto& [guid, tr] : handleByGuid) { if (!tr) continue; // TrackHandles legally carries nulls post-validation (view.cpp) LaneTrack lt; lt.trackGuid = guid; const bool fixedLane = static_cast(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes; const int itemCount = CountTrackMediaItems(tr); lt.items.reserve(static_cast(itemCount)); for (int i = 0; i < itemCount; ++i) { MediaItem* it = GetTrackMediaItem(tr, i); if (!it) continue; const std::string ig = itemGuid(it); if (ig.empty()) continue; LaneItem li; li.guid = ig; li.modeId = itemModeFromMembership(model, ig); const std::string ln = fixedLane ? itemLaneName(tr, it) : std::string{}; li.onManualLane = isOnManualLane(fixedLane, ln); lt.items.push_back(std::move(li)); } tracks.push_back(std::move(lt)); } return tracks; } // Idempotent, and non-destructive: only this reversible flag is written, never // a move in time or across tracks. bool assignItemToLane(MediaTrack* tr, MediaItem* it, int laneOrdinal) { const int current = static_cast(GetMediaItemInfo_Value(it, "I_FIXEDLANE")); if (current == laneOrdinal) return false; // already there — no-op SetMediaItemInfo_Value(it, "I_FIXEDLANE", static_cast(laneOrdinal)); return true; } // Applies the pure LaneMintPlan. Returns true if any project write actually // changed state (⇒ caller keeps the Undo block and refreshes the timeline). // // The plan only ever names managed-prefixed lanes and only ever assigns // managed-eligible items; I_NUMFIXEDLANES is only ever GROWN, never shrunk, // so a user's existing manual lanes are never renamed or reassigned. bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan, const TrackByGuid& handleByGuid) { bool changed = false; std::map> mintsByTrack; for (const LaneMint& m : plan.mints) mintsByTrack[m.trackGuid].push_back(&m); std::map> assignsByTrack; for (const LaneAssign& a : plan.assigns) assignsByTrack[a.trackGuid].push_back(&a); for (const LaneMintPlan::TrackSplit& split : plan.splits) { MediaTrack* tr = resolve(handleByGuid, split.trackGuid); if (!tr) continue; // stale GUID — prune // A track not already in fixed-lane mode is one the tool is splitting // now, so it owns the display; a track already at I_FREEMODE==2 (the // user's own, or a prior tool run) skips this and keeps its display prefs. const int freeMode = static_cast(GetMediaTrackInfo_Value(tr, "I_FREEMODE")); if (freeMode != kFreeModeFixedLanes) { SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast(kFreeModeFixedLanes)); applyTransparentLaneDisplay(tr); changed = true; } // Grow-only: a track may already carry the user's manual lanes, so the // lane count only ever increases; managed lanes occupy the tail ordinals. int laneCount = static_cast(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES")); std::map present = managedLaneOrdinals(tr); for (const LaneMint* m : mintsByTrack[split.trackGuid]) { model.lanes().setManaged(m->trackGuid, m->laneKey, m->modeId); // ownership if (present.count(m->laneKey)) continue; // already minted — idempotent const int laneIdx = laneCount++; SetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES", static_cast(laneCount)); char parm[32]; std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx); std::vector name(m->laneKey.begin(), m->laneKey.end()); name.push_back('\0'); GetSetMediaTrackInfo_String(tr, parm, name.data(), true); present.emplace(m->laneKey, laneIdx); // now resolvable for the assign pass changed = true; } const std::map ordinals = managedLaneOrdinals(tr); const std::map itemsByGuid = itemHandlesByGuid(tr); for (const LaneAssign* a : assignsByTrack[split.trackGuid]) { auto ord = ordinals.find(a->laneKey); if (ord == ordinals.end()) continue; // key not live — prune, never mis-assign auto handle = itemsByGuid.find(a->itemGuid); if (handle == itemsByGuid.end()) continue; // stale item GUID — prune if (assignItemToLane(tr, handle->second, ord->second)) changed = true; } } return changed; } } // namespace bool transportBlocksModeSwitch(ReaProject* proj) { return (GetPlayStateEx(proj) & kTransportMoving) != 0; } bool modeApplyInProgress() { return g_applying; } bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) { if (g_applying) return false; // see ApplyLatch; same fail-closed shape as the guards below ApplyLatch latch; if (!model.modes().contains(targetModeId)) { return false; // reject before touching the project — no partial apply } // Pinned ONCE: begin and end sit a whole apply apart with the pump premise between // them, so resolving "current project" independently at each would let a tab switch // mid-apply open the block on one project and close it on another. ReaProject* const project = proj ? proj : EnumProjects(-1, nullptr, 0); if (!ValidatePtr2(nullptr, project, "ReaProject*")) return false; // The discriminator behind both halves of the contract in view.h. A gate placed // unconditionally here would break tagging and the project-load reapply during // playback; a solo round on every reapply would flicker the user's solo on every // membership edit. const std::string outgoingModeId = model.activeModeId(); const bool realSwitch = targetModeId != outgoingModeId; if (realSwitch && transportBlocksModeSwitch(project)) { return false; // same fail-closed shape as the mode-exists guard above } TrackHandles handleByGuid; std::vector entries = readFolderEntries(project, handleByGuid); TrackByGuid trackByGuid = indexHandles(handleByGuid); FolderTree tree = buildFolderTree(entries); // Prune snapshots for tracks no longer in the live enumeration before // planning (membership is intentionally left alone — see model.reconcile). // Because reapply-on-load routes through applyMode, this also reconciles // on project open. std::set liveGuids; for (const auto& kv : handleByGuid) liveGuids.insert(kv.first); model.reconcile(liveGuids); // Read before any write, and while activeModeId() still names the mode being left. const std::map outgoingSolo = realSwitch ? view::soloedTracks(readTrackSolos(handleByGuid)) : std::map{}; TogglePlan plan = model.planToggle(tree, targetModeId); // Target is guaranteed registered (checked at entry); fall back to the id // defensively if that ever changes. const Mode* targetMode = model.modes().query(targetModeId); const std::string undoLabel = "ReaSampler: activate " + (targetMode ? targetMode->displayName : targetModeId) + " view"; int undoMask = kApplyUndoMask; bool laneModeChanged = false; bool wrote = false; // anything at all this apply changed — see applyMintsUndoPoint std::vector refusedParkNames; FxRestoreDrops fxDrops; int fxDropTracks = 0; Undo_BeginBlock2(project); { UiRefreshHold uiHold; // every write below lands with the TCP/MCP frozen // DISJOIN THE SOLO SURFACES. Both this clear and the replay after // setActiveMode ride the existing undo block — one mode toggle stays one Ctrl-Z. if (realSwitch) { model.soloCache().store(outgoingModeId, outgoingSolo); clearTrackSolos(handleByGuid, outgoingSolo); } // PARK / RESTORE: view_fx_park owns one track's whole move, flags and // per-FX offline together. A refusal is named HERE, while the handle is // fresh — never held past Undo_EndBlock2/TrackList_AdjustWindows. for (const TrackPlan& tp : plan.park) { if (tp.flags.empty()) continue; // every op in a TrackPlan targets one track const std::string& guid = tp.flags.front().guid; MediaTrack* tr = resolve(trackByGuid, guid); // Re-validated per track, not once at resolve: offlining a plugin is // ASSUMED to pump the message loop, so a track deleted during an // earlier iteration's FX writes would leave this handle dangling. if (!tr || !trackStillLive(project, tr)) continue; // stale/deleted GUID — prune const TrackApplyResult r = parkTrack(model, guid, tr, tp.flags); if (r.refused) { refusedParkNames.push_back(trackDisplayName(tr)); continue; } if (r.wrote) wrote = true; if (r.fxMoved) undoMask |= UNDO_STATE_FX; } for (const TrackPlan& tp : plan.restore) { if (tp.flags.empty()) continue; const std::string& guid = tp.flags.front().guid; MediaTrack* tr = resolve(trackByGuid, guid); if (!tr || !trackStillLive(project, tr)) continue; // stale/deleted GUID — prune const TrackApplyResult r = restoreTrack(model, guid, tr, tp.fxOffline, tp.flags); if (r.wrote) wrote = true; if (r.fxMoved) undoMask |= UNDO_STATE_FX; if (r.drops.total() > 0) { fxDrops.add(r.drops); ++fxDropTracks; } } // The FX writes above are the only thing here assumed to pump the message // loop, so this is the one point a handle taken before them can have died. // Re-validated for the whole enumeration rather than per consumer: lane ops, // parent visibility and the solo replay all write through pre-FX handles. for (auto& kv : handleByGuid) { if (trackStillLive(project, kv.second)) continue; trackByGuid.erase(kv.first); kv.second = nullptr; // view_solo's writers already skip a null handle } // MANAGED LANES: the active mode's lane plays+shows, every other managed // lane is silenced+hidden. Empty on a D1-only project — byte-identical there. const LaneApplyResult lanes = applyLaneOps(trackByGuid, plan.lanes); laneModeChanged = lanes.freeMode; if (lanes.wrote) wrote = true; // PARENT VISIBILITY (never parked): recomputed every toggle, never // snapshotted. Only the two visibility flags — never mainSend/FX on a parent. std::set visible = model.visibleTracks(tree, targetModeId); for (const FolderNode& node : tree.nodes) { if (!node.isParent) continue; MediaTrack* tr = resolve(trackByGuid, node.guid); if (!tr) continue; // stale GUID — prune const double show = visible.count(node.guid) ? 1.0 : 0.0; if (writeIfChanged(tr, "B_SHOWINTCP", show)) wrote = true; if (writeIfChanged(tr, "B_SHOWINMIXER", show)) wrote = true; } model.setActiveMode(targetModeId); // Replay + consume, before the single relayout below picks the change up. // Dropped against `visible` (computed above for parent visibility), not the // park plan: a folder parent can be hidden without being parked, and a // hidden track must not receive a replayed solo it carries no visible // control to undo. if (realSwitch) { if (const std::map* cached = model.soloCache().query(targetModeId)) { restoreTrackSolos(handleByGuid, *cached, liveGuids, visible); model.soloCache().clear(targetModeId); } } } // Force REAPER to rebuild the TCP/MCP now rather than on the next user // interaction — AdjustWindows(false) is the full relayout tracks appearing // and disappearing owes. TrackList_AdjustWindows(false); UpdateArrange(); // UpdateTimeline() is owed only when a track was actually toggled into // fixed lanes this apply (SDK requirement for I_FREEMODE changes). if (laneModeChanged) UpdateTimeline(); const bool mint = applyMintsUndoPoint(realSwitch, wrote); Undo_EndBlock2(project, mint ? undoLabel.c_str() : "", mint ? undoMask : 0); // After the undo block and the UI hold: a report is not a project write, and // it must not join what a Ctrl-Z rolls back. reportRefusedParks(project, refusedParkNames); reportFxRestoreDrops(fxDrops, fxDropTracks); return true; } bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) { TrackHandles handleByGuid; std::vector entries = readFolderEntries(proj, handleByGuid); // The tree is needed to detect a content-bearing folder derived-visible in // >1 mode, exactly as applyMode builds it. const FolderTree tree = buildFolderTree(entries); const std::vector tracks = readLaneTracks(model, handleByGuid); const LaneMintPlan plan = planLaneMinting(model, tree, tracks); if (plan.empty()) return false; // nothing to mint — no Undo point for a no-op tick const TrackByGuid trackByGuid = indexHandles(handleByGuid); // `proj` deliberately NOT pinned the way applyMode pins it: nothing between this // begin and its end pumps the message loop, so no tab switch can land between them. Undo_BeginBlock2(proj); const bool changed = applyMintPlan(model, plan, trackByGuid); if (!changed) { // Plan was non-empty but every write was already satisfied — discard // the empty undo point rather than flooding history every detect tick. Undo_EndBlock2(proj, "", 0); // The arrange still needs a redraw: this no-op path is reached when a // freshly-inserted item already landed on the active mode's playing // lane (REAPER places new items on the playing lane), so // assignItemToLane wrote nothing even though the item needs to appear // there now. UpdateArrange() alone (no I_FREEMODE change happened, so // UpdateTimeline isn't owed) is a repaint, not a mutation — stays // outside the undo block. UpdateArrange(); return false; } // Reapply the active mode's lane visibility so freshly-minted lanes take // their play/show state immediately. applyMode is deliberately NOT reused // here — it would re-park/restore whole tracks and recompute parent // visibility, which a lane-only mint must not touch. const TogglePlan togglePlan = model.planToggle(FolderTree{}, model.activeModeId()); applyLaneOps(trackByGuid, togglePlan.lanes); UpdateTimeline(); // a split happened this call — refresh is owed UpdateArrange(); // Lane minting writes track lane config and item lane assignment and nothing // else — never an FX state — so it takes the same honest mask applyMode does // (kApplyUndoMask, whose [verify — DAW] on lane-property domain binds here too). Undo_EndBlock2(proj, "ReaSampler: separate cross-mode content into lanes", kApplyUndoMask); return true; } void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj) { TrackHandles handleByGuid; readFolderEntries(proj, handleByGuid); // populates handleByGuid (tree unused here) // Pure read of REAPER state (no lane created, no I_FREEMODE/I_NUMFIXEDLANES/ // I_FIXEDLANE written) plus an ownership-index write, recovering managed // classification from the durable name. An unprefixed lane is left alone. for (const auto& [guid, tr] : handleByGuid) { const int freeMode = static_cast(GetMediaTrackInfo_Value(tr, "I_FREEMODE")); if (freeMode != kFreeModeFixedLanes) continue; // no fixed lanes ⇒ nothing managed const int numLanes = static_cast(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES")); for (int lane = 0; lane < numLanes; ++lane) { const std::string name = laneName(tr, lane); std::optional key = managedLaneKey(name); if (!key) continue; // manual/unnamed lane — leave off the index std::optional mode = modeIdFromLaneName(name); if (!mode) continue; // prefix-only/illegal name — skip defensively // A mode id encoded in the name may no longer be registered (e.g. // removed since the project was saved). Recording it managed would // make the toggle planner drive a lane keyed to a mode that can // never be active — permanently silenced, orphaning its items. So // skip: the lane stays off the index (manual-by-default) but keeps // its name, so a later re-registration of the mode heals cleanly. if (!model.modes().contains(*mode)) continue; model.lanes().setManaged(guid, *key, *mode); } } } } // namespace reasampler