Ξ-W2-T1 remediation: print master gain into the bake, derive the window from the dialed sound, reset play mode to Trigger
This commit is contained in:
@@ -103,7 +103,7 @@ declared ahead of the instrument slots at that member in `reasampler_processor.h
|
||||
|
||||
## Modules
|
||||
|
||||
- `reaper_bridge` — READ-ONLY bank consumer: receives bank snapshots from the extension and exposes them as a read-only view. **Never writes to the extension's bank** — this is a load-bearing invariant; no mutation path exists in this module. It owns TWO prefix-guarded ext-state write entry points, `writeUsageExtState` (`rsusage_`) and `writeBakeExtState` (`rsbake_`), each refusing every other key; neither weakens the read-only-*bank* invariant, because neither payload is bank state and `banks`/`view`/`tail`/`assign` stay structurally unwritable. It also owns the bake crossing — `extensionActionAvailable` / `invokeExtensionAction` (`NamedCommandLookup` + `Main_OnCommandEx` against `getReaperParent(3)`, the instance's OWN project tab, never the focused one) and `projectTempoBpm`.
|
||||
- `reaper_bridge` — READ-ONLY bank consumer: receives bank snapshots from the extension and exposes them as a read-only view. **Never writes to the extension's bank** — this is a load-bearing invariant; no mutation path exists in this module. It owns TWO prefix-guarded ext-state write entry points, `writeUsageExtState` (`rsusage_`) and `writeBakeExtState` (`rsbake_`), each refusing every other key; neither weakens the read-only-*bank* invariant, because neither payload is bank state and `banks`/`view`/`tail`/`assign` stay structurally unwritable. It also owns the bake crossing — `extensionActionAvailable` / `invokeExtensionAction` (`NamedCommandLookup` + `Main_OnCommandEx` with `getReaperParent(3)`, the instance's OWN project tab, as `proj` — a request, not a DAW-verified guarantee; see the header) and `projectTempoBpm`.
|
||||
- `reasampler_processor` (`shell/instrument/`: `reasampler_processor.cpp` lifecycle + `process()`, `processor_state.cpp` component-state I/O + UI-thread parameter accessors, `processor_reload.cpp` the off-audio-thread `reloadInstrument`/publish family — Q-W2v, T4-12 split; `process()` and its per-block work stay ONE TU on purpose, no cross-TU call on the per-sample path) — VST3 `SingleComponentEffect` shell: declares event-input bus + **permanently stereo** output (GA fix: dynamic mono↔stereo bus renegotiation deleted; `ChannelMode` is now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread `reloadInstrument` + atomic pointer swap so `process()` does no allocation, no file I/O, no bridge calls. The instance state is `{loaded capture id, one InstrumentParams}`, and `reloadInstrument` resolves + decodes exactly that one capture into the `SampleData` the engine plays. **Self-contained playback (pS):** `ComponentState` v10 adds a `SampleRefs` table — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName); `reloadInstrument` decodes directly from `SampleRefs`, bank-free (plays with the extension absent). The bank/bridge is a browser source: loading a capture copies its reference in; the reopen-heal timer + poll-to-play apparatus are removed. `retireIdleDrain()` retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (`setVoiceCount`/`setVoiceMode`/`setMonoTrigger`) rebuild the engine from the already-decoded `SampleData` via the drain-slot swap — no bank re-read, no WAV re-decode, no audible cut to ringing tails. **FB1:** applies the post-mixer `masterGainLinear` (from `ComponentState` v8) as a per-sample ramp over the summed output — no zipper noise. **GA v9:** `channelModeExplicit_` flag persisted; `channelModeFor()` auto-defaults the mode from the loaded capture's channel count when the flag is not set. **pS:** `ComponentState` bumped v9→v10 (`SampleRefs` table); pre-v10 blobs lift to empty refs and re-save self-contained. **pS-usage:** publishes instance usage (held `SampleRefs` paths) to `rsusage_<instanceGuid>` at the tail of `reloadInstrument` (off audio thread) via `reaper_bridge::writeUsageExtState`; `ComponentState` bumped v10→**v11** (`instanceGuid` field); pre-v11 blobs mint guid on first publish.
|
||||
- `reasampler_editor` — VST3 `IPlugView` LICE editor shell: hosts a LICE-drawn child window; the Sample face is home and Browse is a modal picker over it. Split on the Sample face's BAND axis, mirroring the pure `sample_bands` allocator: `editor_session` (session/bridge state, caches, commit-and-reload), `editor_controls` (the ONE `faceLayout` band resolve every paint and hit-test path shares, the node-drag bounds, the value labels, and the per-instance controls the parameter set does not carry — the parameter-set binding itself is the pure `core/instrument/ui/deck_values` module this only adapts int ids onto), `editor_models` (the orthogonal half: which stored struct each transient editor selection names — the staged-envelope pack/unpack, the drawn contour, and the three velocity curves), then matching paint and input sets — `editor_paint`/`editor_input` (dispatch + drag router + hover dispatch), `_chrome`, `_waveform`, `_deck` — plus the two band-independent surfaces (`_browse` for the modal picker, `_curve` for the velocity-curve popup) and `editor_platform` (IPlugView/Win32 window plumbing). Shared internals in `editor_internal.h`, no TU of its own. Drop-onto-editor ingest is NOT shipped (deferred).
|
||||
- `reasampler_embed` — implements `IReaperUIEmbedInterface` so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout to `embed_strip`. A read-only readout: the loaded capture across the keyboard span with its root marked, plus the activity level. It takes no mouse input (there is nothing on the strip to select).
|
||||
|
||||
@@ -121,14 +121,19 @@ BakeChainResult runBake(ReaSamplerProcessor& processor) {
|
||||
|
||||
const InstrumentParams dialed = processor.instrumentParams();
|
||||
const int rootNote = dialed.rootOverride ? *dialed.rootOverride : source->rootNote;
|
||||
const auto plan = planBake(resolveNote(defaultBakeProgram(), *tempo), sampleRate,
|
||||
rootNote);
|
||||
if (!plan) return fail("the programmed capture window is empty");
|
||||
|
||||
// The snapshot comes first: the default program's window is derived from the sound it
|
||||
// carries (a Gate release, a Trigger play span), not from a constant.
|
||||
std::optional<SampleData> snapshot = processor.bakeSnapshot();
|
||||
if (!snapshot) return fail("the loaded capture could not be decoded for the render");
|
||||
|
||||
const instrument::bake::BakeAudio audio = renderBake(std::move(*snapshot), *plan);
|
||||
const auto plan = planBake(
|
||||
resolveNote(defaultBakeProgram(*snapshot, sampleRate, *tempo), *tempo), sampleRate,
|
||||
rootNote);
|
||||
if (!plan) return fail("the programmed capture window is empty");
|
||||
|
||||
const instrument::bake::BakeAudio audio =
|
||||
renderBake(std::move(*snapshot), *plan, processor.masterGainLinear());
|
||||
if (audio.empty()) return fail("the offline pass produced no audio");
|
||||
|
||||
// buildFloat32Wav takes doubles and narrows; the narrowing back to float is the bank's
|
||||
|
||||
@@ -15,8 +15,9 @@ class ReaSamplerProcessor;
|
||||
class ReaperBridge;
|
||||
|
||||
// Whether a bake can run at all right now: a REAPER host, and the extension's bake action
|
||||
// registered. Cheap enough for the editor's sync tick, which is where the button's paint
|
||||
// state is decided — the control must never be enabled and then refuse.
|
||||
// registered. Runs one NamedCommandLookup and builds one std::string per call — cost
|
||||
// unmeasured. It is on the editor's sync tick because that is where the button's paint
|
||||
// state is decided, and the control must never be enabled and then refuse.
|
||||
bool bakeAvailable(ReaperBridge& bridge);
|
||||
|
||||
struct BakeChainResult {
|
||||
|
||||
@@ -235,10 +235,15 @@ void ReaSamplerProcessor::adoptBakedCapture(const SampleRefEntry& entry,
|
||||
}
|
||||
setSelectedSampleId(entry.sampleId);
|
||||
setInstrumentParams(reset);
|
||||
setMasterGainLinear(masterGainLinear);
|
||||
{
|
||||
// ARMED, not stored: publishBuiltLocked applies it inside the same locked section as
|
||||
// the pointer swap. Storing it here instead would put the reset gain under the OLD
|
||||
// capture for the whole bridge-read-plus-WAV-decode the reload below runs first.
|
||||
std::lock_guard<std::mutex> lock(reloadMutex_);
|
||||
gainAtNextPublish_ = masterGainLinear;
|
||||
}
|
||||
// ONE reload for the re-point and the reset together: it decodes the new file and
|
||||
// publishes the neutral parameters in the same swap, so no block is ever rendered with
|
||||
// one of the two applied and not the other.
|
||||
// publishes the neutral parameters in the same swap.
|
||||
reloadInstrument();
|
||||
}
|
||||
|
||||
@@ -292,6 +297,14 @@ void ReaSamplerProcessor::publishBuiltLocked(std::unique_ptr<LoadedInstrument> b
|
||||
}),
|
||||
graveyard_.end());
|
||||
LoadedInstrument* prev = live_.exchange(built.release());
|
||||
// A bake's reset gain lands here rather than at its call site, so the gain and the
|
||||
// capture it belongs to become audible to process() in the same instant. A tail still
|
||||
// ringing out of the drain does take the new gain — one gain sits above every snapshot,
|
||||
// the same shape as ONE BLOCK, ONE RATE (see builtSampleRate_).
|
||||
if (gainAtNextPublish_) {
|
||||
setMasterGainLinear(*gainAtNextPublish_);
|
||||
gainAtNextPublish_.reset();
|
||||
}
|
||||
LoadedInstrument* evicted = draining_.exchange(prev);
|
||||
if (evicted) graveyard_.push_back(std::unique_ptr<LoadedInstrument>(evicted));
|
||||
}
|
||||
|
||||
@@ -155,8 +155,9 @@ bool ReaperBridge::invokeExtensionAction(const std::string& commandName) {
|
||||
const int command = lookupCommand(commandName);
|
||||
if (command == 0) return false;
|
||||
auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
|
||||
// getReaperParent(3), not the focused tab: an instance in a background tab must bake
|
||||
// into ITS OWN project's bank.
|
||||
// getReaperParent(3), not the focused tab — the ask is that an instance in a background
|
||||
// tab bakes into ITS OWN project's bank (see the header on what that does and does not
|
||||
// guarantee).
|
||||
void* proj = reaper->getReaperParent(3);
|
||||
// flag 0 is the conventional "no modifier" value; the header documents no other, and
|
||||
// the extension's hookcommand ignores it.
|
||||
|
||||
@@ -71,11 +71,15 @@ public:
|
||||
// affordance: an unavailable action must read Disabled, never enabled-then-refusing.
|
||||
bool extensionActionAvailable(const std::string& commandName);
|
||||
|
||||
// Fires the action against THIS INSTANCE's own project tab (getReaperParent(3)), not
|
||||
// whichever tab is focused. Returns false when the action is unregistered — the
|
||||
// invocation itself reports nothing, so a caller learns the result from the state the
|
||||
// action wrote, never from here. Must NOT be called from a mouse handler: it runs the
|
||||
// extension's whole bake landing synchronously, and the action re-points this instance.
|
||||
// Fires the action with THIS INSTANCE's own project tab (getReaperParent(3)) as
|
||||
// Main_OnCommandEx's `proj`, rather than leaving it to whichever tab is focused. What
|
||||
// REAPER then makes current for the action's duration is NOT verified in the DAW, so
|
||||
// this is a request, not a guarantee — the extension side re-derives which project a
|
||||
// request came from and refuses one it cannot safely land. Returns false when the action
|
||||
// is unregistered; the invocation itself reports nothing, so a caller learns the result
|
||||
// from the state the action wrote, never from here. Must NOT be called from a mouse
|
||||
// handler: it runs the extension's whole bake landing synchronously, and the action
|
||||
// re-points this instance.
|
||||
bool invokeExtensionAction(const std::string& commandName);
|
||||
|
||||
// The effective project tempo (BPM, quarter notes per minute) at the edit cursor of
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -130,9 +131,9 @@ public:
|
||||
|
||||
// Adopt a landed bake in ONE act: the new capture becomes this instance's ref and
|
||||
// selection, the parameter set and master gain go neutral, and a single reload
|
||||
// publishes both together. The ordering is the whole point — a reset published ahead
|
||||
// of the re-point would put neutral parameters under the OLD capture for a block.
|
||||
// UI thread only.
|
||||
// publishes all three together. The ordering is the whole point — anything published
|
||||
// ahead of the re-point would apply neutral settings to the OLD capture, which is the
|
||||
// one thing they are meaningless against. UI thread only.
|
||||
void adoptBakedCapture(const SampleRefEntry& entry, const InstrumentParams& reset,
|
||||
double masterGainLinear);
|
||||
|
||||
@@ -304,6 +305,10 @@ private:
|
||||
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
|
||||
// A master gain that must reach the audio thread in the SAME swap as the next publish —
|
||||
// the bake's reset, which the old capture would be the wrong thing to apply it to.
|
||||
// Guarded by reloadMutex_, consumed by publishBuiltLocked.
|
||||
std::optional<double> gainAtNextPublish_;
|
||||
|
||||
// The loaded capture's id ("" = no pick -> silence). Off-thread only, not read on the
|
||||
// audio thread.
|
||||
|
||||
Reference in New Issue
Block a user