Merge dev into ux-w1-t1-sample-rate: take the spline live-drag and plugin-drop work
This commit is contained in:
@@ -84,6 +84,17 @@ The editor's `commitLive` is the tier-3 peer of `commitAndReload`; why it still
|
||||
parameter set is recorded at its declaration in `reasampler_editor.h`, and why `liveParams_` is
|
||||
declared ahead of the instrument slots at that member in `reasampler_processor.h`.
|
||||
|
||||
**The drawn-contour drag is the one gesture that publishes WITHOUT committing.** Each move
|
||||
couriers the editor's contours to `liveContours_` (`publishLiveContours`) and writes nothing
|
||||
else — no parameter set, no generation, no host callback — and its release is the
|
||||
`commitAndReload` it always was. That is sound because no contour is a host parameter, so no
|
||||
host-facing reader needs the model to carry it mid-gesture; it is what keeps a motion event off
|
||||
the host entirely (an unlatched `setInstrumentParams` opens a group edit even when nothing
|
||||
exposed moved). `reloadInstrument` republishes the model's contours, so a release, a click-edit
|
||||
(add/delete/hard) and a restore all reach the voices through the reload; capture-lost and the
|
||||
editor closing mid-drag (`removedFromParent`) are the two exits that commit nothing — both
|
||||
re-courier the restored `params_`.
|
||||
|
||||
**The VST3 parameter surface is a THIRD surface onto the one model, never a second copy.** The
|
||||
pure half — the frozen id table, the exposed set, the plain-value layer, the formatter — is
|
||||
`core/instrument/param` and is documented there; this directory only adapts it.
|
||||
@@ -109,7 +120,8 @@ pure half — the frozen id table, the exposed set, the plain-value layer, the f
|
||||
the audio thread and may allocate on the way; `process()` merges that block with the host's
|
||||
automation points into `automationLive_`, which is what `SampleData::live` points at. Two
|
||||
blocks rather than one because the seqlock's single-writer contract is load-bearing and the two
|
||||
writers genuinely differ in thread.
|
||||
writers genuinely differ in thread. The drawn-contour block `liveContours_` has no such pair:
|
||||
no host automates a contour, so the engines read it straight from its off-thread writers.
|
||||
|
||||
### THE AUTHORITY MODEL — who may write a parameter's value, and until when
|
||||
|
||||
@@ -123,6 +135,7 @@ the single authority.** Everything else that holds these values is a cache or a
|
||||
| Writer | Authority begins | Authority ends |
|
||||
|---|---|---|
|
||||
| Editor gesture (`commitLive` / `commitAndReload`) | mouse-down | the commit lands in the model |
|
||||
| Contour drag's courier (`publishLiveContours`) | mouse-down | never writes the model — the release's `commitAndReload` does |
|
||||
| Host controller write (`setParamNormalized`) | the call | the call returns (it writes the model) |
|
||||
| State restore (`setState`) | the call | the call returns |
|
||||
| Bake reset (`adoptBakedCapture`) | the call | the call returns |
|
||||
@@ -130,9 +143,9 @@ the single authority.** Everything else that holds these values is a cache or a
|
||||
| Reload seed (`reloadInstrument`) | never — it does not write `params_` | it only republishes a live block folded from whatever the model already holds |
|
||||
| **Host automation point** (`IParameterChanges`) | the block it lands in | **the UI thread has folded it into the model and republished** |
|
||||
|
||||
Every writer above except the last two writes the model directly, so for those "authority ends"
|
||||
is just "the write happened". Reload seed is not itself a model write — `reloadInstrument` never
|
||||
touches `params_`; the only write in the tree is `setInstrumentParams`'s, `processor_state.cpp:193`
|
||||
Every writer above except the contour courier and the last two writes the model directly, so for
|
||||
those "authority ends" is just "the write happened". Reload seed is not itself a model write —
|
||||
`reloadInstrument` never touches `params_`; the only write in the tree is `setInstrumentParams`'s, `processor_state.cpp:193`
|
||||
— which is why its row states no authority window of its own. The automation lane is the only one
|
||||
that cannot write directly: the SDK delivers it on the audio thread, where the model path
|
||||
allocates (`resolvePlay` copies velocity curves and spline contours). So it patches the
|
||||
@@ -229,13 +242,19 @@ against a performance budget — they are there because `VoiceEngine::applyLiveT
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **`reasampler_processor.h` no longer needs a ceiling exception, and the one it had rested on a
|
||||
false premise.** It was described as ONE class declaration; it also carried two namespace-scope
|
||||
aggregates (`MasterBusMeter`, `LoadedInstrument`) and the automation lane's own state. Both are
|
||||
now split out — `processor_snapshot.h` and `automation_channel.h`, on the same grounds
|
||||
`editor_interaction.h` was split out of `reasampler_editor.h` in this directory: neither is
|
||||
behaviour. What remains is under the ceiling. Its bulk is the drain-slot proof and the
|
||||
RT-discipline constraints, which the comment conventions name as keep-worthy.
|
||||
- **`reasampler_processor.h` carries a small, documented ceiling exception.** It was described as
|
||||
ONE class declaration; it also carried two namespace-scope aggregates (`MasterBusMeter`,
|
||||
`LoadedInstrument`) and the automation lane's own state. All three are split out —
|
||||
`processor_snapshot.h` and `automation_channel.h`, on the same grounds `editor_interaction.h`
|
||||
was split out of `reasampler_editor.h` in this directory: neither is behaviour. What remains is
|
||||
the class declaration itself, a handful of lines over the ceiling since the live-contour block
|
||||
(`liveContours_` + `publishLiveContours`) landed beside `liveParams_` under the same ordering
|
||||
constraint. No further seam exists that isn't behaviour: every remaining aggregate-shaped chunk
|
||||
(the preview mailbox, the drain-slot triple) is read and written inline by `process()` on the
|
||||
audio thread, so splitting it into its own header would either duplicate the RT-discipline
|
||||
comments at a second site or turn a plain member access into a cross-header one for no gain.
|
||||
Its bulk is the drain-slot proof and the RT-discipline constraints, which the comment
|
||||
conventions name as keep-worthy.
|
||||
|
||||
- **The bake click only ARMS; the editor's sync tick runs it.** Calling
|
||||
`Main_OnCommandEx` inline from `WM_LBUTTONDOWN` would run the extension's whole landing
|
||||
|
||||
@@ -178,6 +178,7 @@ bool ReaSamplerEditor::splineOverlayClick(const OverlayArea& waveArea, int x, in
|
||||
// At the ceiling: refused, contour untouched, nothing to roll back. Swallow the click
|
||||
// rather than letting it fall through to a marker grab under the cursor.
|
||||
if (index < 0) return true;
|
||||
publishContoursLive(); // the new point is heard before the first move, not after it
|
||||
}
|
||||
drag_ = DragKind::kSplineNode;
|
||||
curvePointIndex_ = index;
|
||||
@@ -188,7 +189,7 @@ bool ReaSamplerEditor::splineOverlayClick(const OverlayArea& waveArea, int x, in
|
||||
dragCurveRect_ = waveArea.rect;
|
||||
dragStartX_ = x;
|
||||
dragStartY_ = y;
|
||||
invalidate(); // live feedback; the commit lands on WM_LBUTTONUP
|
||||
invalidate(); // the model commit lands on WM_LBUTTONUP
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -235,7 +236,8 @@ void ReaSamplerEditor::dragWaveform(const FaceLayout& fl, int x, int y) {
|
||||
splineFor(overlayEnv_) = VelocityCurve::resolvePointDrag(
|
||||
dragStartCurve_, static_cast<std::size_t>(curvePointIndex_),
|
||||
splineOverlayBox(overlay), dx, y - dragStartY_);
|
||||
invalidate(); // live feedback; commit on release
|
||||
publishContoursLive(); // heard now; the model write stays the release's commit
|
||||
invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "shell/instrument/editor_internal.h" // (transitively: lice + the kit, Windows only)
|
||||
#include "shell/instrument/reasampler_processor.h"
|
||||
#include "core/version/app_version.h" // vstOutputName — channel-forked window class name
|
||||
|
||||
using namespace Steinberg;
|
||||
|
||||
@@ -20,7 +21,32 @@ namespace reasampler::vst {
|
||||
|
||||
#ifdef _WIN32
|
||||
namespace {
|
||||
constexpr const wchar_t* kChildClassName = L"ReaSampler9000VstEditor";
|
||||
|
||||
// Channel-forked so stable and beta never fight over one class registration (a second
|
||||
// RegisterClassW against an already-taken name silently fails, leaving that channel's editors
|
||||
// running the FIRST-registered channel's wndProc). vstOutputName() is ASCII (CMake OUTPUT_NAME),
|
||||
// so a plain widen is safe.
|
||||
const std::wstring& childClassName() {
|
||||
static const std::wstring name = [] {
|
||||
const std::string& base = version::vstOutputName();
|
||||
return std::wstring(base.begin(), base.end()) + L"VstEditor";
|
||||
}();
|
||||
return name;
|
||||
}
|
||||
|
||||
// The module holding a given code address — used to resolve THIS wndProc's own module rather
|
||||
// than the parent's hInstance REAPER hands us. Registering against the parent would put the class
|
||||
// in REAPER's own module while the wndproc still points into our DLL — a dangling procedure
|
||||
// pointer once our DLL unloads but REAPER's class registration outlives it. Takes the address
|
||||
// rather than naming wndProc directly so a free function in this anonymous namespace doesn't need
|
||||
// access to the private member.
|
||||
HMODULE moduleHandleFor(void* codeAddress) {
|
||||
HMODULE mod = nullptr;
|
||||
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(codeAddress), &mod);
|
||||
return mod;
|
||||
}
|
||||
|
||||
// The 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
|
||||
@@ -105,8 +131,9 @@ void ReaSamplerEditor::attachedToParent() {
|
||||
HWND parent = static_cast<HWND>(systemWindow);
|
||||
if (!parent) return;
|
||||
|
||||
HINSTANCE hInst =
|
||||
reinterpret_cast<HINSTANCE>(GetWindowLongPtr(parent, GWLP_HINSTANCE));
|
||||
// Own module, not the parent's hInstance — see moduleHandleFor() above.
|
||||
HINSTANCE hInst = reinterpret_cast<HINSTANCE>(
|
||||
moduleHandleFor(reinterpret_cast<void*>(&ReaSamplerEditor::wndProc)));
|
||||
if (!hInst) hInst = GetModuleHandle(nullptr);
|
||||
|
||||
static bool classRegistered = false;
|
||||
@@ -114,7 +141,7 @@ void ReaSamplerEditor::attachedToParent() {
|
||||
WNDCLASSW wc{};
|
||||
wc.lpfnWndProc = &ReaSamplerEditor::wndProc;
|
||||
wc.hInstance = hInst;
|
||||
wc.lpszClassName = kChildClassName;
|
||||
wc.lpszClassName = childClassName().c_str();
|
||||
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
// CS_DBLCLKS is what makes WM_LBUTTONDBLCLK arrive at all. It also REPLACES the second
|
||||
// WM_LBUTTONDOWN of a double-click, so every surface that counted two downs (Browse's
|
||||
@@ -134,7 +161,7 @@ void ReaSamplerEditor::attachedToParent() {
|
||||
refreshFromBank();
|
||||
|
||||
const ViewRect& r = getRect();
|
||||
childHwnd_ = CreateWindowExW(0, kChildClassName, L"", WS_CHILD | WS_VISIBLE, 0, 0,
|
||||
childHwnd_ = CreateWindowExW(0, childClassName().c_str(), L"", WS_CHILD | WS_VISIBLE, 0, 0,
|
||||
r.getWidth(), r.getHeight(), parent, nullptr, hInst, nullptr);
|
||||
if (childHwnd_) {
|
||||
SetWindowLongPtr(childHwnd_, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
|
||||
@@ -162,6 +189,15 @@ void ReaSamplerEditor::removedFromParent() {
|
||||
// an edit forever and every later internal write to that parameter would emit a bare
|
||||
// performEdit. The capture-lost path normally closes it; this does not rely on Windows
|
||||
// delivering WM_CAPTURECHANGED before the window goes away.
|
||||
if (drag_ == DragKind::kSplineNode) {
|
||||
// A contour drag reaches the voices without the model (see publishContoursLive) — the
|
||||
// window closing mid-drag is a second exit alongside WM_CAPTURECHANGED that must roll
|
||||
// the voices back the same way, or an abandoned drag stays audible after the editor
|
||||
// that started it is gone.
|
||||
params_ = dragStartParams_;
|
||||
publishContoursLive();
|
||||
drag_ = DragKind::kNone;
|
||||
}
|
||||
if (processor_) processor_->endParamGesture();
|
||||
if (childHwnd_) {
|
||||
KillTimer(childHwnd_, kSyncTimerId); // stop the poll before the window goes away
|
||||
@@ -312,6 +348,10 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam,
|
||||
// drag used.
|
||||
if (self->dragCommitsLive(self->drag_, self->dragParamId_)) {
|
||||
self->commitLive();
|
||||
} else if (self->drag_ == DragKind::kSplineNode) {
|
||||
// The contour drag reached the voices without the model; restore
|
||||
// the voices to it the same way (see publishContoursLive).
|
||||
self->publishContoursLive();
|
||||
}
|
||||
}
|
||||
// Peer of onMouseUp's bracket close, and after the rollback for the same
|
||||
|
||||
@@ -251,6 +251,10 @@ void ReaSamplerEditor::commitLive() {
|
||||
processor_->publishLiveParams();
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::publishContoursLive() {
|
||||
if (processor_) processor_->publishLiveContours(params_.play);
|
||||
}
|
||||
|
||||
bool ReaSamplerEditor::deckKnobIsProcessorSide(int paramId) {
|
||||
return paramId == -2 ||
|
||||
paramId == static_cast<int>(ParamControl::kVoiceCount) ||
|
||||
|
||||
@@ -164,6 +164,7 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
// top of the first block after this, which is where the swap below becomes visible
|
||||
// too — so the new snapshot's first note reads it.
|
||||
sample.live = &automationLive_;
|
||||
sample.liveContours = &liveContours_;
|
||||
// BEFORE the publish: the publish is what makes the audio thread re-merge, and the
|
||||
// merge resolves every held automation stage time against this rate. Stored after,
|
||||
// a merge in the window between them would resolve them against the previous
|
||||
@@ -175,6 +176,10 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
// publishLiveParams never holds reloadMutex_, so this is the only nesting.
|
||||
std::lock_guard<std::mutex> lp(livePublishMutex_);
|
||||
liveParams_.publish(instrument::engine::foldLive(sample.play, sample.keyTrack));
|
||||
// The model's contours, so a reload-tier contour edit (a point added, deleted or
|
||||
// made hard, a drag's release) reaches the drain's sounding voices too, and a
|
||||
// courier publish from an abandoned gesture cannot outlive the reload.
|
||||
liveContours_.publish(instrument::engine::foldContours(sample.play));
|
||||
}
|
||||
resolvedId = selId; // the concrete pick that resolved
|
||||
}
|
||||
|
||||
@@ -293,6 +293,14 @@ AutomationChannel::ReleaseProof ReaSamplerProcessor::publishLiveParams() {
|
||||
return AutomationChannel::ReleaseProof::fromPublish(before, liveParams_.generation());
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::publishLiveContours(const PlaySeconds& play) {
|
||||
// Folded outside the lock, as publishLiveParams' block is. A contour is normalized time, so
|
||||
// unlike that block it needs no rate and publishes even before anything is built.
|
||||
const instrument::engine::LiveContours contours = instrument::engine::foldContours(play);
|
||||
std::lock_guard<std::mutex> lock(livePublishMutex_);
|
||||
liveContours_.publish(contours);
|
||||
}
|
||||
|
||||
SampleRefs ReaSamplerProcessor::sampleRefs() {
|
||||
std::lock_guard<std::mutex> lock(refsMutex_);
|
||||
return sampleRefs_;
|
||||
|
||||
@@ -267,6 +267,12 @@ private:
|
||||
// the knob. Does not repaint; callers already do. UI thread only.
|
||||
void commitLive();
|
||||
|
||||
// The drawn-contour drag's per-move publish: params_' three contours straight to the voices,
|
||||
// with NO parameter-set write — why is this directory's CLAUDE.md (the contour-drag note
|
||||
// under the commit tiers). Every path that ends such a drag without its release's
|
||||
// commitAndReload — capture-lost — must call this again after restoring params_. UI thread.
|
||||
void publishContoursLive();
|
||||
|
||||
// Whether an in-flight drag commits live rather than through a reload. A deck knob is
|
||||
// live per deckParamCommit; an envelope-node drag is live in EITHER mode — see
|
||||
// liveCommitFor (deck_groups.h) for why.
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "core/instrument/map/sample_map.h" // InstrumentParams (the one parameter set)
|
||||
#include "core/instrument/map/component_state_io.h" // ComponentState codec
|
||||
#include "core/instrument/engine/limiter.h" // the master bus's post-gain limiter
|
||||
#include "core/instrument/engine/live_contours.h" // LiveContourBlock (the drawn-contour block)
|
||||
#include "core/instrument/engine/live_params.h" // LiveParams (the live-parameter block)
|
||||
#include "core/instrument/engine/meter_accumulate.h" // the meter's block-rate folds + consume
|
||||
#include "core/instrument/param/param_id.h" // the frozen ParamId space + DeckParam binding
|
||||
@@ -31,6 +32,7 @@ namespace reasampler::vst {
|
||||
|
||||
using instrument::map::ComponentState;
|
||||
using instrument::map::InstrumentParams;
|
||||
using instrument::map::PlaySeconds;
|
||||
using instrument::map::SampleRefEntry;
|
||||
using instrument::map::SampleRefs;
|
||||
using instrument::map::kPreviewVelocityDefault;
|
||||
@@ -226,6 +228,11 @@ public:
|
||||
// bakes and publishes). UI thread; serialized against reloadInstrument's own publish. Returns drainAutomationToModel's release proof (automation_channel.h); other callers ignore it.
|
||||
AutomationChannel::ReleaseProof publishLiveParams();
|
||||
|
||||
// Publishes `play`'s three drawn contours to every sounding voice WITHOUT writing the
|
||||
// model — the contour drag's courier (this directory's CLAUDE.md, the authority table).
|
||||
// UI thread; serialized against reloadInstrument's own contour publish.
|
||||
void publishLiveContours(const PlaySeconds& play);
|
||||
|
||||
// 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.
|
||||
@@ -415,6 +422,9 @@ private:
|
||||
// Both live_ and draining_ observe this same block — a block owned by a snapshot would
|
||||
// leave the drain's still-sounding voices deaf to the knob under them.
|
||||
instrument::engine::LiveParams liveParams_;
|
||||
// SampleData::liveContours' one target, under liveParams_' ordering constraint above and for
|
||||
// its reason (this directory's CLAUDE.md owns why it needs no automation merge).
|
||||
instrument::engine::LiveContourBlock liveContours_;
|
||||
// The block the ENGINE reads, and the ONE thing SampleData::live points at. Written only by
|
||||
// the audio thread, which merges liveParams_ with the host's automation points once per block
|
||||
// and republishes ONLY when the RESULT moved — so neither an unchanged model nor a lane
|
||||
@@ -432,10 +442,10 @@ private:
|
||||
// The host lane's state and its release protocol; automation_channel.h owns the mechanism and
|
||||
// this directory's CLAUDE.md the authority model it enforces.
|
||||
AutomationChannel automation_;
|
||||
// Serializes liveParams_.publish's two writer sites (reloadInstrument, publishLiveParams)
|
||||
// only — separate from reloadMutex_ so a knob drag's publish never blocks behind a
|
||||
// reload's WAV decode. The audio thread never takes this; process() only reads via
|
||||
// LiveParams::read's lock-free seqlock retry.
|
||||
// Serializes the writer sites of liveParams_ (reloadInstrument, publishLiveParams) and of
|
||||
// liveContours_ (reloadInstrument, publishLiveContours) only — separate from reloadMutex_ so
|
||||
// a drag's publish never blocks behind a reload's WAV decode. The audio thread never takes
|
||||
// this; it only reads via the seqlock's lock-free retry.
|
||||
std::mutex livePublishMutex_;
|
||||
// The output rate the loaded capture's play params were resolved at (SampleData::
|
||||
// outputRate — the host's), so a live republish resolves the stored wall-clock seconds to
|
||||
|
||||
Reference in New Issue
Block a user