Bound the automation hold to the window the model has not caught up on, and make that authority model stated, enforced and tested
This commit is contained in:
@@ -17,13 +17,14 @@
|
||||
|
||||
#include "public.sdk/source/vst/vstsinglecomponenteffect.h"
|
||||
|
||||
#include "shell/instrument/automation_channel.h" // the automation hold + its release protocol
|
||||
#include "shell/instrument/processor_snapshot.h" // LoadedInstrument + MasterBusMeter
|
||||
#include "shell/instrument/reaper_bridge.h"
|
||||
#include "core/instrument/map/sample_map.h" // InstrumentParams (the one parameter set)
|
||||
#include "core/instrument/map/component_state_io.h" // ComponentState codec
|
||||
#include "core/instrument/engine/limiter.h" // the master bus's post-gain limiter
|
||||
#include "core/instrument/engine/live_params.h" // LiveParams (the live-parameter block)
|
||||
#include "core/instrument/engine/meter_accumulate.h" // the meter's block-rate folds + consume
|
||||
#include "core/instrument/engine/voice_engine.h"
|
||||
#include "core/instrument/param/param_id.h" // the frozen ParamId space + DeckParam binding
|
||||
|
||||
namespace reasampler::vst {
|
||||
@@ -36,48 +37,6 @@ using instrument::map::kPreviewVelocityDefault;
|
||||
|
||||
class ReaSamplerEmbed; // embedded TCP/MCP UI shell (owned below; see queryInterface)
|
||||
|
||||
// What the audio thread publishes about the OUTPUT BUS, post-limiter, once per block. Raw
|
||||
// magnitudes only — the UI converts to dB and runs the ballistics (engine/meter_ballistics),
|
||||
// because a hold timer or a log on the audio thread would be per-block work that buys nothing.
|
||||
struct MasterBusMeter {
|
||||
float peakL = 0.f; // max |x| this block
|
||||
float peakR = 0.f;
|
||||
// Smallest gain the LIMITER computed this block (Limiter::process) — deliberately NOT
|
||||
// scaled by the transition mute, so a toggle over quiet material reads 1 (no reduction)
|
||||
// rather than the mute's own weight. 1 = no reduction.
|
||||
float minGain = 1.f;
|
||||
bool clip = false; // LATCHED at a block peak >= 0 dBFS; only clearMasterBusClip lowers it
|
||||
};
|
||||
|
||||
// The decoded capture + the voice engine playing it. The engine holds a reference to the
|
||||
// sample, so both must live/die together at a stable address — heap-allocated,
|
||||
// non-copyable, non-movable. process() only ever reads this through an atomic pointer.
|
||||
struct LoadedInstrument {
|
||||
SampleData sample;
|
||||
VoiceEngine engine;
|
||||
std::uint64_t installedAt = 0; // reloadGeneration_ at which this was installed into live_
|
||||
|
||||
// Takeover declick is on by default here (product default; the pure core defaults it
|
||||
// off): any voice restart (mono retrigger, legato, poly steal, preview) ramps instead
|
||||
// of clicking.
|
||||
LoadedInstrument(SampleData sd, std::size_t maxVoices,
|
||||
std::uint64_t gen, std::size_t preserveVoiceCap = 0,
|
||||
std::int64_t preserveWindowFrames = 0,
|
||||
VoiceMode voiceMode = VoiceMode::Poly,
|
||||
MonoTrigger monoTrigger = MonoTrigger::Retrigger)
|
||||
: sample(std::move(sd)),
|
||||
engine(maxVoices, sample, preserveVoiceCap, preserveWindowFrames,
|
||||
voiceMode, monoTrigger, /*takeoverDeclick=*/true),
|
||||
installedAt(gen) {}
|
||||
|
||||
// True when nothing in this snapshot is sounding; lets the off-thread retirer park an
|
||||
// idle drain early. Bounded scan (<= maxVoices).
|
||||
bool fullyIdle() const { return engine.activeVoiceCount() == 0; }
|
||||
|
||||
LoadedInstrument(const LoadedInstrument&) = delete;
|
||||
LoadedInstrument& operator=(const LoadedInstrument&) = delete;
|
||||
};
|
||||
|
||||
class ReaSamplerProcessor : public Steinberg::Vst::SingleComponentEffect {
|
||||
public:
|
||||
ReaSamplerProcessor() = default;
|
||||
@@ -140,9 +99,11 @@ public:
|
||||
void syncParamsFromModel();
|
||||
|
||||
// Folds what the audio thread took from the host's parameter queue back into the model and
|
||||
// the controller cache, suppressing the host notification (those values came FROM it).
|
||||
// UI/main thread; a no-op when nothing was automated. Called wherever the model is about to
|
||||
// be READ as authoritative — getState, the bake, the editor's tick.
|
||||
// the controller cache, suppressing the host notification (those values came FROM it), then
|
||||
// RELEASES each folded point's hold — which is what bounds a lane's authority to the window
|
||||
// where it is actually driving. UI/main thread; a no-op when nothing was automated. Called
|
||||
// wherever the model is about to be READ as authoritative — getState, the bake, the editor's
|
||||
// tick. This directory's CLAUDE.md owns the authority model.
|
||||
void drainAutomationToModel();
|
||||
|
||||
// A drag's host-edit bracket, so a host in touch or latch mode records ONE continuous edit
|
||||
@@ -231,6 +192,12 @@ public:
|
||||
InstrumentParams instrumentParams();
|
||||
void setInstrumentParams(const InstrumentParams& params);
|
||||
|
||||
// The model's edit counter, bumped by every setInstrumentParams. A holder of a COPY (the
|
||||
// editor's snapshot) re-seeds when this moves under it; the overload answers both under one
|
||||
// lock, because reading them apart would let the pair disagree.
|
||||
std::uint32_t instrumentParamsGeneration();
|
||||
InstrumentParams instrumentParams(std::uint32_t& generation);
|
||||
|
||||
// Republishes the live-parameter block from the stored parameter set, resolved against the
|
||||
// rate the loaded capture was built at so an unmoved value folds to exactly the frames the
|
||||
// voices already latched. THE tier-3 commit (the three tiers are listed in this
|
||||
@@ -320,10 +287,10 @@ private:
|
||||
void buildParameterList();
|
||||
|
||||
// THE automation read, on the audio thread, at the BLOCK BOUNDARY: the last point of each
|
||||
// queue wins. RT-safe — relaxed atomic stores only. Sample-accurate application would put a
|
||||
// per-sample "did anything change" question on the per-voice-per-sample path, which the
|
||||
// phase-wide guardrail forbids. True when at least one point landed, which is what makes the
|
||||
// merge below it conditional.
|
||||
// queue wins. RT-safe — relaxed/release atomic stores only. Sample-accurate application would
|
||||
// put a per-sample "did anything change" question on the per-voice-per-sample path, which the
|
||||
// phase-wide guardrail forbids. True when at least one point landed ON A CONTROL THE BLOCK
|
||||
// CARRIES, which is what makes the merge below it conditional.
|
||||
bool drainInputParameterChanges(Steinberg::Vst::IParameterChanges* changes);
|
||||
|
||||
// The normalized value a control reads at, from the model — a projection of it, never a
|
||||
@@ -421,25 +388,22 @@ private:
|
||||
// leave the drain's still-sounding voices deaf to the knob under them.
|
||||
instrument::engine::LiveParams liveParams_;
|
||||
// The block the ENGINE reads, and the ONE thing SampleData::live points at. Written only by
|
||||
// the audio thread, which merges liveParams_ with the host's automation points once per
|
||||
// block and republishes ONLY when either side moved — so a block carrying neither costs one
|
||||
// relaxed load and the engine's own read shape is unchanged. Two blocks because the seqlock's
|
||||
// single-writer contract is load-bearing and the two writers differ in thread; this
|
||||
// directory's CLAUDE.md owns the argument.
|
||||
// the audio thread, which merges liveParams_ with the host's automation points once per block
|
||||
// and republishes ONLY when the RESULT moved — so neither an unchanged model nor a lane
|
||||
// resending the value it already sent reaches the per-voice fan-out. A block carrying no
|
||||
// automation and no model change costs one relaxed load plus, when the host passed a non-null
|
||||
// IParameterChanges (REAPER's normal case), one cross-module getParameterCount(). Two blocks
|
||||
// because the seqlock's single-writer contract is load-bearing and the two writers differ in
|
||||
// thread; this directory's CLAUDE.md owns the argument.
|
||||
instrument::engine::LiveParams automationLive_;
|
||||
// Audio thread only. The last liveParams_ generation merged, and the sticky automation values
|
||||
// re-applied over every merge — without them a model republish (any knob move) would revert
|
||||
// an automated parameter until its lane's next point.
|
||||
// Audio thread only. The last liveParams_ generation merged, the last block published (the
|
||||
// republish gate compares against it), and the automation slots themselves.
|
||||
std::uint32_t seenModelGeneration_ = 0;
|
||||
static constexpr std::size_t kDeckParamSlots =
|
||||
static_cast<std::size_t>(instrument::ui::DeckParam::kCount);
|
||||
double automationNorm_[kDeckParamSlots] = {};
|
||||
bool automationHeld_[kDeckParamSlots] = {};
|
||||
// The audio thread's publication of those values to the UI thread's fold. automationAny_
|
||||
// makes the idle drain a single load.
|
||||
std::atomic<double> automationPublished_[kDeckParamSlots] = {};
|
||||
std::atomic<bool> automationPending_[kDeckParamSlots] = {};
|
||||
std::atomic<bool> automationAny_{false};
|
||||
instrument::engine::LiveValues lastMergedLive_{};
|
||||
bool haveMergedLive_ = false;
|
||||
// The host lane's state and its release protocol; automation_channel.h owns the mechanism and
|
||||
// this directory's CLAUDE.md the authority model it enforces.
|
||||
AutomationChannel automation_;
|
||||
// Serializes liveParams_.publish's two writer sites (reloadInstrument, publishLiveParams)
|
||||
// only — separate from reloadMutex_ so a knob drag's publish never blocks behind a
|
||||
// reload's WAV decode. The audio thread never takes this; process() only reads via
|
||||
@@ -509,10 +473,13 @@ private:
|
||||
std::mutex selectionMutex_;
|
||||
std::string selectedSampleId_;
|
||||
|
||||
// The one parameter set. Off-thread only; reloadInstrument bakes it into the SampleData
|
||||
// under the reload lock, never read directly on the audio thread.
|
||||
// The one parameter set — THE model, and the authority every other holder of these values
|
||||
// defers to. Off-thread only; reloadInstrument bakes it into the SampleData under the reload
|
||||
// lock, never read directly on the audio thread. paramsGeneration_ moves with every write, so
|
||||
// a holder of a copy can tell that it has.
|
||||
std::mutex paramsMutex_;
|
||||
InstrumentParams params_;
|
||||
std::uint32_t paramsGeneration_ = 0;
|
||||
|
||||
// Instance-owned sample refs: path + intrinsics per referenced sample. Refreshed
|
||||
// opportunistically from the bank blob when readable; never a bank dependency for
|
||||
|
||||
Reference in New Issue
Block a user