fix: S12 R2 — marker-drag sets selectedZone_, ADSR rate-resolve, gateAdsr doc

Marker drag now sets selectedZone_ so single-capture controls stay reachable. Lifted/default ADSR rescales nominal 44100-Hz frame counts by sampleRate/44100 via adsrNeedsRateResolve; v4-authored zones skip rescale. gateAdsr documented vestigial.
This commit is contained in:
2026-07-27 02:01:43 -04:00
parent 7fb778206c
commit 1d338318e7
7 changed files with 192 additions and 41 deletions
+35
View File
@@ -147,6 +147,23 @@ Keymap buildTier0Keymap(std::vector<AudioSample> frames, int sampleRate,
data.rootNote = rootNote;
data.loop = loop;
data.play = play; // S15/S16 single-capture play params (product defaults unless overridden)
// The default `play` arg carries 44100-Hz nominal ADSR frame counts (kTier0Nominal*).
// Rescale A/D/R by (sampleRate / 44100) so the wall-clock ADSR matches tier0Adsr(sampleRate)
// exactly. Sustain (a level, not a frame count) is unchanged. At 44100 the factor is 1.0 —
// bit-identical to the pre-fix build. The tier-0 single-capture path never carries user-edited
// ADSR (users edit ADSR through zones, which go through buildZonedKeymap), so always rescaling
// here is correct and safe.
if (data.sampleRate != 44100) {
const double factor = static_cast<double>(data.sampleRate) / 44100.0;
data.play.adsr.attackFrames = static_cast<std::int64_t>(
static_cast<double>(data.play.adsr.attackFrames) * factor + 0.5);
data.play.adsr.decayFrames = static_cast<std::int64_t>(
static_cast<double>(data.play.adsr.decayFrames) * factor + 0.5);
data.play.adsr.releaseFrames = static_cast<std::int64_t>(
static_cast<double>(data.play.adsr.releaseFrames) * factor + 0.5);
}
return Keymap::singleSampleChromatic(std::move(data));
}
@@ -190,6 +207,8 @@ ResolvedPerformance resolvePerformance(const std::string& banksJson,
// S15/S16 per-zone play params carry through unchanged (they are instrument state, not
// resolved against the bank) so the keymap build can stamp them onto the SampleData.
rz.play = z.play;
// Carry the rate-resolve flag so buildZonedKeymap can rescale 44100-nominal ADSR counts.
rz.adsrNeedsRateResolve = z.adsrNeedsRateResolve;
out.zones.push_back(std::move(rz));
}
return out;
@@ -215,6 +234,20 @@ Keymap buildZonedKeymap(const std::vector<ResolvedZone>& zones,
data.loop = zones[i].loop;
data.startFrame = zones[i].startFrame; // S11 effective start (override, else 0)
data.play = zones[i].play; // S15/S16 per-zone play mode + engine + envelopes
// If the zone's ADSR came from a v3 lift or a new-zone default (adsrNeedsRateResolve),
// its A/D/R frame counts are 44100-Hz nominals. Rescale to the WAV's actual rate so
// wall-clock ADSR durations match tier0Adsr(sampleRate) exactly. v4 zones (user-edited
// frame counts) carry adsrNeedsRateResolve=false and are left unchanged.
if (zones[i].adsrNeedsRateResolve && data.sampleRate != 44100) {
const double factor = static_cast<double>(data.sampleRate) / 44100.0;
data.play.adsr.attackFrames = static_cast<std::int64_t>(
static_cast<double>(data.play.adsr.attackFrames) * factor + 0.5);
data.play.adsr.decayFrames = static_cast<std::int64_t>(
static_cast<double>(data.play.adsr.decayFrames) * factor + 0.5);
data.play.adsr.releaseFrames = static_cast<std::int64_t>(
static_cast<double>(data.play.adsr.releaseFrames) * factor + 0.5);
}
const std::size_t sampleIndex = km.samples.size();
km.samples.push_back(std::move(data));
KeyZone zone;
@@ -420,10 +453,12 @@ void readZonesPayload(ByteReader& r, PerformanceMap& map) {
}
if (hasAdsr) {
// S12 review fix (v4): the full per-zone A/D/S/R tail — read in the emit order.
// These values were authored at the DAW's rate; mark them resolved (no rescaling).
z.play.adsr.attackFrames = r.i64();
z.play.adsr.decayFrames = r.i64();
z.play.adsr.sustainLevel = bitsToDouble(r.u64());
z.play.adsr.releaseFrames = r.i64();
z.adsrNeedsRateResolve = false; // already rate-resolved; do NOT rescale at keymap build
} else if (hasPlay) {
// v3 blob: A/D/S/R fields are absent. Lift to the tier-0 nominal defaults (44100 Hz)
// so a voice playing this zone sounds bit-identical to the pre-v4 build (back-compat).