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
+82
View File
@@ -1194,6 +1194,84 @@ static void testV3BlobLiftsAdsrToNominalDefaults() {
CHECK(a.decayFrames == kTier0NominalDecayFrames); // 0
CHECK(a.sustainLevel == kTier0NominalSustainLevel); // 1.0
CHECK(a.releaseFrames == kTier0NominalReleaseFrames); // 2646 (0.060 s at 44100)
// The lifted zone must be flagged for rate-resolve so buildZonedKeymap rescales at the live rate.
CHECK(back.zones[0].adsrNeedsRateResolve == true);
}
// A v4 blob (explicit A/D/S/R tail) must NOT set adsrNeedsRateResolve — those values
// were authored at the DAW's rate and must not be rescaled again at keymap build time.
static void testV4BlobClearsAdsrNeedsRateResolve() {
PerformanceMap m;
PerformanceZone z = zone("pad", 0, 127);
z.play.adsr.attackFrames = 441;
z.play.adsr.releaseFrames = 8820;
m.zones.push_back(z);
// A round-trip through serialize/deserialize writes a v4 payload (current version).
const PerformanceMap back = deserializePerformance(serializePerformance(m));
CHECK(back.zones.size() == 1);
if (back.zones.size() != 1) return;
// v4 tail was explicitly read — adsrNeedsRateResolve must be false.
CHECK(back.zones[0].adsrNeedsRateResolve == false);
}
// buildTier0Keymap at 48k must produce ADSR frame counts equal to tier0Adsr(48000):
// attack = round(kTier0NominalAttackFrames * 48000 / 44100) = round(143.67) = 144,
// release = round(kTier0NominalReleaseFrames * 48000 / 44100) = round(2880.0) = 2880.
// This is the "pre-fix, the Gate voice used tier0Adsr(sampleRate_)" invariant restored
// for the single-capture fast path at any DAW rate.
static void testBuildTier0KeymapRescalesAdsrAt48k() {
const Keymap km = buildTier0Keymap({0.5f}, 48000, 60, SampleLoop{});
CHECK(km.samples.size() == 1);
if (km.samples.empty()) return;
const AdsrParams& a = km.samples[0].play.adsr;
// Rescaled from 44100-nominal at 48000 Hz:
CHECK(a.attackFrames == 144); // round(132 * 48000.0 / 44100.0)
CHECK(a.decayFrames == 0); // 0 * factor = 0 (no change)
CHECK(a.sustainLevel == 1.0); // level, not frames (no rescale)
CHECK(a.releaseFrames == 2880); // round(2646 * 48000.0 / 44100.0)
}
// buildZonedKeymap at 48k with adsrNeedsRateResolve=true must rescale ADSR to match
// tier0Adsr(48000), mirroring what Gate voices saw before the per-zone-ADSR fix.
static void testBuildZonedKeymapRescalesNominalAdsrAt48k() {
// Build a zone with nominal 44100-Hz ADSR and the needs-resolve flag (the default).
ResolvedZone z;
z.lowNote = 0; z.highNote = 127; z.rootNote = 60;
z.adsrNeedsRateResolve = true; // 44100-nominal values, rescale needed
z.play.adsr.attackFrames = kTier0NominalAttackFrames; // 132
z.play.adsr.decayFrames = kTier0NominalDecayFrames; // 0
z.play.adsr.sustainLevel = kTier0NominalSustainLevel; // 1.0
z.play.adsr.releaseFrames = kTier0NominalReleaseFrames; // 2646
const DecodedZonePcm pcm{{0.5f}, 48000}; // 48k WAV
const Keymap km = buildZonedKeymap({z}, {pcm});
CHECK(km.samples.size() == 1);
if (km.samples.empty()) return;
const AdsrParams& a = km.samples[0].play.adsr;
CHECK(a.attackFrames == 144); // round(132 * 48000.0 / 44100.0)
CHECK(a.decayFrames == 0); // 0 * factor = 0
CHECK(a.sustainLevel == 1.0); // level, not rescaled
CHECK(a.releaseFrames == 2880); // round(2646 * 48000.0 / 44100.0)
}
// buildZonedKeymap must NOT rescale a zone whose adsrNeedsRateResolve is false —
// those frame counts are explicitly user-authored at the DAW's rate.
static void testBuildZonedKeymapDoesNotRescaleV4Adsr() {
ResolvedZone z;
z.lowNote = 0; z.highNote = 127; z.rootNote = 60;
z.adsrNeedsRateResolve = false; // v4 blob or user-edited — do not rescale
z.play.adsr.attackFrames = 441; // 0.01 s at 44100 Hz (non-nominal)
z.play.adsr.decayFrames = 4410;
z.play.adsr.sustainLevel = 0.7;
z.play.adsr.releaseFrames = 8820;
const DecodedZonePcm pcm{{0.5f}, 48000}; // 48k WAV — rescale would change values
const Keymap km = buildZonedKeymap({z}, {pcm});
CHECK(km.samples.size() == 1);
if (km.samples.empty()) return;
const AdsrParams& a = km.samples[0].play.adsr;
CHECK(a.attackFrames == 441); // unchanged (not rescaled)
CHECK(a.decayFrames == 4410);
CHECK(a.sustainLevel == 0.7);
CHECK(a.releaseFrames == 8820);
}
int main() {
@@ -1249,6 +1327,10 @@ int main() {
testPlayParamsThroughComponentEnvelope();
testFullAdsrV4RoundTrip();
testV3BlobLiftsAdsrToNominalDefaults();
testV4BlobClearsAdsrNeedsRateResolve();
testBuildTier0KeymapRescalesAdsrAt48k();
testBuildZonedKeymapRescalesNominalAdsrAt48k();
testBuildZonedKeymapDoesNotRescaleV4Adsr();
testComponentStateRoundTrip();
testComponentStateLoopStartRoundTrip();
testComponentStateSelectionOnlyNoZones();