Merge pS-w1-t3-state: persist preview-trigger velocity as ComponentState v6 (S-VIEW-4 core)

This commit is contained in:
2026-07-27 13:39:09 -04:00
5 changed files with 154 additions and 19 deletions
+85
View File
@@ -1026,6 +1026,85 @@ static void testComponentStateV5TruncatedMarker() {
CHECK(back.selectionId.empty() && back.map.zones.empty());
}
// --- v6 component state: the S-VIEW-4 preview-trigger velocity ------------------
static void testComponentStatePreviewVelocityRoundTrip() {
// The preview velocity round-trips through the v6 envelope alongside selection + mode + marker
// + zones. A non-default value (not 64) proves the byte is actually read back, not defaulted.
ComponentState s;
s.selectionId = "pick";
s.channelMode = ChannelMode::Stereo;
s.lastConsumedAssignGeneration = 1700000123456LL;
s.previewVelocity = 111; // non-default
s.map.zones.push_back(zone("z0", 0, 127, /*override=*/std::nullopt));
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
CHECK(back.previewVelocity == 111); // velocity survives
CHECK(back.channelMode == ChannelMode::Stereo); // envelope neighbours intact
CHECK(back.lastConsumedAssignGeneration == 1700000123456LL);
CHECK(back.selectionId == "pick");
CHECK(back.map.zones.size() == 1 && back.map.zones[0].sampleId == "z0");
}
static void testComponentStateDefaultPreviewVelocityIsMid() {
// A default-constructed state carries the mid velocity default and round-trips it.
const ComponentState back = deserializeComponentState(serializeComponentState(ComponentState{}), 44100.0);
CHECK(back.previewVelocity == kPreviewVelocityDefault);
CHECK(kPreviewVelocityDefault == 64);
}
static void testComponentStatePreviewVelocityExtremes() {
// The full MIDI velocity range round-trips: 1 (softest audible) and 127 (max) both survive the
// single-byte field without clamping or overflow.
for (std::uint8_t v : {std::uint8_t{1}, std::uint8_t{127}}) {
ComponentState s;
s.previewVelocity = v;
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
CHECK(back.previewVelocity == v);
}
}
static void testComponentStateV5LiftsVelocityToMid() {
// A GENUINE v5 blob (version tag 5: mode byte, 8-byte marker, then id + zones — NO velocity
// byte) must lift previewVelocity to kPreviewVelocityDefault, its mode/marker/selection/zones
// intact. Build it by hand (serializeComponentState now emits v6, so it cannot make a v5 blob).
// This proves an already-saved pre-S-VIEW-4 instance restores at the mid default.
std::vector<std::uint8_t> v5;
v5.push_back(5); v5.push_back(0); v5.push_back(0); v5.push_back(0); // version 5
v5.push_back(1); // channel mode = stereo
for (int i = 0; i < 8; ++i) v5.push_back(0); // marker = 0
const std::string id = "saved";
v5.push_back(static_cast<std::uint8_t>(id.size())); v5.push_back(0); v5.push_back(0); v5.push_back(0);
v5.insert(v5.end(), id.begin(), id.end());
v5.push_back(0); v5.push_back(0); v5.push_back(0); v5.push_back(0); // zone count 0
const ComponentState back = deserializeComponentState(v5, 44100.0);
CHECK(back.previewVelocity == kPreviewVelocityDefault); // no velocity byte in v5 -> mid default
CHECK(back.channelMode == ChannelMode::Stereo); // v5 mode byte honored
CHECK(back.selectionId == "saved");
CHECK(back.map.zones.empty());
}
static void testComponentStateV4LiftsVelocityToMid() {
// A pre-S8/S9 v4 blob (mode byte, then id + zones — no marker, no velocity) also lifts
// previewVelocity to the mid default. Proves the older-than-v5 lift path defaults the field too.
std::vector<std::uint8_t> v4;
v4.push_back(4); v4.push_back(0); v4.push_back(0); v4.push_back(0); // version 4
v4.push_back(0); // channel mode = mono
v4.push_back(0); v4.push_back(0); v4.push_back(0); v4.push_back(0); // id length 0
v4.push_back(0); v4.push_back(0); v4.push_back(0); v4.push_back(0); // zone count 0
const ComponentState back = deserializeComponentState(v4, 44100.0);
CHECK(back.previewVelocity == kPreviewVelocityDefault);
}
static void testComponentStateV6TruncatedVelocity() {
// A v6 blob truncated inside the header before the velocity byte (mode + full marker present,
// velocity byte cut) -> empty, with the mid velocity default holding (bounded read, never throws).
std::vector<std::uint8_t> t{6, 0, 0, 0, 1}; // version 6, mode byte
for (int i = 0; i < 8; ++i) t.push_back(0); // full marker, no velocity byte
const ComponentState back = deserializeComponentState(t, 44100.0);
CHECK(back.previewVelocity == kPreviewVelocityDefault);
CHECK(back.selectionId.empty() && back.map.zones.empty());
}
// --- MERGE COMPOSITION (S9 v5 marker envelope x S15/S16 v3 play-param payload) ----------------
//
// The merge of ps-w9-t1-sync (envelope v5, adds the consumed-assignment marker) and
@@ -1574,6 +1653,12 @@ int main() {
testComponentStateDefaultMarkerIsZero();
testComponentStateV4LiftsMarkerToZero();
testComponentStateV5TruncatedMarker();
testComponentStatePreviewVelocityRoundTrip();
testComponentStateDefaultPreviewVelocityIsMid();
testComponentStatePreviewVelocityExtremes();
testComponentStateV5LiftsVelocityToMid();
testComponentStateV4LiftsVelocityToMid();
testComponentStateV6TruncatedVelocity();
testV5EnvelopeWithMarkerAndPlayParamsRoundTrip();
testV4BlobWithPlayParamsLiftsMarkerZeroKeepsPlay();