412 lines
26 KiB
C++
412 lines
26 KiB
C++
// reasampler_processor.h — the VST3 SingleComponentEffect (Phase S4, Tier 0). Wires the
|
||
// pure S3 sampler core into a real VSTi: it declares an event-input bus + a stereo audio
|
||
// output bus, marshals host MIDI note-on/off into the VoiceEngine, and renders the
|
||
// engine's audio into the output bus — so a chosen bank sample plays chromatically from
|
||
// its root note in REAPER's routing/record/render path.
|
||
//
|
||
// SingleComponentEffect is the SDK's combined processor+controller base — sanctioned
|
||
// for a non-distributable, REAPER-only plugin under D5/D6. It gives us
|
||
// addAudioOutput/addEventInput, IComponent setState/getState for the instance's own
|
||
// state (the selected sample), and the IEditController seat so createView() can hand the
|
||
// host our IPlugView LICE editor.
|
||
//
|
||
// REAL-TIME DISCIPLINE (S4 hard constraint). The audio thread (process) does NO
|
||
// allocation, NO file I/O, NO bridge calls, NO locks. Sample loading — bridge ext-state
|
||
// read, WAV decode, path resolve, keymap build, VoiceEngine construction — all happens
|
||
// OFF the audio thread (reloadFromBank, driven from the main/UI thread) and is handed to
|
||
// process via a single atomic pointer swap. See the LoadedInstrument handoff below.
|
||
|
||
#pragma once
|
||
|
||
#include <atomic>
|
||
#include <cstdint>
|
||
#include <memory>
|
||
#include <mutex>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
#include "public.sdk/source/vst/vstsinglecomponenteffect.h"
|
||
|
||
#include "reaper_bridge.h"
|
||
#include "sample_map.h" // PerformanceMap (the instrument's owned zoned keymap)
|
||
#include "sampler_core.h"
|
||
|
||
namespace reasampler::vst {
|
||
|
||
class ReaSamplerEmbed; // S6 embedded TCP/MCP UI shell (owned below; see queryInterface)
|
||
|
||
// One fully-built, ready-to-play instrument snapshot: the decoded keymap, the voice
|
||
// engine that plays it, and the isolated PREVIEW CARD (Phase S) summed alongside it.
|
||
// Engine and card both hold references into the keymap, so the three MUST live and die
|
||
// together at a STABLE address — hence this is heap-allocated and neither copyable nor
|
||
// movable. The audio thread only ever reads it through an atomic pointer; it is built
|
||
// and destroyed off the audio thread.
|
||
//
|
||
// installedAt: the reloadGeneration_ value at which this instrument was atomically
|
||
// installed into live_. Set on the reload path before the exchange. process() publishes
|
||
// this field (not a fresh re-read of reloadGeneration_) so the published generation is
|
||
// exactly the generation of the instrument actually in hand for the block.
|
||
struct LoadedInstrument {
|
||
Keymap keymap;
|
||
VoiceEngine engine;
|
||
PreviewCard preview; // Phase S: the isolated preview voice — never part of the pool
|
||
std::uint64_t installedAt = 0; // reload generation at which this was installed
|
||
|
||
LoadedInstrument(Keymap km, 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)
|
||
: keymap(std::move(km)),
|
||
engine(maxVoices, keymap, preserveVoiceCap, preserveWindowFrames,
|
||
voiceMode, monoTrigger),
|
||
preview(keymap, preserveWindowFrames),
|
||
installedAt(gen) {}
|
||
|
||
// True when nothing in this snapshot is sounding — engine voices AND the preview card.
|
||
// process() publishes this for the drain slot so the off-thread retirer can park an
|
||
// idle drain in the graveyard early (FA1-review Major #2). Bounded scan (<= maxVoices).
|
||
bool fullyIdle() const { return engine.activeVoiceCount() == 0 && !preview.active(); }
|
||
|
||
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 (held by unique_ptr, forward-declared here)
|
||
// is a complete type 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 (D-B: a performance choice the
|
||
// instrument owns; NEVER written back to the bank). Component-state, so a saved
|
||
// REAPER 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;
|
||
|
||
// S7 channel-mode bus negotiation. The instrument has ONE canonical output arrangement
|
||
// determined by its per-instance channel mode (mono -> kMono, stereo -> kStereo). We
|
||
// accept the host's proposal only when it matches that arrangement; otherwise we reject
|
||
// (kResultFalse) but keep the mode's arrangement, so getBusArrangement / getBusInfo always
|
||
// report the mode's channel count and REAPER routes accordingly. A runtime mode change
|
||
// updates the output bus + calls restartComponent(kIoChanged) to trigger re-negotiation.
|
||
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;
|
||
|
||
// Override queryInterface to additionally expose REAPER's IReaperUIEmbedInterface (S6):
|
||
// REAPER queries the IEditController for it to drive the inline TCP/MCP embed surface.
|
||
// All other iids delegate to SingleComponentEffect's implementation unchanged.
|
||
Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid,
|
||
void** obj) override;
|
||
|
||
// The embedded-strip activity level (0..1), read by the S6 embed shell on the UI thread.
|
||
// Backed by embedPeak_, the per-block mono peak the audio thread stores relaxed — a
|
||
// lock-free advisory readout, never touched with a lock the audio thread could contend.
|
||
double embedActivityLevel() const {
|
||
return static_cast<double>(embedPeak_.load(std::memory_order_relaxed));
|
||
}
|
||
|
||
// Called by the editor (main/UI thread) when the user picks a sample, and internally
|
||
// on load. Reads the live bank over the bridge, resolves+decodes the selected WAV
|
||
// OFF the audio thread, and publishes the built instrument to process() via an
|
||
// atomic swap. Safe to call with no bridge / no bank (leaves silence). Returns the
|
||
// resolved selection id ("" if nothing was loaded) for the editor to reflect.
|
||
std::string reloadFromBank();
|
||
|
||
// The result of a bank-sync poll (S9/S8): what pollBankSync did this tick, so the editor
|
||
// can react (repaint / re-snapshot its own view) only when something actually changed.
|
||
struct BankSyncResult {
|
||
bool reloaded = false; // the bank generation changed -> reloadFromBank ran
|
||
bool applied = false; // a new assignment request was applied -> selection changed
|
||
};
|
||
|
||
// Poll the S9 bank-generation counter and the S8 assignment request over the bridge, OFF
|
||
// THE AUDIO THREAD (the editor's UI timer drives this — NEVER process()). Semantics:
|
||
// * S9: if the bank generation differs from what we last saw, call reloadFromBank() so a
|
||
// recapture/ingest refreshes playback hands-free (atomic swap, glitch-free).
|
||
// * S8: if a NEW (generation > last consumed) assignment request names a resolvable
|
||
// sample AND this instance is the target (isFocusedTarget), apply it as the selection
|
||
// and reload; an unresolvable request is DROPPED silently (marker advanced, no change);
|
||
// a non-target instance neither applies nor advances its marker.
|
||
// The consumed marker advances in component state (marked dirty via the host handler) so a
|
||
// re-open does not re-apply. `isFocusedTarget` is the shell's thundering-herd policy input
|
||
// (the editor passes true only for the instance whose editor is open — see the handoff).
|
||
// Idempotent on an idle tick (generation unchanged + no new request -> no work).
|
||
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 SAME rate reloadFromBank
|
||
// resolves seconds->frames against). The editor's S-VIEW-3 envelope overlay reads it to place
|
||
// its wall-clock seconds on the same time base the voice engine plays them over. 0.0 before
|
||
// setupProcessing runs (the editor guards). Read on the UI thread; a plain load — sampleRate_
|
||
// is set once by setupProcessing before any audio and does not change under the editor.
|
||
double sampleRate() const { return sampleRate_; }
|
||
// The current single-capture selection id (main/UI thread reads for the editor). Guarded
|
||
// by selectionMutex_ — never touched on the audio thread. Since S10 this is the ONE picked
|
||
// capture the default face plays chromatically when the performance map is empty; an EMPTY
|
||
// id resolves to SILENCE (no first-sample fallback). A non-empty zoned map supersedes it.
|
||
std::string selectedSampleId();
|
||
void setSelectedSampleId(const std::string& id);
|
||
|
||
// The performance map (Tier 1: the zoned keymap the instrument owns; D-B). Read/written
|
||
// by the editor on the UI thread; snapshotted under performanceMutex_. NEVER read on the
|
||
// audio thread — reloadFromBank bakes it into the LoadedInstrument's Keymap off-thread.
|
||
PerformanceMap performanceMap();
|
||
void setPerformanceMap(const PerformanceMap& map);
|
||
|
||
// The per-instance channel mode (S7, D-E: mono | stereo). Read/written on the UI thread
|
||
// (the editor toggle) and read off-thread by getState/reloadFromBank; guarded by
|
||
// channelModeMutex_. NEVER read on the audio thread — process() renders against the host's
|
||
// negotiated output channel count, and reloadFromBank bakes the mode into the decode.
|
||
ChannelMode channelMode();
|
||
// Sets the mode. When it CHANGES, updates the output bus arrangement (mono->kMono /
|
||
// stereo->kStereo) and asks the host to re-negotiate I/O via restartComponent(kIoChanged),
|
||
// then reloads the instrument so the next block decodes the new channel count. A no-op set
|
||
// (same mode) does neither. UI thread only.
|
||
void setChannelMode(ChannelMode mode);
|
||
|
||
// The per-instance preview-trigger velocity (S-VIEW-4, MIDI 1..127). Read/written on the
|
||
// UI thread (the Sample-view velocity knob) and by getState/setState (host load-save thread);
|
||
// guarded by previewMutex_. Persisted in component state (v6). NOT read on the audio thread.
|
||
std::uint8_t previewVelocity();
|
||
void setPreviewVelocity(std::uint8_t velocity);
|
||
|
||
// --- Phase S voice-system parameters (per-instance, persisted in component state v7) ---
|
||
// Read/written on the UI thread (the editor's voice deck) and by getState/setState; guarded
|
||
// by voiceParamsMutex_. NOT read on the audio thread — each setter rebuilds the VoiceEngine
|
||
// OFF-thread via rebuildVoiceEngine (a LIGHT rebuild around the already-decoded keymap; no
|
||
// bridge read, no WAV re-decode) published through the same tail-preserving drain-slot swap,
|
||
// so changing polyphony / mode / the retrigger toggle never cuts a ringing tail.
|
||
int voiceCount();
|
||
void setVoiceCount(int count); // clamped to kMinVoiceCount..kMaxVoiceCount
|
||
VoiceMode voiceMode();
|
||
void setVoiceMode(VoiceMode mode);
|
||
MonoTrigger monoTrigger();
|
||
void setMonoTrigger(MonoTrigger trigger);
|
||
|
||
// --- FB1 post-mixer master gain (per-instance, persisted in component state v8) ---------
|
||
// LINEAR gain in [0, masterGainMaxLinear()] (0.0 = -inf/true silence, 1.0 = unity, cap =
|
||
// +24 dB; the pure master_gain module owns the dB knob taper). Held in an atomic so the
|
||
// audio thread applies it with ONE relaxed load per block as a post-sum multiply over the
|
||
// rendered output (engine + drain + preview) — no lock, no rebuild, no per-voice cost.
|
||
// Written by the editor's Gain knob (UI thread) and setState; read by getState + process().
|
||
double masterGainLinear() const {
|
||
return static_cast<double>(masterGain_.load(std::memory_order_relaxed));
|
||
}
|
||
void setMasterGainLinear(double linear); // clamped to [0, masterGainMaxLinear()]
|
||
|
||
// Fire a one-shot PREVIEW note-on / note-off through the live instrument's PREVIEW CARD
|
||
// (S-VIEW-4; Phase S isolation) — a dedicated single voice structurally OUTSIDE the MIDI
|
||
// pool, so a full pool never drops a preview and a preview never steals a playing voice.
|
||
// OFF the audio thread (the editor's preview-trigger button drives these on the UI thread).
|
||
// The request is handed to process() via a lock-free single-slot mailbox drained at block
|
||
// start — no allocation, no lock on the audio thread. previewNoteOn plays `note` at the
|
||
// current previewVelocity(); previewNoteOff releases it (Gate) — Trigger zones ignore
|
||
// note-off and play through. A momentary button (down = on, up = off) reads as a natural
|
||
// key press. This is PLAYBACK ONLY: it never captures, never inserts a timeline item.
|
||
void previewNoteOn(int note);
|
||
void previewNoteOff(int note);
|
||
|
||
private:
|
||
// Apply `mode` to the output audio bus's SpeakerArrangement (kMono / kStereo). Called from
|
||
// initialize (topology) and setChannelMode (runtime change). Does NOT re-negotiate — the
|
||
// caller drives restartComponent when appropriate.
|
||
void applyOutputArrangement(ChannelMode mode);
|
||
|
||
// Phase S drain retirement (FA1-review Major #2): if process() has published that the
|
||
// CURRENT drain instrument is fully idle (every engine voice + the preview card silent),
|
||
// move it out of the drain slot into the graveyard and prune — so an edited-away snapshot
|
||
// stops costing resident memory as soon as its tails die, instead of squatting in the slot
|
||
// until the NEXT reload. Off the audio thread only (takes reloadMutex_); driven from
|
||
// pollBankSync's UI-timer tick (the same cadence that drives reloads — an idle drain with
|
||
// no editor open simply waits for the next reload/deactivate, exactly the pre-fix bound).
|
||
// Safe against a racing process(): idleness is monotone (the drain receives no note-ons)
|
||
// and the published value names the drain's OWN installedAt, so a stale publication about
|
||
// an OLDER drain can never retire a newer one; the graveyard prune's monotone-generation
|
||
// proof (see below) covers the free.
|
||
void retireIdleDrain();
|
||
|
||
// Phase S voice-param LIGHT rebuild (voice-review Major #3): rebuild the engine + preview
|
||
// card around a COPY of the LIVE instrument's already-decoded Keymap — no bridge read, no
|
||
// filesystem, no WAV re-decode — and publish through the same tail-preserving drain-slot
|
||
// swap as a full reload. A polyphony/mode/trigger change touches no audio data, so the
|
||
// full reloadFromBank (which re-decodes every zone WAV from disk on the UI thread) was
|
||
// pure waste — a visible UI stall on a many-zone instrument. Copying the keymap is safe:
|
||
// it is immutable after construction and, under reloadMutex_, the live instrument can
|
||
// neither be swapped nor freed while we read it. When nothing is loaded this is a no-op —
|
||
// the new params bake into the next real reload. Off the audio thread only.
|
||
void rebuildVoiceEngine();
|
||
|
||
// Publish `built` (null = install silence) into live_: prune the graveyard by the last
|
||
// process()-published generation, swap `built` into live_, displace the previous live into
|
||
// the drain slot, and park the drain-evicted instrument in the graveyard. REQUIRES
|
||
// reloadMutex_ held — factored out so reloadFromBank and rebuildVoiceEngine share the ONE
|
||
// safety-critical swap dance (see the handoff proof below).
|
||
void publishBuiltLocked(std::unique_ptr<LoadedInstrument> built);
|
||
|
||
ReaperBridge bridge_;
|
||
|
||
// --- The audio-thread handoff (S4 real-time discipline, FA1 drain slot) --
|
||
// process() atomically loads `live_` AND `draining_` at block start and marshals/renders
|
||
// against them — two atomic acquires, no lock, no free on the audio thread.
|
||
//
|
||
// reloadFromBank() (off-thread, serialized by reloadMutex_) builds a new
|
||
// LoadedInstrument and atomically swaps it into `live_`. The DISPLACED instrument is
|
||
// NOT freed and NOT silenced: it moves into `draining_`, where process() keeps
|
||
// rendering its already-sounding voices (and routes note-offs to it) so a reload —
|
||
// a curve/param edit, a bank-generation refresh, an applied assignment — never cuts a
|
||
// ringing note (FA1, bug 3b). New note-ons go ONLY to the live instrument, so the next
|
||
// trigger plays the new state. The instrument evicted FROM the drain slot (two reloads
|
||
// old) is parked in `graveyard_` for reclaim — a rapid second reload hard-cuts only the
|
||
// oldest edit's tails (bounded compromise, documented).
|
||
//
|
||
// Bounded reclaim: process() publishes the MINIMUM installedAt over the (non-null)
|
||
// pointers it holds this block via processGeneration_ — a single atomic store, RT-safe.
|
||
// The reload path frees graveyard entries whose installedAt < seen (the last published
|
||
// value).
|
||
//
|
||
// Safety argument: both slots are monotone in installedAt over time (live_ receives
|
||
// successively newer builds; draining_ receives successively newer displaced lives), so
|
||
// the published minimum is monotone across blocks, and any future process() load yields
|
||
// installedAt >= seen. An entry only reaches the graveyard by leaving BOTH slots
|
||
// (single-writer under reloadMutex_), so a graveyard entry with installedAt < seen can
|
||
// never again be loaded and is not currently held — freeing it is safe. process()
|
||
// publishes BEFORE rendering, so the pointers it renders with are covered by the value
|
||
// the pruner reads (a stale lower read is merely conservative).
|
||
//
|
||
// The graveyard's upper bound is the number of reloads since process last ran
|
||
// (typically 0–1 in normal use). Remaining entries drain at setActive(false) /
|
||
// terminate(), when the host guarantees process is 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)
|
||
// Phase S: the installedAt of the drain instrument process() last observed FULLY IDLE
|
||
// (0 = none / the current drain 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
|
||
|
||
// The single-capture selection id (S10: the ONE picked capture; "" = no pick -> silence).
|
||
// Off-thread only; a small mutex guards the string against a getState/editor race. NOT
|
||
// read on the audio thread.
|
||
std::mutex selectionMutex_;
|
||
std::string selectedSampleId_;
|
||
|
||
// The performance map (Tier 1: the instrument's owned zoned keymap). Off-thread only;
|
||
// guarded against a getState/editor race. NOT read on the audio thread — reloadFromBank
|
||
// bakes it into the LoadedInstrument's Keymap under the reload lock.
|
||
std::mutex performanceMutex_;
|
||
PerformanceMap performanceMap_;
|
||
|
||
// The per-instance channel mode (S7). Off-thread only (UI + getState + reloadFromBank);
|
||
// guarded against a getState/editor race. Default Mono preserves pre-S7 behavior. NOT read
|
||
// on the audio thread — process renders against the host's negotiated output channel count.
|
||
std::mutex channelModeMutex_;
|
||
ChannelMode channelMode_ = ChannelMode::Mono;
|
||
|
||
// The last assignment-request generation this instance CONSUMED (S8 reader). Persisted in
|
||
// component state (v5) so a re-open does not re-apply a request the user already got and
|
||
// then changed away from. Written by pollBankSync (UI/timer thread) and getState; read by
|
||
// pollBankSync + getState; seeded by setState. Guarded against a getState/poll race. NEVER
|
||
// read on the audio thread. 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 (S9 reader). UI/timer-thread only (pollBankSync
|
||
// is the sole reader/writer) — no mutex needed, and it is NOT persisted. Initialized to a
|
||
// -1 SENTINEL (no real generation can be negative — parseBankGeneration yields >= 0) so the
|
||
// FIRST poll after an editor open BASELINES the seen value without a redundant reload (setState
|
||
// already loaded the current bank); a subsequent generation CHANGE then drives the reload.
|
||
// NOT read on the audio thread.
|
||
std::int64_t lastSeenBankGeneration_ = -1;
|
||
|
||
// S-VIEW-4 preview-trigger velocity (MIDI 1..127). Persisted in component state (v6) so the
|
||
// user's chosen strike velocity survives a project save/reload. Since Wave 2 the Sample-view
|
||
// velocity knob writes it on the UI thread, so it is guarded by previewMutex_; setState and
|
||
// getState (load/save thread) share the same guard. Default kPreviewVelocityDefault (64). NOT
|
||
// read on the audio thread.
|
||
std::mutex previewMutex_;
|
||
std::uint8_t previewVelocity_ = kPreviewVelocityDefault;
|
||
|
||
// Phase S voice-system parameters (per-instance, persisted in component state v7). Off-thread
|
||
// only (UI voice deck + getState/setState + reloadFromBank); guarded against a getState/editor
|
||
// race. Defaults {16, Poly, Retrigger} reproduce pre-Phase-S behavior. NOT read on the audio
|
||
// thread — reloadFromBank bakes them into the LoadedInstrument's engine off-thread.
|
||
std::mutex voiceParamsMutex_;
|
||
int voiceCount_ = kDefaultVoiceCount;
|
||
VoiceMode voiceMode_ = VoiceMode::Poly;
|
||
MonoTrigger monoTrigger_ = MonoTrigger::Retrigger;
|
||
|
||
// FB1 post-mixer master gain (LINEAR; persisted in component state v8). A lock-free
|
||
// atomic — the ONE voice-param the audio thread reads directly (a single relaxed load
|
||
// per block, applied as a post-sum multiply). Default unity = pre-FB1 output.
|
||
std::atomic<float> masterGain_{1.0f};
|
||
|
||
// --- S-VIEW-4 preview-trigger mailbox (off-thread -> audio thread, lock-free) ---------
|
||
// The editor's preview-trigger button posts a note-on/off request from the UI thread; process()
|
||
// drains it at block start and drives the live instrument's PREVIEW CARD (Phase S — never the
|
||
// MIDI pool). ONE slot per direction, each a packed
|
||
// request whose high bits are a monotonically-incrementing sequence so process() detects a NEW
|
||
// request by comparing against the last sequence it consumed (never re-firing a stale one). The
|
||
// low 8 bits carry the note (on) / note (off); the on request also carries the velocity in the
|
||
// next 8 bits, latched at post time so the audio thread reads no shared velocity field. A single
|
||
// relaxed atomic load per block on the audio thread — RT-safe (no alloc, no lock).
|
||
// packed = (seq << 16) | (velocity << 8) | note [note-on]
|
||
// packed = (seq << 16) | note [note-off]
|
||
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 so setActive/reload can size against it. Read
|
||
// off-thread only. 0.0 is explicitly invalid — setupProcessing sets the real host rate
|
||
// before any audio, and reloadFromBank guards on it before use.
|
||
double sampleRate_ = 0.0;
|
||
Steinberg::int32 maxBlockSize_ = 4096;
|
||
|
||
// --- S6 embedded TCP/MCP UI ---------------------------------------------
|
||
// The embed shell (IReaperUIEmbedInterface), created lazily on the first queryInterface
|
||
// and owned here for the processor's lifetime. REAPER borrows AddRef'd references from
|
||
// queryInterface; the shell's refcount is a no-op because THIS unique_ptr governs its
|
||
// destruction (the processor always outlives the borrowed references).
|
||
std::unique_ptr<ReaSamplerEmbed> embed_;
|
||
|
||
// The per-block mono peak (0..1+) the audio thread stores relaxed; the embed strip's
|
||
// level indicator reads it via embedActivityLevel(). Advisory only — a plain atomic,
|
||
// no ordering coupling, never guarded by a lock the audio thread touches.
|
||
std::atomic<float> embedPeak_{0.f};
|
||
};
|
||
|
||
} // namespace reasampler::vst
|