#pragma once // bank_model — the HEART of ReaSampler: the per-project sample bank. `Sample` // metadata struct + `BankModel` (add/remove/query/tier moves/dedup-by-hash + JSON // round-trip to/from std::string). #include #include #include #include namespace reasampler::model { // How the source audio was obtained; the capture backends 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 // stored so capture doesn't re-derive one from the other and risk rounding. 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 an FX-chain snapshot (a thin drift fingerprint, NOT a restorable // chunk) — re-capture-from-source uses it to detect chain drift and replay the // original capture request. 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; }; // Sample-accurate sustain-loop bounds, as frame indices into the captured file — a // bank intrinsic (like sampleRate or length) the MIDI-playback instrument uses to // hold notes past the recorded length. One optional struct (not two loose // optionals) so "both points or neither" is structural, not a rule to re-check at // every boundary. Frame indices, not seconds — the instrument relates them to time // via the file's sample rate. Invariant (enforced at deserialize): 0 <= start <= // end; start == end is a valid zero-length loop marker. struct LoopPoints { std::int64_t start = 0; std::int64_t end = 0; bool operator==(const LoopPoints& 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 // BankModel::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 // Time signature at capture time, stamped alongside captureTempo so the // bars.beats.subdivisions read-out is stable under later project meter changes. // 0/0 means UNSTAMPED (pre-existing sample, or a capture that could not read the // meter); the metadata formatter renders a blank musical read-out then, keeping s.ms. int captureTimeSigNum = 0; // meter numerator (e.g. 4 in 4/4); 0 = unstamped int captureTimeSigDenom = 0; // meter denominator (e.g. 4 in 4/4); 0 = unstamped std::optional key; // musical key, when known // Bank intrinsics for the MIDI-playback instrument, additive like `provenance`. // Both default cleanly empty: pre-existing samples deserialize without them and // re-serialize without inventing values. // - rootNote: MIDI note (0..127) the sample was recorded at, so the instrument // can repitch it across the keyboard. DISTINCT from the musical `key` above: // `key` is a human label ("F#m"); `rootNote` is the exact pitch for repitch. // Populated only where derivable — never guessed when the source isn't a // single played note. // - loop: sustain-loop bounds, populated only where explicitly set. std::optional rootNote; std::optional loop; Levels levels; bool clipped = false; Tier tier = Tier::Scratch; std::string contentHash; // dedup key (see BankModel) 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 BankModel::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 panel can iterate in stable order. class BankModel { 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); // Replaces the sample carrying `id` IN PLACE (preserving its position in // insertion order) with `updated`. Used by re-capture-from-source: a // provenanced sample's file is regenerated and its metadata refreshed while // its identity (id) and slot are kept, so the panel shows the same tile // updated rather than a reordered new entry. `updated.id` should equal `id`; // a differing id is written through as given. Does NOT dedup — an in-place // refresh is not a new insert, so collapse-by-hash (which guards inserts) // does not apply. Returns false (no mutation) if `id` is absent or // `updated.relativePath` is absolute (relative-paths-only still holds here). bool updateInPlace(const std::string& id, const Sample& updated); // 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 BankModel& 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::model