230 lines
9.9 KiB
C++
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
|