#pragma once // bank_sync — decision logic for bank-generation change-detection and the instrument-side // assignment-request consume. The instrument polls two "reasampler" ext-state keys off the // audio thread: the bank-generation counter (has the bank changed?) and the assignment // request (should I switch to a just-ingested sample?). The shell reads the raw strings and // owns cadence (a UI-thread timer, never `process`) + side effects (reloadInstrument, // setSelectedSampleId); this module owns only the yes/no decisions, so they're provable // without a host. assignment_request.h owns the wire format; this owns the consume decision. #include #include #include #include "core/wire/assignment_request.h" // AssignmentRequest (the decoded request this consumes) namespace reasampler::instrument::map { using wire::AssignmentRequest; // Default for a project with no bank_generation key yet (pre-existing project); the first // real bump (>= 1) then reads as a change against this. inline constexpr std::int64_t kBankGenerationAbsent = 0; // Absent / empty / malformed / negative / overflowing all yield kBankGenerationAbsent (0), // never a crash or spurious reload. Whole-string parse: trailing garbage rejects the value, // so a torn/partial write is ignored until the next clean poll. std::int64_t parseBankGeneration(const std::string& raw); // Inverse of parseBankGeneration: plain decimal, no sign, no padding — byte-stable across // writes of the same value. std::string formatBankGeneration(std::int64_t generation); // True when `current` differs from `seen` (not just increases — a project switch/reload can // legitimately lower the value, and the reader should still re-read the bank). bool bankGenerationChanged(std::int64_t seen, std::int64_t current); // Verdict of the assignment-request consume decision below. apply and consumedGeneration // advancing are NOT the same event — a request naming an unresolvable sample is dropped // (consumed-as-seen) without applying, so the shell doesn't re-evaluate it every poll. struct AssignConsumeDecision { bool apply = false; // set this instance's selection to (bankId, sampleId) + reload std::string bankId; // the request's bank (valid only when apply) std::string sampleId; // the request's sample (valid only when apply) std::int64_t consumedGeneration = 0; // the marker to persist (== lastConsumed when nothing new) }; // `lastConsumed` persists across reopen so a request already applied and manually changed // away from is not reapplied. `resolves` is whether (bankId, sampleId) exists in the live // bank right now. `isFocusedTarget` gates thundering-herd (only the focused-editor instance // applies; others neither apply nor advance their marker, staying eligible if focus moves). // // Rules, in order: (1) no request or generation <= lastConsumed -> no-op. (2) new but not // the target -> no-op, marker unchanged (stays eligible later). (3) new, target, but doesn't // resolve -> drop silently, marker still advances (consumed-as-seen, never re-evaluated). // (4) new, target, resolves -> apply + advance marker. AssignConsumeDecision consumeDecision(const std::optional& request, std::int64_t lastConsumed, bool resolves, bool isFocusedTarget); } // namespace reasampler::instrument::map