feat(S9/S8): bank-generation change-detection + instrument-side assignment reader

Extension stamps a monotonic bank_generation counter, bumped at content
mutations; the VST3 instrument polls it off-thread on an editor timer and
consumes assignment requests, refreshing playback hands-free.
This commit is contained in:
2026-07-26 23:46:06 -04:00
parent 725f3e7d3c
commit 66d47fb410
21 changed files with 838 additions and 36 deletions
+50
View File
@@ -38,6 +38,14 @@ namespace {
#ifdef _WIN32
constexpr const wchar_t* kChildClassName = L"ReaSampler9000VstEditor";
// The S9/S8 change-detection poll (WM_TIMER on the child window). A low-frequency UI-thread
// timer: responsive enough that a recapture/ingest/assign refreshes "within a bounded cadence"
// (the S9 verify criterion) yet cheap — three small ext-state reads per tick, coalescing many
// bumps between ticks into one reload. 500 ms is a deliberate build-time residual: fast enough
// to feel hands-free, slow enough to be free. The id is a per-window SetTimer id (any nonzero).
constexpr UINT_PTR kSyncTimerId = 1;
constexpr UINT kSyncTimerIntervalMs = 500;
// Top-level band metrics (shell arithmetic — the load-bearing card/tab/key/zone geometry
// is in capture_browser / keyboard_strip). The title band names the plugin + a live
// readout; the toggle band carries the Browser/Zones switch; the setup band (single-
@@ -173,6 +181,35 @@ void ReaSamplerEditor::rebuildVisible() {
}
}
#ifdef _WIN32
// Windows-only (the WM_TIMER cadence + invalidate() are the Win32 child-window path). Declared
// under the same _WIN32 guard in the header; keep the definition guarded to match (D5 makes
// Windows the only build target, but the TU must still compile elsewhere).
void ReaSamplerEditor::onSyncTimer() {
// UI thread (WM_TIMER). Poll the S9 bank generation + the S8 assignment request via the
// processor (off the audio thread — the poll itself never touches process()). NEVER while a
// drag is in flight: a reload mid-drag would rebuild the instrument and repaint under the
// user's cursor, yanking the edit. The next tick (500 ms) picks up the change after release.
if (!processor_) return;
if (drag_ != DragKind::kNone) return; // defer past the in-flight edit
// An open editor marks THIS instance the focused assignment target (the thundering-herd
// policy — only an editor-open instance applies a pending assign; see the handoff). Pass
// true so this instance consumes the request; instances with no editor open do not poll at
// all (the timer is bound to the child window), so they never contend for the request.
const ReaSamplerProcessor::BankSyncResult r = processor_->pollBankSync(/*isFocusedTarget=*/true);
// Re-snapshot the editor's own view only when something changed (a reload from a bank
// content change, or an applied assignment). refreshFromBank re-reads the bank blob + the
// processor's (possibly just-updated) selection/map and drops the stale thumbnail/PCM
// caches, then repaints — so the browser + setup surface reflect the new bank hands-free.
if (r.reloaded || r.applied) {
refreshFromBank();
invalidate();
}
}
#endif // _WIN32
void ReaSamplerEditor::commitAndReload() {
// UI thread only. Publish the edited selection + zones to the processor, then rebuild
// the instrument off the audio thread (reloadFromBank bakes them into the live Keymap).
@@ -372,11 +409,21 @@ void ReaSamplerEditor::attachedToParent() {
r.getWidth(), r.getHeight(), parent, nullptr, hInst, nullptr);
if (childHwnd_) {
SetWindowLongPtr(childHwnd_, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
// Start the S9/S8 change-detection poll (UI thread). Tied to the child window's
// lifetime — created here, killed in removedFromParent — so an instance whose editor
// is closed does NOT poll (the editor-open-only cadence; see the handoff limitation).
SetTimer(childHwnd_, kSyncTimerId, kSyncTimerIntervalMs, nullptr);
// Poll ONCE immediately so a pending assignment (an S8 ingest fired while this editor
// was closed) or a bank change applies the instant the editor opens, rather than waiting
// up to one timer interval. refreshFromBank above already primed the view; this folds in
// any pending assign/generation so the just-opened editor shows the assigned capture.
onSyncTimer();
}
}
void ReaSamplerEditor::removedFromParent() {
if (childHwnd_) {
KillTimer(childHwnd_, kSyncTimerId); // stop the poll before the window goes away
DestroyWindow(childHwnd_);
childHwnd_ = nullptr;
}
@@ -1063,6 +1110,9 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam,
self->invalidate();
}
return 0;
case WM_TIMER:
if (self && wParam == kSyncTimerId) self->onSyncTimer();
return 0;
case WM_ERASEBKGND:
return 1; // fully repaint in WM_PAINT; skip the flicker-inducing erase
default: