Merge pS-w1-t3-state: persist preview-trigger velocity as ComponentState v6 (S-VIEW-4 core)
This commit is contained in:
@@ -197,6 +197,9 @@ tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) {
|
||||
channelMode_ = cs.channelMode;
|
||||
}
|
||||
applyOutputArrangement(cs.channelMode);
|
||||
// S-VIEW-4: restore the per-instance preview velocity. No mutex — setState is a load-time
|
||||
// call serialized by the host; there is no concurrent writer before Wave 2.
|
||||
previewVelocity_ = cs.previewVelocity;
|
||||
// Rebuild from the restored state (off-thread — setState is a load-time call).
|
||||
reloadFromBank();
|
||||
return kResultOk;
|
||||
@@ -217,6 +220,7 @@ tresult PLUGIN_API ReaSamplerProcessor::getState(IBStream* state) {
|
||||
std::lock_guard<std::mutex> lock(assignMarkerMutex_);
|
||||
state_out.lastConsumedAssignGeneration = lastConsumedAssignGeneration_; // S8 reader marker
|
||||
}
|
||||
state_out.previewVelocity = previewVelocity_; // S-VIEW-4: persist the preview strike velocity
|
||||
const std::vector<std::uint8_t> bytes = serializeComponentState(state_out);
|
||||
if (!bytes.empty()) {
|
||||
const tresult wr = state->write(const_cast<std::uint8_t*>(bytes.data()),
|
||||
|
||||
@@ -250,6 +250,12 @@ private:
|
||||
// NOT read on the audio thread.
|
||||
std::int64_t lastSeenBankGeneration_ = -1;
|
||||
|
||||
// S-VIEW-4 preview-trigger velocity (MIDI 1..127). Persisted in component state (v6) so the
|
||||
// user's chosen strike velocity survives a project save/reload. No concurrent writer before
|
||||
// Wave 2 (no editor knob yet) — setState and getState are the sole accessors, both on the
|
||||
// load/save thread (host-serialized). Default kPreviewVelocityDefault (64).
|
||||
std::uint8_t previewVelocity_ = kPreviewVelocityDefault;
|
||||
|
||||
// Latched from setupProcessing so setActive/reload can size against it. Read
|
||||
// off-thread only. 0.0 is explicitly invalid — setupProcessing sets the real host rate
|
||||
// before any audio, and reloadFromBank guards on it before use.
|
||||
|
||||
+29
-3
@@ -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
|
||||
|
||||
+30
-16
@@ -405,21 +405,23 @@ PerformanceMap deserializePerformance(const std::vector<std::uint8_t>& bytes,
|
||||
// instance with NO pick and NO zones restores EMPTY (silence + the "pick a capture" empty
|
||||
// state), never auto-playing sample #1.
|
||||
//
|
||||
// Format (envelope v5): 4-byte LE version tag (== 5), then a 1-byte channel-mode field (0 = mono,
|
||||
// 1 = stereo), then an 8-byte LE last-consumed-assignment generation (S8/S9 reader marker),
|
||||
// then a 4-byte LE selection-id length + id bytes, then the CURRENT zones payload (identical to
|
||||
// serializePerformance's body — its own self-describing version, see the ZONES-PAYLOAD block).
|
||||
// The 8-byte marker is the ONLY envelope-v5 addition over envelope-v4 — the envelope grew a field,
|
||||
// the zones payload is untouched (a PARALLEL track owns zone-record extension under its own
|
||||
// versioning; the two version numbers are independent axes). BACK-COMPAT on
|
||||
// read (every older blob lifts to channelMode = MONO and lastConsumedAssignGeneration = 0,
|
||||
// preserving current behavior for already-saved instances):
|
||||
// * v5 blob -> {channelMode, lastConsumedAssignGeneration, selectionId, zones} direct.
|
||||
// * v4 blob -> {channelMode, 0, selectionId, zones}: pre-S8/S9 reader (no marker).
|
||||
// * v3 blob -> {mono, 0, selectionId, zones}: pre-S7 had no channel mode.
|
||||
// * v2 blob -> {mono, 0, "", zones}: an S5 instance had zones but no separate selection.
|
||||
// * v1 blob -> {mono, 0, id, one full-keyboard zone}: the S4 single-selection lift.
|
||||
// * empty/unknown -> {mono, 0, "", no zones}: EMPTY (the S10 silent empty state).
|
||||
// Format (envelope v6): 4-byte LE version tag (== 6), then a 1-byte channel-mode field (0 = mono,
|
||||
// 1 = stereo), then an 8-byte LE last-consumed-assignment generation (S8/S9 reader marker), then a
|
||||
// 1-byte preview-trigger velocity (S-VIEW-4, MIDI 1..127), then a 4-byte LE selection-id length +
|
||||
// id bytes, then the CURRENT zones payload (identical to serializePerformance's body — its own
|
||||
// self-describing version, see the ZONES-PAYLOAD block). The 1-byte preview velocity is the ONLY
|
||||
// envelope-v6 addition over envelope-v5 — the envelope grew a field, the zones payload is untouched
|
||||
// (a PARALLEL track owns zone-record extension under its own versioning; the two version numbers
|
||||
// are independent axes). BACK-COMPAT on read (every older blob lifts to channelMode = MONO,
|
||||
// lastConsumedAssignGeneration = 0, and previewVelocity = kPreviewVelocityDefault, preserving
|
||||
// current behavior for already-saved instances):
|
||||
// * v6 blob -> {channelMode, lastConsumedAssignGeneration, previewVelocity, selectionId, zones} direct.
|
||||
// * v5 blob -> {channelMode, lastConsumedAssignGeneration, mid, selectionId, zones}: pre-S-VIEW-4 (no velocity).
|
||||
// * v4 blob -> {channelMode, 0, mid, selectionId, zones}: pre-S8/S9 reader (no marker).
|
||||
// * v3 blob -> {mono, 0, mid, selectionId, zones}: pre-S7 had no channel mode.
|
||||
// * v2 blob -> {mono, 0, mid, "", zones}: an S5 instance had zones but no separate selection.
|
||||
// * v1 blob -> {mono, 0, mid, id, one full-keyboard zone}: the S4 single-selection lift.
|
||||
// * empty/unknown -> {mono, 0, mid, "", no zones}: EMPTY (the S10 silent empty state).
|
||||
//
|
||||
// WHY THE MARKER PERSISTS (S8 reader requirement). The last-consumed assignment generation is
|
||||
// the disambiguator that stops a re-opened instance re-applying a stale assign_request the user
|
||||
@@ -428,14 +430,26 @@ PerformanceMap deserializePerformance(const std::vector<std::uint8_t>& bytes,
|
||||
// bank_sync::consumeDecision). A fresh instance defaults to 0, so a genuinely new first assign
|
||||
// (generation >= 1) still applies. It is the instrument's OWN state (D-B), never written to the
|
||||
// bank — the extension owns the assign_request key; the instrument only tracks what it consumed.
|
||||
// The preview-trigger velocity default (S-VIEW-4): a mid MIDI velocity. An older blob with no
|
||||
// velocity byte lifts to this, and a fresh instance starts here — an audible-but-not-hot default.
|
||||
inline constexpr std::uint8_t kPreviewVelocityDefault = 64;
|
||||
|
||||
struct ComponentState {
|
||||
std::string selectionId; // the single-capture pick; "" = no pick
|
||||
PerformanceMap map; // the opt-in zones; empty = no zones
|
||||
ChannelMode channelMode = ChannelMode::Mono; // S7 output mode; default mono (D-E)
|
||||
std::int64_t lastConsumedAssignGeneration = 0; // S8/S9: last assign_request generation consumed
|
||||
// S-VIEW-4 preview-trigger velocity (MIDI 1..127): a PER-INSTANCE performance choice (sibling
|
||||
// of channelMode, NOT per-zone), persisted so the Sample-view preview button retains the user's
|
||||
// chosen strike velocity across saves. Defaults to kPreviewVelocityDefault.
|
||||
std::uint8_t previewVelocity = kPreviewVelocityDefault;
|
||||
};
|
||||
|
||||
inline constexpr std::uint32_t kComponentStateVersion = 5;
|
||||
inline constexpr std::uint32_t kComponentStateVersion = 6;
|
||||
|
||||
// The pre-S-VIEW-4 combined-state version (selection + zones + channel mode + consumed marker, no
|
||||
// preview velocity). Retained so deserializeComponentState can lift a v5 blob to a mid velocity.
|
||||
inline constexpr std::uint32_t kSelectionZonesModeMarkerV5Version = 5;
|
||||
|
||||
// The pre-S8/S9-reader combined-state version (selection + zones + channel mode, no consumed
|
||||
// marker). Retained so deserializeComponentState can lift a v4 blob to {mode, 0, sel, zones}.
|
||||
|
||||
Reference in New Issue
Block a user