03e760631c
Inline keymap/level strip drawn into REAPER's embed bitmap, reusing the LICE idiom. Pure embed_strip layout/hit-test (tested); processor exposes the embed interface off queryInterface and publishes an RT-safe block peak for the level.
199 lines
11 KiB
C++
199 lines
11 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 and the voice
|
||
// engine that plays it. The engine holds a reference into the keymap, so the two 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;
|
||
std::uint64_t installedAt = 0; // reload generation at which this was installed
|
||
|
||
LoadedInstrument(Keymap km, std::size_t maxVoices, const AdsrParams& adsr,
|
||
std::uint64_t gen)
|
||
: keymap(std::move(km)), engine(maxVoices, keymap, adsr), installedAt(gen) {}
|
||
|
||
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;
|
||
|
||
//--- 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 bridge, for the editor's live-state readout + sample list. Owned here; the
|
||
// editor borrows it (outlives the editor).
|
||
ReaperBridge& bridge() { return bridge_; }
|
||
// The current selection id (main/UI thread reads for the editor). Guarded by
|
||
// selectionMutex_ — never touched on the audio thread. In Tier 1 the selection is the
|
||
// Tier-0 FALLBACK sample (played chromatically when the performance map is empty); the
|
||
// zoned map, when non-empty, 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);
|
||
|
||
private:
|
||
ReaperBridge bridge_;
|
||
|
||
// --- The audio-thread handoff (S4 real-time discipline) -----------------
|
||
// process() atomically loads `live_` at block start and marshals/renders against it —
|
||
// a single atomic acquire, 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 on the reload path: process() may still be mid-block reading it, and two
|
||
// rapid reloads could otherwise free a pointer process is using. Instead it is parked
|
||
// in `graveyard_` tagged with the reload generation at which it was displaced.
|
||
//
|
||
// Bounded reclaim: process() publishes inst->installedAt (the generation at which the
|
||
// held instrument was installed) via processGeneration_ — a single atomic store, RT-
|
||
// safe. The reload path prunes graveyard entries where displacedAt <= seen (where seen
|
||
// is the last published processGeneration_).
|
||
//
|
||
// Safety argument: an entry with displacedAt == D was displaced by reload D, which
|
||
// simultaneously installed its successor with installedAt == D. process() publishing
|
||
// seen == D means it holds that successor (or a later one). In either case, the
|
||
// displaced entry is not the pointer process is using, so freeing it is safe. The
|
||
// pruning condition is therefore <= (not strict <): an entry displaced at exactly the
|
||
// published generation is also provably unreachable.
|
||
//
|
||
// 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<std::uint64_t> reloadGeneration_{0}; // incremented by each reload (off-thread, under reloadMutex_; read atomically by process)
|
||
std::atomic<std::uint64_t> processGeneration_{0}; // generation last seen by process (written on audio thread, read off-thread)
|
||
struct GraveyardEntry {
|
||
std::uint64_t displacedAt = 0; // reloadGeneration_ value when this was displaced
|
||
std::unique_ptr<LoadedInstrument> instrument;
|
||
};
|
||
std::vector<GraveyardEntry> graveyard_; // drained on reclaim + setActive(false) + terminate
|
||
std::mutex reloadMutex_; // serializes off-thread reloads + graveyard access
|
||
|
||
// The selected sample id (Tier-0 fallback sample). 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_;
|
||
|
||
// Latched from setupProcessing so setActive/reload can size against it. Read
|
||
// off-thread only.
|
||
double sampleRate_ = 44100.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
|