S4 Tier 0: the bank plays — VST3 marshals MIDI to the S3 core, reads the live bank + resolves WAV the M4 way, mono downmix, lock-free load handoff, LICE sample-pick
This commit is contained in:
@@ -1,21 +1,53 @@
|
||||
// reasampler_processor.h — the VST3 SingleComponentEffect skeleton (Phase S1). THIN
|
||||
// shell: an instrument that declares an event-input bus + a stereo audio-output bus,
|
||||
// sets up processing, and runs an empty (silent) process. Nothing plays yet — S4 wires
|
||||
// the pure sampler core into process(); S1 only proves REAPER hosts it.
|
||||
// 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 (verified: SDK class
|
||||
// reference). It gives us addAudioOutput/addEventInput and the IEditController seat, so
|
||||
// createView() can hand the host our IPlugView LICE editor.
|
||||
// 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 "sampler_core.h"
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
// 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.
|
||||
struct LoadedInstrument {
|
||||
Keymap keymap;
|
||||
VoiceEngine engine;
|
||||
|
||||
LoadedInstrument(Keymap km, std::size_t maxVoices, const AdsrParams& adsr)
|
||||
: keymap(std::move(km)), engine(maxVoices, keymap, adsr) {}
|
||||
|
||||
LoadedInstrument(const LoadedInstrument&) = delete;
|
||||
LoadedInstrument& operator=(const LoadedInstrument&) = delete;
|
||||
};
|
||||
|
||||
class ReaSamplerProcessor : public Steinberg::Vst::SingleComponentEffect {
|
||||
public:
|
||||
ReaSamplerProcessor() = default;
|
||||
@@ -30,10 +62,16 @@ public:
|
||||
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;
|
||||
// Empty in the spike: emits silence (S4 fills it).
|
||||
// Marshals MIDI -> VoiceEngine -> audio output. Real-time safe (no alloc/IO/lock).
|
||||
Steinberg::tresult PLUGIN_API process(
|
||||
Steinberg::Vst::ProcessData& data) override;
|
||||
|
||||
@@ -41,8 +79,50 @@ public:
|
||||
// Hands the host our LICE IPlugView editor.
|
||||
Steinberg::IPlugView* PLUGIN_API createView(Steinberg::FIDString name) override;
|
||||
|
||||
// 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.
|
||||
std::string selectedSampleId();
|
||||
void setSelectedSampleId(const std::string& id);
|
||||
|
||||
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_` and reclaimed only when process is GUARANTEED stopped — at
|
||||
// setActive(false) / terminate(), which the host never runs concurrently with
|
||||
// process. The graveyard grows by one engine per reload during a session (bounded by
|
||||
// user sample switches — a few objects), a deliberate leak-until-deactivate trade for
|
||||
// a lock-free, race-free audio thread. Tier 2 can add epoch-based reclaim if needed.
|
||||
std::atomic<LoadedInstrument*> live_{nullptr};
|
||||
std::vector<std::unique_ptr<LoadedInstrument>> graveyard_; // freed only when stopped
|
||||
std::mutex reloadMutex_; // serializes off-thread reloads + graveyard access
|
||||
|
||||
// The selected sample id (instance state). 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_;
|
||||
|
||||
// Latched from setupProcessing so setActive/reload can size against it. Read
|
||||
// off-thread only.
|
||||
double sampleRate_ = 44100.0;
|
||||
Steinberg::int32 maxBlockSize_ = 4096;
|
||||
};
|
||||
|
||||
} // namespace reasampler::vst
|
||||
|
||||
Reference in New Issue
Block a user