S12: store wall-clock ADSR/pitch-env as seconds, resolve to frames at live rate

Kill kTier0Nominal*, adsrNeedsRateResolve, tier0Adsr, gateAdsr. Zones payload v5 carries
seconds; v3 legacy reads convert at the frozen authoring rate; v4 (branch-only) dropped.
Editor sliders now seconds. Engine takes frames resolved at keymap build.
This commit is contained in:
2026-07-27 02:51:46 -04:00
parent 1d338318e7
commit a54af277e4
10 changed files with 539 additions and 433 deletions
+22 -25
View File
@@ -341,18 +341,15 @@ public:
// Starts this voice on `note` at `velocity`, playing `sample` (a stable reference
// the caller must keep alive for the voice's lifetime — the Keymap owns it), repitched
// from `rootNote`. All five AHDSR fields (A/H/D/S/R) are read directly from
// sample.play.adsr — the per-zone values stamped by buildTier0Keymap / buildZonedKeymap.
// `gateAdsr` is the VoiceEngine's instrument-wide ADSR parameter, accepted for interface
// compatibility but NOT used by start() (vestigial since the S12 review fix moved A/D/S/R
// onto the per-zone SampleData). The S15 play MODE + Trigger params and the S16 pitch
// ENGINE + pitch envelope are read from `sample.play`. The Preserve shifters MUST already
// be pre-sized (presizePreserveShifters, off-thread) — start() only reset()s + warm()s
// them (RT-safe, no allocation) since it runs on the audio thread inside process(). The warm
// silence pass settles the OLA taps before the first output frame (no cold-start click).
// Byte-identical to the pre-S15 engine when sample.play is default (Gate + Varispeed + no
// pitch env).
void start(int note, int velocity, const SampleData& sample, int rootNote,
const AdsrParams& gateAdsr);
// sample.play.adsr — the per-zone values (in FRAMES) resolved from the stored seconds by
// buildTier0Keymap / buildZonedKeymap against the live sample rate. The S15 play MODE +
// Trigger params and the S16 pitch ENGINE + pitch envelope are read from `sample.play`.
// The Preserve shifters MUST already be pre-sized (presizePreserveShifters, off-thread) —
// start() only reset()s + warm()s them (RT-safe, no allocation) since it runs on the audio
// thread inside process(). The warm silence pass settles the OLA taps before the first
// output frame (no cold-start click). Byte-identical to the pre-S15 engine when sample.play
// is default (Gate + Varispeed + no pitch env).
void start(int note, int velocity, const SampleData& sample, int rootNote);
// Gate off — begins the amplitude release. In GATE mode this enters the AHDSR release; in
// TRIGGER mode it is a NO-OP (Trigger ignores note-off and plays through to its play length).
@@ -456,18 +453,19 @@ class VoiceEngine {
public:
// Builds an engine with `maxVoices` voices (the polyphony bound) playing from
// `keymap`. The keymap must outlive the engine (the engine holds a reference — it
// reads zones and sample data through it, never copies PCM). `adsr` is the instrument-wide
// Gate AHDSR timing (attack/decay/sustain/release); each zone's HOLD stage + play mode +
// pitch engine ride on its SampleData::play. `preserveVoiceCap` (S16) bounds how many
// Preserve-engine voices may sound at once (the shifter is materially heavier than
// Varispeed) — a Preserve note-on beyond the cap is dropped rather than glitching; 0 means
// "no separate Preserve cap" (bounded only by maxVoices). `preserveWindowFrames` is the OLA
// window (in OUTPUT frames) every voice's Preserve pitch shifters are PRE-SIZED to at
// construction (OFF the audio thread), so note-on (which runs in process()) never allocates;
// 0 leaves them pass-through (a Varispeed-only instrument pays no ring cost). The processor
// derives it from the host sample rate (kPreserveWindowMs). Defaulted so existing callers
// (and the pure-core tests) are unaffected.
VoiceEngine(std::size_t maxVoices, const Keymap& keymap, const AdsrParams& adsr,
// reads zones and sample data through it, never copies PCM). Every AHDSR field (A/H/D/S/R)
// + play mode + pitch engine rides on each zone's SampleData::play (in FRAMES, resolved
// from the stored seconds at keymap build); the engine holds no instrument-wide ADSR.
// `preserveVoiceCap` (S16) bounds how many Preserve-engine voices may sound at once (the
// shifter is materially heavier than Varispeed) — a Preserve note-on beyond the cap is
// dropped rather than glitching; 0 means "no separate Preserve cap" (bounded only by
// maxVoices). `preserveWindowFrames` is the OLA window (in OUTPUT frames) every voice's
// Preserve pitch shifters are PRE-SIZED to at construction (OFF the audio thread), so
// note-on (which runs in process()) never allocates; 0 leaves them pass-through (a
// Varispeed-only instrument pays no ring cost). The processor derives it from the host
// sample rate (kPreserveWindowMs). Defaulted so existing callers (and the pure-core tests)
// are unaffected.
VoiceEngine(std::size_t maxVoices, const Keymap& keymap,
std::size_t preserveVoiceCap = 0, std::int64_t preserveWindowFrames = 0);
// MIDI note-on. Resolves the note+velocity to a zone; if none matches (out of
@@ -524,7 +522,6 @@ private:
std::vector<Voice> voices_;
const Keymap& keymap_;
AdsrParams adsr_;
std::size_t preserveVoiceCap_ = 0; // S16: max simultaneous Preserve voices (0 = no separate cap)
std::uint64_t nextStartOrder_ = 1; // monotonic; 0 reserved for "never started"
};