Ψ-W1-T2: disjoint per-mode solo surfaces and a playback-gated mode switch

Solo is cached, cleared and replayed per mode on a real switch only; the switch is refused visibly while the transport runs. The footer segment now routes through the activate actions, so a panel switch finally persists.
This commit is contained in:
2026-08-01 19:43:18 -04:00
parent 8bf6841f7b
commit 9c234c2e6b
23 changed files with 811 additions and 49 deletions
+86
View File
@@ -0,0 +1,86 @@
#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 <cstddef>
#include <map>
#include <set>
#include <string>
#include <vector>
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<std::string, int> soloedTracks(const std::vector<TrackSolo>& 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<std::string, int>& soloed);
const std::map<std::string, int>* query(const std::string& modeId) const;
bool clear(const std::string& modeId);
const std::map<std::string, std::map<std::string, int>>& 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<std::string>& liveGuids);
bool operator==(const SoloCache& o) const { return byMode_ == o.byMode_; }
private:
std::map<std::string, std::map<std::string, int>> 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 parked in the incoming mode — a parked track is hidden and 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.
// 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
// come back unparked.
std::vector<SoloOp> planSoloRestore(const std::map<std::string, int>& cached,
const std::set<std::string>& liveGuids,
const std::set<std::string>& parkedGuids);
} // namespace reasampler::view