Q-W6: registration table (OCP) in main.cpp; bank verbs -> shell/bank_ops(Session&); persist.h + wav_trim + namespaces.h shims deleted; 61/61
capture.h realtime seam split to capture_realtime_shell.h; GetProjExtState grow-loop rehomed to core/wire/ext_state_read; stale persist.cpp/bank_panel.cpp comment refs fixed; CLAUDE.md persist/bank_book/actions bullets updated. Command-id suffixes, display phrases, and undo labels byte-identical.
This commit is contained in:
@@ -110,7 +110,7 @@ std::string resolveBankFile(const std::string& projectDir,
|
||||
|
||||
std::string projectDirOfRpp(const std::string& rppPath) {
|
||||
// An unsaved project reports an empty .rpp path; keep it empty so downstream
|
||||
// resolution refuses (no default-location fallback). Mirrors persist.cpp's prior
|
||||
// resolution refuses (no default-location fallback). Mirrors the former persist shell's
|
||||
// projectDirOf exactly: parent_path of the .rpp, then normalizeSlashes.
|
||||
if (rppPath.empty()) return {};
|
||||
std::string dir = std::filesystem::path(rppPath).parent_path().string();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
// tail_control — the REAPER-free logic behind the docked bank_panel's tail-mode
|
||||
// toggle. The panel shell (bank_panel.cpp) owns the SWELL window, LICE drawing, and
|
||||
// toggle. The panel shell (shell/panel/) owns the SWELL window, LICE drawing, and
|
||||
// click hit-testing; what is NOT DAW-bound — the cycle order, the manual-length
|
||||
// clamp, and the toggle's label text — lives here so it is unit-tested outside the
|
||||
// DAW (CLAUDE.md §load-bearing split). Mirror of bank_grid / mode_switch.
|
||||
@@ -27,7 +27,7 @@ inline constexpr double kDefaultManualTailMs = 2000.0;
|
||||
inline constexpr double kManualStepMs = 250.0;
|
||||
|
||||
// The panel's current tail setting: the mode plus the length used ONLY when the
|
||||
// mode is Manual. Held as in-memory panel/session state (bank_panel.cpp), default
|
||||
// mode is Manual. Held as in-memory panel/session state (shell/panel), default
|
||||
// None so a capture with no explicit choice stays exact-bounds / byte-identical to
|
||||
// today. `manualMs` is a stored default a future fine-adjust UI can tune; it is
|
||||
// clamped to the 8 s cap (kMaxTailMs) before it ever reaches a CaptureRequest.
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#pragma once
|
||||
// wav_trim — TRANSITIONAL forwarding header (Q-W3, audit §4e WAV/RIFF consolidation).
|
||||
//
|
||||
// The one pure owner of the WAV/RIFF byte format is now core/capture/wav_codec.{h,cpp}
|
||||
// (chunk walker + layout parse + float32 build + size-field patch + content hash).
|
||||
// Everything this header used to declare (WavLayout / parseWavLayout /
|
||||
// extractFloatFrames / WavTruncatePlan / planWavTruncate) lives there, same
|
||||
// namespace (reasampler::capture), same signatures — this include is a pure alias.
|
||||
//
|
||||
// Kept ONLY so the TUs a parallel wave owns (sample_map.h and the VST editor/
|
||||
// processor god-TUs, Q-W2v) compile untouched — editing them here would collide
|
||||
// with that wave's in-flight split. Retire this header (and point its includers at
|
||||
// wav_codec.h) once Q-W2v lands.
|
||||
|
||||
#include "core/capture/wav_codec.h"
|
||||
@@ -25,7 +25,7 @@
|
||||
// #include <windows.h>` unconditionally, which CANNOT enter the pure sampler_core module
|
||||
// (CLAUDE.md load-bearing split: NO vendor/host/SDK types; sampler_core_tests links neither
|
||||
// SDK and compiles outside the DAW). So the Preserve DSP lands as route (b): a house-native
|
||||
// pure module alongside peaks / wav_trim, CTest-testable, RT-disciplined. Same
|
||||
// pure module alongside peaks / wav_codec, CTest-testable, RT-disciplined. Same
|
||||
// PitchEngine::Preserve contract behind the seam — if WDL is ever preferred it swaps in at
|
||||
// the SHELL, never in the pure core.
|
||||
//
|
||||
@@ -53,7 +53,7 @@
|
||||
//
|
||||
// PURE MODULE: NO VST3, NO REAPER, NO SWELL, NO vendor/ includes. Standard library only.
|
||||
// Shares the `AudioSample` float alias from peaks (the one house precedent — sampler_core /
|
||||
// wav_trim do the same).
|
||||
// wav_codec does the same).
|
||||
//
|
||||
// RT DISCIPLINE (S16 hard constraint). `configure()` sizes the ring ONCE (off the audio
|
||||
// thread, at voice allocation). `prime()` / `warm()` only copy into the pre-sized ring
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// structurally: sampler_core_tests links neither SDK (see CMakeLists §2i).
|
||||
//
|
||||
// It shares the `AudioSample` float alias from peaks — the one house precedent for a
|
||||
// pure module leaning on peaks for the audio-domain type (wav_trim does the same). The
|
||||
// pure module leaning on peaks for the audio-domain type (wav_codec does the same). The
|
||||
// S2 seam fields (root note, loop points) enter as plain int / frame-index inputs; the
|
||||
// core does no file I/O — it is handed decoded sample frames and produces audio frames.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// The bridge shell (reaper_bridge.cpp) resolves REAPER API functions by name over the
|
||||
// host callback and invokes them; the one fiddly-and-easy-to-get-wrong part around
|
||||
// GetProjExtState — interpreting its int return against the buffer it filled — is pure
|
||||
// and unit-tested here. Mirror of capture_paths / wav_trim splitting the arithmetic out
|
||||
// and unit-tested here. Mirror of capture_paths / wav_codec splitting the arithmetic out
|
||||
// of a REAPER-facing shell.
|
||||
//
|
||||
// The S1 spike ALSO carried a string-scan JSON reader (extractJsonStringField) as a
|
||||
@@ -36,63 +36,8 @@ namespace reasampler::instrument::map {
|
||||
std::optional<std::string> decodeGetProjExtState(int apiReturn,
|
||||
const std::string& buffer);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The GetProjExtState GROW-LOOP retry policy (Q-W5 rider, T2-04).
|
||||
// ---------------------------------------------------------------------------
|
||||
// GetProjExtState writes into a caller-supplied buffer with no documented
|
||||
// query-the-size call, so a large value (bank blob, usage record) must be read by
|
||||
// growing a buffer until the value fits strictly inside it. Three shells carried
|
||||
// hand-rolled copies of that loop (persist's ext-state reads, usage_scan's
|
||||
// prune-safety-adjacent record read, reaper_bridge's VST-side bank read); the ONE
|
||||
// policy now lives here so the retry/termination rules cannot drift. The fiddly
|
||||
// part is the termination taxonomy, which each caller folds differently:
|
||||
//
|
||||
// * Absent — the API returned <= 0 on some attempt: the key holds no value.
|
||||
// (persist -> "" empty bank; usage_scan / bridge -> nullopt)
|
||||
// * Complete — the written C string fits STRICTLY inside the buffer (size+1 <
|
||||
// cap), so it cannot have been clipped: `value` is the whole value.
|
||||
// * Overflow — the value never fit under the 16 MB ceiling: it is unreadable
|
||||
// WHOLE, which is NOT the same as absent. (persist warns on the
|
||||
// console; usage_scan folds it to the prune fail-safe abort)
|
||||
//
|
||||
// `read` is one GetProjExtState-shaped attempt: int read(char* buf, int cap),
|
||||
// returning the API's int. A template, statically dispatched per call site — no
|
||||
// virtual calls, no std::function (the §3 performance guardrail); the caller binds
|
||||
// the project/namespace/key (or a resolved function pointer, VST side) in a lambda.
|
||||
struct GrowingExtStateRead {
|
||||
enum class Status { Absent, Complete, Overflow };
|
||||
Status status = Status::Absent;
|
||||
int apiReturn = 0; // the FINAL attempt's return (<= 0 iff Absent); feeds
|
||||
// decodeGetProjExtState on the bridge path unchanged
|
||||
std::string value; // the whole value; meaningful only when Complete
|
||||
};
|
||||
|
||||
template <class ReadFn>
|
||||
GrowingExtStateRead readProjExtStateGrowing(ReadFn&& read) {
|
||||
// Start generous; grow ×4 if REAPER reports the value may have been clipped
|
||||
// (the return is the value length; equal-to-capacity-minus-NUL is ambiguous,
|
||||
// so only a strict fit terminates). Ceiling 16 MB — give up rather than loop
|
||||
// forever on a pathological value.
|
||||
GrowingExtStateRead result;
|
||||
for (int cap = 1 << 16; cap <= (1 << 24); cap <<= 2) {
|
||||
std::vector<char> buf(static_cast<std::size_t>(cap), '\0');
|
||||
const int rv = read(buf.data(), cap);
|
||||
result.apiReturn = rv;
|
||||
if (rv <= 0) {
|
||||
result.status = GrowingExtStateRead::Status::Absent;
|
||||
return result;
|
||||
}
|
||||
buf[static_cast<std::size_t>(cap) - 1] = '\0'; // defensive: guard against a read() that fills the buffer without honoring NUL-termination within cap
|
||||
std::string s(buf.data());
|
||||
if (static_cast<int>(s.size()) + 1 < cap) {
|
||||
result.status = GrowingExtStateRead::Status::Complete;
|
||||
result.value = std::move(s);
|
||||
return result;
|
||||
}
|
||||
// else: possibly truncated -> grow and retry.
|
||||
}
|
||||
result.status = GrowingExtStateRead::Status::Overflow;
|
||||
return result;
|
||||
}
|
||||
// The GetProjExtState GROW-LOOP retry policy (T2-04) lived here through Q-W5; it
|
||||
// was rehomed to core/wire/ext_state_read.h in Q-W6 (its consumers are 2:1
|
||||
// extension-side, so it belongs on the neutral wire seam, not the instrument map).
|
||||
|
||||
} // namespace reasampler::instrument::map
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// sample_map — pure implementation (the RESOLUTION half; the ComponentState codec
|
||||
// lives in component_state_io.cpp since Q-W2v). See sample_map.h. NO VST3 / REAPER /
|
||||
// SWELL / vendor includes; standard library + the pure bank_book / wav_trim / sampler_core.
|
||||
// SWELL / vendor includes; standard library + the pure bank_book / wav_codec / sampler_core.
|
||||
|
||||
#include "core/instrument/map/sample_map.h"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// "reasampler" bank ext-state + a decoded WAV into the plain data the sampler core
|
||||
// plays, and (de)serialize the instance's selected-sample choice for VST3 component
|
||||
// state. NO VST3, NO REAPER, NO SWELL, NO vendor/ includes at the boundary — the
|
||||
// mirror of capture_paths / wav_trim / bridge_marshal splitting the fiddly, testable
|
||||
// mirror of capture_paths / wav_codec / bridge_marshal splitting the fiddly, testable
|
||||
// arithmetic out of a host-facing shell.
|
||||
//
|
||||
// WHY IT EXISTS (S4 seams). The instrument reads the bank over the live-state seam
|
||||
@@ -14,7 +14,7 @@
|
||||
// downmix its decoded PCM to the core's mono contract, and build the Tier-0 chromatic
|
||||
// Keymap — is pure and unit-tested here.
|
||||
//
|
||||
// It links bank_book (the shared BankBook::deserialize) and wav_trim (the shared
|
||||
// It links bank_book (the shared BankBook::deserialize) and wav_codec (the shared
|
||||
// 32-bit-float WAV parse — no third WAV reader) and sampler_core (the Keymap /
|
||||
// SampleData it produces). All three are pure; this stays pure.
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
#include "core/model/bank_book.h" // BankBook::deserialize (shared bank JSON parse)
|
||||
#include "core/instrument/engine/sampler_core.h" // Keymap, SampleData, SampleLoop
|
||||
#include "core/capture/wav_trim.h" // parseWavLayout, extractFloatFrames (shared WAV parse)
|
||||
#include "core/capture/wav_codec.h" // parseWavLayout, extractFloatFrames (shared WAV parse)
|
||||
|
||||
namespace reasampler::instrument::map {
|
||||
|
||||
@@ -167,7 +167,7 @@ struct BankChoice {
|
||||
};
|
||||
std::vector<BankChoice> listBanks(const std::string& banksJson);
|
||||
|
||||
// Downmix interleaved float frames (the shape wav_trim::extractFloatFrames yields:
|
||||
// Downmix interleaved float frames (the shape wav_codec's extractFloatFrames yields:
|
||||
// [f0c0,f0c1,...,f1c0,...]) to the core's MONO contract by AVERAGING channels per
|
||||
// frame. `channelCount` is the interleave stride (>= 1). CHANNEL POLICY (Tier 0,
|
||||
// documented + surfaced): the S3 core is mono-per-sample by design; bank WAVs preserve
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
// right edge and wins the tie, so the fade can be dragged open from zero).
|
||||
//
|
||||
// DELIBERATELY ENGINE-FREE (house pattern — param_slider does the same). It does NOT depend on
|
||||
// sample_map / sampler_core (which would drag bank_book / wav_trim in). The shell reads the
|
||||
// sample_map / sampler_core (which would drag bank_book / wav_codec in). The shell reads the
|
||||
// zone's AdsrSeconds / TriggerParams and packs them into the small AmpEnvelope view struct here.
|
||||
// AHDSR times are wall-clock SECONDS (rate-free, matching the stored domain — Daniel's no-
|
||||
// hardcoded-rate ruling); Trigger fades are FRACTIONS of the play span. The one rate-bound input
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
//
|
||||
// It reuses the same Rect + contains() as editor_geometry (one shared geometry idiom), so
|
||||
// this header depends on editor_geometry.h rather than redefining a rectangle type. Audio
|
||||
// is the peaks AudioSample float alias (the one house precedent — sampler_core / wav_trim do
|
||||
// is the peaks AudioSample float alias (the one house precedent — sampler_core / wav_codec do
|
||||
// the same), so the zero-crossing helper takes the same mono PCM the shell already decoded.
|
||||
|
||||
#pragma once
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//
|
||||
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO
|
||||
// vendor/ includes. Standard library only. Unit-tested outside the DAW — the same
|
||||
// "small pure type + JSON round-trip" pattern as wav_trim / tab_strip.
|
||||
// "small pure type + JSON round-trip" pattern as wav_codec / tab_strip.
|
||||
//
|
||||
// -- What it is --------------------------------------------------------------
|
||||
//
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
#pragma once
|
||||
// core/namespaces.h — Q-W1 INTERIM flat-namespace shim for the not-yet-split
|
||||
// god/shell TUs (bank_panel / actions / persist / ingest / main / view / capture
|
||||
// shells / the VST editor+processor). The Q-W1 sub-namespaces move every clean pure
|
||||
// module's symbols out of the flat `reasampler` namespace; the god modules keep their
|
||||
// pre-split internals, which reference those symbols unqualified (or qualified as
|
||||
// `reasampler::X`). Nominating every sub-namespace inside `reasampler` restores both
|
||||
// forms ([namespace.qual]p2 routes qualified lookup through using-directives), so the
|
||||
// god internals stay untouched until their own split waves.
|
||||
//
|
||||
// SCOPE CONTRACT: included ONLY by god/shell TUs pending their split wave
|
||||
// (Q-W2/Q-W2v/Q-W3/Q-W4/Q-W5). Clean core modules must NOT include this — they
|
||||
// reference cross-subsystem symbols by their real namespace homes. Each split wave
|
||||
// drops this include from the TUs it rewrites; when the last split lands, delete
|
||||
// this header.
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace model {}
|
||||
namespace view {}
|
||||
namespace capture {}
|
||||
namespace audio {}
|
||||
namespace ui {}
|
||||
namespace reclaim {}
|
||||
namespace version {}
|
||||
namespace json {}
|
||||
namespace util {}
|
||||
namespace wire {}
|
||||
namespace instrument {
|
||||
namespace engine {}
|
||||
namespace map {}
|
||||
namespace ui {}
|
||||
} // namespace instrument
|
||||
|
||||
using namespace model;
|
||||
using namespace view;
|
||||
using namespace capture;
|
||||
using namespace audio;
|
||||
using namespace ui;
|
||||
using namespace reclaim;
|
||||
using namespace version;
|
||||
using namespace util;
|
||||
using namespace wire;
|
||||
using namespace instrument::engine;
|
||||
using namespace instrument::map;
|
||||
using namespace instrument::ui;
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
#include "core/ui/rect.h"
|
||||
// bank_grid — the REAPER-free layout math and cache-key logic behind the docked
|
||||
// bank_panel (M5, Wave A). The panel shell (bank_panel.cpp) owns the SWELL window,
|
||||
// bank_panel (M5, Wave A). The panel shell (shell/panel/) owns the SWELL window,
|
||||
// LICE drawing, and PCM reads; ALL of that is REAPER-bound and DAW-verified. What
|
||||
// is NOT DAW-bound — how N sample cells tile a panel of a given pixel size, and
|
||||
// the key that identifies a cached thumbnail — lives here so it is unit-tested
|
||||
@@ -80,7 +80,7 @@ std::string thumbnailKeyString(const ThumbnailKey& key);
|
||||
// --- Interaction (M5 Wave B): hit-test, selection, keyboard nav --------------
|
||||
//
|
||||
// All REAPER-free so the panel's interaction LOGIC is unit-tested outside the DAW,
|
||||
// exactly as the layout math is. The panel shell (bank_panel.cpp) reads live mouse
|
||||
// exactly as the layout math is. The panel shell (shell/panel/) reads live mouse
|
||||
// coordinates / key codes / modifier state via SWELL and calls into these; it owns
|
||||
// no selection arithmetic of its own.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// card_drag — the REAPER-free decision logic behind the L7 in-grid reorder drag. Three
|
||||
// pure concerns live here so they are unit-tested outside the DAW (CLAUDE.md §load-bearing
|
||||
// split); the SWELL wiring, SetCursor call, cursor resources, and drop-target draw stay in
|
||||
// the shell (bank_panel.cpp). Mirror of drag_out::decideGesture.
|
||||
// the shell (shell/panel/panel_drag.cpp). Mirror of drag_out::decideGesture.
|
||||
//
|
||||
// 1. GESTURE PRECEDENCE (F3 settled). A live drag resolves to exactly one gesture, in a
|
||||
// strict precedence the shell evaluates on every mouse-move / at drop:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// drag_out — the REAPER-free / OS-free decision logic behind the bank_panel's native OS
|
||||
// drag-out (Milestone 11, the final polish point). Two pure concerns live here so they are
|
||||
// unit-tested outside the DAW (CLAUDE.md §load-bearing split); the OLE / SWELL initiation
|
||||
// and the bank_panel gesture hook stay in the shell (drag_out_win.* + bank_panel.cpp).
|
||||
// and the bank_panel gesture hook stay in the shell (drag_out_win.* + shell/panel/panel_drag.cpp).
|
||||
//
|
||||
// 1. GESTURE BOUNDARY (invariant #4 — do not regress the internal drag). The panel
|
||||
// already runs an INTERNAL drag: press a selected cell, cross a threshold, drop onto
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// footer_bar — the REAPER-free, LICE-free layout + hit-test math for the bank_panel's L4
|
||||
// footer LEFT group: the narrowed [Arrange|Design] mode toggle, its compact per-mode count
|
||||
// label, and the Tail button, laid out left-to-right at the footer's left. The panel shell
|
||||
// (bank_panel.cpp) owns the SWELL window, LICE drawing, and the click dispatch (cycle tail /
|
||||
// (shell/panel/) owns the SWELL window, LICE drawing, and the click dispatch (cycle tail /
|
||||
// activate a mode); what is NOT DAW-bound — WHERE the toggle box, the count label, and the
|
||||
// Tail button sit, and which one a click lands on — lives here so it is unit-tested outside
|
||||
// the DAW (CLAUDE.md §load-bearing split). Mirror of action_bar / mode_switch / prune_button.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// prune_button — the REAPER-free layout math behind the bank_panel's Prune button
|
||||
// (Phase R, Wave 3 — R3, fork R-E). A single labelled button drawn in the panel's
|
||||
// tail-footer strip that fires the "Prune bank folder" command. The panel shell
|
||||
// (bank_panel.cpp) owns the SWELL window, LICE drawing, and the Main_OnCommand
|
||||
// (shell/panel/) owns the SWELL window, LICE drawing, and the Main_OnCommand
|
||||
// dispatch of the registered command id — all REAPER-bound, DAW-verified. What is
|
||||
// NOT DAW-bound — WHERE the button sits in the footer and whether a click lands on
|
||||
// it — lives here so it is unit-tested outside the DAW (CLAUDE.md §load-bearing
|
||||
@@ -42,7 +42,7 @@ using ButtonRect = Rect; // Q-W1: the shared concrete ui::Rect (core/ui/rect.h)
|
||||
// * buttonWidth — the button's fixed width.
|
||||
// * rightInset — gap from the footer's right edge to the button's right edge (the
|
||||
// button sits left of this inset, clearing the right-aligned version
|
||||
// readout). COUPLED TO drawFooter (bank_panel.cpp): the version readout
|
||||
// readout). COUPLED TO drawFooter (panel_render.cpp): the version readout
|
||||
// uses an 8 px right margin. The button's right edge lands at
|
||||
// footer.right - 84, i.e. 76 px left of the readout's right margin —
|
||||
// enough clearance for the ~10-char label. ALSO COUPLED to
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// tabs). What is NOT DAW-bound — how N fixed-width tabs tile a strip of a given
|
||||
// pixel width, where the overflow chevrons sit, which tab/chevron a click lands in,
|
||||
// and how far the strip may scroll — lives here so it is unit-tested outside the
|
||||
// DAW (CLAUDE.md §load-bearing split). The panel shell (bank_panel.cpp) owns the
|
||||
// DAW (CLAUDE.md §load-bearing split). The panel shell (shell/panel/) owns the
|
||||
// SWELL window, LICE drawing, and the live BankBook read; it calls into this seam
|
||||
// for every rect and every hit. Mirror of mode_switch / bank_grid.
|
||||
//
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// mode_switch — the REAPER-free layout math behind the bank_panel's Design-View
|
||||
// mode switch (Phase D, Wave 4 — D5). A segmented control `[ Arrange | Design ]`
|
||||
// (N-mode general, one segment per registered mode) drawn in a fixed-height header
|
||||
// strip at the top of the docked panel. The panel shell (bank_panel.cpp) owns the
|
||||
// strip at the top of the docked panel. The panel shell (shell/panel/) owns the
|
||||
// SWELL window, LICE drawing, and the live ViewModeModel read + mode activation —
|
||||
// all REAPER-bound, DAW-verified. What is NOT DAW-bound — how N segments tile a
|
||||
// header rectangle, and which segment a click lands in — lives here so it is
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
// ext_state_read — the GetProjExtState GROW-LOOP retry policy (T2-04; rehomed to
|
||||
// core/wire in Q-W6 — its consumers are the extension's persist/usage-scan shells
|
||||
// AND the instrument's bridge, so it lives on the neutral wire seam rather than in
|
||||
// the instrument-side bridge_marshal decode helper it started in).
|
||||
//
|
||||
// GetProjExtState writes into a caller-supplied buffer with no documented
|
||||
// query-the-size call, so a large value (bank blob, usage record) must be read by
|
||||
// growing a buffer until the value fits strictly inside it. Three shells carried
|
||||
// hand-rolled copies of that loop (persist's ext-state reads, usage_scan's
|
||||
// prune-safety-adjacent record read, reaper_bridge's VST-side bank read); the ONE
|
||||
// policy lives here so the retry/termination rules cannot drift. The fiddly part
|
||||
// is the termination taxonomy, which each caller folds differently:
|
||||
//
|
||||
// * Absent — the API returned <= 0 on some attempt: the key holds no value.
|
||||
// (persist -> "" empty bank; usage_scan / bridge -> nullopt)
|
||||
// * Complete — the written C string fits STRICTLY inside the buffer (size+1 <
|
||||
// cap), so it cannot have been clipped: `value` is the whole value.
|
||||
// * Overflow — the value never fit under the 16 MB ceiling: it is unreadable
|
||||
// WHOLE, which is NOT the same as absent. (persist warns on the
|
||||
// console; usage_scan folds it to the prune fail-safe abort)
|
||||
//
|
||||
// `read` is one GetProjExtState-shaped attempt: int read(char* buf, int cap),
|
||||
// returning the API's int. A template, statically dispatched per call site — no
|
||||
// virtual calls, no std::function (the §3 performance guardrail); the caller binds
|
||||
// the project/namespace/key (or a resolved function pointer, VST side) in a lambda.
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace reasampler::wire {
|
||||
|
||||
struct GrowingExtStateRead {
|
||||
enum class Status { Absent, Complete, Overflow };
|
||||
Status status = Status::Absent;
|
||||
int apiReturn = 0; // the FINAL attempt's return (<= 0 iff Absent); feeds
|
||||
// decodeGetProjExtState on the bridge path unchanged
|
||||
std::string value; // the whole value; meaningful only when Complete
|
||||
};
|
||||
|
||||
template <class ReadFn>
|
||||
GrowingExtStateRead readProjExtStateGrowing(ReadFn&& read) {
|
||||
// Start generous; grow ×4 if REAPER reports the value may have been clipped
|
||||
// (the return is the value length; equal-to-capacity-minus-NUL is ambiguous,
|
||||
// so only a strict fit terminates). Ceiling 16 MB — give up rather than loop
|
||||
// forever on a pathological value.
|
||||
GrowingExtStateRead result;
|
||||
for (int cap = 1 << 16; cap <= (1 << 24); cap <<= 2) {
|
||||
std::vector<char> buf(static_cast<std::size_t>(cap), '\0');
|
||||
const int rv = read(buf.data(), cap);
|
||||
result.apiReturn = rv;
|
||||
if (rv <= 0) {
|
||||
result.status = GrowingExtStateRead::Status::Absent;
|
||||
return result;
|
||||
}
|
||||
buf[static_cast<std::size_t>(cap) - 1] = '\0'; // defensive: guard against a read() that fills the buffer without honoring NUL-termination within cap
|
||||
std::string s(buf.data());
|
||||
if (static_cast<int>(s.size()) + 1 < cap) {
|
||||
result.status = GrowingExtStateRead::Status::Complete;
|
||||
result.value = std::move(s);
|
||||
return result;
|
||||
}
|
||||
// else: possibly truncated -> grow and retry.
|
||||
}
|
||||
result.status = GrowingExtStateRead::Status::Overflow;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace reasampler::wire
|
||||
Reference in New Issue
Block a user