Merge Wave 2: Design View shell restructure (FX off the switch, park/lanes/reconcile/tracks split, record-scoped repair, expose-on-lost-record)
This commit is contained in:
+12
-10
@@ -39,8 +39,11 @@ add_library(reaper_reasampler MODULE
|
|||||||
${LICE_SRC}
|
${LICE_SRC}
|
||||||
${REASAMPLER_SRC_DIR}/shell/capture/insert.cpp
|
${REASAMPLER_SRC_DIR}/shell/capture/insert.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/view/view.cpp
|
${REASAMPLER_SRC_DIR}/shell/view/view.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/view/view_fx_park.cpp
|
${REASAMPLER_SRC_DIR}/shell/view/view_lanes.cpp
|
||||||
|
${REASAMPLER_SRC_DIR}/shell/view/view_park.cpp
|
||||||
|
${REASAMPLER_SRC_DIR}/shell/view/view_reconcile.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/view/view_solo.cpp
|
${REASAMPLER_SRC_DIR}/shell/view/view_solo.cpp
|
||||||
|
${REASAMPLER_SRC_DIR}/shell/view/view_tracks.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/capture/track_guid.cpp
|
${REASAMPLER_SRC_DIR}/shell/capture/track_guid.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/capture/provenance_shell.cpp
|
${REASAMPLER_SRC_DIR}/shell/capture/provenance_shell.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/capture/item_read.cpp
|
${REASAMPLER_SRC_DIR}/shell/capture/item_read.cpp
|
||||||
@@ -56,20 +59,19 @@ add_library(reaper_reasampler MODULE
|
|||||||
${REASAMPLER_SRC_DIR}/shell/actions/insert_fx_action.cpp
|
${REASAMPLER_SRC_DIR}/shell/actions/insert_fx_action.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/persist/usage_scan.cpp
|
${REASAMPLER_SRC_DIR}/shell/persist/usage_scan.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(reaper_reasampler PRIVATE json wire file_bytes bank_model capture_paths capture_name peaks bank_grid mode_switch tab_strip view_mode_model view_tree fx_state guid_diff lane_keys solo_cache insert_plan render_settings render_window track_topology batch_capture tail_control capture_realtime bank_book wav_codec origin_ledger tracking_authority prune_reconcile prune_button app_version provenance drag_out instrument_drop theme component_geometry action_bar footer_bar overflow_menu mode_enable insert_fx_enable tooltip card_meta card_drag assignment_request bank_sync sample_usage bake_wire resample_name export_bank package_pickers)
|
target_link_libraries(reaper_reasampler PRIVATE json wire file_bytes bank_model capture_paths capture_name peaks bank_grid mode_switch tab_strip view_mode_model view_tree fx_state applied_state guid_diff lane_keys solo_cache insert_plan render_settings render_window track_topology batch_capture tail_control capture_realtime bank_book wav_codec origin_ledger tracking_authority prune_reconcile prune_button app_version provenance drag_out instrument_drop theme component_geometry action_bar footer_bar overflow_menu mode_enable insert_fx_enable tooltip card_meta card_drag assignment_request bank_sync sample_usage bake_wire resample_name export_bank package_pickers)
|
||||||
# NOT linked here, deliberately: sampler_core / pitch_shift / the filter / limiter. The
|
# NOT linked here, deliberately: sampler_core / pitch_shift / the filter / limiter. The
|
||||||
# instrument renders its own bake in its own process, which is what keeps the extension's
|
# instrument renders its own bake in its own process, which is what keeps the extension's
|
||||||
# link graph free of the voice engine — a link edge to it here means the design drifted.
|
# link graph free of the voice engine — a link edge to it here means the design drifted.
|
||||||
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
||||||
|
|
||||||
# The park surface's decisions are pure (header-inline, no REAPER types), so they are
|
# The view shell's decisions are pure (header-inline, no REAPER types), so they are
|
||||||
# CTest-covered like a core/ module: the snapshot trust test, the refusal message and
|
# CTest-covered like a core/ module: the Flag -> parameter mapping, the apply's
|
||||||
# its once-per-changed-set gate, and the apply's undo-point fold. Declared here rather
|
# undo-point fold, and the reconcile's scope rule and drift trigger. Declared here
|
||||||
# than in a src/shell/view/CMakeLists.txt because that directory deliberately has none
|
# rather than in a src/shell/view/CMakeLists.txt because that directory deliberately
|
||||||
# — its TUs are compiled into this target directly. view_mode_model is linked for
|
# has none — its TUs are compiled into this target directly.
|
||||||
# makeParkPlan, the flag set the trust test folds over; pinning that against hand-built
|
reasampler_test(view_park LINK view_mode_model)
|
||||||
# ops would not catch a change to the planner.
|
reasampler_test(view_reconcile LINK applied_state view_mode_model)
|
||||||
reasampler_test(view_fx_park LINK fx_offline view_mode_model)
|
|
||||||
|
|
||||||
# Bank-package import: the promptless verb plus its action skin. Kept as its own
|
# Bank-package import: the promptless verb plus its action skin. Kept as its own
|
||||||
# appended block rather than merged into the lists above, so the two package
|
# appended block rather than merged into the lists above, so the two package
|
||||||
|
|||||||
+8
-1
@@ -38,7 +38,9 @@
|
|||||||
#include "shell/panel/panel_input.h" // bankPanelRefresh / bankPanelNotifyProjectLoaded
|
#include "shell/panel/panel_input.h" // bankPanelRefresh / bankPanelNotifyProjectLoaded
|
||||||
#include "shell/panel/panel_window.h" // panel lifecycle (init/toggle/open-query/shutdown)
|
#include "shell/panel/panel_window.h" // panel lifecycle (init/toggle/open-query/shutdown)
|
||||||
#include "shell/persist/session.h" // ReaSamplerSession
|
#include "shell/persist/session.h" // ReaSamplerSession
|
||||||
#include "shell/view/view.h" // reconcileManagedLanes / applyMode
|
#include "shell/view/view.h" // applyMode
|
||||||
|
#include "shell/view/view_lanes.h" // reconcileManagedLanes
|
||||||
|
#include "shell/view/view_reconcile.h" // reconcileAppliedFlags
|
||||||
|
|
||||||
namespace capture = reasampler::capture;
|
namespace capture = reasampler::capture;
|
||||||
|
|
||||||
@@ -205,6 +207,11 @@ static void OnTimer()
|
|||||||
// the cross-session source of truth) BEFORE reapplying visibility. Never
|
// the cross-session source of truth) BEFORE reapplying visibility. Never
|
||||||
// re-mints, never mass-tags.
|
// re-mints, never mass-tags.
|
||||||
reasampler::reconcileManagedLanes(g_session.view(), nullptr);
|
reasampler::reconcileManagedLanes(g_session.view(), nullptr);
|
||||||
|
// An undo/redo reload also arrives here, and REAPER's undo does not restore
|
||||||
|
// the visibility flags a park drove (UNDO_STATE_TRACKCFG does not carry them).
|
||||||
|
// Repair the tracks the reloaded model no longer records BEFORE the reapply,
|
||||||
|
// which only ever drives tracks the model still records.
|
||||||
|
reasampler::reconcileAppliedFlags(g_session.view(), nullptr);
|
||||||
reasampler::applyMode(g_session.view(), g_session.view().activeModeId(), nullptr);
|
reasampler::applyMode(g_session.view(), g_session.view().activeModeId(), nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
#include "shell/capture/item_read.h" // shared MediaItem* -> GUID + fixed-lane-name reads (D2 W3-B)
|
#include "shell/capture/item_read.h" // shared MediaItem* -> GUID + fixed-lane-name reads (D2 W3-B)
|
||||||
#include "shell/capture/track_guid.h" // shared MediaTrack* -> canonical GUID key
|
#include "shell/capture/track_guid.h" // shared MediaTrack* -> canonical GUID key
|
||||||
#include "shell/panel/panel_window.h" // bankPanelInvalidate — footer toggle repaint
|
#include "shell/panel/panel_window.h" // bankPanelInvalidate — footer toggle repaint
|
||||||
#include "shell/view/view.h" // applyMode + mintManagedLanes (D2 shell)
|
#include "shell/view/view.h" // applyMode (D2 shell)
|
||||||
|
#include "shell/view/view_lanes.h" // mintManagedLanes
|
||||||
|
|
||||||
#define REAPERAPI_MINIMAL
|
#define REAPERAPI_MINIMAL
|
||||||
#define REAPERAPI_WANT_CountSelectedTracks
|
#define REAPERAPI_WANT_CountSelectedTracks
|
||||||
|
|||||||
@@ -21,7 +21,8 @@
|
|||||||
#include "shell/capture/track_guid.h" // guidString
|
#include "shell/capture/track_guid.h" // guidString
|
||||||
#include "shell/panel/panel_input.h" // bankPanelTailSetting
|
#include "shell/panel/panel_input.h" // bankPanelTailSetting
|
||||||
#include "shell/persist/session.h"
|
#include "shell/persist/session.h"
|
||||||
#include "shell/view/view.h" // applyMode / mintManagedLanes
|
#include "shell/view/view.h" // applyMode
|
||||||
|
#include "shell/view/view_lanes.h" // mintManagedLanes
|
||||||
|
|
||||||
#define REAPERAPI_MINIMAL
|
#define REAPERAPI_MINIMAL
|
||||||
#define REAPERAPI_WANT_CountTrackMediaItems
|
#define REAPERAPI_WANT_CountTrackMediaItems
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
#include "core/view/view_mode_model.h" // autoTagNewContent / NewItem / AutoTag
|
#include "core/view/view_mode_model.h" // autoTagNewContent / NewItem / AutoTag
|
||||||
#include "shell/capture/item_read.h" // itemGuid / itemLaneName — shared item-read seam
|
#include "shell/capture/item_read.h" // itemGuid / itemLaneName — shared item-read seam
|
||||||
#include "shell/capture/track_guid.h" // guidString — canonical track GUID key
|
#include "shell/capture/track_guid.h" // guidString — canonical track GUID key
|
||||||
#include "shell/view/view.h" // mintManagedLanes / transportBlocksModeSwitch
|
#include "shell/view/view.h" // transportBlocksModeSwitch
|
||||||
|
#include "shell/view/view_lanes.h" // mintManagedLanes
|
||||||
|
|
||||||
// New-content detection: enumerate live tracks + items and read fixed-lane state to
|
// New-content detection: enumerate live tracks + items and read fixed-lane state to
|
||||||
// classify an item's lane as managed vs manual.
|
// classify an item's lane as managed vs manual.
|
||||||
|
|||||||
+47
-467
@@ -1,98 +1,50 @@
|
|||||||
// See view.h. Compiled into the reaper_reasampler module; includes
|
// See view.h. Compiled into the reaper_reasampler module; includes
|
||||||
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
||||||
// Tree arithmetic lives in view_tree (pure) and one track's park/restore in
|
// Tree arithmetic lives in view_tree (pure), one track's park/restore in view_park
|
||||||
// view_fx_park; this file owns the plan iteration, lanes, parents and the one
|
// and the lane writes in view_lanes; this file owns the plan iteration, parent
|
||||||
// undo block they all ride.
|
// visibility, and the one undo block they all ride.
|
||||||
|
|
||||||
#include "shell/view/view.h"
|
#include "shell/view/view.h"
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <optional>
|
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "shell/capture/item_read.h"
|
|
||||||
#include "core/view/fx_offline.h"
|
|
||||||
#include "core/view/fx_state.h"
|
|
||||||
#include "core/view/lane_keys.h"
|
|
||||||
#include "core/view/solo_cache.h"
|
#include "core/view/solo_cache.h"
|
||||||
#include "shell/capture/track_guid.h"
|
|
||||||
#include "shell/view/view_fx_park.h"
|
|
||||||
#include "shell/view/view_solo.h"
|
|
||||||
#include "core/view/view_tree.h"
|
#include "core/view/view_tree.h"
|
||||||
|
#include "shell/view/view_lanes.h"
|
||||||
|
#include "shell/view/view_park.h"
|
||||||
|
#include "shell/view/view_reconcile.h"
|
||||||
|
#include "shell/view/view_solo.h"
|
||||||
|
#include "shell/view/view_tracks.h"
|
||||||
|
|
||||||
#define REAPERAPI_MINIMAL
|
#define REAPERAPI_MINIMAL
|
||||||
#define REAPERAPI_WANT_CountTracks
|
|
||||||
#define REAPERAPI_WANT_EnumProjects
|
#define REAPERAPI_WANT_EnumProjects
|
||||||
#define REAPERAPI_WANT_GetPlayStateEx
|
#define REAPERAPI_WANT_GetPlayStateEx
|
||||||
#define REAPERAPI_WANT_GetTrack
|
|
||||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
|
||||||
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
|
||||||
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
|
|
||||||
#define REAPERAPI_WANT_PreventUIRefresh
|
|
||||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||||
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
||||||
#define REAPERAPI_WANT_UpdateArrange
|
#define REAPERAPI_WANT_UpdateArrange
|
||||||
#define REAPERAPI_WANT_UpdateTimeline
|
#define REAPERAPI_WANT_UpdateTimeline
|
||||||
#define REAPERAPI_WANT_ValidatePtr2
|
#define REAPERAPI_WANT_ValidatePtr2
|
||||||
// Lane minting: item-side lane reads/writes assigning each item to its mode's lane.
|
|
||||||
#define REAPERAPI_WANT_CountTrackMediaItems
|
|
||||||
#define REAPERAPI_WANT_GetTrackMediaItem
|
|
||||||
#define REAPERAPI_WANT_GetMediaItemInfo_Value
|
|
||||||
#define REAPERAPI_WANT_SetMediaItemInfo_Value
|
|
||||||
#include "reaper_plugin_functions.h"
|
#include "reaper_plugin_functions.h"
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
using view::buildFolderTree;
|
using view::buildFolderTree;
|
||||||
using view::isOnManualLane;
|
|
||||||
using view::managedLaneKey;
|
|
||||||
using view::modeIdFromLaneName;
|
|
||||||
using view::TrackFolderEntry;
|
using view::TrackFolderEntry;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// I_FREEMODE value for fixed lanes. SDK: 0=normal, 1=free item positioning, 2=fixed lanes.
|
|
||||||
constexpr int kFreeModeFixedLanes = 2;
|
|
||||||
|
|
||||||
// GetPlayStateEx bitmask. SDK: &1 playing, &2 paused, &4 recording — paused is
|
// GetPlayStateEx bitmask. SDK: &1 playing, &2 paused, &4 recording — paused is
|
||||||
// deliberately excluded, nothing is moving there.
|
// deliberately excluded, nothing is moving there.
|
||||||
constexpr int kTransportMoving = 1 | 4;
|
constexpr int kTransportMoving = 1 | 4;
|
||||||
|
|
||||||
// The domains one apply actually writes: track config/routing (visibility,
|
// applyMode is reachable from the load tick, the actions and render_in_place. Nothing
|
||||||
// B_MAINSEND, I_SOLO), item lane assignment, and the ext-state block
|
// it calls is known to pump the message loop now that the FX writes are gone, but a
|
||||||
// (UNDO_STATE_MISCCFG covers extension state). NOT UNDO_STATE_ALL, which
|
// re-entrant apply would plan against the same model the outer loop is iterating and
|
||||||
// includes UNDO_STATE_FX and so makes every undo record carry every track's FX
|
// consume records out from under it — one dropped call is cheaper than that. Fail closed.
|
||||||
// state — a cost that scales with the project's plugin count rather than with
|
|
||||||
// what the switch changed. FX is OR'd back in per apply, only when I_FXEN or a
|
|
||||||
// per-FX offline state really moved (TrackApplyResult::fxMoved).
|
|
||||||
// [verify — DAW] INFERRED, not documented: UNDO_STATE_TRACKCFG reads
|
|
||||||
// "track/master vol/pan/routing" (reaper_plugin.h:1541) and names neither
|
|
||||||
// B_SHOWINTCP/B_SHOWINMIXER nor the fixed-lane properties (assumed to ride
|
|
||||||
// UNDO_STATE_ITEMS, :1543). Wrong ⇒ a Ctrl-Z restores less than the switch did.
|
|
||||||
constexpr int kApplyUndoMask = UNDO_STATE_TRACKCFG | UNDO_STATE_ITEMS | UNDO_STATE_MISCCFG;
|
|
||||||
|
|
||||||
// Holds REAPER's UI refresh off for the write phase; the deliberate rebuild
|
|
||||||
// (TrackList_AdjustWindows + UpdateArrange) runs after it releases. RAII because
|
|
||||||
// an unbalanced pair leaves the user's UI frozen with no way back — the SDK's
|
|
||||||
// own warning at reaper_plugin_functions.h:5581.
|
|
||||||
struct UiRefreshHold {
|
|
||||||
UiRefreshHold() { PreventUIRefresh(1); }
|
|
||||||
~UiRefreshHold() { PreventUIRefresh(-1); }
|
|
||||||
UiRefreshHold(const UiRefreshHold&) = delete;
|
|
||||||
UiRefreshHold& operator=(const UiRefreshHold&) = delete;
|
|
||||||
};
|
|
||||||
|
|
||||||
// applyMode is reachable from the load tick, the actions and render_in_place, and its
|
|
||||||
// TrackFX_SetOffline calls are assumed to pump the message loop — so any of them can
|
|
||||||
// re-enter it mid-apply, planning against the same model the outer loop is iterating.
|
|
||||||
// An inner restoreTrack consuming a snapshot the outer sits mid-park over leaves that
|
|
||||||
// track flags-restored, chain-offline and snapshotless: refused forever. Fail closed.
|
|
||||||
bool g_applying = false;
|
bool g_applying = false;
|
||||||
struct ApplyLatch {
|
struct ApplyLatch {
|
||||||
ApplyLatch() { g_applying = true; }
|
ApplyLatch() { g_applying = true; }
|
||||||
@@ -101,275 +53,6 @@ struct ApplyLatch {
|
|||||||
ApplyLatch& operator=(const ApplyLatch&) = delete;
|
ApplyLatch& operator=(const ApplyLatch&) = delete;
|
||||||
};
|
};
|
||||||
|
|
||||||
// C_LANESCOLLAPSED=2: render a tool-split track like a normal single-lane
|
|
||||||
// track showing only the playing lane (SDK: 1=collapsed, 2=hidden-lanes-exist
|
|
||||||
// but displays as non-fixed-lane).
|
|
||||||
constexpr int kLanesDisplayAsNormal = 2;
|
|
||||||
|
|
||||||
// C_LANESETTINGS &32 = hide per-lane buttons; OR'd in, never clobbering the
|
|
||||||
// mask. Deliberately NOT setting &1 (auto-remove empty lanes): the lazy-mint
|
|
||||||
// decision never mints an empty lane, so &1 buys nothing and risks REAPER
|
|
||||||
// silently removing a managed lane out from under the ownership index.
|
|
||||||
constexpr int kLaneSettingsHideButtons = 32;
|
|
||||||
|
|
||||||
// Makes a tool-split track's display read as an ordinary track. Gated by every
|
|
||||||
// caller on the tool-driven transition INTO fixed lanes (freeMode != 2 before
|
|
||||||
// the flip) — a track already in fixed-lane mode (the user's own) never
|
|
||||||
// reaches this, so a user's comp-lane display prefs are never stomped.
|
|
||||||
void applyTransparentLaneDisplay(MediaTrack* tr) {
|
|
||||||
SetMediaTrackInfo_Value(tr, "C_LANESCOLLAPSED",
|
|
||||||
static_cast<double>(kLanesDisplayAsNormal));
|
|
||||||
const int settings = static_cast<int>(GetMediaTrackInfo_Value(tr, "C_LANESETTINGS"));
|
|
||||||
SetMediaTrackInfo_Value(tr, "C_LANESETTINGS",
|
|
||||||
static_cast<double>(settings | kLaneSettingsHideButtons));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The master track is not enumerated by GetTrack (index space excludes it),
|
|
||||||
// so it can never enter the tree — the master-untouched invariant holds by
|
|
||||||
// construction.
|
|
||||||
std::vector<TrackFolderEntry> readFolderEntries(
|
|
||||||
ReaProject* proj,
|
|
||||||
TrackHandles& handleByGuid) {
|
|
||||||
std::vector<TrackFolderEntry> entries;
|
|
||||||
int count = CountTracks(proj);
|
|
||||||
entries.reserve(static_cast<std::size_t>(count));
|
|
||||||
handleByGuid.reserve(static_cast<std::size_t>(count));
|
|
||||||
for (int i = 0; i < count; ++i) {
|
|
||||||
MediaTrack* tr = GetTrack(proj, i);
|
|
||||||
if (!tr) continue;
|
|
||||||
std::string guid = guidString(tr);
|
|
||||||
if (guid.empty()) continue;
|
|
||||||
int depth = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FOLDERDEPTH"));
|
|
||||||
entries.push_back(TrackFolderEntry{guid, depth});
|
|
||||||
handleByGuid.emplace_back(guid, tr);
|
|
||||||
}
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every park, restore, parent and lane op resolves its GUID; a linear scan made
|
|
||||||
// that O(T²) on the one path whose cost the user waits on. The vector form
|
|
||||||
// survives beside it because view_solo's writers take one pass over it.
|
|
||||||
using TrackByGuid = std::unordered_map<std::string, MediaTrack*>;
|
|
||||||
|
|
||||||
TrackByGuid indexHandles(const TrackHandles& handleByGuid) {
|
|
||||||
TrackByGuid byGuid;
|
|
||||||
byGuid.reserve(handleByGuid.size());
|
|
||||||
for (const auto& kv : handleByGuid) byGuid.emplace(kv.first, kv.second);
|
|
||||||
return byGuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaTrack* resolve(const TrackByGuid& byGuid, const std::string& guid) {
|
|
||||||
auto it = byGuid.find(guid);
|
|
||||||
return it == byGuid.end() ? nullptr : it->second; // stale/deleted GUID — pruned by being skipped
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lane `laneIdx`'s durable name (P_LANENAME:n) on `tr`, or empty if unnamed /
|
|
||||||
// unavailable (non-fixed-lane track).
|
|
||||||
std::string laneName(MediaTrack* tr, int laneIdx) {
|
|
||||||
char parm[32];
|
|
||||||
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
|
|
||||||
char buf[512] = {0};
|
|
||||||
if (!GetSetMediaTrackInfo_String(tr, parm, buf, false)) return {};
|
|
||||||
return std::string(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Managed lane durable key -> current ordinal on `tr`. Manual lanes are
|
|
||||||
// omitted, so a key absent from the map must not be driven.
|
|
||||||
std::map<std::string, int> managedLaneOrdinals(MediaTrack* tr) {
|
|
||||||
std::map<std::string, int> byKey;
|
|
||||||
const int numLanes = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
|
||||||
for (int lane = 0; lane < numLanes; ++lane) {
|
|
||||||
std::optional<std::string> key = managedLaneKey(laneName(tr, lane));
|
|
||||||
if (key) byKey.emplace(*key, lane); // first ordinal wins if names collide
|
|
||||||
}
|
|
||||||
return byKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track-side C_LANEPLAYS:N alone hides+silences every item on lane N (SDK:
|
|
||||||
// item-side C_LANEPLAYS is read-only, so no per-item write exists or is
|
|
||||||
// needed). B_FIXEDLANE_HIDDEN is also read-only — hide/show follows from
|
|
||||||
// C_LANEPLAYS=0/1, never written directly.
|
|
||||||
bool applyLanePlays(MediaTrack* tr, int laneIdx, int lanePlays) {
|
|
||||||
char parm[32];
|
|
||||||
std::snprintf(parm, sizeof(parm), "C_LANEPLAYS:%d", laneIdx);
|
|
||||||
return writeIfChanged(tr, parm, static_cast<double>(lanePlays));
|
|
||||||
}
|
|
||||||
|
|
||||||
// `freeMode` alone decides whether UpdateTimeline is owed; `wrote` is any project
|
|
||||||
// write at all, which is what the apply's undo-point decision reads.
|
|
||||||
struct LaneApplyResult {
|
|
||||||
bool freeMode = false;
|
|
||||||
bool wrote = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Groups ops by track, reconciles each op's durable laneKey to the track's
|
|
||||||
// current ordinal (a stale/renamed/deleted key is pruned, never mis-driven),
|
|
||||||
// enables fixed-lane mode on any track carrying a managed lane, and drives
|
|
||||||
// C_LANEPLAYS. UpdateTimeline() is the caller's job when `freeMode` comes back
|
|
||||||
// true (SDK: required after an I_FREEMODE change).
|
|
||||||
LaneApplyResult applyLaneOps(const TrackByGuid& handleByGuid,
|
|
||||||
const std::vector<LanePlayOp>& lanes) {
|
|
||||||
LaneApplyResult out;
|
|
||||||
if (lanes.empty()) return out;
|
|
||||||
|
|
||||||
std::map<std::string, std::vector<const LanePlayOp*>> byTrack;
|
|
||||||
for (const LanePlayOp& op : lanes) byTrack[op.trackGuid].push_back(&op);
|
|
||||||
|
|
||||||
for (const auto& [guid, ops] : byTrack) {
|
|
||||||
MediaTrack* tr = resolve(handleByGuid, guid);
|
|
||||||
if (!tr) continue; // stale GUID — prune
|
|
||||||
|
|
||||||
// Every track reaching here already owns a managed lane (planToggle
|
|
||||||
// only emits ops for managed lanes), so re-asserting fixed-lane mode
|
|
||||||
// is always a tool-driven (re)split — never a user's untouched
|
|
||||||
// manual-fixed-lane track — and gets the same transparent display.
|
|
||||||
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
|
||||||
if (freeMode != kFreeModeFixedLanes) {
|
|
||||||
SetMediaTrackInfo_Value(tr, "I_FREEMODE",
|
|
||||||
static_cast<double>(kFreeModeFixedLanes));
|
|
||||||
applyTransparentLaneDisplay(tr);
|
|
||||||
out.freeMode = true;
|
|
||||||
out.wrote = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::map<std::string, int> ordinals = managedLaneOrdinals(tr);
|
|
||||||
for (const LanePlayOp* op : ops) {
|
|
||||||
auto it = ordinals.find(op->laneKey);
|
|
||||||
if (it == ordinals.end()) continue; // key not live on this track — prune
|
|
||||||
if (applyLanePlays(tr, it->second, op->lanePlays)) out.wrote = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One pass, so the assign loop needs no per-item re-scan.
|
|
||||||
std::map<std::string, MediaItem*> itemHandlesByGuid(MediaTrack* tr) {
|
|
||||||
std::map<std::string, MediaItem*> byGuid;
|
|
||||||
const int itemCount = CountTrackMediaItems(tr);
|
|
||||||
for (int i = 0; i < itemCount; ++i) {
|
|
||||||
MediaItem* it = GetTrackMediaItem(tr, i);
|
|
||||||
if (!it) continue;
|
|
||||||
std::string ig = itemGuid(it);
|
|
||||||
if (!ig.empty()) byGuid.emplace(std::move(ig), it);
|
|
||||||
}
|
|
||||||
return byGuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// An untagged item is Arrange by default (mirrors leafBelongsToMode). A
|
|
||||||
// show-both/multi-mode item resolves to its first mode id — unusual for lane
|
|
||||||
// content, and any one mode is sufficient for the decision.
|
|
||||||
std::string itemModeFromMembership(const ViewModeModel& model, const std::string& itemGuid) {
|
|
||||||
const std::set<std::string> modes = model.membership().modesOf(itemGuid);
|
|
||||||
if (modes.empty()) return kArrangeModeId;
|
|
||||||
return *modes.begin();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The per-track LaneItem picture the pure decision consumes. Manual-lane reads
|
|
||||||
// are skipped on a non-fixed-lane track (isOnManualLane is false there anyway).
|
|
||||||
std::vector<LaneTrack> readLaneTracks(
|
|
||||||
const ViewModeModel& model,
|
|
||||||
const TrackHandles& handleByGuid) {
|
|
||||||
std::vector<LaneTrack> tracks;
|
|
||||||
tracks.reserve(handleByGuid.size());
|
|
||||||
for (const auto& [guid, tr] : handleByGuid) {
|
|
||||||
if (!tr) continue; // TrackHandles legally carries nulls post-validation (view.cpp)
|
|
||||||
LaneTrack lt;
|
|
||||||
lt.trackGuid = guid;
|
|
||||||
|
|
||||||
const bool fixedLane =
|
|
||||||
static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes;
|
|
||||||
|
|
||||||
const int itemCount = CountTrackMediaItems(tr);
|
|
||||||
lt.items.reserve(static_cast<std::size_t>(itemCount));
|
|
||||||
for (int i = 0; i < itemCount; ++i) {
|
|
||||||
MediaItem* it = GetTrackMediaItem(tr, i);
|
|
||||||
if (!it) continue;
|
|
||||||
const std::string ig = itemGuid(it);
|
|
||||||
if (ig.empty()) continue;
|
|
||||||
LaneItem li;
|
|
||||||
li.guid = ig;
|
|
||||||
li.modeId = itemModeFromMembership(model, ig);
|
|
||||||
const std::string ln = fixedLane ? itemLaneName(tr, it) : std::string{};
|
|
||||||
li.onManualLane = isOnManualLane(fixedLane, ln);
|
|
||||||
lt.items.push_back(std::move(li));
|
|
||||||
}
|
|
||||||
tracks.push_back(std::move(lt));
|
|
||||||
}
|
|
||||||
return tracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Idempotent, and non-destructive: only this reversible flag is written, never
|
|
||||||
// a move in time or across tracks.
|
|
||||||
bool assignItemToLane(MediaTrack* tr, MediaItem* it, int laneOrdinal) {
|
|
||||||
const int current = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
|
|
||||||
if (current == laneOrdinal) return false; // already there — no-op
|
|
||||||
SetMediaItemInfo_Value(it, "I_FIXEDLANE", static_cast<double>(laneOrdinal));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Applies the pure LaneMintPlan. Returns true if any project write actually
|
|
||||||
// changed state (⇒ caller keeps the Undo block and refreshes the timeline).
|
|
||||||
//
|
|
||||||
// The plan only ever names managed-prefixed lanes and only ever assigns
|
|
||||||
// managed-eligible items; I_NUMFIXEDLANES is only ever GROWN, never shrunk,
|
|
||||||
// so a user's existing manual lanes are never renamed or reassigned.
|
|
||||||
bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
|
|
||||||
const TrackByGuid& handleByGuid) {
|
|
||||||
bool changed = false;
|
|
||||||
|
|
||||||
std::map<std::string, std::vector<const LaneMint*>> mintsByTrack;
|
|
||||||
for (const LaneMint& m : plan.mints) mintsByTrack[m.trackGuid].push_back(&m);
|
|
||||||
std::map<std::string, std::vector<const LaneAssign*>> assignsByTrack;
|
|
||||||
for (const LaneAssign& a : plan.assigns) assignsByTrack[a.trackGuid].push_back(&a);
|
|
||||||
|
|
||||||
for (const LaneMintPlan::TrackSplit& split : plan.splits) {
|
|
||||||
MediaTrack* tr = resolve(handleByGuid, split.trackGuid);
|
|
||||||
if (!tr) continue; // stale GUID — prune
|
|
||||||
|
|
||||||
// A track not already in fixed-lane mode is one the tool is splitting
|
|
||||||
// now, so it owns the display; a track already at I_FREEMODE==2 (the
|
|
||||||
// user's own, or a prior tool run) skips this and keeps its display prefs.
|
|
||||||
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
|
||||||
if (freeMode != kFreeModeFixedLanes) {
|
|
||||||
SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast<double>(kFreeModeFixedLanes));
|
|
||||||
applyTransparentLaneDisplay(tr);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grow-only: a track may already carry the user's manual lanes, so the
|
|
||||||
// lane count only ever increases; managed lanes occupy the tail ordinals.
|
|
||||||
int laneCount = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
|
||||||
|
|
||||||
std::map<std::string, int> present = managedLaneOrdinals(tr);
|
|
||||||
|
|
||||||
for (const LaneMint* m : mintsByTrack[split.trackGuid]) {
|
|
||||||
model.lanes().setManaged(m->trackGuid, m->laneKey, m->modeId); // ownership
|
|
||||||
if (present.count(m->laneKey)) continue; // already minted — idempotent
|
|
||||||
|
|
||||||
const int laneIdx = laneCount++;
|
|
||||||
SetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES", static_cast<double>(laneCount));
|
|
||||||
char parm[32];
|
|
||||||
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
|
|
||||||
std::vector<char> name(m->laneKey.begin(), m->laneKey.end());
|
|
||||||
name.push_back('\0');
|
|
||||||
GetSetMediaTrackInfo_String(tr, parm, name.data(), true);
|
|
||||||
present.emplace(m->laneKey, laneIdx); // now resolvable for the assign pass
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::map<std::string, int> ordinals = managedLaneOrdinals(tr);
|
|
||||||
const std::map<std::string, MediaItem*> itemsByGuid = itemHandlesByGuid(tr);
|
|
||||||
for (const LaneAssign* a : assignsByTrack[split.trackGuid]) {
|
|
||||||
auto ord = ordinals.find(a->laneKey);
|
|
||||||
if (ord == ordinals.end()) continue; // key not live — prune, never mis-assign
|
|
||||||
auto handle = itemsByGuid.find(a->itemGuid);
|
|
||||||
if (handle == itemsByGuid.end()) continue; // stale item GUID — prune
|
|
||||||
if (assignItemToLane(tr, handle->second, ord->second)) changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool transportBlocksModeSwitch(ReaProject* proj) {
|
bool transportBlocksModeSwitch(ReaProject* proj) {
|
||||||
@@ -386,9 +69,9 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
return false; // reject before touching the project — no partial apply
|
return false; // reject before touching the project — no partial apply
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pinned ONCE: begin and end sit a whole apply apart with the pump premise between
|
// Pinned ONCE: resolving "current project" independently at the begin and the end
|
||||||
// them, so resolving "current project" independently at each would let a tab switch
|
// would let a tab switch mid-apply open the block on one project and close it on
|
||||||
// mid-apply open the block on one project and close it on another.
|
// another.
|
||||||
ReaProject* const project = proj ? proj : EnumProjects(-1, nullptr, 0);
|
ReaProject* const project = proj ? proj : EnumProjects(-1, nullptr, 0);
|
||||||
if (!ValidatePtr2(nullptr, project, "ReaProject*")) return false;
|
if (!ValidatePtr2(nullptr, project, "ReaProject*")) return false;
|
||||||
|
|
||||||
@@ -405,13 +88,12 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
|
|
||||||
TrackHandles handleByGuid;
|
TrackHandles handleByGuid;
|
||||||
std::vector<TrackFolderEntry> entries = readFolderEntries(project, handleByGuid);
|
std::vector<TrackFolderEntry> entries = readFolderEntries(project, handleByGuid);
|
||||||
TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
const TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
||||||
FolderTree tree = buildFolderTree(entries);
|
FolderTree tree = buildFolderTree(entries);
|
||||||
|
|
||||||
// Prune snapshots for tracks no longer in the live enumeration before
|
// Prune records for tracks no longer in the live enumeration before planning
|
||||||
// planning (membership is intentionally left alone — see model.reconcile).
|
// (membership is intentionally left alone — see model.reconcile). Because
|
||||||
// Because reapply-on-load routes through applyMode, this also reconciles
|
// reapply-on-load routes through applyMode, this also reconciles on project open.
|
||||||
// on project open.
|
|
||||||
std::set<std::string> liveGuids;
|
std::set<std::string> liveGuids;
|
||||||
for (const auto& kv : handleByGuid) liveGuids.insert(kv.first);
|
for (const auto& kv : handleByGuid) liveGuids.insert(kv.first);
|
||||||
model.reconcile(liveGuids);
|
model.reconcile(liveGuids);
|
||||||
@@ -430,12 +112,9 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
"ReaSampler: activate " +
|
"ReaSampler: activate " +
|
||||||
(targetMode ? targetMode->displayName : targetModeId) + " view";
|
(targetMode ? targetMode->displayName : targetModeId) + " view";
|
||||||
|
|
||||||
int undoMask = kApplyUndoMask;
|
|
||||||
bool laneModeChanged = false;
|
bool laneModeChanged = false;
|
||||||
bool wrote = false; // anything at all this apply changed — see applyMintsUndoPoint
|
bool wrote = false; // anything at all this apply changed — see applyMintsUndoPoint
|
||||||
std::vector<std::string> refusedParkNames;
|
std::vector<std::string> exposedNames; // named while handles are fresh, reported after the block
|
||||||
FxRestoreDrops fxDrops;
|
|
||||||
int fxDropTracks = 0;
|
|
||||||
|
|
||||||
Undo_BeginBlock2(project);
|
Undo_BeginBlock2(project);
|
||||||
{
|
{
|
||||||
@@ -448,55 +127,36 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
clearTrackSolos(handleByGuid, outgoingSolo);
|
clearTrackSolos(handleByGuid, outgoingSolo);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PARK / RESTORE: view_fx_park owns one track's whole move, flags and
|
// PARK / RESTORE: view_park owns one track's whole move. Flags only — the
|
||||||
// per-FX offline together. A refusal is named HERE, while the handle is
|
// parked chain stays online until the reclaimer reaches it.
|
||||||
// fresh — never held past Undo_EndBlock2/TrackList_AdjustWindows.
|
|
||||||
for (const TrackPlan& tp : plan.park) {
|
for (const TrackPlan& tp : plan.park) {
|
||||||
if (tp.flags.empty()) continue; // every op in a TrackPlan targets one track
|
if (tp.flags.empty()) continue; // every op in a TrackPlan targets one track
|
||||||
const std::string& guid = tp.flags.front().guid;
|
const std::string& guid = tp.flags.front().guid;
|
||||||
MediaTrack* tr = resolve(trackByGuid, guid);
|
MediaTrack* tr = resolve(trackByGuid, guid);
|
||||||
// Re-validated per track, not once at resolve: offlining a plugin is
|
if (!tr) continue; // stale/deleted GUID — prune
|
||||||
// ASSUMED to pump the message loop, so a track deleted during an
|
const TrackParkResult r = parkTrack(model, guid, tr, tp.flags);
|
||||||
// earlier iteration's FX writes would leave this handle dangling.
|
|
||||||
if (!tr || !trackStillLive(project, tr)) continue; // stale/deleted GUID — prune
|
|
||||||
|
|
||||||
const TrackApplyResult r = parkTrack(model, guid, tr, tp.flags);
|
|
||||||
if (r.refused) { refusedParkNames.push_back(trackDisplayName(tr)); continue; }
|
|
||||||
if (r.wrote) wrote = true;
|
if (r.wrote) wrote = true;
|
||||||
if (r.fxMoved) undoMask |= UNDO_STATE_FX;
|
// Named here, not after the block: TrackList_AdjustWindows below can
|
||||||
|
// invalidate the handle.
|
||||||
|
if (r.exposed) exposedNames.push_back(trackDisplayName(tr));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const TrackPlan& tp : plan.restore) {
|
for (const TrackPlan& tp : plan.restore) {
|
||||||
if (tp.flags.empty()) continue;
|
if (tp.flags.empty()) continue;
|
||||||
const std::string& guid = tp.flags.front().guid;
|
const std::string& guid = tp.flags.front().guid;
|
||||||
MediaTrack* tr = resolve(trackByGuid, guid);
|
MediaTrack* tr = resolve(trackByGuid, guid);
|
||||||
if (!tr || !trackStillLive(project, tr)) continue; // stale/deleted GUID — prune
|
if (!tr) continue; // stale/deleted GUID — prune
|
||||||
|
if (restoreTrack(model, guid, tr, tp.flags)) wrote = true;
|
||||||
const std::vector<FxOfflineOp> fxOps = view::fxRestoreTargets(model, guid);
|
|
||||||
const TrackApplyResult r = restoreTrack(model, guid, tr, fxOps, tp.flags);
|
|
||||||
if (r.wrote) wrote = true;
|
|
||||||
if (r.fxMoved) undoMask |= UNDO_STATE_FX;
|
|
||||||
if (r.drops.total() > 0) { fxDrops.add(r.drops); ++fxDropTracks; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The FX writes above are the only thing here assumed to pump the message
|
// MANAGED LANES: the active mode's lane plays+shows, every other managed lane
|
||||||
// loop, so this is the one point a handle taken before them can have died.
|
// is silenced+hidden. Empty on a D1-only project — byte-identical there.
|
||||||
// Re-validated for the whole enumeration rather than per consumer: lane ops,
|
|
||||||
// parent visibility and the solo replay all write through pre-FX handles.
|
|
||||||
for (auto& kv : handleByGuid) {
|
|
||||||
if (trackStillLive(project, kv.second)) continue;
|
|
||||||
trackByGuid.erase(kv.first);
|
|
||||||
kv.second = nullptr; // view_solo's writers already skip a null handle
|
|
||||||
}
|
|
||||||
|
|
||||||
// MANAGED LANES: the active mode's lane plays+shows, every other managed
|
|
||||||
// lane is silenced+hidden. Empty on a D1-only project — byte-identical there.
|
|
||||||
const LaneApplyResult lanes = applyLaneOps(trackByGuid, plan.lanes);
|
const LaneApplyResult lanes = applyLaneOps(trackByGuid, plan.lanes);
|
||||||
laneModeChanged = lanes.freeMode;
|
laneModeChanged = lanes.freeMode;
|
||||||
if (lanes.wrote) wrote = true;
|
if (lanes.wrote) wrote = true;
|
||||||
|
|
||||||
// PARENT VISIBILITY (never parked): recomputed every toggle, never
|
// PARENT VISIBILITY (never parked): recomputed every toggle, never recorded.
|
||||||
// snapshotted. Only the two visibility flags — never mainSend/FX on a parent.
|
// Only the two visibility flags — never mainSend/FX on a parent.
|
||||||
std::set<std::string> visible = model.visibleTracks(tree, targetModeId);
|
std::set<std::string> visible = model.visibleTracks(tree, targetModeId);
|
||||||
for (const FolderNode& node : tree.nodes) {
|
for (const FolderNode& node : tree.nodes) {
|
||||||
if (!node.isParent) continue;
|
if (!node.isParent) continue;
|
||||||
@@ -511,9 +171,8 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
|
|
||||||
// Replay + consume, before the single relayout below picks the change up.
|
// Replay + consume, before the single relayout below picks the change up.
|
||||||
// Dropped against `visible` (computed above for parent visibility), not the
|
// Dropped against `visible` (computed above for parent visibility), not the
|
||||||
// park plan: a folder parent can be hidden without being parked, and a
|
// park plan: a folder parent can be hidden without being parked, and a hidden
|
||||||
// hidden track must not receive a replayed solo it carries no visible
|
// track must not receive a replayed solo it carries no visible control to undo.
|
||||||
// control to undo.
|
|
||||||
if (realSwitch) {
|
if (realSwitch) {
|
||||||
if (const std::map<std::string, int>* cached = model.soloCache().query(targetModeId)) {
|
if (const std::map<std::string, int>* cached = model.soloCache().query(targetModeId)) {
|
||||||
restoreTrackSolos(handleByGuid, *cached, liveGuids, visible);
|
restoreTrackSolos(handleByGuid, *cached, liveGuids, visible);
|
||||||
@@ -523,105 +182,26 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Force REAPER to rebuild the TCP/MCP now rather than on the next user
|
// Force REAPER to rebuild the TCP/MCP now rather than on the next user
|
||||||
// interaction — AdjustWindows(false) is the full relayout tracks appearing
|
// interaction — AdjustWindows(false) is the full relayout tracks appearing and
|
||||||
// and disappearing owes.
|
// disappearing owes.
|
||||||
TrackList_AdjustWindows(false);
|
TrackList_AdjustWindows(false);
|
||||||
UpdateArrange();
|
UpdateArrange();
|
||||||
|
|
||||||
// UpdateTimeline() is owed only when a track was actually toggled into
|
// UpdateTimeline() is owed only when a track was actually toggled into fixed lanes
|
||||||
// fixed lanes this apply (SDK requirement for I_FREEMODE changes).
|
// this apply (SDK requirement for I_FREEMODE changes).
|
||||||
if (laneModeChanged) UpdateTimeline();
|
if (laneModeChanged) UpdateTimeline();
|
||||||
|
|
||||||
const bool mint = applyMintsUndoPoint(realSwitch, wrote);
|
const bool mint = applyMintsUndoPoint(realSwitch, wrote);
|
||||||
Undo_EndBlock2(project, mint ? undoLabel.c_str() : "", mint ? undoMask : 0);
|
Undo_EndBlock2(project, mint ? undoLabel.c_str() : "", mint ? kViewUndoMask : 0);
|
||||||
|
|
||||||
// After the undo block and the UI hold: a report is not a project write, and
|
// After the undo block: an EXPOSE report is a console message, not a project
|
||||||
// it must not join what a Ctrl-Z rolls back.
|
// write, and must not join what a Ctrl-Z rolls back.
|
||||||
reportRefusedParks(project, refusedParkNames);
|
reportExposedTracks(project, exposedNames);
|
||||||
reportFxRestoreDrops(fxDrops, fxDropTracks);
|
|
||||||
|
// The tool's own account of what it just left driven — the only one that survives
|
||||||
|
// an undo rolling the model back underneath it.
|
||||||
|
noteAppliedState(project, model);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
|
||||||
TrackHandles handleByGuid;
|
|
||||||
std::vector<TrackFolderEntry> entries = readFolderEntries(proj, handleByGuid);
|
|
||||||
// The tree is needed to detect a content-bearing folder derived-visible in
|
|
||||||
// >1 mode, exactly as applyMode builds it.
|
|
||||||
const FolderTree tree = buildFolderTree(entries);
|
|
||||||
|
|
||||||
const std::vector<LaneTrack> tracks = readLaneTracks(model, handleByGuid);
|
|
||||||
const LaneMintPlan plan = planLaneMinting(model, tree, tracks);
|
|
||||||
if (plan.empty()) return false; // nothing to mint — no Undo point for a no-op tick
|
|
||||||
|
|
||||||
const TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
|
||||||
|
|
||||||
// `proj` deliberately NOT pinned the way applyMode pins it: nothing between this
|
|
||||||
// begin and its end pumps the message loop, so no tab switch can land between them.
|
|
||||||
Undo_BeginBlock2(proj);
|
|
||||||
const bool changed = applyMintPlan(model, plan, trackByGuid);
|
|
||||||
|
|
||||||
if (!changed) {
|
|
||||||
// Plan was non-empty but every write was already satisfied — discard
|
|
||||||
// the empty undo point rather than flooding history every detect tick.
|
|
||||||
Undo_EndBlock2(proj, "", 0);
|
|
||||||
|
|
||||||
// The arrange still needs a redraw: this no-op path is reached when a
|
|
||||||
// freshly-inserted item already landed on the active mode's playing
|
|
||||||
// lane (REAPER places new items on the playing lane), so
|
|
||||||
// assignItemToLane wrote nothing even though the item needs to appear
|
|
||||||
// there now. UpdateArrange() alone (no I_FREEMODE change happened, so
|
|
||||||
// UpdateTimeline isn't owed) is a repaint, not a mutation — stays
|
|
||||||
// outside the undo block.
|
|
||||||
UpdateArrange();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reapply the active mode's lane visibility so freshly-minted lanes take
|
|
||||||
// their play/show state immediately. applyMode is deliberately NOT reused
|
|
||||||
// here — it would re-park/restore whole tracks and recompute parent
|
|
||||||
// visibility, which a lane-only mint must not touch.
|
|
||||||
const TogglePlan togglePlan = model.planToggle(FolderTree{}, model.activeModeId());
|
|
||||||
applyLaneOps(trackByGuid, togglePlan.lanes);
|
|
||||||
|
|
||||||
UpdateTimeline(); // a split happened this call — refresh is owed
|
|
||||||
UpdateArrange();
|
|
||||||
|
|
||||||
// Lane minting writes track lane config and item lane assignment and nothing
|
|
||||||
// else — never an FX state — so it takes the same honest mask applyMode does
|
|
||||||
// (kApplyUndoMask, whose [verify — DAW] on lane-property domain binds here too).
|
|
||||||
Undo_EndBlock2(proj, "ReaSampler: separate cross-mode content into lanes", kApplyUndoMask);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
|
||||||
TrackHandles handleByGuid;
|
|
||||||
readFolderEntries(proj, handleByGuid); // populates handleByGuid (tree unused here)
|
|
||||||
|
|
||||||
// Pure read of REAPER state (no lane created, no I_FREEMODE/I_NUMFIXEDLANES/
|
|
||||||
// I_FIXEDLANE written) plus an ownership-index write, recovering managed
|
|
||||||
// classification from the durable name. An unprefixed lane is left alone.
|
|
||||||
for (const auto& [guid, tr] : handleByGuid) {
|
|
||||||
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
|
||||||
if (freeMode != kFreeModeFixedLanes) continue; // no fixed lanes ⇒ nothing managed
|
|
||||||
|
|
||||||
const int numLanes = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
|
||||||
for (int lane = 0; lane < numLanes; ++lane) {
|
|
||||||
const std::string name = laneName(tr, lane);
|
|
||||||
std::optional<std::string> key = managedLaneKey(name);
|
|
||||||
if (!key) continue; // manual/unnamed lane — leave off the index
|
|
||||||
std::optional<std::string> mode = modeIdFromLaneName(name);
|
|
||||||
if (!mode) continue; // prefix-only/illegal name — skip defensively
|
|
||||||
|
|
||||||
// A mode id encoded in the name may no longer be registered (e.g.
|
|
||||||
// removed since the project was saved). Recording it managed would
|
|
||||||
// make the toggle planner drive a lane keyed to a mode that can
|
|
||||||
// never be active — permanently silenced, orphaning its items. So
|
|
||||||
// skip: the lane stays off the index (manual-by-default) but keeps
|
|
||||||
// its name, so a later re-registration of the mode heals cleanly.
|
|
||||||
if (!model.modes().contains(*mode)) continue;
|
|
||||||
model.lanes().setManaged(guid, *key, *mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|||||||
+31
-42
@@ -1,10 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// REAPER-facing shell of Design View (D2): reads the live folder tree, runs
|
// REAPER-facing shell of Design View: reads the live folder tree, runs
|
||||||
// ViewModeModel's pure planner, and applies the resulting flag / per-FX /
|
// ViewModeModel's pure planner, and applies the resulting flag / lane / parent
|
||||||
// lane writes. The .cpp is the sole REAPER-facing TU here (CLAUDE.md contract:
|
// writes under ONE undo block. See src/shell/view/CLAUDE.md for the enforced
|
||||||
// only main.cpp defines the API pointers). See src/shell/view/CLAUDE.md for
|
// invariants (never touch master/mute, solo cached per mode and never lost,
|
||||||
// the enforced invariants (never touch master/mute, solo cached per mode and
|
// record-based restore).
|
||||||
// never lost, snapshot-based restore).
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -25,51 +24,41 @@ bool transportBlocksModeSwitch(ReaProject* proj);
|
|||||||
|
|
||||||
// Does one applyMode leave an undo point behind?
|
// Does one applyMode leave an undo point behind?
|
||||||
//
|
//
|
||||||
// A REAPPLY that wrote nothing must not: the load-tick reapply over a project
|
// A REAPPLY that wrote nothing must not: the load-tick reapply over a project saved
|
||||||
// saved fully parked re-plans a park for every inactive leaf and finds every
|
// fully parked re-plans a park for every inactive leaf and finds every flag already
|
||||||
// flag and every FX already where it wants them, so a point there would mean
|
// where it wants them, so a point there would mean opening a project costs an undo
|
||||||
// opening a project costs an undo step. `Undo_EndBlock2(proj, "", 0)` is the
|
// step. `Undo_EndBlock2(proj, "", 0)` is the discard form (mintManagedLanes' no-op
|
||||||
// discard form (mintManagedLanes' no-op path uses the same idiom).
|
// path and the reconcile pass use the same idiom).
|
||||||
//
|
//
|
||||||
// A real switch mints regardless, fail-safe: it is an explicitly fired action
|
// A real switch mints regardless, fail-safe: it is an explicitly fired action and has
|
||||||
// and has to stay undoable even in the degenerate case where the plan found
|
// to stay undoable even in the degenerate case where the plan found nothing to write.
|
||||||
// nothing to write. Note what applyMode CANNOT see when it decides — the
|
// Note what applyMode CANNOT see when it decides — the `view_state` ext-state write
|
||||||
// `view_state` ext-state write lands in persistViewState, after this block has
|
// lands in persistViewState, after this block has closed (src/shell/view/CLAUDE.md's
|
||||||
// closed (src/shell/view/CLAUDE.md's Gotchas).
|
// Gotchas).
|
||||||
inline bool applyMintsUndoPoint(bool realSwitch, bool wroteAnything) {
|
inline bool applyMintsUndoPoint(bool realSwitch, bool wroteAnything) {
|
||||||
return realSwitch || wroteAnything;
|
return realSwitch || wroteAnything;
|
||||||
}
|
}
|
||||||
|
|
||||||
// True while an applyMode call is on the stack. applyMode fails closed when re-entered
|
// True while an applyMode call is on the stack. applyMode fails closed when
|
||||||
// (its FX writes are assumed to pump the message loop, so a timer tick or an action can
|
// re-entered, and this lets a caller with no fallback test BEFORE it spends a
|
||||||
// land inside one), and this lets a caller with no fallback test BEFORE it spends a
|
|
||||||
// one-shot signal on a call that would be refused — see main.cpp's load glue.
|
// one-shot signal on a call that would be refused — see main.cpp's load glue.
|
||||||
bool modeApplyInProgress();
|
bool modeApplyInProgress();
|
||||||
|
|
||||||
// Snapshots each about-to-park track's flags into `model`, caches/clears the
|
// Captures each about-to-park track's prior flag values into `model`, caches/clears
|
||||||
// outgoing mode's solo state and replays the incoming mode's, runs planToggle,
|
// the outgoing mode's solo state and replays the incoming mode's, runs planToggle,
|
||||||
// applies park/restore writes — flags AND per-FX offline, synchronously — plus
|
// applies the park/restore flag writes plus managed-lane and parent-visibility
|
||||||
// parent visibility flags, then sets the active mode. ONE switch is ONE undo
|
// writes, then sets the active mode. ONE switch is ONE undo point; everything it
|
||||||
// point; everything it moved rolls back in a single Ctrl-Z. Returns false (no
|
// moved rolls back in a single Ctrl-Z.
|
||||||
// mutation) if `targetModeId` isn't registered, if this is a real switch
|
//
|
||||||
// (target != active) while the transport is running, if it is re-entered
|
// FX are NOT written here. Offlining a parked chain is the reclaimer's job, off this
|
||||||
|
// path: TrackFX_SetOffline was 1897ms of a measured 1942ms switch, against 0.02ms for
|
||||||
|
// all the flag work below. Do not reintroduce a synchronous FX write.
|
||||||
|
//
|
||||||
|
// Returns false (no mutation) if `targetModeId` isn't registered, if this is a real
|
||||||
|
// switch (target != active) while the transport is running, if it is re-entered
|
||||||
// (modeApplyInProgress), or if the project does not validate. A reapply (target ==
|
// (modeApplyInProgress), or if the project does not validate. A reapply (target ==
|
||||||
// active) is never gated and never touches solo. `proj` == nullptr means the
|
// active) is never gated and never touches solo. `proj` == nullptr means the current
|
||||||
// current project, resolved ONCE and used for every call this apply makes.
|
// project, resolved ONCE and used for every call this apply makes.
|
||||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj);
|
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj);
|
||||||
|
|
||||||
// Splits any track visible in more than one mode while carrying its own media
|
|
||||||
// into fixed lanes (one managed lane per involved mode), assigns items, and
|
|
||||||
// records ownership in `model`. Never touches manual lanes. Runs planLaneMinting
|
|
||||||
// (model + tree aware); wraps the mutation in one Undo block when non-empty.
|
|
||||||
// Returns true if any lane was minted (repaint hint). `proj` == nullptr means
|
|
||||||
// the current project.
|
|
||||||
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj);
|
|
||||||
|
|
||||||
// Recovers managed-lane ownership from durable P_LANENAME on project open —
|
|
||||||
// self-healing, without minting/reassigning anything and without touching
|
|
||||||
// membership. A lane without the managed prefix is left untouched (manual).
|
|
||||||
// `proj` == nullptr means the current project.
|
|
||||||
void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj);
|
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|||||||
@@ -1,229 +0,0 @@
|
|||||||
// 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
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
// Design View's park surface: one track parked or restored — the trust test,
|
|
||||||
// the snapshot lifecycle, and the flag + per-FX writes both halves address
|
|
||||||
// through this one file. Every write declared here runs synchronously inside
|
|
||||||
// applyMode's own undo block, which is what makes one switch one undo point —
|
|
||||||
// see src/shell/view/CLAUDE.md's FX-parking caveat.
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "core/view/fx_offline.h"
|
|
||||||
#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 {
|
|
||||||
|
|
||||||
// -- may this chain be snapshotted? -------------------------------------------
|
|
||||||
//
|
|
||||||
// A pre-park snapshot may only ever be taken from a chain NO park has touched.
|
|
||||||
// Its values are restore's sole source of truth, so a snapshot lifted from an
|
|
||||||
// already-parked chain records park state as the user's state, and every later
|
|
||||||
// restore then faithfully writes hidden/out-of-mix/FX-disabled back — permanent
|
|
||||||
// and silent. A model carrying no snapshot is NOT evidence the chain is clean;
|
|
||||||
// src/shell/view/CLAUDE.md lists the routes that still reach that pair.
|
|
||||||
//
|
|
||||||
// A snapshot has TWO halves and the test covers both, disjunctively: the flags
|
|
||||||
// can read clean while the FX are still offline (a hand recovery that stopped at
|
|
||||||
// the flags, one hand-offlined plugin) and vice versa. Either half reading
|
|
||||||
// parked poisons the whole snapshot, so either half alone refuses.
|
|
||||||
|
|
||||||
// The REAPER track parameter each driven flag addresses. One home for the
|
|
||||||
// mapping — park's live read, restore's write and the trust test below 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Does the track already sit at every value this park would write? Then a park
|
|
||||||
// reached it. `readFlag` answers one Flag's live value; templating on it lets the
|
|
||||||
// live path compare as it reads (no per-track vector) while the tests drive the
|
|
||||||
// same fold from a table, keyed by Flag rather than by plan order. An empty plan
|
|
||||||
// is evidence of nothing and answers false.
|
|
||||||
template <class ReadFlag>
|
|
||||||
bool parkFlagsAlreadyApplied(const std::vector<TrackFlagOp>& parkFlags, ReadFlag readFlag) {
|
|
||||||
if (parkFlags.empty()) return false;
|
|
||||||
for (const TrackFlagOp& op : parkFlags)
|
|
||||||
if (readFlag(op.flag) != op.value) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DISJUNCTIVE fold — rationale is the snapshot-source invariant above (also
|
|
||||||
// src/shell/view/CLAUDE.md).
|
|
||||||
inline bool chainReadsParked(bool parkFlagsRead, bool anyFxOffline) {
|
|
||||||
return parkFlagsRead || anyFxOffline;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class ParkAction {
|
|
||||||
SnapshotThenPark, // clean chain — capture the pre-park truth, then park
|
|
||||||
ParkOnly, // still parked; the held snapshot IS the pre-park truth
|
|
||||||
Refuse, // no snapshot AND a parked chain — the truth is gone
|
|
||||||
};
|
|
||||||
|
|
||||||
// Refusing leaves the track exactly as found: no flag write, no FX write, no
|
|
||||||
// snapshot. Nothing false is committed, so the moment the user's own edit puts
|
|
||||||
// the chain back somewhere trustworthy the next park captures it correctly —
|
|
||||||
// where a snapshot of the parked state would have closed that door for good.
|
|
||||||
inline ParkAction decidePark(bool haveSnapshot, bool chainReadsParked) {
|
|
||||||
if (haveSnapshot) return ParkAction::ParkOnly;
|
|
||||||
return chainReadsParked ? ParkAction::Refuse : ParkAction::SnapshotThenPark;
|
|
||||||
}
|
|
||||||
|
|
||||||
// `decidePark` over the live track: the four driven flags AND the FX chain, which
|
|
||||||
// a snapshot taken here would record both halves of. Skips the reads on the
|
|
||||||
// ParkOnly path, where decidePark ignores the second term anyway.
|
|
||||||
ParkAction decideParkForTrack(MediaTrack* tr, bool haveSnapshot,
|
|
||||||
const std::vector<TrackFlagOp>& parkFlags);
|
|
||||||
|
|
||||||
// "Track N" for an unnamed track (GetTrackName, matching capture::trackName's
|
|
||||||
// choice of API for the same reason). Exported so a caller holding the handle
|
|
||||||
// only briefly (view.cpp's park loop, before Undo_EndBlock2/TrackList_AdjustWindows
|
|
||||||
// invalidate it) can capture the name at resolve time rather than the handle.
|
|
||||||
std::string trackDisplayName(MediaTrack* tr);
|
|
||||||
|
|
||||||
// The console report for the tracks one apply refused, empty when none. NAMES
|
|
||||||
// them: a refused track is hidden from both the TCP and the mixer, so a count
|
|
||||||
// alone points at nothing the user can go and find. It asserts no cause — the
|
|
||||||
// pair it detects has several routes (src/shell/view/CLAUDE.md), and only some of
|
|
||||||
// them are anyone's mistake.
|
|
||||||
inline std::string describeRefusedParks(const std::vector<std::string>& trackNames) {
|
|
||||||
if (trackNames.empty()) return {};
|
|
||||||
std::string msg = "ReaSampler: " + std::to_string(trackNames.size()) + " track" +
|
|
||||||
(trackNames.size() == 1 ? "" : "s") +
|
|
||||||
" left unparked -- no stored pre-park state, and a live state that "
|
|
||||||
"already reads as parked; parking now would record that as your own "
|
|
||||||
"state.\n";
|
|
||||||
for (const std::string& name : trackNames) msg += " " + name + "\n";
|
|
||||||
msg += "To bring a track back under mode control: restore its TCP and mixer visibility, "
|
|
||||||
"its main send and its FX enable, AND set every FX in its chain online -- FX enable "
|
|
||||||
"is the chain bypass and does not bring an individually offlined FX back. The next "
|
|
||||||
"mode switch will then park it correctly.\n";
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pure gate behind reportRefusedParks' one-per-changed-set memo: prints (i.e.
|
|
||||||
// updates the memo) on any change of owner or of the named set, stays silent on
|
|
||||||
// an exact repeat. An empty `now` still counts as a change from a non-empty
|
|
||||||
// `last` — that update itself prints nothing (describeRefusedParks({}) is
|
|
||||||
// empty) but resets the memo so a later refusal is never mistaken for a repeat
|
|
||||||
// of one that already healed.
|
|
||||||
inline bool shouldReport(bool sameOwnerAsLast, const std::vector<std::string>& last,
|
|
||||||
const std::vector<std::string>& now) {
|
|
||||||
return !(sameOwnerAsLast && last == now);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prints describeRefusedParks for `refusedNames` (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 refusal recurs on every reapply — every project open,
|
|
||||||
// every tag/untag — and repeating it verbatim is noise the user cannot act on any
|
|
||||||
// better the second time. Gated by shouldReport.
|
|
||||||
void reportRefusedParks(ReaProject* proj, const std::vector<std::string>& refusedNames);
|
|
||||||
|
|
||||||
// -- parking and restoring one track ------------------------------------------
|
|
||||||
//
|
|
||||||
// Both run INSIDE applyMode's undo block, on the switch's own synchronous path,
|
|
||||||
// so one switch is one undo point.
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Is `tr` still a live track of `proj` (nullptr = current project)? The apply's
|
|
||||||
// loops re-ask per track: a plugin unloading is assumed to pump the message
|
|
||||||
// loop, so a handle resolved before the first FX write can be dead by the last.
|
|
||||||
// `proj` is resolved to a concrete pointer here because ValidatePtr2 documents
|
|
||||||
// no nullptr-means-current-project contract (reaper_plugin_functions.h:7899),
|
|
||||||
// and a false there would silently skip every track.
|
|
||||||
bool trackStillLive(ReaProject* proj, MediaTrack* tr);
|
|
||||||
|
|
||||||
// What one track's park or restore did, folded into the apply's block state.
|
|
||||||
struct TrackApplyResult {
|
|
||||||
bool wrote = false; // a project write or a snapshot moved — the undo-point verdict
|
|
||||||
bool fxMoved = false; // ⇒ the block's mask owes UNDO_STATE_FX
|
|
||||||
bool refused = false; // park only: the chain was left exactly as found
|
|
||||||
|
|
||||||
// Restore only: captured FX state resolveFxRestore could not place. Reported
|
|
||||||
// rather than swallowed — silence would read as "restore worked" while an FX
|
|
||||||
// sat at whatever state the park left it in.
|
|
||||||
FxRestoreDrops drops;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Parks one inactive leaf: the trust decision (decidePark), the pre-park
|
|
||||||
// snapshot when it is owed, then flags before FX so the track is out of the mix
|
|
||||||
// and bypassed before its plugins start unloading. On `refused` nothing at all
|
|
||||||
// was written — the caller names the track (trackDisplayName) while its handle
|
|
||||||
// is still fresh.
|
|
||||||
TrackApplyResult parkTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
|
||||||
const std::vector<TrackFlagOp>& flags);
|
|
||||||
|
|
||||||
// Restores one returning leaf: FX before flags, park's order mirrored, so the
|
|
||||||
// chain comes back online while still bypassed and out of the mix and the track
|
|
||||||
// never sounds through a half-restored chain. Consumes the snapshot — from that
|
|
||||||
// point the flags are back at their captured values, and a later reapply must
|
|
||||||
// not replan a restore over whatever the user has changed since.
|
|
||||||
TrackApplyResult restoreTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
|
||||||
const std::vector<FxOfflineOp>& fxOps,
|
|
||||||
const std::vector<TrackFlagOp>& flags);
|
|
||||||
|
|
||||||
// One console line for a whole apply's drops. Called after the undo block
|
|
||||||
// closes: a report is not a project write and must not join what a Ctrl-Z rolls
|
|
||||||
// back.
|
|
||||||
void reportFxRestoreDrops(const FxRestoreDrops& drops, int trackCount);
|
|
||||||
|
|
||||||
} // namespace reasampler
|
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
// See view_lanes.h. Compiled into the reaper_reasampler module; includes
|
||||||
|
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
||||||
|
// The lane decisions are pure (planToggle / planLaneMinting, core/view); this file
|
||||||
|
// owns the live lane reads and writes they resolve against.
|
||||||
|
|
||||||
|
#include "shell/view/view_lanes.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/view/lane_keys.h"
|
||||||
|
#include "core/view/view_tree.h"
|
||||||
|
#include "shell/capture/item_read.h"
|
||||||
|
#include "shell/view/view_park.h" // writeIfChanged — the read-before-write every driven parm takes
|
||||||
|
|
||||||
|
#define REAPERAPI_MINIMAL
|
||||||
|
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||||
|
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
||||||
|
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
|
||||||
|
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||||
|
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||||
|
#define REAPERAPI_WANT_UpdateArrange
|
||||||
|
#define REAPERAPI_WANT_UpdateTimeline
|
||||||
|
#define REAPERAPI_WANT_CountTrackMediaItems
|
||||||
|
#define REAPERAPI_WANT_GetTrackMediaItem
|
||||||
|
#define REAPERAPI_WANT_GetMediaItemInfo_Value
|
||||||
|
#define REAPERAPI_WANT_SetMediaItemInfo_Value
|
||||||
|
#include "reaper_plugin_functions.h"
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
using view::buildFolderTree;
|
||||||
|
using view::isOnManualLane;
|
||||||
|
using view::managedLaneKey;
|
||||||
|
using view::modeIdFromLaneName;
|
||||||
|
using view::TrackFolderEntry;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// I_FREEMODE value for fixed lanes. SDK: 0=normal, 1=free item positioning, 2=fixed lanes.
|
||||||
|
constexpr int kFreeModeFixedLanes = 2;
|
||||||
|
|
||||||
|
// C_LANESCOLLAPSED=2: render a tool-split track like a normal single-lane track
|
||||||
|
// showing only the playing lane (SDK: 1=collapsed, 2=hidden-lanes-exist but displays
|
||||||
|
// as non-fixed-lane).
|
||||||
|
constexpr int kLanesDisplayAsNormal = 2;
|
||||||
|
|
||||||
|
// C_LANESETTINGS &32 = hide per-lane buttons; OR'd in, never clobbering the mask.
|
||||||
|
// Deliberately NOT setting &1 (auto-remove empty lanes): the lazy-mint decision never
|
||||||
|
// mints an empty lane, so &1 buys nothing and risks REAPER silently removing a
|
||||||
|
// managed lane out from under the ownership index.
|
||||||
|
constexpr int kLaneSettingsHideButtons = 32;
|
||||||
|
|
||||||
|
// Makes a tool-split track's display read as an ordinary track. Gated by every caller
|
||||||
|
// on the tool-driven transition INTO fixed lanes (freeMode != 2 before the flip) — a
|
||||||
|
// track already in fixed-lane mode (the user's own) never reaches this, so a user's
|
||||||
|
// comp-lane display prefs are never stomped.
|
||||||
|
void applyTransparentLaneDisplay(MediaTrack* tr) {
|
||||||
|
SetMediaTrackInfo_Value(tr, "C_LANESCOLLAPSED",
|
||||||
|
static_cast<double>(kLanesDisplayAsNormal));
|
||||||
|
const int settings = static_cast<int>(GetMediaTrackInfo_Value(tr, "C_LANESETTINGS"));
|
||||||
|
SetMediaTrackInfo_Value(tr, "C_LANESETTINGS",
|
||||||
|
static_cast<double>(settings | kLaneSettingsHideButtons));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lane `laneIdx`'s durable name (P_LANENAME:n) on `tr`, or empty if unnamed /
|
||||||
|
// unavailable (non-fixed-lane track).
|
||||||
|
std::string laneName(MediaTrack* tr, int laneIdx) {
|
||||||
|
char parm[32];
|
||||||
|
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
|
||||||
|
char buf[512] = {0};
|
||||||
|
if (!GetSetMediaTrackInfo_String(tr, parm, buf, false)) return {};
|
||||||
|
return std::string(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Managed lane durable key -> current ordinal on `tr`. Manual lanes are omitted, so a
|
||||||
|
// key absent from the map must not be driven.
|
||||||
|
std::map<std::string, int> managedLaneOrdinals(MediaTrack* tr) {
|
||||||
|
std::map<std::string, int> byKey;
|
||||||
|
const int numLanes = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
||||||
|
for (int lane = 0; lane < numLanes; ++lane) {
|
||||||
|
std::optional<std::string> key = managedLaneKey(laneName(tr, lane));
|
||||||
|
if (key) byKey.emplace(*key, lane); // first ordinal wins if names collide
|
||||||
|
}
|
||||||
|
return byKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track-side C_LANEPLAYS:N alone hides+silences every item on lane N (SDK: item-side
|
||||||
|
// C_LANEPLAYS is read-only, so no per-item write exists or is needed).
|
||||||
|
// B_FIXEDLANE_HIDDEN is also read-only — hide/show follows from C_LANEPLAYS=0/1,
|
||||||
|
// never written directly.
|
||||||
|
bool applyLanePlays(MediaTrack* tr, int laneIdx, int lanePlays) {
|
||||||
|
char parm[32];
|
||||||
|
std::snprintf(parm, sizeof(parm), "C_LANEPLAYS:%d", laneIdx);
|
||||||
|
return writeIfChanged(tr, parm, static_cast<double>(lanePlays));
|
||||||
|
}
|
||||||
|
|
||||||
|
// One pass, so the assign loop needs no per-item re-scan.
|
||||||
|
std::map<std::string, MediaItem*> itemHandlesByGuid(MediaTrack* tr) {
|
||||||
|
std::map<std::string, MediaItem*> byGuid;
|
||||||
|
const int itemCount = CountTrackMediaItems(tr);
|
||||||
|
for (int i = 0; i < itemCount; ++i) {
|
||||||
|
MediaItem* it = GetTrackMediaItem(tr, i);
|
||||||
|
if (!it) continue;
|
||||||
|
std::string ig = itemGuid(it);
|
||||||
|
if (!ig.empty()) byGuid.emplace(std::move(ig), it);
|
||||||
|
}
|
||||||
|
return byGuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An untagged item is Arrange by default (mirrors leafBelongsToMode). A
|
||||||
|
// show-both/multi-mode item resolves to its first mode id — unusual for lane content,
|
||||||
|
// and any one mode is sufficient for the decision.
|
||||||
|
std::string itemModeFromMembership(const ViewModeModel& model, const std::string& itemGuid) {
|
||||||
|
const std::set<std::string> modes = model.membership().modesOf(itemGuid);
|
||||||
|
if (modes.empty()) return kArrangeModeId;
|
||||||
|
return *modes.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The per-track LaneItem picture the pure decision consumes. Manual-lane reads are
|
||||||
|
// skipped on a non-fixed-lane track (isOnManualLane is false there anyway).
|
||||||
|
std::vector<LaneTrack> readLaneTracks(const ViewModeModel& model,
|
||||||
|
const TrackHandles& handleByGuid) {
|
||||||
|
std::vector<LaneTrack> tracks;
|
||||||
|
tracks.reserve(handleByGuid.size());
|
||||||
|
for (const auto& [guid, tr] : handleByGuid) {
|
||||||
|
LaneTrack lt;
|
||||||
|
lt.trackGuid = guid;
|
||||||
|
|
||||||
|
const bool fixedLane =
|
||||||
|
static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes;
|
||||||
|
|
||||||
|
const int itemCount = CountTrackMediaItems(tr);
|
||||||
|
lt.items.reserve(static_cast<std::size_t>(itemCount));
|
||||||
|
for (int i = 0; i < itemCount; ++i) {
|
||||||
|
MediaItem* it = GetTrackMediaItem(tr, i);
|
||||||
|
if (!it) continue;
|
||||||
|
const std::string ig = itemGuid(it);
|
||||||
|
if (ig.empty()) continue;
|
||||||
|
LaneItem li;
|
||||||
|
li.guid = ig;
|
||||||
|
li.modeId = itemModeFromMembership(model, ig);
|
||||||
|
const std::string ln = fixedLane ? itemLaneName(tr, it) : std::string{};
|
||||||
|
li.onManualLane = isOnManualLane(fixedLane, ln);
|
||||||
|
lt.items.push_back(std::move(li));
|
||||||
|
}
|
||||||
|
tracks.push_back(std::move(lt));
|
||||||
|
}
|
||||||
|
return tracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idempotent, and non-destructive: only this reversible flag is written, never a move
|
||||||
|
// in time or across tracks.
|
||||||
|
bool assignItemToLane(MediaTrack* tr, MediaItem* it, int laneOrdinal) {
|
||||||
|
const int current = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
|
||||||
|
if (current == laneOrdinal) return false; // already there — no-op
|
||||||
|
SetMediaItemInfo_Value(it, "I_FIXEDLANE", static_cast<double>(laneOrdinal));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applies the pure LaneMintPlan. Returns true if any project write actually changed
|
||||||
|
// state (⇒ caller keeps the Undo block and refreshes the timeline).
|
||||||
|
//
|
||||||
|
// The plan only ever names managed-prefixed lanes and only ever assigns
|
||||||
|
// managed-eligible items; I_NUMFIXEDLANES is only ever GROWN, never shrunk, so a
|
||||||
|
// user's existing manual lanes are never renamed or reassigned.
|
||||||
|
bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
|
||||||
|
const TrackByGuid& handleByGuid) {
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
std::map<std::string, std::vector<const LaneMint*>> mintsByTrack;
|
||||||
|
for (const LaneMint& m : plan.mints) mintsByTrack[m.trackGuid].push_back(&m);
|
||||||
|
std::map<std::string, std::vector<const LaneAssign*>> assignsByTrack;
|
||||||
|
for (const LaneAssign& a : plan.assigns) assignsByTrack[a.trackGuid].push_back(&a);
|
||||||
|
|
||||||
|
for (const LaneMintPlan::TrackSplit& split : plan.splits) {
|
||||||
|
MediaTrack* tr = resolve(handleByGuid, split.trackGuid);
|
||||||
|
if (!tr) continue; // stale GUID — prune
|
||||||
|
|
||||||
|
// A track not already in fixed-lane mode is one the tool is splitting now, so
|
||||||
|
// it owns the display; a track already at I_FREEMODE==2 (the user's own, or a
|
||||||
|
// prior tool run) skips this and keeps its display prefs.
|
||||||
|
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||||
|
if (freeMode != kFreeModeFixedLanes) {
|
||||||
|
SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast<double>(kFreeModeFixedLanes));
|
||||||
|
applyTransparentLaneDisplay(tr);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grow-only: a track may already carry the user's manual lanes, so the lane
|
||||||
|
// count only ever increases; managed lanes occupy the tail ordinals.
|
||||||
|
int laneCount = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
||||||
|
|
||||||
|
std::map<std::string, int> present = managedLaneOrdinals(tr);
|
||||||
|
|
||||||
|
for (const LaneMint* m : mintsByTrack[split.trackGuid]) {
|
||||||
|
model.lanes().setManaged(m->trackGuid, m->laneKey, m->modeId); // ownership
|
||||||
|
if (present.count(m->laneKey)) continue; // already minted — idempotent
|
||||||
|
|
||||||
|
const int laneIdx = laneCount++;
|
||||||
|
SetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES", static_cast<double>(laneCount));
|
||||||
|
char parm[32];
|
||||||
|
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
|
||||||
|
std::vector<char> name(m->laneKey.begin(), m->laneKey.end());
|
||||||
|
name.push_back('\0');
|
||||||
|
GetSetMediaTrackInfo_String(tr, parm, name.data(), true);
|
||||||
|
present.emplace(m->laneKey, laneIdx); // now resolvable for the assign pass
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::map<std::string, int> ordinals = managedLaneOrdinals(tr);
|
||||||
|
const std::map<std::string, MediaItem*> itemsByGuid = itemHandlesByGuid(tr);
|
||||||
|
for (const LaneAssign* a : assignsByTrack[split.trackGuid]) {
|
||||||
|
auto ord = ordinals.find(a->laneKey);
|
||||||
|
if (ord == ordinals.end()) continue; // key not live — prune, never mis-assign
|
||||||
|
auto handle = itemsByGuid.find(a->itemGuid);
|
||||||
|
if (handle == itemsByGuid.end()) continue; // stale item GUID — prune
|
||||||
|
if (assignItemToLane(tr, handle->second, ord->second)) changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
LaneApplyResult applyLaneOps(const TrackByGuid& handleByGuid,
|
||||||
|
const std::vector<LanePlayOp>& lanes) {
|
||||||
|
LaneApplyResult out;
|
||||||
|
if (lanes.empty()) return out;
|
||||||
|
|
||||||
|
std::map<std::string, std::vector<const LanePlayOp*>> byTrack;
|
||||||
|
for (const LanePlayOp& op : lanes) byTrack[op.trackGuid].push_back(&op);
|
||||||
|
|
||||||
|
for (const auto& [guid, ops] : byTrack) {
|
||||||
|
MediaTrack* tr = resolve(handleByGuid, guid);
|
||||||
|
if (!tr) continue; // stale GUID — prune
|
||||||
|
|
||||||
|
// Every track reaching here already owns a managed lane (planToggle only emits
|
||||||
|
// ops for managed lanes), so re-asserting fixed-lane mode is always a
|
||||||
|
// tool-driven (re)split — never a user's untouched manual-fixed-lane track —
|
||||||
|
// and gets the same transparent display.
|
||||||
|
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||||
|
if (freeMode != kFreeModeFixedLanes) {
|
||||||
|
SetMediaTrackInfo_Value(tr, "I_FREEMODE",
|
||||||
|
static_cast<double>(kFreeModeFixedLanes));
|
||||||
|
applyTransparentLaneDisplay(tr);
|
||||||
|
out.freeMode = true;
|
||||||
|
out.wrote = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::map<std::string, int> ordinals = managedLaneOrdinals(tr);
|
||||||
|
for (const LanePlayOp* op : ops) {
|
||||||
|
auto it = ordinals.find(op->laneKey);
|
||||||
|
if (it == ordinals.end()) continue; // key not live on this track — prune
|
||||||
|
if (applyLanePlays(tr, it->second, op->lanePlays)) out.wrote = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||||
|
TrackHandles handleByGuid;
|
||||||
|
std::vector<TrackFolderEntry> entries = readFolderEntries(proj, handleByGuid);
|
||||||
|
// The tree is needed to detect a content-bearing folder derived-visible in
|
||||||
|
// >1 mode, exactly as applyMode builds it.
|
||||||
|
const FolderTree tree = buildFolderTree(entries);
|
||||||
|
|
||||||
|
const std::vector<LaneTrack> tracks = readLaneTracks(model, handleByGuid);
|
||||||
|
const LaneMintPlan plan = planLaneMinting(model, tree, tracks);
|
||||||
|
if (plan.empty()) return false; // nothing to mint — no Undo point for a no-op tick
|
||||||
|
|
||||||
|
const TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
||||||
|
|
||||||
|
// `proj` deliberately NOT pinned the way applyMode pins it: nothing between this
|
||||||
|
// begin and its end pumps the message loop, so no tab switch can land between them.
|
||||||
|
Undo_BeginBlock2(proj);
|
||||||
|
const bool changed = applyMintPlan(model, plan, trackByGuid);
|
||||||
|
|
||||||
|
if (!changed) {
|
||||||
|
// Plan was non-empty but every write was already satisfied — discard the empty
|
||||||
|
// undo point rather than flooding history every detect tick.
|
||||||
|
Undo_EndBlock2(proj, "", 0);
|
||||||
|
|
||||||
|
// The arrange still needs a redraw: this no-op path is reached when a
|
||||||
|
// freshly-inserted item already landed on the active mode's playing lane
|
||||||
|
// (REAPER places new items on the playing lane), so assignItemToLane wrote
|
||||||
|
// nothing even though the item needs to appear there now. UpdateArrange()
|
||||||
|
// alone (no I_FREEMODE change happened, so UpdateTimeline isn't owed) is a
|
||||||
|
// repaint, not a mutation — stays outside the undo block.
|
||||||
|
UpdateArrange();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reapply the active mode's lane visibility so freshly-minted lanes take their
|
||||||
|
// play/show state immediately. applyMode is deliberately NOT reused here — it
|
||||||
|
// would re-park/restore whole tracks and recompute parent visibility, which a
|
||||||
|
// lane-only mint must not touch.
|
||||||
|
const TogglePlan togglePlan = model.planToggle(FolderTree{}, model.activeModeId());
|
||||||
|
applyLaneOps(trackByGuid, togglePlan.lanes);
|
||||||
|
|
||||||
|
UpdateTimeline(); // a split happened this call — refresh is owed
|
||||||
|
UpdateArrange();
|
||||||
|
|
||||||
|
Undo_EndBlock2(proj, "ReaSampler: separate cross-mode content into lanes", kViewUndoMask);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||||
|
TrackHandles handleByGuid;
|
||||||
|
readFolderEntries(proj, handleByGuid); // populates handleByGuid (tree unused here)
|
||||||
|
|
||||||
|
// Pure read of REAPER state (no lane created, no I_FREEMODE/I_NUMFIXEDLANES/
|
||||||
|
// I_FIXEDLANE written) plus an ownership-index write, recovering managed
|
||||||
|
// classification from the durable name. An unprefixed lane is left alone.
|
||||||
|
for (const auto& [guid, tr] : handleByGuid) {
|
||||||
|
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||||
|
if (freeMode != kFreeModeFixedLanes) continue; // no fixed lanes ⇒ nothing managed
|
||||||
|
|
||||||
|
const int numLanes = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
||||||
|
for (int lane = 0; lane < numLanes; ++lane) {
|
||||||
|
const std::string name = laneName(tr, lane);
|
||||||
|
std::optional<std::string> key = managedLaneKey(name);
|
||||||
|
if (!key) continue; // manual/unnamed lane — leave off the index
|
||||||
|
std::optional<std::string> mode = modeIdFromLaneName(name);
|
||||||
|
if (!mode) continue; // prefix-only/illegal name — skip defensively
|
||||||
|
|
||||||
|
// A mode id encoded in the name may no longer be registered (e.g. removed
|
||||||
|
// since the project was saved). Recording it managed would make the toggle
|
||||||
|
// planner drive a lane keyed to a mode that can never be active —
|
||||||
|
// permanently silenced, orphaning its items. So skip: the lane stays off
|
||||||
|
// the index (manual-by-default) but keeps its name, so a later
|
||||||
|
// re-registration of the mode heals cleanly.
|
||||||
|
if (!model.modes().contains(*mode)) continue;
|
||||||
|
model.lanes().setManaged(guid, *key, *mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
// Managed fixed lanes, REAPER side: minting one lane per mode on a track whose own
|
||||||
|
// content spans modes, driving the active mode's lane audible and every other
|
||||||
|
// managed lane silent+hidden, and recovering ownership from the durable P_LANENAME
|
||||||
|
// on project open. Manual lanes are never minted, driven, renamed or reassigned.
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/view/view_mode_model.h"
|
||||||
|
#include "shell/view/view_tracks.h"
|
||||||
|
|
||||||
|
// Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header.
|
||||||
|
class ReaProject;
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
// `freeMode` alone decides whether UpdateTimeline is owed (SDK: required after an
|
||||||
|
// I_FREEMODE change); `wrote` is any project write at all, which is what the apply's
|
||||||
|
// undo-point decision reads.
|
||||||
|
struct LaneApplyResult {
|
||||||
|
bool freeMode = false;
|
||||||
|
bool wrote = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reconciles each op's durable laneKey to the track's current ordinal (a
|
||||||
|
// stale/renamed/deleted key is pruned, never mis-driven) and drives C_LANEPLAYS.
|
||||||
|
LaneApplyResult applyLaneOps(const TrackByGuid& handleByGuid,
|
||||||
|
const std::vector<LanePlayOp>& lanes);
|
||||||
|
|
||||||
|
// Splits any track visible in more than one mode while carrying its own media into
|
||||||
|
// fixed lanes (one managed lane per involved mode), assigns items, and records
|
||||||
|
// ownership in `model`. Wraps the mutation in one Undo block when non-empty. Returns
|
||||||
|
// true if any lane was minted (repaint hint). `proj` == nullptr means the current
|
||||||
|
// project.
|
||||||
|
bool mintManagedLanes(ViewModeModel& model, ReaProject* proj);
|
||||||
|
|
||||||
|
// Recovers managed-lane ownership from durable P_LANENAME on project open —
|
||||||
|
// self-healing, without minting/reassigning anything and without touching
|
||||||
|
// membership. A lane without the managed prefix is left untouched (manual).
|
||||||
|
// `proj` == nullptr means the current project.
|
||||||
|
void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj);
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
// 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 <cstring> // strnlen — bounded read of GetTrackName's buffer
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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 {
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<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 int fxCount = TrackFX_GetCount(tr);
|
||||||
|
snap.fxOffline.reserve(static_cast<std::size_t>(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<TrackFlagOp>& flags) {
|
||||||
|
bool wrote = false;
|
||||||
|
for (const TrackFlagOp& op : flags)
|
||||||
|
if (writeIfChanged(tr, trackFlagParm(op.flag), static_cast<double>(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<TrackFlagOp>& 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<std::string> g_reportedExposed;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const int kViewUndoMask = UNDO_STATE_TRACKCFG | UNDO_STATE_ITEMS | UNDO_STATE_MISCCFG;
|
||||||
|
|
||||||
|
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<TrackFlagOp>& 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<int>(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<TrackFlagOp>& flags) {
|
||||||
|
applyFlags(tr, flags);
|
||||||
|
model.releaseSnapshotFlags(guid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void reportExposedTracks(ReaProject* proj, const std::vector<std::string>& 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
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
#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 <set>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<FxOfflineState> unionFxOfflineOnRepark(
|
||||||
|
const std::vector<FxOfflineState>& prior, const std::vector<FxOfflineState>& live) {
|
||||||
|
std::vector<FxOfflineState> merged = prior;
|
||||||
|
std::set<std::string> 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 <class ReadFlag>
|
||||||
|
bool chainReadsParked(const std::vector<TrackFlagOp>& 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<TrackFlagOp>& 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<std::string>& 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<std::string>& last,
|
||||||
|
const std::vector<std::string>& 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<std::string>& 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<TrackFlagOp>& flags);
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// See view_reconcile.h. Compiled into the reaper_reasampler module; includes
|
||||||
|
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
||||||
|
// The decision is pure (header, test_view_reconcile.cpp); this file owns the memo's
|
||||||
|
// lifetime and the writes.
|
||||||
|
|
||||||
|
#include "shell/view/view_reconcile.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "shell/view/view_park.h" // trackFlagParm / writeIfChanged
|
||||||
|
#include "shell/view/view_tracks.h"
|
||||||
|
|
||||||
|
#define REAPERAPI_MINIMAL
|
||||||
|
#define REAPERAPI_WANT_EnumProjects
|
||||||
|
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
||||||
|
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||||
|
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||||
|
#define REAPERAPI_WANT_UpdateArrange
|
||||||
|
#define REAPERAPI_WANT_ValidatePtr2
|
||||||
|
#include "reaper_plugin_functions.h"
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// What the last apply left driven, per project. Keyed by ReaProject*, COMPARE-ONLY
|
||||||
|
// and never dereferenced (the same discipline the panel's own project memos take): a
|
||||||
|
// closed project's pointer must never be followed, only found unequal or used as a
|
||||||
|
// map key. A project absent here gets no repair — the tool has no account of driving
|
||||||
|
// anything there, so it has no opinion. Never pruned on project close: churn is a
|
||||||
|
// handful of entries per session, cheap enough that a close hook would cost more than
|
||||||
|
// it saves. One slot used to serve every project; a tab-switch mid-session silently
|
||||||
|
// discarded whichever project's memo wasn't "current" — see planReconcilePerProject.
|
||||||
|
std::map<ReaProject*, view::AppliedState> g_applied;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void noteAppliedState(ReaProject* proj, const ViewModeModel& model) {
|
||||||
|
ReaProject* const project = proj ? proj : EnumProjects(-1, nullptr, 0);
|
||||||
|
g_applied[project] = view::appliedState(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reconcileAppliedFlags(ViewModeModel& model, ReaProject* proj) {
|
||||||
|
ReaProject* const project = proj ? proj : EnumProjects(-1, nullptr, 0);
|
||||||
|
if (!ValidatePtr2(nullptr, project, "ReaProject*")) return;
|
||||||
|
|
||||||
|
const view::AppliedState reloaded = view::appliedState(model);
|
||||||
|
const ProjectReconcileResult result = planReconcilePerProject(g_applied, project, reloaded);
|
||||||
|
|
||||||
|
// Adopted whether or not anything is written: from here the memo describes the
|
||||||
|
// project as it now stands, so a second reload cannot replay a repair that has
|
||||||
|
// already happened over values the user has since changed.
|
||||||
|
g_applied[project] = result.newEntry;
|
||||||
|
if (result.ops.empty()) return;
|
||||||
|
|
||||||
|
TrackHandles handleByGuid;
|
||||||
|
readFolderEntries(project, handleByGuid);
|
||||||
|
const TrackByGuid trackByGuid = indexHandles(handleByGuid);
|
||||||
|
|
||||||
|
bool wrote = false;
|
||||||
|
Undo_BeginBlock2(project);
|
||||||
|
{
|
||||||
|
UiRefreshHold uiHold;
|
||||||
|
for (const TrackFlagOp& op : result.ops) {
|
||||||
|
MediaTrack* tr = resolve(trackByGuid, op.guid);
|
||||||
|
if (!tr) continue; // stale/deleted GUID — prune
|
||||||
|
if (writeIfChanged(tr, trackFlagParm(op.flag), static_cast<double>(op.value)))
|
||||||
|
wrote = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Undo_EndBlock2(project, "", 0); // discard form — see the header
|
||||||
|
|
||||||
|
if (wrote) {
|
||||||
|
TrackList_AdjustWindows(false); // tracks reappearing owe the full relayout
|
||||||
|
UpdateArrange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#pragma once
|
||||||
|
// The repair pass for a project state REAPER reloaded under us. An undo/redo rolls
|
||||||
|
// the view_state ext block back but NOT B_SHOWINTCP/B_SHOWINMIXER — UNDO_STATE_TRACKCFG
|
||||||
|
// does not carry them (DAW-confirmed) — so a park the model no longer records can be
|
||||||
|
// left driven on the track with nobody holding its captured values but us.
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/view/applied_state.h"
|
||||||
|
#include "core/view/view_mode_model.h"
|
||||||
|
|
||||||
|
// Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header.
|
||||||
|
class ReaProject;
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
// The flags to hand back, and to whom.
|
||||||
|
//
|
||||||
|
// SCOPE — the rule this pass exists to keep: a track is driven only where a park
|
||||||
|
// record names it, and the value written is one that record captured. `applied` is
|
||||||
|
// the tool's account of an act it PERFORMED, so a track absent from it (and from the
|
||||||
|
// reloaded model) is one the tool never drove, holds no captured value for, and has
|
||||||
|
// no opinion about. Keying the repair on "the model calls this track active" instead
|
||||||
|
// is the rejected shape: it asserts B_MAINSEND=1 on a track the user disabled by hand.
|
||||||
|
//
|
||||||
|
// A track the RELOADED model still parks is left out too — it still has an owner, and
|
||||||
|
// the reapply that follows this pass drives it from the live model.
|
||||||
|
//
|
||||||
|
// THE LOAD-BEARING RULE is narrower than the `applied == reloaded` early-out below
|
||||||
|
// looks: only a GUID in `applied.parked` that is ABSENT from `reloaded.parked` can
|
||||||
|
// ever produce an op. A drifted `activeModeId` alone, or a drifted captured VALUE
|
||||||
|
// under a GUID both sides still name, fails the equality check (so the early-out is
|
||||||
|
// skipped) but the loop below finds that GUID still recorded and writes nothing —
|
||||||
|
// the check is a cheap skip, not a "did anything change" decision. See
|
||||||
|
// tests/test_view_reconcile.cpp's drift-alone case for the documented inertness.
|
||||||
|
inline std::vector<TrackFlagOp> planReconcile(const view::AppliedState& applied,
|
||||||
|
const view::AppliedState& reloaded) {
|
||||||
|
std::vector<TrackFlagOp> ops;
|
||||||
|
if (applied == reloaded) return ops; // no drift in what we applied — nothing to repair
|
||||||
|
|
||||||
|
std::set<std::string> stillRecorded;
|
||||||
|
for (const view::AppliedTrack& t : reloaded.parked) stillRecorded.insert(t.guid);
|
||||||
|
|
||||||
|
for (const view::AppliedTrack& t : applied.parked) {
|
||||||
|
if (stillRecorded.count(t.guid)) continue;
|
||||||
|
ops.push_back(TrackFlagOp{t.guid, Flag::ShowInTcp, t.showInTcp});
|
||||||
|
ops.push_back(TrackFlagOp{t.guid, Flag::ShowInMixer, t.showInMixer});
|
||||||
|
ops.push_back(TrackFlagOp{t.guid, Flag::MainSend, t.mainSend});
|
||||||
|
ops.push_back(TrackFlagOp{t.guid, Flag::FxEnable, t.fxEnable});
|
||||||
|
}
|
||||||
|
return ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The per-project memo's pure decision: given every project the tool has a memo for
|
||||||
|
// so far, which project just reloaded, and its fresh fingerprint, produce the ops to
|
||||||
|
// write back plus the entry the memo should hold for that project from here on.
|
||||||
|
// `ReaProject*` is used only as an opaque map key here — never dereferenced, the same
|
||||||
|
// compare-only discipline the .cpp's memo itself keeps.
|
||||||
|
//
|
||||||
|
// A project absent from `applied` gets no repair (see SCOPE above: no memo, no
|
||||||
|
// opinion), but its `reloaded` fingerprint is still the entry handed back — a project
|
||||||
|
// this pass has never seen before starts its own history here rather than being
|
||||||
|
// re-skipped on every future compare too.
|
||||||
|
struct ProjectReconcileResult {
|
||||||
|
std::vector<TrackFlagOp> ops;
|
||||||
|
view::AppliedState newEntry;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline ProjectReconcileResult planReconcilePerProject(
|
||||||
|
const std::map<ReaProject*, view::AppliedState>& applied, ReaProject* project,
|
||||||
|
const view::AppliedState& reloaded) {
|
||||||
|
auto it = applied.find(project);
|
||||||
|
ProjectReconcileResult result;
|
||||||
|
result.ops = it != applied.end() ? planReconcile(it->second, reloaded)
|
||||||
|
: std::vector<TrackFlagOp>{};
|
||||||
|
result.newEntry = reloaded;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Records what `model` left driven in `proj` (nullptr = current project). Called at
|
||||||
|
// the end of every apply: the fingerprint is the ONLY account of what the tool drove
|
||||||
|
// once an undo has rolled the model back underneath it.
|
||||||
|
void noteAppliedState(ReaProject* proj, const ViewModeModel& model);
|
||||||
|
|
||||||
|
// Runs planReconcile over the reloaded `model` and writes its ops. The undo block it
|
||||||
|
// opens is deliberately DISCARDED — this runs in the wake of a Ctrl-Z, and a repair
|
||||||
|
// that minted a point would become the thing the user's next Ctrl-Z undoes.
|
||||||
|
void reconcileAppliedFlags(ViewModeModel& model, ReaProject* proj);
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -6,21 +6,13 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "core/view/solo_cache.h"
|
#include "core/view/solo_cache.h"
|
||||||
|
#include "shell/view/view_tracks.h" // TrackHandles — the enumeration these writers pass over
|
||||||
// Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header.
|
|
||||||
class MediaTrack;
|
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
// The (GUID, handle) enumeration applyMode builds from GetTrack. GetTrack's index
|
|
||||||
// space excludes the master track, so no solo read or write reachable through this
|
|
||||||
// type can ever land on master.
|
|
||||||
using TrackHandles = std::vector<std::pair<std::string, MediaTrack*>>;
|
|
||||||
|
|
||||||
std::vector<view::TrackSolo> readTrackSolos(const TrackHandles& handles);
|
std::vector<view::TrackSolo> readTrackSolos(const TrackHandles& handles);
|
||||||
|
|
||||||
// Applies each op to its live handle in ONE pass over `handles`; an op naming a GUID
|
// Applies each op to its live handle in ONE pass over `handles`; an op naming a GUID
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// See view_tracks.h. Compiled into the reaper_reasampler module; includes
|
||||||
|
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
||||||
|
|
||||||
|
#include "shell/view/view_tracks.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "shell/capture/track_guid.h"
|
||||||
|
|
||||||
|
#define REAPERAPI_MINIMAL
|
||||||
|
#define REAPERAPI_WANT_CountTracks
|
||||||
|
#define REAPERAPI_WANT_GetTrack
|
||||||
|
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||||
|
#define REAPERAPI_WANT_PreventUIRefresh
|
||||||
|
#include "reaper_plugin_functions.h"
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
std::vector<view::TrackFolderEntry> readFolderEntries(ReaProject* proj,
|
||||||
|
TrackHandles& handleByGuid) {
|
||||||
|
std::vector<view::TrackFolderEntry> entries;
|
||||||
|
const int count = CountTracks(proj);
|
||||||
|
entries.reserve(static_cast<std::size_t>(count));
|
||||||
|
handleByGuid.reserve(static_cast<std::size_t>(count));
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
MediaTrack* tr = GetTrack(proj, i);
|
||||||
|
if (!tr) continue;
|
||||||
|
std::string guid = guidString(tr);
|
||||||
|
if (guid.empty()) continue;
|
||||||
|
const int depth = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FOLDERDEPTH"));
|
||||||
|
entries.push_back(view::TrackFolderEntry{guid, depth});
|
||||||
|
handleByGuid.emplace_back(std::move(guid), tr);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackByGuid indexHandles(const TrackHandles& handleByGuid) {
|
||||||
|
TrackByGuid byGuid;
|
||||||
|
byGuid.reserve(handleByGuid.size());
|
||||||
|
for (const auto& kv : handleByGuid) byGuid.emplace(kv.first, kv.second);
|
||||||
|
return byGuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
UiRefreshHold::UiRefreshHold() { PreventUIRefresh(1); }
|
||||||
|
UiRefreshHold::~UiRefreshHold() { PreventUIRefresh(-1); }
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#pragma once
|
||||||
|
// The scaffolding every Design View write pass sets up: the live GUID-keyed track
|
||||||
|
// enumeration it plans against, and the UI-refresh hold its writes land under.
|
||||||
|
// The master track is outside GetTrack's index space, so nothing reached through
|
||||||
|
// these types can land on it.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/view/view_tree.h"
|
||||||
|
|
||||||
|
// Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header.
|
||||||
|
class MediaTrack;
|
||||||
|
class ReaProject;
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
// (GUID, handle) in arrange order — the shape view_solo's one-pass writers take.
|
||||||
|
using TrackHandles = std::vector<std::pair<std::string, MediaTrack*>>;
|
||||||
|
|
||||||
|
// The same handles indexed for lookup. Every park, restore, parent and lane op
|
||||||
|
// resolves a GUID; a linear scan made that O(T²) on the one path the user waits on.
|
||||||
|
using TrackByGuid = std::unordered_map<std::string, MediaTrack*>;
|
||||||
|
|
||||||
|
// Enumerates `proj` (nullptr = current project) into `handleByGuid` and returns each
|
||||||
|
// track's I_FOLDERDEPTH entry for buildFolderTree. A track REAPER reports no GUID
|
||||||
|
// for is skipped by both.
|
||||||
|
std::vector<view::TrackFolderEntry> readFolderEntries(ReaProject* proj,
|
||||||
|
TrackHandles& handleByGuid);
|
||||||
|
|
||||||
|
TrackByGuid indexHandles(const TrackHandles& handleByGuid);
|
||||||
|
|
||||||
|
inline MediaTrack* resolve(const TrackByGuid& byGuid, const std::string& guid) {
|
||||||
|
auto it = byGuid.find(guid);
|
||||||
|
return it == byGuid.end() ? nullptr : it->second; // stale/deleted GUID — pruned by being skipped
|
||||||
|
}
|
||||||
|
|
||||||
|
// Holds REAPER's UI refresh off for a write phase; the deliberate rebuild
|
||||||
|
// (TrackList_AdjustWindows + UpdateArrange) runs after it releases. RAII because an
|
||||||
|
// unbalanced pair leaves the user's UI frozen with no way back — the SDK's own
|
||||||
|
// warning at reaper_plugin_functions.h:5581.
|
||||||
|
struct UiRefreshHold {
|
||||||
|
UiRefreshHold();
|
||||||
|
~UiRefreshHold();
|
||||||
|
UiRefreshHold(const UiRefreshHold&) = delete;
|
||||||
|
UiRefreshHold& operator=(const UiRefreshHold&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
// Standalone tests for the Design View park surface's pure decisions — no
|
|
||||||
// REAPER, no test framework.
|
|
||||||
//
|
|
||||||
// The properties under test: a pre-park snapshot is never taken from a chain a
|
|
||||||
// park has already touched, the refusal that follows names its tracks and is
|
|
||||||
// reported once per changed set, and one mode apply leaves at most ONE undo
|
|
||||||
// point — none at all when a reapply found everything already where it wanted it.
|
|
||||||
|
|
||||||
#include "../src/shell/view/view_fx_park.h"
|
|
||||||
#include "../src/shell/view/view.h" // applyMintsUndoPoint — the apply's undo-point fold
|
|
||||||
|
|
||||||
#include "core/view/view_mode_model.h" // makeParkPlan — the park flags' only producer
|
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
using namespace reasampler;
|
|
||||||
|
|
||||||
static int g_fail = 0;
|
|
||||||
#define CHECK(cond) do { if(!(cond)) { \
|
|
||||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
|
||||||
|
|
||||||
// -- may this chain be snapshotted? -------------------------------------------
|
|
||||||
//
|
|
||||||
// Rationale: the snapshot-source invariant (view_fx_park.h, this directory's
|
|
||||||
// CLAUDE.md).
|
|
||||||
|
|
||||||
// The live values the fold reads, keyed by Flag — so an assertion names the flag
|
|
||||||
// it varies rather than a position in makeParkPlan's op order.
|
|
||||||
struct FlagValues {
|
|
||||||
int showInTcp = 0, showInMixer = 0, mainSend = 0, fxEnable = 0;
|
|
||||||
|
|
||||||
int operator()(Flag f) const {
|
|
||||||
switch (f) {
|
|
||||||
case Flag::ShowInTcp: return showInTcp;
|
|
||||||
case Flag::ShowInMixer: return showInMixer;
|
|
||||||
case Flag::MainSend: return mainSend;
|
|
||||||
case Flag::FxEnable: return fxEnable;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static void testAChainSittingAtEveryValueTheParkWouldWriteReadsAsParked() {
|
|
||||||
// The four zeros are what a parked track's driven flags actually read; if
|
|
||||||
// makeParkPlan ever writes something else, this is the test that says so.
|
|
||||||
const TrackPlan park = makeParkPlan("{A}");
|
|
||||||
|
|
||||||
CHECK(park.flags.size() == 4);
|
|
||||||
CHECK(parkFlagsAlreadyApplied(park.flags, FlagValues{0, 0, 0, 0}));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void testOneFlagStillAtTheUsersValueMeansNoParkReachedTheChain() {
|
|
||||||
const TrackPlan park = makeParkPlan("{A}");
|
|
||||||
|
|
||||||
// Each case varies exactly one flag, so together they also prove all four are
|
|
||||||
// in the plan: a missing op makes its case read parked and fail here.
|
|
||||||
CHECK(!parkFlagsAlreadyApplied(park.flags, FlagValues{1, 0, 0, 0}));
|
|
||||||
CHECK(!parkFlagsAlreadyApplied(park.flags, FlagValues{0, 1, 0, 0}));
|
|
||||||
CHECK(!parkFlagsAlreadyApplied(park.flags, FlagValues{0, 0, 1, 0}));
|
|
||||||
CHECK(!parkFlagsAlreadyApplied(park.flags, FlagValues{0, 0, 0, 1}));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void testAnEmptyPlanProvesNothing() {
|
|
||||||
CHECK(!parkFlagsAlreadyApplied(std::vector<TrackFlagOp>{}, FlagValues{0, 0, 0, 0}));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void testEitherHalfOfTheChainReadingParkedIsEnoughToRefuse() {
|
|
||||||
// Disjunction pinned; rationale at chainReadsParked (view_fx_park.h).
|
|
||||||
CHECK(!chainReadsParked(/*parkFlagsRead=*/false, /*anyFxOffline=*/false));
|
|
||||||
CHECK(chainReadsParked(/*parkFlagsRead=*/true, /*anyFxOffline=*/false));
|
|
||||||
CHECK(chainReadsParked(/*parkFlagsRead=*/false, /*anyFxOffline=*/true));
|
|
||||||
CHECK(chainReadsParked(/*parkFlagsRead=*/true, /*anyFxOffline=*/true));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void testACleanChainIsSnapshottedThenParked() {
|
|
||||||
CHECK(decidePark(/*haveSnapshot=*/false, /*chainReadsParked=*/false) ==
|
|
||||||
ParkAction::SnapshotThenPark);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void testAHeldSnapshotIsNeverOverwrittenWhateverTheChainReads() {
|
|
||||||
// The held snapshot IS the pre-park truth, so the chain is not consulted —
|
|
||||||
// which is what lets the shell skip the live flag reads on this path.
|
|
||||||
CHECK(decidePark(/*haveSnapshot=*/true, /*chainReadsParked=*/false) == ParkAction::ParkOnly);
|
|
||||||
CHECK(decidePark(/*haveSnapshot=*/true, /*chainReadsParked=*/true) == ParkAction::ParkOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void testAParkedChainWithNoSnapshotIsRefusedRatherThanResnapshotted() {
|
|
||||||
// The defect this whole section exists for: the truth is gone, so the only
|
|
||||||
// non-destructive act is to leave the track alone. Snapshotting here commits
|
|
||||||
// park state as the user's state and no later restore can undo it.
|
|
||||||
CHECK(decidePark(/*haveSnapshot=*/false, /*chainReadsParked=*/true) == ParkAction::Refuse);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void testRefusalNamesEveryRefusedTrackAndSaysNothingWhenNoneWere() {
|
|
||||||
CHECK(describeRefusedParks({}).empty());
|
|
||||||
|
|
||||||
const std::string one = describeRefusedParks({"Bass"});
|
|
||||||
CHECK(one.find("1 track ") != std::string::npos);
|
|
||||||
CHECK(one.find("Bass") != std::string::npos);
|
|
||||||
|
|
||||||
const std::string two = describeRefusedParks({"Bass", "Drum bus"});
|
|
||||||
CHECK(two.find("2 tracks ") != std::string::npos);
|
|
||||||
CHECK(two.find("Bass") != std::string::npos);
|
|
||||||
CHECK(two.find("Drum bus") != std::string::npos);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void testRefusalRecoveryNamesThePerFxHalfAndAssertsNoCause() {
|
|
||||||
const std::string msg = describeRefusedParks({"Bass"});
|
|
||||||
|
|
||||||
// I_FXEN is the chain bypass: a user who restores only the four flags leaves
|
|
||||||
// every individually offlined FX offline and walks straight back into a
|
|
||||||
// refusal, so the recovery has to spell the per-FX step out.
|
|
||||||
CHECK(msg.find("every FX in its chain online") != std::string::npos);
|
|
||||||
// And it must not name a cause: the pair has several routes, and on a track
|
|
||||||
// the user themselves keeps hidden/bypassed there was no lost state at all.
|
|
||||||
CHECK(msg.find("undo") == std::string::npos);
|
|
||||||
CHECK(msg.find("lost") == std::string::npos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- the refusal memo's print-suppression gate --------------------------------
|
|
||||||
//
|
|
||||||
// reportRefusedParks' own memo (owner + last-reported names, both REAPER-side
|
|
||||||
// statics) can't be driven from here, but the pure gate behind it can: prints
|
|
||||||
// (i.e. updates the memo) on any change, stays silent on an exact repeat, and
|
|
||||||
// treats a fresh empty set as a change too, so a later real refusal is never
|
|
||||||
// mistaken for a repeat of one that already healed.
|
|
||||||
|
|
||||||
static void testUnchangedOwnerAndSetStaysSilent() {
|
|
||||||
CHECK(!shouldReport(/*sameOwnerAsLast=*/true, {"Bass"}, {"Bass"}));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void testADifferentNamedSetReports() {
|
|
||||||
CHECK(shouldReport(/*sameOwnerAsLast=*/true, {"Bass"}, {"Bass", "Drum bus"}));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void testADifferentOwnerReportsEvenWithTheSameNames() {
|
|
||||||
// Two alternating project tabs must not suppress each other's first refusal.
|
|
||||||
CHECK(shouldReport(/*sameOwnerAsLast=*/false, {"Bass"}, {"Bass"}));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void testAFreshEmptySetReportsAndResetsTheMemo() {
|
|
||||||
CHECK(shouldReport(/*sameOwnerAsLast=*/true, {"Bass"}, {}));
|
|
||||||
// Already empty, same owner: nothing changed, stays silent.
|
|
||||||
CHECK(!shouldReport(/*sameOwnerAsLast=*/true, {}, {}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- the apply's one undo point ----------------------------------------------
|
|
||||||
//
|
|
||||||
// These pin applyMintsUndoPoint's fold only. The `wroteAnything` verdict it
|
|
||||||
// consumes — writeIfChanged / parkTrack / restoreTrack, all running against live
|
|
||||||
// REAPER calls — cannot be pinned here; docs/VERIFICATION.md §"Mode switching"
|
|
||||||
// is the sole cover for that plumbing being wired correctly.
|
|
||||||
|
|
||||||
static void testARealSwitchAlwaysLeavesAPointEvenWithNothingToWrite() {
|
|
||||||
// An explicitly fired action stays undoable even when the plan found nothing
|
|
||||||
// to write — the discard form is for reapplies only.
|
|
||||||
CHECK(applyMintsUndoPoint(/*realSwitch=*/true, /*wroteAnything=*/false));
|
|
||||||
CHECK(applyMintsUndoPoint(/*realSwitch=*/true, /*wroteAnything=*/true));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void testAReapplyThatWroteSomethingLeavesAPoint() {
|
|
||||||
// A tag/untag reapply that actually parked a track: real project state moved.
|
|
||||||
CHECK(applyMintsUndoPoint(/*realSwitch=*/false, /*wroteAnything=*/true));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void testAReapplyThatWroteNothingLeavesNoPointAtAll() {
|
|
||||||
// The project-load reapply over a project saved fully parked: every flag and
|
|
||||||
// every FX already sit where the plan wants them. Opening a project must not
|
|
||||||
// cost the user an undo step.
|
|
||||||
CHECK(!applyMintsUndoPoint(/*realSwitch=*/false, /*wroteAnything=*/false));
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
testAChainSittingAtEveryValueTheParkWouldWriteReadsAsParked();
|
|
||||||
testOneFlagStillAtTheUsersValueMeansNoParkReachedTheChain();
|
|
||||||
testAnEmptyPlanProvesNothing();
|
|
||||||
testEitherHalfOfTheChainReadingParkedIsEnoughToRefuse();
|
|
||||||
testACleanChainIsSnapshottedThenParked();
|
|
||||||
testAHeldSnapshotIsNeverOverwrittenWhateverTheChainReads();
|
|
||||||
testAParkedChainWithNoSnapshotIsRefusedRatherThanResnapshotted();
|
|
||||||
testRefusalNamesEveryRefusedTrackAndSaysNothingWhenNoneWere();
|
|
||||||
testRefusalRecoveryNamesThePerFxHalfAndAssertsNoCause();
|
|
||||||
|
|
||||||
testUnchangedOwnerAndSetStaysSilent();
|
|
||||||
testADifferentNamedSetReports();
|
|
||||||
testADifferentOwnerReportsEvenWithTheSameNames();
|
|
||||||
testAFreshEmptySetReportsAndResetsTheMemo();
|
|
||||||
|
|
||||||
testARealSwitchAlwaysLeavesAPointEvenWithNothingToWrite();
|
|
||||||
testAReapplyThatWroteSomethingLeavesAPoint();
|
|
||||||
testAReapplyThatWroteNothingLeavesNoPointAtAll();
|
|
||||||
|
|
||||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
|
||||||
return g_fail ? 1 : 0;
|
|
||||||
}
|
|
||||||
@@ -814,13 +814,20 @@ using LiveFlags = std::map<std::string, int>;
|
|||||||
// Runs one applyMode-equivalent toggle against `vm` + `live`. `guard` selects the
|
// Runs one applyMode-equivalent toggle against `vm` + `live`. `guard` selects the
|
||||||
// fixed behavior vs. the original buggy (snapshot-every-park) one.
|
// fixed behavior vs. the original buggy (snapshot-every-park) one.
|
||||||
// Returns nothing; mutates `vm` snapshots/active mode and `live` flags in place,
|
// Returns nothing; mutates `vm` snapshots/active mode and `live` flags in place,
|
||||||
// exactly mirroring view.cpp's park then restore then setActiveMode ordering.
|
// mirroring view.cpp's park then restore then setActiveMode ordering: guarded park
|
||||||
|
// gates on holdsSnapshotFlags (parkTrack's own gate, current shell — there is no
|
||||||
|
// refusal branch any more, see view_park.h) and guarded restore spends the flag half
|
||||||
|
// through releaseSnapshotFlags (restoreTrack's own call), not a full clearSnapshot.
|
||||||
|
// The FX half of the two-half record is never exercised here (nothing sets fxHeld
|
||||||
|
// false), so it stays held throughout and never blocks a re-capture the way
|
||||||
|
// flagsHeld does — the one place this harness is narrower than the real record.
|
||||||
//
|
//
|
||||||
// Guarded, the park mirrors decidePark's three ways: a held snapshot parks
|
// `live[guid] == 0` doubles as this harness's single-flag stand-in for
|
||||||
// without recapturing, a chain that already reads parked with no snapshot is
|
// parkTrack's real chainReadsParked (all four driven flags at their parked
|
||||||
// REFUSED (left exactly as found), and only a clean chain is snapshotted. The one
|
// value) — the harness tracks one representative flag, not four, so "already at
|
||||||
// `live` flag stands in for the whole park-flag-plus-FX fold the shell reads —
|
// its parked value" is the whole predicate here. `vm.snapshot(guid) == nullptr`
|
||||||
// the model half of the defect is identical either way.
|
// mirrors parkTrack's own gate for "no record of any kind" — see that function's
|
||||||
|
// header doc for why an FX-held-only record must not reach EXPOSE.
|
||||||
void simulateApplyMode(ViewModeModel& vm, LiveFlags& live, const FolderTree& tree,
|
void simulateApplyMode(ViewModeModel& vm, LiveFlags& live, const FolderTree& tree,
|
||||||
const std::string& target, bool guard) {
|
const std::string& target, bool guard) {
|
||||||
TogglePlan plan = vm.planToggle(tree, target);
|
TogglePlan plan = vm.planToggle(tree, target);
|
||||||
@@ -830,8 +837,14 @@ void simulateApplyMode(ViewModeModel& vm, LiveFlags& live, const FolderTree& tre
|
|||||||
if (tp.flags.empty()) continue;
|
if (tp.flags.empty()) continue;
|
||||||
const std::string& guid = tp.flags.front().guid;
|
const std::string& guid = tp.flags.front().guid;
|
||||||
if (guard) {
|
if (guard) {
|
||||||
if (vm.snapshot(guid) == nullptr) {
|
if (!vm.holdsSnapshotFlags(guid)) {
|
||||||
if (live[guid] == 0) continue; // reads parked, no snapshot — refuse
|
if (!vm.snapshot(guid) && live[guid] == 0) {
|
||||||
|
// EXPOSE: hand it back visible rather than adopt the
|
||||||
|
// park-shaped read as intent. No snapshot stored; this cycle
|
||||||
|
// does not park the track.
|
||||||
|
live[guid] = 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
TrackSnapshot snap;
|
TrackSnapshot snap;
|
||||||
snap.showInTcp = live[guid];
|
snap.showInTcp = live[guid];
|
||||||
vm.storeSnapshot(guid, snap);
|
vm.storeSnapshot(guid, snap);
|
||||||
@@ -844,13 +857,13 @@ void simulateApplyMode(ViewModeModel& vm, LiveFlags& live, const FolderTree& tre
|
|||||||
live[guid] = 0; // park hides it
|
live[guid] = 0; // park hides it
|
||||||
}
|
}
|
||||||
|
|
||||||
// RESTORE: write snapshot flag back, then drop the snapshot.
|
// RESTORE: write snapshot flag back, then spend the flag half.
|
||||||
for (const auto& tp : plan.restore) {
|
for (const auto& tp : plan.restore) {
|
||||||
if (tp.flags.empty()) continue;
|
if (tp.flags.empty()) continue;
|
||||||
const std::string& guid = tp.flags.front().guid;
|
const std::string& guid = tp.flags.front().guid;
|
||||||
if (const TrackSnapshot* snap = vm.snapshot(guid))
|
if (const TrackSnapshot* snap = vm.snapshot(guid))
|
||||||
live[guid] = snap->showInTcp; // restore the captured visible flag
|
live[guid] = snap->showInTcp; // restore the captured visible flag
|
||||||
vm.clearSnapshot(guid);
|
vm.releaseSnapshotFlags(guid);
|
||||||
}
|
}
|
||||||
|
|
||||||
vm.setActiveMode(target);
|
vm.setActiveMode(target);
|
||||||
@@ -939,18 +952,19 @@ static void testNestedToggleSnapshotSurvivesRepark() {
|
|||||||
|
|
||||||
// -- 9b. Reload strand: snapshots gone, live flags still parked ---------------
|
// -- 9b. Reload strand: snapshots gone, live flags still parked ---------------
|
||||||
//
|
//
|
||||||
// The pair the park refusal exists for, driven through the same harness. A model
|
// A model replaced without the project rolling back with it — an unreadable
|
||||||
// replaced without the project rolling back with it — an unreadable view_state, a
|
// view_state, a snapshot reconciled away while its track was deleted, a hand or
|
||||||
// snapshot reconciled away while its track was deleted, a hand or script edit —
|
// script edit — leaves tracks sitting at parked values with nothing recording what
|
||||||
// leaves tracks sitting at parked values with nothing recording what they were
|
// they were before. Daniel's ruling: hand the track back rather than either
|
||||||
// before. Snapshotting there commits the parked state as the user's, and the very
|
// adopting the parked read as intent (silently wipes a hand repair on the very
|
||||||
// next toggle writes it back over whatever they have since fixed by hand.
|
// next restore, or hides an untouched track forever) or refusing outright (the
|
||||||
|
// retired `decidePark`). EXPOSE does neither: no snapshot stored, the flags
|
||||||
|
// driven visible, and the very next reapply — no user action needed — finds an
|
||||||
|
// honest chain and captures/parks it normally.
|
||||||
|
|
||||||
static void testReloadedModelWithParkedTracksRefusesRatherThanResnapshotting() {
|
static void testReloadedModelWithParkedTracksIsExposedNotAdopted() {
|
||||||
const FolderTree tree = nestedTree();
|
const FolderTree tree = nestedTree();
|
||||||
|
|
||||||
// GUARDED (fixed shell).
|
|
||||||
{
|
|
||||||
ViewModeModel vm;
|
ViewModeModel vm;
|
||||||
vm.membership().tag("{L1}", kDesignModeId);
|
vm.membership().tag("{L1}", kDesignModeId);
|
||||||
LiveFlags live{{"{L1}", 1}, {"{L2}", 1}, {"{L3}", 1}};
|
LiveFlags live{{"{L1}", 1}, {"{L2}", 1}, {"{L3}", 1}};
|
||||||
@@ -960,49 +974,29 @@ static void testReloadedModelWithParkedTracksRefusesRatherThanResnapshotting() {
|
|||||||
CHECK(vm.snapshot("{L2}") != nullptr);
|
CHECK(vm.snapshot("{L2}") != nullptr);
|
||||||
|
|
||||||
// The reload: the model comes back with no snapshots while the project's
|
// The reload: the model comes back with no snapshots while the project's
|
||||||
// tracks are still parked.
|
// tracks are still parked -- park-shaped, no record.
|
||||||
vm.clearSnapshot("{L2}");
|
vm.clearSnapshot("{L2}");
|
||||||
vm.clearSnapshot("{L3}");
|
vm.clearSnapshot("{L3}");
|
||||||
|
|
||||||
// The load tick reapplies the saved active mode. Both leaves are refused —
|
// The load-tick reapply hits the no-record/park-shaped pair on both leaves.
|
||||||
// nothing written, and nothing false captured.
|
// EXPOSE fires for each: handed back visible, no snapshot stored -- neither
|
||||||
|
// leaf needed a hand recovery first, and {L3} (never touched by hand in the
|
||||||
|
// old test) comes back on its own exactly like {L2}.
|
||||||
simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true);
|
simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true);
|
||||||
|
CHECK(live["{L2}"] == 1 && live["{L3}"] == 1);
|
||||||
CHECK(vm.snapshot("{L2}") == nullptr && vm.snapshot("{L3}") == nullptr);
|
CHECK(vm.snapshot("{L2}") == nullptr && vm.snapshot("{L3}") == nullptr);
|
||||||
CHECK(live["{L2}"] == 0 && live["{L3}"] == 0);
|
|
||||||
|
|
||||||
// The documented hand recovery, on {L2} only, while still in Design.
|
// Self-healing: the chain no longer reads park-shaped, so the very next apply
|
||||||
// Toggling back must leave it where the user put it: with no snapshot
|
// of the same target takes the normal path -- captures the (now-visible)
|
||||||
// there is nothing to restore FROM, so the restore writes nothing.
|
// truth and parks it correctly.
|
||||||
live["{L2}"] = 1;
|
|
||||||
simulateApplyMode(vm, live, tree, kArrangeModeId, /*guard=*/true);
|
|
||||||
CHECK(live["{L2}"] == 1); // the hand fix survives
|
|
||||||
CHECK(live["{L3}"] == 0); // never recovered — stuck, but never falsely committed
|
|
||||||
|
|
||||||
// And {L2} is back under mode control from there: the next park sees a
|
|
||||||
// clean chain, captures the user's value, and restores it.
|
|
||||||
simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true);
|
simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true);
|
||||||
CHECK(live["{L2}"] == 0 && vm.snapshot("{L2}") != nullptr);
|
CHECK(live["{L2}"] == 0 && live["{L3}"] == 0);
|
||||||
|
CHECK(vm.snapshot("{L2}") != nullptr && vm.snapshot("{L3}") != nullptr);
|
||||||
|
|
||||||
|
// A normal round trip back to Arrange restores the captured (visible) truth --
|
||||||
|
// the wipe the old adopt-based behavior produced is gone.
|
||||||
simulateApplyMode(vm, live, tree, kArrangeModeId, /*guard=*/true);
|
simulateApplyMode(vm, live, tree, kArrangeModeId, /*guard=*/true);
|
||||||
CHECK(live["{L2}"] == 1);
|
CHECK(live["{L2}"] == 1 && live["{L3}"] == 1);
|
||||||
}
|
|
||||||
|
|
||||||
// UNGUARDED (the defect): the reapply after the reload recaptures the parked
|
|
||||||
// zero, so the very next toggle writes it back over the hand recovery.
|
|
||||||
{
|
|
||||||
ViewModeModel vm;
|
|
||||||
vm.membership().tag("{L1}", kDesignModeId);
|
|
||||||
LiveFlags live{{"{L1}", 1}, {"{L2}", 1}, {"{L3}", 1}};
|
|
||||||
|
|
||||||
simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/false);
|
|
||||||
vm.clearSnapshot("{L2}");
|
|
||||||
vm.clearSnapshot("{L3}");
|
|
||||||
simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/false);
|
|
||||||
|
|
||||||
live["{L2}"] = 1; // the same hand recovery
|
|
||||||
simulateApplyMode(vm, live, tree, kArrangeModeId, /*guard=*/false);
|
|
||||||
|
|
||||||
CHECK(live["{L2}"] == 0); // wiped — the false snapshot won
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
@@ -2190,7 +2184,7 @@ int main() {
|
|||||||
testUntaggedLeavesManagedByModeSystem();
|
testUntaggedLeavesManagedByModeSystem();
|
||||||
testTaggedLeafBehaviorUnchangedWithUntagged();
|
testTaggedLeafBehaviorUnchangedWithUntagged();
|
||||||
testNestedToggleSnapshotSurvivesRepark();
|
testNestedToggleSnapshotSurvivesRepark();
|
||||||
testReloadedModelWithParkedTracksRefusesRatherThanResnapshotting();
|
testReloadedModelWithParkedTracksIsExposedNotAdopted();
|
||||||
testReconcilePrunesOrphanedSnapshots();
|
testReconcilePrunesOrphanedSnapshots();
|
||||||
testReconcileFullLiveSetIsNoOp();
|
testReconcileFullLiveSetIsNoOp();
|
||||||
testReconcileThenReparkLifecycleIntact();
|
testReconcileThenReparkLifecycleIntact();
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
// Standalone tests for the Design View park surface's pure decisions — no REAPER, no
|
||||||
|
// test framework.
|
||||||
|
//
|
||||||
|
// The properties under test: the four flags a park drives address exactly the four
|
||||||
|
// REAPER parameters the tool owns (never mute, never solo), and one mode apply leaves
|
||||||
|
// at most ONE undo point — none at all when a reapply found everything already where
|
||||||
|
// it wanted it.
|
||||||
|
|
||||||
|
#include "../src/shell/view/view_park.h"
|
||||||
|
#include "../src/shell/view/view.h" // applyMintsUndoPoint — the apply's undo-point fold
|
||||||
|
|
||||||
|
#include "core/view/view_mode_model.h" // makeParkPlan — the park flags' only producer
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace reasampler;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
// -- what a park is allowed to address ----------------------------------------
|
||||||
|
//
|
||||||
|
// trackFlagParm is the ONE mapping the park's pre-park read, the restore's write and
|
||||||
|
// the reconcile's repair all share; a mistyped parameter there is silent at compile
|
||||||
|
// time and drives the wrong track property forever.
|
||||||
|
|
||||||
|
static void testEachDrivenFlagAddressesItsOwnReaperParameter() {
|
||||||
|
const std::set<std::string> parms{trackFlagParm(Flag::ShowInTcp),
|
||||||
|
trackFlagParm(Flag::ShowInMixer),
|
||||||
|
trackFlagParm(Flag::MainSend),
|
||||||
|
trackFlagParm(Flag::FxEnable)};
|
||||||
|
CHECK(parms.size() == 4); // no two flags collapse onto one parameter
|
||||||
|
CHECK(parms.count("B_SHOWINTCP") == 1);
|
||||||
|
CHECK(parms.count("B_SHOWINMIXER") == 1);
|
||||||
|
CHECK(parms.count("B_MAINSEND") == 1);
|
||||||
|
CHECK(parms.count("I_FXEN") == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testNoDrivenFlagCanEverAddressMuteOrSolo() {
|
||||||
|
// The never-touch-mute invariant, held at the one place a Flag becomes a REAPER
|
||||||
|
// parameter name.
|
||||||
|
for (const Flag f : {Flag::ShowInTcp, Flag::ShowInMixer, Flag::MainSend, Flag::FxEnable}) {
|
||||||
|
const std::string parm = trackFlagParm(f);
|
||||||
|
CHECK(parm != "B_MUTE");
|
||||||
|
CHECK(parm != "I_SOLO");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testAParkPlanDrivesEveryDrivenFlagToZero() {
|
||||||
|
// The four zeros are what a parked track's driven flags actually read; if
|
||||||
|
// makeParkPlan ever writes something else, this is the test that says so.
|
||||||
|
const TrackPlan park = makeParkPlan("{A}");
|
||||||
|
|
||||||
|
CHECK(park.flags.size() == 4);
|
||||||
|
std::set<std::string> parms;
|
||||||
|
for (const TrackFlagOp& op : park.flags) {
|
||||||
|
CHECK(op.guid == "{A}");
|
||||||
|
CHECK(op.value == 0);
|
||||||
|
parms.insert(trackFlagParm(op.flag));
|
||||||
|
}
|
||||||
|
CHECK(parms.size() == 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- the apply's one undo point ----------------------------------------------
|
||||||
|
//
|
||||||
|
// These pin applyMintsUndoPoint's fold only. The `wroteAnything` verdict it consumes
|
||||||
|
// — writeIfChanged / parkTrack / restoreTrack, all running against live REAPER calls
|
||||||
|
// — cannot be pinned here; docs/VERIFICATION.md §"Mode switching" is the sole cover
|
||||||
|
// for that plumbing being wired correctly.
|
||||||
|
|
||||||
|
static void testARealSwitchAlwaysLeavesAPointEvenWithNothingToWrite() {
|
||||||
|
// An explicitly fired action stays undoable even when the plan found nothing to
|
||||||
|
// write — the discard form is for reapplies only.
|
||||||
|
CHECK(applyMintsUndoPoint(/*realSwitch=*/true, /*wroteAnything=*/false));
|
||||||
|
CHECK(applyMintsUndoPoint(/*realSwitch=*/true, /*wroteAnything=*/true));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testAReapplyThatWroteSomethingLeavesAPoint() {
|
||||||
|
// A tag/untag reapply that actually parked a track: real project state moved.
|
||||||
|
CHECK(applyMintsUndoPoint(/*realSwitch=*/false, /*wroteAnything=*/true));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testAReapplyThatWroteNothingLeavesNoPointAtAll() {
|
||||||
|
// The project-load reapply over a project saved fully parked: every flag already
|
||||||
|
// sits where the plan wants it. Opening a project must not cost the user an undo
|
||||||
|
// step.
|
||||||
|
CHECK(!applyMintsUndoPoint(/*realSwitch=*/false, /*wroteAnything=*/false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- the re-park union: a plugin added between a restore and the next park --------
|
||||||
|
//
|
||||||
|
// Sequence under test: park -> restore -> user adds plugin P -> re-park. Without the
|
||||||
|
// union, the record's prior verbatim-keep would drop P silently -- the reclaimer
|
||||||
|
// offlines it with everything else, and no snapshot ever names it to bring it back.
|
||||||
|
|
||||||
|
static void testAPluginAddedWhileRestoredJoinsTheUnionRatherThanBeingDropped() {
|
||||||
|
const std::vector<FxOfflineState> prior{
|
||||||
|
FxOfflineState{"{FX-A}", 1}, FxOfflineState{"{FX-B}", 0}};
|
||||||
|
const std::vector<FxOfflineState> live{
|
||||||
|
FxOfflineState{"{FX-A}", 0}, FxOfflineState{"{FX-B}", 0}, FxOfflineState{"{FX-P}", 0}};
|
||||||
|
|
||||||
|
const std::vector<FxOfflineState> merged = unionFxOfflineOnRepark(prior, live);
|
||||||
|
|
||||||
|
CHECK(merged.size() == 3);
|
||||||
|
bool foundA = false, foundB = false, foundP = false;
|
||||||
|
for (const FxOfflineState& fx : merged) {
|
||||||
|
if (fx.fxGuid == "{FX-A}") { foundA = true; CHECK(fx.offline == 1); } // record wins
|
||||||
|
if (fx.fxGuid == "{FX-B}") { foundB = true; CHECK(fx.offline == 0); } // record wins
|
||||||
|
if (fx.fxGuid == "{FX-P}") { foundP = true; CHECK(fx.offline == 0); } // added from live
|
||||||
|
}
|
||||||
|
CHECK(foundA && foundB && foundP);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testAPluginRemovedWhileRestoredStaysInTheUnionForResolveFxRestoreToDrop() {
|
||||||
|
// The union is not where a removed plugin's entry disappears -- resolveFxRestore
|
||||||
|
// owns that at restore time (missingIdentity, reported), so the merge must keep
|
||||||
|
// naming it rather than quietly forgetting it here.
|
||||||
|
const std::vector<FxOfflineState> prior{
|
||||||
|
FxOfflineState{"{FX-A}", 1}, FxOfflineState{"{FX-REMOVED}", 1}};
|
||||||
|
const std::vector<FxOfflineState> live{FxOfflineState{"{FX-A}", 0}};
|
||||||
|
|
||||||
|
const std::vector<FxOfflineState> merged = unionFxOfflineOnRepark(prior, live);
|
||||||
|
|
||||||
|
CHECK(merged.size() == 2);
|
||||||
|
bool foundRemoved = false;
|
||||||
|
for (const FxOfflineState& fx : merged)
|
||||||
|
if (fx.fxGuid == "{FX-REMOVED}") foundRemoved = true;
|
||||||
|
CHECK(foundRemoved);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- chainReadsParked: the EXPOSE trigger, flags only -------------------------
|
||||||
|
//
|
||||||
|
// The live values the fold reads, keyed by Flag -- so an assertion names the flag
|
||||||
|
// it varies rather than a position in makeParkPlan's op order.
|
||||||
|
struct FlagValues {
|
||||||
|
int showInTcp = 0, showInMixer = 0, mainSend = 0, fxEnable = 0;
|
||||||
|
|
||||||
|
int operator()(Flag f) const {
|
||||||
|
switch (f) {
|
||||||
|
case Flag::ShowInTcp: return showInTcp;
|
||||||
|
case Flag::ShowInMixer: return showInMixer;
|
||||||
|
case Flag::MainSend: return mainSend;
|
||||||
|
case Flag::FxEnable: return fxEnable;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static void testAChainSittingAtEveryParkedValueReadsAsParkShaped() {
|
||||||
|
const TrackPlan park = makeParkPlan("{A}");
|
||||||
|
CHECK(chainReadsParked(park.flags, FlagValues{0, 0, 0, 0}));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testOneFlagStillAtTheUsersValueMeansNotParkShaped() {
|
||||||
|
const TrackPlan park = makeParkPlan("{A}");
|
||||||
|
|
||||||
|
// Each case varies exactly one flag, so together they also prove all four are
|
||||||
|
// in the plan: a missing op makes its case read parked and fail here.
|
||||||
|
CHECK(!chainReadsParked(park.flags, FlagValues{1, 0, 0, 0}));
|
||||||
|
CHECK(!chainReadsParked(park.flags, FlagValues{0, 1, 0, 0}));
|
||||||
|
CHECK(!chainReadsParked(park.flags, FlagValues{0, 0, 1, 0}));
|
||||||
|
CHECK(!chainReadsParked(park.flags, FlagValues{0, 0, 0, 1}));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testAnEmptyPlanProvesNothing() {
|
||||||
|
CHECK(!chainReadsParked(std::vector<TrackFlagOp>{}, FlagValues{0, 0, 0, 0}));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFxOfflineAloneIsNotFoldedIntoTheParkedRead() {
|
||||||
|
// chainReadsParked takes no FX argument at all: an FX-only-offline chain
|
||||||
|
// cannot make it read parked, by construction -- there is nothing here it
|
||||||
|
// could read. That is the resolved design question (see view_park.h):
|
||||||
|
// flags-fine/FX-offline stays on the ordinary capture path, never EXPOSE.
|
||||||
|
const TrackPlan park = makeParkPlan("{A}");
|
||||||
|
CHECK(!chainReadsParked(park.flags, FlagValues{1, 1, 1, 1})); // flags all fine
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- the EXPOSE console report -------------------------------------------------
|
||||||
|
|
||||||
|
static void testExposedTracksAreNamedAndEmptyListSaysNothing() {
|
||||||
|
CHECK(describeExposedTracks({}).empty());
|
||||||
|
|
||||||
|
const std::string one = describeExposedTracks({"Bass"});
|
||||||
|
CHECK(one.find("1 track ") != std::string::npos);
|
||||||
|
CHECK(one.find("Bass") != std::string::npos);
|
||||||
|
|
||||||
|
const std::string two = describeExposedTracks({"Bass", "Drum bus"});
|
||||||
|
CHECK(two.find("2 tracks ") != std::string::npos);
|
||||||
|
CHECK(two.find("Bass") != std::string::npos);
|
||||||
|
CHECK(two.find("Drum bus") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testExposedReportNeverUsesRefusalWording() {
|
||||||
|
// Daniel's ruling was EXPOSE, not refuse -- the wording must never suggest the
|
||||||
|
// tool declined to act.
|
||||||
|
const std::string msg = describeExposedTracks({"Bass"});
|
||||||
|
CHECK(msg.find("refus") == std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testShouldReportSkipsAnExactRepeatOnly() {
|
||||||
|
const std::vector<std::string> a{"Bass"};
|
||||||
|
const std::vector<std::string> b{"Bass", "Drum bus"};
|
||||||
|
|
||||||
|
CHECK(!shouldReport(/*sameOwnerAsLast=*/true, a, a)); // exact repeat: silent
|
||||||
|
CHECK(shouldReport(/*sameOwnerAsLast=*/true, a, b)); // set changed: reprint
|
||||||
|
CHECK(shouldReport(/*sameOwnerAsLast=*/false, a, a)); // owner (project) changed: reprint
|
||||||
|
CHECK(shouldReport(/*sameOwnerAsLast=*/true, a, {})); // healed to none: reprint (resets memo)
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testEachDrivenFlagAddressesItsOwnReaperParameter();
|
||||||
|
testNoDrivenFlagCanEverAddressMuteOrSolo();
|
||||||
|
testAParkPlanDrivesEveryDrivenFlagToZero();
|
||||||
|
|
||||||
|
testARealSwitchAlwaysLeavesAPointEvenWithNothingToWrite();
|
||||||
|
testAReapplyThatWroteSomethingLeavesAPoint();
|
||||||
|
testAReapplyThatWroteNothingLeavesNoPointAtAll();
|
||||||
|
|
||||||
|
testAPluginAddedWhileRestoredJoinsTheUnionRatherThanBeingDropped();
|
||||||
|
testAPluginRemovedWhileRestoredStaysInTheUnionForResolveFxRestoreToDrop();
|
||||||
|
|
||||||
|
testAChainSittingAtEveryParkedValueReadsAsParkShaped();
|
||||||
|
testOneFlagStillAtTheUsersValueMeansNotParkShaped();
|
||||||
|
testAnEmptyPlanProvesNothing();
|
||||||
|
testFxOfflineAloneIsNotFoldedIntoTheParkedRead();
|
||||||
|
|
||||||
|
testExposedTracksAreNamedAndEmptyListSaysNothing();
|
||||||
|
testExposedReportNeverUsesRefusalWording();
|
||||||
|
testShouldReportSkipsAnExactRepeatOnly();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
// Standalone tests for the Design View repair pass's pure decision — no REAPER, no
|
||||||
|
// test framework.
|
||||||
|
//
|
||||||
|
// The two properties the pass exists for: it drives a flag ONLY where a park record
|
||||||
|
// names the track (so a track the tool never parked can never have its main send
|
||||||
|
// re-enabled under it), and it fires on applied-output drift rather than on "an undo
|
||||||
|
// happened". Both AppliedStates are produced by the real fingerprint
|
||||||
|
// (view::appliedState) over real models, so a change to what the fingerprint carries
|
||||||
|
// shows up here rather than being papered over by hand-built fixtures.
|
||||||
|
|
||||||
|
#include "../src/shell/view/view_reconcile.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace reasampler;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
TrackSnapshot captured(int showInTcp, int showInMixer, int mainSend, int fxEnable) {
|
||||||
|
TrackSnapshot snap;
|
||||||
|
snap.showInTcp = showInTcp;
|
||||||
|
snap.showInMixer = showInMixer;
|
||||||
|
snap.mainSend = mainSend;
|
||||||
|
snap.fxEnable = fxEnable;
|
||||||
|
return snap;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasOp(const std::vector<TrackFlagOp>& ops, const std::string& guid, Flag flag, int value) {
|
||||||
|
for (const TrackFlagOp& op : ops)
|
||||||
|
if (op.guid == guid && op.flag == flag && op.value == value) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool namesTrack(const std::vector<TrackFlagOp>& ops, const std::string& guid) {
|
||||||
|
for (const TrackFlagOp& op : ops)
|
||||||
|
if (op.guid == guid) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// -- the trigger: applied-output drift, not "an undo happened" -----------------
|
||||||
|
|
||||||
|
static void testAnUndoThatLeftOurAppliedOutputAloneRepairsNothing() {
|
||||||
|
// The rejected trigger is "did an undo occur" — an undo that touched nothing we
|
||||||
|
// drive still answers yes, and repairing on it rewrites state the user owns.
|
||||||
|
ViewModeModel model;
|
||||||
|
model.setActiveMode(kDesignModeId);
|
||||||
|
model.storeSnapshot("{A}", captured(1, 1, 1, 1));
|
||||||
|
|
||||||
|
const view::AppliedState state = view::appliedState(model);
|
||||||
|
CHECK(planReconcile(state, state).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testADriftedActiveModeAloneIsNotAReasonToDriveATrack() {
|
||||||
|
// Drift is the gate, not the instruction: the record still has an owner, so the
|
||||||
|
// reapply that follows this pass is what drives it.
|
||||||
|
ViewModeModel applied;
|
||||||
|
applied.setActiveMode(kDesignModeId);
|
||||||
|
applied.storeSnapshot("{A}", captured(1, 1, 1, 1));
|
||||||
|
|
||||||
|
ViewModeModel reloaded; // active mode Arrange — the fingerprint differs
|
||||||
|
reloaded.storeSnapshot("{A}", captured(1, 1, 1, 1));
|
||||||
|
|
||||||
|
CHECK(view::appliedState(applied) != view::appliedState(reloaded));
|
||||||
|
CHECK(planReconcile(view::appliedState(applied), view::appliedState(reloaded)).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- the scope rule: a record names the track, or nothing is written -----------
|
||||||
|
|
||||||
|
static void testARecordTheReloadedModelLostIsHandedBackItsCapturedValues() {
|
||||||
|
// The undo case: REAPER rolled view_state back to before the switch, so the model
|
||||||
|
// no longer records the park — but the flags are still driven on the track, and
|
||||||
|
// this memo is the only surviving account of what they were.
|
||||||
|
ViewModeModel applied;
|
||||||
|
applied.setActiveMode(kDesignModeId);
|
||||||
|
applied.storeSnapshot("{A}", captured(1, 1, 1, 1));
|
||||||
|
|
||||||
|
const ViewModeModel reloaded; // Arrange, no records at all
|
||||||
|
|
||||||
|
const std::vector<TrackFlagOp> ops =
|
||||||
|
planReconcile(view::appliedState(applied), view::appliedState(reloaded));
|
||||||
|
|
||||||
|
CHECK(ops.size() == 4);
|
||||||
|
CHECK(hasOp(ops, "{A}", Flag::ShowInTcp, 1));
|
||||||
|
CHECK(hasOp(ops, "{A}", Flag::ShowInMixer, 1));
|
||||||
|
CHECK(hasOp(ops, "{A}", Flag::MainSend, 1));
|
||||||
|
CHECK(hasOp(ops, "{A}", Flag::FxEnable, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testARepairedFlagReturnsToItsCapturedValueNeverToOn() {
|
||||||
|
// A track the user had OUT of the master mix and FX-bypassed before we parked it:
|
||||||
|
// the repair must hand back exactly that, not a hardcoded "on".
|
||||||
|
ViewModeModel applied;
|
||||||
|
applied.setActiveMode(kDesignModeId);
|
||||||
|
applied.storeSnapshot("{A}", captured(1, 0, 0, 0));
|
||||||
|
|
||||||
|
const ViewModeModel reloaded;
|
||||||
|
|
||||||
|
const std::vector<TrackFlagOp> ops =
|
||||||
|
planReconcile(view::appliedState(applied), view::appliedState(reloaded));
|
||||||
|
|
||||||
|
CHECK(hasOp(ops, "{A}", Flag::MainSend, 0));
|
||||||
|
CHECK(hasOp(ops, "{A}", Flag::ShowInMixer, 0));
|
||||||
|
CHECK(hasOp(ops, "{A}", Flag::FxEnable, 0));
|
||||||
|
CHECK(!hasOp(ops, "{A}", Flag::MainSend, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testATrackNeitherStateRecordsIsNeverDrivenAtAll() {
|
||||||
|
// The regression this rule is built against: a track the model merely calls
|
||||||
|
// active is NOT a track the tool parked. It holds no captured value, so asserting
|
||||||
|
// B_MAINSEND=1 on it would silently re-enable a send the user disabled by hand.
|
||||||
|
ViewModeModel applied;
|
||||||
|
applied.setActiveMode(kDesignModeId);
|
||||||
|
applied.storeSnapshot("{A}", captured(1, 1, 1, 1));
|
||||||
|
|
||||||
|
ViewModeModel reloaded;
|
||||||
|
reloaded.membership().tag("{B}", kArrangeModeId); // active, never parked, no record
|
||||||
|
|
||||||
|
const std::vector<TrackFlagOp> ops =
|
||||||
|
planReconcile(view::appliedState(applied), view::appliedState(reloaded));
|
||||||
|
|
||||||
|
CHECK(namesTrack(ops, "{A}"));
|
||||||
|
CHECK(!namesTrack(ops, "{B}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testASpentFlagHalfIsNotARecordToRepairFrom() {
|
||||||
|
// A restore already handed the flags back and zeroed them on the record; the FX
|
||||||
|
// half outlives that. Repairing from the spent half would drive those zeros —
|
||||||
|
// i.e. re-park the track — so the fingerprint must not carry it.
|
||||||
|
ViewModeModel applied;
|
||||||
|
applied.setActiveMode(kDesignModeId);
|
||||||
|
applied.storeSnapshot("{A}", captured(1, 1, 1, 1));
|
||||||
|
applied.releaseSnapshotFlags("{A}");
|
||||||
|
|
||||||
|
const ViewModeModel reloaded;
|
||||||
|
|
||||||
|
CHECK(planReconcile(view::appliedState(applied), view::appliedState(reloaded)).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- the per-project memo: a tab switch must not erase another project's account --
|
||||||
|
|
||||||
|
static void testASecondProjectNotedInBetweenDoesNotEraseTheFirstProjectsMemo() {
|
||||||
|
// The bug this replaced: a single global slot meant noting project B's applied
|
||||||
|
// state overwrote project A's entirely, so a repair for A run afterward found no
|
||||||
|
// memo to compare against and silently skipped -- exactly what a tab-switch then
|
||||||
|
// an undo on the ORIGINAL project produces within one timer period.
|
||||||
|
ReaProject* const projA = reinterpret_cast<ReaProject*>(1);
|
||||||
|
ReaProject* const projB = reinterpret_cast<ReaProject*>(2);
|
||||||
|
|
||||||
|
ViewModeModel appliedA;
|
||||||
|
appliedA.setActiveMode(kDesignModeId);
|
||||||
|
appliedA.storeSnapshot("{A}", captured(1, 1, 1, 1));
|
||||||
|
|
||||||
|
const ViewModeModel appliedB; // untouched — B has nothing driven
|
||||||
|
|
||||||
|
std::map<ReaProject*, view::AppliedState> memo;
|
||||||
|
memo[projA] = view::appliedState(appliedA);
|
||||||
|
memo[projB] = view::appliedState(appliedB); // noted in between; must not clobber A
|
||||||
|
|
||||||
|
const ViewModeModel reloadedA; // A's park record dropped, as an undo would leave it
|
||||||
|
|
||||||
|
const ProjectReconcileResult result =
|
||||||
|
planReconcilePerProject(memo, projA, view::appliedState(reloadedA));
|
||||||
|
|
||||||
|
CHECK(result.ops.size() == 4);
|
||||||
|
CHECK(hasOp(result.ops, "{A}", Flag::MainSend, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testAProjectWithNoPriorMemoGetsNoRepair() {
|
||||||
|
// "A project we hold no memo for gets no repair" -- the tool has no account of
|
||||||
|
// driving anything there, so it has no opinion (see the header's SCOPE note).
|
||||||
|
ReaProject* const projC = reinterpret_cast<ReaProject*>(3);
|
||||||
|
const std::map<ReaProject*, view::AppliedState> memo; // never noted
|
||||||
|
|
||||||
|
const ViewModeModel reloaded;
|
||||||
|
const ProjectReconcileResult result =
|
||||||
|
planReconcilePerProject(memo, projC, view::appliedState(reloaded));
|
||||||
|
|
||||||
|
CHECK(result.ops.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testAnUndoThatLeftOurAppliedOutputAloneRepairsNothing();
|
||||||
|
testADriftedActiveModeAloneIsNotAReasonToDriveATrack();
|
||||||
|
|
||||||
|
testARecordTheReloadedModelLostIsHandedBackItsCapturedValues();
|
||||||
|
testARepairedFlagReturnsToItsCapturedValueNeverToOn();
|
||||||
|
testATrackNeitherStateRecordsIsNeverDrivenAtAll();
|
||||||
|
testASpentFlagHalfIsNotARecordToRepairFrom();
|
||||||
|
|
||||||
|
testASecondProjectNotedInBetweenDoesNotEraseTheFirstProjectsMemo();
|
||||||
|
testAProjectWithNoPriorMemoGetsNoRepair();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user