#pragma once // Per-mode solo surface: the cache of raw I_SOLO values a real mode switch banks on // the way out and replays on the way back, plus the two decisions over it. Pure — // the GetMediaTrackInfo_Value/SetMediaTrackInfo_Value pair is shell/view/view_solo. // Values are the RAW I_SOLO int, never collapsed to a bool: the SDK's domain is // 0=off, 1=solo, 2=solo-in-place, 5=safe solo, 6=safe solo-in-place, and all four // non-zero variants must survive the round trip. #include #include #include #include #include namespace reasampler::view { // One live (track GUID, I_SOLO) reading from the shell's enumeration. struct TrackSolo { std::string guid; int solo = 0; }; // One I_SOLO write the shell must apply. struct SoloOp { std::string guid; int value = 0; bool operator==(const SoloOp& o) const { return guid == o.guid && value == o.value; } }; // The soloed subset of a live enumeration. Zero is the resting value — a track at // zero is neither cached (nothing to replay) nor cleared (nothing to undo), so a // project with no solo anywhere produces no cache entry and no project write at // all. Empty GUIDs are dropped: they can never resolve back to a track. std::map soloedTracks(const std::vector& live); // mode id -> (track GUID -> raw I_SOLO). Lifecycle mirrors the park/restore // snapshots: stored on the way out of a mode, consumed on the way back in, pruned // when a GUID stops existing. class SoloCache { public: // Replaces `modeId`'s entry. An EMPTY set removes it rather than storing an // empty record — otherwise a serialized cache would parse back to a model that // differs from its source, breaking the model's round-trip contract. bool store(const std::string& modeId, const std::map& soloed); const std::map* query(const std::string& modeId) const; bool clear(const std::string& modeId); const std::map>& all() const { return byMode_; } bool empty() const { return byMode_.empty(); } // Drops every cached GUID absent from `liveGuids`, and any mode left empty. // Returns the number of GUID entries removed. // // Pruned like the snapshots and unlike membership: a cached solo is a captured // prior value awaiting replay onto one specific track, so a stale entry // surviving a delete would replay onto whatever track later reuses that GUID — // soloing a track the user never soloed and silencing the rest of the mix. // The cost is the mirror case: undoing a track delete restores the GUID but not // its cached solo. One lost solo the user can see and re-click beats an // inexplicable mix-wide mute. std::size_t reconcile(const std::set& liveGuids); bool operator==(const SoloCache& o) const { return byMode_ == o.byMode_; } private: std::map> byMode_; }; // The incoming mode's restore writes, in GUID order. Two kinds of entry are dropped // rather than written: // * a GUID absent from `liveGuids` — the track is gone (same prune rule as above); // * a GUID absent from `visibleGuids` — not visible in the incoming mode, whether // because the leaf is parked or because it is a folder parent that is itself // derived-invisible there. Either way the track is hidden and (for a parked // leaf) carries B_MAINSEND=0, so soloing it would silence the whole mix while // contributing nothing audible, and the user would have no visible control to // undo it. A show-both leaf is a member of every mode's visible set, so it is // never dropped by this rule. // The caller consumes the whole mode entry regardless (clear-on-restore), so a // dropped entry does not linger as zombie state waiting on a track that may never // become visible again. std::vector planSoloRestore(const std::map& cached, const std::set& liveGuids, const std::set& visibleGuids); } // namespace reasampler::view