#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 #include #include #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 { // 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 re-park's FX half, unioned rather than overwritten: keeps every FX identity // `prior` already names (the user's real pre-park state, still offline while the // reclaimer hasn't run) and appends whatever identity `live` sees that `prior` // does not — a plugin added during the restored interval, which a verbatim keep // of `prior` would otherwise never capture and leave stuck offline for good once // the reclaimer lands. Only meaningful when `prior` is Identity-keyed; a // Slot-keyed legacy record has no identities to union against, so parkTrack falls // back to trusting it verbatim instead of calling this. An empty fxGuid (REAPER // reported no identity) is never deduped against another empty one — it is // unrestorable either way, so appending it costs nothing and dedup would be a // coin flip. inline std::vector unionFxOfflineOnRepark( const std::vector& prior, const std::vector& live) { std::vector merged = prior; std::set named; for (const FxOfflineState& fx : merged) if (!fx.fxGuid.empty()) named.insert(fx.fxGuid); for (const FxOfflineState& fx : live) if (fx.fxGuid.empty() || named.insert(fx.fxGuid).second) merged.push_back(fx); return merged; } // Does the track already sit at every value `parkFlags` would write? Templated on // the read so live code compares as it reads (one call per Flag, no live-value // vector) while a test drives the same fold from a table. FX-offline is // deliberately NOT folded in: Wave 3 moved every per-FX write off this path onto // the reclaimer, so a chain with these four flags fine but an FX hand-offlined is // not evidence THIS path ever touched the track — only the flags it would itself // have driven are. An empty plan is evidence of nothing and answers false. template bool chainReadsParked(const std::vector& parkFlags, ReadFlag readFlag) { if (parkFlags.empty()) return false; for (const TrackFlagOp& op : parkFlags) if (readFlag(op.flag) != op.value) return false; return true; } // The undo-state domains both the mode-switch apply (view.cpp) and lane minting // (view_lanes.cpp) write: track config/routing, 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 — neither writer moves FX state, so carrying the // project's whole FX chunk on every point would cost with the plugin count for // nothing. One symbol, not two identically-valued constants free to diverge. // [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. extern const int kViewUndoMask; // 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); // What one parkTrack call did. `wrote` is any project write at all — a flag write // or a record write — and feeds the apply's undo-point fold. `exposed` is true iff // this call handed the track back visible instead of parking or adopting it; the // caller (view.cpp) collects exposed tracks across the whole apply for the one // batched console report. struct TrackParkResult { bool wrote = false; bool exposed = false; }; // Parks one inactive leaf: capture the pre-park truth when the model does not // already hold ANY record for it (holdsSnapshotFlags false AND model.snapshot == // nullptr), then drive the flags. // // A record with a held FX half but no held flag half (a re-park landing right // after a restore, before the reclaimer catches up) is NOT the no-record case — // model.snapshot(guid) is non-null there, so it takes the existing union path // below, never the branch this comment is about. That is how EXPOSE is known not // to fire on a track the tool legitimately still holds a record for: EXPOSE is // gated on snapshot(guid) == nullptr, and a legitimately-held record is by // definition non-null (eraseIfSpent only drops a record once BOTH halves are // spent). // // The true no-record case splits on whether the live chain already reads // park-shaped (chainReadsParked, flags only — see its own comment for why FX is // excluded). Not park-shaped: the chain is the track's own honest state, captured // and parked exactly as ever. Park-shaped: parking now would adopt a park with no // account behind it — an undo/redo or reload that dropped the snapshot, a hand or // script edit, or simply a track the user keeps hidden — as the user's intent, // which either wipes a hand repair on the very next restore or hides an untouched // track for good. EXPOSE hands it back instead: no snapshot stored, the four // flags driven to visible/active, nothing reported here (the caller batches it). // The next apply sees an honest chain again and captures/parks it normally — // self-healing, no user action required. TrackParkResult parkTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr, const std::vector& flags); // GetTrackName, matching capture::trackName's reason for the same choice: "Track // N" for an unnamed track, matching REAPER's own convention. Exported so a caller // holding the handle only briefly (view.cpp's park loop, before // Undo_EndBlock2/TrackList_AdjustWindows can invalidate it) captures the name at // resolve time rather than the handle. std::string trackDisplayName(MediaTrack* tr); // One console message for the tracks EXPOSEd this apply, empty when none. NAMES // them: an exposed track was hidden from both the TCP and the mixer moments ago, // so a bare count points at nothing the user can go and find. Says the tool lost // its own record and handed the track back — never "refused". inline std::string describeExposedTracks(const std::vector& trackNames) { if (trackNames.empty()) return {}; const bool one = trackNames.size() == 1; std::string msg = "ReaSampler: " + std::to_string(trackNames.size()) + " track" + (one ? "" : "s") + " handed back visible -- the tool's record of " + (one ? "its" : "their") + " pre-park state is gone (an undo/redo or a project reload dropped " "it), and parking now would have recorded the parked state as " "yours instead.\n"; for (const std::string& name : trackNames) msg += " " + name + "\n"; msg += "The next mode switch will park it correctly from what you see now.\n"; return msg; } // Pure gate behind reportExposedTracks' one-per-changed-set memo: true on any // change of owner or of the named set, false on an exact repeat. inline bool shouldReport(bool sameOwnerAsLast, const std::vector& last, const std::vector& now) { return !(sameOwnerAsLast && last == now); } // Prints describeExposedTracks for `trackNames` (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 exposure recurs on every reapply until the track // self-heals, and repeating it verbatim is noise the user cannot act on any // better the second time. `proj` is used only as a memo key, never dereferenced. void reportExposedTracks(ReaProject* proj, const std::vector& trackNames); // 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& flags); } // namespace reasampler