fix(voice): Trigger legato keys on held-stack depth, CC123 allNotesOff clears mono stack, voice-param edits rebuild from decoded PCM (no re-decode), mono sizes 1 shifter ring
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
#include "pluginterfaces/vst/ivstaudioprocessor.h"
|
||||
#include "pluginterfaces/vst/ivsteditcontroller.h" // RestartFlags::kIoChanged (S7 re-negotiate)
|
||||
#include "pluginterfaces/vst/ivstevents.h"
|
||||
#include "pluginterfaces/vst/ivstmidicontrollers.h" // kCtrlAllNotesOff / kCtrlAllSoundsOff (panic)
|
||||
#include "pluginterfaces/vst/vstspeaker.h"
|
||||
|
||||
#include "public.sdk/source/vst/vstbus.h" // Vst::AudioBus::setArrangement (S7 output arr)
|
||||
@@ -314,10 +315,12 @@ void ReaSamplerProcessor::setVoiceCount(int count) {
|
||||
if (voiceCount_ == count) return; // no-op: don't churn a rebuild
|
||||
voiceCount_ = count;
|
||||
}
|
||||
// Rebuild the engine OFF-thread through the drain-slot swap (the FA1 machinery): the
|
||||
// displaced instrument keeps rendering its ringing tails, so a polyphony change never
|
||||
// cuts a sounding note. Same contract for the mode/trigger setters below.
|
||||
reloadFromBank();
|
||||
// LIGHT rebuild OFF-thread through the drain-slot swap: the engine is reconstructed from
|
||||
// the already-decoded keymap (no bridge re-read, no WAV re-decode — a polyphony change
|
||||
// touches no audio data) and the displaced instrument keeps rendering its ringing tails,
|
||||
// so a voice-param change never cuts a sounding note NOR stalls the UI re-decoding every
|
||||
// zone from disk. Same contract for the mode/trigger setters below.
|
||||
rebuildVoiceEngine();
|
||||
}
|
||||
|
||||
VoiceMode ReaSamplerProcessor::voiceMode() {
|
||||
@@ -331,7 +334,7 @@ void ReaSamplerProcessor::setVoiceMode(VoiceMode mode) {
|
||||
if (voiceMode_ == mode) return;
|
||||
voiceMode_ = mode;
|
||||
}
|
||||
reloadFromBank();
|
||||
rebuildVoiceEngine();
|
||||
}
|
||||
|
||||
MonoTrigger ReaSamplerProcessor::monoTrigger() {
|
||||
@@ -345,7 +348,7 @@ void ReaSamplerProcessor::setMonoTrigger(MonoTrigger trigger) {
|
||||
if (monoTrigger_ == trigger) return;
|
||||
monoTrigger_ = trigger;
|
||||
}
|
||||
reloadFromBank();
|
||||
rebuildVoiceEngine();
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::previewNoteOn(int note) {
|
||||
@@ -525,12 +528,20 @@ std::string ReaSamplerProcessor::reloadFromBank() {
|
||||
// `built` is heap-owned; release() hands ownership to the atomic; the drain-evicted
|
||||
// pointer is re-owned by the graveyard.
|
||||
//
|
||||
// Bounded reclaim: free graveyard entries whose installedAt < seen, where seen is
|
||||
// the minimum installedAt process() published over the pointers it holds. Both
|
||||
// slots are monotone in installedAt, so seen is monotone and any future process()
|
||||
// load yields installedAt >= seen — an entry below seen is provably unreachable
|
||||
// (see the header proof). Remaining entries drain at setActive(false) / terminate()
|
||||
// when process is guaranteed stopped.
|
||||
publishBuiltLocked(std::move(built));
|
||||
return resolvedId;
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::publishBuiltLocked(std::unique_ptr<LoadedInstrument> built) {
|
||||
// REQUIRES reloadMutex_ held (single-writer over both slots + the graveyard). Shared by
|
||||
// reloadFromBank and rebuildVoiceEngine — the one safety-critical swap dance.
|
||||
//
|
||||
// Bounded reclaim: free graveyard entries whose installedAt < seen, where seen is
|
||||
// the minimum installedAt process() published over the pointers it holds. Both
|
||||
// slots are monotone in installedAt, so seen is monotone and any future process()
|
||||
// load yields installedAt >= seen — an entry below seen is provably unreachable
|
||||
// (see the header proof). Remaining entries drain at setActive(false) / terminate()
|
||||
// when process is guaranteed stopped.
|
||||
const std::uint64_t seen = processGeneration_.load(std::memory_order_acquire);
|
||||
graveyard_.erase(
|
||||
std::remove_if(graveyard_.begin(), graveyard_.end(),
|
||||
@@ -541,7 +552,40 @@ std::string ReaSamplerProcessor::reloadFromBank() {
|
||||
LoadedInstrument* prev = live_.exchange(built.release());
|
||||
LoadedInstrument* evicted = draining_.exchange(prev);
|
||||
if (evicted) graveyard_.push_back(std::unique_ptr<LoadedInstrument>(evicted));
|
||||
return resolvedId;
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::rebuildVoiceEngine() {
|
||||
// OFF THE AUDIO THREAD (the editor's voice-deck click handlers). See the header contract:
|
||||
// a voice-param change touches NO audio data, so this rebuilds the engine + preview card
|
||||
// around a COPY of the live instrument's already-decoded keymap — no bridge, no disk —
|
||||
// and publishes through the same drain-slot swap, so ringing tails survive.
|
||||
std::lock_guard<std::mutex> lock(reloadMutex_);
|
||||
LoadedInstrument* cur = live_.load(std::memory_order_acquire);
|
||||
if (!cur) return; // nothing loaded: the new params bake into the next real reload.
|
||||
|
||||
int builtVoiceCount = kDefaultVoiceCount;
|
||||
VoiceMode builtVoiceMode = VoiceMode::Poly;
|
||||
MonoTrigger builtMonoTrigger = MonoTrigger::Retrigger;
|
||||
{
|
||||
std::lock_guard<std::mutex> vp(voiceParamsMutex_);
|
||||
builtVoiceCount = voiceCount_;
|
||||
builtVoiceMode = voiceMode_;
|
||||
builtMonoTrigger = monoTrigger_;
|
||||
}
|
||||
|
||||
const std::uint64_t gen = reloadGeneration_.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
// Same Preserve-window derivation as reloadFromBank (kPreserveWindowMs at the host rate).
|
||||
std::int64_t preserveWindow = static_cast<std::int64_t>(
|
||||
kPreserveWindowMs * sampleRate_ / 1000.0 + 0.5);
|
||||
if (preserveWindow < 2) preserveWindow = 2;
|
||||
|
||||
// Deep-copy the decoded PCM + zones. Safe to read concurrently with process(): the keymap
|
||||
// is immutable after construction, and under reloadMutex_ nobody can free `cur`.
|
||||
Keymap km = cur->keymap;
|
||||
auto built = std::make_unique<LoadedInstrument>(
|
||||
std::move(km), static_cast<std::size_t>(builtVoiceCount), gen,
|
||||
kPreserveVoiceCap, preserveWindow, builtVoiceMode, builtMonoTrigger);
|
||||
publishBuiltLocked(std::move(built));
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::retireIdleDrain() {
|
||||
@@ -721,6 +765,26 @@ tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
||||
} else if (e.type == Event::kNoteOffEvent) {
|
||||
if (inst) inst->engine.noteOff(e.noteOff.pitch);
|
||||
if (drain) drain->engine.noteOff(e.noteOff.pitch);
|
||||
} else if (e.type == Event::kLegacyMIDICCOutEvent) {
|
||||
// PANIC (Phase S voice-review Major #2): CC 123 (All Notes Off) / CC 120 (All
|
||||
// Sound Off) reset the engine — clear the mono held stack and release every
|
||||
// voice, live AND drain, engine AND preview card — so a phantom held-stack
|
||||
// entry left by a lost note-off can never be resurrected by the mono fallback
|
||||
// and sustain forever. REAPER delivers raw input MIDI CC to a VST3 instrument
|
||||
// as kLegacyMIDICCOut events on the INPUT event list (a REAPER-ism — the type
|
||||
// is nominally an output event; DAW-verify, see handoff). allNotesOff /
|
||||
// releaseAll are RT-safe (no allocation, bounded scans).
|
||||
const auto cc = static_cast<int>(e.midiCCOut.controlNumber);
|
||||
if (cc == kCtrlAllNotesOff || cc == kCtrlAllSoundsOff) {
|
||||
if (inst) {
|
||||
inst->engine.allNotesOff();
|
||||
inst->preview.releaseAll();
|
||||
}
|
||||
if (drain) {
|
||||
drain->engine.allNotesOff();
|
||||
drain->preview.releaseAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user