From 5db620be78ef403b3534ff8ce8367408dc6f26f8 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Mon, 27 Jul 2026 13:34:39 -0400 Subject: [PATCH] fix(processor): close S-VIEW-4 previewVelocity persistence loop Add previewVelocity_ member to ReaSamplerProcessor; populate it in getState and restore it in setState so the field round-trips in the plugin, not just in the pure unit test. Clamp 1..127 at the deserialize read boundary in sample_map.cpp. --- src/vst/reasampler_processor.cpp | 4 ++++ src/vst/reasampler_processor.h | 6 ++++++ src/vst/sample_map.cpp | 6 +++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/vst/reasampler_processor.cpp b/src/vst/reasampler_processor.cpp index c14f7ce..5d65efd 100644 --- a/src/vst/reasampler_processor.cpp +++ b/src/vst/reasampler_processor.cpp @@ -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 lock(assignMarkerMutex_); state_out.lastConsumedAssignGeneration = lastConsumedAssignGeneration_; // S8 reader marker } + state_out.previewVelocity = previewVelocity_; // S-VIEW-4: persist the preview strike velocity const std::vector bytes = serializeComponentState(state_out); if (!bytes.empty()) { const tresult wr = state->write(const_cast(bytes.data()), diff --git a/src/vst/reasampler_processor.h b/src/vst/reasampler_processor.h index fa7ff2e..f68fe7d 100644 --- a/src/vst/reasampler_processor.h +++ b/src/vst/reasampler_processor.h @@ -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. diff --git a/src/vst/sample_map.cpp b/src/vst/sample_map.cpp index 0346171..6c4b97e 100644 --- a/src/vst/sample_map.cpp +++ b/src/vst/sample_map.cpp @@ -628,7 +628,11 @@ ComponentState deserializeComponentState(const std::vector& bytes, 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) - out.previewVelocity = previewVel; + // 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