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.
|
||||
|
||||
Reference in New Issue
Block a user