Service both VST3 parameter channels, and promote pitch key-track and Trigger length so all 44 ids issue

The SDK's own single-component sample drains inputParameterChanges in
process() and implements setParamNormalized; automation was reading the
GUI channel alone. The audio thread now patches a block it solely owns.
This commit is contained in:
2026-08-02 16:22:17 -04:00
parent bfaa0f2614
commit de5654fb6f
44 changed files with 1205 additions and 302 deletions
+35 -11
View File
@@ -91,20 +91,26 @@ tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) {
legacyLiftConcluded_.store(false, std::memory_order_relaxed);
reloadInstrument();
paramNotifySuppressed_ = false;
// Every exposed parameter now reads the blob's value. Ordering against the host's first
// parameter block is irrelevant BY CONSTRUCTION rather than by assumption: there is one
// model and one funnel per control, so whichever of the two writes last simply wins, and
// the host's display follows the model either way.
// Every exposed parameter now reads the blob's value. Its ordering against the host's first
// parameter block does not need to be known: an automation point held by the audio thread is
// re-applied over every merge, so a lane outranks this restore whichever way round the two
// arrive. That is VST3's own rule — a written lane outranks anything the plug-in sets — not
// a race we lost.
syncParamsFromModel();
// This caller has no editor to flush for it. At the TAIL on purpose: a host that services the
// restart synchronously deactivates/reactivates, and our setActive(true) resumes or reloads
// against the refs above, which are only fully restored once this function has run to here.
flushLatencyRestart();
flushGainNotify();
return kResultOk;
}
tresult PLUGIN_API ReaSamplerProcessor::getState(IBStream* state) {
if (!state) return kResultFalse;
// Before the snapshot, not after: the blob is authoritative, so anything the host's
// automation wrote must be in the model by the time it is serialised. This is the one drain
// site that is not an optimisation — a save with no editor open still has to see it.
drainAutomationToModel();
// Persists the full instance state — never written to the "reasampler" bank ext-state.
// No pick serializes to {"", default params}, restoring as silence (never auto-playing
// sample #1).
@@ -163,10 +169,15 @@ InstrumentParams ReaSamplerProcessor::instrumentParams() {
}
void ReaSamplerProcessor::setInstrumentParams(const InstrumentParams& params) {
InstrumentParams before;
// The exposed values alone, not the whole set: this funnel fires per mouse move on every live
// knob and node drag, and InstrumentParams owns seven vectors — copying all of them to diff
// 44 doubles is the cost, and the diff is what the notification actually needs.
double before[kDeckParamSlots];
{
std::lock_guard<std::mutex> lock(paramsMutex_);
before = params_;
for (const instrument::param::ParamRow& row : instrument::param::exposedParams()) {
before[static_cast<std::size_t>(row.deck)] = modelParamNormalized(params_, row.deck);
}
params_ = params;
}
// Every writer of the parameter set — setState, the editor's commits, the bake's adopt —
@@ -250,8 +261,9 @@ void ReaSamplerProcessor::clearMasterBusClip() {
void ReaSamplerProcessor::publishLiveParams() {
const int rate = builtSampleRate_.load(std::memory_order_relaxed);
if (rate <= 0) return;
const InstrumentParams params = instrumentParams();
const instrument::engine::LiveValues block =
instrument::engine::foldLive(resolvePlay(instrumentParams().play, rate));
instrument::engine::foldLive(resolvePlay(params.play, rate), params.keyTrack);
// livePublishMutex_ enforces the seqlock's single-writer contract (live_params.h) against
// reloadInstrument's publish — held for the publish call only, not the fold above.
std::lock_guard<std::mutex> lock(livePublishMutex_);
@@ -329,20 +341,32 @@ void ReaSamplerProcessor::setMonoTrigger(MonoTrigger trigger) {
rebuildVoiceEngine();
}
void ReaSamplerProcessor::setMasterGainLinear(double linear) {
bool ReaSamplerProcessor::publishMasterGainLinear(double linear) {
// Clamp to the master_gain taper (0 = silence, cap = +24 dB). One relaxed atomic
// store — no rebuild, no lock (a post-sum trim is not a keymap fact).
if (!(linear >= 0.0)) linear = 0.0; // also catches NaN
const double maxLin = masterGainMaxLinear();
if (linear > maxLin) linear = maxLin;
const float value = static_cast<float>(linear);
const float previous = masterGain_.exchange(value, std::memory_order_relaxed);
return masterGain_.exchange(value, std::memory_order_relaxed) != value;
}
void ReaSamplerProcessor::setMasterGainLinear(double linear) {
const bool moved = publishMasterGainLinear(linear);
// Gain's own notification funnel — it is the one exposed control that does not ride the
// parameter set, so setInstrumentParams' diff cannot see it. Compared for a real change so a
// reload's republish of an unmoved gain writes nothing into a host's automation lane.
if (paramNotifySuppressed_ || previous == value) return;
if (paramNotifySuppressed_ || !moved) return;
notifyParamChanged(instrument::param::kParamMasterGain,
instrument::engine::masterGainNormFromLinear(linear));
instrument::engine::masterGainNormFromLinear(masterGainLinear()));
}
void ReaSamplerProcessor::flushGainNotify() {
if (!componentHandler) return; // an arm raised before the handler connected waits
if (!gainNotifyPending_.exchange(false, std::memory_order_acquire)) return;
if (paramNotifySuppressed_) return;
notifyParamChanged(instrument::param::kParamMasterGain,
instrument::engine::masterGainNormFromLinear(masterGainLinear()));
}
void ReaSamplerProcessor::previewNoteOn(int note) {