493 lines
28 KiB
C++
493 lines
28 KiB
C++
// reasampler_processor.h — VST3 SingleComponentEffect wiring the pure sampler core into
|
|
// a playable instrument: event-input + stereo output bus, MIDI -> VoiceEngine, render.
|
|
// Self-contained playback: component state owns per-sample WAV path + decode intrinsics
|
|
// (SampleRefs); the bank blob is an opportunistic browser source, never a playback
|
|
// dependency. Audio thread (process()) does no allocation/file-IO/bridge calls/locks;
|
|
// loading happens off-thread (reloadInstrument) and hands off via one atomic pointer swap.
|
|
|
|
#pragma once
|
|
|
|
#include <atomic>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "public.sdk/source/vst/vstsinglecomponenteffect.h"
|
|
|
|
#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/voice_engine.h"
|
|
|
|
namespace reasampler::vst {
|
|
|
|
using instrument::map::ComponentState;
|
|
using instrument::map::InstrumentParams;
|
|
using instrument::map::SampleRefEntry;
|
|
using instrument::map::SampleRefs;
|
|
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;
|
|
// Out-of-line so the owned ReaSamplerEmbed (unique_ptr, forward-declared here) is
|
|
// complete at the destruction point (defined in the .cpp).
|
|
~ReaSamplerProcessor() override;
|
|
|
|
// The factory create function (registered in vst_entry.cpp).
|
|
static Steinberg::FUnknown* createInstance(void* /*context*/);
|
|
|
|
//--- from IComponent / IPluginBase -------------------------------------
|
|
// Connects the REAPER bridge (context is REAPER's IHostApplication) and declares
|
|
// the instrument bus topology.
|
|
Steinberg::tresult PLUGIN_API initialize(Steinberg::FUnknown* context) override;
|
|
Steinberg::tresult PLUGIN_API terminate() override;
|
|
Steinberg::tresult PLUGIN_API setActive(Steinberg::TBool state) override;
|
|
|
|
// Instance state = the selected bank sample id (a performance choice the instrument
|
|
// owns; never written back to the bank). Component-state, so a saved project restores
|
|
// which sample each instance plays.
|
|
Steinberg::tresult PLUGIN_API setState(Steinberg::IBStream* state) override;
|
|
Steinberg::tresult PLUGIN_API getState(Steinberg::IBStream* state) override;
|
|
|
|
//--- from IAudioProcessor ----------------------------------------------
|
|
Steinberg::tresult PLUGIN_API setupProcessing(
|
|
Steinberg::Vst::ProcessSetup& setup) override;
|
|
// Marshals MIDI -> VoiceEngine -> audio output. Real-time safe (no alloc/IO/lock).
|
|
Steinberg::tresult PLUGIN_API process(
|
|
Steinberg::Vst::ProcessData& data) override;
|
|
|
|
// The plugin's PDC report: 0 with the limiter bypassed, the limiter's lookahead with it
|
|
// engaged. Read from the PERSISTED enable, never from a transient — the SDK's contract
|
|
// (pluginterfaces/vst/ivsteditcontroller.h, kLatencyChanged) is that the host asks this
|
|
// AFTER the deactivate/reactivate it performs, and setActive(false) clears the engine.
|
|
Steinberg::uint32 PLUGIN_API getLatencySamples() override;
|
|
|
|
// Fixed stereo output bus — channel mode is a decode policy, never a bus fact; mono
|
|
// renders dual-mono through it. Do not reintroduce per-instance bus renegotiation.
|
|
// Accepts only a single stereo output proposal; otherwise rejects and keeps stereo.
|
|
Steinberg::tresult PLUGIN_API setBusArrangements(
|
|
Steinberg::Vst::SpeakerArrangement* inputs, Steinberg::int32 numIns,
|
|
Steinberg::Vst::SpeakerArrangement* outputs, Steinberg::int32 numOuts) override;
|
|
|
|
//--- from IEditController -----------------------------------------------
|
|
// Hands the host our LICE IPlugView editor.
|
|
Steinberg::IPlugView* PLUGIN_API createView(Steinberg::FIDString name) override;
|
|
|
|
// Additionally exposes REAPER's IReaperUIEmbedInterface (queried by REAPER to drive the
|
|
// inline TCP/MCP embed); all other iids delegate to SingleComponentEffect unchanged.
|
|
Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid,
|
|
void** obj) override;
|
|
|
|
// The embedded-strip activity level (0..1) for the embed shell, UI thread. The last block's
|
|
// loudest channel — a non-consuming read, so it stays correct however often the editor's
|
|
// meter drains its own accumulators (or never does, with no editor open).
|
|
double embedActivityLevel() const {
|
|
return static_cast<double>(advisoryPeak_.load(std::memory_order_relaxed));
|
|
}
|
|
|
|
// What the audio thread published since the LAST call: peaks maxed and minGain minimised
|
|
// across every block in that window. CONSUMING — it resets the accumulators as it reads, so
|
|
// exactly one reader may call it, and that reader is the editor's meter tick. UI thread.
|
|
MasterBusMeter masterBusMeter();
|
|
void clearMasterBusClip();
|
|
|
|
// Resolves the selection against the instance-owned SampleRefs, decodes its WAV
|
|
// off-thread, and publishes the built instrument via atomic swap — no bank read
|
|
// required. When the bank blob is readable it's first folded into the refs table
|
|
// (refreshRefsFromBank; the browser's copy-the-ref-in + recapture-sync mechanism). A
|
|
// missing/unreadable WAV is the defined no-play (silence, no retry). Returns the
|
|
// resolved selection id ("" if nothing loaded).
|
|
std::string reloadInstrument();
|
|
|
|
// The dialed sound as plain data for the offline bake: the SAME refs resolve + decode +
|
|
// build reloadInstrument runs, with no live block attached. Rebuilt rather than copied
|
|
// off the live snapshot because a tier-3 live edit leaves that snapshot's own play
|
|
// params stale on purpose — copying it would bake the pre-drag values. nullopt when
|
|
// nothing is loaded or the WAV is unreadable. Off the audio thread (file I/O).
|
|
std::optional<SampleData> bakeSnapshot();
|
|
|
|
// Adopt a landed bake in ONE act: the new capture becomes this instance's ref and
|
|
// selection, the parameter set and master gain go neutral, and a single reload
|
|
// publishes all three together. The ordering is the whole point — anything published
|
|
// ahead of the re-point would apply neutral settings to the OLD capture, which is the
|
|
// one thing they are meaningless against. UI thread only.
|
|
void adoptBakedCapture(const SampleRefEntry& entry, const InstrumentParams& reset,
|
|
double masterGainLinear);
|
|
|
|
// What pollBankSync did this tick, so the editor can react only when something changed.
|
|
struct BankSyncResult {
|
|
bool reloaded = false; // bank generation changed (or a legacy lift landed) -> reloaded
|
|
bool applied = false; // a new assignment request was applied -> selection changed
|
|
};
|
|
|
|
// Off-thread poll (editor's UI timer only) of the bank generation + assignment request;
|
|
// playback never depends on it. Generation change -> reload; a resolvable NEW assignment
|
|
// targeting this instance (isFocusedTarget) -> apply as selection + reload (unresolvable
|
|
// ones drop silently, marker still advances); pre-v10 legacy blobs retry the bank read
|
|
// until the refs lift in, then stop (legacyLiftShouldRun). The consumed marker persists
|
|
// so a re-open does not re-apply. Idempotent on an idle tick.
|
|
BankSyncResult pollBankSync(bool isFocusedTarget);
|
|
|
|
// The bridge, for the editor's live-state readout + sample list. Owned here; the
|
|
// editor borrows it (outlives the editor).
|
|
ReaperBridge& bridge() { return bridge_; }
|
|
|
|
// The live host sample rate latched from setupProcessing; the editor's envelope overlay
|
|
// shares this time base. 0.0 before setupProcessing runs.
|
|
double sampleRate() const { return sampleRate_; }
|
|
// The loaded capture's id (guarded by selectionMutex_, never read on the audio thread).
|
|
// Empty id -> silence, no first-sample fallback.
|
|
std::string selectedSampleId();
|
|
void setSelectedSampleId(const std::string& id);
|
|
|
|
// The one parameter set governing that capture. UI thread, guarded by paramsMutex_;
|
|
// never read on the audio thread — reloadInstrument bakes it into the SampleData
|
|
// off-thread.
|
|
InstrumentParams instrumentParams();
|
|
void setInstrumentParams(const InstrumentParams& params);
|
|
|
|
// 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
|
|
// directory's CLAUDE.md). Callers pair this with setInstrumentParams exactly as they
|
|
// paired it with reloadInstrument. No-op before anything has been decoded (the next reload
|
|
// bakes and publishes). UI thread; serialized against reloadInstrument's own publish.
|
|
void publishLiveParams();
|
|
|
|
// Per-instance channel mode (mono | stereo), guarded by channelModeMutex_, never read
|
|
// on the audio thread. Decode policy only (downmix vs L/R split) — the output bus is
|
|
// fixed stereo, so a mode change never renegotiates host I/O.
|
|
ChannelMode channelMode();
|
|
// Editor toggle: latches the mode explicit (auto-default stops fighting it) and
|
|
// reloads so the next block decodes the new channel count. UI thread only.
|
|
void setChannelMode(ChannelMode mode);
|
|
|
|
// Per-instance preview-trigger velocity (MIDI 1..127), guarded by previewMutex_, not
|
|
// read on the audio thread.
|
|
std::uint8_t previewVelocity();
|
|
void setPreviewVelocity(std::uint8_t velocity);
|
|
|
|
// Voice-system parameters (per-instance), guarded by voiceParamsMutex_, not read on the
|
|
// audio thread — each setter rebuilds via rebuildVoiceEngine (already-decoded SampleData,
|
|
// no bridge/WAV re-read) through the same drain-slot swap, so a change never cuts a tail.
|
|
int voiceCount();
|
|
void setVoiceCount(int count); // clamped to kMinVoiceCount..kMaxVoiceCount
|
|
VoiceMode voiceMode();
|
|
void setVoiceMode(VoiceMode mode);
|
|
MonoTrigger monoTrigger();
|
|
void setMonoTrigger(MonoTrigger trigger);
|
|
|
|
// Post-mixer master gain, linear in [0, masterGainMaxLinear()] (0 = true silence, 1 =
|
|
// unity, cap +24 dB). Atomic — the audio thread applies it as a per-block post-sum
|
|
// multiply, no lock, no rebuild.
|
|
double masterGainLinear() const {
|
|
return static_cast<double>(masterGain_.load(std::memory_order_relaxed));
|
|
}
|
|
void setMasterGainLinear(double linear); // clamped to [0, masterGainMaxLinear()]
|
|
|
|
// The master-bus limiter's single enable (persisted in the parameter set). UI thread only:
|
|
// a thin wrapper over setInstrumentParams, the one funnel that both mirrors the flag and
|
|
// requests the host's kLatencyChanged restart, which the SDK requires be issued from the UI
|
|
// thread and which process() must therefore never trigger. Setting the value it already
|
|
// holds is a no-op, so repeated clicks on one segment cost no restart.
|
|
bool limiterEnabled() const {
|
|
return limiterEnabled_.load(std::memory_order_relaxed);
|
|
}
|
|
void setLimiterEnabled(bool on);
|
|
|
|
// Fires a one-shot preview note-on/off through the live VoiceEngine — the same
|
|
// noteOn/noteOff host MIDI uses, so a preview is a real voice (counts against voice
|
|
// count, can steal/be stolen, respects Poly/Mono + Retrigger/Legato). Off the audio
|
|
// thread; handed to process() via a lock-free single-slot mailbox drained at block
|
|
// start. Never captures, never inserts a timeline item.
|
|
void previewNoteOn(int note);
|
|
void previewNoteOff(int note);
|
|
|
|
// Snapshot copy of the instance-owned sample refs, for the editor's waveform/loop
|
|
// fallback when the bank blob is unreadable. Guarded by refsMutex_.
|
|
SampleRefs sampleRefs();
|
|
|
|
// This instance's usage identity, minted here if it has never published. It names BOTH
|
|
// the "rsusage_" record the bake's tie query must exclude and the "rsbake_" request
|
|
// key, so the two can never name different instances. Off the audio thread.
|
|
std::string usageInstanceGuid();
|
|
|
|
private:
|
|
// If process() published that the drain instrument is fully idle, move it into the
|
|
// graveyard and prune — so an edited-away snapshot stops costing memory as soon as its
|
|
// tails die. Off the audio thread only (driven by pollBankSync); safe against a racing
|
|
// process() because idleness is monotone and the publication names the drain's own
|
|
// installedAt (a stale value can never retire a newer occupant).
|
|
void retireIdleDrain();
|
|
|
|
// Light voice-param rebuild: rebuilds the engine around a copy of the live instrument's
|
|
// already-decoded SampleData (no bridge/disk) and publishes through the same drain-slot
|
|
// swap as a full reload. No-op when nothing is loaded. Off the audio thread only.
|
|
void rebuildVoiceEngine();
|
|
|
|
// Pre-v10 legacy-lift gate: true when a lift attempt this tick could make progress
|
|
// (see legacyLiftConcluded_). Off the audio thread only (bridge read + bank parse).
|
|
bool legacyLiftShouldRun();
|
|
|
|
// Publishes `built` (null = install silence) into live_: prunes the graveyard by the
|
|
// last process()-published generation, swaps `built` into live_, displaces the previous
|
|
// live into the drain slot, and parks the evicted drain instrument in the graveyard.
|
|
// Requires reloadMutex_ held — shared by reloadInstrument and rebuildVoiceEngine.
|
|
void publishBuiltLocked(std::unique_ptr<LoadedInstrument> built);
|
|
|
|
// Folds one block's reading into the accumulator it belongs to — a running max for a peak,
|
|
// a running min for the limiter's gain. Read-modify-write rather than a bare store, so the
|
|
// ~47 blocks that elapse between two 500 ms UI frames at 48 kHz/512 all reach the meter
|
|
// instead of the one it happened to sample. Relaxed throughout: the accumulators are
|
|
// advisory, and no other state is ordered against them. Block rate, never per frame.
|
|
static void foldPeak(std::atomic<float>& acc, float blockPeak) {
|
|
if (blockPeak > acc.load(std::memory_order_relaxed)) {
|
|
acc.store(blockPeak, std::memory_order_relaxed);
|
|
}
|
|
}
|
|
static void foldMinGain(std::atomic<float>& acc, float blockMinGain) {
|
|
if (blockMinGain < acc.load(std::memory_order_relaxed)) {
|
|
acc.store(blockMinGain, std::memory_order_relaxed);
|
|
}
|
|
}
|
|
|
|
// Publishes a silent block. EVERY process() path that emits no audio calls this. It clears
|
|
// only the ADVISORY level, which is a last-block reading: the meter accumulators need
|
|
// nothing here, because a block that emitted no audio contributes no peak and no gain
|
|
// reduction, and the UI's own read is what resets them.
|
|
void publishSilentMeterBlock() {
|
|
advisoryPeak_.store(0.f, std::memory_order_relaxed);
|
|
}
|
|
|
|
// Mirrors the persisted limiter enable onto the audio thread and the latency reader. Called
|
|
// from every writer of the parameter set, so the three views can never disagree.
|
|
void publishLimiterEnabled(bool on);
|
|
|
|
// Publishes this instance's held captures to its per-instance ext-state key
|
|
// ("rsusage_<instanceGuid>") so the extension's prune can never reclaim them. Called at
|
|
// the tail of every reloadInstrument, off the audio thread. Mints instanceGuid_ on
|
|
// first need; re-mints on a detected clone (FX copy / track duplication).
|
|
void publishUsage(const SampleRefs& refs, const std::vector<std::string>& ids);
|
|
|
|
ReaperBridge bridge_;
|
|
|
|
// The ONE live-parameter block for this instance, declared ahead of the instrument slots
|
|
// so it outlives every snapshot that points at it (members destruct in reverse order).
|
|
// Both live_ and draining_ observe this same block — a block owned by a snapshot would
|
|
// leave the drain's still-sounding voices deaf to the knob under them.
|
|
instrument::engine::LiveParams liveParams_;
|
|
// 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
|
|
// LiveParams::read's lock-free seqlock retry.
|
|
std::mutex livePublishMutex_;
|
|
// The rate the loaded capture was decoded/built at, so a live republish resolves the
|
|
// stored wall-clock seconds to exactly the frames the built SampleData carries. 0 = nothing
|
|
// built yet.
|
|
//
|
|
// ONE BLOCK, ONE RATE: this is stamped by whichever capture built last, and a reload
|
|
// publishes the new block before installing the new instrument. Swapping to a capture at a
|
|
// different rate therefore hands drain voices still ringing from the old-rate capture
|
|
// envelope frame counts resolved at the NEW rate (~8.8% timing shift on a 48k->44.1k swap).
|
|
// Unavoidable while one block sits above every snapshot, and it touches a release tail
|
|
// only.
|
|
std::atomic<int> builtSampleRate_{0};
|
|
|
|
// --- The audio-thread handoff (drain slot) ---
|
|
// process() atomically loads live_ + draining_ at block start (two acquires, no lock).
|
|
// reloadInstrument() (off-thread, serialized by reloadMutex_) swaps a new build into
|
|
// live_; the displaced instrument moves to draining_, where process() keeps rendering
|
|
// its already-sounding voices (and routes note-offs to it) so a reload never cuts a
|
|
// ringing note — new note-ons go only to live_. The instrument evicted from draining_
|
|
// (two reloads old) parks in graveyard_ for reclaim.
|
|
//
|
|
// Reclaim: process() publishes the minimum installedAt it holds via processGeneration_
|
|
// (one relaxed store); the reload path frees graveyard entries older than that. Safe
|
|
// because both slots are monotone in installedAt, so the published minimum is monotone
|
|
// and an entry only reaches the graveyard after leaving both slots under reloadMutex_ —
|
|
// an entry below the published minimum can never be loaded again.
|
|
//
|
|
// Graveyard upper bound: reloads since process last ran (typically 0-1). Remaining
|
|
// entries drain at setActive(false) / terminate(), when process is guaranteed stopped.
|
|
std::atomic<LoadedInstrument*> live_{nullptr};
|
|
std::atomic<LoadedInstrument*> draining_{nullptr}; // displaced instrument still rendering its tails
|
|
std::atomic<std::uint64_t> reloadGeneration_{0}; // incremented by each reload (off-thread, under reloadMutex_; read atomically by process)
|
|
std::atomic<std::uint64_t> processGeneration_{0}; // min installedAt held by process (written on audio thread, read off-thread)
|
|
// The installedAt of the drain instrument process() last observed fully idle (0 = none /
|
|
// still sounds). Written relaxed on the audio thread each block; read by retireIdleDrain()
|
|
// off-thread. Naming the generation (not a bool) closes the swap race: a publication about
|
|
// an old drain can never retire its successor.
|
|
std::atomic<std::uint64_t> drainIdleGeneration_{0};
|
|
std::vector<std::unique_ptr<LoadedInstrument>> graveyard_; // drained on reclaim + setActive(false) + terminate
|
|
std::mutex reloadMutex_; // serializes off-thread reloads + graveyard access
|
|
// A master gain that must reach the audio thread in the SAME swap as the next publish —
|
|
// the bake's reset, which the old capture would be the wrong thing to apply it to.
|
|
// Guarded by reloadMutex_, consumed by publishBuiltLocked.
|
|
std::optional<double> gainAtNextPublish_;
|
|
|
|
// The loaded capture's id ("" = no pick -> silence). Off-thread only, not read on the
|
|
// audio thread.
|
|
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.
|
|
std::mutex paramsMutex_;
|
|
InstrumentParams params_;
|
|
|
|
// Instance-owned sample refs: path + intrinsics per referenced sample. Refreshed
|
|
// opportunistically from the bank blob when readable; never a bank dependency for
|
|
// playback. Off-thread only.
|
|
std::mutex refsMutex_;
|
|
SampleRefs sampleRefs_;
|
|
|
|
// Usage-publish identity (see publishUsage). instanceGuid_ is the persisted per-instance
|
|
// identity; usageNonce_ is this incarnation's per-lifetime owner nonce (never persisted —
|
|
// a persisted nonce would clone with the state on FX copy, letting a sibling clean-
|
|
// replace over another's held paths). Minted lazily; cleared on setState.
|
|
std::mutex usageMutex_;
|
|
std::string instanceGuid_;
|
|
std::string usageNonce_;
|
|
|
|
// Per-instance channel mode, default Mono; not read on the audio thread (process
|
|
// renders against the host's negotiated channel count). channelModeExplicit_: false =
|
|
// reloadInstrument may auto-default the mode from the loaded capture; true = the user
|
|
// deliberately toggled it (never fought thereafter).
|
|
std::mutex channelModeMutex_;
|
|
ChannelMode channelMode_ = ChannelMode::Mono;
|
|
bool channelModeExplicit_ = false;
|
|
|
|
// The last assignment-request generation consumed, persisted so a re-open does not
|
|
// re-apply a stale request. Default 0 -> a genuinely new first assign (gen >= 1) applies.
|
|
std::mutex assignMarkerMutex_;
|
|
std::int64_t lastConsumedAssignGeneration_ = 0;
|
|
|
|
// The bank generation this instance last saw. UI/timer-thread only (pollBankSync's sole
|
|
// reader/writer), not persisted. -1 sentinel baselines the first poll without a
|
|
// redundant reload; a later generation change then drives the reload.
|
|
std::int64_t lastSeenBankGeneration_ = -1;
|
|
|
|
// Legacy-lift terminating latch: set once legacyLiftShouldRun proves the referenced ids
|
|
// stale against a readable bank blob, so the lift stops re-firing every tick. Reset by
|
|
// setState (a new blob = new facts).
|
|
std::atomic<bool> legacyLiftConcluded_{false};
|
|
|
|
// Preview-trigger velocity (MIDI 1..127, persisted). Default kPreviewVelocityDefault
|
|
// (64). Not read on the audio thread.
|
|
std::mutex previewMutex_;
|
|
std::uint8_t previewVelocity_ = kPreviewVelocityDefault;
|
|
|
|
// Voice-system parameters (per-instance, persisted). Defaults {16, Poly, Retrigger}.
|
|
// Not read on the audio thread — reloadInstrument bakes them into the engine off-thread.
|
|
std::mutex voiceParamsMutex_;
|
|
int voiceCount_ = kDefaultVoiceCount;
|
|
VoiceMode voiceMode_ = VoiceMode::Poly;
|
|
MonoTrigger monoTrigger_ = MonoTrigger::Retrigger;
|
|
|
|
// Post-mixer master gain (linear, persisted). Lock-free atomic target; the audio thread
|
|
// ramps gainCurrent_ toward it per-sample (~20 ms wall-clock at every host rate) so
|
|
// knob moves produce no zipper noise.
|
|
std::atomic<float> masterGain_{1.0f};
|
|
// Audio-thread running gain value, stepping at most gainRampStep_ per sample toward the
|
|
// target. Written/read exclusively on the audio thread — no atomics needed.
|
|
float gainCurrent_ = 1.0f;
|
|
// Per-sample ramp step derived from kGainRampSeconds against the live host rate in
|
|
// setupProcessing — never a baked-in rate. Default is the 48 kHz value.
|
|
float gainRampStep_ = 1.0f / 960.0f;
|
|
|
|
// --- Preview-trigger mailbox (off-thread -> audio thread, lock-free) -----------------
|
|
// One slot per direction, packed as (seq << 16) | (velocity << 8) | note [on] or
|
|
// (seq << 16) | note [off]. process() detects a new request by comparing the packed
|
|
// sequence against the last one consumed — a single relaxed atomic load per block,
|
|
// RT-safe (no alloc, no lock).
|
|
std::atomic<std::uint32_t> previewOnRequest_{0}; // 0 = no request posted yet
|
|
std::atomic<std::uint32_t> previewOffRequest_{0};
|
|
std::uint16_t previewOnSeq_ = 0; // UI-thread post counter (never 0 after first post)
|
|
std::uint16_t previewOffSeq_ = 0;
|
|
std::uint16_t previewOnConsumed_ = 0; // audio-thread: last on-seq fired
|
|
std::uint16_t previewOffConsumed_ = 0; // audio-thread: last off-seq fired
|
|
|
|
// Latched from setupProcessing; 0.0 is explicitly invalid (reloadInstrument guards on it).
|
|
double sampleRate_ = 0.0;
|
|
Steinberg::int32 maxBlockSize_ = 4096;
|
|
|
|
// The embed shell, created lazily on the first queryInterface and owned here for the
|
|
// processor's lifetime; REAPER's borrowed AddRef'd references are outlived by this
|
|
// unique_ptr, so its own refcount is a no-op.
|
|
std::unique_ptr<ReaSamplerEmbed> embed_;
|
|
|
|
// The master-bus limiter, applied post-gain over the summed output. Its own enable target
|
|
// is the mirror of params_.limiterEnabled; limiterEnabled_ is the lock-free copy
|
|
// getLatencySamples answers from.
|
|
instrument::engine::Limiter limiter_;
|
|
std::atomic<bool> limiterEnabled_{false};
|
|
|
|
// What the audio thread publishes about the output bus each block, relaxed. The peaks and
|
|
// minGain ACCUMULATE (max / min) across every block since the UI last read, and
|
|
// masterBusMeter() resets them as it reads — the fix for a bar that displayed roughly one
|
|
// block in fifty. No dB, no ballistics, no hold timer here;
|
|
// the UI runs those off these values and its own elapsed time.
|
|
std::atomic<float> meterPeakL_{0.f};
|
|
std::atomic<float> meterPeakR_{0.f};
|
|
std::atomic<float> meterMinGain_{1.f};
|
|
std::atomic<bool> meterClip_{false};
|
|
|
|
// The embed strip's activity level: the LAST block's loudest channel, plainly overwritten.
|
|
// Separate from the accumulators above because it answers a different question ("how loud
|
|
// is it now") and has a different reader — sharing one would make each reader's reset
|
|
// silently truncate the other's window.
|
|
std::atomic<float> advisoryPeak_{0.f};
|
|
};
|
|
|
|
} // namespace reasampler::vst
|