self-contained playback — ComponentState v10 SampleRefs (path+intrinsics owned by the instance), reloadInstrument decodes bank-free, heal timer + poll-to-play removed; bank is a browser source
This commit is contained in:
@@ -10,11 +10,19 @@
|
||||
// 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 — 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.
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -131,27 +139,35 @@ public:
|
||||
}
|
||||
|
||||
// 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();
|
||||
// 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.
|
||||
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.
|
||||
struct BankSyncResult {
|
||||
bool reloaded = false; // the bank generation changed -> reloadFromBank ran
|
||||
// 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 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).
|
||||
// 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.
|
||||
// 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).
|
||||
@@ -162,7 +178,7 @@ public:
|
||||
// editor borrows it (outlives the editor).
|
||||
ReaperBridge& bridge() { return bridge_; }
|
||||
|
||||
// The live host sample rate latched from setupProcessing (the SAME rate reloadFromBank
|
||||
// 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_
|
||||
@@ -177,14 +193,14 @@ public:
|
||||
|
||||
// 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.
|
||||
// audio thread — reloadInstrument 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
|
||||
// (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 reloadFromBank bakes the mode into the decode.
|
||||
// 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
|
||||
@@ -240,32 +256,12 @@ public:
|
||||
void previewNoteOn(int note);
|
||||
void previewNoteOff(int note);
|
||||
|
||||
// Reopen-heal retry tick — the target of the module-internal Win32 heal timer (see
|
||||
// armHealRetry below), NOT host-facing. Public only because the file-static TIMERPROC
|
||||
// in the .cpp must reach it. Main thread; re-runs reloadFromBank (which disarms the
|
||||
// timer itself on success) and disarms when the bounded retry budget runs out.
|
||||
void healTick();
|
||||
// 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_.
|
||||
SampleRefs sampleRefs();
|
||||
|
||||
private:
|
||||
// --- Reopen-heal retry (the NON-editor reload trigger) --------------------------
|
||||
// A project-restored instance with intent (a selection or zones) can come up SILENT:
|
||||
// REAPER runs track-FX setState before the extension's PROJEXTSTATE block is parsed,
|
||||
// so the setState-time reload reads an empty bank. The editor's WM_TIMER poll heals
|
||||
// that — but only if the user opens the editor; an instance played via host MIDI with
|
||||
// the editor never attached stayed silent indefinitely. Mechanism: reloadFromBank
|
||||
// itself detects "built NOTHING despite restored intent, bridge connected" (off the
|
||||
// audio thread — it just tried) and arms a BOUNDED, HWND-less Win32 retry timer
|
||||
// (SetTimer + TIMERPROC: fires on the arming thread's message pump — REAPER's main
|
||||
// thread, where every reload path already runs). Each tick re-runs reloadFromBank,
|
||||
// which disarms on success or when the intent is gone; the bound stops the churn for
|
||||
// an instance whose WAV is genuinely missing. A deliberately-empty instance never
|
||||
// arms (no intent). process() is untouched — fully RT-safe. Main-thread only.
|
||||
// No-ops on non-Windows builds (the VST target is Windows-only).
|
||||
void armHealRetry();
|
||||
void disarmHealRetry();
|
||||
std::uintptr_t healTimerId_ = 0; // 0 = not armed (main thread only)
|
||||
int healRetriesLeft_ = 0; // remaining timer-tick retries (main thread only)
|
||||
|
||||
// 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
|
||||
@@ -283,7 +279,7 @@ private:
|
||||
// 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
|
||||
// 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 —
|
||||
@@ -293,7 +289,7 @@ private:
|
||||
// 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
|
||||
// reloadMutex_ held — factored out so reloadInstrument and rebuildVoiceEngine share the ONE
|
||||
// safety-critical swap dance (see the handoff proof below).
|
||||
void publishBuiltLocked(std::unique_ptr<LoadedInstrument> built);
|
||||
|
||||
@@ -303,7 +299,7 @@ private:
|
||||
// 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
|
||||
// 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 —
|
||||
@@ -349,16 +345,25 @@ private:
|
||||
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
|
||||
// guarded against a getState/editor race. NOT read on the audio thread — reloadInstrument
|
||||
// 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);
|
||||
// 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.
|
||||
std::mutex refsMutex_;
|
||||
SampleRefs sampleRefs_;
|
||||
|
||||
// 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
|
||||
// reloadFromBank may auto-default from the loaded capture's channel count; true = the user
|
||||
// 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.
|
||||
std::mutex channelModeMutex_;
|
||||
ChannelMode channelMode_ = ChannelMode::Mono;
|
||||
@@ -375,17 +380,12 @@ private:
|
||||
// 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.
|
||||
// REOPEN HEAL exception: when that setState-time load LEFT NOTHING LIVE despite restored
|
||||
// intent (a selection or zones) — the project-load ordering can run setState before the
|
||||
// extension's PROJEXTSTATE block is parseable, so the bridge read came back empty — the
|
||||
// first poll reloads instead of silently baselining, or the instrument would stay silent
|
||||
// until some param change forced a reload. If that heal reload STILL leaves nothing live,
|
||||
// pollBankSync resets this back to the -1 sentinel so the next tick re-arms the heal —
|
||||
// the retry is bounded by the intent check (a deliberately-empty instance never heals),
|
||||
// and it also covers a bank whose generation counter was never bumped (0 != 0 can never
|
||||
// fire the generation path). NOT read on the audio thread.
|
||||
// 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. The only
|
||||
// poll-driven reload besides a generation change is the pre-v10 LEGACY LIFT (see
|
||||
// pollBankSync). 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
|
||||
@@ -397,9 +397,9 @@ private:
|
||||
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
|
||||
// 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 — reloadFromBank bakes them into the LoadedInstrument's engine off-thread.
|
||||
// thread — reloadInstrument bakes them into the LoadedInstrument's engine off-thread.
|
||||
std::mutex voiceParamsMutex_;
|
||||
int voiceCount_ = kDefaultVoiceCount;
|
||||
VoiceMode voiceMode_ = VoiceMode::Poly;
|
||||
@@ -435,7 +435,7 @@ private:
|
||||
|
||||
// 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.
|
||||
// before any audio, and reloadInstrument guards on it before use.
|
||||
double sampleRate_ = 0.0;
|
||||
Steinberg::int32 maxBlockSize_ = 4096;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user