S-VIEW-6: per-zone keyTrack scalar (0-200%, default 100%) with pure ratio math in both repitch engines

keyTrackedRatio is bit-identical to pitchRatio at 100%; keyTrack threads through PerformanceZone->ResolvedZone->KeyZone into baseRatio_, so Varispeed and Preserve both inherit it. Zones payload bumped to v6; older blobs lift to 1.0.
This commit is contained in:
2026-07-27 13:25:53 -04:00
parent 3b9b78b82c
commit 42dd30b2fe
6 changed files with 252 additions and 6 deletions
+12 -1
View File
@@ -216,6 +216,9 @@ ResolvedPerformance resolvePerformance(const std::string& banksJson,
// Effective root: override beats bank intrinsic beats middle-C default.
rz.rootNote = z.rootOverride ? *z.rootOverride
: (found->rootNote ? *found->rootNote : 60);
// S-VIEW-6: the key-tracking scalar is instrument state (not a bank fact) — carried
// straight through to the resolved zone and applied in the repitch math at play time.
rz.keyTrack = z.keyTrack;
// Effective loop / start (S11): the instrument's per-zone override wins over the
// bank's S2 intrinsic; absent -> the intrinsic (loop) / frame 0 (start). The bank is
// never mutated — this only shapes what the core plays for THIS instance (D-B).
@@ -260,6 +263,7 @@ Keymap buildZonedKeymap(const std::vector<ResolvedZone>& zones,
zone.lowNote = zones[i].lowNote;
zone.highNote = zones[i].highNote;
zone.rootNote = zones[i].rootNote;
zone.keyTrack = zones[i].keyTrack; // S-VIEW-6: applied in keyTrackedRatio at play time
zone.sampleIndex = sampleIndex;
km.zones.push_back(zone);
}
@@ -403,6 +407,8 @@ void putZonesPayload(std::vector<std::uint8_t>& out, const PerformanceMap& map)
putU64le(out, doubleToBits(pp.adsr.decaySeconds));
putU64le(out, doubleToBits(pp.adsr.sustainLevel));
putU64le(out, doubleToBits(pp.adsr.releaseSeconds));
// PAYLOAD v6 (S-VIEW-6): the per-zone key-tracking scalar, appended last (1.0 = 100% ET).
putU64le(out, doubleToBits(z.keyTrack));
}
}
@@ -423,7 +429,8 @@ void readZonesPayload(ByteReader& r, PerformanceMap& map, double projectRate) {
extended = (pv >= 2); // v2+ carries the loop/start tail
}
const bool legacyV3Play = (pv == 3); // legacy S15/S16 play tail, wall-clock in 44.1k frames
const bool secondsPlay = (pv >= 5); // current: full play params, wall-clock in seconds
const bool secondsPlay = (pv >= 5); // v5+: full play params, wall-clock in seconds
const bool keyTrackTail = (pv >= 6); // v6+ (S-VIEW-6): per-zone keyTrack scalar appended last
const std::uint32_t count = r.u32();
for (std::uint32_t i = 0; i < count && r.ok; ++i) {
// z.play defaults to the PRODUCT defaults (Gate + Preserve + tier-0 AHDSR seconds). A
@@ -482,6 +489,10 @@ void readZonesPayload(ByteReader& r, PerformanceMap& map, double projectRate) {
z.play.adsr.sustainLevel = bitsToDouble(r.u64());
z.play.adsr.releaseSeconds = bitsToDouble(r.u64());
}
// PAYLOAD v6 (S-VIEW-6): the key-tracking scalar, appended after the v5 play tail. A pre-v6
// payload (no field) leaves the PerformanceZone default (keyTrack = 1.0 = 100% ET), so an
// already-saved instance repitches BIT-IDENTICALLY to the pre-S-VIEW-6 engine.
if (keyTrackTail) z.keyTrack = bitsToDouble(r.u64());
// Payload versions 4 (branch-only frames tail, never shipped) and any unknown pv leave the
// seconds product defaults on z.play — a v4 blob cannot exist outside this branch.
if (!r.ok) break; // truncated mid-zone -> keep what parsed cleanly, drop the rest