Decouple the instrument reload from VST3 activation, and make the master meter's accumulate exact
This commit is contained in:
@@ -96,10 +96,6 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
// retired-slot free is single-writer; never taken on the audio thread.
|
||||
std::lock_guard<std::mutex> lock(reloadMutex_);
|
||||
|
||||
// Mint this reload's generation number first so the built instrument is stamped
|
||||
// before publishing.
|
||||
const std::uint64_t gen = reloadGeneration_.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
|
||||
// 1. Self-contained resolution: the instance-owned refs table is the source of truth.
|
||||
// The live bank blob, when readable, is folded in first (refreshRefsFromBank — the
|
||||
// browser's copy-the-ref-in + recapture-sync mechanism), but its absence changes
|
||||
@@ -124,17 +120,6 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
// Governs how the WAV decodes (mono downmix vs 2-channel); auto-defaulted from the
|
||||
// capture's own channel count below, before the decode.
|
||||
ChannelMode mode = channelMode();
|
||||
// Snapshot the voice-system parameters once — baked into the built engine's
|
||||
// construction (immutable config; a later change rebuilds).
|
||||
int builtVoiceCount = kDefaultVoiceCount;
|
||||
VoiceMode builtVoiceMode = VoiceMode::Poly;
|
||||
MonoTrigger builtMonoTrigger = MonoTrigger::Retrigger;
|
||||
{
|
||||
std::lock_guard<std::mutex> vp(voiceParamsMutex_);
|
||||
builtVoiceCount = voiceCount_;
|
||||
builtVoiceMode = voiceMode_;
|
||||
builtMonoTrigger = monoTrigger_;
|
||||
}
|
||||
|
||||
std::string resolvedId;
|
||||
std::unique_ptr<LoadedInstrument> built;
|
||||
@@ -177,17 +162,7 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
}
|
||||
}
|
||||
|
||||
if (havePlayable) {
|
||||
// Preserve OLA window in output frames from the host rate (kPreserveWindowMs),
|
||||
// pre-sized here so process()-time note-on never allocates. Floored at 2 so a
|
||||
// valid window is always a real ring, covering a pathological host rate <= 0 too.
|
||||
std::int64_t preserveWindow = static_cast<std::int64_t>(
|
||||
kPreserveWindowMs * sampleRate_ / 1000.0 + 0.5);
|
||||
if (preserveWindow < 2) preserveWindow = 2;
|
||||
built = std::make_unique<LoadedInstrument>(
|
||||
std::move(sample), static_cast<std::size_t>(builtVoiceCount), gen,
|
||||
kPreserveVoiceCap, preserveWindow, builtVoiceMode, builtMonoTrigger);
|
||||
}
|
||||
if (havePlayable) built = buildInstrumentLocked(std::move(sample));
|
||||
|
||||
// 3. Publish: atomically install the new instrument via the drain-slot swap (see the
|
||||
// header). A null `built` (no ref / unreadable WAV) installs silence while any
|
||||
@@ -246,9 +221,10 @@ void ReaSamplerProcessor::adoptBakedCapture(const SampleRefEntry& entry,
|
||||
// ONE reload for the re-point and the reset together: it decodes the new file and
|
||||
// publishes the neutral parameters in the same swap.
|
||||
reloadInstrument();
|
||||
// The bake's reset may have flipped the limiter; deliver the host's latency restart here
|
||||
// rather than leaving it to the editor's tick, so an adopt is correct with no editor open.
|
||||
// At the tail for the same reason setState's is (see there).
|
||||
// The bake's reset may have flipped the limiter; delivering the restart here rather than
|
||||
// leaving it to the editor's tick is a LATENCY improvement, not a correctness one — the
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -301,6 +277,10 @@ void ReaSamplerProcessor::publishBuiltLocked(std::unique_ptr<LoadedInstrument> b
|
||||
return e->installedAt < seen;
|
||||
}),
|
||||
graveyard_.end());
|
||||
// Any publish supersedes the deactivate's park: whatever is installed here is the newer
|
||||
// truth, and a park surviving it would be reinstalled over this instrument at the next
|
||||
// activation (the setState-while-inactive case).
|
||||
dormantSample_.reset();
|
||||
LoadedInstrument* prev = live_.exchange(built.release());
|
||||
// A bake's reset gain lands here rather than at its call site, so the gain and the
|
||||
// capture it belongs to become audible to process() within one block of each other.
|
||||
@@ -317,6 +297,34 @@ void ReaSamplerProcessor::publishBuiltLocked(std::unique_ptr<LoadedInstrument> b
|
||||
if (evicted) graveyard_.push_back(std::unique_ptr<LoadedInstrument>(evicted));
|
||||
}
|
||||
|
||||
std::unique_ptr<LoadedInstrument>
|
||||
ReaSamplerProcessor::buildInstrumentLocked(SampleData sample) {
|
||||
// REQUIRES reloadMutex_ held. The ONE construction of a playable snapshot, so the three
|
||||
// callers (full reload, voice-param rebuild, reactivation) cannot drift on the generation
|
||||
// stamp, the voice-system snapshot or the ring size.
|
||||
int builtVoiceCount = kDefaultVoiceCount;
|
||||
VoiceMode builtVoiceMode = VoiceMode::Poly;
|
||||
MonoTrigger builtMonoTrigger = MonoTrigger::Retrigger;
|
||||
{
|
||||
// Baked into the engine's construction (immutable config; a later change rebuilds).
|
||||
std::lock_guard<std::mutex> vp(voiceParamsMutex_);
|
||||
builtVoiceCount = voiceCount_;
|
||||
builtVoiceMode = voiceMode_;
|
||||
builtMonoTrigger = monoTrigger_;
|
||||
}
|
||||
// Preserve OLA window in output frames from the host rate (kPreserveWindowMs), pre-sized
|
||||
// here so process()-time note-on never allocates. Floored at 2 so a valid window is always
|
||||
// a real ring, covering a pathological host rate <= 0 too. Re-derived per build, so a
|
||||
// reactivation after the host changed its rate gets a ring sized for the new one.
|
||||
std::int64_t preserveWindow = static_cast<std::int64_t>(
|
||||
kPreserveWindowMs * sampleRate_ / 1000.0 + 0.5);
|
||||
if (preserveWindow < 2) preserveWindow = 2;
|
||||
const std::uint64_t gen = reloadGeneration_.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
return std::make_unique<LoadedInstrument>(
|
||||
std::move(sample), static_cast<std::size_t>(builtVoiceCount), gen,
|
||||
kPreserveVoiceCap, preserveWindow, builtVoiceMode, builtMonoTrigger);
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::rebuildVoiceEngine() {
|
||||
// Off the audio thread. A voice-param change touches no audio data, so this rebuilds
|
||||
// the engine around a copy of the live instrument's already-decoded SampleData — no
|
||||
@@ -324,31 +332,25 @@ void ReaSamplerProcessor::rebuildVoiceEngine() {
|
||||
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 reloadInstrument.
|
||||
std::int64_t preserveWindow = static_cast<std::int64_t>(
|
||||
kPreserveWindowMs * sampleRate_ / 1000.0 + 0.5);
|
||||
if (preserveWindow < 2) preserveWindow = 2;
|
||||
|
||||
// Deep-copy the decoded sample: safe to read concurrently with process() because the
|
||||
// SampleData is immutable after construction and reloadMutex_ prevents `cur` from being
|
||||
// freed.
|
||||
SampleData sample = cur->sample;
|
||||
auto built = std::make_unique<LoadedInstrument>(
|
||||
std::move(sample), static_cast<std::size_t>(builtVoiceCount), gen,
|
||||
kPreserveVoiceCap, preserveWindow, builtVoiceMode, builtMonoTrigger);
|
||||
publishBuiltLocked(std::move(built));
|
||||
publishBuiltLocked(buildInstrumentLocked(cur->sample));
|
||||
}
|
||||
|
||||
bool ReaSamplerProcessor::resumeDormantInstrument() {
|
||||
// Off the audio thread (setActive only). The reactivation half of the lifetime split: the
|
||||
// voice state the deactivate destroyed is rebuilt, the PCM it parked is reused as-is.
|
||||
std::lock_guard<std::mutex> lock(reloadMutex_);
|
||||
// A publish that landed while inactive (setState's reload, a bake's adopt) IS the
|
||||
// activation state — its voices have never rendered, and it has already superseded the
|
||||
// park. Rebuilding here would displace a correct instrument into the drain slot.
|
||||
if (live_.load(std::memory_order_acquire)) return true;
|
||||
if (!dormantSample_) return false;
|
||||
// MOVED, not copied: the park exists for this one handoff, and keeping it would hold a
|
||||
// second copy of the PCM for the whole active lifetime.
|
||||
publishBuiltLocked(buildInstrumentLocked(std::move(*dormantSample_)));
|
||||
return true;
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::retireIdleDrain() {
|
||||
|
||||
Reference in New Issue
Block a user