2356958930
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.
212 lines
12 KiB
C++
212 lines
12 KiB
C++
#pragma once
|
|
// sample_map — PURE mapping logic for the S4 Tier-0 instrument: turn the live
|
|
// "reasampler" bank ext-state + a decoded WAV into the plain data the sampler core
|
|
// plays, and (de)serialize the instance's selected-sample choice for VST3 component
|
|
// state. NO VST3, NO REAPER, NO SWELL, NO vendor/ includes at the boundary — the
|
|
// mirror of capture_paths / wav_trim / bridge_marshal splitting the fiddly, testable
|
|
// arithmetic out of a host-facing shell.
|
|
//
|
|
// WHY IT EXISTS (S4 seams). The instrument reads the bank over the live-state seam
|
|
// (the "banks" ext-state blob) and the audio over the file seam (the on-disk WAV).
|
|
// Both of those raw inputs cross the bridge/file boundary in the shell; everything
|
|
// after — parse the bank with the SHARED bank_model/bank_book JSON path (NOT a second
|
|
// parser; the S1 spike's string-scan reader is retired), pick the selected sample,
|
|
// downmix its decoded PCM to the core's mono contract, and build the Tier-0 chromatic
|
|
// Keymap — is pure and unit-tested here.
|
|
//
|
|
// It links bank_book (the shared BankBook::deserialize) and wav_trim (the shared
|
|
// 32-bit-float WAV parse — no third WAV reader) and sampler_core (the Keymap /
|
|
// SampleData it produces). All three are pure; this stays pure.
|
|
|
|
#include <cstdint>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "bank_book.h" // BankBook::deserialize (shared bank JSON parse)
|
|
#include "sampler_core.h" // Keymap, SampleData, SampleLoop
|
|
#include "wav_trim.h" // parseWavLayout, extractFloatFrames (shared WAV parse)
|
|
|
|
namespace reasampler {
|
|
|
|
// The bank sample this instance is bound to, distilled from the live "banks" blob:
|
|
// the project-relative WAV path the file seam must resolve+decode, plus the S2 bank
|
|
// intrinsics the core repitches / loops by. A pure value — no host, no PCM yet.
|
|
struct SelectedSample {
|
|
std::string relativePath; // project-relative; the shell resolves it (M4 convention)
|
|
int rootNote = 60; // S2 intrinsic; defaults to middle C when the bank left it empty
|
|
SampleLoop loop; // S2 intrinsic; hasLoop=false when the bank left it empty
|
|
};
|
|
|
|
// Resolve the bound sample from the live bank blob. `banksJson` is the raw "banks"
|
|
// ext-state value the bridge read (may be empty / malformed — an unsaved or pre-bank
|
|
// project). `sampleId` is this instance's stored selection.
|
|
//
|
|
// Precedence, all pure:
|
|
// * empty / malformed banksJson -> nullopt (nothing to play)
|
|
// * sampleId names a sample in ANY bank -> that sample (searched pool + named)
|
|
// * sampleId empty or not found, bank has -> the FIRST sample in ordinal order
|
|
// >= 1 sample (a sensible default so a fresh
|
|
// instance plays SOMETHING; the UI can
|
|
// then pick a specific one)
|
|
// * bank has zero samples -> nullopt
|
|
//
|
|
// The "first sample" fallback is deliberate: Tier 0 is "the bank plays", and a brand-
|
|
// new instance with no stored selection should map the bank's first sample rather than
|
|
// stay silent until the user opens the editor.
|
|
std::optional<SelectedSample> selectSample(const std::string& banksJson,
|
|
const std::string& sampleId);
|
|
|
|
// All (id, displayName) pairs across every bank in ordinal order (pool first), for the
|
|
// selection UI to list. Empty for an empty / malformed blob. Pure projection over the
|
|
// shared parse — the UI never parses JSON itself.
|
|
struct SampleChoice {
|
|
std::string id;
|
|
std::string displayName;
|
|
};
|
|
std::vector<SampleChoice> listSamples(const std::string& banksJson);
|
|
|
|
// Downmix interleaved float frames (the shape wav_trim::extractFloatFrames yields:
|
|
// [f0c0,f0c1,...,f1c0,...]) to the core's MONO contract by AVERAGING channels per
|
|
// frame. `channelCount` is the interleave stride (>= 1). CHANNEL POLICY (Tier 0,
|
|
// documented + surfaced): the S3 core is mono-per-sample by design; bank WAVs preserve
|
|
// their source channel count, so a stereo (or N-channel) capture is folded to a single
|
|
// mono stream here by an equal-weight average. Averaging (not "take L", not summing) is
|
|
// the least-surprising, no-clip default — a centered mono source stays unity, and a
|
|
// hard-panned source is attenuated rather than silenced or doubled. Empty / zero-stride
|
|
// in -> empty out. Pure.
|
|
std::vector<AudioSample> downmixToMono(const std::vector<AudioSample>& interleaved,
|
|
int channelCount);
|
|
|
|
// Build the Tier-0 chromatic keymap for one decoded, mono sample: one zone spanning
|
|
// the whole keyboard, repitched from `rootNote`, looped per `loop`. This is the
|
|
// single-sample degenerate case (Keymap::singleSampleChromatic) with the S2 intrinsics
|
|
// threaded in. `monoFrames` is the downmixed PCM; `sampleRate` is the WAV's rate.
|
|
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
|
|
// performance choice, held by the instrument, never written back to the bank). It is a
|
|
// single string id. serialize/deserialize keep the on-the-wire form explicit and
|
|
// versioned so a future Tier can extend it without breaking already-saved instances.
|
|
//
|
|
// Format (v1): a 4-byte little-endian version tag (== 1) followed by the id bytes. No
|
|
// length prefix is needed — the id runs to the end of the stream (the host tells us the
|
|
// byte count). deserializeSelection tolerates a truncated / wrong-version / empty blob
|
|
// by returning "" (no selection — the instrument falls back to the bank's first sample),
|
|
// never throwing across the host boundary.
|
|
|
|
inline constexpr std::uint32_t kSelectionStateVersion = 1;
|
|
|
|
// The selected-sample id serialized to bytes for IBStream (getState).
|
|
std::vector<std::uint8_t> serializeSelection(const std::string& sampleId);
|
|
|
|
// The selected-sample id parsed back from IBStream bytes (setState). Unknown version,
|
|
// too-short, or empty -> "" (graceful no-selection).
|
|
std::string deserializeSelection(const std::vector<std::uint8_t>& bytes);
|
|
|
|
} // namespace reasampler
|