#pragma once // bank_model — the HEART of ReaSampler, deliberately free of any REAPER type so // it compiles and unit-tests OUTSIDE the DAW. It owns the per-project sample // bank: the `Sample` metadata struct and the `BankIndex` (add / remove / query / // tier moves / dedup-by-hash + JSON round-trip to/from std::string). // // PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO // vendor/ includes. Standard library only. #include #include #include #include namespace reasampler { // How the source audio was obtained. Kept in the pure core (no REAPER coupling); // the capture backends (M3/M8) map their own notion onto these. enum class SourceMode { MasterMix, // offline render of the master output SelectedTracks, // offline render of selected tracks SelectedItems, // offline render of selected media items TimeSelection, // offline render bounded by the time selection RazorArea, // offline render of a razor edit area Realtime, // realtime record of wet output }; // Retention tier. `Scratch` is auto-prunable working material; `Archive` is kept. enum class Tier { Scratch, Archive, }; // Sample-accurate source bounds, in both project seconds and PPQ (ticks). Both // are stored because capture needs seconds and musical placement needs PPQ; we // refuse to re-derive one from the other and risk rounding (precision invariant). struct SourceRange { double startSeconds = 0.0; double endSeconds = 0.0; double startPpq = 0.0; double endPpq = 0.0; bool operator==(const SourceRange& o) const; }; // Present only when a sample was resampled FROM another sample. Carries the // parent's id and the FX-chain snapshot string captured at resample time, so the // null-test / re-capture-from-source action (M10) can reconstruct the chain. struct Provenance { std::string parentSampleId; std::string fxChainSnapshot; bool operator==(const Provenance& o) const; }; // Loudness / level metrics measured from the captured file. struct Levels { double peakDb = 0.0; double rmsDb = 0.0; double lufs = 0.0; bool operator==(const Levels& o) const; }; // The metadata record for one captured sample. The audio itself lives in a // project-relative file; `relativePath` is ALWAYS relative (enforced at the // BankIndex::add boundary — see AddResult). struct Sample { std::string id; // stable unique id (assigned by the caller) std::string displayName; std::string relativePath; // project-relative; never absolute (invariant) SourceMode sourceMode = SourceMode::MasterMix; SourceRange sourceRange; // Track GUID(s) the capture came from, when applicable (empty otherwise). std::vector trackGuids; double wetDry = 1.0; // 1.0 = fully wet, 0.0 = fully dry int channelCount = 0; int sampleRate = 0; double lengthSeconds = 0.0; double lengthBeats = 0.0; double captureTempo = 0.0; // project tempo (BPM) at capture time std::optional key; // musical key, when known Levels levels; bool clipped = false; Tier tier = Tier::Scratch; std::string contentHash; // dedup key (see BankIndex) std::optional provenance; // set only when resampled std::int64_t createdTimestamp = 0; // unix epoch seconds bool operator==(const Sample& o) const; bool operator!=(const Sample& o) const { return !(*this == o); } // A scratch-tier sample is auto-prunable; archive is kept. bool isAutoPrunable() const { return tier == Tier::Scratch; } }; // Outcome of BankIndex::add. `add` rejects rather than silently mutating: // - RejectedAbsolutePath: relativePath was absolute (precision invariant). // - RejectedEmptyId: id was empty (the collection is keyed by id). // - Collapsed: content hash matched an existing entry; the existing // entry is kept and the add is a no-op (dedup). // - Added: inserted as a new entry. enum class AddResult { Added, Collapsed, RejectedAbsolutePath, RejectedEmptyId, }; // An ordered, id-keyed collection of Samples with content-hash dedup, tier // moves/filtering, and lossless JSON round-trip. Insertion order is preserved // so a future panel (M5) can iterate in stable order. class BankIndex { public: // Adds a sample. Enforces the relative-paths-only invariant and dedups by // content hash (an equal-hash add collapses onto the existing entry rather // than duplicating). See AddResult for the full outcome set. AddResult add(const Sample& sample); // Removes the sample with `id`. Returns true if one was removed. bool remove(const std::string& id); // Returns the sample with `id`, or nullptr if absent. The pointer is // invalidated by any mutating call. const Sample* query(const std::string& id) const; // Returns the sample whose contentHash matches, or nullptr. Empty hashes are // never matched (they do not participate in dedup). const Sample* findByHash(const std::string& contentHash) const; // Moves the sample with `id` to `tier`. Returns true if the sample existed. bool moveTier(const std::string& id, Tier tier); // Returns copies of all samples in the given tier, in insertion order. std::vector byTier(Tier tier) const; // All samples in insertion order. const std::vector& all() const { return samples_; } std::size_t size() const { return samples_.size(); } bool empty() const { return samples_.empty(); } bool operator==(const BankIndex& o) const { return samples_ == o.samples_; } // Serializes the whole index to a JSON string (lossless round-trip). std::string serialize() const; // Parses a JSON string produced by serialize(). Returns std::nullopt on // malformed / truncated input (error signaled, never UB). On success the // returned index satisfies deserialize(serialize(x)) == x. static std::optional deserialize(const std::string& json); private: std::vector samples_; // insertion order preserved }; } // namespace reasampler