Q-W1 pt2: core/shell/app relocation + sub-namespaces; one concrete ui::Rect (LTRB fork retired); slot_map split from bank_book; BankIndex→BankModel; 59/59 green
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
#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 `BankModel` (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 <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace reasampler::model {
|
||||
|
||||
// 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 (a thin drift fingerprint, NOT a
|
||||
// restorable chunk) captured at resample time; the re-capture-from-source action
|
||||
// (M10) 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
|
||||
// (Phase S seam field, D-B). A bank intrinsic — a fact about the file, like
|
||||
// sampleRate or length — consumed by the future MIDI-playback instrument to hold
|
||||
// notes past the recorded length. Modeled as one optional struct (not two loose
|
||||
// optionals) so "both points or neither" is a structural invariant, not a rule to
|
||||
// re-check at every boundary. Frame indices, not seconds, because the loop is a
|
||||
// per-sample-frame contract; the instrument reads the file's sample rate to relate
|
||||
// them to time. Invariant (enforced at the deserialize boundary): 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<std::string> 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 (L7 F1 — stamped alongside captureTempo so the
|
||||
// bars.beats.subdivisions read-out is stable under later project meter changes).
|
||||
// 0/0 means UNSTAMPED (pre-L7 sample, or a capture that could not read the meter);
|
||||
// the metadata formatter renders a blank musical read-out for 0/0 and keeps 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<std::string> key; // musical key, when known
|
||||
|
||||
// Phase S seam fields (D-B) — bank intrinsics for the MIDI-playback instrument,
|
||||
// additive like `provenance` (M1). Both default cleanly empty: pre-Phase-S
|
||||
// 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 at/after capture only where derivable — left empty (never guessed)
|
||||
// when the source is not a single played note.
|
||||
// - loop: sustain-loop bounds, populated only where explicitly set.
|
||||
std::optional<int> rootNote;
|
||||
std::optional<LoopPoints> loop;
|
||||
|
||||
Levels levels;
|
||||
bool clipped = false;
|
||||
|
||||
Tier tier = Tier::Scratch;
|
||||
|
||||
std::string contentHash; // dedup key (see BankModel)
|
||||
|
||||
std::optional<Provenance> 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 future panel (M5) 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 M10 re-capture-from-source: a
|
||||
// provenanced sample's file is regenerated and its metadata (relativePath,
|
||||
// contentHash, levels, timestamp, ...) refreshed while its identity (id) and
|
||||
// slot are kept, so the bank panel shows the same tile updated rather than a
|
||||
// reordered new entry. `updated.id` should equal `id` (the caller keeps the id
|
||||
// stable); a differing id is written through as given (the caller's contract).
|
||||
// Does NOT dedup — an in-place refresh of one entry is not a new insert, so the
|
||||
// collapse-by-hash rule (which guards NEW inserts) does not apply. Returns false
|
||||
// (no mutation) if `id` is absent or `updated.relativePath` is absolute
|
||||
// (the relative-paths-only invariant still holds for the replacement).
|
||||
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<Sample> byTier(Tier tier) const;
|
||||
|
||||
// All samples in insertion order.
|
||||
const std::vector<Sample>& 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<BankModel> deserialize(const std::string& json);
|
||||
|
||||
private:
|
||||
std::vector<Sample> samples_; // insertion order preserved
|
||||
};
|
||||
|
||||
} // namespace reasampler::model
|
||||
Reference in New Issue
Block a user