Cut core/instrument/map comment bloat ~30% (comments only, zero code change)
This commit is contained in:
@@ -1,22 +1,10 @@
|
||||
#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_codec / 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_codec (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.
|
||||
// sample_map — turns the live "reasampler" bank ext-state + a decoded WAV into the plain
|
||||
// data the sampler core plays, and (de)serializes the instance's zone/selection state.
|
||||
// The bank is read over the live-state seam, audio over the file seam; both raw inputs
|
||||
// cross the bridge/file boundary in the shell, everything after (bank parse via the shared
|
||||
// bank_book JSON path, sample pick, mono downmix, keymap build) is pure and unit-tested
|
||||
// here. Links bank_book, wav_codec, and sampler_core (all pure).
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
@@ -29,77 +17,56 @@
|
||||
|
||||
namespace reasampler::instrument::map {
|
||||
|
||||
// Cross-subsystem deps by their real namespace homes (Q-W2v: sample_map now lives in
|
||||
// instrument::map; the engine family stays in flat `reasampler` until its own wave).
|
||||
using audio::AudioSample;
|
||||
using instrument::engine::VelocityCurve;
|
||||
using instrument::engine::VelocityPoint;
|
||||
|
||||
// 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.
|
||||
// The bank sample this instance is bound to, distilled from the live "banks" blob: the
|
||||
// project-relative WAV path the file seam resolves+decodes, plus the 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
|
||||
int channelCount = 0; // bank intrinsic (capture channel count); 0 = unknown (older
|
||||
// bank entries) — the GA channel-mode auto-default skips it
|
||||
std::string relativePath; // project-relative; the shell resolves it
|
||||
int rootNote = 60; // defaults to middle C when the bank left it empty
|
||||
SampleLoop loop; // hasLoop=false when the bank left it empty
|
||||
int channelCount = 0; // capture channel count; 0 = unknown (older bank entries) —
|
||||
// the GA channel-mode auto-default skips it
|
||||
};
|
||||
|
||||
// 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.
|
||||
// `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 empty -> nullopt (NO selection -> silence)
|
||||
// * sampleId names a sample in ANY bank -> that sample (searched pool + named)
|
||||
// * sampleId set but not found (stale) -> nullopt (the sample was deleted/moved;
|
||||
// the editor returns to the empty state)
|
||||
//
|
||||
// POLICY REVERSAL (S10, 2026-07-26 — supersedes the S4 first-sample fallback). A fresh
|
||||
// instance with no stored selection resolves to nullopt (SILENCE), NOT the bank's first
|
||||
// sample: the metric is time-to-first-note via an explicit pick, and a mystery auto-play
|
||||
// of sample #1 was the anti-pattern. A stale stored id (no longer resolves) ALSO returns
|
||||
// nullopt rather than silently substituting a different sample — the editor reflects the
|
||||
// missing selection with its "pick a capture" empty state instead of masking it.
|
||||
// Precedence: empty/malformed banksJson -> nullopt. Empty sampleId -> nullopt (no selection
|
||||
// is SILENCE, not the bank's first sample — deliberate: the metric is time-to-first-note via
|
||||
// an explicit pick, and mystery auto-play of sample #1 was the anti-pattern). sampleId found
|
||||
// in any bank -> that sample. sampleId set but not found (stale) -> nullopt, same as no
|
||||
// selection — the editor shows its "pick a capture" empty state rather than masking it.
|
||||
std::optional<SelectedSample> selectSample(const std::string& banksJson,
|
||||
const std::string& sampleId);
|
||||
|
||||
// GA auto-default rule (pure, tested): given the capture's requested channel count, the
|
||||
// instance's current mode, and whether the user has explicitly toggled the mode, return
|
||||
// the mode to apply. Explicit choice is never overridden. An unknown channelCount (0)
|
||||
// leaves the current mode unchanged. Used by reloadInstrument in the single-capture path.
|
||||
// * isExplicit == true -> current (user's choice stands)
|
||||
// * channelCount == 0 -> current (unknown, skip)
|
||||
// * channelCount >= 2 -> Stereo
|
||||
// * channelCount == 1 -> Mono
|
||||
// Auto-default rule: given the capture's channel count, current mode, and whether the user
|
||||
// explicitly toggled it, return the mode to apply. Explicit choice is never overridden;
|
||||
// channelCount == 0 (unknown) leaves the current mode; >= 2 -> Stereo; == 1 -> Mono.
|
||||
ChannelMode channelModeFor(int channelCount, ChannelMode current, bool isExplicit);
|
||||
|
||||
// --- Instance-owned sample references (pS self-contained playback) -------------
|
||||
// --- Instance-owned sample references (self-contained playback) -------------
|
||||
//
|
||||
// THE ARCHITECTURE CORRECTION: the instrument must never go silent because the extension's
|
||||
// ext-state has not parsed yet (or the extension is absent). So the instance persists, in
|
||||
// its OWN component state, a small table of everything it needs to PLAY each referenced
|
||||
// bank sample: the project-relative WAV path + the decode intrinsics (root note, loop,
|
||||
// channel count) — exactly a SelectedSample, keyed by the bank sample id. On load the
|
||||
// shell decodes straight from these refs; the bank blob is a BROWSER SOURCE that also
|
||||
// refreshes this table opportunistically when readable (recapture/root edits stay live),
|
||||
// never a runtime lifeline.
|
||||
// The instrument must never go silent just because the extension's ext-state hasn't parsed
|
||||
// yet (or the extension is absent). So the instance persists, in its OWN component state, a
|
||||
// table of everything needed to PLAY each referenced bank sample: path + decode intrinsics
|
||||
// (root, loop, channel count), keyed by bank sample id. The shell decodes straight from
|
||||
// these refs; the bank blob is a browser source that refreshes the table opportunistically
|
||||
// when readable, never a runtime lifeline.
|
||||
//
|
||||
// POLICY (follows from ownership): a sample deleted from the BANK no longer silences an
|
||||
// instance that carries its ref — the instance keeps playing while the FILE exists (normal
|
||||
// sampler behavior; prune deleting the file yields the defined no-play). This deliberately
|
||||
// supersedes the S10 stale-id-silence rule, which was an artifact of bank-side resolution.
|
||||
struct PerformanceMap; // defined below (Tier 1); referencedSampleIds spans both tiers
|
||||
// Consequence: a sample deleted from the bank no longer silences an instance that carries
|
||||
// its ref — it keeps playing while the file exists (normal sampler behavior; prune deleting
|
||||
// the file yields the defined no-play).
|
||||
struct PerformanceMap; // defined below; referencedSampleIds spans both selection + zones
|
||||
|
||||
struct SampleRefEntry {
|
||||
std::string sampleId; // the bank sample id this ref was copied from (the seam key)
|
||||
SelectedSample ref; // path + intrinsics, sufficient to decode + play without a bank
|
||||
// The sample's bank display name at copy time — DISPLAY ONLY (the editor's label falls
|
||||
// back to it when the bank snapshot is unavailable, mirroring the waveform/loop ref
|
||||
// fallback); never consulted by resolution. Empty for a table written before the field
|
||||
// existed in-session (it back-fills on the next bank refresh).
|
||||
// Bank display name at copy time — DISPLAY ONLY (editor label fallback when the bank
|
||||
// snapshot is unavailable); never consulted by resolution.
|
||||
std::string displayName;
|
||||
};
|
||||
using SampleRefs = std::vector<SampleRefEntry>;
|
||||
@@ -112,43 +79,32 @@ const SelectedSample* findRef(const SampleRefs& refs, const std::string& sampleI
|
||||
std::vector<std::string> referencedSampleIds(const std::string& selectionId,
|
||||
const PerformanceMap& map);
|
||||
|
||||
// Upsert a ref for each id in `ids` that resolves in the live bank blob (the same
|
||||
// distillation selectSample performs), copying the bank display name alongside the decode
|
||||
// intrinsics. A miss leaves any existing entry untouched — the instance owns its copy; a
|
||||
// bank deletion never strips a ref. Empty/malformed blob -> no-op.
|
||||
// Upsert a ref for each id in `ids` that resolves in the live bank blob, copying the display
|
||||
// name alongside the decode intrinsics. A miss leaves any existing entry untouched — the
|
||||
// instance owns its copy; a bank deletion never strips a ref. Empty/malformed blob -> no-op.
|
||||
void refreshRefsFromBank(SampleRefs& refs, const std::string& banksJson,
|
||||
const std::vector<std::string>& ids);
|
||||
|
||||
// The pre-v10 LEGACY-LIFT terminating decision (pure, so the no-churn rule is provable
|
||||
// without a host): can a refs lift MAKE PROGRESS against this bank blob for the ids the
|
||||
// instance references?
|
||||
// * Retry — the blob is absent/empty/unparseable: not readable YET, keep retrying (the
|
||||
// project's ext-state may simply not have parsed).
|
||||
// * Lift — the blob parses and at least one id resolves: a lift copies a ref in (the
|
||||
// refs table then goes non-empty and the lift never re-fires).
|
||||
// * Stale — the blob parses and NO id resolves (an empty `ids` included): the ids are
|
||||
// PROVABLY stale — the bank is readable and does not know them — so there is nothing
|
||||
// to lift, ever. The shell latches this and stops retrying (no per-tick churn).
|
||||
// Legacy-lift terminating decision: can a refs lift make progress against this bank blob
|
||||
// for the ids the instance references?
|
||||
// * Retry — blob absent/empty/unparseable: not readable yet, keep retrying.
|
||||
// * Lift — blob parses and at least one id resolves: copy a ref in (never re-fires once
|
||||
// the refs table is non-empty).
|
||||
// * Stale — blob parses and no id resolves: provably stale, nothing to lift, ever — the
|
||||
// shell latches this and stops retrying (no per-tick churn).
|
||||
enum class LegacyLiftDecision { Retry, Lift, Stale };
|
||||
LegacyLiftDecision legacyLiftDecision(const std::optional<std::string>& banksJson,
|
||||
const std::vector<std::string>& ids);
|
||||
|
||||
// Keep only the entries whose id is in `ids` (getState hygiene: the persisted table tracks
|
||||
// exactly what the instance currently plays, so it cannot grow with browsing history).
|
||||
// Keep only the entries whose id is in `ids` (getState hygiene: the persisted table cannot
|
||||
// grow with browsing history).
|
||||
void retainRefs(SampleRefs& refs, const std::vector<std::string>& ids);
|
||||
|
||||
// One entry in the capture browser's card list: the stable id + display name plus the S2
|
||||
// intrinsics + bank the browser draws as a card (peak thumbnail + name + root/key badge,
|
||||
// filterable by bank). Peaks are NOT here — they are computed shell-side from the decoded
|
||||
// PCM (the `Sample` metadata carries no envelope; see reasampler_editor's thumbnail cache,
|
||||
// the mirror of bank_panel::thumbnailFor). This carries only what the bank blob already
|
||||
// holds: the metadata the card badge + bank filter need. Pure projection over the shared
|
||||
// parse — the UI never parses JSON itself.
|
||||
//
|
||||
// - rootNote: the S2 rootNote intrinsic when the bank set it (nullopt otherwise — the
|
||||
// badge shows "root: —" / no root, never a guessed value).
|
||||
// - key: the optional human musical key label ("F#m"), when the bank set it.
|
||||
// - bankId: the id of the bank this sample lives in (the bank filter matches on it).
|
||||
// One entry in the capture browser's card list: stable id + display name + intrinsics +
|
||||
// bank, for a card (peak thumbnail + name + root/key badge, filterable by bank). Peaks are
|
||||
// NOT here — computed shell-side from the decoded PCM (reasampler_editor's thumbnail
|
||||
// cache). rootNote is nullopt when the bank left it empty (badge shows no root, never a
|
||||
// guessed value). Pure projection over the shared parse — the UI never parses JSON itself.
|
||||
struct SampleChoice {
|
||||
std::string id;
|
||||
std::string displayName;
|
||||
@@ -167,35 +123,27 @@ struct BankChoice {
|
||||
};
|
||||
std::vector<BankChoice> listBanks(const std::string& banksJson);
|
||||
|
||||
// Downmix interleaved float frames (the shape wav_codec's 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.
|
||||
// Downmix interleaved float frames ([f0c0,f0c1,...,f1c0,...]) to the core's MONO contract
|
||||
// by AVERAGING channels per frame (`channelCount` is the interleave stride, >= 1) — not
|
||||
// "take L", not summing: a centered mono source stays unity, 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);
|
||||
|
||||
// Deinterleave one channel (`which`, 0-based) out of interleaved frames. `channelCount` is
|
||||
// the interleave stride (>= 1); `which` is clamped to a valid channel (a request past the
|
||||
// source's last channel reads the last channel, so a mono source asked for channel 1 yields
|
||||
// channel 0 again — the dual-mono building block). Empty / zero-stride in -> empty out. Pure.
|
||||
// Deinterleave one channel (`which`, 0-based). `which` clamps to a valid channel (a request
|
||||
// past the last channel reads the last channel, so a mono source asked for channel 1 yields
|
||||
// channel 0 — the dual-mono building block). Empty/zero-stride in -> empty out. Pure.
|
||||
std::vector<AudioSample> extractChannel(const std::vector<AudioSample>& interleaved,
|
||||
int channelCount, int which);
|
||||
|
||||
// --- Stored (wall-clock SECONDS) per-zone play params -------------------------
|
||||
//
|
||||
// DOMAIN SPLIT (S12 remediation — Daniel's ruling: no hardcoded sample rate in the program).
|
||||
// The instrument stores and edits WALL-CLOCK performance times as SECONDS, rate-free; the
|
||||
// engine (sampler_core's ZonePlayParams, on SampleData) receives FRAMES resolved from the
|
||||
// LIVE sample rate at keymap build. AHDSR (A/H/D/S/R) and the AD pitch envelope (attack/decay)
|
||||
// are wall-clock — the voice advances them once per OUTPUT frame — so they live here in seconds.
|
||||
// Quantities anchored to the source file's timeline (start point, loop points, Trigger %-length
|
||||
// and its fades — the fades anchor to the source-frame read offset, PLAN.md §S15) stay in source
|
||||
// frames / fractions and are carried through unchanged (TriggerParams is reused verbatim).
|
||||
// Daniel's standing ruling: no hardcoded sample rate anywhere in the program. The
|
||||
// instrument stores/edits wall-clock performance times (AHDSR A/H/D/R, pitch-env A/D) as
|
||||
// SECONDS, rate-free; the engine receives FRAMES resolved from the LIVE sample rate at
|
||||
// keymap build. Quantities anchored to the source file's timeline (start point, loop
|
||||
// points, Trigger %-length + fades) stay in source frames/fractions, carried through
|
||||
// unchanged (TriggerParams reused verbatim).
|
||||
//
|
||||
// The stored AHDSR times (seconds). sustainLevel is dimensionless (0..1), not a time.
|
||||
struct AdsrSeconds {
|
||||
@@ -214,61 +162,51 @@ struct PitchEnvSeconds {
|
||||
double peakSemitones = 0.0; // signed depth at the peak
|
||||
};
|
||||
|
||||
// The stored per-zone play bundle: wall-clock times in SECONDS, source-timeline quantities in
|
||||
// frames/fractions (TriggerParams). This is the instrument-owned (D-B), serialized, editor-facing
|
||||
// representation — distinct from sampler_core's engine-facing ZonePlayParams (frames). The keymap
|
||||
// builders resolve this to a frame-domain ZonePlayParams against the live sample rate.
|
||||
// The stored per-zone play bundle: wall-clock times in SECONDS, source-timeline quantities
|
||||
// in frames/fractions (TriggerParams). Instrument-owned, serialized, editor-facing —
|
||||
// distinct from sampler_core's engine-facing ZonePlayParams (frames).
|
||||
struct ZonePlaySeconds {
|
||||
PlayMode playMode = PlayMode::Gate;
|
||||
AdsrSeconds adsr; // Gate: AHDSR (seconds)
|
||||
TriggerParams trigger; // Trigger: %-length + fades (source frames)
|
||||
PitchEngine pitchEngine = kDefaultPitchEngine; // product default: Preserve (S16-F1)
|
||||
PitchEngine pitchEngine = kDefaultPitchEngine; // product default: Preserve
|
||||
PitchEnvSeconds pitchEnv; // AD pitch modulation (seconds), off by default
|
||||
};
|
||||
|
||||
// Resolve a stored seconds bundle to the engine's frame-domain ZonePlayParams against a live
|
||||
// sample rate (frames = round(seconds * rate)). Source-timeline fields (trigger, engine, mode,
|
||||
// peak, enabled) carry through unchanged. `sampleRate` must be > 0 (the caller guards this).
|
||||
// sample rate (frames = round(seconds * rate)). Source-timeline fields carry through
|
||||
// unchanged. `sampleRate` must be > 0 (the caller guards this).
|
||||
ZonePlayParams resolvePlay(const ZonePlaySeconds& stored, int sampleRate);
|
||||
|
||||
// Build the Tier-0 chromatic keymap for one decoded sample: one zone spanning the whole
|
||||
// keyboard, repitched from `rootNote`, looped per `loop`. The single-sample degenerate case
|
||||
// (Keymap::singleSampleChromatic) with the S2 intrinsics threaded in. `frames` is channel 0
|
||||
// (mono, or L); `framesR` is channel 1 (R) — pass EMPTY for a mono sample (the default),
|
||||
// which yields a mono SampleData byte-identical to the pre-S7 build. A `framesR` whose length
|
||||
// mismatches `frames` is dropped (SampleData::channelCount() falls back to mono), so a bad
|
||||
// pair never half-plays. `sampleRate` is the WAV's rate.
|
||||
// `play` carries the S15/S16 per-zone play params (SECONDS) for the single-capture path; it
|
||||
// defaults to the PRODUCT defaults (Gate + tier-0 AHDSR seconds + Preserve engine, S16-F1) so a
|
||||
// picked single capture plays under the same default engine as a zone would. This function
|
||||
// resolves the wall-clock seconds to frames against `sampleRate` before stamping the SampleData.
|
||||
// keyboard, repitched from `rootNote`, looped per `loop` (Keymap::singleSampleChromatic).
|
||||
// `frames` is channel 0 (mono, or L); `framesR` is channel 1 (R) — pass EMPTY for a mono
|
||||
// sample. A `framesR` whose length mismatches `frames` is dropped (falls back to mono), so a
|
||||
// bad pair never half-plays. `sampleRate` is the WAV's rate. `play` carries the per-zone play
|
||||
// params (SECONDS); defaults to the product defaults (Gate + tier-0 AHDSR + Preserve) so a
|
||||
// picked single capture plays under the same default engine as a zone would. Resolves the
|
||||
// wall-clock seconds to frames against `sampleRate` before stamping the SampleData.
|
||||
Keymap buildTier0Keymap(std::vector<AudioSample> frames, int sampleRate,
|
||||
int rootNote, const SampleLoop& loop,
|
||||
std::vector<AudioSample> framesR = {},
|
||||
const ZonePlaySeconds& play = ZonePlaySeconds{});
|
||||
|
||||
// --- Performance map (Tier 1, D-B: the instrument's OWN state) ---------------
|
||||
// --- Performance map (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.
|
||||
// samples zoned across the keyboard, each with a key range and a root note. A performance
|
||||
// choice, so it lives in the instrument (VST3 component state), never written back to the
|
||||
// bank. Pure value type: names bank samples by id (the stable seam key), holds no PCM — the
|
||||
// shell resolves+decodes each id's WAV, and 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).
|
||||
//
|
||||
// S11 loop/start overrides (instrument-owned, D-B — mirror of rootOverride): the sustain
|
||||
// loop and the initial read position are FACTS about the file (S2 bank intrinsics), but the
|
||||
// instrument may override them per zone WITHOUT writing back to the bank. loopOverride wins
|
||||
// over the bank's S2 loop intrinsic when set; startPoint sets the voice's initial read frame
|
||||
// (absent -> frame 0). Both are seeded from the bank intrinsic in the editor and stored here;
|
||||
// resolvePerformance folds override-beats-intrinsic into the effective ResolvedZone.
|
||||
// One authored zone: a bank sample mapped to an inclusive [lowNote, highNote] key range.
|
||||
// rootOverride absent -> repitch from the bank sample's own rootNote intrinsic (or middle C
|
||||
// when empty). loopOverride/startPoint mirror rootOverride: the sustain loop and initial
|
||||
// read position are facts about the file, but the instrument may override them per zone
|
||||
// without writing back to the bank (loopOverride wins when set; startPoint sets the voice's
|
||||
// initial read frame, absent -> 0). resolvePerformance folds override-beats-intrinsic into
|
||||
// the effective ResolvedZone.
|
||||
struct PerformanceZone {
|
||||
std::string sampleId; // bank sample id this zone plays
|
||||
int lowNote = 0; // inclusive
|
||||
@@ -277,146 +215,125 @@ struct PerformanceZone {
|
||||
std::optional<SampleLoop> loopOverride; // instrument-owned sustain loop; absent -> bank intrinsic
|
||||
std::optional<std::int64_t> startPoint; // instrument-owned initial read frame; absent -> 0
|
||||
|
||||
// S-VIEW-6 key-tracking scalar (instrument-owned, D-B — mirror of rootOverride): how far
|
||||
// playback pitch tracks the keyboard around the root. 1.0 (100%) is standard 12-tone-ET (the
|
||||
// DEFAULT; a pre-S-VIEW-6 blob with no keyTrack tail lifts to exactly 1.0, so already-saved
|
||||
// instances are bit-identical); 0.0 = no tracking (every key plays root pitch); 2.0 = double.
|
||||
// NOT flag-gated — always present in the CURRENT payload (v6). Carried through to KeyZone by
|
||||
// resolvePerformance and applied in keyTrackedRatio inside BOTH repitch engines.
|
||||
// Key-tracking scalar: how far playback pitch tracks the keyboard around the root. 1.0
|
||||
// (100%, standard 12-tone-ET) is the default — a blob predating this field lifts to
|
||||
// exactly 1.0, so already-saved instances are bit-identical. 0.0 = no tracking (every
|
||||
// key plays root pitch); 2.0 = double. Applied in keyTrackedRatio inside both repitch
|
||||
// engines.
|
||||
double keyTrack = 1.0;
|
||||
|
||||
// S-VIEW-9 velocity->amp transfer curve (instrument-owned, D-B — mirror of keyTrack): maps the
|
||||
// note-on MIDI velocity (0..127) to the voice's amp gain, replacing the fixed linear velocity/127.
|
||||
// A per-sound performance characteristic, so it varies PER ZONE. DEFAULT = flat y=1 (R10-F1
|
||||
// Option A, Daniel-approved): every velocity plays at unity. This is a DELIBERATE, non-back-compat
|
||||
// behavior change — a pre-S-VIEW-9 blob (no velocityCurve field) lifts to flat y=1, so an
|
||||
// already-saved zone's soft hits play LOUDER than under the old linear map. Intended; do NOT
|
||||
// preserve the linear response. Carried to KeyZone by resolvePerformance, eval'd in Voice::start.
|
||||
// Sequenced on the zones-payload axis AFTER keyTrack (payload v6 -> v7).
|
||||
// Velocity->amp transfer curve: maps note-on MIDI velocity (0..127) to voice amp gain,
|
||||
// replacing the old fixed linear velocity/127. Per-zone. Default = flat y=1 (Daniel-
|
||||
// approved): every velocity plays at unity. DELIBERATE non-back-compat behavior change —
|
||||
// a blob predating this field lifts to flat y=1, so an already-saved zone's soft hits
|
||||
// play LOUDER than under the old linear map. Do NOT preserve the linear response. Eval'd
|
||||
// in Voice::start.
|
||||
VelocityCurve velocityCurve = VelocityCurve::flat();
|
||||
|
||||
// S15/S16 per-zone play parameters (play mode + AHDSR + Trigger %-length/fades; pitch
|
||||
// engine + AD pitch envelope). Instrument-owned (D-B), never a bank fact — mirror of the
|
||||
// loop/start overrides. Wall-clock times are stored in SECONDS (rate-free); the keymap build
|
||||
// resolves them to frames at the live sample rate. Defaults to the PRODUCT defaults for a NEW
|
||||
// zone: Gate play mode, tier-0 AHDSR seconds (0.003 attack / 0.060 release), hold 0, no fades,
|
||||
// PRESERVE pitch engine (S16-F1), pitch env off. An older zone-payload blob (no S15/S16 tail)
|
||||
// lifts to exactly these defaults on read (see the PAYLOAD versioning).
|
||||
// Per-zone play parameters (play mode + AHDSR + Trigger %-length/fades; pitch engine +
|
||||
// AD pitch envelope). Instrument-owned, never a bank fact. Wall-clock times stored in
|
||||
// SECONDS (rate-free); keymap build resolves to frames at the live sample rate. Defaults
|
||||
// for a NEW zone: Gate, tier-0 AHDSR seconds (0.003 attack / 0.060 release), hold 0, no
|
||||
// fades, Preserve pitch engine, pitch env off. An older zone blob lacking this tail lifts
|
||||
// to exactly these defaults on read.
|
||||
ZonePlaySeconds play;
|
||||
};
|
||||
|
||||
// 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).
|
||||
// overlap resolution — first zone in order wins (mirrors the core's first-match
|
||||
// Keymap::resolve); overlaps are neither rejected nor clamped, deterministic by construction.
|
||||
struct PerformanceMap {
|
||||
std::vector<PerformanceZone> zones;
|
||||
|
||||
bool empty() const { return zones.empty(); }
|
||||
};
|
||||
|
||||
// Single-capture ("Sample face") zone-lifecycle reconcile — the zone-bleed fix (issue 3a).
|
||||
// Single-capture ("Sample face") zone-lifecycle reconcile — the zone-bleed fix.
|
||||
//
|
||||
// The Sample face materializes ONE full-range [0,127] zone for the loaded sample on first
|
||||
// control edit (ensureSampleZone). Loading a different sample used to change only the
|
||||
// selection id, leaving the previous sample's full-range zone in the map — and since zone
|
||||
// resolution is FIRST-MATCH in order, that stale zone shadowed every later one forever: the
|
||||
// engine kept playing the old sample while the editor drew the new one's zone (matched by
|
||||
// sampleId, order-blind). This function is called at every selection-change site so the zone
|
||||
// the editor draws is the zone the engine plays.
|
||||
// control edit. Loading a different sample used to change only the selection id, leaving
|
||||
// the previous sample's full-range zone in the map — and since zone resolution is
|
||||
// first-match in order, that stale zone shadowed every later one forever: the engine kept
|
||||
// playing the old sample while the editor drew the new one's zone. This function is called
|
||||
// at every selection-change site so the zone the editor draws is the zone the engine plays.
|
||||
//
|
||||
// Rules (pure, order-preserving where it matters):
|
||||
// * empty `selectedId` or empty map -> untouched, false.
|
||||
// * ANY zone with an authored key range (not the full [0,127]) -> the map is Zone-view
|
||||
// authorship; first-match order is load-bearing there — untouched, false. The Sample
|
||||
// face never creates a narrow zone, so a narrow zone proves deliberate multi-zone intent.
|
||||
// * else (every zone full-range — the map is purely Sample-face-shaped): keep only the
|
||||
// first zone bound to `selectedId` (the selection's own params are not reset); drop
|
||||
// the rest. A selection with no zone yet empties the map (the shell then plays the
|
||||
// selection via the Tier-0 fast path with product defaults).
|
||||
// Rules (order-preserving where it matters):
|
||||
// * empty `selectedId` or empty map -> untouched, false.
|
||||
// * ANY zone with an authored key range (not full [0,127]) -> Zone-view authorship,
|
||||
// first-match order is load-bearing there — untouched, false (the Sample face never
|
||||
// creates a narrow zone, so a narrow zone proves deliberate multi-zone intent).
|
||||
// * else (every zone full-range) -> keep only the first zone bound to `selectedId`
|
||||
// (params preserved); drop the rest. A selection with no zone yet empties the map.
|
||||
// Returns true iff the map changed (the caller republishes + reloads on true).
|
||||
bool reconcileSingleCaptureZones(PerformanceMap& map, const std::string& selectedId);
|
||||
|
||||
// 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.
|
||||
// One resolved zone ready for the shell to decode + the pure build to stitch: project-
|
||||
// relative WAV path (file seam), effective root note (override beats bank intrinsic beats
|
||||
// middle-C default), loop intrinsic, 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
|
||||
double keyTrack = 1.0; // S-VIEW-6 key-tracking scalar, carried from PerformanceZone (1.0 = 100% ET)
|
||||
VelocityCurve velocityCurve = VelocityCurve::flat(); // S-VIEW-9 velocity->amp curve, carried from PerformanceZone
|
||||
SampleLoop loop; // effective: loopOverride, else bank S2 intrinsic (S11)
|
||||
std::int64_t startFrame = 0; // effective initial read frame: startPoint, else 0 (S11)
|
||||
double keyTrack = 1.0; // carried from PerformanceZone (1.0 = 100% ET)
|
||||
VelocityCurve velocityCurve = VelocityCurve::flat(); // carried from PerformanceZone
|
||||
SampleLoop loop; // effective: loopOverride, else bank intrinsic
|
||||
std::int64_t startFrame = 0; // effective initial read frame: startPoint, else 0
|
||||
ZonePlaySeconds play; // S15/S16 per-zone play params (SECONDS; resolved to frames at build)
|
||||
};
|
||||
|
||||
// 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).
|
||||
// `zones` are the zones whose sampleId still resolves, IN MAP ORDER (overlap-order
|
||||
// preserved). `droppedSampleIds`: a zone naming a deleted/moved-out sample is dropped
|
||||
// cleanly — not an error, not silence for the whole map — and 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.
|
||||
// Resolve a performance map against the live "banks" ext-state blob. Each zone's sampleId
|
||||
// is looked up across every bank; a hit yields a ResolvedZone with the effective root note
|
||||
// and loop intrinsic; a miss appends to droppedSampleIds. Empty/malformed blob or empty map
|
||||
// -> empty result.
|
||||
//
|
||||
// NOT the live load path since pS: reloadInstrument resolves via resolvePerformanceFromRefs
|
||||
// (the instance-owned refs). This bank-side resolver is retained as the TESTED REFERENCE
|
||||
// the refs path is verified against (testResolveFromRefsMatchesBankResolve) — both share
|
||||
// foldZone, so the drift test is what keeps the shared fold honest.
|
||||
// NOT the live load path — reloadInstrument resolves via resolvePerformanceFromRefs (the
|
||||
// instance-owned refs). Retained as the TESTED REFERENCE the refs path is verified against
|
||||
// (both share foldZone, so the drift test keeps the shared fold honest).
|
||||
ResolvedPerformance resolvePerformance(const std::string& banksJson,
|
||||
const PerformanceMap& map);
|
||||
|
||||
// Resolve a performance map against the INSTANCE-OWNED refs table (pS self-contained
|
||||
// playback) — the bank-free mirror of resolvePerformance, sharing the same override-
|
||||
// beats-intrinsic fold, so the two paths cannot drift. A zone whose sampleId has no ref
|
||||
// is dropped + reported (same stale-id shape as the bank path). Pure.
|
||||
// The bank-free mirror of resolvePerformance, against the INSTANCE-OWNED refs table —
|
||||
// shares the same override-beats-intrinsic fold, so the two paths cannot drift. A zone
|
||||
// whose sampleId has no ref is dropped + reported (same stale-id shape as the bank path).
|
||||
ResolvedPerformance resolvePerformanceFromRefs(const SampleRefs& refs,
|
||||
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).
|
||||
// Build a zoned Keymap from resolved zones + their decoded mono PCM. `decoded[i]` matches
|
||||
// `zones[i]` in length + order. One SampleData per zone (a sample used by two zones is
|
||||
// decoded twice — acceptable here, the shell may dedup by path later). Zone order preserved
|
||||
// so first-match overlap resolution matches authored order. A zone whose decoded frames are
|
||||
// empty is SKIPPED (an unreadable WAV drops the zone, not the map).
|
||||
struct DecodedZonePcm {
|
||||
std::vector<AudioSample> monoFrames; // channel 0 (mono, or L of a stereo decode)
|
||||
int sampleRate = 0; // 0 is explicitly invalid; every consumer must
|
||||
// receive the WAV's real rate before use.
|
||||
int sampleRate = 0; // 0 is explicitly invalid
|
||||
std::vector<AudioSample> framesR; // channel 1 (R); EMPTY for a mono decode
|
||||
};
|
||||
Keymap buildZonedKeymap(const std::vector<ResolvedZone>& zones,
|
||||
const std::vector<DecodedZonePcm>& decoded);
|
||||
|
||||
// Apply the S7 cross-mode channel policy (D-E) to freshly-decoded interleaved PCM, yielding
|
||||
// the 1- or 2-channel DecodedZonePcm the keymap build consumes. `interleaved` is the WAV's
|
||||
// float frames (stride = `sourceChannels`); `mode` is the instance's channel mode.
|
||||
// * MONO mode -> downmix to one channel (the existing policy: average all source
|
||||
// channels). framesR EMPTY. A mono or stereo source both collapse.
|
||||
// * STEREO mode, mono src -> DUAL-MONO: channel 0 duplicated into channel 1 (centered).
|
||||
// * STEREO mode, stereo src -> channels 0 and 1 taken as-is (L/R). A source with >2 channels
|
||||
// takes channels 0 and 1 (documented; the sampler's stereo image is
|
||||
// the first two channels — no surround fold).
|
||||
// Empty / zero-channel input -> a DecodedZonePcm with empty frames (the caller drops the zone
|
||||
// or plays silence). Pure — the shell does the file I/O and hands the interleaved buffer here.
|
||||
// Apply the cross-mode channel policy to freshly-decoded interleaved PCM, yielding the 1- or
|
||||
// 2-channel DecodedZonePcm the keymap build consumes. `interleaved` is the WAV's float
|
||||
// frames (stride = `sourceChannels`); `mode` is the instance's channel mode.
|
||||
// * MONO mode -> downmix to one channel (average all source channels).
|
||||
// * STEREO mode, mono src -> dual-mono: channel 0 duplicated into channel 1 (centered).
|
||||
// * STEREO mode, stereo+ src -> channels 0 and 1 as-is (no surround fold on >2 channels).
|
||||
// Empty/zero-channel input -> empty frames (caller drops the zone or plays silence).
|
||||
DecodedZonePcm decodeChannels(const std::vector<AudioSample>& interleaved,
|
||||
int sourceChannels, ChannelMode mode, int sampleRate);
|
||||
|
||||
// The ComponentState envelope + zones-payload binary codec (serializePerformance /
|
||||
// serializeComponentState / serializeSelection + the deserializers and every version
|
||||
// constant) lives in component_state_io.h (Q-W2v split, T4-13 ≡ T2-07): the codec grows
|
||||
// on every envelope bump and is consumed by the EXTENSION's preset-blob path too — the
|
||||
// split lets both artifacts share the codec while only the VST links the voice engine.
|
||||
// The ComponentState envelope + zones-payload binary codec lives in component_state_io.h:
|
||||
// it grows on every envelope bump and is consumed by the extension's preset-blob path too,
|
||||
// so both artifacts share the codec while only the VST links the voice engine.
|
||||
|
||||
} // namespace reasampler::instrument::map
|
||||
|
||||
Reference in New Issue
Block a user