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
+19 -7
View File
@@ -146,16 +146,18 @@ std::string ReaSamplerProcessor::reloadInstrument() {
buildFromRef(*sel, params, projectDir, mode)) {
sample = std::move(*decoded);
havePlayable = true;
// Point the built snapshot at the instance's ONE live block and seed it from
// the very PlayParams the voices latch, so an untouched knob folds to the same
// frames the build resolved and a note-on with a live block sounds identical
// to one without.
sample.live = &liveParams_;
// Point the built snapshot at the instance's ONE engine-facing block and seed the
// MODEL block from the very PlayParams the voices latch, so an untouched knob folds
// to the same frames the build resolved and a note-on with a live block sounds
// identical to one without. process() merges the seed into automationLive_ at the
// top of the first block after this, which is where the swap below becomes visible
// too — so the new snapshot's first note reads it.
sample.live = &automationLive_;
{
// reloadMutex_ (held for this whole function) nests livePublishMutex_ here;
// publishLiveParams never holds reloadMutex_, so this is the only nesting.
std::lock_guard<std::mutex> lp(livePublishMutex_);
liveParams_.publish(instrument::engine::foldLive(sample.play));
liveParams_.publish(instrument::engine::foldLive(sample.play, sample.keyTrack));
}
builtSampleRate_.store(sample.sampleRate, std::memory_order_relaxed);
resolvedId = selId; // the concrete pick that resolved
@@ -226,6 +228,8 @@ void ReaSamplerProcessor::adoptBakedCapture(const SampleRefEntry& entry,
// bake chain only ever runs from that tick, so the arm would be drained on the next one
// anyway. At the tail for the same reason setState's is (see there).
flushLatencyRestart();
// The reset's gain notification, armed under reloadMutex_ inside that reload.
flushGainNotify();
}
void ReaSamplerProcessor::publishUsage(const SampleRefs& refs,
@@ -289,8 +293,16 @@ void ReaSamplerProcessor::publishBuiltLocked(std::unique_ptr<LoadedInstrument> b
// gain sitting above every snapshot, the same shape as ONE BLOCK, ONE RATE (see
// builtSampleRate_).
if (gainAtNextPublish_) {
setMasterGainLinear(*gainAtNextPublish_);
// The MIRROR is inline (that is the sound this publish belongs to); the host
// notification is ARMED and delivered after reloadMutex_ is released. performEdit
// reaches the host handler, a host may re-enter this object synchronously from it, and
// setActive(false) takes this same non-recursive mutex — the identical hazard the
// latency restart is deferred for.
const bool moved = publishMasterGainLinear(*gainAtNextPublish_);
gainAtNextPublish_.reset();
// Armed only on a real change, so a reset that lands on the gain already set writes
// nothing into a host's automation lane — the same compare setMasterGainLinear makes.
if (moved) gainNotifyPending_.store(true, std::memory_order_release);
}
LoadedInstrument* evicted = draining_.exchange(prev);
if (evicted) graveyard_.push_back(std::unique_ptr<LoadedInstrument>(evicted));