Ξ-W2-T1: the resample bake chain — instrument renders, extension banks, one click re-points and resets
This commit is contained in:
@@ -83,8 +83,12 @@ declared ahead of the instrument slots at that member in `reasampler_processor.h
|
||||
|
||||
**Non-goals / guardrails.**
|
||||
- The instrument never captures and never inserts into the arrange. Playback is a
|
||||
read-only act over the bank. Any instrument path that captures, places a timeline
|
||||
item, or writes back into the bank is a bug.
|
||||
read-only act over the bank. Any instrument path that places a timeline item, or that
|
||||
writes bank state itself, is a bug. **The resample bake is not an exception to that
|
||||
and does not widen it:** the instrument RENDERS its own sound and REQUESTS a landing;
|
||||
the extension is what captures the file into the bank and writes the index. The
|
||||
instrument's whole outbound surface is one prefix-guarded request key plus one action
|
||||
id — see `instrument_bake` and `reaper_bridge` below.
|
||||
- The instrument never ingests. Capture, import, and drop-ingest are *extension*
|
||||
acts; the instrument only reads and plays. A drop onto the editor window (if ever
|
||||
shipped) is relayed to the extension as an ingest request — the instrument never
|
||||
@@ -99,17 +103,26 @@ 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. **pS-usage:** gains `writeUsageExtState` (prefix-guarded — accepts only `rsusage_`-prefixed keys, refuses all others) so the processor can publish usage without weakening the read-only-bank invariant.
|
||||
- `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`.
|
||||
- `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).
|
||||
- `editor_stroke` — the editor's LICE side of the analytic stroker: builds a coverage mask with the pure `core/ui/stroke_aa` and blends it into the bitmap ONCE, writing straight to the bitmap's bits (the arithmetic matches LICE's own mode-0 combine, so a stroke composites identically to every other kit draw). Every radial and spline stroke on the editor routes through `strokeArcAA` / `strokePolylineAA` / `strokeLineAA`. Holds the draw-thread-only scratch mask and arc point list — reuse, not a hidden dependency: threading a canvas through the eight paint sites would grow those signatures to carry an allocation detail. Deliberately does NOT touch `shell/panel/draw_kit`: the waveform stroke, the docked bank panel and the browse cards are out of this seam's blast radius.
|
||||
- `instrument_bake` — the instrument's half of the resample chain, on the UI thread: render the dialed sound through the pure `core/instrument/bake` modules, stage the WAV OUTSIDE the bank folder, publish one `rsbake_<guid>` request, invoke the extension's landing action SYNCHRONOUSLY, read the outcome back over the same key, then adopt + reset in one act. Two stack-RAII guards mirror `FxBypassGuard`'s discipline: the staged file and the request key are both cleared on every exit path, so a failed bake leaves no temp, no bank entry and no parameter reset. `bakeAvailable` is the affordance's paint gate.
|
||||
- `vst_entry` — VST3 entry point: `GetPluginFactory` export, class registration, channel-forked class UIDs.
|
||||
- `editor_internal.h` — INTERNAL shared helpers for the `reasampler_editor` TU family, included only by the editor's own shell TUs (`editor_session` / `editor_controls` / `editor_paint_*` / `editor_input_*` / `editor_platform`), never a public seam: the `Rect`↔kit adapters, small draw primitives (knob face / title band), label helpers, and the velocity-curve box derivation — the helpers more than one band TU needs. The deck's control ids, group ids and group composition are the pure `deck_groups` module's, not this file's. The piano-strip and root-key draws live in `editor_paint_chrome`, their only consumer, not here.
|
||||
- `reasampler_vst.h` — shared identity constants for the ReaSampler VST3 instrument (Phase S): the plugin's class UID (the channel-selected `Steinberg::FUID`, built from the FOREVER-FROZEN macros in `core/wire/reasampler_uid.h`), vendor name/URL/email, so the processor, factory, and editor agree. A class UID is FOREVER-STABLE once shipped — minted once, never regenerated. *(Newly authored per this dispatch's brief — no existing root-CLAUDE.md bullet; verified by reading `src/shell/instrument/reasampler_vst.h` directly.)*
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **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
|
||||
nested inside a mouse handler with `SetCapture` held, while the invoked action re-points
|
||||
the very instance whose frame is on the stack. Deferring by one tick is same-thread and
|
||||
in-instance — it is NOT a cross-process poller/nonce handshake, and it must not grow into
|
||||
one.
|
||||
- The bake's availability probe runs on the SAME tick that paints the button, so the
|
||||
control can never be enabled on one tick and refuse on the next.
|
||||
- `editor_internal.h` is include-only — it has no TU of its own and must never become
|
||||
a public seam; only the `reasampler_editor` band-axis TUs include it.
|
||||
- **The editor window class carries `CS_DBLCLKS`, which REPLACES the second button-down of
|
||||
|
||||
@@ -66,6 +66,7 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
|
||||
editor_input_curve.cpp
|
||||
editor_stroke.cpp
|
||||
editor_platform.cpp
|
||||
instrument_bake.cpp
|
||||
reasampler_embed.cpp
|
||||
reaper_bridge.cpp
|
||||
# draw_kit is compiled into each module rather than being a static library — see root
|
||||
@@ -88,7 +89,8 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
|
||||
waveform_view bank_sync browser_scroll param_slider tooltip
|
||||
theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit
|
||||
knob_deck deck_groups deck_values curve_popup spline_edit master_gain sample_usage
|
||||
file_bytes curve_law stroke_aa)
|
||||
file_bytes curve_law stroke_aa
|
||||
bake_plan bake_render bake_reset bake_wire wav_codec)
|
||||
# SDK_INC gives the REAPER VST3 interfaces + API header for the bridge; WDL_INC gives
|
||||
# LICE for the editor. The VST3 SDK headers arrive via vst3_sdk PUBLIC.
|
||||
target_include_directories(reasampler_vst PRIVATE ${REASAMPLER_SRC_DIR} ${SDK_INC} ${WDL_INC})
|
||||
|
||||
@@ -40,6 +40,14 @@ bool ReaSamplerEditor::mouseDownChrome(const FaceLayout& fl, int x, int y) {
|
||||
invalidate();
|
||||
return true;
|
||||
}
|
||||
// Resample bake. Arm only: onSyncTimer runs it a tick later, off this handler's stack
|
||||
// and with no mouse capture held (instrument_bake.h says why that matters). A click
|
||||
// while the extension is absent is swallowed — the button already paints Disabled.
|
||||
if (contains(cr.bake, x, y)) {
|
||||
if (bakeAvailable_) bakePending_ = true;
|
||||
invalidate();
|
||||
return true;
|
||||
}
|
||||
// Radial preview-velocity knob: grab-anchored vertical drag — the grab itself never
|
||||
// jumps the value; the delta from the grab point maps via knobDragValue.
|
||||
if (contains(cr.velCell, x, y)) {
|
||||
@@ -109,6 +117,7 @@ ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverChrome(const FaceLayout& fl
|
||||
const ChromeRects& cr = fl.chrome;
|
||||
if (contains(cr.navBrowse, x, y)) return {HoverKind::kNavBrowse, -1};
|
||||
if (selectedId_.empty()) return {}; // empty state — no interactive surfaces beyond nav
|
||||
if (contains(cr.bake, x, y)) return {HoverKind::kBake, -1};
|
||||
if (contains(cr.preview, x, y)) return {HoverKind::kPreview, -1};
|
||||
if (contains(cr.velCell, x, y)) return {HoverKind::kVelKnob, -1};
|
||||
if (contains(cr.chanMono, x, y)) return {HoverKind::kChanMono, -1};
|
||||
|
||||
@@ -112,6 +112,9 @@ void ReaSamplerEditor::paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool
|
||||
} else {
|
||||
title += " [host: no bridge]";
|
||||
}
|
||||
// A bake outcome outranks the identity readout while it lasts: the click's only other
|
||||
// feedback is the sound itself, which is by design indistinguishable from before.
|
||||
if (bakeMessageTicks_ > 0 && !bakeMessage_.empty()) title = bakeMessage_;
|
||||
kitText(bmp, cr.title, title.c_str(), kToolbarFont, Role::TextPrimary);
|
||||
|
||||
// Browse: the picker. When nothing is loaded it is the empty state's dominant
|
||||
@@ -128,6 +131,20 @@ void ReaSamplerEditor::paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool
|
||||
// nothing picked there is no root, no preview and no channel decision to make.
|
||||
if (empty) return;
|
||||
|
||||
// Resample bake. Disabled without the extension: the bank is the extension's surface,
|
||||
// so with it absent there is no writer and the control must read unavailable rather
|
||||
// than accept a click it cannot honour.
|
||||
{
|
||||
const KitButtonBox box{toKitBox(cr.bake)};
|
||||
const InteractionState st =
|
||||
!bakeAvailable_ ? InteractionState::Disabled
|
||||
: (bakePending_ ? InteractionState::Active
|
||||
: (isHovered(HoverKind::kBake, -1)
|
||||
? InteractionState::Hover
|
||||
: InteractionState::Rest));
|
||||
drawButton(bmp, box, "Bake", st, /*warn=*/false);
|
||||
}
|
||||
|
||||
// Preview-trigger button (fires the loaded capture at root through the live voice engine).
|
||||
// A drawn play triangle rather than a label or an embedded image: it inherits the button's
|
||||
// own foreground role, so it stays legible in every interaction state at no build cost.
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "ext_keys.h"
|
||||
#include "core/instrument/engine/loop/loop_span.h" // defaultLoopBounds (the shared ghost span)
|
||||
#include "core/instrument/ui/browser_scroll.h" // nameMatchesQuery (type-to-filter)
|
||||
#include "shell/instrument/instrument_bake.h" // the deferred bake the sync tick runs
|
||||
#include "shell/instrument/reaper_bridge.h"
|
||||
#include "shell/instrument/reasampler_processor.h"
|
||||
|
||||
@@ -39,6 +40,10 @@ using ui::ThumbnailKey;
|
||||
using ui::thumbnailKeyString;
|
||||
using util::readFileBytes;
|
||||
|
||||
// Sync ticks a bake outcome stays in the title band. Long enough to read, short enough
|
||||
// that it does not shadow the identity readout.
|
||||
constexpr int kBakeMessageTicks = 20;
|
||||
|
||||
ReaSamplerEditor::ReaSamplerEditor(ReaSamplerProcessor* processor)
|
||||
: CPluginView(nullptr), processor_(processor) {
|
||||
// The default IS the enforced floor (checkSizeConstraint) — the face opens at the size its
|
||||
@@ -104,6 +109,28 @@ void ReaSamplerEditor::onSyncTimer() {
|
||||
if (!processor_) return;
|
||||
if (drag_ != DragKind::kNone) return; // defer past the in-flight edit
|
||||
|
||||
// Resolve the bake affordance's availability on the SAME tick that paints it, so it
|
||||
// can never be enabled on one tick and refuse on the next.
|
||||
const bool available = bakeAvailable(processor_->bridge());
|
||||
if (available != bakeAvailable_) {
|
||||
bakeAvailable_ = available;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
// The click armed it; this is where it runs — same thread, one tick later, no mouse
|
||||
// capture held, and no REAPER action nested inside a mouse handler.
|
||||
if (bakePending_) {
|
||||
bakePending_ = false;
|
||||
const BakeChainResult result = runBake(*processor_);
|
||||
bakeMessage_ = result.message;
|
||||
bakeMessageTicks_ = kBakeMessageTicks;
|
||||
// A landed bake re-pointed the instance and reset the parameter set; re-snapshot
|
||||
// so the face draws the new capture and its neutral controls.
|
||||
if (result.ok) refreshFromBank();
|
||||
invalidate();
|
||||
}
|
||||
if (bakeMessageTicks_ > 0 && --bakeMessageTicks_ == 0) invalidate();
|
||||
|
||||
// An open editor is the focused assignment target (thundering-herd policy); instances
|
||||
// with no editor open never poll (the timer is bound to the child window).
|
||||
const ReaSamplerProcessor::BankSyncResult r = processor_->pollBankSync(/*isFocusedTarget=*/true);
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
// See instrument_bake.h.
|
||||
|
||||
#include "shell/instrument/instrument_bake.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/capture/wav_codec.h" // buildFloat32Wav (the bank byte format)
|
||||
#include "core/instrument/bake/bake_plan.h"
|
||||
#include "core/instrument/bake/bake_render.h"
|
||||
#include "core/instrument/bake/bake_reset.h"
|
||||
#include "core/instrument/note/tempo.h"
|
||||
#include "core/wire/bake_wire.h"
|
||||
#include "ext_keys.h"
|
||||
#include "shell/instrument/reaper_bridge.h"
|
||||
#include "shell/instrument/reasampler_processor.h"
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
using capture::buildFloat32Wav;
|
||||
using instrument::bake::defaultBakeProgram;
|
||||
using instrument::bake::planBake;
|
||||
using instrument::bake::renderBake;
|
||||
using instrument::bake::resetAfterBake;
|
||||
using instrument::map::SampleRefEntry;
|
||||
using instrument::map::SelectedSample;
|
||||
using instrument::note::Tempo;
|
||||
using instrument::note::resolveNote;
|
||||
using wire::BakeOutcome;
|
||||
using wire::BakeRequest;
|
||||
using wire::BakeStatus;
|
||||
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Deletes the staged file on EVERY exit path, success or failure — the same stack-RAII
|
||||
// discipline the capture shell's FX-bypass guard follows. On success the extension has
|
||||
// already COPIED the bytes into the bank, so the delete here is what keeps the temp from
|
||||
// outliving the click. Best-effort: a file already gone is not an error.
|
||||
//
|
||||
// Orphan policy: a crash between the write and the invoke leaves one file in the OS temp
|
||||
// directory, which is exactly what that directory is swept for. Nothing here scans or
|
||||
// deletes files it did not itself create.
|
||||
class StagedFileGuard {
|
||||
public:
|
||||
explicit StagedFileGuard(std::string path) : path_(std::move(path)) {}
|
||||
~StagedFileGuard() {
|
||||
if (path_.empty()) return;
|
||||
std::error_code ec;
|
||||
fs::remove(path_, ec);
|
||||
}
|
||||
StagedFileGuard(const StagedFileGuard&) = delete;
|
||||
StagedFileGuard& operator=(const StagedFileGuard&) = delete;
|
||||
|
||||
private:
|
||||
std::string path_;
|
||||
};
|
||||
|
||||
// Clears the request key on every exit path. A key left holding a request would be picked
|
||||
// up by the next bake's landing pass and re-run against a temp file that no longer exists.
|
||||
class RequestKeyGuard {
|
||||
public:
|
||||
RequestKeyGuard(ReaperBridge& bridge, std::string key)
|
||||
: bridge_(bridge), key_(std::move(key)) {}
|
||||
~RequestKeyGuard() { bridge_.writeBakeExtState(key_, ""); }
|
||||
RequestKeyGuard(const RequestKeyGuard&) = delete;
|
||||
RequestKeyGuard& operator=(const RequestKeyGuard&) = delete;
|
||||
|
||||
private:
|
||||
ReaperBridge& bridge_;
|
||||
std::string key_;
|
||||
};
|
||||
|
||||
BakeChainResult fail(std::string message) {
|
||||
return BakeChainResult{false, std::move(message)};
|
||||
}
|
||||
|
||||
bool writeFileBytes(const std::string& path, const std::vector<std::uint8_t>& bytes) {
|
||||
std::ofstream f(path, std::ios::binary | std::ios::trunc);
|
||||
if (!f) return false;
|
||||
f.write(reinterpret_cast<const char*>(bytes.data()),
|
||||
static_cast<std::streamsize>(bytes.size()));
|
||||
return f.good();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool bakeAvailable(ReaperBridge& bridge) {
|
||||
return bridge.isConnected() &&
|
||||
bridge.extensionActionAvailable(wire::bakeActionLookupName());
|
||||
}
|
||||
|
||||
BakeChainResult runBake(ReaSamplerProcessor& processor) {
|
||||
ReaperBridge& bridge = processor.bridge();
|
||||
if (!bakeAvailable(bridge))
|
||||
return fail("resample needs the ReaSampler extension loaded");
|
||||
|
||||
const std::string selectionId = processor.selectedSampleId();
|
||||
if (selectionId.empty()) return fail("nothing loaded to resample");
|
||||
|
||||
// The whole ref entry, not just its SelectedSample: the display name that the new
|
||||
// capture's is derived from sits beside the intrinsics.
|
||||
const instrument::map::SampleRefs refs = processor.sampleRefs();
|
||||
const SampleRefEntry* sourceEntry = nullptr;
|
||||
for (const SampleRefEntry& e : refs)
|
||||
if (e.sampleId == selectionId) { sourceEntry = &e; break; }
|
||||
if (!sourceEntry) return fail("the loaded capture has no resolvable file");
|
||||
const SelectedSample* source = &sourceEntry->ref;
|
||||
|
||||
const int sampleRate = static_cast<int>(processor.sampleRate());
|
||||
if (sampleRate <= 0) return fail("the host has not reported a sample rate yet");
|
||||
|
||||
const std::optional<Tempo> tempo = Tempo::fromBpm(bridge.projectTempoBpm());
|
||||
if (!tempo) return fail("the project tempo could not be read");
|
||||
|
||||
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");
|
||||
|
||||
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);
|
||||
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
|
||||
// own 32-bit-float contract, so the round trip is exact.
|
||||
std::vector<double> interleaved(audio.interleaved.begin(), audio.interleaved.end());
|
||||
const std::vector<std::uint8_t> bytes =
|
||||
buildFloat32Wav(audio.channelCount, static_cast<std::uint32_t>(audio.sampleRate),
|
||||
static_cast<std::size_t>(audio.frameCount()), interleaved);
|
||||
|
||||
const std::string instanceGuid = processor.usageInstanceGuid();
|
||||
const std::int64_t stamp = static_cast<std::int64_t>(std::time(nullptr));
|
||||
|
||||
// OUTSIDE the bank folder, always: the bank holds indexed captures only, and a stray
|
||||
// file there would read as a prune orphan.
|
||||
std::error_code ec;
|
||||
const fs::path stagedPath =
|
||||
fs::temp_directory_path(ec) /
|
||||
("reasampler_bake_" + instanceGuid + "_" + std::to_string(stamp) + ".wav");
|
||||
if (ec) return fail("no writable temp directory for the staged render");
|
||||
const std::string staged = stagedPath.string();
|
||||
StagedFileGuard stagedGuard(staged);
|
||||
if (!writeFileBytes(staged, bytes)) return fail("could not stage the rendered file");
|
||||
|
||||
BakeRequest request;
|
||||
request.instanceGuid = instanceGuid;
|
||||
request.stagedFilePath = staged;
|
||||
request.sourceSampleId = selectionId;
|
||||
request.sourceRelativePath = source->relativePath;
|
||||
request.sourceDisplayName = sourceEntry->displayName;
|
||||
request.ownUsageKey = usageKeyFor(instanceGuid);
|
||||
request.rootNote = plan->note;
|
||||
request.generation = stamp;
|
||||
|
||||
const std::string key = bakeKeyFor(instanceGuid);
|
||||
RequestKeyGuard keyGuard(bridge, key);
|
||||
if (!bridge.writeBakeExtState(key, wire::encodeBakeRequest(request)))
|
||||
return fail("could not publish the bake request");
|
||||
|
||||
// Synchronous: the extension's landing runs to completion inside this call and writes
|
||||
// its outcome back over the same key before returning.
|
||||
if (!bridge.invokeExtensionAction(wire::bakeActionLookupName()))
|
||||
return fail("the ReaSampler extension's bake action is not registered");
|
||||
|
||||
const std::optional<std::string> raw = bridge.readReasamplerExtState(key);
|
||||
const std::optional<BakeOutcome> outcome =
|
||||
raw ? wire::decodeBakeOutcome(*raw) : std::nullopt;
|
||||
// No outcome at all means the action never reached our request — an older extension
|
||||
// that registers the id but does not read this key, or an invocation REAPER deferred.
|
||||
if (!outcome) return fail("the extension did not answer the bake request");
|
||||
if (outcome->generation != request.generation)
|
||||
return fail("the extension answered a different bake request");
|
||||
if (outcome->status != BakeStatus::Ok)
|
||||
return fail(outcome->message.empty() ? std::string("the bake was refused")
|
||||
: outcome->message);
|
||||
|
||||
SampleRefEntry entry;
|
||||
entry.sampleId = outcome->sampleId;
|
||||
entry.displayName = outcome->displayName;
|
||||
entry.ref.relativePath = outcome->relativePath;
|
||||
entry.ref.rootNote = outcome->rootNote;
|
||||
entry.ref.channelCount = outcome->channelCount;
|
||||
// No loop: the loop points shaped the render and are meaningless against the new file
|
||||
// (bake_reset owns that rule for the parameter set; this is its bank-intrinsic peer).
|
||||
|
||||
const instrument::bake::BakeReset reset = resetAfterBake(dialed);
|
||||
processor.adoptBakedCapture(entry, reset.params, reset.masterGainLinear);
|
||||
|
||||
// The extension's own wording, which distinguishes the three landings (replaced, added,
|
||||
// and pointed at an identical existing entry) more precisely than this side can.
|
||||
return BakeChainResult{true, "resampled -- " + outcome->message};
|
||||
}
|
||||
|
||||
} // namespace reasampler::vst
|
||||
@@ -0,0 +1,36 @@
|
||||
// instrument_bake — the instrument's half of the resample chain: render the dialed sound
|
||||
// offline, stage it outside the bank, ask the extension to bank it, then re-point and go
|
||||
// neutral. UI thread only; nothing here is on any audio path.
|
||||
//
|
||||
// The extension is REQUIRED. Without it the bank has no writer, so the affordance reads
|
||||
// Disabled and this refuses rather than half-baking.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
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.
|
||||
bool bakeAvailable(ReaperBridge& bridge);
|
||||
|
||||
struct BakeChainResult {
|
||||
bool ok = false;
|
||||
std::string message; // always populated — the editor prints it either way
|
||||
};
|
||||
|
||||
// Runs the whole chain against `processor`. On failure NOTHING has changed: no temp file
|
||||
// survives, no bank entry was added, no assign was written, and the dialed state is
|
||||
// exactly as it was before the click.
|
||||
//
|
||||
// MUST NOT be called from a mouse handler. It invokes a REAPER action synchronously — an
|
||||
// offline landing nested inside a click, with the mouse captured, re-pointing the very
|
||||
// instance whose frame is on the stack. The editor defers it to its own timer tick.
|
||||
BakeChainResult runBake(ReaSamplerProcessor& processor);
|
||||
|
||||
} // namespace reasampler::vst
|
||||
@@ -77,6 +77,18 @@ std::optional<DecodedPcm> decodeRelative(const std::string& projectDir,
|
||||
return out;
|
||||
}
|
||||
|
||||
// The ONE decode + build the reload and the bake snapshot share, so a baked render can
|
||||
// never be built from a different fold than the one the user is hearing.
|
||||
std::optional<SampleData> buildFromRef(const SelectedSample& sel,
|
||||
const InstrumentParams& params,
|
||||
const std::string& projectDir, ChannelMode mode) {
|
||||
std::optional<DecodedPcm> pcm = decodeRelative(projectDir, sel.relativePath, mode);
|
||||
if (!pcm) return std::nullopt;
|
||||
SampleData sample = buildSampleData(resolveCapture(sel, params), std::move(*pcm));
|
||||
if (!sample.playable()) return std::nullopt;
|
||||
return sample;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
@@ -144,25 +156,23 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
channelModeExplicit_);
|
||||
mode = channelMode_;
|
||||
}
|
||||
std::optional<DecodedPcm> pcm = decodeRelative(projectDir, sel->relativePath, mode);
|
||||
if (pcm) {
|
||||
sample = buildSampleData(resolveCapture(*sel, params), std::move(*pcm));
|
||||
havePlayable = sample.playable();
|
||||
if (havePlayable) {
|
||||
// Point the built snapshot at the instance's ONE live block and seed it from
|
||||
// the very PlayParams the voices latch, so an untouched knob folds to the same
|
||||
// frames the build resolved and a note-on with a live block sounds identical
|
||||
// to one without.
|
||||
sample.live = &liveParams_;
|
||||
{
|
||||
// reloadMutex_ (held for this whole function) nests livePublishMutex_ here;
|
||||
// publishLiveParams never holds reloadMutex_, so this is the only nesting.
|
||||
std::lock_guard<std::mutex> lp(livePublishMutex_);
|
||||
liveParams_.publish(instrument::engine::foldLive(sample.play));
|
||||
}
|
||||
builtSampleRate_.store(sample.sampleRate, std::memory_order_relaxed);
|
||||
resolvedId = selId; // the concrete pick that resolved
|
||||
if (std::optional<SampleData> decoded =
|
||||
buildFromRef(*sel, params, projectDir, mode)) {
|
||||
sample = std::move(*decoded);
|
||||
havePlayable = true;
|
||||
// Point the built snapshot at the instance's ONE live block and seed it from
|
||||
// the very PlayParams the voices latch, so an untouched knob folds to the same
|
||||
// frames the build resolved and a note-on with a live block sounds identical
|
||||
// to one without.
|
||||
sample.live = &liveParams_;
|
||||
{
|
||||
// reloadMutex_ (held for this whole function) nests livePublishMutex_ here;
|
||||
// publishLiveParams never holds reloadMutex_, so this is the only nesting.
|
||||
std::lock_guard<std::mutex> lp(livePublishMutex_);
|
||||
liveParams_.publish(instrument::engine::foldLive(sample.play));
|
||||
}
|
||||
builtSampleRate_.store(sample.sampleRate, std::memory_order_relaxed);
|
||||
resolvedId = selId; // the concrete pick that resolved
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +200,48 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
return resolvedId;
|
||||
}
|
||||
|
||||
std::string ReaSamplerProcessor::usageInstanceGuid() {
|
||||
std::lock_guard<std::mutex> lock(usageMutex_);
|
||||
if (instanceGuid_.empty()) instanceGuid_ = mintUsageInstanceGuid();
|
||||
return instanceGuid_;
|
||||
}
|
||||
|
||||
std::optional<SampleData> ReaSamplerProcessor::bakeSnapshot() {
|
||||
const std::string selId = selectedSampleId();
|
||||
if (selId.empty()) return std::nullopt;
|
||||
const InstrumentParams params = instrumentParams();
|
||||
SampleRefs refs;
|
||||
{
|
||||
std::lock_guard<std::mutex> rl(refsMutex_);
|
||||
refs = sampleRefs_;
|
||||
}
|
||||
const SelectedSample* sel = findRef(refs, selId);
|
||||
if (!sel) return std::nullopt;
|
||||
// No live block is attached: the bake renders the stored parameter set, which is what
|
||||
// every knob has already written, rather than whatever the audio thread is observing.
|
||||
return buildFromRef(*sel, params, bridge_.activeProjectDir(), channelMode());
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::adoptBakedCapture(const SampleRefEntry& entry,
|
||||
const InstrumentParams& reset,
|
||||
double masterGainLinear) {
|
||||
{
|
||||
std::lock_guard<std::mutex> rl(refsMutex_);
|
||||
bool replaced = false;
|
||||
for (SampleRefEntry& existing : sampleRefs_) {
|
||||
if (existing.sampleId == entry.sampleId) { existing = entry; replaced = true; break; }
|
||||
}
|
||||
if (!replaced) sampleRefs_.push_back(entry);
|
||||
}
|
||||
setSelectedSampleId(entry.sampleId);
|
||||
setInstrumentParams(reset);
|
||||
setMasterGainLinear(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.
|
||||
reloadInstrument();
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::publishUsage(const SampleRefs& refs,
|
||||
const std::vector<std::string>& ids) {
|
||||
if (!bridge_.isConnected()) return; // non-REAPER host / no ext-state — nothing to do
|
||||
|
||||
@@ -42,6 +42,10 @@ bool ReaperBridge::connect(Steinberg::FUnknown* context) {
|
||||
setProjExtState_ = nullptr;
|
||||
getTrackGuid_ = nullptr;
|
||||
guidToString_ = nullptr;
|
||||
namedCommandLookup_ = nullptr;
|
||||
mainOnCommandEx_ = nullptr;
|
||||
getCursorPositionEx_ = nullptr;
|
||||
timeMapGetTimeSigAtTime_ = nullptr;
|
||||
hostApp_ = nullptr;
|
||||
if (!context) return false;
|
||||
|
||||
@@ -69,6 +73,17 @@ bool ReaperBridge::connect(Steinberg::FUnknown* context) {
|
||||
reaper->getReaperApi("GetTrackGUID"));
|
||||
guidToString_ = reinterpret_cast<GuidToStringFn>(
|
||||
reaper->getReaperApi("guidToString"));
|
||||
// The bake crossing: resolve the extension's action id by name, then fire it against
|
||||
// this instance's own project. All degrade to null gracefully — an unresolvable pair
|
||||
// simply leaves the bake affordance disabled.
|
||||
namedCommandLookup_ = reinterpret_cast<NamedCommandLookupFn>(
|
||||
reaper->getReaperApi("NamedCommandLookup"));
|
||||
mainOnCommandEx_ = reinterpret_cast<MainOnCommandExFn>(
|
||||
reaper->getReaperApi("Main_OnCommandEx"));
|
||||
getCursorPositionEx_ = reinterpret_cast<GetCursorPositionExFn>(
|
||||
reaper->getReaperApi("GetCursorPositionEx"));
|
||||
timeMapGetTimeSigAtTime_ = reinterpret_cast<TimeMapGetTimeSigAtTimeFn>(
|
||||
reaper->getReaperApi("TimeMap_GetTimeSigAtTime"));
|
||||
|
||||
return getProjExtState_ != nullptr;
|
||||
}
|
||||
@@ -95,25 +110,70 @@ std::optional<std::string> ReaperBridge::readReasamplerExtState(const std::strin
|
||||
return decodeGetProjExtState(read.apiReturn, read.value);
|
||||
}
|
||||
|
||||
bool ReaperBridge::writeUsageExtState(const std::string& usageKey,
|
||||
const std::string& value) {
|
||||
bool ReaperBridge::writeGuarded(const std::string& key, const std::string& value,
|
||||
const char* requiredPrefix) {
|
||||
if (!setProjExtState_ || !hostApp_) return false;
|
||||
// Read-only-bank guard: this module writes usage keys and nothing else. A non-
|
||||
// "rsusage_" key is refused rather than widening the instrument's write surface
|
||||
// (banks/view/tail/assign stay extension-owned).
|
||||
const std::string prefix = kProjExtUsageKeyPrefix;
|
||||
if (usageKey.compare(0, prefix.size(), prefix) != 0) return false;
|
||||
// Read-only-BANK guard: this module writes the two sanctioned per-instance prefixes
|
||||
// and nothing else. Any other key is refused rather than widening the instrument's
|
||||
// write surface (banks/view/tail/assign stay extension-owned).
|
||||
const std::string prefix = requiredPrefix;
|
||||
if (key.compare(0, prefix.size(), prefix) != 0) return false;
|
||||
|
||||
auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
|
||||
void* proj = reaper->getReaperParent(3); // null = current project (same as reads)
|
||||
// SetProjExtState returns the size of the extname's state — after storing a
|
||||
// non-empty value that's necessarily > 0, so <= 0 means the write did not land (the
|
||||
// publish path retries next reload tick; a silent drop would leave holds unprotected).
|
||||
// A deliberate CLEAR (empty value) shrinks the state and can legitimately return 0,
|
||||
// so it is reported as landed.
|
||||
const int rv =
|
||||
setProjExtState_(proj, kProjExtNamespace(), usageKey.c_str(), value.c_str());
|
||||
// Deliberately NO MarkProjectDirty: a usage change always rides a component-state
|
||||
// change that already dirties the project.
|
||||
return rv > 0;
|
||||
setProjExtState_(proj, kProjExtNamespace(), key.c_str(), value.c_str());
|
||||
return value.empty() ? true : rv > 0;
|
||||
}
|
||||
|
||||
bool ReaperBridge::writeUsageExtState(const std::string& usageKey,
|
||||
const std::string& value) {
|
||||
return writeGuarded(usageKey, value, kProjExtUsageKeyPrefix);
|
||||
}
|
||||
|
||||
bool ReaperBridge::writeBakeExtState(const std::string& bakeKey,
|
||||
const std::string& value) {
|
||||
return writeGuarded(bakeKey, value, kProjExtBakeKeyPrefix);
|
||||
}
|
||||
|
||||
int ReaperBridge::lookupCommand(const std::string& commandName) {
|
||||
if (!namedCommandLookup_ || commandName.empty()) return 0;
|
||||
return namedCommandLookup_(commandName.c_str());
|
||||
}
|
||||
|
||||
bool ReaperBridge::extensionActionAvailable(const std::string& commandName) {
|
||||
return mainOnCommandEx_ != nullptr && lookupCommand(commandName) != 0;
|
||||
}
|
||||
|
||||
bool ReaperBridge::invokeExtensionAction(const std::string& commandName) {
|
||||
if (!mainOnCommandEx_ || !hostApp_) return false;
|
||||
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.
|
||||
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.
|
||||
mainOnCommandEx_(command, 0, proj);
|
||||
return true;
|
||||
}
|
||||
|
||||
double ReaperBridge::projectTempoBpm() {
|
||||
if (!timeMapGetTimeSigAtTime_ || !getCursorPositionEx_ || !hostApp_) return 0.0;
|
||||
auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
|
||||
void* proj = reaper->getReaperParent(3);
|
||||
int num = 0, denom = 0;
|
||||
double tempo = 0.0;
|
||||
timeMapGetTimeSigAtTime_(proj, getCursorPositionEx_(proj), &num, &denom, &tempo);
|
||||
// The meter is read but not applied: the note model's beat is a quarter note by
|
||||
// ruling (core/instrument/note/CLAUDE.md), so the undivided BPM is the right one.
|
||||
return tempo;
|
||||
}
|
||||
|
||||
std::string ReaperBridge::currentTrackGuid() {
|
||||
|
||||
@@ -44,15 +44,45 @@ public:
|
||||
// project or when unconnected. Not RT-safe.
|
||||
std::string activeProjectDir();
|
||||
|
||||
// Writes THIS INSTANCE's usage record: the ONE sanctioned instrument-side ext-state
|
||||
// write. `usageKey` MUST carry the "rsusage_" prefix (ext_keys.h's usageKeyFor); any
|
||||
// other key is refused, enforcing the read-only-bank invariant structurally (banks/
|
||||
// view/tail/assign stay unwritable from the instrument). Returns true iff written
|
||||
// (the SetProjExtState return is checked). NOT RT-safe — publish sites are the
|
||||
// off-audio-thread reload path only. Deliberately does NOT mark the project dirty: a
|
||||
// usage change always rides a component-state change that already does.
|
||||
// The instrument's TWO sanctioned ext-state write surfaces, each accepting exactly one
|
||||
// key prefix and refusing every other key. That structural refusal is what keeps the
|
||||
// read-only-BANK invariant intact — banks/view/tail/assign stay unwritable from here —
|
||||
// and neither payload is bank state. Both return true iff the write landed (the
|
||||
// SetProjExtState return is checked) and neither is RT-safe: the call sites are the
|
||||
// off-audio-thread reload path and the editor's UI tick.
|
||||
//
|
||||
// Neither marks the project dirty. A usage change always rides a component-state change
|
||||
// that already does; a bake request is transient and is cleared in the same tick.
|
||||
|
||||
// THIS INSTANCE's usage record. `usageKey` MUST carry the "rsusage_" prefix
|
||||
// (ext_keys.h's usageKeyFor).
|
||||
bool writeUsageExtState(const std::string& usageKey, const std::string& value);
|
||||
|
||||
// THIS INSTANCE's resample-bake request. `bakeKey` MUST carry the "rsbake_" prefix
|
||||
// (ext_keys.h's bakeKeyFor). An empty value clears the key.
|
||||
bool writeBakeExtState(const std::string& bakeKey, const std::string& value);
|
||||
|
||||
// --- Extension action invocation (the bake crossing) ------------------------------
|
||||
//
|
||||
// `commandName` is the NamedCommandLookup spelling — the registered command_id string
|
||||
// with a leading underscore, which the registration string itself does not carry.
|
||||
|
||||
// Whether the extension is loaded AND has registered this action. Used to paint the
|
||||
// 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.
|
||||
bool invokeExtensionAction(const std::string& commandName);
|
||||
|
||||
// The effective project tempo (BPM, quarter notes per minute) at the edit cursor of
|
||||
// this instance's own project. 0.0 when unconnected or unresolvable — the caller
|
||||
// refuses rather than substituting a tempo (no hardcoded rates or tempos in src/).
|
||||
double projectTempoBpm();
|
||||
|
||||
// The canonical GUID string of the track hosting this FX instance (same rendering as
|
||||
// the extension's track_guid::guidString, so usage records compare byte-equal
|
||||
// against its live-FX enumeration). Empty when unconnected or no track context (the
|
||||
@@ -70,9 +100,14 @@ private:
|
||||
// EnumProjects(-1, projfnOut, sz) -> active project + its .rpp path. idx=-1 (current
|
||||
// tab) follows the active project, same convention as the persist shell.
|
||||
using EnumProjectsFn = void* (*)(int idx, char* projfnOut, int projfnOut_sz);
|
||||
// Used ONLY by writeUsageExtState (prefix-guarded) — see the read-only-bank note there.
|
||||
// Used ONLY by the two prefix-guarded writers — see the read-only-bank note there.
|
||||
using SetProjExtStateFn = int (*)(void* proj, const char* extname, const char* key,
|
||||
const char* value);
|
||||
using NamedCommandLookupFn = int (*)(const char* commandName);
|
||||
using MainOnCommandExFn = void (*)(int command, int flag, void* proj);
|
||||
using GetCursorPositionExFn = double (*)(void* proj);
|
||||
using TimeMapGetTimeSigAtTimeFn = void (*)(void* proj, double time, int* numOut,
|
||||
int* denomOut, double* tempoOut);
|
||||
// Opaque-pointer signatures so the header stays SDK-type-free; the GUID* is passed
|
||||
// straight through, never dereferenced here.
|
||||
using GetTrackGuidFn = void* (*)(void* tr);
|
||||
@@ -85,6 +120,19 @@ private:
|
||||
SetProjExtStateFn setProjExtState_ = nullptr;
|
||||
GetTrackGuidFn getTrackGuid_ = nullptr;
|
||||
GuidToStringFn guidToString_ = nullptr;
|
||||
NamedCommandLookupFn namedCommandLookup_ = nullptr;
|
||||
MainOnCommandExFn mainOnCommandEx_ = nullptr;
|
||||
GetCursorPositionExFn getCursorPositionEx_ = nullptr;
|
||||
TimeMapGetTimeSigAtTimeFn timeMapGetTimeSigAtTime_ = nullptr;
|
||||
|
||||
// The one prefix guard both public writers route through, so the two cannot diverge
|
||||
// in how strictly they refuse a key.
|
||||
bool writeGuarded(const std::string& key, const std::string& value,
|
||||
const char* requiredPrefix);
|
||||
// 0 when the action is not registered (the extension is absent or older). REAPER's
|
||||
// documented "not found" return is 0 by convention only — the header does not state
|
||||
// it — so every caller treats 0 as unavailable and never as a valid command id.
|
||||
int lookupCommand(const std::string& commandName);
|
||||
};
|
||||
|
||||
} // namespace reasampler::vst
|
||||
|
||||
@@ -121,6 +121,7 @@ private:
|
||||
kChanMono, // the mono channel-mode segment
|
||||
kChanStereo, // the stereo channel-mode segment
|
||||
kPreview, // the preview-trigger button
|
||||
kBake, // the resample-bake trigger
|
||||
kControl, // a knob-deck element (index = control id)
|
||||
kInnerDial, // a knob cell's inner curve dial (index = the OUTER control id)
|
||||
kEnvRadio, // an envelope deck's overlay-select radio (index = radio control id)
|
||||
@@ -422,6 +423,16 @@ private:
|
||||
// up. One note at a time — a fresh press releases the prior.
|
||||
int previewingNote_ = -1;
|
||||
|
||||
// Resample bake. The click only ARMS it; the sync tick runs it. Running it inline
|
||||
// would nest a synchronous REAPER action — which re-points this very instance — inside
|
||||
// a mouse handler with the capture held.
|
||||
bool bakePending_ = false;
|
||||
// Whether the extension's bake action is registered, resolved on the same tick that
|
||||
// governs the button's paint, so the control is never enabled and then refusing.
|
||||
bool bakeAvailable_ = false;
|
||||
std::string bakeMessage_; // last outcome, shown in the title band
|
||||
int bakeMessageTicks_ = 0; // sync ticks the message survives
|
||||
|
||||
// The editor-drop -> extension-ingest relay is not shipped (the bridge is read-only):
|
||||
// an OS drop just flashes a "drop on the panel instead" banner (dropHintTicks_ counts
|
||||
// down via the sync tick). Never ingests, never inserts a timeline item.
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace reasampler::vst {
|
||||
|
||||
using instrument::map::ComponentState;
|
||||
using instrument::map::InstrumentParams;
|
||||
using instrument::map::SampleRefEntry;
|
||||
using instrument::map::SampleRefs;
|
||||
using instrument::map::kPreviewVelocityDefault;
|
||||
|
||||
@@ -120,6 +121,21 @@ public:
|
||||
// resolved selection id ("" if nothing loaded).
|
||||
std::string reloadInstrument();
|
||||
|
||||
// The dialed sound as plain data for the offline bake: the SAME refs resolve + decode +
|
||||
// build reloadInstrument runs, with no live block attached. Rebuilt rather than copied
|
||||
// off the live snapshot because a tier-3 live edit leaves that snapshot's own play
|
||||
// params stale on purpose — copying it would bake the pre-drag values. nullopt when
|
||||
// nothing is loaded or the WAV is unreadable. Off the audio thread (file I/O).
|
||||
std::optional<SampleData> bakeSnapshot();
|
||||
|
||||
// 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.
|
||||
void adoptBakedCapture(const SampleRefEntry& entry, const InstrumentParams& reset,
|
||||
double masterGainLinear);
|
||||
|
||||
// What pollBankSync did this tick, so the editor can react only when something changed.
|
||||
struct BankSyncResult {
|
||||
bool reloaded = false; // bank generation changed (or a legacy lift landed) -> reloaded
|
||||
@@ -203,6 +219,11 @@ public:
|
||||
// fallback when the bank blob is unreadable. Guarded by refsMutex_.
|
||||
SampleRefs sampleRefs();
|
||||
|
||||
// This instance's usage identity, minted here if it has never published. It names BOTH
|
||||
// the "rsusage_" record the bake's tie query must exclude and the "rsbake_" request
|
||||
// key, so the two can never name different instances. Off the audio thread.
|
||||
std::string usageInstanceGuid();
|
||||
|
||||
private:
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user