Files
reasampler/src/core/model/bank_model.h
T

209 lines
8.5 KiB
C++

#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 <cstdint>
#include <optional>
#include <string>
#include <vector>
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<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, 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<std::string> 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<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 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<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