Merge ps-w9-t2-modes: S15/S16 sampling modes + pitch engines

This commit is contained in:
2026-07-27 00:18:02 -04:00
13 changed files with 1528 additions and 77 deletions
+111
View File
@@ -1031,6 +1031,113 @@ static void testComponentStateV5TruncatedMarker() {
CHECK(back.selectionId.empty() && back.map.zones.empty());
}
// --- S15/S16 zone-payload v3: per-zone play params round-trip + back-compat lift -------------
static void testPlayParamsRoundTrip() {
// A zone carrying explicit S15/S16 play params (Trigger mode, hold, fades, Varispeed engine,
// pitch env on) must round-trip ALL fields losslessly through the payload-v3 tail.
PerformanceMap m;
PerformanceZone z = zone("lead", 20, 100, /*override=*/55);
z.play.playMode = PlayMode::Trigger;
z.play.adsr.holdFrames = 1234;
z.play.trigger.lengthFraction = 0.375;
z.play.trigger.fadeInFrames = 64;
z.play.trigger.fadeOutFrames = 128;
z.play.pitchEngine = PitchEngine::Varispeed;
z.play.pitchEnv.enabled = true;
z.play.pitchEnv.attackFrames = 10;
z.play.pitchEnv.decayFrames = 500;
z.play.pitchEnv.peakSemitones = -7.5;
m.zones.push_back(z);
const PerformanceMap back = deserializePerformance(serializePerformance(m));
CHECK(back.zones.size() == 1);
if (back.zones.size() != 1) return;
const ZonePlayParams& p = back.zones[0].play;
CHECK(p.playMode == PlayMode::Trigger);
CHECK(p.adsr.holdFrames == 1234);
CHECK(p.trigger.lengthFraction == 0.375); // exact double round-trip
CHECK(p.trigger.fadeInFrames == 64);
CHECK(p.trigger.fadeOutFrames == 128);
CHECK(p.pitchEngine == PitchEngine::Varispeed);
CHECK(p.pitchEnv.enabled == true);
CHECK(p.pitchEnv.attackFrames == 10);
CHECK(p.pitchEnv.decayFrames == 500);
CHECK(p.pitchEnv.peakSemitones == -7.5); // exact double round-trip
}
static void testPlayParamsComposeWithLoopStart() {
// S11 (loop/start) x S15/S16 (play params) tails co-exist per zone: both round-trip together.
PerformanceMap m;
PerformanceZone z = zone("pad", 0, 60);
SampleLoop lp; lp.hasLoop = true; lp.start = 111; lp.end = 222;
z.loopOverride = lp;
z.startPoint = 333;
z.play.playMode = PlayMode::Gate;
z.play.adsr.holdFrames = 999;
z.play.pitchEngine = PitchEngine::Preserve;
m.zones.push_back(z);
const PerformanceMap back = deserializePerformance(serializePerformance(m));
CHECK(back.zones.size() == 1);
if (back.zones.size() != 1) return;
CHECK(back.zones[0].loopOverride.has_value() &&
back.zones[0].loopOverride->start == 111 && back.zones[0].loopOverride->end == 222);
CHECK(back.zones[0].startPoint.has_value() && *back.zones[0].startPoint == 333);
CHECK(back.zones[0].play.adsr.holdFrames == 999);
CHECK(back.zones[0].play.pitchEngine == PitchEngine::Preserve);
}
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
// deliberate behavior change for already-saved instruments. Hand-build a v2 record exactly.
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);
};
u32(kPerformanceStateVersion); // envelope version (2)
u32(kZonesFormatMarker); // marker -> a versioned payload
u32(2); // PAYLOAD VERSION 2 (S11, no play tail)
u32(1); // zone count 1
const std::string id = "old";
u32(static_cast<std::uint32_t>(id.size()));
b.insert(b.end(), id.begin(), id.end());
u32(5); // lowNote
u32(80); // highNote
b.push_back(0); // hasRootOverride = 0
b.push_back(0); // hasLoopOverride = 0
b.push_back(0); // hasStartPoint = 0 (record ends here in v2)
const PerformanceMap back = deserializePerformance(b);
CHECK(back.zones.size() == 1);
if (back.zones.size() != 1) return;
CHECK(back.zones[0].sampleId == "old");
// Lifted to product defaults: Gate play mode, PRESERVE engine (the S16-F1 default), env off.
CHECK(back.zones[0].play.playMode == PlayMode::Gate);
CHECK(back.zones[0].play.pitchEngine == kDefaultPitchEngine); // == Preserve
CHECK(back.zones[0].play.pitchEnv.enabled == false);
CHECK(back.zones[0].play.adsr.holdFrames == 0);
}
static void testPlayParamsThroughComponentEnvelope() {
// The play params round-trip through the v4 COMPONENT envelope too (the composition property:
// the zones payload is envelope-independent, so v4 {channelMode, selection, zones} carries them).
ComponentState s;
s.selectionId = "pick";
s.channelMode = ChannelMode::Stereo;
PerformanceZone z = zone("z", 0, 127);
z.play.playMode = PlayMode::Trigger;
z.play.trigger.lengthFraction = 0.9;
z.play.pitchEngine = PitchEngine::Varispeed;
s.map.zones.push_back(z);
const ComponentState back = deserializeComponentState(serializeComponentState(s));
CHECK(back.channelMode == ChannelMode::Stereo);
CHECK(back.map.zones.size() == 1);
if (back.map.zones.size() != 1) return;
CHECK(back.map.zones[0].play.playMode == PlayMode::Trigger);
CHECK(back.map.zones[0].play.trigger.lengthFraction == 0.9);
CHECK(back.map.zones[0].play.pitchEngine == PitchEngine::Varispeed);
}
int main() {
testSelectByIdHit();
testSelectEmptyIdIsSilence();
@@ -1078,6 +1185,10 @@ int main() {
testPerformanceStateV1BackCompat();
testPerformanceStateGarbage();
testPerformanceStateNegativeNotesRoundTrip();
testPlayParamsRoundTrip();
testPlayParamsComposeWithLoopStart();
testPlayParamsV2BackCompatLiftsToDefaults();
testPlayParamsThroughComponentEnvelope();
testComponentStateRoundTrip();
testComponentStateLoopStartRoundTrip();
testComponentStateSelectionOnlyNoZones();