S-VIEW-9: velocity->amp transfer curve (pure velocity_curve module + zones-payload v7 + Voice::start apply)
Default flat y=1 (R10-F1 Option A) replaces the linear velocity/127 at note-on — a deliberate, non-back-compat behavior change; v1-v6 blobs lift to the flat default.
This commit is contained in:
@@ -1370,6 +1370,104 @@ static void testKeyTrackV5BackCompatLiftsToUnity() {
|
||||
CHECK(back.zones[0].keyTrack == 1.0); // no keyTrack tail -> default 1.0 (bit-identical repitch)
|
||||
}
|
||||
|
||||
// --- S-VIEW-9 velocity->amp curve: v7 round-trip + resolve-through + v6 back-compat lift ---------
|
||||
|
||||
static void testVelocityCurveRoundTrip() {
|
||||
// A per-zone velocity curve survives the payload-v7 round trip losslessly (exact point coords).
|
||||
// A second zone left at the flat default proves the field is per-record and defaults to flat y=1.
|
||||
PerformanceMap m;
|
||||
PerformanceZone z = zone("lead", 20, 100);
|
||||
z.velocityCurve = vst::VelocityCurve::linear();
|
||||
z.velocityCurve.addPoint(60.0, 0.3); // an interior knot to exercise multi-point round-trip
|
||||
m.zones.push_back(z);
|
||||
m.zones.push_back(zone("pad", 0, 19)); // default flat curve
|
||||
const PerformanceMap back = deserializePerformance(serializePerformance(m), 44100.0);
|
||||
CHECK(back.zones.size() == 2);
|
||||
if (back.zones.size() != 2) return;
|
||||
CHECK(back.zones[0].velocityCurve.equals(z.velocityCurve)); // exact point round-trip
|
||||
CHECK(back.zones[1].velocityCurve.equals(vst::VelocityCurve::flat())); // default preserved
|
||||
// And the flat default really is unity everywhere (R10-F1 Option A), not the old linear ramp.
|
||||
CHECK(back.zones[1].velocityCurve.eval(1.0) == 1.0);
|
||||
CHECK(back.zones[1].velocityCurve.eval(64.0) == 1.0);
|
||||
}
|
||||
|
||||
static void testVelocityCurveThroughComponentEnvelope() {
|
||||
// The curve round-trips through the ComponentState envelope too (zones-payload is envelope-
|
||||
// independent, so it carries the v7 tail unchanged).
|
||||
ComponentState s;
|
||||
s.selectionId = "pick";
|
||||
PerformanceZone z = zone("pick", 0, 127);
|
||||
z.velocityCurve = vst::VelocityCurve::linear();
|
||||
s.map.zones.push_back(z);
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||
CHECK(back.map.zones.size() == 1);
|
||||
if (back.map.zones.size() != 1) return;
|
||||
CHECK(back.map.zones[0].velocityCurve.equals(vst::VelocityCurve::linear()));
|
||||
}
|
||||
|
||||
static void testVelocityCurveResolvesToZone() {
|
||||
// resolvePerformance carries the curve from PerformanceZone through to ResolvedZone, so the
|
||||
// keymap build (and thus the voice engine at start()) sees the authored curve.
|
||||
const std::string json = bookJson({makeSample("a", "Kick", "b/a.wav", 36)}, {});
|
||||
PerformanceMap m;
|
||||
PerformanceZone z = zone("a", 0, 127);
|
||||
z.velocityCurve = vst::VelocityCurve::linear();
|
||||
m.zones.push_back(z);
|
||||
const ResolvedPerformance r = resolvePerformance(json, m);
|
||||
CHECK(r.zones.size() == 1);
|
||||
if (r.zones.size() != 1) return;
|
||||
CHECK(r.zones[0].velocityCurve.equals(vst::VelocityCurve::linear()));
|
||||
}
|
||||
|
||||
static void testVelocityCurveV6BackCompatLiftsToFlat() {
|
||||
// A v6 PAYLOAD blob (marker + version 6 + full play tail + keyTrack, but NO velocity-curve field)
|
||||
// lifts every zone to VelocityCurve::flat() (R10-F1 Option A — flat y=1). This is the DELIBERATE
|
||||
// non-back-compat behavior change: an instance saved BEFORE S-VIEW-9 now plays every velocity at
|
||||
// unity, NOT the old linear velocity/127. Hand-build the exact v6 record shape.
|
||||
std::vector<std::uint8_t> b;
|
||||
auto u32 = [&](std::uint32_t v) {
|
||||
b.push_back(v & 0xFF); b.push_back((v >> 8) & 0xFF);
|
||||
b.push_back((v >> 16) & 0xFF); b.push_back((v >> 24) & 0xFF);
|
||||
};
|
||||
auto f64 = [&](double d) {
|
||||
std::uint64_t bits; std::memcpy(&bits, &d, sizeof(bits));
|
||||
for (int i = 0; i < 8; ++i) b.push_back(static_cast<std::uint8_t>((bits >> (i * 8)) & 0xFF));
|
||||
};
|
||||
auto i64 = [&](std::int64_t v) {
|
||||
std::uint64_t bits = static_cast<std::uint64_t>(v);
|
||||
for (int i = 0; i < 8; ++i) b.push_back(static_cast<std::uint8_t>((bits >> (i * 8)) & 0xFF));
|
||||
};
|
||||
u32(kPerformanceStateVersion); // envelope version (2)
|
||||
u32(kZonesFormatMarker); // marker -> a versioned payload
|
||||
u32(6); // PAYLOAD VERSION 6 (pre-S-VIEW-9, keyTrack but no curve)
|
||||
u32(1); // zone count 1
|
||||
const std::string id = "v6saved";
|
||||
u32(static_cast<std::uint32_t>(id.size()));
|
||||
b.insert(b.end(), id.begin(), id.end());
|
||||
u32(10); u32(70); // low/high
|
||||
b.push_back(0); // hasRootOverride = 0
|
||||
b.push_back(0); // hasLoopOverride = 0
|
||||
b.push_back(0); // hasStartPoint = 0
|
||||
// v5 play tail.
|
||||
b.push_back(0); // playMode = Gate
|
||||
f64(0.0); // adsr.holdSeconds
|
||||
f64(1.0); // trigger.lengthFraction
|
||||
i64(0); i64(0); // trigger fades
|
||||
b.push_back(1); // pitchEngine = Preserve
|
||||
b.push_back(0); // pitchEnv.enabled = false
|
||||
f64(0.0); f64(0.0); f64(0.0); // pitchEnv attack/decay/peak
|
||||
f64(0.003); f64(0.0); f64(1.0); f64(0.060); // adsr A/D/S/R
|
||||
f64(0.5); // v6 keyTrack (0.5) — present, but no curve tail follows
|
||||
const PerformanceMap back = deserializePerformance(b, 44100.0);
|
||||
CHECK(back.zones.size() == 1);
|
||||
if (back.zones.size() != 1) return;
|
||||
CHECK(back.zones[0].sampleId == "v6saved");
|
||||
CHECK(back.zones[0].keyTrack == 0.5); // the v6 field still read correctly
|
||||
// No curve tail -> flat y=1 default (the deliberate behavior change).
|
||||
CHECK(back.zones[0].velocityCurve.equals(vst::VelocityCurve::flat()));
|
||||
CHECK(back.zones[0].velocityCurve.eval(20.0) == 1.0); // a soft hit now plays at unity
|
||||
}
|
||||
|
||||
static void testPlayParamsV2BackCompatLiftsToDefaults() {
|
||||
// A pre-S15 PAYLOAD v2 blob (marker + version 2 + record with the S11 tail but NO play tail)
|
||||
// lifts each zone to the PRODUCT defaults: Gate + Preserve (S16-F1) + no fades + env off — the
|
||||
@@ -1619,6 +1717,10 @@ int main() {
|
||||
testKeyTrackThroughComponentEnvelope();
|
||||
testKeyTrackResolvesToZone();
|
||||
testKeyTrackV5BackCompatLiftsToUnity();
|
||||
testVelocityCurveRoundTrip();
|
||||
testVelocityCurveThroughComponentEnvelope();
|
||||
testVelocityCurveResolvesToZone();
|
||||
testVelocityCurveV6BackCompatLiftsToFlat();
|
||||
testPlayParamsV2BackCompatLiftsToDefaults();
|
||||
testPlayParamsThroughComponentEnvelope();
|
||||
testFullAdsrSecondsRoundTrip();
|
||||
|
||||
Reference in New Issue
Block a user