Files
reasampler/src/shell/view/view_fx_park.cpp
T

230 lines
9.9 KiB
C++

// 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