Files
reasampler/src/vst/reasampler_processor.h
T
daniel 03e760631c S6: embedded TCP/MCP UI via IReaperUIEmbedInterface
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.
2026-07-27 04:09:49 -04:00

199 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 01 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