247 lines
13 KiB
C++
247 lines
13 KiB
C++
#pragma once
|
|
// bank_book — the pure core of the multi-bank phase (Phase B), deliberately free
|
|
// of any REAPER type so it compiles and unit-tests OUTSIDE the DAW. It is the
|
|
// third instance of the same "pure registry + JSON round-trip, unit-tested outside
|
|
// the DAW" pattern as bank_model and view_mode_model.
|
|
//
|
|
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
|
|
// vendor/ includes. Standard library only.
|
|
//
|
|
// -- What it is --------------------------------------------------------------
|
|
//
|
|
// An ordered registry of banks. Each bank = { stable id, display name, ordinal,
|
|
// BankIndex }. The book WRAPS N BankIndex instances — bank_model / BankIndex are
|
|
// UNTOUCHED (additive: no bankId on Sample). Movement of samples between banks is
|
|
// index-only (remove from source's BankIndex, add to destination's); files never
|
|
// relocate — banks are logical groupings over one shared file pool.
|
|
//
|
|
// -- The pool (privileged, not special-cased) --------------------------------
|
|
//
|
|
// Structurally the pool is bank-zero — one Bank among many, seeded on construction
|
|
// with a fixed id (kPoolBankId) and fixed display name (kPoolBankName), ordinal 0.
|
|
// Semantically it is privileged, and the privileges are enforced HERE in the pure
|
|
// rules layer (CONTEXT.md §Multi-bank guardrail — not deferred to a shell):
|
|
// * always exists (seeded on construction; the book never reaches zero banks)
|
|
// * un-deletable (deleteBank rejects the pool)
|
|
// * un-renamable (renameBank rejects the pool)
|
|
// * un-evacuable (evacuate rejects the pool — the pool is evacuation's
|
|
// destination, not a source)
|
|
//
|
|
// -- Id minting is the CALLER'S job (design decision) ------------------------
|
|
//
|
|
// createBank takes a caller-supplied stable id, mirroring bank_model's "id
|
|
// assigned by the caller" and view_mode_model's mode ids. The pure core has no
|
|
// REAPER genGuid / RNG and deliberately introduces none: a fake in-model id source
|
|
// would not be a real GUID anyway, and keeping ids caller-supplied lets the B2
|
|
// shell mint a genuine REAPER GUID while the model stays pure and deterministically
|
|
// testable. The model still enforces the invariants: non-empty, unique, not the
|
|
// reserved pool id.
|
|
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "bank_model.h"
|
|
|
|
namespace reasampler {
|
|
|
|
// The pool's fixed identity. The id is reserved: createBank rejects it, and the
|
|
// pool is always bank-zero. The name is fixed: renameBank rejects the pool.
|
|
inline constexpr const char* kPoolBankId = "pool";
|
|
inline constexpr const char* kPoolBankName = "Pool";
|
|
|
|
// One bank: a stable id, a display name, an ordinal (tab/display order), and its
|
|
// own BankIndex. The pool is the bank whose id == kPoolBankId.
|
|
struct Bank {
|
|
std::string id; // stable, persisted; the pool's is kPoolBankId
|
|
std::string displayName; // mutable for named banks; fixed "Pool" for the pool
|
|
int ordinal = 0; // display order; pool is 0, named banks 1..N
|
|
BankIndex index; // this bank's samples
|
|
|
|
bool isPool() const { return id == kPoolBankId; }
|
|
|
|
bool operator==(const Bank& o) const {
|
|
return id == o.id && displayName == o.displayName &&
|
|
ordinal == o.ordinal && index == o.index;
|
|
}
|
|
};
|
|
|
|
// Outcome of a cross-bank sample move/copy. Mirrors AddResult's honesty: the op
|
|
// reports what happened rather than silently mutating on a bad request.
|
|
// - Moved / Copied: the sample was transferred to the destination as a new entry.
|
|
// - Collapsed: the destination already held the hash; it collapsed onto the
|
|
// existing entry (a no-op add on the destination side). For a
|
|
// MOVE the source entry is STILL removed; for a COPY the source
|
|
// entry is (as always) retained.
|
|
// - RejectedUnknownBank: a source or destination id named no bank.
|
|
// - RejectedSampleAbsent: the sample id was not in the source bank.
|
|
// - RejectedSameBank: source and destination were the same bank (no-op).
|
|
enum class TransferResult {
|
|
Moved,
|
|
Copied,
|
|
Collapsed,
|
|
RejectedUnknownBank,
|
|
RejectedSampleAbsent,
|
|
RejectedSameBank,
|
|
};
|
|
|
|
// An ordered registry of banks with the pool seeded as bank-zero, per-bank sample
|
|
// indices, an active-bank pointer, and lossless JSON round-trip. The heart of the
|
|
// multi-bank phase — mirror of bank_model / view_mode_model.
|
|
class BankBook {
|
|
public:
|
|
BankBook(); // seeds the pool (id kPoolBankId, name kPoolBankName, ordinal 0);
|
|
// active bank = pool; zero named banks.
|
|
|
|
// -- Bank lifecycle ------------------------------------------------------
|
|
|
|
// Creates a named bank with the caller-supplied stable id and display name,
|
|
// assigning the next ordinal. Rejects (returns false, no mutation) an empty id,
|
|
// a duplicate id, the reserved pool id, or a display name that duplicates an
|
|
// existing bank's name (including the pool's "Pool"). Display-name uniqueness is
|
|
// trimmed + case-insensitive (ASCII): "Drums", "drums", and " Drums " collide.
|
|
bool createBank(const std::string& id, const std::string& displayName);
|
|
|
|
// Renames a named bank. Rejects (false, no mutation) an unknown id, the pool, or a
|
|
// target name already used by a DIFFERENT bank (trimmed + case-insensitive, as
|
|
// createBank). Renaming a bank to its own current name is a no-op success.
|
|
bool renameBank(const std::string& id, const std::string& displayName);
|
|
|
|
// Deletes a NAMED bank, removing it (and its member index entries) from the
|
|
// registry. Files are a shell/prune concern and are NOT touched here. Rejects
|
|
// (false, no mutation) an unknown id or the pool. Remaining banks' ordinals are
|
|
// compacted so the pool stays 0 and named banks stay contiguous 1..N. If the
|
|
// deleted bank was active, the active bank falls back to the pool.
|
|
bool deleteBank(const std::string& id);
|
|
|
|
// Reorders a NAMED bank to `newOrdinal` (clamped into the named-bank range),
|
|
// shifting the others to keep ordinals contiguous. The pool is pinned at 0 and
|
|
// cannot be reordered. Rejects (false, no mutation) an unknown id or the pool.
|
|
bool reorderBank(const std::string& id, int newOrdinal);
|
|
|
|
// Moves EVERY member of a named bank into the pool (index-only, observing the
|
|
// same destination-collapse-by-hash as a move), leaving the bank empty. Rejects
|
|
// (false, no mutation) an unknown id or the pool (the pool is the destination,
|
|
// never a source). Returns true on success even if the bank was already empty.
|
|
bool evacuate(const std::string& id);
|
|
|
|
// -- Active bank ---------------------------------------------------------
|
|
|
|
// The active bank's id (the capture target). Defaults to the pool.
|
|
const std::string& activeBankId() const { return activeBankId_; }
|
|
|
|
// Sets the active bank. Rejects (returns false, no change) an id that names no
|
|
// bank — an invalid set never corrupts state.
|
|
bool setActiveBank(const std::string& id);
|
|
|
|
// The active bank's BankIndex — the index the capture layer adds to. Always
|
|
// valid (the active id always names a live bank; it falls back to the pool).
|
|
BankIndex& activeIndex();
|
|
const BankIndex& activeIndex() const;
|
|
|
|
// -- Sample movement (index-only; files never relocate) ------------------
|
|
|
|
// Moves a sample by id from `fromBankId` to `toBankId`: removes it from the
|
|
// source index and adds it to the destination (observing destination
|
|
// collapse-by-hash). See TransferResult for the full outcome set.
|
|
TransferResult moveSample(const std::string& sampleId,
|
|
const std::string& fromBankId,
|
|
const std::string& toBankId);
|
|
|
|
// Copies a sample by id from `fromBankId` to `toBankId`: the source entry is
|
|
// retained, the destination gains it (observing destination collapse-by-hash).
|
|
// Same hash may then live in both banks — cross-bank dedup is NOT enforced.
|
|
TransferResult copySample(const std::string& sampleId,
|
|
const std::string& fromBankId,
|
|
const std::string& toBankId);
|
|
|
|
// -- Query ---------------------------------------------------------------
|
|
|
|
// The bank with `id`, or nullptr. Pointer invalidated by any mutating call.
|
|
Bank* bank(const std::string& id);
|
|
const Bank* bank(const std::string& id) const;
|
|
|
|
// The bank's BankIndex by id, or nullptr. Convenience over bank()->index.
|
|
BankIndex* index(const std::string& id);
|
|
const BankIndex* index(const std::string& id) const;
|
|
|
|
// The pool (always present). Never null.
|
|
Bank& pool();
|
|
const Bank& pool() const;
|
|
|
|
// All banks in ordinal order (pool first). The pool is always banks()[0].
|
|
const std::vector<Bank>& banks() const { return banks_; }
|
|
|
|
std::size_t size() const { return banks_.size(); } // >= 1 (the pool)
|
|
|
|
bool operator==(const BankBook& o) const {
|
|
return banks_ == o.banks_ && activeBankId_ == o.activeBankId_;
|
|
}
|
|
|
|
// -- Persistence ---------------------------------------------------------
|
|
|
|
// Serializes the whole book to a JSON string (lossless round-trip): the pool
|
|
// folded in as bank-zero + named banks + per-bank indices + ordinals + active
|
|
// id. deserialize(serialize(x)) == x.
|
|
std::string serialize() const;
|
|
|
|
// Parses a book JSON produced by serialize(). std::nullopt on malformed input.
|
|
//
|
|
// LEGACY MIGRATION: a bare legacy bank_index JSON (the pre-multi-bank shape, an
|
|
// object with a "samples" array and no "banks" key) is promoted into the pool's
|
|
// index, yielding a book of { pool } with zero named banks — one-way, lossless.
|
|
// After migration the book blob is authoritative (the caller persists the book
|
|
// shape going forward; the legacy key is retired by the B2 shell).
|
|
static std::optional<BankBook> deserialize(const std::string& json);
|
|
|
|
// Resolve a BankBook from the two persisted ext-state values a project may carry:
|
|
// the authoritative `banks` blob and the retired-but-possibly-present legacy
|
|
// `bank_index` blob. The persist shell (B2) hands both raw strings straight here so
|
|
// the load-source decision stays REAPER-free and unit-tested. Precedence:
|
|
// 1. non-empty `banksJson` present -> deserialize it (authoritative). If it is
|
|
// MALFORMED, do NOT silently fall back to the legacy blob — a corrupt `banks`
|
|
// blob is an error, not an absence; return an empty book so a stale legacy key
|
|
// can never resurrect a superseded single-bank state over a broken book.
|
|
// 2. else non-empty `legacyJson` -> deserialize it (one-way pool migration).
|
|
// 3. else (both absent/empty) -> a fresh empty book (pool only).
|
|
// Never returns nullopt: an unloadable input degrades to the empty book (matching
|
|
// the shell's existing "malformed -> ignore, start empty" behaviour), so the caller
|
|
// has one branchless install path.
|
|
static BankBook loadFromPersisted(const std::string& banksJson,
|
|
const std::string& legacyJson);
|
|
|
|
private:
|
|
std::vector<Bank> banks_; // ordinal order; banks_[0] is always the pool
|
|
std::string activeBankId_; // always names a live bank; defaults to pool
|
|
|
|
// True if a bank OTHER than `exceptId` already carries `name`'s uniqueness key
|
|
// (trimmed + case-insensitive, ASCII). Backs the create/rename uniqueness check;
|
|
// pass exceptId=id to let a bank keep (or re-case/-space) its own name.
|
|
bool displayNameTaken(const std::string& name, const std::string& exceptId) const;
|
|
|
|
// Re-sorts banks_ by ordinal (pool pinned first) and rewrites ordinals to a
|
|
// contiguous 0..N-1 so the pool is 0 and named banks are 1..N. Called after any
|
|
// structural change (create / delete / reorder).
|
|
void normalizeOrdinals();
|
|
|
|
// Replaces the book's banks with a parsed set, normalizes ordinals, and resolves
|
|
// the active bank (falling back to the pool if the id names no bank). Used only
|
|
// by deserialize; kept private so the public surface stays create/rename/etc.
|
|
void adoptBanks(std::vector<Bank>&& banks, const std::string& activeBank);
|
|
};
|
|
|
|
// The next bank id to activate when cycling the active bank forward, in ordinal
|
|
// order (the ids arrive pool-first, named 1..N, matching banks()). Wraps: the id
|
|
// after the last returns the first (pool → named → … → pool). This is the pure
|
|
// decision behind the "cycle active bank" action — the shell reads the book's
|
|
// ordered bank ids + current active id, asks for the next, and activates it.
|
|
// * empty list -> "" (nothing to cycle to)
|
|
// * single id (pool-only) -> that id (a one-bank book stays put)
|
|
// * currentBankId not present -> the first id (a sane home to jump to)
|
|
// Exposed as a free function (not a BankBook member) so it is unit-testable against
|
|
// a bare id vector without a full book. Mirror of view_mode_model's nextModeId.
|
|
std::string nextBankId(const std::vector<std::string>& orderedBankIds,
|
|
const std::string& currentBankId);
|
|
|
|
} // namespace reasampler
|