Strip FX writes from the Design View mode switch; split view.cpp into park/lanes/reconcile/tracks and add the record-scoped undo repair pass
This commit is contained in:
+48
-465
@@ -1,98 +1,62 @@
|
||||
// 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.
|
||||
// Tree arithmetic lives in view_tree (pure), one track's park/restore in view_park
|
||||
// and the lane writes in view_lanes; this file owns the plan iteration, parent
|
||||
// visibility, and the one undo block they all ride.
|
||||
|
||||
#include "shell/view/view.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "shell/capture/item_read.h"
|
||||
#include "core/view/fx_offline.h"
|
||||
#include "core/view/fx_state.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"
|
||||
#include "shell/view/view_lanes.h"
|
||||
#include "shell/view/view_park.h"
|
||||
#include "shell/view/view_reconcile.h"
|
||||
#include "shell/view/view_solo.h"
|
||||
#include "shell/view/view_tracks.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.
|
||||
// B_MAINSEND, I_FXEN, I_SOLO), item lane assignment, and the ext-state block
|
||||
// (UNDO_STATE_MISCCFG covers extension state). NOT UNDO_STATE_ALL, and no
|
||||
// UNDO_STATE_FX at all — the apply moves no FX state, so making every undo record
|
||||
// carry the project's whole FX chunk would cost with the plugin count for nothing.
|
||||
// [verify — DAW] INFERRED, not documented: UNDO_STATE_TRACKCFG reads "track/master
|
||||
// vol/pan/routing" (reaper_plugin.h:1541) and names neither the fixed-lane properties
|
||||
// (assumed to ride UNDO_STATE_ITEMS, :1543) nor B_SHOWINTCP/B_SHOWINMIXER — and for
|
||||
// the visibility pair that inference is now DISPROVEN in the DAW: REAPER's undo does
|
||||
// not restore them, which is what view_reconcile exists to repair.
|
||||
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.
|
||||
// applyMode is reachable from the load tick, the actions and render_in_place. Nothing
|
||||
// it calls is known to pump the message loop now that the FX writes are gone, but a
|
||||
// re-entrant apply would plan against the same model the outer loop is iterating and
|
||||
// consume records out from under it — one dropped call is cheaper than that. Fail closed.
|
||||
bool g_applying = false;
|
||||
struct ApplyLatch {
|
||||
ApplyLatch() { g_applying = true; }
|
||||
@@ -101,275 +65,6 @@ struct ApplyLatch {
|
||||
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<double>(kLanesDisplayAsNormal));
|
||||
const int settings = static_cast<int>(GetMediaTrackInfo_Value(tr, "C_LANESETTINGS"));
|
||||
SetMediaTrackInfo_Value(tr, "C_LANESETTINGS",
|
||||
static_cast<double>(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<TrackFolderEntry> readFolderEntries(
|
||||
ReaProject* proj,
|
||||
TrackHandles& handleByGuid) {
|
||||
std::vector<TrackFolderEntry> entries;
|
||||
int count = CountTracks(proj);
|
||||
entries.reserve(static_cast<std::size_t>(count));
|
||||
handleByGuid.reserve(static_cast<std::size_t>(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<int>(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<std::string, MediaTrack*>;
|
||||
|
||||
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<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;
|
||||
}
|
||||
|
||||
// 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<double>(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<LanePlayOp>& lanes) {
|
||||
LaneApplyResult out;
|
||||
if (lanes.empty()) return out;
|
||||
|
||||
std::map<std::string, std::vector<const LanePlayOp*>> 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<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||
if (freeMode != kFreeModeFixedLanes) {
|
||||
SetMediaTrackInfo_Value(tr, "I_FREEMODE",
|
||||
static_cast<double>(kFreeModeFixedLanes));
|
||||
applyTransparentLaneDisplay(tr);
|
||||
out.freeMode = true;
|
||||
out.wrote = true;
|
||||
}
|
||||
|
||||
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
|
||||
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<std::string, MediaItem*> itemHandlesByGuid(MediaTrack* tr) {
|
||||
std::map<std::string, MediaItem*> 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<std::string> 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<LaneTrack> readLaneTracks(
|
||||
const ViewModeModel& model,
|
||||
const TrackHandles& handleByGuid) {
|
||||
std::vector<LaneTrack> 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<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes;
|
||||
|
||||
const int itemCount = CountTrackMediaItems(tr);
|
||||
lt.items.reserve(static_cast<std::size_t>(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<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
|
||||
if (current == laneOrdinal) return false; // already there — no-op
|
||||
SetMediaItemInfo_Value(it, "I_FIXEDLANE", static_cast<double>(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<std::string, std::vector<const LaneMint*>> mintsByTrack;
|
||||
for (const LaneMint& m : plan.mints) mintsByTrack[m.trackGuid].push_back(&m);
|
||||
std::map<std::string, std::vector<const LaneAssign*>> 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<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||
if (freeMode != kFreeModeFixedLanes) {
|
||||
SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast<double>(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<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
||||
|
||||
std::map<std::string, int> 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<double>(laneCount));
|
||||
char parm[32];
|
||||
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
|
||||
std::vector<char> 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<std::string, int> ordinals = managedLaneOrdinals(tr);
|
||||
const std::map<std::string, MediaItem*> 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) {
|
||||
@@ -386,9 +81,9 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
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.
|
||||
// Pinned ONCE: resolving "current project" independently at the begin and the end
|
||||
// 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;
|
||||
|
||||
@@ -405,13 +100,12 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
|
||||
TrackHandles handleByGuid;
|
||||
std::vector<TrackFolderEntry> entries = readFolderEntries(project, handleByGuid);
|
||||
TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
||||
const 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.
|
||||
// Prune records 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<std::string> liveGuids;
|
||||
for (const auto& kv : handleByGuid) liveGuids.insert(kv.first);
|
||||
model.reconcile(liveGuids);
|
||||
@@ -430,12 +124,8 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
"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<std::string> refusedParkNames;
|
||||
FxRestoreDrops fxDrops;
|
||||
int fxDropTracks = 0;
|
||||
|
||||
Undo_BeginBlock2(project);
|
||||
{
|
||||
@@ -448,55 +138,32 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
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.
|
||||
// PARK / RESTORE: view_park owns one track's whole move. Flags only — the
|
||||
// parked chain stays online until the reclaimer reaches it.
|
||||
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;
|
||||
if (!tr) continue; // stale/deleted GUID — prune
|
||||
if (parkTrack(model, guid, tr, tp.flags)) wrote = true;
|
||||
}
|
||||
|
||||
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 std::vector<FxOfflineOp> fxOps = view::fxRestoreTargets(model, guid);
|
||||
const TrackApplyResult r = restoreTrack(model, guid, tr, fxOps, tp.flags);
|
||||
if (r.wrote) wrote = true;
|
||||
if (r.fxMoved) undoMask |= UNDO_STATE_FX;
|
||||
if (r.drops.total() > 0) { fxDrops.add(r.drops); ++fxDropTracks; }
|
||||
if (!tr) continue; // stale/deleted GUID — prune
|
||||
if (restoreTrack(model, guid, tr, tp.flags)) wrote = true;
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
// PARENT VISIBILITY (never parked): recomputed every toggle, never recorded.
|
||||
// Only the two visibility flags — never mainSend/FX on a parent.
|
||||
std::set<std::string> visible = model.visibleTracks(tree, targetModeId);
|
||||
for (const FolderNode& node : tree.nodes) {
|
||||
if (!node.isParent) continue;
|
||||
@@ -511,9 +178,8 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
|
||||
// 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.
|
||||
// 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<std::string, int>* cached = model.soloCache().query(targetModeId)) {
|
||||
restoreTrackSolos(handleByGuid, *cached, liveGuids, visible);
|
||||
@@ -523,105 +189,22 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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).
|
||||
// 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);
|
||||
Undo_EndBlock2(project, mint ? undoLabel.c_str() : "", mint ? kApplyUndoMask : 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);
|
||||
// The tool's own account of what it just left driven — the only one that survives
|
||||
// an undo rolling the model back underneath it.
|
||||
noteAppliedState(project, model);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||
TrackHandles handleByGuid;
|
||||
std::vector<TrackFolderEntry> 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<LaneTrack> 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<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||
if (freeMode != kFreeModeFixedLanes) continue; // no fixed lanes ⇒ nothing managed
|
||||
|
||||
const int numLanes = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
||||
for (int lane = 0; lane < numLanes; ++lane) {
|
||||
const std::string name = laneName(tr, lane);
|
||||
std::optional<std::string> key = managedLaneKey(name);
|
||||
if (!key) continue; // manual/unnamed lane — leave off the index
|
||||
std::optional<std::string> 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
|
||||
|
||||
+31
-42
@@ -1,10 +1,9 @@
|
||||
#pragma once
|
||||
// REAPER-facing shell of Design View (D2): reads the live folder tree, runs
|
||||
// ViewModeModel's pure planner, and applies the resulting flag / per-FX /
|
||||
// lane writes. The .cpp is the sole REAPER-facing TU here (CLAUDE.md contract:
|
||||
// only main.cpp defines the API pointers). See src/shell/view/CLAUDE.md for
|
||||
// the enforced invariants (never touch master/mute, solo cached per mode and
|
||||
// never lost, snapshot-based restore).
|
||||
// REAPER-facing shell of Design View: reads the live folder tree, runs
|
||||
// ViewModeModel's pure planner, and applies the resulting flag / lane / parent
|
||||
// writes under ONE undo block. See src/shell/view/CLAUDE.md for the enforced
|
||||
// invariants (never touch master/mute, solo cached per mode and never lost,
|
||||
// record-based restore).
|
||||
|
||||
#include <string>
|
||||
|
||||
@@ -25,51 +24,41 @@ bool transportBlocksModeSwitch(ReaProject* proj);
|
||||
|
||||
// Does one applyMode leave an undo point behind?
|
||||
//
|
||||
// A REAPPLY that wrote nothing must not: the load-tick reapply over a project
|
||||
// saved fully parked re-plans a park for every inactive leaf and finds every
|
||||
// flag and every FX already where it wants them, so a point there would mean
|
||||
// opening a project costs an undo step. `Undo_EndBlock2(proj, "", 0)` is the
|
||||
// discard form (mintManagedLanes' no-op path uses the same idiom).
|
||||
// A REAPPLY that wrote nothing must not: the load-tick reapply over a project saved
|
||||
// fully parked re-plans a park for every inactive leaf and finds every flag already
|
||||
// where it wants them, so a point there would mean opening a project costs an undo
|
||||
// step. `Undo_EndBlock2(proj, "", 0)` is the discard form (mintManagedLanes' no-op
|
||||
// path and the reconcile pass use the same idiom).
|
||||
//
|
||||
// A real switch mints regardless, fail-safe: it is an explicitly fired action
|
||||
// and has to stay undoable even in the degenerate case where the plan found
|
||||
// nothing to write. Note what applyMode CANNOT see when it decides — the
|
||||
// `view_state` ext-state write lands in persistViewState, after this block has
|
||||
// closed (src/shell/view/CLAUDE.md's Gotchas).
|
||||
// A real switch mints regardless, fail-safe: it is an explicitly fired action and has
|
||||
// to stay undoable even in the degenerate case where the plan found nothing to write.
|
||||
// Note what applyMode CANNOT see when it decides — the `view_state` ext-state write
|
||||
// lands in persistViewState, after this block has closed (src/shell/view/CLAUDE.md's
|
||||
// Gotchas).
|
||||
inline bool applyMintsUndoPoint(bool realSwitch, bool wroteAnything) {
|
||||
return realSwitch || wroteAnything;
|
||||
}
|
||||
|
||||
// True while an applyMode call is on the stack. applyMode fails closed when re-entered
|
||||
// (its FX writes are assumed to pump the message loop, so a timer tick or an action can
|
||||
// land inside one), and this lets a caller with no fallback test BEFORE it spends a
|
||||
// True while an applyMode call is on the stack. applyMode fails closed when
|
||||
// re-entered, and this lets a caller with no fallback test BEFORE it spends a
|
||||
// one-shot signal on a call that would be refused — see main.cpp's load glue.
|
||||
bool modeApplyInProgress();
|
||||
|
||||
// Snapshots each about-to-park track's flags into `model`, caches/clears the
|
||||
// outgoing mode's solo state and replays the incoming mode's, runs planToggle,
|
||||
// applies park/restore writes — flags AND per-FX offline, synchronously — plus
|
||||
// parent visibility flags, then sets the active mode. ONE switch is ONE undo
|
||||
// point; everything it moved rolls back in a single Ctrl-Z. Returns false (no
|
||||
// mutation) if `targetModeId` isn't registered, if this is a real switch
|
||||
// (target != active) while the transport is running, if it is re-entered
|
||||
// Captures each about-to-park track's prior flag values into `model`, caches/clears
|
||||
// the outgoing mode's solo state and replays the incoming mode's, runs planToggle,
|
||||
// applies the park/restore flag writes plus managed-lane and parent-visibility
|
||||
// writes, then sets the active mode. ONE switch is ONE undo point; everything it
|
||||
// moved rolls back in a single Ctrl-Z.
|
||||
//
|
||||
// FX are NOT written here. Offlining a parked chain is the reclaimer's job, off this
|
||||
// path: TrackFX_SetOffline was 1897ms of a measured 1942ms switch, against 0.02ms for
|
||||
// all the flag work below. Do not reintroduce a synchronous FX write.
|
||||
//
|
||||
// Returns false (no mutation) if `targetModeId` isn't registered, if this is a real
|
||||
// switch (target != active) while the transport is running, if it is re-entered
|
||||
// (modeApplyInProgress), or if the project does not validate. A reapply (target ==
|
||||
// active) is never gated and never touches solo. `proj` == nullptr means the
|
||||
// current project, resolved ONCE and used for every call this apply makes.
|
||||
// active) is never gated and never touches solo. `proj` == nullptr means the current
|
||||
// project, resolved ONCE and used for every call this apply makes.
|
||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj);
|
||||
|
||||
// Splits any track visible in more than one mode while carrying its own media
|
||||
// into fixed lanes (one managed lane per involved mode), assigns items, and
|
||||
// records ownership in `model`. Never touches manual lanes. Runs planLaneMinting
|
||||
// (model + tree aware); wraps the mutation in one Undo block when non-empty.
|
||||
// Returns true if any lane was minted (repaint hint). `proj` == nullptr means
|
||||
// the current project.
|
||||
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj);
|
||||
|
||||
// Recovers managed-lane ownership from durable P_LANENAME on project open —
|
||||
// self-healing, without minting/reassigning anything and without touching
|
||||
// membership. A lane without the managed prefix is left untouched (manual).
|
||||
// `proj` == nullptr means the current project.
|
||||
void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj);
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
// See view_fx_park.h. Compiled into the reaper_reasampler module; includes
|
||||
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
||||
// The decisions are pure (header, test_view_fx_park.cpp); this file owns the
|
||||
// REAPER reads/writes and their ordering against the live enumeration.
|
||||
|
||||
#include "shell/view/view_fx_park.h"
|
||||
|
||||
#include <cstring> // strnlen — bounded read of GetTrackName's buffer
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_GetTrackName
|
||||
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_ShowConsoleMsg
|
||||
#define REAPERAPI_WANT_TrackFX_GetCount
|
||||
#define REAPERAPI_WANT_TrackFX_GetFXGUID
|
||||
#define REAPERAPI_WANT_TrackFX_GetOffline
|
||||
#define REAPERAPI_WANT_TrackFX_SetOffline
|
||||
#define REAPERAPI_WANT_ValidatePtr2
|
||||
#define REAPERAPI_WANT_guidToString
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace {
|
||||
|
||||
// The last refusal report, so an unchanged one is not reprinted on every reapply.
|
||||
ReaProject* g_reportedOwner = nullptr;
|
||||
std::vector<std::string> g_reportedRefusals;
|
||||
|
||||
ReaProject* currentProject() { return EnumProjects(-1, nullptr, 0); }
|
||||
|
||||
// Returns whether it actually wrote — the apply's undo point and its
|
||||
// UNDO_STATE_FX bit both hang off that verdict.
|
||||
//
|
||||
// [verify — DAW] TrackFX_SetOffline unloads and re-instantiates the plugin, so
|
||||
// writing a state that already holds is assumed to cost the same unload/reload
|
||||
// as a real change — the header documents no internal short-circuit either way.
|
||||
// The compare is cheap and correct regardless; only the size of what it saves is
|
||||
// unconfirmed.
|
||||
bool setOfflineIfChanged(MediaTrack* tr, int fx, bool offline) {
|
||||
if (TrackFX_GetOffline(tr, fx) == offline) return false;
|
||||
TrackFX_SetOffline(tr, fx, offline);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The FX's own durable identity, braced exactly like the track GUID keys. Empty
|
||||
// when REAPER reports none — an FX we cannot name is one we cannot restore, and
|
||||
// fx_offline treats it that way rather than guessing at its slot. Lifetime is
|
||||
// settled: the string copy is taken immediately and the GUID* is never held
|
||||
// past this call (reaper_plugin_functions.h:7348 documents no null contract for
|
||||
// TrackFX_GetFXGUID; treating null as "no identity" is the safe read).
|
||||
//
|
||||
// [verify — DAW] STABILITY across a chain mutation is not settled the same way:
|
||||
// confirm the GUID for one FX instance survives a native drag-reorder, an SWS
|
||||
// move (SNM_MoveOrRemoveTrackFX — SWS issue #802 reports the FXID lines do not
|
||||
// follow the plugin after that call, i.e. wrong-plugin restores or mass drops
|
||||
// through fx_offline on that path specifically), a save/reload round trip, and
|
||||
// two live instances of one plugin type staying distinguishable. See
|
||||
// src/shell/view/CLAUDE.md's Gotchas for the SWS-path risk this leaves open.
|
||||
std::string fxGuidString(MediaTrack* tr, int fx) {
|
||||
GUID* g = TrackFX_GetFXGUID(tr, fx);
|
||||
if (!g) return {};
|
||||
char buf[64] = {0}; // guidToString needs a >=64-char destination (SDK contract)
|
||||
guidToString(g, buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// The chain as it stands now: identity by current slot. Snapshot, park and
|
||||
// restore all address FX through this one plain 0..TrackFX_GetCount-1
|
||||
// enumeration — never the 0x1000000/0x2000000 input-FX or container forms — so
|
||||
// whatever it covers, all three cover identically.
|
||||
std::vector<std::string> liveFxGuids(MediaTrack* tr) {
|
||||
const int fxCount = TrackFX_GetCount(tr);
|
||||
std::vector<std::string> guids;
|
||||
guids.reserve(static_cast<std::size_t>(fxCount));
|
||||
for (int fx = 0; fx < fxCount; ++fx) guids.push_back(fxGuidString(tr, fx));
|
||||
return guids;
|
||||
}
|
||||
|
||||
// ANY offline FX, not every — see the snapshot-source invariant (view_fx_park.h,
|
||||
// src/shell/view/CLAUDE.md) for why.
|
||||
bool anyFxOffline(MediaTrack* tr) {
|
||||
const int fxCount = TrackFX_GetCount(tr);
|
||||
for (int fx = 0; fx < fxCount; ++fx)
|
||||
if (TrackFX_GetOffline(tr, fx)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Captures prior driven-flag state before parking. Never reads B_MUTE/I_SOLO;
|
||||
// ints preserve whatever REAPER reported (TrackSnapshot's defensive contract).
|
||||
// Must run before EITHER half of the park writes — both halves are read here.
|
||||
// `fxKeying` stays Identity: a live read always knows the chain.
|
||||
TrackSnapshot snapshotTrack(MediaTrack* tr) {
|
||||
TrackSnapshot snap;
|
||||
auto read = [tr](Flag f) {
|
||||
return static_cast<int>(GetMediaTrackInfo_Value(tr, trackFlagParm(f)));
|
||||
};
|
||||
snap.showInTcp = read(Flag::ShowInTcp);
|
||||
snap.showInMixer = read(Flag::ShowInMixer);
|
||||
snap.mainSend = read(Flag::MainSend);
|
||||
snap.fxEnable = read(Flag::FxEnable);
|
||||
|
||||
const std::vector<std::string> guids = liveFxGuids(tr);
|
||||
snap.fxOffline.reserve(guids.size());
|
||||
for (std::size_t fx = 0; fx < guids.size(); ++fx)
|
||||
snap.fxOffline.push_back(
|
||||
FxOfflineState{guids[fx], TrackFX_GetOffline(tr, static_cast<int>(fx)) ? 1 : 0});
|
||||
return snap;
|
||||
}
|
||||
|
||||
// Returns whether it wrote; raises `fxMoved` when a flag in the FX domain moved.
|
||||
bool applyFlags(MediaTrack* tr, const std::vector<TrackFlagOp>& flags, bool& fxMoved) {
|
||||
bool wrote = false;
|
||||
for (const TrackFlagOp& op : flags) {
|
||||
if (!writeIfChanged(tr, trackFlagParm(op.flag), static_cast<double>(op.value))) continue;
|
||||
wrote = true;
|
||||
// I_FXEN is the only FX-domain flag driven here, so it is the only one
|
||||
// whose undo record has to carry UNDO_STATE_FX.
|
||||
if (op.flag == Flag::FxEnable) fxMoved = true;
|
||||
}
|
||||
return wrote;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// GetTrackName, not P_NAME, matching capture::trackName's reason for the same
|
||||
// choice. The buffer is read bounded — the SDK documents no NUL within
|
||||
// bufOut_sz. A track the report cannot name still has to appear in it, so the
|
||||
// read failing yields a placeholder rather than dropping the entry.
|
||||
std::string trackDisplayName(MediaTrack* tr) {
|
||||
std::vector<char> buf(1024, '\0');
|
||||
if (!GetTrackName || !GetTrackName(tr, buf.data(), static_cast<int>(buf.size())))
|
||||
return "(name unavailable)";
|
||||
return std::string(buf.data(), strnlen(buf.data(), buf.size()));
|
||||
}
|
||||
|
||||
ParkAction decideParkForTrack(MediaTrack* tr, bool haveSnapshot,
|
||||
const std::vector<TrackFlagOp>& parkFlags) {
|
||||
if (haveSnapshot) return decidePark(true, false); // second term unread — skip the reads
|
||||
const bool flagsRead = parkFlagsAlreadyApplied(parkFlags, [tr](Flag f) {
|
||||
return static_cast<int>(GetMediaTrackInfo_Value(tr, trackFlagParm(f)));
|
||||
});
|
||||
return decidePark(false, chainReadsParked(flagsRead, anyFxOffline(tr)));
|
||||
}
|
||||
|
||||
void reportRefusedParks(ReaProject* proj, const std::vector<std::string>& refusedNames) {
|
||||
ReaProject* const owner = proj ? proj : currentProject();
|
||||
|
||||
if (!shouldReport(owner == g_reportedOwner, g_reportedRefusals, refusedNames)) return;
|
||||
g_reportedOwner = owner;
|
||||
g_reportedRefusals = refusedNames;
|
||||
|
||||
const std::string msg = describeRefusedParks(refusedNames);
|
||||
// "!SHOW:" so an unattended project-load reapply cannot force the console open
|
||||
// (reaper_plugin_functions.h:6536).
|
||||
if (!msg.empty()) ShowConsoleMsg(("!SHOW:" + msg).c_str());
|
||||
}
|
||||
|
||||
bool writeIfChanged(MediaTrack* tr, const char* parm, double value) {
|
||||
if (GetMediaTrackInfo_Value(tr, parm) == value) return false;
|
||||
SetMediaTrackInfo_Value(tr, parm, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool trackStillLive(ReaProject* proj, MediaTrack* tr) {
|
||||
return ValidatePtr2(proj ? proj : currentProject(), tr, "MediaTrack*");
|
||||
}
|
||||
|
||||
TrackApplyResult parkTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
const std::vector<TrackFlagOp>& flags) {
|
||||
TrackApplyResult out;
|
||||
|
||||
const ParkAction action = decideParkForTrack(tr, model.snapshot(guid) != nullptr, flags);
|
||||
if (action == ParkAction::Refuse) {
|
||||
out.refused = true;
|
||||
return out;
|
||||
}
|
||||
if (action == ParkAction::SnapshotThenPark) {
|
||||
model.storeSnapshot(guid, snapshotTrack(tr));
|
||||
out.wrote = true;
|
||||
}
|
||||
|
||||
if (applyFlags(tr, flags, out.fxMoved)) out.wrote = true;
|
||||
|
||||
const int fxCount = TrackFX_GetCount(tr);
|
||||
for (int fx = 0; fx < fxCount; ++fx)
|
||||
if (setOfflineIfChanged(tr, fx, true)) out.fxMoved = out.wrote = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
TrackApplyResult restoreTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
const std::vector<FxOfflineOp>& fxOps,
|
||||
const std::vector<TrackFlagOp>& flags) {
|
||||
TrackApplyResult out;
|
||||
|
||||
// Which live FX each captured state belongs to is resolveFxRestore's call —
|
||||
// the plan is written VERBATIM, never a blanket "online".
|
||||
const FxRestoreResolution res = resolveFxRestore(fxOps, liveFxGuids(tr));
|
||||
for (const FxOfflineWrite& w : res.writes)
|
||||
if (setOfflineIfChanged(tr, w.fxIndex, w.offline)) out.fxMoved = out.wrote = true;
|
||||
out.drops = res.drops;
|
||||
// The chain is back online HERE, which is the only thing that spends the FX half.
|
||||
model.releaseSnapshotFx(guid);
|
||||
|
||||
if (applyFlags(tr, flags, out.fxMoved)) out.wrote = true;
|
||||
model.releaseSnapshotFlags(guid);
|
||||
|
||||
// Unconditional, even when every write above was elided: the record is consumed
|
||||
// here, so this apply changed state and is not repeatable — and `wrote`, whose one
|
||||
// reader asks exactly that, would be a lie at false. NOT a claim about what the
|
||||
// undo record holds: the block never carries the record either way, that being
|
||||
// model state persisted after Undo_EndBlock2 (src/shell/view/CLAUDE.md).
|
||||
out.wrote = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
void reportFxRestoreDrops(const FxRestoreDrops& drops, int trackCount) {
|
||||
const std::string msg = describeFxRestoreDrops(drops, trackCount);
|
||||
// "!SHOW:" (reaper_plugin_functions.h:6536) keeps it from force-opening the
|
||||
// console: this also runs behind an unattended project-load reapply, and
|
||||
// cannot tell that case from an interactive toggle.
|
||||
if (!msg.empty()) ShowConsoleMsg(("!SHOW:" + msg).c_str());
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -1,185 +0,0 @@
|
||||
#pragma once
|
||||
// Design View's park surface: one track parked or restored — the trust test,
|
||||
// the snapshot lifecycle, and the flag + per-FX writes both halves address
|
||||
// through this one file. Every write declared here runs synchronously inside
|
||||
// applyMode's own undo block, which is what makes one switch one undo point —
|
||||
// see src/shell/view/CLAUDE.md's FX-parking caveat.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/view/fx_offline.h"
|
||||
#include "core/view/view_mode_model.h" // Flag / TrackFlagOp — what a park writes
|
||||
|
||||
// Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header.
|
||||
class MediaTrack;
|
||||
class ReaProject;
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// -- may this chain be snapshotted? -------------------------------------------
|
||||
//
|
||||
// A pre-park snapshot may only ever be taken from a chain NO park has touched.
|
||||
// Its values are restore's sole source of truth, so a snapshot lifted from an
|
||||
// already-parked chain records park state as the user's state, and every later
|
||||
// restore then faithfully writes hidden/out-of-mix/FX-disabled back — permanent
|
||||
// and silent. A model carrying no snapshot is NOT evidence the chain is clean;
|
||||
// src/shell/view/CLAUDE.md lists the routes that still reach that pair.
|
||||
//
|
||||
// A snapshot has TWO halves and the test covers both, disjunctively: the flags
|
||||
// can read clean while the FX are still offline (a hand recovery that stopped at
|
||||
// the flags, one hand-offlined plugin) and vice versa. Either half reading
|
||||
// parked poisons the whole snapshot, so either half alone refuses.
|
||||
|
||||
// The REAPER track parameter each driven flag addresses. One home for the
|
||||
// mapping — park's live read, restore's write and the trust test below must
|
||||
// never disagree about which parameter a Flag names.
|
||||
inline const char* trackFlagParm(Flag f) {
|
||||
switch (f) {
|
||||
case Flag::ShowInTcp: return "B_SHOWINTCP";
|
||||
case Flag::ShowInMixer: return "B_SHOWINMIXER";
|
||||
case Flag::MainSend: return "B_MAINSEND";
|
||||
case Flag::FxEnable: return "I_FXEN";
|
||||
}
|
||||
return "B_SHOWINTCP"; // unreachable; keeps the compiler quiet
|
||||
}
|
||||
|
||||
// Does the track already sit at every value this park would write? Then a park
|
||||
// reached it. `readFlag` answers one Flag's live value; templating on it lets the
|
||||
// live path compare as it reads (no per-track vector) while the tests drive the
|
||||
// same fold from a table, keyed by Flag rather than by plan order. An empty plan
|
||||
// is evidence of nothing and answers false.
|
||||
template <class ReadFlag>
|
||||
bool parkFlagsAlreadyApplied(const std::vector<TrackFlagOp>& parkFlags, ReadFlag readFlag) {
|
||||
if (parkFlags.empty()) return false;
|
||||
for (const TrackFlagOp& op : parkFlags)
|
||||
if (readFlag(op.flag) != op.value) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// DISJUNCTIVE fold — rationale is the snapshot-source invariant above (also
|
||||
// src/shell/view/CLAUDE.md).
|
||||
inline bool chainReadsParked(bool parkFlagsRead, bool anyFxOffline) {
|
||||
return parkFlagsRead || anyFxOffline;
|
||||
}
|
||||
|
||||
enum class ParkAction {
|
||||
SnapshotThenPark, // clean chain — capture the pre-park truth, then park
|
||||
ParkOnly, // still parked; the held snapshot IS the pre-park truth
|
||||
Refuse, // no snapshot AND a parked chain — the truth is gone
|
||||
};
|
||||
|
||||
// Refusing leaves the track exactly as found: no flag write, no FX write, no
|
||||
// snapshot. Nothing false is committed, so the moment the user's own edit puts
|
||||
// the chain back somewhere trustworthy the next park captures it correctly —
|
||||
// where a snapshot of the parked state would have closed that door for good.
|
||||
inline ParkAction decidePark(bool haveSnapshot, bool chainReadsParked) {
|
||||
if (haveSnapshot) return ParkAction::ParkOnly;
|
||||
return chainReadsParked ? ParkAction::Refuse : ParkAction::SnapshotThenPark;
|
||||
}
|
||||
|
||||
// `decidePark` over the live track: the four driven flags AND the FX chain, which
|
||||
// a snapshot taken here would record both halves of. Skips the reads on the
|
||||
// ParkOnly path, where decidePark ignores the second term anyway.
|
||||
ParkAction decideParkForTrack(MediaTrack* tr, bool haveSnapshot,
|
||||
const std::vector<TrackFlagOp>& parkFlags);
|
||||
|
||||
// "Track N" for an unnamed track (GetTrackName, matching capture::trackName's
|
||||
// choice of API for the same reason). Exported so a caller holding the handle
|
||||
// only briefly (view.cpp's park loop, before Undo_EndBlock2/TrackList_AdjustWindows
|
||||
// invalidate it) can capture the name at resolve time rather than the handle.
|
||||
std::string trackDisplayName(MediaTrack* tr);
|
||||
|
||||
// The console report for the tracks one apply refused, empty when none. NAMES
|
||||
// them: a refused track is hidden from both the TCP and the mixer, so a count
|
||||
// alone points at nothing the user can go and find. It asserts no cause — the
|
||||
// pair it detects has several routes (src/shell/view/CLAUDE.md), and only some of
|
||||
// them are anyone's mistake.
|
||||
inline std::string describeRefusedParks(const std::vector<std::string>& trackNames) {
|
||||
if (trackNames.empty()) return {};
|
||||
std::string msg = "ReaSampler: " + std::to_string(trackNames.size()) + " track" +
|
||||
(trackNames.size() == 1 ? "" : "s") +
|
||||
" left unparked -- no stored pre-park state, and a live state that "
|
||||
"already reads as parked; parking now would record that as your own "
|
||||
"state.\n";
|
||||
for (const std::string& name : trackNames) msg += " " + name + "\n";
|
||||
msg += "To bring a track back under mode control: restore its TCP and mixer visibility, "
|
||||
"its main send and its FX enable, AND set every FX in its chain online -- FX enable "
|
||||
"is the chain bypass and does not bring an individually offlined FX back. The next "
|
||||
"mode switch will then park it correctly.\n";
|
||||
return msg;
|
||||
}
|
||||
|
||||
// Pure gate behind reportRefusedParks' one-per-changed-set memo: prints (i.e.
|
||||
// updates the memo) on any change of owner or of the named set, stays silent on
|
||||
// an exact repeat. An empty `now` still counts as a change from a non-empty
|
||||
// `last` — that update itself prints nothing (describeRefusedParks({}) is
|
||||
// empty) but resets the memo so a later refusal is never mistaken for a repeat
|
||||
// of one that already healed.
|
||||
inline bool shouldReport(bool sameOwnerAsLast, const std::vector<std::string>& last,
|
||||
const std::vector<std::string>& now) {
|
||||
return !(sameOwnerAsLast && last == now);
|
||||
}
|
||||
|
||||
// Prints describeRefusedParks for `refusedNames` (captured by the caller while its
|
||||
// track handles are still fresh — see view.cpp's park loop), ONCE per changed set
|
||||
// per project: the same refusal recurs on every reapply — every project open,
|
||||
// every tag/untag — and repeating it verbatim is noise the user cannot act on any
|
||||
// better the second time. Gated by shouldReport.
|
||||
void reportRefusedParks(ReaProject* proj, const std::vector<std::string>& refusedNames);
|
||||
|
||||
// -- parking and restoring one track ------------------------------------------
|
||||
//
|
||||
// Both run INSIDE applyMode's undo block, on the switch's own synchronous path,
|
||||
// so one switch is one undo point.
|
||||
|
||||
// A track parameter written only when it is not already there. A reapply
|
||||
// re-plans every flag it already applied, so most of what it writes is a value
|
||||
// the track already holds. The read is ASSUMED cheaper than the write it elides
|
||||
// (unmeasured); what it certainly does is keep those no-op writes out of the
|
||||
// undo record's mask. Returns whether it wrote.
|
||||
bool writeIfChanged(MediaTrack* tr, const char* parm, double value);
|
||||
|
||||
// Is `tr` still a live track of `proj` (nullptr = current project)? The apply's
|
||||
// loops re-ask per track: a plugin unloading is assumed to pump the message
|
||||
// loop, so a handle resolved before the first FX write can be dead by the last.
|
||||
// `proj` is resolved to a concrete pointer here because ValidatePtr2 documents
|
||||
// no nullptr-means-current-project contract (reaper_plugin_functions.h:7899),
|
||||
// and a false there would silently skip every track.
|
||||
bool trackStillLive(ReaProject* proj, MediaTrack* tr);
|
||||
|
||||
// What one track's park or restore did, folded into the apply's block state.
|
||||
struct TrackApplyResult {
|
||||
bool wrote = false; // a project write or a snapshot moved — the undo-point verdict
|
||||
bool fxMoved = false; // ⇒ the block's mask owes UNDO_STATE_FX
|
||||
bool refused = false; // park only: the chain was left exactly as found
|
||||
|
||||
// Restore only: captured FX state resolveFxRestore could not place. Reported
|
||||
// rather than swallowed — silence would read as "restore worked" while an FX
|
||||
// sat at whatever state the park left it in.
|
||||
FxRestoreDrops drops;
|
||||
};
|
||||
|
||||
// Parks one inactive leaf: the trust decision (decidePark), the pre-park
|
||||
// snapshot when it is owed, then flags before FX so the track is out of the mix
|
||||
// and bypassed before its plugins start unloading. On `refused` nothing at all
|
||||
// was written — the caller names the track (trackDisplayName) while its handle
|
||||
// is still fresh.
|
||||
TrackApplyResult parkTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
const std::vector<TrackFlagOp>& flags);
|
||||
|
||||
// Restores one returning leaf: FX before flags, park's order mirrored, so the
|
||||
// chain comes back online while still bypassed and out of the mix and the track
|
||||
// never sounds through a half-restored chain. Consumes the snapshot — from that
|
||||
// point the flags are back at their captured values, and a later reapply must
|
||||
// not replan a restore over whatever the user has changed since.
|
||||
TrackApplyResult restoreTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
const std::vector<FxOfflineOp>& fxOps,
|
||||
const std::vector<TrackFlagOp>& flags);
|
||||
|
||||
// One console line for a whole apply's drops. Called after the undo block
|
||||
// closes: a report is not a project write and must not join what a Ctrl-Z rolls
|
||||
// back.
|
||||
void reportFxRestoreDrops(const FxRestoreDrops& drops, int trackCount);
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,346 @@
|
||||
// See view_lanes.h. Compiled into the reaper_reasampler module; includes
|
||||
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
||||
// The lane decisions are pure (planToggle / planLaneMinting, core/view); this file
|
||||
// owns the live lane reads and writes they resolve against.
|
||||
|
||||
#include "shell/view/view_lanes.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "core/view/lane_keys.h"
|
||||
#include "core/view/view_tree.h"
|
||||
#include "shell/capture/item_read.h"
|
||||
#include "shell/view/view_park.h" // writeIfChanged — the read-before-write every driven parm takes
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
|
||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#define REAPERAPI_WANT_UpdateArrange
|
||||
#define REAPERAPI_WANT_UpdateTimeline
|
||||
#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;
|
||||
|
||||
// The same domains the apply's own mask names (view.cpp's kApplyUndoMask, which owns
|
||||
// the rationale): lane minting writes track lane config and item lane assignment.
|
||||
constexpr int kLaneUndoMask = UNDO_STATE_TRACKCFG | UNDO_STATE_ITEMS | UNDO_STATE_MISCCFG;
|
||||
|
||||
// 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<double>(kLanesDisplayAsNormal));
|
||||
const int settings = static_cast<int>(GetMediaTrackInfo_Value(tr, "C_LANESETTINGS"));
|
||||
SetMediaTrackInfo_Value(tr, "C_LANESETTINGS",
|
||||
static_cast<double>(settings | kLaneSettingsHideButtons));
|
||||
}
|
||||
|
||||
// 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<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;
|
||||
}
|
||||
|
||||
// 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<double>(lanePlays));
|
||||
}
|
||||
|
||||
// One pass, so the assign loop needs no per-item re-scan.
|
||||
std::map<std::string, MediaItem*> itemHandlesByGuid(MediaTrack* tr) {
|
||||
std::map<std::string, MediaItem*> 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<std::string> 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<LaneTrack> readLaneTracks(const ViewModeModel& model,
|
||||
const TrackHandles& handleByGuid) {
|
||||
std::vector<LaneTrack> tracks;
|
||||
tracks.reserve(handleByGuid.size());
|
||||
for (const auto& [guid, tr] : handleByGuid) {
|
||||
LaneTrack lt;
|
||||
lt.trackGuid = guid;
|
||||
|
||||
const bool fixedLane =
|
||||
static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes;
|
||||
|
||||
const int itemCount = CountTrackMediaItems(tr);
|
||||
lt.items.reserve(static_cast<std::size_t>(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<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
|
||||
if (current == laneOrdinal) return false; // already there — no-op
|
||||
SetMediaItemInfo_Value(it, "I_FIXEDLANE", static_cast<double>(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<std::string, std::vector<const LaneMint*>> mintsByTrack;
|
||||
for (const LaneMint& m : plan.mints) mintsByTrack[m.trackGuid].push_back(&m);
|
||||
std::map<std::string, std::vector<const LaneAssign*>> 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<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||
if (freeMode != kFreeModeFixedLanes) {
|
||||
SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast<double>(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<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
||||
|
||||
std::map<std::string, int> 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<double>(laneCount));
|
||||
char parm[32];
|
||||
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
|
||||
std::vector<char> 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<std::string, int> ordinals = managedLaneOrdinals(tr);
|
||||
const std::map<std::string, MediaItem*> 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
|
||||
|
||||
LaneApplyResult applyLaneOps(const TrackByGuid& handleByGuid,
|
||||
const std::vector<LanePlayOp>& lanes) {
|
||||
LaneApplyResult out;
|
||||
if (lanes.empty()) return out;
|
||||
|
||||
std::map<std::string, std::vector<const LanePlayOp*>> 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<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||
if (freeMode != kFreeModeFixedLanes) {
|
||||
SetMediaTrackInfo_Value(tr, "I_FREEMODE",
|
||||
static_cast<double>(kFreeModeFixedLanes));
|
||||
applyTransparentLaneDisplay(tr);
|
||||
out.freeMode = true;
|
||||
out.wrote = true;
|
||||
}
|
||||
|
||||
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
|
||||
if (applyLanePlays(tr, it->second, op->lanePlays)) out.wrote = true;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||
TrackHandles handleByGuid;
|
||||
std::vector<TrackFolderEntry> 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<LaneTrack> 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);
|
||||
|
||||
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();
|
||||
|
||||
Undo_EndBlock2(proj, "ReaSampler: separate cross-mode content into lanes", kLaneUndoMask);
|
||||
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<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||
if (freeMode != kFreeModeFixedLanes) continue; // no fixed lanes ⇒ nothing managed
|
||||
|
||||
const int numLanes = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
||||
for (int lane = 0; lane < numLanes; ++lane) {
|
||||
const std::string name = laneName(tr, lane);
|
||||
std::optional<std::string> key = managedLaneKey(name);
|
||||
if (!key) continue; // manual/unnamed lane — leave off the index
|
||||
std::optional<std::string> 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
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
// Managed fixed lanes, REAPER side: minting one lane per mode on a track whose own
|
||||
// content spans modes, driving the active mode's lane audible and every other
|
||||
// managed lane silent+hidden, and recovering ownership from the durable P_LANENAME
|
||||
// on project open. Manual lanes are never minted, driven, renamed or reassigned.
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "core/view/view_mode_model.h"
|
||||
#include "shell/view/view_tracks.h"
|
||||
|
||||
// Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header.
|
||||
class ReaProject;
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// `freeMode` alone decides whether UpdateTimeline is owed (SDK: required after an
|
||||
// I_FREEMODE change); `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;
|
||||
};
|
||||
|
||||
// Reconciles each op's durable laneKey to the track's current ordinal (a
|
||||
// stale/renamed/deleted key is pruned, never mis-driven) and drives C_LANEPLAYS.
|
||||
LaneApplyResult applyLaneOps(const TrackByGuid& handleByGuid,
|
||||
const std::vector<LanePlayOp>& lanes);
|
||||
|
||||
// Splits any track visible in more than one mode while carrying its own media into
|
||||
// fixed lanes (one managed lane per involved mode), assigns items, and records
|
||||
// ownership in `model`. Wraps the mutation in one Undo block when non-empty. Returns
|
||||
// true if any lane was minted (repaint hint). `proj` == nullptr means the current
|
||||
// project.
|
||||
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj);
|
||||
|
||||
// Recovers managed-lane ownership from durable P_LANENAME on project open —
|
||||
// self-healing, without minting/reassigning anything and without touching
|
||||
// membership. A lane without the managed prefix is left untouched (manual).
|
||||
// `proj` == nullptr means the current project.
|
||||
void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj);
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,114 @@
|
||||
// See view_park.h. Compiled into the reaper_reasampler module; includes
|
||||
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
||||
|
||||
#include "shell/view/view_park.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_TrackFX_GetCount
|
||||
#define REAPERAPI_WANT_TrackFX_GetFXGUID
|
||||
#define REAPERAPI_WANT_TrackFX_GetOffline
|
||||
#define REAPERAPI_WANT_guidToString
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace {
|
||||
|
||||
// The FX's own durable identity, braced exactly like the track GUID keys. Empty
|
||||
// when REAPER reports none — an FX we cannot name is one we cannot restore, and
|
||||
// fx_offline treats it that way rather than guessing at its slot. Lifetime is
|
||||
// settled: the string copy is taken immediately and the GUID* is never held past
|
||||
// this call (reaper_plugin_functions.h:7348 documents no null contract for
|
||||
// TrackFX_GetFXGUID; treating null as "no identity" is the safe read).
|
||||
//
|
||||
// [verify — DAW] STABILITY across a chain mutation is not settled the same way:
|
||||
// confirm the GUID for one FX instance survives a native drag-reorder, an SWS move
|
||||
// (SNM_MoveOrRemoveTrackFX — SWS issue #802 reports the FXID lines do not follow
|
||||
// the plugin after that call, i.e. wrong-plugin restores or mass drops through
|
||||
// fx_offline on that path specifically), a save/reload round trip, and two live
|
||||
// instances of one plugin type staying distinguishable. See src/shell/view/CLAUDE.md's
|
||||
// Gotchas for the SWS-path risk this leaves open.
|
||||
std::string fxGuidString(MediaTrack* tr, int fx) {
|
||||
GUID* g = TrackFX_GetFXGUID(tr, fx);
|
||||
if (!g) return {};
|
||||
char buf[64] = {0}; // guidToString needs a >=64-char destination (SDK contract)
|
||||
guidToString(g, buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// Captures prior driven-flag state, and the chain's per-FX offline state, before
|
||||
// parking. Never reads B_MUTE/I_SOLO; ints preserve whatever REAPER reported
|
||||
// (TrackSnapshot's defensive contract). The plain 0..TrackFX_GetCount-1 enumeration
|
||||
// — never the 0x1000000/0x2000000 input-FX or container forms — is the same one the
|
||||
// reclaimer resolves against. `fxKeying` stays Identity: a live read always knows
|
||||
// the chain.
|
||||
TrackSnapshot snapshotTrack(MediaTrack* tr) {
|
||||
TrackSnapshot snap;
|
||||
auto read = [tr](Flag f) {
|
||||
return static_cast<int>(GetMediaTrackInfo_Value(tr, trackFlagParm(f)));
|
||||
};
|
||||
snap.showInTcp = read(Flag::ShowInTcp);
|
||||
snap.showInMixer = read(Flag::ShowInMixer);
|
||||
snap.mainSend = read(Flag::MainSend);
|
||||
snap.fxEnable = read(Flag::FxEnable);
|
||||
|
||||
const int fxCount = TrackFX_GetCount(tr);
|
||||
snap.fxOffline.reserve(static_cast<std::size_t>(fxCount));
|
||||
for (int fx = 0; fx < fxCount; ++fx)
|
||||
snap.fxOffline.push_back(
|
||||
FxOfflineState{fxGuidString(tr, fx), TrackFX_GetOffline(tr, fx) ? 1 : 0});
|
||||
return snap;
|
||||
}
|
||||
|
||||
bool applyFlags(MediaTrack* tr, const std::vector<TrackFlagOp>& flags) {
|
||||
bool wrote = false;
|
||||
for (const TrackFlagOp& op : flags)
|
||||
if (writeIfChanged(tr, trackFlagParm(op.flag), static_cast<double>(op.value)))
|
||||
wrote = true;
|
||||
return wrote;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool writeIfChanged(MediaTrack* tr, const char* parm, double value) {
|
||||
if (GetMediaTrackInfo_Value(tr, parm) == value) return false;
|
||||
SetMediaTrackInfo_Value(tr, parm, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parkTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
const std::vector<TrackFlagOp>& flags) {
|
||||
bool wrote = false;
|
||||
|
||||
if (!model.holdsSnapshotFlags(guid)) {
|
||||
TrackSnapshot snap = snapshotTrack(tr);
|
||||
// A restore hands the flags back immediately but leaves the chain offline
|
||||
// until the reclaimer gets to it, so a re-park lands here with the FX half
|
||||
// still held over a still-parked chain. Keep it: the live read above would
|
||||
// record the park's own offline state as the user's, permanently.
|
||||
const TrackSnapshot* prior = model.snapshot(guid);
|
||||
if (prior && prior->fxHeld) {
|
||||
snap.fxOffline = prior->fxOffline;
|
||||
snap.fxKeying = prior->fxKeying;
|
||||
}
|
||||
model.storeSnapshot(guid, snap);
|
||||
wrote = true;
|
||||
}
|
||||
|
||||
if (applyFlags(tr, flags)) wrote = true;
|
||||
return wrote;
|
||||
}
|
||||
|
||||
bool restoreTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
const std::vector<TrackFlagOp>& flags) {
|
||||
applyFlags(tr, flags);
|
||||
model.releaseSnapshotFlags(guid);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
// Design View's park surface: one track's flag park or restore, plus the park
|
||||
// record that makes the restore non-destructive. FLAG DOMAIN ONLY — a park record's
|
||||
// FX half is captured here (a read) but never written here; the reclaimer owns
|
||||
// every TrackFX_SetOffline, off the switch's synchronous path.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/view/view_mode_model.h" // Flag / TrackFlagOp — what a park writes
|
||||
|
||||
// Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header.
|
||||
class MediaTrack;
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// The REAPER track parameter each driven flag addresses. One home for the mapping —
|
||||
// the park's pre-park read, the restore's write and the reconcile's repair must
|
||||
// never disagree about which parameter a Flag names.
|
||||
inline const char* trackFlagParm(Flag f) {
|
||||
switch (f) {
|
||||
case Flag::ShowInTcp: return "B_SHOWINTCP";
|
||||
case Flag::ShowInMixer: return "B_SHOWINMIXER";
|
||||
case Flag::MainSend: return "B_MAINSEND";
|
||||
case Flag::FxEnable: return "I_FXEN";
|
||||
}
|
||||
return "B_SHOWINTCP"; // unreachable; keeps the compiler quiet
|
||||
}
|
||||
|
||||
// A track parameter written only when it is not already there. A reapply re-plans
|
||||
// every flag it already applied, so most of what it writes is a value the track
|
||||
// already holds. The read is ASSUMED cheaper than the write it elides (unmeasured);
|
||||
// what it certainly does is keep those no-op writes out of the undo record's mask.
|
||||
// Returns whether it wrote.
|
||||
bool writeIfChanged(MediaTrack* tr, const char* parm, double value);
|
||||
|
||||
// Parks one inactive leaf: capture the pre-park truth when the model does not
|
||||
// already hold it, then drive the flags. Returns whether anything moved — a project
|
||||
// write or the record.
|
||||
//
|
||||
// The record is the tool's own account of an act it performed, and it is the ONLY
|
||||
// account — nothing infers a park from a live value any more. That is what retired
|
||||
// the old refusal: a track that merely LOOKS parked while no record names it is a
|
||||
// track the tool did not park, so its values are the user's own and capturing them
|
||||
// is right. So the capture is owed exactly when the flag half is not held.
|
||||
//
|
||||
// A still-held FX half is the one thing not re-read: it outlives the flag half the
|
||||
// restore spends, the chain stays offline until the reclaimer onlines it, and
|
||||
// re-reading it there would file the park's own state as the user's.
|
||||
bool parkTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
const std::vector<TrackFlagOp>& flags);
|
||||
|
||||
// Restores one returning leaf from its record and spends the record's flag half.
|
||||
// Always true: the record is consumed here, so this apply changed state and is not
|
||||
// repeatable, which is exactly what its one reader (the undo-point decision) asks.
|
||||
bool restoreTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
const std::vector<TrackFlagOp>& flags);
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,80 @@
|
||||
// See view_reconcile.h. Compiled into the reaper_reasampler module; includes
|
||||
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
||||
// The decision is pure (header, test_view_reconcile.cpp); this file owns the memo's
|
||||
// lifetime and the writes.
|
||||
|
||||
#include "shell/view/view_reconcile.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "shell/view/view_park.h" // trackFlagParm / writeIfChanged
|
||||
#include "shell/view/view_tracks.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#define REAPERAPI_WANT_UpdateArrange
|
||||
#define REAPERAPI_WANT_ValidatePtr2
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace {
|
||||
|
||||
// What the last apply left driven, and in which project. The owner is COMPARE-ONLY
|
||||
// and never dereferenced (the same discipline the panel's own project memos take): a
|
||||
// closed project's pointer must never be followed, only found unequal. A project we
|
||||
// hold no memo for gets no repair — the tool has no account of driving anything
|
||||
// there, so it has no opinion.
|
||||
ReaProject* g_appliedOwner = nullptr;
|
||||
view::AppliedState g_applied;
|
||||
|
||||
} // namespace
|
||||
|
||||
void noteAppliedState(ReaProject* proj, const ViewModeModel& model) {
|
||||
g_appliedOwner = proj ? proj : EnumProjects(-1, nullptr, 0);
|
||||
g_applied = view::appliedState(model);
|
||||
}
|
||||
|
||||
void reconcileAppliedFlags(ViewModeModel& model, ReaProject* proj) {
|
||||
ReaProject* const project = proj ? proj : EnumProjects(-1, nullptr, 0);
|
||||
if (!ValidatePtr2(nullptr, project, "ReaProject*")) return;
|
||||
|
||||
const view::AppliedState reloaded = view::appliedState(model);
|
||||
const std::vector<TrackFlagOp> ops =
|
||||
project == g_appliedOwner ? planReconcile(g_applied, reloaded)
|
||||
: std::vector<TrackFlagOp>{};
|
||||
|
||||
// Adopted whether or not anything is written: from here the memo describes the
|
||||
// project as it now stands, so a second reload cannot replay a repair that has
|
||||
// already happened over values the user has since changed.
|
||||
g_appliedOwner = project;
|
||||
g_applied = reloaded;
|
||||
if (ops.empty()) return;
|
||||
|
||||
TrackHandles handleByGuid;
|
||||
readFolderEntries(project, handleByGuid);
|
||||
const TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
||||
|
||||
bool wrote = false;
|
||||
Undo_BeginBlock2(project);
|
||||
{
|
||||
UiRefreshHold uiHold;
|
||||
for (const TrackFlagOp& op : ops) {
|
||||
MediaTrack* tr = resolve(trackByGuid, op.guid);
|
||||
if (!tr) continue; // stale/deleted GUID — prune
|
||||
if (writeIfChanged(tr, trackFlagParm(op.flag), static_cast<double>(op.value)))
|
||||
wrote = true;
|
||||
}
|
||||
}
|
||||
Undo_EndBlock2(project, "", 0); // discard form — see the header
|
||||
|
||||
if (wrote) {
|
||||
TrackList_AdjustWindows(false); // tracks reappearing owe the full relayout
|
||||
UpdateArrange();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
// The repair pass for a project state REAPER reloaded under us. An undo/redo rolls
|
||||
// the view_state ext block back but NOT B_SHOWINTCP/B_SHOWINMIXER — UNDO_STATE_TRACKCFG
|
||||
// does not carry them (DAW-confirmed) — so a park the model no longer records can be
|
||||
// left driven on the track with nobody holding its captured values but us.
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/view/applied_state.h"
|
||||
#include "core/view/view_mode_model.h"
|
||||
|
||||
// Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header.
|
||||
class ReaProject;
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// The flags to hand back, and to whom.
|
||||
//
|
||||
// SCOPE — the rule this pass exists to keep: a track is driven only where a park
|
||||
// record names it, and the value written is one that record captured. `applied` is
|
||||
// the tool's account of an act it PERFORMED, so a track absent from it (and from the
|
||||
// reloaded model) is one the tool never drove, holds no captured value for, and has
|
||||
// no opinion about. Keying the repair on "the model calls this track active" instead
|
||||
// is the rejected shape: it asserts B_MAINSEND=1 on a track the user disabled by hand.
|
||||
//
|
||||
// A track the RELOADED model still parks is left out too — it still has an owner, and
|
||||
// the reapply that follows this pass drives it from the live model.
|
||||
inline std::vector<TrackFlagOp> planReconcile(const view::AppliedState& applied,
|
||||
const view::AppliedState& reloaded) {
|
||||
std::vector<TrackFlagOp> ops;
|
||||
if (applied == reloaded) return ops; // no drift in what we applied — nothing to repair
|
||||
|
||||
std::set<std::string> stillRecorded;
|
||||
for (const view::AppliedTrack& t : reloaded.parked) stillRecorded.insert(t.guid);
|
||||
|
||||
for (const view::AppliedTrack& t : applied.parked) {
|
||||
if (stillRecorded.count(t.guid)) continue;
|
||||
ops.push_back(TrackFlagOp{t.guid, Flag::ShowInTcp, t.showInTcp});
|
||||
ops.push_back(TrackFlagOp{t.guid, Flag::ShowInMixer, t.showInMixer});
|
||||
ops.push_back(TrackFlagOp{t.guid, Flag::MainSend, t.mainSend});
|
||||
ops.push_back(TrackFlagOp{t.guid, Flag::FxEnable, t.fxEnable});
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
// Records what `model` left driven in `proj` (nullptr = current project). Called at
|
||||
// the end of every apply: the fingerprint is the ONLY account of what the tool drove
|
||||
// once an undo has rolled the model back underneath it.
|
||||
void noteAppliedState(ReaProject* proj, const ViewModeModel& model);
|
||||
|
||||
// Runs planReconcile over the reloaded `model` and writes its ops. The undo block it
|
||||
// opens is deliberately DISCARDED — this runs in the wake of a Ctrl-Z, and a repair
|
||||
// that minted a point would become the thing the user's next Ctrl-Z undoes.
|
||||
void reconcileAppliedFlags(ViewModeModel& model, ReaProject* proj);
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -6,21 +6,13 @@
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "core/view/solo_cache.h"
|
||||
|
||||
// Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header.
|
||||
class MediaTrack;
|
||||
#include "shell/view/view_tracks.h" // TrackHandles — the enumeration these writers pass over
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// The (GUID, handle) enumeration applyMode builds from GetTrack. GetTrack's index
|
||||
// space excludes the master track, so no solo read or write reachable through this
|
||||
// type can ever land on master.
|
||||
using TrackHandles = std::vector<std::pair<std::string, MediaTrack*>>;
|
||||
|
||||
std::vector<view::TrackSolo> readTrackSolos(const TrackHandles& handles);
|
||||
|
||||
// Applies each op to its live handle in ONE pass over `handles`; an op naming a GUID
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// See view_tracks.h. Compiled into the reaper_reasampler module; includes
|
||||
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
||||
|
||||
#include "shell/view/view_tracks.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "shell/capture/track_guid.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_CountTracks
|
||||
#define REAPERAPI_WANT_GetTrack
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_PreventUIRefresh
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
std::vector<view::TrackFolderEntry> readFolderEntries(ReaProject* proj,
|
||||
TrackHandles& handleByGuid) {
|
||||
std::vector<view::TrackFolderEntry> entries;
|
||||
const int count = CountTracks(proj);
|
||||
entries.reserve(static_cast<std::size_t>(count));
|
||||
handleByGuid.reserve(static_cast<std::size_t>(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;
|
||||
const int depth = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FOLDERDEPTH"));
|
||||
entries.push_back(view::TrackFolderEntry{guid, depth});
|
||||
handleByGuid.emplace_back(std::move(guid), tr);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
TrackByGuid indexHandles(const TrackHandles& handleByGuid) {
|
||||
TrackByGuid byGuid;
|
||||
byGuid.reserve(handleByGuid.size());
|
||||
for (const auto& kv : handleByGuid) byGuid.emplace(kv.first, kv.second);
|
||||
return byGuid;
|
||||
}
|
||||
|
||||
UiRefreshHold::UiRefreshHold() { PreventUIRefresh(1); }
|
||||
UiRefreshHold::~UiRefreshHold() { PreventUIRefresh(-1); }
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
// The scaffolding every Design View write pass sets up: the live GUID-keyed track
|
||||
// enumeration it plans against, and the UI-refresh hold its writes land under.
|
||||
// The master track is outside GetTrack's index space, so nothing reached through
|
||||
// these types can land on it.
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "core/view/view_tree.h"
|
||||
|
||||
// Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header.
|
||||
class MediaTrack;
|
||||
class ReaProject;
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// (GUID, handle) in arrange order — the shape view_solo's one-pass writers take.
|
||||
using TrackHandles = std::vector<std::pair<std::string, MediaTrack*>>;
|
||||
|
||||
// The same handles indexed for lookup. Every park, restore, parent and lane op
|
||||
// resolves a GUID; a linear scan made that O(T²) on the one path the user waits on.
|
||||
using TrackByGuid = std::unordered_map<std::string, MediaTrack*>;
|
||||
|
||||
// Enumerates `proj` (nullptr = current project) into `handleByGuid` and returns each
|
||||
// track's I_FOLDERDEPTH entry for buildFolderTree. A track REAPER reports no GUID
|
||||
// for is skipped by both.
|
||||
std::vector<view::TrackFolderEntry> readFolderEntries(ReaProject* proj,
|
||||
TrackHandles& handleByGuid);
|
||||
|
||||
TrackByGuid indexHandles(const TrackHandles& handleByGuid);
|
||||
|
||||
inline 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
|
||||
}
|
||||
|
||||
// Holds REAPER's UI refresh off for a 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();
|
||||
~UiRefreshHold();
|
||||
UiRefreshHold(const UiRefreshHold&) = delete;
|
||||
UiRefreshHold& operator=(const UiRefreshHold&) = delete;
|
||||
};
|
||||
|
||||
} // namespace reasampler
|
||||
Reference in New Issue
Block a user