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:
@@ -75,6 +75,28 @@ add_library(view_tree STATIC src/view_tree.cpp)
|
|||||||
target_include_directories(view_tree PUBLIC src)
|
target_include_directories(view_tree PUBLIC src)
|
||||||
target_link_libraries(view_tree PUBLIC view_mode_model)
|
target_link_libraries(view_tree PUBLIC view_mode_model)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2d'') Pure guid_diff library — NO REAPER, NO SWELL. The D2 Wave-2 new-content
|
||||||
|
# detection core: current \ previous GUID diff + the first-poll-after-open
|
||||||
|
# baseline guard (and per-project reset). Split out so the fiddly baseline/diff
|
||||||
|
# logic is unit-tested outside the DAW; the bank_panel timer that reads REAPER's
|
||||||
|
# live track/item GUID set and applies the tags is DAW-verified. Mirror of
|
||||||
|
# view_tree splitting the folder-depth walk out of view.cpp.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
add_library(guid_diff STATIC src/guid_diff.cpp)
|
||||||
|
target_include_directories(guid_diff PUBLIC src)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2d''') Pure lane_keys library — NO REAPER, NO SWELL. The managed/manual fixed-lane
|
||||||
|
# heuristic (D2 Wave-2): a lane whose durable P_LANENAME:n carries the
|
||||||
|
# "reasampler:" prefix is tool-managed and keyed by that stable name; any other
|
||||||
|
# lane is user-minted manual and off-limits. Resolves design point #1 (auto-tag
|
||||||
|
# exemption) and #2 (name-keyed identity survives ordinal renumber). Split out
|
||||||
|
# so the prefix rule is unit-tested; view.cpp reads the names from REAPER.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
add_library(lane_keys STATIC src/lane_keys.cpp)
|
||||||
|
target_include_directories(lane_keys PUBLIC src)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 2e) Pure insert_plan library — NO REAPER, NO SWELL. The InsertMedia `mode`
|
# 2e) Pure insert_plan library — NO REAPER, NO SWELL. The InsertMedia `mode`
|
||||||
# bitmask arithmetic behind the `insert` shell (M6). Split out so the
|
# bitmask arithmetic behind the `insert` shell (M6). Split out so the
|
||||||
@@ -140,6 +162,14 @@ add_executable(view_tree_tests tests/test_view_tree.cpp)
|
|||||||
target_link_libraries(view_tree_tests PRIVATE view_tree)
|
target_link_libraries(view_tree_tests PRIVATE view_tree)
|
||||||
add_test(NAME view_tree_tests COMMAND view_tree_tests)
|
add_test(NAME view_tree_tests COMMAND view_tree_tests)
|
||||||
|
|
||||||
|
add_executable(guid_diff_tests tests/test_guid_diff.cpp)
|
||||||
|
target_link_libraries(guid_diff_tests PRIVATE guid_diff)
|
||||||
|
add_test(NAME guid_diff_tests COMMAND guid_diff_tests)
|
||||||
|
|
||||||
|
add_executable(lane_keys_tests tests/test_lane_keys.cpp)
|
||||||
|
target_link_libraries(lane_keys_tests PRIVATE lane_keys)
|
||||||
|
add_test(NAME lane_keys_tests COMMAND lane_keys_tests)
|
||||||
|
|
||||||
add_executable(insert_plan_tests tests/test_insert_plan.cpp)
|
add_executable(insert_plan_tests tests/test_insert_plan.cpp)
|
||||||
target_link_libraries(insert_plan_tests PRIVATE insert_plan)
|
target_link_libraries(insert_plan_tests PRIVATE insert_plan)
|
||||||
add_test(NAME insert_plan_tests COMMAND insert_plan_tests)
|
add_test(NAME insert_plan_tests COMMAND insert_plan_tests)
|
||||||
@@ -181,6 +211,8 @@ add_library(reaper_reasampler MODULE
|
|||||||
src/view_tree.cpp
|
src/view_tree.cpp
|
||||||
src/view.cpp
|
src/view.cpp
|
||||||
src/track_guid.cpp
|
src/track_guid.cpp
|
||||||
|
src/guid_diff.cpp
|
||||||
|
src/lane_keys.cpp
|
||||||
src/actions.cpp
|
src/actions.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch view_mode_model insert_plan render_settings realtime_record)
|
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch view_mode_model insert_plan render_settings realtime_record)
|
||||||
|
|||||||
@@ -32,6 +32,8 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -39,10 +41,14 @@
|
|||||||
#include "bank_grid.h"
|
#include "bank_grid.h"
|
||||||
#include "bank_model.h"
|
#include "bank_model.h"
|
||||||
#include "capture_paths.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 "mode_switch.h"
|
||||||
#include "peaks.h"
|
#include "peaks.h"
|
||||||
#include "persist.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.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);
|
// 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).
|
// 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_GetMainHwnd
|
||||||
#define REAPERAPI_WANT_PCM_Source_CreateFromFile
|
#define REAPERAPI_WANT_PCM_Source_CreateFromFile
|
||||||
#define REAPERAPI_WANT_PCM_Source_Destroy
|
#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):
|
// Stock preview API (verified against reaper_plugin.h / reaper_plugin_functions.h):
|
||||||
// PlayPreview/StopPreview drive a caller-owned preview_register_t. These are the
|
// PlayPreview/StopPreview drive a caller-owned preview_register_t. These are the
|
||||||
// STOCK symbols (not SWS-only) — see the audition section below.
|
// STOCK symbols (not SWS-only) — see the audition section below.
|
||||||
@@ -185,6 +201,20 @@ struct PanelState {
|
|||||||
PCM_source* previewSrc = nullptr;
|
PCM_source* previewSrc = nullptr;
|
||||||
bool previewActive = false;
|
bool previewActive = false;
|
||||||
bool previewInited = false; // guards double init / deinit
|
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;
|
PanelState g_panel;
|
||||||
@@ -526,6 +556,112 @@ bool refreshFingerprint() {
|
|||||||
return true;
|
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 ---------------------------------------------------------
|
// --- Audition preview ---------------------------------------------------------
|
||||||
//
|
//
|
||||||
// READ-ONLY / NON-DESTRUCTIVE (load-bearing principle): audition is PREVIEW
|
// READ-ONLY / NON-DESTRUCTIVE (load-bearing principle): audition is PREVIEW
|
||||||
@@ -899,6 +1035,12 @@ std::vector<std::string> bankPanelSelectedSampleIds() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void bankPanelRefresh() {
|
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;
|
if (!g_panel.open || !g_panel.hwnd) return;
|
||||||
// Repaint only when the bank actually changed (generation bump). Cheap tick
|
// Repaint only when the bank actually changed (generation bump). Cheap tick
|
||||||
// otherwise — just a fingerprint string compare.
|
// 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 "view.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "lane_keys.h"
|
||||||
#include "track_guid.h"
|
#include "track_guid.h"
|
||||||
#include "view_tree.h"
|
#include "view_tree.h"
|
||||||
|
|
||||||
@@ -22,19 +27,29 @@
|
|||||||
#define REAPERAPI_WANT_GetTrack
|
#define REAPERAPI_WANT_GetTrack
|
||||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||||
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
||||||
|
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
|
||||||
#define REAPERAPI_WANT_TrackFX_GetCount
|
#define REAPERAPI_WANT_TrackFX_GetCount
|
||||||
#define REAPERAPI_WANT_TrackFX_GetOffline
|
#define REAPERAPI_WANT_TrackFX_GetOffline
|
||||||
#define REAPERAPI_WANT_TrackFX_SetOffline
|
#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_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
|
||||||
#include "reaper_plugin_functions.h"
|
#include "reaper_plugin_functions.h"
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
namespace {
|
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
|
// The parmname for each planner Flag. All four are documented bool*/int* track
|
||||||
// info params driven through the double-valued Get/SetMediaTrackInfo_Value API.
|
// info params driven through the double-valued Get/SetMediaTrackInfo_Value API.
|
||||||
const char* flagParm(Flag f) {
|
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
|
} // namespace
|
||||||
|
|
||||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
|
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);
|
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
|
// 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
|
// 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
|
// 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);
|
TrackList_AdjustWindows(false);
|
||||||
UpdateArrange();
|
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);
|
Undo_EndBlock2(proj, undoLabel.c_str(), -1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-4
@@ -1,10 +1,12 @@
|
|||||||
#include "view_mode_model.h"
|
#include "view_mode_model.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <climits>
|
#include <climits>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
// view_mode_model implementation.
|
// 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
|
// 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
|
// 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.
|
// 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;
|
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
|
// "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
|
// 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.
|
// 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()) {
|
for (const auto& [ref, ownership] : lanes_.all()) {
|
||||||
if (!ownership.isManaged()) continue; // manual lanes are off-limits
|
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);
|
const int lanePlays = laneModeState(*ownership.managedMode, targetMode);
|
||||||
plan.lanes.push_back(LanePlayOp{ref.trackGuid, ref.laneKey, lanePlays});
|
plan.lanes.push_back(LanePlayOp{ref.trackGuid, ref.laneKey, lanePlays});
|
||||||
}
|
}
|
||||||
@@ -448,7 +469,8 @@ std::string ViewModeModel::serialize() const {
|
|||||||
{
|
{
|
||||||
bool first = true;
|
bool first = true;
|
||||||
for (const auto& [guid, mem] : membership_.all()) {
|
for (const auto& [guid, mem] : membership_.all()) {
|
||||||
if (!first) out += ','; first = false;
|
if (!first) out += ',';
|
||||||
|
first = false;
|
||||||
ObjWriter e(out);
|
ObjWriter e(out);
|
||||||
e.keyStr("guid", guid);
|
e.keyStr("guid", guid);
|
||||||
e.keyBegin("modes");
|
e.keyBegin("modes");
|
||||||
@@ -456,7 +478,8 @@ std::string ViewModeModel::serialize() const {
|
|||||||
{
|
{
|
||||||
bool mf = true;
|
bool mf = true;
|
||||||
for (const auto& id : mem.modeIds) {
|
for (const auto& id : mem.modeIds) {
|
||||||
if (!mf) out += ','; mf = false;
|
if (!mf) out += ',';
|
||||||
|
mf = false;
|
||||||
writeEscaped(out, id);
|
writeEscaped(out, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,7 +495,8 @@ std::string ViewModeModel::serialize() const {
|
|||||||
{
|
{
|
||||||
bool first = true;
|
bool first = true;
|
||||||
for (const auto& [guid, snap] : snapshots_) {
|
for (const auto& [guid, snap] : snapshots_) {
|
||||||
if (!first) out += ','; first = false;
|
if (!first) out += ',';
|
||||||
|
first = false;
|
||||||
ObjWriter e(out);
|
ObjWriter e(out);
|
||||||
e.keyStr("guid", guid);
|
e.keyStr("guid", guid);
|
||||||
e.keyRaw("showInTcp", intToStr(snap.showInTcp));
|
e.keyRaw("showInTcp", intToStr(snap.showInTcp));
|
||||||
@@ -494,7 +518,8 @@ std::string ViewModeModel::serialize() const {
|
|||||||
{
|
{
|
||||||
bool first = true;
|
bool first = true;
|
||||||
for (const auto& [ref, ownership] : lanes_.all()) {
|
for (const auto& [ref, ownership] : lanes_.all()) {
|
||||||
if (!first) out += ','; first = false;
|
if (!first) out += ',';
|
||||||
|
first = false;
|
||||||
ObjWriter e(out);
|
ObjWriter e(out);
|
||||||
e.keyStr("trackGuid", ref.trackGuid);
|
e.keyStr("trackGuid", ref.trackGuid);
|
||||||
e.keyStr("laneKey", ref.laneKey);
|
e.keyStr("laneKey", ref.laneKey);
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
// Standalone tests for reasampler::newGuids + GuidBaseline — no REAPER, no test
|
||||||
|
// framework. Mirror of test_view_mode_model: iterate the hard logic outside the DAW.
|
||||||
|
//
|
||||||
|
// Covers (D2 Wave-2 new-content detection):
|
||||||
|
// 1. newGuids: current \ previous, empty-GUID filtering, determinism.
|
||||||
|
// 2. GuidBaseline first-poll guard: the first observe() after open reports NOTHING
|
||||||
|
// new (pre-existing content stays Arrange) and establishes the baseline.
|
||||||
|
// 3. Incremental detection: only GUIDs added since the prior observe() are returned.
|
||||||
|
// 4. Deletion drops from the baseline so a reused GUID is re-detected.
|
||||||
|
// 5. reset() (project switch) re-arms the first-poll guard: the next observe()
|
||||||
|
// re-baselines and reports nothing new — never diffs across projects.
|
||||||
|
|
||||||
|
#include "../src/guid_diff.h"
|
||||||
|
|
||||||
|
#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)
|
||||||
|
|
||||||
|
static bool has(const std::vector<std::string>& v, const std::string& g) {
|
||||||
|
for (const auto& e : v) if (e == g) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- 1. newGuids set difference ----------------------------------------------
|
||||||
|
|
||||||
|
static void testNewGuidsDifference() {
|
||||||
|
std::set<std::string> prev{"{A}", "{B}"};
|
||||||
|
std::set<std::string> cur{"{A}", "{B}", "{C}", "{D}"};
|
||||||
|
|
||||||
|
auto added = newGuids(prev, cur);
|
||||||
|
CHECK(added.size() == 2);
|
||||||
|
CHECK(has(added, "{C}"));
|
||||||
|
CHECK(has(added, "{D}"));
|
||||||
|
CHECK(!has(added, "{A}")); // pre-existing, not new
|
||||||
|
CHECK(!has(added, "{B}"));
|
||||||
|
|
||||||
|
// No change ⇒ nothing new.
|
||||||
|
CHECK(newGuids(cur, cur).empty());
|
||||||
|
|
||||||
|
// A removed GUID is not "new" (it is absent from current).
|
||||||
|
std::set<std::string> shrunk{"{A}"};
|
||||||
|
CHECK(newGuids(prev, shrunk).empty());
|
||||||
|
|
||||||
|
// Determinism: ascending set order.
|
||||||
|
std::set<std::string> p2;
|
||||||
|
std::set<std::string> c2{"{Z}", "{A}", "{M}"};
|
||||||
|
auto ordered = newGuids(p2, c2);
|
||||||
|
CHECK(ordered.size() == 3);
|
||||||
|
CHECK(ordered[0] == "{A}" && ordered[1] == "{M}" && ordered[2] == "{Z}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testNewGuidsIgnoresEmpty() {
|
||||||
|
std::set<std::string> prev{"{A}"};
|
||||||
|
std::set<std::string> cur{"", "{A}", "{B}"}; // empty ⇒ a GUID-read failure
|
||||||
|
auto added = newGuids(prev, cur);
|
||||||
|
CHECK(added.size() == 1);
|
||||||
|
CHECK(has(added, "{B}"));
|
||||||
|
CHECK(!has(added, "")); // never tag an empty GUID
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- 2. First-poll guard -----------------------------------------------------
|
||||||
|
|
||||||
|
static void testBaselineFirstPollReportsNothing() {
|
||||||
|
GuidBaseline b;
|
||||||
|
CHECK(!b.primed());
|
||||||
|
// First observe after open: pre-existing content must NOT be tagged.
|
||||||
|
auto first = b.observe({"{A}", "{B}", "{C}"});
|
||||||
|
CHECK(first.empty()); // nothing new at open
|
||||||
|
CHECK(b.primed());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- 3. Incremental detection ------------------------------------------------
|
||||||
|
|
||||||
|
static void testBaselineIncremental() {
|
||||||
|
GuidBaseline b;
|
||||||
|
b.observe({"{A}", "{B}"}); // baseline
|
||||||
|
auto t1 = b.observe({"{A}", "{B}", "{C}"});
|
||||||
|
CHECK(t1.size() == 1 && has(t1, "{C}")); // only the newly-added GUID
|
||||||
|
|
||||||
|
// Next tick with a further addition — earlier-added {C} is now baseline.
|
||||||
|
auto t2 = b.observe({"{A}", "{B}", "{C}", "{D}"});
|
||||||
|
CHECK(t2.size() == 1 && has(t2, "{D}"));
|
||||||
|
CHECK(!has(t2, "{C}"));
|
||||||
|
|
||||||
|
// A steady state reports nothing new.
|
||||||
|
CHECK(b.observe({"{A}", "{B}", "{C}", "{D}"}).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- 4. Deletion drops from baseline; reused GUID re-detected ----------------
|
||||||
|
|
||||||
|
static void testBaselineDeletionReDetect() {
|
||||||
|
GuidBaseline b;
|
||||||
|
b.observe({"{A}", "{B}"});
|
||||||
|
// Delete {B}: not "new", and drops out of the baseline.
|
||||||
|
CHECK(b.observe({"{A}"}).empty());
|
||||||
|
// {B} reappears (REAPER reused the GUID or the user re-added) ⇒ detected again.
|
||||||
|
auto again = b.observe({"{A}", "{B}"});
|
||||||
|
CHECK(again.size() == 1 && has(again, "{B}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- 5. reset() re-arms the first-poll guard (project switch) -----------------
|
||||||
|
|
||||||
|
static void testResetReBaselines() {
|
||||||
|
GuidBaseline b;
|
||||||
|
b.observe({"{A}"}); // project 1 baseline
|
||||||
|
b.observe({"{A}", "{B}"}); // {B} detected in project 1
|
||||||
|
|
||||||
|
b.reset();
|
||||||
|
CHECK(!b.primed());
|
||||||
|
// Switching to project 2: its pre-existing content must NOT be mass-tagged even
|
||||||
|
// though those GUIDs were never seen before reset.
|
||||||
|
auto afterSwitch = b.observe({"{X}", "{Y}", "{Z}"});
|
||||||
|
CHECK(afterSwitch.empty()); // re-baselined, nothing new
|
||||||
|
CHECK(b.primed());
|
||||||
|
// Content created in project 2 after the switch IS detected.
|
||||||
|
auto p2new = b.observe({"{X}", "{Y}", "{Z}", "{W}"});
|
||||||
|
CHECK(p2new.size() == 1 && has(p2new, "{W}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testNewGuidsDifference();
|
||||||
|
testNewGuidsIgnoresEmpty();
|
||||||
|
testBaselineFirstPollReportsNothing();
|
||||||
|
testBaselineIncremental();
|
||||||
|
testBaselineDeletionReDetect();
|
||||||
|
testResetReBaselines();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// Standalone tests for reasampler::lane_keys — no REAPER, no test framework. The pure
|
||||||
|
// managed/manual lane-name heuristic that resolves D2 design points #1 (auto-tag
|
||||||
|
// exemption) and #2 (durable lane identity vs ordinal renumber).
|
||||||
|
//
|
||||||
|
// Covers:
|
||||||
|
// 1. isManagedLaneName: only the "reasampler:" prefix is managed; everything else
|
||||||
|
// (empty, user comp names, near-miss prefixes) is manual.
|
||||||
|
// 2. managedLaneKey: managed name -> its durable key; manual/unnamed -> nullopt.
|
||||||
|
// 3. Round-trip: managedLaneKey(laneNameForMode(m)) == "reasampler:" + m, so the
|
||||||
|
// Wave-3 minting path and the read path cannot drift.
|
||||||
|
|
||||||
|
#include "../src/lane_keys.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
static void testIsManagedLaneName() {
|
||||||
|
// Tool-minted managed names.
|
||||||
|
CHECK(isManagedLaneName("reasampler:design"));
|
||||||
|
CHECK(isManagedLaneName("reasampler:arrange"));
|
||||||
|
CHECK(isManagedLaneName("reasampler:")); // prefix alone still ours (odd but managed)
|
||||||
|
|
||||||
|
// Manual / user lanes are never managed.
|
||||||
|
CHECK(!isManagedLaneName("")); // unnamed lane ⇒ manual
|
||||||
|
CHECK(!isManagedLaneName("Comp 1")); // user comp lane
|
||||||
|
CHECK(!isManagedLaneName("Lead vocal"));
|
||||||
|
CHECK(!isManagedLaneName("reasample")); // near-miss, no colon ⇒ not ours
|
||||||
|
CHECK(!isManagedLaneName("Reasampler:design")); // case-sensitive prefix
|
||||||
|
CHECK(!isManagedLaneName(" reasampler:x")); // leading space ⇒ not a prefix match
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testManagedLaneKey() {
|
||||||
|
// Managed lane: the durable name IS the key.
|
||||||
|
auto k = managedLaneKey("reasampler:design");
|
||||||
|
CHECK(k.has_value() && *k == "reasampler:design");
|
||||||
|
|
||||||
|
// Manual / unnamed lanes have no managed key (⇒ treated as manual, never driven).
|
||||||
|
CHECK(!managedLaneKey("").has_value());
|
||||||
|
CHECK(!managedLaneKey("Comp 1").has_value());
|
||||||
|
CHECK(!managedLaneKey("guitar-double").has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testRoundTrip() {
|
||||||
|
// Minting then reading must agree: managedLaneKey(laneNameForMode(m)) recovers the
|
||||||
|
// prefixed name for every mode id.
|
||||||
|
for (const std::string mode : {std::string("arrange"), std::string("design"),
|
||||||
|
std::string("mixdown")}) {
|
||||||
|
const std::string name = laneNameForMode(mode);
|
||||||
|
CHECK(name == "reasampler:" + mode);
|
||||||
|
CHECK(isManagedLaneName(name));
|
||||||
|
auto key = managedLaneKey(name);
|
||||||
|
CHECK(key.has_value() && *key == "reasampler:" + mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testIsManagedLaneName();
|
||||||
|
testManagedLaneKey();
|
||||||
|
testRoundTrip();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -940,6 +940,40 @@ static void testLaneOwnershipIndex() {
|
|||||||
CHECK(!idx.remove("{T}", "l0"));
|
CHECK(!idx.remove("{T}", "l0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- D2.2b Last-writer-wins ownership replace (round-trip) --------------------
|
||||||
|
//
|
||||||
|
// setManual then setManaged on the SAME (guid, laneKey) must leave EXACTLY ONE
|
||||||
|
// managed entry — the ownership record is replaced, not accumulated. Guards the
|
||||||
|
// "retag a lane the tool now owns" contract and its persistence: the replace must
|
||||||
|
// survive a serialize/deserialize round-trip with no stray manual duplicate.
|
||||||
|
static void testLaneOwnershipLastWriterWins() {
|
||||||
|
ViewModeModel vm;
|
||||||
|
|
||||||
|
// Manual first, then managed on the same lane — the managed write replaces.
|
||||||
|
CHECK(vm.lanes().setManual("{T}", "l0"));
|
||||||
|
CHECK(vm.lanes().setManaged("{T}", "l0", kDesignModeId));
|
||||||
|
CHECK(vm.lanes().size() == 1); // one entry, not two
|
||||||
|
const LaneOwnership* o = vm.lanes().query("{T}", "l0");
|
||||||
|
CHECK(o && o->isManaged() && *o->managedMode == kDesignModeId);
|
||||||
|
|
||||||
|
// The reverse also replaces: managed -> manual leaves exactly one manual entry.
|
||||||
|
CHECK(vm.lanes().setManual("{T}", "l0"));
|
||||||
|
CHECK(vm.lanes().size() == 1);
|
||||||
|
const LaneOwnership* m = vm.lanes().query("{T}", "l0");
|
||||||
|
CHECK(m && m->isManual());
|
||||||
|
|
||||||
|
// Back to managed, then round-trip: exactly one managed entry survives, no stray
|
||||||
|
// manual duplicate resurrected by (de)serialization.
|
||||||
|
CHECK(vm.lanes().setManaged("{T}", "l0", kArrangeModeId));
|
||||||
|
auto back = ViewModeModel::deserialize(vm.serialize());
|
||||||
|
CHECK(back.has_value());
|
||||||
|
if (back) {
|
||||||
|
CHECK(back->lanes().size() == 1);
|
||||||
|
const LaneOwnership* r = back->lanes().query("{T}", "l0");
|
||||||
|
CHECK(r && r->isManaged() && *r->managedMode == kArrangeModeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -- D2.3/D2.4 Managed-only: planner + query never emit a manual lane --------
|
// -- D2.3/D2.4 Managed-only: planner + query never emit a manual lane --------
|
||||||
//
|
//
|
||||||
// Required case: a track with a manual lane + managed mode lanes — neither the planner
|
// Required case: a track with a manual lane + managed mode lanes — neither the planner
|
||||||
@@ -1124,6 +1158,7 @@ int main() {
|
|||||||
// D2 two-canvas lane extension
|
// D2 two-canvas lane extension
|
||||||
testLaneModeStateAndPlayValues();
|
testLaneModeStateAndPlayValues();
|
||||||
testLaneOwnershipIndex();
|
testLaneOwnershipIndex();
|
||||||
|
testLaneOwnershipLastWriterWins();
|
||||||
testManagedOnlyPlannerAndQuery();
|
testManagedOnlyPlannerAndQuery();
|
||||||
testAutoTagDecision();
|
testAutoTagDecision();
|
||||||
testLaneJsonRoundTrip();
|
testLaneJsonRoundTrip();
|
||||||
|
|||||||
Reference in New Issue
Block a user