// 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 // strnlen — bounded read of GetTrackName's buffer #include #include #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 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 liveFxGuids(MediaTrack* tr) { const int fxCount = TrackFX_GetCount(tr); std::vector guids; guids.reserve(static_cast(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(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 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(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& flags, bool& fxMoved) { bool wrote = false; for (const TrackFlagOp& op : flags) { if (!writeIfChanged(tr, trackFlagParm(op.flag), static_cast(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 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())); } ParkAction decideParkForTrack(MediaTrack* tr, bool haveSnapshot, const std::vector& parkFlags) { if (haveSnapshot) return decidePark(true, false); // second term unread — skip the reads const bool flagsRead = parkFlagsAlreadyApplied(parkFlags, [tr](Flag f) { return static_cast(GetMediaTrackInfo_Value(tr, trackFlagParm(f))); }); return decidePark(false, chainReadsParked(flagsRead, anyFxOffline(tr))); } void reportRefusedParks(ReaProject* proj, const std::vector& 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& 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& fxOps, const std::vector& 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