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
+29 -3
View File
@@ -549,6 +549,9 @@ std::vector<std::uint8_t> serializeComponentState(const ComponentState& state) {
// two's-complement, precedes the selection id. Follows the mode byte so a v4 reader that
// stops at the mode byte is a strict prefix (see the v4 lift below).
putU64le(out, asU64(state.lastConsumedAssignGeneration));
// v6 envelope addition (S-VIEW-4): the preview-trigger velocity, 1 byte (MIDI 1..127). Follows
// the marker so a v5 blob is a strict prefix of a v6 blob up to this byte (see the v5 lift).
out.push_back(state.previewVelocity);
// Length-prefixed selection id (it precedes the zones payload, so it MUST be framed —
// unlike the v1 selection blob where the id ran to end-of-stream).
putU32le(out, static_cast<std::uint32_t>(state.selectionId.size()));
@@ -608,16 +611,39 @@ ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes,
readZonesPayload(r, out.map, projectRate);
return out; // marker stays 0 (pre-S8/S9 reader)
}
// BACK-COMPAT: a v5 blob (pre-S-VIEW-4 {mode, marker, selection, zones}, no preview-velocity
// byte): mode byte, then the 8-byte marker, then the id + zones body — no velocity byte.
// previewVelocity defaults to kPreviewVelocityDefault (set at construction), so an already-saved
// pre-S-VIEW-4 instance restores at the mid default.
if (version == kSelectionZonesModeMarkerV5Version) {
const std::uint8_t modeByte = r.u8();
if (!r.ok) return out; // truncated before the mode byte -> empty (mono default holds)
out.channelMode = (modeByte == 1) ? ChannelMode::Stereo : ChannelMode::Mono;
out.lastConsumedAssignGeneration = r.i64();
if (!r.ok) return out; // truncated before/inside the marker -> empty (marker 0 holds)
const std::uint32_t idLen = r.u32();
out.selectionId = r.str(idLen);
if (!r.ok) { out.selectionId.clear(); return out; } // truncated id -> empty
readZonesPayload(r, out.map, projectRate);
return out; // previewVelocity stays at the mid default (pre-S-VIEW-4)
}
if (version != kComponentStateVersion) return out; // unknown -> empty
// v5: the channel-mode byte, then the 8-byte consumed-assignment marker, precede the v3
// body. A non-{0,1} mode byte is treated as mono (conservative default) rather than
// rejected — a corrupt mode never silences the instance.
// v6: the channel-mode byte, then the 8-byte consumed-assignment marker, then the 1-byte
// preview velocity, precede the v3 body. A non-{0,1} mode byte is treated as mono
// (conservative default) rather than rejected — a corrupt mode never silences the instance.
const std::uint8_t modeByte = r.u8();
if (!r.ok) return out; // truncated before the mode byte -> empty (mono default holds)
out.channelMode = (modeByte == 1) ? ChannelMode::Stereo : ChannelMode::Mono;
out.lastConsumedAssignGeneration = r.i64();
if (!r.ok) return out; // truncated before/inside the marker -> empty (marker 0 holds)
const std::uint8_t previewVel = r.u8();
if (!r.ok) return out; // truncated before the velocity byte -> empty (mid default holds)
// Clamp to the documented MIDI 1..127 range: a 0 byte (or any out-of-spec value from a
// corrupt blob) falls back to the mid default rather than silencing the preview trigger.
out.previewVelocity = (previewVel >= 1 && previewVel <= 127)
? previewVel
: kPreviewVelocityDefault;
const std::uint32_t idLen = r.u32();
out.selectionId = r.str(idLen);
if (!r.ok) { out.selectionId.clear(); return out; } // truncated id -> empty