Files
reasampler/src/view_mode_model.cpp
T

725 lines
26 KiB
C++

#include "view_mode_model.h"
#include <algorithm>
#include <cerrno>
#include <climits>
#include <cstdio>
#include <cstdlib>
// view_mode_model implementation.
//
// JSON is hand-rolled and self-contained, mirroring bank_model's approach (brief:
// keep the pure core dependency-free — no third-party JSON lib). A compact writer
// plus a recursive-descent parser covers the field set: the mode registry, the
// GUID-keyed membership map, per-track snapshots (with a variable-length per-FX
// offline vector), and the active mode. Ints are emitted plainly; strings are
// escaped identically to bank_model so control chars and unicode survive.
namespace reasampler {
// ---------------------------------------------------------------------------
// equality
// ---------------------------------------------------------------------------
bool Mode::operator==(const Mode& o) const {
return id == o.id && displayName == o.displayName && ordinal == o.ordinal;
}
// ---------------------------------------------------------------------------
// ModeRegistry
// ---------------------------------------------------------------------------
ModeRegistry::ModeRegistry() {
modes_.push_back(Mode{kArrangeModeId, "Arrange", 0});
modes_.push_back(Mode{kDesignModeId, "Design", 1});
}
bool ModeRegistry::add(const Mode& mode) {
if (mode.id.empty()) return false;
if (query(mode.id) != nullptr) return false; // ids are unique
modes_.push_back(mode);
// Keep ordinal order stable; std::stable_sort so equal ordinals keep insertion
// order (the tie-break documented in the header).
std::stable_sort(modes_.begin(), modes_.end(),
[](const Mode& a, const Mode& b) { return a.ordinal < b.ordinal; });
return true;
}
const Mode* ModeRegistry::query(const std::string& id) const {
for (const auto& m : modes_)
if (m.id == id) return &m;
return nullptr;
}
// ---------------------------------------------------------------------------
// MembershipIndex
// ---------------------------------------------------------------------------
bool MembershipIndex::tag(const std::string& guid, const std::string& modeId) {
if (guid.empty() || modeId.empty()) return false;
Membership& m = entries_[guid];
m.modeIds.clear(); // a leaf lives in exactly one mode (show-both aside)
m.modeIds.insert(modeId);
return true;
}
bool MembershipIndex::untag(const std::string& guid) {
return entries_.erase(guid) > 0;
}
bool MembershipIndex::setShowBoth(const std::string& guid, bool showBoth) {
if (guid.empty()) return false;
entries_[guid].showBoth = showBoth; // creates an Arrange-default entry if new
return true;
}
bool MembershipIndex::restore(const std::string& guid, const Membership& membership) {
if (guid.empty()) return false;
entries_[guid] = membership;
return true;
}
const Membership* MembershipIndex::query(const std::string& guid) const {
auto it = entries_.find(guid);
return it == entries_.end() ? nullptr : &it->second;
}
std::set<std::string> MembershipIndex::modesOf(const std::string& guid) const {
const Membership* m = query(guid);
return m ? m->modeIds : std::set<std::string>{};
}
// ---------------------------------------------------------------------------
// planner helpers
// ---------------------------------------------------------------------------
TrackPlan makeParkPlan(const std::string& guid, int fxCount) {
// Parking contract: hide both panels, out of the mix, FX bypassed, every FX
// offline. All fixed zeros — park never consults a snapshot.
TrackPlan p;
p.flags = {
{guid, Flag::ShowInTcp, 0},
{guid, Flag::ShowInMixer, 0},
{guid, Flag::MainSend, 0},
{guid, Flag::FxEnable, 0},
};
for (int i = 0; i < fxCount; ++i)
p.fxOffline.push_back({guid, i, true});
return p;
}
TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap) {
// Restore contract: every driven flag returns to its SNAPSHOTTED value — never
// a hardcoded "on"/default. A flag captured at 0 restores to 0.
TrackPlan p;
p.flags = {
{guid, Flag::ShowInTcp, snap.showInTcp},
{guid, Flag::ShowInMixer, snap.showInMixer},
{guid, Flag::MainSend, snap.mainSend},
{guid, Flag::FxEnable, snap.fxEnable},
};
for (std::size_t i = 0; i < snap.fxOffline.size(); ++i)
p.fxOffline.push_back({guid, static_cast<int>(i), snap.fxOffline[i] != 0});
return p;
}
// ---------------------------------------------------------------------------
// ViewModeModel
// ---------------------------------------------------------------------------
ViewModeModel::ViewModeModel() : activeModeId_(kArrangeModeId) {}
bool ViewModeModel::setActiveMode(const std::string& modeId) {
if (!modes_.contains(modeId)) return false;
activeModeId_ = modeId;
return true;
}
void ViewModeModel::storeSnapshot(const std::string& guid, const TrackSnapshot& snap) {
snapshots_[guid] = snap;
}
void ViewModeModel::clearSnapshot(const std::string& guid) {
snapshots_.erase(guid);
}
const TrackSnapshot* ViewModeModel::snapshot(const std::string& guid) const {
auto it = snapshots_.find(guid);
return it == snapshots_.end() ? nullptr : &it->second;
}
bool ViewModeModel::leafBelongsToMode(const std::string& guid, const std::string& modeId) const {
const Membership* m = membership_.query(guid);
if (!m) return modeId == kArrangeModeId; // untagged ⇒ Arrange default
if (m->showBoth) return true; // show-both ⇒ every mode
if (m->modeIds.empty()) return modeId == kArrangeModeId; // show-both-cleared, no mode
return m->modeIds.count(modeId) > 0;
}
std::set<std::string> ViewModeModel::visibleTracks(const FolderTree& tree,
const std::string& modeId) const {
std::set<std::string> visible;
// Pass 1: every leaf that belongs to the mode is visible.
for (const auto& node : tree.nodes) {
if (node.isParent) continue; // parents derived in pass 2
if (leafBelongsToMode(node.guid, modeId))
visible.insert(node.guid);
}
// Pass 2: a parent is visible if any descendant leaf is visible. Walk each
// visible leaf up its parent chain and mark ancestors. Parent chains are read
// from the supplied tree only (no REAPER access). A cycle-guard bounds the walk
// in case a malformed tree links a node to itself.
std::map<std::string, std::string> parentOf;
for (const auto& node : tree.nodes) parentOf[node.guid] = node.parentGuid;
// Snapshot the leaf-visible set so we don't re-walk parents we add mid-loop.
const std::vector<std::string> seeds(visible.begin(), visible.end());
for (const auto& leaf : seeds) {
auto it = parentOf.find(leaf);
std::size_t guard = 0;
while (it != parentOf.end() && !it->second.empty() && guard++ < parentOf.size()) {
const std::string& parent = it->second;
if (!visible.insert(parent).second) break; // already marked ⇒ chain done
it = parentOf.find(parent);
}
}
return visible;
}
TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string& targetMode) const {
TogglePlan plan;
// Index the tree so we can classify each tagged GUID (leaf vs parent vs stale).
std::map<std::string, const FolderNode*> byGuid;
for (const auto& node : tree.nodes) byGuid[node.guid] = &node;
for (const auto& [guid, m] : membership_.all()) {
auto it = byGuid.find(guid);
if (it == byGuid.end()) continue; // stale GUID: prune-safe, ignore
if (it->second->isParent) continue; // parents are derived, never parked
if (m.showBoth) continue; // show-both leaves are never parked
const bool active = leafBelongsToMode(guid, targetMode);
if (active) {
// Returning to visibility: restore from snapshot if we have one. No
// snapshot ⇒ the track was never parked, nothing to restore.
if (const TrackSnapshot* snap = snapshot(guid))
plan.restore.push_back(makeRestorePlan(guid, *snap));
} else {
// Inactive tagged leaf ⇒ park. fxOffline is intentionally empty here:
// the D2 shell expands per-FX offline writes using TrackFX_GetCount.
// The pure model has no access to REAPER FX counts at plan time;
// makeParkPlan(guid, 0) emits only the scalar flags as a result.
plan.park.push_back(makeParkPlan(guid, /*fxCount=*/0));
}
}
return plan;
}
bool ViewModeModel::operator==(const ViewModeModel& o) const {
return modes_ == o.modes_ && membership_ == o.membership_ &&
activeModeId_ == o.activeModeId_ && snapshots_ == o.snapshots_;
}
// ===========================================================================
// JSON — writer
// ===========================================================================
namespace {
void writeEscaped(std::string& out, const std::string& s) {
out += '"';
for (char c : s) {
switch (c) {
case '"': out += "\\\""; break;
case '\\': out += "\\\\"; break;
case '\b': out += "\\b"; break;
case '\f': out += "\\f"; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default:
if (static_cast<unsigned char>(c) < 0x20) {
char buf[8];
std::snprintf(buf, sizeof(buf), "\\u%04x", static_cast<unsigned char>(c));
out += buf;
} else {
out += c;
}
}
}
out += '"';
}
std::string intToStr(int v) {
char buf[16];
std::snprintf(buf, sizeof(buf), "%d", v);
return buf;
}
void writeIntArray(std::string& out, const std::vector<int>& v) {
out += '[';
for (std::size_t i = 0; i < v.size(); ++i) {
if (i) out += ',';
out += intToStr(v[i]);
}
out += ']';
}
class ObjWriter {
public:
explicit ObjWriter(std::string& out) : out_(out) { out_ += '{'; }
~ObjWriter() { out_ += '}'; }
void keyRaw(const char* key, const std::string& rawValue) {
sep();
writeEscaped(out_, key);
out_ += ':';
out_ += rawValue;
}
void keyStr(const char* key, const std::string& value) {
sep();
writeEscaped(out_, key);
out_ += ':';
writeEscaped(out_, value);
}
void keyBegin(const char* key) {
sep();
writeEscaped(out_, key);
out_ += ':';
}
private:
void sep() { if (first_) first_ = false; else out_ += ','; }
std::string& out_;
bool first_ = true;
};
} // namespace
std::string ViewModeModel::serialize() const {
std::string out;
{
ObjWriter root(out);
root.keyRaw("version", intToStr(1));
root.keyStr("activeMode", activeModeId_);
// modes
root.keyBegin("modes");
out += '[';
{
const auto& all = modes_.all();
for (std::size_t i = 0; i < all.size(); ++i) {
if (i) out += ',';
ObjWriter m(out);
m.keyStr("id", all[i].id);
m.keyStr("displayName", all[i].displayName);
m.keyRaw("ordinal", intToStr(all[i].ordinal));
}
}
out += ']';
// membership: array of { guid, modes[], showBoth }
root.keyBegin("membership");
out += '[';
{
bool first = true;
for (const auto& [guid, mem] : membership_.all()) {
if (!first) out += ','; first = false;
ObjWriter e(out);
e.keyStr("guid", guid);
e.keyBegin("modes");
out += '[';
{
bool mf = true;
for (const auto& id : mem.modeIds) {
if (!mf) out += ','; mf = false;
writeEscaped(out, id);
}
}
out += ']';
e.keyRaw("showBoth", mem.showBoth ? "true" : "false");
}
}
out += ']';
// snapshots: array of { guid, showInTcp, showInMixer, mainSend, fxEnable, fxOffline[] }
root.keyBegin("snapshots");
out += '[';
{
bool first = true;
for (const auto& [guid, snap] : snapshots_) {
if (!first) out += ','; first = false;
ObjWriter e(out);
e.keyStr("guid", guid);
e.keyRaw("showInTcp", intToStr(snap.showInTcp));
e.keyRaw("showInMixer", intToStr(snap.showInMixer));
e.keyRaw("mainSend", intToStr(snap.mainSend));
e.keyRaw("fxEnable", intToStr(snap.fxEnable));
e.keyBegin("fxOffline");
writeIntArray(out, snap.fxOffline);
}
}
out += ']';
} // root closes here (see bank_model note on NRVO + deferred close)
return out;
}
// ===========================================================================
// JSON — parser (recursive descent; false on any malformed input, never UB)
// ===========================================================================
namespace {
class Parser {
public:
explicit Parser(const std::string& s) : s_(s) {}
bool parseModel(ViewModeModel& out);
private:
const std::string& s_;
std::size_t pos_ = 0;
bool eof() const { return pos_ >= s_.size(); }
void skipWs() {
while (!eof()) {
char c = s_[pos_];
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') ++pos_;
else break;
}
}
bool consume(char c) {
skipWs();
if (eof() || s_[pos_] != c) return false;
++pos_;
return true;
}
bool parseString(std::string& out);
bool parseRawScalar(std::string& out);
bool parseInt(int& out);
bool parseBool(bool& out);
bool parseKey(std::string& key);
bool skipValue();
bool parseModes(ModeRegistry& reg);
bool parseMembership(MembershipIndex& idx);
bool parseSnapshots(std::map<std::string, TrackSnapshot>& snaps);
bool parseIntArray(std::vector<int>& out);
};
bool Parser::parseString(std::string& out) {
skipWs();
if (eof() || s_[pos_] != '"') return false;
++pos_;
out.clear();
while (!eof()) {
char c = s_[pos_++];
if (c == '"') return true;
if (c == '\\') {
if (eof()) return false;
char e = s_[pos_++];
switch (e) {
case '"': out += '"'; break;
case '\\': out += '\\'; break;
case '/': out += '/'; break;
case 'b': out += '\b'; break;
case 'f': out += '\f'; break;
case 'n': out += '\n'; break;
case 'r': out += '\r'; break;
case 't': out += '\t'; break;
case 'u': {
auto readHex4 = [&](unsigned int& cp) -> bool {
if (pos_ + 4 > s_.size()) return false;
cp = 0;
for (int i = 0; i < 4; ++i) {
char h = s_[pos_++];
cp <<= 4;
if (h >= '0' && h <= '9') cp |= static_cast<unsigned>(h - '0');
else if (h >= 'a' && h <= 'f') cp |= static_cast<unsigned>(h - 'a' + 10);
else if (h >= 'A' && h <= 'F') cp |= static_cast<unsigned>(h - 'A' + 10);
else return false;
}
return true;
};
unsigned int hi = 0;
if (!readHex4(hi)) return false;
unsigned int codePoint = hi;
if (hi >= 0xD800 && hi <= 0xDBFF) {
if (pos_ + 6 > s_.size()) return false;
if (s_[pos_] != '\\' || s_[pos_ + 1] != 'u') return false;
pos_ += 2;
unsigned int lo = 0;
if (!readHex4(lo)) return false;
if (lo < 0xDC00 || lo > 0xDFFF) return false;
codePoint = 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00);
} else if (hi >= 0xDC00 && hi <= 0xDFFF) {
return false;
}
if (codePoint <= 0x7F) {
out += static_cast<char>(codePoint);
} else if (codePoint <= 0x7FF) {
out += static_cast<char>(0xC0 | (codePoint >> 6));
out += static_cast<char>(0x80 | (codePoint & 0x3F));
} else if (codePoint <= 0xFFFF) {
out += static_cast<char>(0xE0 | (codePoint >> 12));
out += static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F));
out += static_cast<char>(0x80 | (codePoint & 0x3F));
} else {
out += static_cast<char>(0xF0 | (codePoint >> 18));
out += static_cast<char>(0x80 | ((codePoint >> 12) & 0x3F));
out += static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F));
out += static_cast<char>(0x80 | (codePoint & 0x3F));
}
break;
}
default: return false;
}
} else {
out += c;
}
}
return false; // unterminated
}
bool Parser::parseRawScalar(std::string& out) {
skipWs();
std::size_t start = pos_;
while (!eof()) {
char c = s_[pos_];
if (c == ',' || c == '}' || c == ']' || c == ' ' || c == '\t' ||
c == '\n' || c == '\r')
break;
++pos_;
}
if (pos_ == start) return false;
out.assign(s_, start, pos_ - start);
return true;
}
bool Parser::parseInt(int& out) {
std::string tok;
if (!parseRawScalar(tok)) return false;
const char* b = tok.c_str();
char* end = nullptr;
errno = 0;
long long v = std::strtoll(b, &end, 10);
if (end != b + tok.size()) return false;
if (errno == ERANGE) return false;
if (v < INT_MIN || v > INT_MAX) return false;
out = static_cast<int>(v);
return true;
}
bool Parser::parseBool(bool& out) {
std::string tok;
if (!parseRawScalar(tok)) return false;
if (tok == "true") { out = true; return true; }
if (tok == "false") { out = false; return true; }
return false;
}
bool Parser::parseKey(std::string& key) {
if (!parseString(key)) return false;
return consume(':');
}
bool Parser::skipValue() {
skipWs();
if (eof()) return false;
char c = s_[pos_];
if (c == '"') { std::string tmp; return parseString(tmp); }
if (c == '{' || c == '[') {
char open = c, close = (c == '{') ? '}' : ']';
++pos_;
int depth = 1;
while (!eof() && depth > 0) {
char d = s_[pos_];
if (d == '"') { std::string tmp; if (!parseString(tmp)) return false; continue; }
if (d == open) ++depth;
else if (d == close) --depth;
++pos_;
}
return depth == 0;
}
std::string tmp;
return parseRawScalar(tmp);
}
bool Parser::parseIntArray(std::vector<int>& out) {
if (!consume('[')) return false;
skipWs();
if (consume(']')) return true;
do {
int v = 0;
if (!parseInt(v)) return false;
out.push_back(v);
} while (consume(','));
return consume(']');
}
// The registry starts seeded (Arrange + Design). Deserialization must reproduce the
// serialized set exactly, so we replace the seeded contents with the parsed ones —
// add() dedups by id, so a serialized Arrange/Design would otherwise be rejected as
// duplicates and the ordinals/names would not round-trip. We therefore parse into a
// fresh vector and swap. `reg` is passed empty (see parseModel).
bool Parser::parseModes(ModeRegistry& reg) {
if (!consume('[')) return false;
skipWs();
if (consume(']')) return true; // empty array (unusual, but valid)
do {
if (!consume('{')) return false;
Mode m;
bool haveId = false;
do {
std::string k;
if (!parseKey(k)) return false;
if (k == "id") { if (!parseString(m.id)) return false; haveId = true; }
else if (k == "displayName") { if (!parseString(m.displayName)) return false; }
else if (k == "ordinal") { if (!parseInt(m.ordinal)) return false; }
else if (!skipValue()) return false;
} while (consume(','));
if (!consume('}')) return false;
if (!haveId || !reg.add(m)) return false; // malformed / duplicate id
} while (consume(','));
return consume(']');
}
bool Parser::parseMembership(MembershipIndex& idx) {
if (!consume('[')) return false;
skipWs();
if (consume(']')) return true;
do {
if (!consume('{')) return false;
std::string guid;
Membership mem;
bool haveGuid = false;
do {
std::string k;
if (!parseKey(k)) return false;
if (k == "guid") { if (!parseString(guid)) return false; haveGuid = true; }
else if (k == "modes") {
if (!consume('[')) return false;
skipWs();
if (!consume(']')) {
do {
std::string id;
if (!parseString(id)) return false;
mem.modeIds.insert(id);
} while (consume(','));
if (!consume(']')) return false;
}
}
else if (k == "showBoth") { if (!parseBool(mem.showBoth)) return false; }
else if (!skipValue()) return false;
} while (consume(','));
if (!consume('}')) return false;
if (!haveGuid || guid.empty()) return false;
// Install the entry verbatim (tag() would clear a multi-mode set and drop
// show-both). A serialized entry is trusted to already satisfy the model's
// invariants.
//
// Deliberate tolerance: we do NOT validate that membership modeIds reference
// registered modes, and we do not validate snapshot GUIDs against the index.
// Stale-GUID and stale-mode tolerance is a stated invariant of this model —
// a deserialized entry is treated as trusted data, not as live cross-checked
// state. Rejecting stale entries here would violate that invariant. The one
// exception is activeMode (validated below in parseModel): a persisted active
// mode that no longer exists has an immediate behavioral consequence, so it
// is caught and the parse is rejected.
if (!idx.restore(guid, mem)) return false;
} while (consume(','));
return consume(']');
}
bool Parser::parseSnapshots(std::map<std::string, TrackSnapshot>& snaps) {
if (!consume('[')) return false;
skipWs();
if (consume(']')) return true;
do {
if (!consume('{')) return false;
std::string guid;
TrackSnapshot snap;
bool haveGuid = false;
do {
std::string k;
if (!parseKey(k)) return false;
if (k == "guid") { if (!parseString(guid)) return false; haveGuid = true; }
else if (k == "showInTcp") { if (!parseInt(snap.showInTcp)) return false; }
else if (k == "showInMixer") { if (!parseInt(snap.showInMixer)) return false; }
else if (k == "mainSend") { if (!parseInt(snap.mainSend)) return false; }
else if (k == "fxEnable") { if (!parseInt(snap.fxEnable)) return false; }
else if (k == "fxOffline") { if (!parseIntArray(snap.fxOffline)) return false; }
else if (!skipValue()) return false;
} while (consume(','));
if (!consume('}')) return false;
if (!haveGuid || guid.empty()) return false;
snaps[guid] = snap;
} while (consume(','));
return consume(']');
}
bool Parser::parseModel(ViewModeModel& out) {
if (!consume('{')) return false;
skipWs();
if (consume('}')) return true; // lenient empty root ⇒ default-seeded model
ModeRegistry reg; // seeded default; REPLACED if a modes array is present
bool haveModes = false;
std::string activeMode;
bool haveActive = false;
MembershipIndex membership;
std::map<std::string, TrackSnapshot> snaps;
do {
std::string key;
if (!parseKey(key)) return false;
if (key == "activeMode") {
if (!parseString(activeMode)) return false;
haveActive = true;
} else if (key == "modes") {
ModeRegistry fresh = ModeRegistry::makeEmpty(); // parse into empty, then own
if (!parseModes(fresh)) return false;
reg = fresh;
haveModes = true;
} else if (key == "membership") {
if (!parseMembership(membership)) return false;
} else if (key == "snapshots") {
if (!parseSnapshots(snaps)) return false;
} else {
// Unknown keys and the "version" field are skipped here.
// "version" is serialized as a forward-compat placeholder — there is no
// active version gate yet; all persisted data is parsed the same way
// regardless of the value. A future gate would add a version branch here.
if (!skipValue()) return false;
}
} while (consume(','));
if (!consume('}')) return false;
skipWs();
if (!eof()) return false; // trailing garbage
if (haveModes) out.modes() = reg;
out.membership() = membership;
for (const auto& [guid, snap] : snaps) out.storeSnapshot(guid, snap);
if (haveActive) {
if (!out.setActiveMode(activeMode)) return false; // active mode must exist
}
return true;
}
} // namespace
std::optional<ViewModeModel> ViewModeModel::deserialize(const std::string& json) {
ViewModeModel vm;
Parser p(json);
if (!p.parseModel(vm)) return std::nullopt;
return vm;
}
} // namespace reasampler