feat(view): D2 Wave 2 — apply managed-lane ops + timer-diff auto-tag new content
Drives fixed-lane C_LANEPLAYS for managed lanes only (name-keyed, ordinal-renumber safe) in the view shell; diffs live track/item GUIDs on the panel timer to auto-tag new content to the active mode, first-poll-guarded. Pure guid_diff + lane_keys modules unit-tested; folds in Wave-1 polish.
This commit is contained in:
@@ -32,6 +32,8 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -39,10 +41,14 @@
|
||||
#include "bank_grid.h"
|
||||
#include "bank_model.h"
|
||||
#include "capture_paths.h"
|
||||
#include "guid_diff.h" // GuidBaseline — new-content detection (D2 Wave 2)
|
||||
#include "lane_keys.h" // managed/manual lane heuristic (D2 Wave 2)
|
||||
#include "mode_switch.h"
|
||||
#include "peaks.h"
|
||||
#include "persist.h"
|
||||
#include "track_guid.h" // guidString — canonical track GUID key (D2 Wave 2)
|
||||
#include "view.h" // applyMode — the D2/D4 mode-activation entrypoint the switch fires
|
||||
#include "view_mode_model.h" // autoTagNewContent / NewItem (D2 Wave 2)
|
||||
|
||||
// SWELL / LICE. On macOS/Linux SWELL is provided by the host (SWELL_PROVIDED_BY_APP);
|
||||
// on Windows we use native Win32 (windows.h first, then swell.h no-ops on _WIN32).
|
||||
@@ -73,6 +79,16 @@
|
||||
#define REAPERAPI_WANT_GetMainHwnd
|
||||
#define REAPERAPI_WANT_PCM_Source_CreateFromFile
|
||||
#define REAPERAPI_WANT_PCM_Source_Destroy
|
||||
// New-content detection (D2 Wave 2): enumerate live tracks + items and read fixed-lane
|
||||
// state to classify an item's lane as managed vs manual.
|
||||
#define REAPERAPI_WANT_CountTracks
|
||||
#define REAPERAPI_WANT_GetTrack
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
|
||||
#define REAPERAPI_WANT_CountTrackMediaItems
|
||||
#define REAPERAPI_WANT_GetTrackMediaItem
|
||||
#define REAPERAPI_WANT_GetMediaItemInfo_Value
|
||||
#define REAPERAPI_WANT_GetSetMediaItemInfo_String
|
||||
// Stock preview API (verified against reaper_plugin.h / reaper_plugin_functions.h):
|
||||
// PlayPreview/StopPreview drive a caller-owned preview_register_t. These are the
|
||||
// STOCK symbols (not SWS-only) — see the audition section below.
|
||||
@@ -185,6 +201,20 @@ struct PanelState {
|
||||
PCM_source* previewSrc = nullptr;
|
||||
bool previewActive = false;
|
||||
bool previewInited = false; // guards double init / deinit
|
||||
|
||||
// --- New-content detection (D2 Wave 2) ------------------------------------
|
||||
//
|
||||
// Each timer tick diffs the live track+item GUID set against the previous tick to
|
||||
// auto-tag content created SINCE the last tick into the then-active mode. The
|
||||
// baseline carries the first-poll-after-open guard so pre-existing content is never
|
||||
// mass-tagged (it stays Arrange). `lastProject` detects a project switch so the
|
||||
// baseline re-arms per project (a switch never diffs across two projects). Both live
|
||||
// for the extension's lifetime alongside the session, independent of panel open/close
|
||||
// — detection must run whether or not the dock is visible (content is created in the
|
||||
// arrange, not the panel).
|
||||
GuidBaseline contentBaseline;
|
||||
ReaProject* lastProject = nullptr;
|
||||
bool sawProject = false; // false until the first detect tick sees a project
|
||||
};
|
||||
|
||||
PanelState g_panel;
|
||||
@@ -526,6 +556,112 @@ bool refreshFingerprint() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- New-content detection (D2 Wave 2) ----------------------------------------
|
||||
//
|
||||
// REAPER exposes no "item/track added" callback, so we diff live project state on the
|
||||
// existing timer. Each tick: enumerate every track GUID and every item GUID, diff
|
||||
// against the previous tick (GuidBaseline, first-poll-guarded), and auto-tag the new
|
||||
// GUIDs into the active mode via the pure autoTagNewContent. An item on a MANUAL lane
|
||||
// is exempt (design point #1) — its lane's durable name lacks the managed prefix. All
|
||||
// enumeration is READ-ONLY on the project; the only mutation is to the in-memory
|
||||
// membership index (persisted by persist on the next save, same as an action-driven tag).
|
||||
|
||||
// The durable name of item `it`'s fixed lane, or empty if the item's track is not in
|
||||
// fixed-lane mode (⇒ not a lane at all, treated as non-manual normal content). Mirrors
|
||||
// view.cpp's laneName but item-side: read the item's I_FIXEDLANE ordinal, then that
|
||||
// lane's P_LANENAME:n off the owning track.
|
||||
std::string itemLaneName(MediaTrack* tr, MediaItem* it) {
|
||||
if (static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) != 2)
|
||||
return {}; // not a fixed-lane track ⇒ no lane name (normal timeline content)
|
||||
const int laneIdx = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
|
||||
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);
|
||||
}
|
||||
|
||||
// An item's canonical GUID string via GetSetMediaItemInfo_String("GUID"). Empty on
|
||||
// failure (a read failure must never be tagged — guid_diff/autoTag both skip empties).
|
||||
std::string itemGuid(MediaItem* it) {
|
||||
char buf[64] = {0};
|
||||
if (!GetSetMediaItemInfo_String(it, "GUID", buf, false)) return {};
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// Enumerates the live project's track + item GUIDs. Fills `allGuids` (the full live set,
|
||||
// baseline input) and, for each item, records whether it sits on a manual lane so a
|
||||
// newly-detected item can be exempted from auto-tag without a second project walk.
|
||||
void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
|
||||
std::map<std::string, bool>& itemOnManualLane) {
|
||||
const int trackCount = CountTracks(proj);
|
||||
for (int t = 0; t < trackCount; ++t) {
|
||||
MediaTrack* tr = GetTrack(proj, t);
|
||||
if (!tr) continue;
|
||||
std::string tg = guidString(tr);
|
||||
if (!tg.empty()) allGuids.insert(tg);
|
||||
|
||||
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()) continue;
|
||||
allGuids.insert(ig);
|
||||
// Manual iff the item is on a fixed lane whose name is NOT tool-managed.
|
||||
const std::string ln = itemLaneName(tr, it);
|
||||
itemOnManualLane[ig] = (!ln.empty() && !isManagedLaneName(ln));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One detection tick: diff live GUIDs against the baseline and auto-tag the new ones
|
||||
// into the active mode. Runs every timer tick regardless of panel open/close (content
|
||||
// is created in the arrange). READ-ONLY on the project; mutates only the in-memory
|
||||
// membership index.
|
||||
void detectNewContent() {
|
||||
if (!g_panel.session) return;
|
||||
|
||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||
|
||||
// Project switch (or first ever tick) re-arms the first-poll guard so we never diff
|
||||
// across two projects. GUID-address recycling is bounded here: a missed reset can at
|
||||
// worst re-baseline against the wrong project for one tick; the identity-of-record
|
||||
// (persist's minted project GUID) governs the bank/model reload, not this detector.
|
||||
if (!g_panel.sawProject || proj != g_panel.lastProject) {
|
||||
g_panel.contentBaseline.reset();
|
||||
g_panel.lastProject = proj;
|
||||
g_panel.sawProject = true;
|
||||
}
|
||||
|
||||
std::set<std::string> live;
|
||||
std::map<std::string, bool> itemOnManualLane;
|
||||
enumerateLiveGuids(proj, live, itemOnManualLane);
|
||||
|
||||
const std::vector<std::string> added = g_panel.contentBaseline.observe(live);
|
||||
if (added.empty()) return; // first poll after open, or nothing new this tick
|
||||
|
||||
// Split the new GUIDs into tracks vs items so the pure decision can apply the
|
||||
// manual-lane exemption to items only. A GUID present in the item-lane map is an
|
||||
// item; otherwise it is a track (track GUIDs never appear in that map).
|
||||
std::vector<std::string> newTracks;
|
||||
std::vector<NewItem> newItems;
|
||||
for (const std::string& g : added) {
|
||||
auto it = itemOnManualLane.find(g);
|
||||
if (it == itemOnManualLane.end()) {
|
||||
newTracks.push_back(g); // a track GUID
|
||||
} else {
|
||||
newItems.push_back(NewItem{g, it->second}); // an item; carries its exemption
|
||||
}
|
||||
}
|
||||
|
||||
ViewModeModel& model = g_panel.session->view();
|
||||
const std::vector<AutoTag> tags =
|
||||
autoTagNewContent(newTracks, newItems, model.activeModeId());
|
||||
for (const AutoTag& tag : tags)
|
||||
model.membership().tag(tag.guid, tag.modeId);
|
||||
}
|
||||
|
||||
// --- Audition preview ---------------------------------------------------------
|
||||
//
|
||||
// READ-ONLY / NON-DESTRUCTIVE (load-bearing principle): audition is PREVIEW
|
||||
@@ -899,6 +1035,12 @@ std::vector<std::string> bankPanelSelectedSampleIds() {
|
||||
}
|
||||
|
||||
void bankPanelRefresh() {
|
||||
// New-content auto-tag detection runs EVERY tick regardless of panel open/close:
|
||||
// tracks/items are created in the arrange view, not the panel, so detection must
|
||||
// not be gated on the dock being visible. READ-ONLY on the project; only mutates
|
||||
// the in-memory membership index (persist saves it like any action-driven tag).
|
||||
detectNewContent();
|
||||
|
||||
if (!g_panel.open || !g_panel.hwnd) return;
|
||||
// Repaint only when the bank actually changed (generation bump). Cheap tick
|
||||
// otherwise — just a fingerprint string compare.
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// guid_diff implementation — pure set arithmetic for new-content detection. See
|
||||
// guid_diff.h. No REAPER, no SWELL — std only.
|
||||
|
||||
#include "guid_diff.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
std::vector<std::string> newGuids(const std::set<std::string>& previous,
|
||||
const std::set<std::string>& current) {
|
||||
std::vector<std::string> added;
|
||||
// current \ previous. std::set iterates ascending, so set_difference yields a
|
||||
// deterministic order without a separate sort.
|
||||
for (const std::string& g : current) {
|
||||
if (g.empty()) continue; // never tag a GUID-read failure
|
||||
if (previous.count(g) == 0) added.push_back(g);
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
std::vector<std::string> GuidBaseline::observe(const std::set<std::string>& current) {
|
||||
if (!primed_) {
|
||||
// First poll after open/reset: establish the baseline, report nothing new so
|
||||
// pre-existing content is NOT auto-tagged (it defaults to Arrange).
|
||||
baseline_ = current;
|
||||
primed_ = true;
|
||||
return {};
|
||||
}
|
||||
std::vector<std::string> added = newGuids(baseline_, current);
|
||||
// Advance the baseline to the full current set. Using `current` (not baseline_ ∪
|
||||
// added) means a DELETED GUID drops out of the baseline too, so if REAPER later
|
||||
// reuses that GUID for genuinely new content it is detected again — the baseline
|
||||
// tracks the live set exactly, not a monotonic union.
|
||||
baseline_ = current;
|
||||
return added;
|
||||
}
|
||||
|
||||
void GuidBaseline::reset() {
|
||||
baseline_.clear();
|
||||
primed_ = false; // next observe() re-baselines (first-poll guard re-armed)
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
// guid_diff — the pure, REAPER-free core of the D2 Wave-2 new-content detection.
|
||||
//
|
||||
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
|
||||
// vendor/ includes. Standard library only. Unit-tested outside the DAW.
|
||||
//
|
||||
// The shell (bank_panel timer) reads REAPER's live track/item GUID set each tick;
|
||||
// this module owns the DECISION of "which GUIDs are new since the last tick" and the
|
||||
// first-poll-after-open guard so pre-existing content is never mass-tagged. Keeping
|
||||
// this here — rather than in the shell — means the fiddly baseline/diff logic is
|
||||
// unit-tested, mirroring how view_tree splits the folder-depth walk out of view.cpp.
|
||||
//
|
||||
// The shell then hands the "new since last tick" GUIDs to the pure autoTagNewContent
|
||||
// (view_mode_model) to produce the membership writes.
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// The GUIDs present in `current` but absent from `previous` — i.e. new since the
|
||||
// previous poll. Order is the set's ascending order (deterministic; the caller does
|
||||
// not depend on discovery order). Empty GUIDs are ignored (a GUID read failure at the
|
||||
// shell boundary must never be tagged).
|
||||
std::vector<std::string> newGuids(const std::set<std::string>& previous,
|
||||
const std::set<std::string>& current);
|
||||
|
||||
// Tracks the live GUID set across polls for ONE project, implementing the
|
||||
// first-poll-after-open guard: the first observation after a (re)start establishes a
|
||||
// BASELINE and reports NOTHING new, so pre-existing content stays at its default
|
||||
// (Arrange) rather than being mass-tagged. Every subsequent observe() returns only the
|
||||
// GUIDs created since the prior observe().
|
||||
//
|
||||
// Project switches are handled by reset(): the shell detects a project change (the
|
||||
// active ReaProject* / project GUID changed) and calls reset() so the next observe()
|
||||
// re-baselines against the newly-opened project instead of diffing across two
|
||||
// unrelated projects (which would spuriously "detect" the entire new project as new
|
||||
// content, or miss content because a same-GUID collision looked pre-existing).
|
||||
class GuidBaseline {
|
||||
public:
|
||||
// Observes the current live GUID set. On the FIRST call after construction or
|
||||
// reset() this records the baseline and returns {} (nothing is "new" at open).
|
||||
// On every later call it returns the GUIDs added since the previous call and
|
||||
// advances the baseline to `current`. Empty GUIDs are ignored.
|
||||
std::vector<std::string> observe(const std::set<std::string>& current);
|
||||
|
||||
// Re-arms the first-poll guard: the next observe() re-baselines and reports
|
||||
// nothing new. Called on a project switch so detection never diffs across
|
||||
// projects.
|
||||
void reset();
|
||||
|
||||
// True until the first observe() after construction/reset — exposed for the shell
|
||||
// to reason about (and for tests) about whether a baseline is established yet.
|
||||
bool primed() const { return primed_; }
|
||||
|
||||
private:
|
||||
std::set<std::string> baseline_;
|
||||
bool primed_ = false; // false ⇒ next observe() sets the baseline
|
||||
};
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,34 @@
|
||||
// lane_keys implementation — pure string convention, no REAPER. See lane_keys.h.
|
||||
|
||||
#include "lane_keys.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace {
|
||||
// Does `s` start with the managed-lane prefix?
|
||||
bool hasManagedPrefix(const std::string& s) {
|
||||
const std::size_t n = std::strlen(kManagedLanePrefix);
|
||||
return s.size() >= n && s.compare(0, n, kManagedLanePrefix) == 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool isManagedLaneName(const std::string& laneName) {
|
||||
return hasManagedPrefix(laneName);
|
||||
}
|
||||
|
||||
std::optional<std::string> managedLaneKey(const std::string& laneName) {
|
||||
if (!hasManagedPrefix(laneName)) return std::nullopt; // manual/unnamed ⇒ no key
|
||||
// The durable name IS the key (stable across ordinal renumber). Keeping the full
|
||||
// prefixed name — rather than stripping to the mode id — means the key is globally
|
||||
// unambiguous and the ownership index's mode field remains the single source of
|
||||
// truth for which mode owns the lane.
|
||||
return laneName;
|
||||
}
|
||||
|
||||
std::string laneNameForMode(const std::string& modeId) {
|
||||
return std::string(kManagedLanePrefix) + modeId;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
// lane_keys — the pure, REAPER-free convention that maps a REAPER fixed lane's
|
||||
// durable NAME (P_LANENAME:n) to the opaque lane-key the pure view_mode_model uses,
|
||||
// and the managed/manual heuristic that rides on it.
|
||||
//
|
||||
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL. std only.
|
||||
// Unit-tested outside the DAW. The shell (view.cpp) reads each lane's P_LANENAME:n
|
||||
// string from REAPER and asks this module whether the lane is tool-managed and what
|
||||
// its stable lane-key is; the shell never re-derives the prefix rule itself.
|
||||
//
|
||||
// -- Design point #2 (lane-identity robustness) resolution --------------------
|
||||
//
|
||||
// REAPER exposes no durable per-lane GUID. The only lane identity is the ordinal
|
||||
// I_FIXEDLANE, which REAPER RENUMBERS when lanes are reordered or deleted — so keying
|
||||
// the ownership index by raw ordinal would silently corrupt managed/manual ownership
|
||||
// on any reorder. REAPER DOES expose a writable, durable lane NAME (P_LANENAME:n) that
|
||||
// travels with the lane across renumber. So the tool names each lane it mints with a
|
||||
// stable, prefixed identity ("reasampler:<mode>") and keys the ownership index by that
|
||||
// NAME, not the ordinal. On each apply the shell walks the track's lanes by current
|
||||
// ordinal, reads each name, and reconciles ordinal<->laneKey — so a C_LANEPLAYS:N
|
||||
// write always targets the lane's CURRENT ordinal for a given durable key even after a
|
||||
// reorder. A lane WITHOUT the prefix was not minted by the tool: it is manual and
|
||||
// off-limits (the fixed-lane analog of "never touch mute/solo").
|
||||
//
|
||||
// -- Design point #1 (manual-lane exemption) resolution -----------------------
|
||||
//
|
||||
// The SAME prefix rule is the manual/managed heuristic for auto-tag: an item on a lane
|
||||
// whose name lacks the "reasampler:" prefix is on a manual lane and is EXEMPT from
|
||||
// auto-tag. isManagedLaneName is the single predicate both the toggle-apply path and
|
||||
// the new-content detection path consult, so the boundary is defined in one place and
|
||||
// unit-tested.
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// The prefix the tool stamps on every lane NAME it mints. A lane name carrying this
|
||||
// prefix is a managed lane the tool created; any other name (or an empty/unnamed lane)
|
||||
// is a user-minted manual lane. Stable-forever: changing it would strand the ownership
|
||||
// of every lane in every already-saved project, so treat it like an action id string.
|
||||
inline constexpr const char* kManagedLanePrefix = "reasampler:";
|
||||
|
||||
// True iff `laneName` is a tool-minted managed-lane name (carries kManagedLanePrefix).
|
||||
// This is the load-bearing managed/manual predicate for BOTH design points #1 and #2.
|
||||
bool isManagedLaneName(const std::string& laneName);
|
||||
|
||||
// The opaque lane-key the pure model keys by, for a lane with REAPER name `laneName`.
|
||||
// For a managed lane the key IS the durable name (stable across ordinal renumber). For
|
||||
// a manual/unnamed lane there is no managed key: returns std::nullopt so the caller
|
||||
// treats the lane as manual (never driven, items on it exempt from auto-tag).
|
||||
std::optional<std::string> managedLaneKey(const std::string& laneName);
|
||||
|
||||
// The lane NAME the tool mints for the lane owned by `modeId` (kManagedLanePrefix +
|
||||
// modeId). The inverse of managedLaneKey for a managed lane: managedLaneKey(
|
||||
// laneNameForMode(m)) == kManagedLanePrefix + m. Exposed for the Wave-3 lane-minting
|
||||
// path and for tests; the apply path in this wave only READS names, but the round-trip
|
||||
// contract is asserted here so minting and reading cannot drift.
|
||||
std::string laneNameForMode(const std::string& modeId);
|
||||
|
||||
} // namespace reasampler
|
||||
+128
@@ -10,10 +10,15 @@
|
||||
|
||||
#include "view.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "lane_keys.h"
|
||||
#include "track_guid.h"
|
||||
#include "view_tree.h"
|
||||
|
||||
@@ -22,19 +27,29 @@
|
||||
#define REAPERAPI_WANT_GetTrack
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
|
||||
#define REAPERAPI_WANT_TrackFX_GetCount
|
||||
#define REAPERAPI_WANT_TrackFX_GetOffline
|
||||
#define REAPERAPI_WANT_TrackFX_SetOffline
|
||||
#define REAPERAPI_WANT_CountTrackMediaItems
|
||||
#define REAPERAPI_WANT_GetTrackMediaItem
|
||||
#define REAPERAPI_WANT_GetMediaItemInfo_Value
|
||||
#define REAPERAPI_WANT_SetMediaItemInfo_Value
|
||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#define REAPERAPI_WANT_TrackList_AdjustWindows
|
||||
#define REAPERAPI_WANT_UpdateArrange
|
||||
#define REAPERAPI_WANT_UpdateTimeline
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace {
|
||||
|
||||
// Track fixed-lane mode value (I_FREEMODE=2). See SDK: 0=normal, 1=free item
|
||||
// positioning, 2=fixed lanes.
|
||||
constexpr int kFreeModeFixedLanes = 2;
|
||||
|
||||
// The parmname for each planner Flag. All four are documented bool*/int* track
|
||||
// info params driven through the double-valued Get/SetMediaTrackInfo_Value API.
|
||||
const char* flagParm(Flag f) {
|
||||
@@ -132,6 +147,103 @@ void restoreFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& fxOffline)
|
||||
}
|
||||
}
|
||||
|
||||
// -- Managed-lane application (D2 Wave 2) ------------------------------------
|
||||
//
|
||||
// The pure planner emits LanePlayOps keyed by (trackGuid, laneKey) where laneKey is
|
||||
// the lane's DURABLE name (lane_keys convention: "reasampler:<mode>"). REAPER's
|
||||
// C_LANEPLAYS:N is keyed by the lane's CURRENT ORDINAL, which renumbers on reorder.
|
||||
// So before applying, we build the ordinal<->key reconcile for a track by reading each
|
||||
// lane's P_LANENAME:n; the write then targets the correct current ordinal for a given
|
||||
// durable key even after a reorder (design point #2). A lane whose name lacks the
|
||||
// managed prefix is manual and never appears in this map, so it can never be driven.
|
||||
|
||||
// Reads lane index `laneIdx`'s durable name off track `tr` (P_LANENAME:n). Empty if
|
||||
// the lane is unnamed or the param is 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);
|
||||
}
|
||||
|
||||
// Maps each MANAGED lane's durable key -> its current ordinal on `tr`, by walking the
|
||||
// track's I_NUMFIXEDLANES lanes and reading each name. Manual (unprefixed/unnamed)
|
||||
// lanes are omitted, so a key absent from the map is a lane the tool must not drive.
|
||||
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;
|
||||
}
|
||||
|
||||
// Drives one managed lane on `tr` to `lanePlays` (C_LANEPLAYS value): the track-side
|
||||
// C_LANEPLAYS:N for lane ordinal `laneIdx`, plus every ITEM on that lane (item-side
|
||||
// C_LANEPLAYS). Items are matched to the lane by their read-only I_FIXEDLANE ordinal.
|
||||
// B_FIXEDLANE_HIDDEN is READ-ONLY (SDK) — hide/show follows from C_LANEPLAYS=0/1, never
|
||||
// written directly. Non-destructive: only reversible play/show flags; no item is moved
|
||||
// or deleted.
|
||||
void applyLanePlays(MediaTrack* tr, int laneIdx, int lanePlays) {
|
||||
char parm[32];
|
||||
std::snprintf(parm, sizeof(parm), "C_LANEPLAYS:%d", laneIdx);
|
||||
SetMediaTrackInfo_Value(tr, parm, static_cast<double>(lanePlays));
|
||||
|
||||
const int itemCount = CountTrackMediaItems(tr);
|
||||
for (int i = 0; i < itemCount; ++i) {
|
||||
MediaItem* it = GetTrackMediaItem(tr, i);
|
||||
if (!it) continue;
|
||||
const int itemLane = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
|
||||
if (itemLane != laneIdx) continue; // item is on a different lane
|
||||
SetMediaItemInfo_Value(it, "C_LANEPLAYS", static_cast<double>(lanePlays));
|
||||
}
|
||||
}
|
||||
|
||||
// Applies the plan's managed-lane ops. Groups ops by track, resolves each op's durable
|
||||
// laneKey to the track's current ordinal (skipping any key not present on the live
|
||||
// track — a stale/renamed/deleted managed lane is pruned, never mis-driven), enables
|
||||
// fixed-lane mode on any track that carries a managed lane, and drives C_LANEPLAYS.
|
||||
// UpdateTimeline() is called ONCE at the end (SDK: required after I_FREEMODE changes).
|
||||
// Returns true if any track's I_FREEMODE was (re)set to fixed lanes (⇒ needs timeline
|
||||
// refresh). MANAGED lanes only — plan.lanes never contains a manual lane (pure planner
|
||||
// gates on the ownership index), and a manual lane's name never resolves to a key here,
|
||||
// so the invariant is enforced twice.
|
||||
bool applyLaneOps(const std::vector<std::pair<std::string, MediaTrack*>>& handleByGuid,
|
||||
const std::vector<LanePlayOp>& lanes) {
|
||||
if (lanes.empty()) return false;
|
||||
|
||||
// Group op indices by track guid so we read each track's lane map once.
|
||||
std::map<std::string, std::vector<const LanePlayOp*>> byTrack;
|
||||
for (const LanePlayOp& op : lanes) byTrack[op.trackGuid].push_back(&op);
|
||||
|
||||
bool touchedFreeMode = false;
|
||||
for (const auto& [guid, ops] : byTrack) {
|
||||
MediaTrack* tr = resolve(handleByGuid, guid);
|
||||
if (!tr) continue; // stale GUID — prune
|
||||
|
||||
// Ensure fixed-lane mode is on before driving lane play state. A track carrying
|
||||
// a managed lane must be in I_FREEMODE=2; set it only if not already, and flag
|
||||
// that a timeline refresh is owed.
|
||||
const int freeMode = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE"));
|
||||
if (freeMode != kFreeModeFixedLanes) {
|
||||
SetMediaTrackInfo_Value(tr, "I_FREEMODE",
|
||||
static_cast<double>(kFreeModeFixedLanes));
|
||||
touchedFreeMode = true;
|
||||
}
|
||||
|
||||
// Reconcile durable keys -> current ordinals on THIS track, then drive each op.
|
||||
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
|
||||
applyLanePlays(tr, it->second, op->lanePlays);
|
||||
}
|
||||
}
|
||||
return touchedFreeMode;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
|
||||
@@ -194,6 +306,16 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
model.clearSnapshot(guid);
|
||||
}
|
||||
|
||||
// MANAGED LANES (D2 item-level projection): drive C_LANEPLAYS so the active mode's
|
||||
// managed lane plays+shows and every inactive-mode managed lane is silenced+hidden.
|
||||
// plan.lanes carries MANAGED lanes only (the pure planner gates on the ownership
|
||||
// index); applyLaneOps additionally resolves each op's durable key against the live
|
||||
// track's lane names, so a manual lane — which never carries the managed prefix —
|
||||
// can never be driven. Empty for a D1-only project (no fixed lanes), leaving D1
|
||||
// behavior byte-identical. UpdateTimeline() is owed only if a track's I_FREEMODE
|
||||
// was (re)set to fixed lanes (SDK requirement); deferred to the refresh block below.
|
||||
const bool laneModeChanged = applyLaneOps(handleByGuid, plan.lanes);
|
||||
|
||||
// PARENT VISIBILITY (never parked): visibleTracks() marks a parent visible when
|
||||
// a descendant leaf is visible in the target mode OR the parent belongs to the
|
||||
// mode by its own membership (untagged folder → Arrange default). Recomputed
|
||||
@@ -228,6 +350,12 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
TrackList_AdjustWindows(false);
|
||||
UpdateArrange();
|
||||
|
||||
// A fixed-lane mode change (I_FREEMODE -> 2) requires UpdateTimeline() to take
|
||||
// visible effect (SDK). Call it only when we actually toggled a track into fixed
|
||||
// lanes this apply; the C_LANEPLAYS writes themselves are picked up by the arrange
|
||||
// refresh above.
|
||||
if (laneModeChanged) UpdateTimeline();
|
||||
|
||||
Undo_EndBlock2(proj, undoLabel.c_str(), -1);
|
||||
return true;
|
||||
}
|
||||
|
||||
+29
-4
@@ -1,10 +1,12 @@
|
||||
#include "view_mode_model.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cerrno>
|
||||
#include <climits>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <utility>
|
||||
|
||||
// view_mode_model implementation.
|
||||
//
|
||||
@@ -121,6 +123,13 @@ int laneModeState(const std::string& managedMode, const std::string& activeMode)
|
||||
// and hidden (C_LANEPLAYS = 0). Exclusive membership: only one stance's lane at a
|
||||
// time. Show-both, which keeps a lane audible across modes, is a per-lane opt-out
|
||||
// the shell layers on; the default per-mode decision here is exclusive.
|
||||
//
|
||||
// EXCLUSIVITY ASSUMPTION (one managed lane per mode per track): the model assumes a
|
||||
// given (track, mode) owns AT MOST ONE managed lane. C_LANEPLAYS=1 means "this lane
|
||||
// plays EXCLUSIVELY" — two lanes on the same track both claiming mode M would both
|
||||
// be told to play exclusively on M's toggle, which REAPER cannot honor coherently
|
||||
// (the last write wins in the DAW). The Wave-3 lane-minting path is responsible for
|
||||
// upholding one-lane-per-(track,mode); planToggle asserts it in debug builds.
|
||||
return managedMode == activeMode ? kLanePlaysExclusive : kLaneSilent;
|
||||
}
|
||||
|
||||
@@ -319,8 +328,20 @@ TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string&
|
||||
// "never touch mute/solo"). Lane ownership is not a tree property, so this walks the
|
||||
// ownership index directly, not the FolderTree; a project with no fixed lanes leaves
|
||||
// plan.lanes empty and the plan is byte-identical to a D1 plan.
|
||||
#ifndef NDEBUG
|
||||
// Debug-time guard for the one-managed-lane-per-mode-per-track exclusivity
|
||||
// assumption (see laneModeState). Two managed lanes on the same track claiming the
|
||||
// same mode would both be told to play exclusively on that mode's toggle, which
|
||||
// REAPER cannot honor. Cheap set membership over the (usually tiny) managed-lane
|
||||
// set; compiled out of release builds.
|
||||
std::set<std::pair<std::string, std::string>> seenTrackMode; // (trackGuid, mode)
|
||||
#endif
|
||||
for (const auto& [ref, ownership] : lanes_.all()) {
|
||||
if (!ownership.isManaged()) continue; // manual lanes are off-limits
|
||||
#ifndef NDEBUG
|
||||
assert(seenTrackMode.insert({ref.trackGuid, *ownership.managedMode}).second &&
|
||||
"two managed lanes on one track claim the same mode (exclusivity broken)");
|
||||
#endif
|
||||
const int lanePlays = laneModeState(*ownership.managedMode, targetMode);
|
||||
plan.lanes.push_back(LanePlayOp{ref.trackGuid, ref.laneKey, lanePlays});
|
||||
}
|
||||
@@ -448,7 +469,8 @@ std::string ViewModeModel::serialize() const {
|
||||
{
|
||||
bool first = true;
|
||||
for (const auto& [guid, mem] : membership_.all()) {
|
||||
if (!first) out += ','; first = false;
|
||||
if (!first) out += ',';
|
||||
first = false;
|
||||
ObjWriter e(out);
|
||||
e.keyStr("guid", guid);
|
||||
e.keyBegin("modes");
|
||||
@@ -456,7 +478,8 @@ std::string ViewModeModel::serialize() const {
|
||||
{
|
||||
bool mf = true;
|
||||
for (const auto& id : mem.modeIds) {
|
||||
if (!mf) out += ','; mf = false;
|
||||
if (!mf) out += ',';
|
||||
mf = false;
|
||||
writeEscaped(out, id);
|
||||
}
|
||||
}
|
||||
@@ -472,7 +495,8 @@ std::string ViewModeModel::serialize() const {
|
||||
{
|
||||
bool first = true;
|
||||
for (const auto& [guid, snap] : snapshots_) {
|
||||
if (!first) out += ','; first = false;
|
||||
if (!first) out += ',';
|
||||
first = false;
|
||||
ObjWriter e(out);
|
||||
e.keyStr("guid", guid);
|
||||
e.keyRaw("showInTcp", intToStr(snap.showInTcp));
|
||||
@@ -494,7 +518,8 @@ std::string ViewModeModel::serialize() const {
|
||||
{
|
||||
bool first = true;
|
||||
for (const auto& [ref, ownership] : lanes_.all()) {
|
||||
if (!first) out += ','; first = false;
|
||||
if (!first) out += ',';
|
||||
first = false;
|
||||
ObjWriter e(out);
|
||||
e.keyStr("trackGuid", ref.trackGuid);
|
||||
e.keyStr("laneKey", ref.laneKey);
|
||||
|
||||
Reference in New Issue
Block a user