// 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 // strnlen — bounded read of GetTrackName's buffer #include #include #define REAPERAPI_MINIMAL #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_guidToString #include "reaper_plugin_functions.h" namespace reasampler { namespace { // 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(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(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& flags) { bool wrote = false; for (const TrackFlagOp& op : flags) if (writeIfChanged(tr, trackFlagParm(op.flag), static_cast(op.value))) wrote = true; return wrote; } // The visible/active mirror of a park write: same four ops, value 1 instead of // whatever the plan says (always 0 for a park plan — see makeParkPlan). Reusing // the incoming park ops rather than minting a separate "expose plan" keeps the // two writes provably addressing the same four parameters. bool exposeFlags(MediaTrack* tr, const std::vector& parkFlags) { bool wrote = false; for (const TrackFlagOp& op : parkFlags) if (writeIfChanged(tr, trackFlagParm(op.flag), 1.0)) wrote = true; return wrote; } // The last EXPOSE report, so an unchanged set is not reprinted on every reapply // the track hasn't yet self-healed from. ReaProject* g_reportedOwner = nullptr; std::vector g_reportedExposed; } // namespace const int kViewUndoMask = UNDO_STATE_TRACKCFG | UNDO_STATE_ITEMS | UNDO_STATE_MISCCFG; // 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); } bool writeIfChanged(MediaTrack* tr, const char* parm, double value) { if (GetMediaTrackInfo_Value(tr, parm) == value) return false; SetMediaTrackInfo_Value(tr, parm, value); return true; } TrackParkResult parkTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr, const std::vector& flags) { TrackParkResult result; if (!model.holdsSnapshotFlags(guid)) { const TrackSnapshot* prior = model.snapshot(guid); // No record of ANY kind (see parkTrack's header doc for why prior == nullptr // is the right gate): the ambiguous case. A live chain already reading // park-shaped is EXPOSEd — handed back visible, no snapshot stored — rather // than adopted as the user's intent. if (!prior) { auto read = [tr](Flag f) { return static_cast(GetMediaTrackInfo_Value(tr, trackFlagParm(f))); }; if (chainReadsParked(flags, read)) { if (exposeFlags(tr, flags)) result.wrote = true; result.exposed = true; return result; // this cycle hands the track back; it does not park it } } 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. Union the live read against the // record rather than overwriting either way — see unionFxOfflineOnRepark. // // `prior->fxHeld` is provably redundant here today — eraseIfSpent // (view_mode_model.cpp) guarantees a surviving record has fxHeld == true // — so the false arm is unreached. Kept anyway: it is the direct // statement of what this branch actually needs, and cheaper to keep true // than to re-derive if that guarantee ever loosens. if (prior && prior->fxHeld && prior->fxKeying == FxKeying::Identity) { snap.fxOffline = unionFxOfflineOnRepark(prior->fxOffline, snap.fxOffline); snap.fxKeying = FxKeying::Identity; } else if (prior && prior->fxHeld) { // Slot-keyed legacy record (lifted from a pre-identity view_state): no // identities to union against, so fall back to the old behavior and // trust the record verbatim over live slots that may have moved since. snap.fxOffline = prior->fxOffline; snap.fxKeying = prior->fxKeying; } model.storeSnapshot(guid, snap); result.wrote = true; } if (applyFlags(tr, flags)) result.wrote = true; return result; } bool restoreTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr, const std::vector& flags) { applyFlags(tr, flags); model.releaseSnapshotFlags(guid); return true; } std::string trackDisplayName(MediaTrack* tr) { std::vector buf(1024, '\0'); if (!GetTrackName || !GetTrackName(tr, buf.data(), static_cast(buf.size()))) return "(name unavailable)"; return std::string(buf.data(), strnlen(buf.data(), buf.size())); } void reportExposedTracks(ReaProject* proj, const std::vector& trackNames) { if (!shouldReport(proj == g_reportedOwner, g_reportedExposed, trackNames)) return; g_reportedOwner = proj; g_reportedExposed = trackNames; const std::string msg = describeExposedTracks(trackNames); // "!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()); } } // namespace reasampler