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
+4 -17
View File
@@ -37,10 +37,6 @@ namespace {
// notes neither click on nor cut off abruptly; sustain at unity (velocity does the
// dynamics), a short release for a natural tail. Times are in seconds, converted to
// frames against the live sample rate at build time.
constexpr double kAttackSeconds = 0.003;
constexpr double kDecaySeconds = 0.0;
constexpr double kSustainLevel = 1.0;
constexpr double kReleaseSeconds = 0.060;
constexpr std::size_t kMaxVoices = 16;
// S16 Preserve-mode voice cap: a Preserve voice runs a per-voice OLA pitch shifter and is
@@ -50,16 +46,6 @@ constexpr std::size_t kMaxVoices = 16;
// cost — see the handoff CPU note. 8 is a conservative half of kMaxVoices pending DAW profiling.
constexpr std::size_t kPreserveVoiceCap = 8;
AdsrParams tier0Adsr(double sampleRate) {
const double sr = sampleRate > 0.0 ? sampleRate : 44100.0;
AdsrParams p;
p.attackFrames = static_cast<std::int64_t>(kAttackSeconds * sr);
p.decayFrames = static_cast<std::int64_t>(kDecaySeconds * sr);
p.sustainLevel = kSustainLevel;
p.releaseFrames = static_cast<std::int64_t>(kReleaseSeconds * sr);
return p;
}
// Read a whole file into a byte buffer. Off-thread only (blocking file I/O). Empty on
// any failure — the caller treats an unreadable WAV as "nothing to play".
std::vector<std::uint8_t> readFileBytes(const std::string& path) {
@@ -384,12 +370,13 @@ std::string ReaSamplerProcessor::reloadFromBank() {
if (haveKeymap) {
// Preserve OLA window in OUTPUT frames from the host sample rate (kPreserveWindowMs).
// Every voice's shifter is pre-sized to this off-thread here, so process()-time
// note-on never allocates. Floored at 2 so a valid window is always a real ring.
// note-on never allocates. Floored at 2 so a valid window is always a real ring
// (which also covers a pathological host rate <= 0 — no rate literal needed).
std::int64_t preserveWindow = static_cast<std::int64_t>(
kPreserveWindowMs * (sampleRate_ > 0.0 ? sampleRate_ : 44100.0) / 1000.0 + 0.5);
kPreserveWindowMs * sampleRate_ / 1000.0 + 0.5);
if (preserveWindow < 2) preserveWindow = 2;
built = std::make_unique<LoadedInstrument>(
std::move(km), kMaxVoices, tier0Adsr(sampleRate_), gen, kPreserveVoiceCap,
std::move(km), kMaxVoices, gen, kPreserveVoiceCap,
preserveWindow);
}
}