feat(bank_model): add Phase S seam fields (rootNote + loop points) to Sample

Additive optional MIDI root note and sustain-loop points with JSON round-trip and deserialize-boundary validation; capture leaves them empty (not derivable). Mirrors the provenance addition; BankIndex behavior unchanged.
This commit is contained in:
2026-07-26 15:29:57 -04:00
parent 5595ba42f9
commit b5a573aebb
5 changed files with 226 additions and 1 deletions
+28
View File
@@ -63,6 +63,22 @@ struct Levels {
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
// BankIndex::add boundary — see AddResult).
@@ -95,6 +111,18 @@ struct Sample {
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;