S5 Tier-1: zoned keymap editor + performance-map playback/persistence

Zone editor in the IPlugView LICE surface, zoned resolution built off-thread into the
LoadedInstrument keymap with Tier-0 fallback, performance map in VST3 component state
with v1 back-compat. Pure resolve/build/serialize + geometry with CTest coverage.
This commit is contained in:
2026-07-26 17:28:15 -04:00
parent 6ae843345c
commit 2356958930
10 changed files with 1063 additions and 63 deletions
+101
View File
@@ -85,6 +85,107 @@ std::vector<AudioSample> downmixToMono(const std::vector<AudioSample>& interleav
Keymap buildTier0Keymap(std::vector<AudioSample> monoFrames, int sampleRate,
int rootNote, const SampleLoop& loop);
// --- Performance map (Tier 1, D-B: the instrument's OWN state) ---------------
//
// The performance map is the keymap the user authors IN the instrument: several bank
// samples zoned across the keyboard, each with a key range and a root note. It is a
// PERFORMANCE CHOICE (D-B), so it lives in the instrument (VST3 component state), never
// written back to the bank. Root note per zone is SEEDED from the S2 bank intrinsic but
// OVERRIDABLE here — the override lives on the zone, never on `Sample`.
//
// Pure value type: it names bank samples by id (the stable seam key) and holds no PCM.
// The shell resolves each id's WAV over the file seam and decodes it; the pure zone-build
// stitches the decoded frames + this map into a sampler_core Keymap.
// One authored zone: a bank sample mapped to an inclusive [lowNote, highNote] key range,
// with an optional root-note override. rootOverride absent -> repitch from the bank
// sample's own S2 rootNote intrinsic (or middle C when the bank left it empty).
struct PerformanceZone {
std::string sampleId; // bank sample id this zone plays
int lowNote = 0; // inclusive
int highNote = 127; // inclusive
std::optional<int> rootOverride; // instrument-owned override; absent -> bank intrinsic
};
// The instrument's performance map: an ordered list of zones. Order is authoritative for
// overlap resolution (OVERLAP POLICY: first zone in order wins, mirroring the S3 core's
// first-match Keymap::resolve — overlaps are neither rejected nor clamped, the earlier
// zone simply takes the contested keys; documented, deterministic).
struct PerformanceMap {
std::vector<PerformanceZone> zones;
bool empty() const { return zones.empty(); }
};
// One resolved zone ready for the shell to decode + the pure build to stitch: the bank
// sample's project-relative WAV path (file seam), the EFFECTIVE root note (override beats
// bank intrinsic beats middle-C default), the loop intrinsic, and the key range. Distinct
// from PerformanceZone (which names an id) — this is the id resolved against the live bank.
struct ResolvedZone {
std::string relativePath; // project-relative; the shell resolves + decodes it
int lowNote = 0;
int highNote = 127;
int rootNote = 60; // effective: override, else bank intrinsic, else 60
SampleLoop loop; // bank intrinsic
};
// The result of resolving a performance map against the live bank blob. `zones` are the
// zones whose sampleId still resolves to a bank sample, IN MAP ORDER (so overlap-order is
// preserved). `droppedSampleIds` are the ids that no longer resolve (STALE-ID POLICY: a
// zone naming a deleted/moved-out sample is DROPPED cleanly — not an error, not silence
// for the whole map — and its id is reported here so the editor can flag/prune it).
struct ResolvedPerformance {
std::vector<ResolvedZone> zones;
std::vector<std::string> droppedSampleIds;
};
// Resolve a performance map against the live "banks" ext-state blob. Pure: shared
// bank_book parse, no host, no PCM. Each zone's sampleId is looked up across every bank
// (pool + named); a hit yields a ResolvedZone with the effective root note (rootOverride,
// else the sample's S2 rootNote, else 60) and the sample's loop intrinsic; a miss appends
// the id to droppedSampleIds. Empty/malformed blob or empty map -> empty result (the shell
// then falls back to Tier-0 — see reloadFromBank).
ResolvedPerformance resolvePerformance(const std::string& banksJson,
const PerformanceMap& map);
// Build a zoned Keymap from resolved zones + their decoded mono PCM. `decoded[i]` is the
// downmixed frames + sample rate for `zones[i]` (same length + order as `zones`). One
// SampleData per zone (Tier 1: one sample per key-region; a sample used by two zones is
// decoded twice — acceptable at this tier, the shell may dedup by path later). Zone order
// is preserved so first-match overlap resolution matches the map's authored order. A zone
// whose decoded frames are empty is SKIPPED (an unreadable WAV drops the zone, not the
// map). Empty zones in -> empty Keymap (silence).
struct DecodedZonePcm {
std::vector<AudioSample> monoFrames;
int sampleRate = 44100;
};
Keymap buildZonedKeymap(const std::vector<ResolvedZone>& zones,
const std::vector<DecodedZonePcm>& decoded);
// --- Performance-map instance state (VST3 setState/getState) -----------------
//
// The performance map is the instrument's OWN state (D-B), serialized to the VST3
// component-state IBStream — NOT written to the "reasampler" bank ext-state (the
// instrument is a read-only bank consumer; S4 precedent). Versioned binary, tolerant of
// truncation/wrong-version by design (bounded reads, never throws across the host).
//
// Format (v2): 4-byte LE version tag (== 2), then a 4-byte LE zone count, then per zone:
// 4-byte LE id length, id bytes, 4-byte LE lowNote, 4-byte LE highNote,
// 1 byte hasOverride (0/1), 4-byte LE rootOverride (present only when hasOverride==1).
// BACK-COMPAT: a v1 blob (the S4 single-selection format: version tag 1 + id bytes) is
// lifted to a single full-keyboard zone playing that id (no override) — so an instance
// saved under Tier 0 restores as a one-zone Tier-1 map. A truncated/unknown/empty blob
// deserializes to an EMPTY map (the instrument falls back to Tier-0 first-sample).
inline constexpr std::uint32_t kPerformanceStateVersion = 2;
// The performance map serialized to bytes for IBStream (getState).
std::vector<std::uint8_t> serializePerformance(const PerformanceMap& map);
// The performance map parsed back from IBStream bytes (setState). A v2 blob parses
// directly; a v1 blob lifts to a single full-keyboard zone; anything else -> empty map.
PerformanceMap deserializePerformance(const std::vector<std::uint8_t>& bytes);
// --- Instance state (VST3 setState/getState) --------------------------------
//
// The instrument's OWN state is which bank sample it plays (D-B: the selection is a