Cut shell/instrument comment bloat ~34% (comments only, zero code change)
This commit is contained in:
@@ -1,28 +1,9 @@
|
||||
// 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.
|
||||
//
|
||||
// SELF-CONTAINED PLAYBACK (pS architecture correction). The instance OWNS its sample: the
|
||||
// component state persists, per referenced bank sample, the project-relative WAV path +
|
||||
// decode intrinsics (SampleRefs), and reloadInstrument decodes straight from that table.
|
||||
// The extension's bank blob is a BROWSER SOURCE that opportunistically refreshes the refs
|
||||
// when readable — NEVER a runtime requirement for playback. A project restored before the
|
||||
// extension's PROJEXTSTATE parses (or with the extension absent) plays on load; the old
|
||||
// reopen-heal timer + poll-to-play machinery that papered over the bank dependency is gone.
|
||||
//
|
||||
// REAL-TIME DISCIPLINE (S4 hard constraint). The audio thread (process) does NO
|
||||
// allocation, NO file I/O, NO bridge calls, NO locks. Sample loading — ref resolve, WAV
|
||||
// decode, keymap build, VoiceEngine construction — all happens OFF the audio thread
|
||||
// (reloadInstrument, driven from the main/UI thread) and is handed to process via a
|
||||
// single atomic pointer swap. See the LoadedInstrument handoff below.
|
||||
// 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
|
||||
|
||||
@@ -42,37 +23,24 @@
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
// Cross-subsystem deps by their real namespace homes (Q-W2v: the core/namespaces.h shim
|
||||
// is retired from the processor family; the engine family's symbols — Keymap, VoiceEngine,
|
||||
// ChannelMode, VoiceMode, MonoTrigger, the voice-count constants — still live in flat
|
||||
// `reasampler` and resolve via the enclosing namespace).
|
||||
using instrument::map::ComponentState;
|
||||
using instrument::map::PerformanceMap;
|
||||
using instrument::map::SampleRefs;
|
||||
using instrument::map::kPreviewVelocityDefault;
|
||||
|
||||
class ReaSamplerEmbed; // S6 embedded TCP/MCP UI shell (owned below; see queryInterface)
|
||||
class ReaSamplerEmbed; // 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 references 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.
|
||||
// Decoded keymap + the voice engine playing it. The engine holds references into the
|
||||
// keymap, 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 {
|
||||
Keymap keymap;
|
||||
VoiceEngine engine;
|
||||
std::uint64_t installedAt = 0; // reload generation at which this was installed
|
||||
std::uint64_t installedAt = 0; // reloadGeneration_ at which this was installed into live_
|
||||
|
||||
// The takeover declick (GA fix, rev 2) is opted IN here — the PRODUCT default: any
|
||||
// restart of a sounding voice (mono Retrigger takeover/fallback, cross-sample legato
|
||||
// restart, POLY at-cap steal — the preview note included, now that it is a real pool
|
||||
// voice) smooths the cut via the difference-seeded ramp instead of clicking. The pure
|
||||
// core defaults it off (regression baseline) — same layering as kDefaultPitchEngine.
|
||||
// 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(Keymap km, std::size_t maxVoices,
|
||||
std::uint64_t gen, std::size_t preserveVoiceCap = 0,
|
||||
std::int64_t preserveWindowFrames = 0,
|
||||
@@ -83,9 +51,8 @@ struct LoadedInstrument {
|
||||
voiceMode, monoTrigger, /*takeoverDeclick=*/true),
|
||||
installedAt(gen) {}
|
||||
|
||||
// True when nothing in this snapshot is sounding. 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).
|
||||
// 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;
|
||||
@@ -95,8 +62,8 @@ struct LoadedInstrument {
|
||||
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).
|
||||
// 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).
|
||||
@@ -109,9 +76,9 @@ 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.
|
||||
// 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;
|
||||
|
||||
@@ -122,11 +89,9 @@ public:
|
||||
Steinberg::tresult PLUGIN_API process(
|
||||
Steinberg::Vst::ProcessData& data) override;
|
||||
|
||||
// Output-bus negotiation. The instrument has ONE canonical output arrangement: a FIXED
|
||||
// stereo bus (GA fix — the channel mode is a decode policy, never a bus fact; mono mode
|
||||
// renders dual-mono through it). We accept the host's proposal only when it is a single
|
||||
// stereo output; otherwise we reject (kResultFalse) but keep our stereo arrangement, so
|
||||
// getBusArrangement / getBusInfo always report 2 channels and the host routes accordingly.
|
||||
// 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;
|
||||
@@ -135,107 +100,73 @@ public:
|
||||
// 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.
|
||||
// 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), 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.
|
||||
// The embedded-strip activity level (0..1) for the embed shell, UI thread. Backed by
|
||||
// embedPeak_, a lock-free relaxed atomic the audio thread writes each block.
|
||||
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. SELF-CONTAINED (pS): resolves the selection/zones against the instance-OWNED
|
||||
// SampleRefs table, decodes each WAV OFF the audio thread, and publishes the built
|
||||
// instrument to process() via an atomic swap — NO bank read is required for playback.
|
||||
// When the live bank blob IS readable it is first folded into the refs table
|
||||
// (refreshRefsFromBank), which is both the browser's copy-the-ref-in mechanism and the
|
||||
// S9 live-recapture sync. A missing/unreadable WAV is the defined no-play (silence, no
|
||||
// retry). Returns the resolved selection id ("" if nothing was loaded) for the editor.
|
||||
// Resolves selection/zones against the instance-owned SampleRefs, decodes each 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 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.
|
||||
// What pollBankSync did this tick, so the editor can react only when something changed.
|
||||
struct BankSyncResult {
|
||||
// The bank generation changed (or a pre-v10 legacy lift landed an instrument) ->
|
||||
// reloadInstrument ran and the editor should re-snapshot its bank view.
|
||||
bool reloaded = false;
|
||||
bool reloaded = false; // bank generation changed (or a legacy lift landed) -> reloaded
|
||||
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()). This is an
|
||||
// EDITOR/BROWSER sync path — playback never depends on it (pS). Semantics:
|
||||
// * S9: if the bank generation differs from what we last saw, call reloadInstrument() 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.
|
||||
// * LEGACY LIFT: a pre-v10 blob restored with intent but no refs retries the (cheap)
|
||||
// bank read until the blob is parseable, then reloads ONCE to copy the refs in.
|
||||
// TERMINATING: once the blob parses and NO referenced id resolves, the ids are
|
||||
// provably stale — the lift concludes permanently (legacyLiftShouldRun) instead of
|
||||
// churning a full bank read + reload every tick forever.
|
||||
// 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).
|
||||
// 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 SAME rate reloadInstrument
|
||||
// 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.
|
||||
// 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 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.
|
||||
// The single-capture selection id (guarded by selectionMutex_, never read on the audio
|
||||
// thread): the default face's pick when the performance map is empty; a non-empty map
|
||||
// supersedes it. Empty id -> silence, no first-sample fallback.
|
||||
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 — reloadInstrument bakes it into the LoadedInstrument's Keymap off-thread.
|
||||
// The performance map (zoned keymap). UI thread, guarded by performanceMutex_; never
|
||||
// read on the audio thread — reloadInstrument bakes it into the 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/reloadInstrument; guarded by
|
||||
// channelModeMutex_. NEVER read on the audio thread — process() renders against the host's
|
||||
// negotiated output channel count, and reloadInstrument bakes the mode into the decode.
|
||||
// GA fix: the mode is a DECODE policy only (downmix vs L/R split). The output bus is a
|
||||
// FIXED stereo bus — mono mode renders dual-mono through it (centered) — so a mode change
|
||||
// never renegotiates host I/O (the mono<->stereo bus flip's live pin remap was the
|
||||
// hard-right-pan defect).
|
||||
// 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();
|
||||
// Sets the mode from the EDITOR TOGGLE (a deliberate user choice): latches the mode
|
||||
// EXPLICIT (the GA auto-default stops fighting it), and on a CHANGE reloads the instrument
|
||||
// so the next block decodes the new channel count. UI thread only.
|
||||
// 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);
|
||||
|
||||
// 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.
|
||||
// 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);
|
||||
|
||||
// --- 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.
|
||||
// Voice-system parameters (per-instance), guarded by voiceParamsMutex_, not read on the
|
||||
// audio thread — each setter rebuilds via rebuildVoiceEngine (already-decoded keymap, 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();
|
||||
@@ -243,251 +174,160 @@ public:
|
||||
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().
|
||||
// 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()]
|
||||
|
||||
// Fire a one-shot PREVIEW note-on / note-off through the live instrument's MAIN
|
||||
// VoiceEngine — the SAME noteOn/noteOff calls host MIDI takes, so a preview is a REAL
|
||||
// voice: it counts against the voice count, can steal / be stolen, and respects
|
||||
// Poly/Mono + Retrigger/Legato (deliberate reversal of the retired PreviewCard's
|
||||
// isolation — preview must obey voicing). The editor posts the loaded capture's /
|
||||
// selected zone's ROOT note (plays at unity); previewNoteOn plays it at the current
|
||||
// previewVelocity() (the velocity curve applies); previewNoteOff releases it (Gate) —
|
||||
// Trigger zones ignore note-off and play through. OFF the audio thread (the editor's
|
||||
// preview-trigger button, 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. 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.
|
||||
// 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);
|
||||
|
||||
// The instance-owned sample refs (pS self-contained playback): a snapshot copy for the
|
||||
// editor (waveform/loop-intrinsic fallback when the bank blob is not readable). UI
|
||||
// thread; guarded by refsMutex_.
|
||||
// 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();
|
||||
|
||||
private:
|
||||
// Phase S drain retirement (FA1-review Major #2): if process() has published that the
|
||||
// CURRENT drain instrument is fully idle (every engine voice 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.
|
||||
// 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();
|
||||
|
||||
// Phase S voice-param LIGHT rebuild (voice-review Major #3): rebuild the engine
|
||||
// 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 reloadInstrument (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.
|
||||
// Light voice-param rebuild: rebuilds the engine around a copy of the live instrument's
|
||||
// already-decoded Keymap (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();
|
||||
|
||||
// The pre-v10 LEGACY LIFT gate (#A): true when a lift attempt this tick could make
|
||||
// progress. Latches legacyLiftConcluded_ on a Stale proof (see the member below); the
|
||||
// pure decision itself is sample_map's legacyLiftDecision. Off the audio thread only
|
||||
// (bridge read + bank parse).
|
||||
// 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();
|
||||
|
||||
// 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 reloadInstrument and rebuildVoiceEngine share the ONE
|
||||
// safety-critical swap dance (see the handoff proof below).
|
||||
// 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);
|
||||
|
||||
// pS-usage: publish this instance's held captures to its per-instance ext-state key
|
||||
// ("rsusage_<instanceGuid>") so the extension's prune counts them as referenced — a
|
||||
// capture a live instance holds can never be pruned. Called at the end of every
|
||||
// reloadInstrument (the ONE choke point every play-set change funnels through:
|
||||
// selection change, zone edits, assignment consume, bank refresh, setState load), so
|
||||
// publishing is EAGER and needs no timer — a closed-editor instance's record is
|
||||
// already in ext-state from its last change/load. OFF THE AUDIO THREAD only (bridge
|
||||
// calls). Mints instanceGuid_ on first need; RE-mints when planUsagePublish detects
|
||||
// this state was cloned onto another track (FX copy / track duplication). Idempotent
|
||||
// on an unchanged play-set (skipWrite). `refs`/`ids` are reloadInstrument's own
|
||||
// snapshot — the refs table and the id set the instance currently plays.
|
||||
// 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 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.
|
||||
// --- 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.
|
||||
//
|
||||
// reloadInstrument() (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).
|
||||
// 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.
|
||||
//
|
||||
// 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.
|
||||
// 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)
|
||||
// Phase S: the installedAt of the drain instrument process() last observed FULLY IDLE
|
||||
// (every engine voice silent; 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.
|
||||
// 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
|
||||
|
||||
// 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.
|
||||
// The single-capture selection id ("" = no pick -> silence). Off-thread only, 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 — reloadInstrument
|
||||
// bakes it into the LoadedInstrument's Keymap under the reload lock.
|
||||
// The performance map (zoned keymap). Off-thread only; reloadInstrument bakes it into
|
||||
// the Keymap under the reload lock, never read directly on the audio thread.
|
||||
std::mutex performanceMutex_;
|
||||
PerformanceMap performanceMap_;
|
||||
|
||||
// The instance-OWNED sample refs (pS self-contained playback): the path + intrinsics
|
||||
// per referenced bank sample that setState restores, reloadInstrument resolves/decodes
|
||||
// from, and getState persists (v10). Refreshed opportunistically from the bank blob
|
||||
// when it is readable; NEVER a bank dependency for playback. Off-thread only (UI +
|
||||
// load/save + reload); guarded against a getState/reload race. NOT read on the audio
|
||||
// thread.
|
||||
// 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_;
|
||||
|
||||
// pS-usage publish identity + lifetime nonce (see publishUsage). instanceGuid_ is
|
||||
// the persisted per-instance identity (ComponentState v11; empty until first
|
||||
// publish); usageNonce_ is THIS incarnation's per-LIFETIME owner nonce, carried
|
||||
// INSIDE the published wire (UsageRecord.ownerNonce) — planUsagePublish's exact
|
||||
// ownership discriminator between "my own write" (clean replace) and "a foreign
|
||||
// writer" (union / re-mint). NEVER persisted: a persisted nonce would clone with
|
||||
// the state on FX copy, and two same-track copies converging on byte-identical
|
||||
// wires is exactly the ambiguity the nonce exists to break (a wire-equality
|
||||
// discriminator let sibling A clean-replace over sibling B's still-held paths —
|
||||
// the delete direction). Minted lazily on first publish; cleared on setState (a
|
||||
// restored blob is a new lifetime). Guarded by usageMutex_ (publish runs under
|
||||
// reloadMutex_ but getState/setState do not).
|
||||
// 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_;
|
||||
|
||||
// The per-instance channel mode (S7). Off-thread only (UI + getState + reloadInstrument);
|
||||
// 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.
|
||||
// channelModeExplicit_ (GA, persisted v9): false = the mode is an un-touched default that
|
||||
// reloadInstrument may auto-default from the loaded capture's channel count; true = the user
|
||||
// deliberately toggled the mode (setChannelMode latches it) and it is never fought.
|
||||
// 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 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.
|
||||
// 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 (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 instrument from the OWNED refs); a subsequent generation
|
||||
// CHANGE then drives the reload. Since pS there is NO reopen-heal here: playback never
|
||||
// depends on this poll — a v10 blob plays from its own refs at setState time. Besides a
|
||||
// generation change, pollBankSync reloads only for an APPLIED S8 assignment and for the
|
||||
// pre-v10 LEGACY LIFT. NOT read on the audio thread.
|
||||
// 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;
|
||||
|
||||
// The pre-v10 LEGACY LIFT's terminating latch (#A): set once legacyLiftShouldRun proves
|
||||
// the referenced ids STALE against a readable bank blob (LegacyLiftDecision::Stale) —
|
||||
// there is nothing to lift, so the lift stops re-firing (the steady state is one relaxed
|
||||
// load per tick, no bank read). Reset by setState (a new blob = new facts). NOT consulted
|
||||
// by the genChanged/applied reload paths, so a later bank change that re-introduces an id
|
||||
// (e.g. an extension-side undo) still refreshes the refs — the latch only gates the lift.
|
||||
// Atomic: written on the UI-timer thread (pollBankSync) and the host load thread (setState).
|
||||
// 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};
|
||||
|
||||
// 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.
|
||||
// Preview-trigger velocity (MIDI 1..127, persisted). 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 + reloadInstrument); guarded against a getState/editor
|
||||
// race. Defaults {16, Poly, Retrigger} reproduce pre-Phase-S behavior. NOT read on the audio
|
||||
// thread — reloadInstrument bakes them into the LoadedInstrument's engine off-thread.
|
||||
// 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;
|
||||
|
||||
// FB1 post-mixer master gain (LINEAR; persisted in component state v8). A lock-free
|
||||
// atomic — the target the UI thread writes; the audio thread ramps gainCurrent_ toward
|
||||
// it per-sample each block (linear interpolation, ~20 ms wall-clock at every host rate)
|
||||
// so sudden knob moves produce no zipper noise and the true-zero bottom causes no click.
|
||||
// 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};
|
||||
// The audio-thread running gain value: tracks masterGain_ across blocks, stepping at
|
||||
// most gainRampStep_ per sample toward the target. Starts at unity (pre-FB1 default).
|
||||
// Written and read exclusively on the audio thread — no atomics needed.
|
||||
// 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;
|
||||
// T3-01: the per-sample ramp step, derived from kGainRampSeconds (20 ms wall-clock)
|
||||
// against the live host rate in setupProcessing — never a baked-in rate. The default is
|
||||
// the 48 kHz value so behavior before the first setupProcessing is unchanged. Written in
|
||||
// setupProcessing (host-serialized against process), read on the audio thread.
|
||||
// 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;
|
||||
|
||||
// --- 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 MAIN VoiceEngine — the same
|
||||
// noteOn/noteOff host MIDI takes, so the preview obeys voicing. 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]
|
||||
// --- 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)
|
||||
@@ -495,22 +335,17 @@ private:
|
||||
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 reloadInstrument guards on it before use.
|
||||
// Latched from setupProcessing; 0.0 is explicitly invalid (reloadInstrument guards on it).
|
||||
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).
|
||||
// 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 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.
|
||||
// Per-block mono peak the audio thread stores relaxed; embedActivityLevel() reads it
|
||||
// for the embed strip's level indicator. Advisory only.
|
||||
std::atomic<float> embedPeak_{0.f};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user