Ξ-W2-T1: the resample bake chain — instrument renders, extension banks, one click re-points and resets
This commit is contained in:
@@ -79,6 +79,7 @@ This directory owns two cross-artifact contracts specifically:
|
||||
- `wire` (`core/wire`) — the ONE length-prefixed ext-state wire codec (Q-W1): `putField`/`parseUnsignedDecimal` + the bounds-checked `Cursor` (`field`/`fieldInt`/`fieldInt64`/`fieldSizeT`/`fieldDouble`), replacing four near-identical copies (`provenance` / `assignment_request` / `sample_usage` / `bank_sync`). `core/wire/bytes.h` is the sibling little-endian byte codec (`putLE`, `ByteReader`, `doubleToBits`/`bitsToDouble`) that `component_state_io` is the biggest consumer of. `core/wire/ext_state_read.h` owns the `GetProjExtState` grow-loop retry policy (Absent/Complete/Overflow) shared by `persist`, `usage_scan`, and `reaper_bridge`. `core/wire/reasampler_uid.h` (the FOREVER-FROZEN VST3 class-UID macros) also lives in this directory.
|
||||
- `reasampler_uid.h` — SDK-free header owning the FOREVER-FROZEN VST3 class-UID integer macros (stable + beta pairs, `REASAMPLER_PROC_UID_*` / `REASAMPLER_PROC_UID_BETA_*`) and the `REASAMPLER_ACTIVE_UID_*` channel-selector macros. Split out of `reasampler_vst.h` so the pure extension side (`instrument_drop`) can derive the `.vstpreset` class-ID hex string without pulling in the VST3 SDK. Both `reasampler_vst.h` (runtime `FUID`) and `instrument_drop` (preset hex string) source from this single header — the binary identity and the preset-file identity cannot diverge.
|
||||
- `assignment_request` — pure ingest-assign wire: typed request record carrying the drop payload from the `ingest` shell through to the VST3 bridge.
|
||||
- `bake_wire` — the resample bake's request/outcome pair on ONE per-instance key (`rsbake_<guid>`): the instrument writes a `BakeRequest`, invokes the extension's action synchronously, and reads the extension's `BakeOutcome` back over the same key inside that one call. Not a handshake — a call and a return, and it must not grow a claim protocol. Also the ONE home of the bake action's command-id suffix and of the leading underscore `NamedCommandLookup` needs but `rec->Register("command_id", …)` does not, so both artifacts name one action. `BakeStatus` values are WIRE INTEGERS: never renumber, only append, and an unrecognized value decodes as `Failed` rather than as the numeric default `Ok`.
|
||||
- `instrument_drop` — pure FX-drop payload builder: constructs a Steinberg-format `.vstpreset` image (channel-active class ID + the instrument's own component state, capture pre-selected) the shell applies via `TrackFX_SetPreset`; owns the `infoNamesFxHotspot` prefix classifier for `GetThingFromPoint` tokens. All-or-nothing contract — caller rolls back via `TrackFX_Delete` on any failure.
|
||||
- `sample_usage` — instance-usage wire: `UsageRecord`, `planUsagePublish` (fresh/heal/clean-replace/union/remint publish plan), `foldUsageRecords`/`usageHeldPaths` (liveness fold — protect-all when records exist but no instance is live; abort→protect-all on unreadable record; `counted` carries key-attributed live records), `identityMatches` (ReaSampler 9000 FX identity). REAPER-free, unit-tested. The mirror of `assignment_request` on the instrument→extension direction: the wire format and the two safety-critical decisions (what to write on publish, which records count at prune time) are pure so they are provable without a DAW. It lives here because it is a *wire format* with an instrument-side writer; the fold's output is consumed by `core/tracking`'s authority, which owns every consumer-facing decision built on it.
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@ reasampler_test(wire LINK wire)
|
||||
reasampler_pure_library(assignment_request SOURCES assignment_request.cpp LINK PRIVATE wire)
|
||||
reasampler_test(assignment_request LINK assignment_request)
|
||||
|
||||
# app_version is PUBLIC: the action's lookup name is channel-qualified, so a consumer that
|
||||
# resolves the action reads the same channel identity the extension registered under.
|
||||
reasampler_pure_library(bake_wire SOURCES bake_wire.cpp LINK PRIVATE wire PUBLIC app_version)
|
||||
reasampler_test(bake_wire LINK bake_wire)
|
||||
|
||||
reasampler_pure_library(sample_usage SOURCES sample_usage.cpp LINK PRIVATE wire)
|
||||
# prune_reconcile composes the protection proof at the pure layer: a capture held by a live
|
||||
# instance lands in the referenced union, so pruneOrphans can never emit it.
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// bake_wire.cpp — see bake_wire.h. Pure: standard library only.
|
||||
|
||||
#include "core/wire/bake_wire.h"
|
||||
|
||||
#include "core/version/app_version.h"
|
||||
#include "core/wire/wire.h"
|
||||
|
||||
namespace reasampler::wire {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kRequestMagic = "rsbakereq1";
|
||||
constexpr const char* kOutcomeMagic = "rsbakeout1";
|
||||
|
||||
using wire::putField;
|
||||
using Cursor = wire::Cursor;
|
||||
|
||||
// An unrecognized integer is `Failed`, not a parse error: the two artifacts ship
|
||||
// independently, and a newer extension naming a failure this build has no word for must
|
||||
// still read as a failure rather than as Ok (which is what the numeric default would be).
|
||||
BakeStatus statusFromWire(int raw) {
|
||||
switch (static_cast<BakeStatus>(raw)) {
|
||||
case BakeStatus::Ok:
|
||||
case BakeStatus::Failed:
|
||||
case BakeStatus::NoProject:
|
||||
case BakeStatus::StagedMissing:
|
||||
case BakeStatus::NoSource:
|
||||
case BakeStatus::IndexRejected:
|
||||
return static_cast<BakeStatus>(raw);
|
||||
}
|
||||
return BakeStatus::Failed;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string bakeActionLookupName() {
|
||||
return "_" + version::channelCommandId(kBakeActionSuffix);
|
||||
}
|
||||
|
||||
bool BakeRequest::operator==(const BakeRequest& o) const {
|
||||
return instanceGuid == o.instanceGuid && stagedFilePath == o.stagedFilePath &&
|
||||
sourceSampleId == o.sourceSampleId &&
|
||||
sourceRelativePath == o.sourceRelativePath &&
|
||||
sourceDisplayName == o.sourceDisplayName && ownUsageKey == o.ownUsageKey &&
|
||||
rootNote == o.rootNote && generation == o.generation;
|
||||
}
|
||||
|
||||
bool BakeOutcome::operator==(const BakeOutcome& o) const {
|
||||
return status == o.status && sampleId == o.sampleId &&
|
||||
relativePath == o.relativePath && displayName == o.displayName &&
|
||||
rootNote == o.rootNote && channelCount == o.channelCount &&
|
||||
replaced == o.replaced && message == o.message && generation == o.generation;
|
||||
}
|
||||
|
||||
std::string encodeBakeRequest(const BakeRequest& req) {
|
||||
std::string out = kRequestMagic;
|
||||
putField(out, req.instanceGuid);
|
||||
putField(out, req.stagedFilePath);
|
||||
putField(out, req.sourceSampleId);
|
||||
putField(out, req.sourceRelativePath);
|
||||
putField(out, req.sourceDisplayName);
|
||||
putField(out, req.ownUsageKey);
|
||||
putField(out, std::to_string(req.rootNote));
|
||||
putField(out, std::to_string(req.generation));
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<BakeRequest> decodeBakeRequest(const std::string& wire) {
|
||||
Cursor cur(wire);
|
||||
if (!cur.literal(kRequestMagic)) return std::nullopt;
|
||||
|
||||
BakeRequest req;
|
||||
if (!cur.field(req.instanceGuid)) return std::nullopt;
|
||||
if (!cur.field(req.stagedFilePath)) return std::nullopt;
|
||||
if (!cur.field(req.sourceSampleId)) return std::nullopt;
|
||||
if (!cur.field(req.sourceRelativePath)) return std::nullopt;
|
||||
if (!cur.field(req.sourceDisplayName)) return std::nullopt;
|
||||
if (!cur.field(req.ownUsageKey)) return std::nullopt;
|
||||
if (!cur.fieldInt(req.rootNote)) return std::nullopt;
|
||||
if (!cur.fieldInt64(req.generation)) return std::nullopt;
|
||||
|
||||
if (!cur.ok() || !cur.atEnd()) return std::nullopt;
|
||||
return req;
|
||||
}
|
||||
|
||||
std::string encodeBakeOutcome(const BakeOutcome& outcome) {
|
||||
std::string out = kOutcomeMagic;
|
||||
putField(out, std::to_string(static_cast<int>(outcome.status)));
|
||||
putField(out, outcome.sampleId);
|
||||
putField(out, outcome.relativePath);
|
||||
putField(out, outcome.displayName);
|
||||
putField(out, std::to_string(outcome.rootNote));
|
||||
putField(out, std::to_string(outcome.channelCount));
|
||||
putField(out, outcome.replaced ? "1" : "0");
|
||||
putField(out, outcome.message);
|
||||
putField(out, std::to_string(outcome.generation));
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<BakeOutcome> decodeBakeOutcome(const std::string& wire) {
|
||||
Cursor cur(wire);
|
||||
if (!cur.literal(kOutcomeMagic)) return std::nullopt;
|
||||
|
||||
BakeOutcome outcome;
|
||||
int rawStatus = 0;
|
||||
std::string replaced;
|
||||
if (!cur.fieldInt(rawStatus)) return std::nullopt;
|
||||
if (!cur.field(outcome.sampleId)) return std::nullopt;
|
||||
if (!cur.field(outcome.relativePath)) return std::nullopt;
|
||||
if (!cur.field(outcome.displayName)) return std::nullopt;
|
||||
if (!cur.fieldInt(outcome.rootNote)) return std::nullopt;
|
||||
if (!cur.fieldInt(outcome.channelCount)) return std::nullopt;
|
||||
if (!cur.field(replaced)) return std::nullopt;
|
||||
if (!cur.field(outcome.message)) return std::nullopt;
|
||||
if (!cur.fieldInt64(outcome.generation)) return std::nullopt;
|
||||
|
||||
if (!cur.ok() || !cur.atEnd()) return std::nullopt;
|
||||
if (replaced != "0" && replaced != "1") return std::nullopt;
|
||||
outcome.status = statusFromWire(rawStatus);
|
||||
outcome.replaced = (replaced == "1");
|
||||
return outcome;
|
||||
}
|
||||
|
||||
} // namespace reasampler::wire
|
||||
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
// bake_wire — the resample bake's request/outcome pair, on ONE per-instance ext-state key
|
||||
// ("rsbake_<instanceGuid>"). Pure: no REAPER/VST3/SWELL/vendor includes.
|
||||
//
|
||||
// The instrument writes a BakeRequest, invokes the extension's bake action synchronously,
|
||||
// and reads the BakeOutcome the action wrote back over the same key. That is a call and a
|
||||
// return in one UI tick — deliberately NOT the poller/nonce handshake this seam once
|
||||
// spiked; nothing here may grow a claim protocol.
|
||||
//
|
||||
// `generation` exists for the same reason it does on assignment_request: a repeat bake of
|
||||
// the same source is otherwise indistinguishable from a stale value, and an outcome whose
|
||||
// generation does not echo the request is a leftover, not an answer.
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace reasampler::wire {
|
||||
|
||||
// The extension action the instrument invokes, as its command-id suffix. FOREVER-STABLE
|
||||
// per channel like every other registered suffix. Both artifacts read this one symbol.
|
||||
inline constexpr const char* kBakeActionSuffix = "RESAMPLE_BAKE";
|
||||
|
||||
// The NamedCommandLookup spelling of that action: a LEADING UNDERSCORE the
|
||||
// rec->Register("command_id", …) string itself does not carry. The one place that
|
||||
// underscore is written.
|
||||
std::string bakeActionLookupName();
|
||||
|
||||
// What the instrument asks for. Every path is the instrument's own knowledge: it staged
|
||||
// the file, it knows which capture it was resampling, and it knows its own usage key.
|
||||
struct BakeRequest {
|
||||
std::string instanceGuid; // the asking instance ("rsbake_<guid>" names the key)
|
||||
std::string stagedFilePath; // ABSOLUTE, outside the bank folder; the instrument deletes it
|
||||
std::string sourceSampleId; // the capture being resampled (lineage parent)
|
||||
std::string sourceRelativePath; // its project-relative path — the tie query's subject
|
||||
std::string sourceDisplayName; // what the new entry's name is derived from
|
||||
std::string ownUsageKey; // "rsusage_<guid>" — excluded from the tie scan
|
||||
int rootNote = 60;
|
||||
std::int64_t generation = 0;
|
||||
|
||||
bool operator==(const BakeRequest& o) const;
|
||||
bool operator!=(const BakeRequest& o) const { return !(*this == o); }
|
||||
};
|
||||
|
||||
// Why a bake did not land. Values are WIRE INTEGERS — never renumber, only append; an
|
||||
// unrecognized value decodes as `Failed` so a newer extension's vocabulary cannot make an
|
||||
// older instrument read a failure as a success.
|
||||
enum class BakeStatus : int {
|
||||
Ok = 0,
|
||||
Failed = 1, // generic / unrecognized
|
||||
NoProject = 2, // unsaved project: the bank has no location
|
||||
StagedMissing = 3, // the staged file was gone or unreadable
|
||||
NoSource = 4, // the source capture is not in any bank
|
||||
IndexRejected = 5, // the bank refused the add
|
||||
};
|
||||
|
||||
// What the extension did. On Ok the four sample fields describe the landed entry, so the
|
||||
// instance can adopt it without a bank read — the same self-contained discipline the
|
||||
// SampleRefs table exists for.
|
||||
struct BakeOutcome {
|
||||
BakeStatus status = BakeStatus::Failed;
|
||||
std::string sampleId;
|
||||
std::string relativePath;
|
||||
std::string displayName;
|
||||
int rootNote = 60;
|
||||
int channelCount = 0;
|
||||
bool replaced = false; // false = a distinct entry was added
|
||||
std::string message; // human-readable, for the console
|
||||
std::int64_t generation = 0; // echoes the request's
|
||||
|
||||
bool operator==(const BakeOutcome& o) const;
|
||||
bool operator!=(const BakeOutcome& o) const { return !(*this == o); }
|
||||
};
|
||||
|
||||
// Length-prefixed fields behind a magic+version tag, the house idiom, so arbitrary bytes
|
||||
// in a path or a name round-trip whole.
|
||||
std::string encodeBakeRequest(const BakeRequest& req);
|
||||
std::string encodeBakeOutcome(const BakeOutcome& outcome);
|
||||
|
||||
// std::nullopt on malformed/truncated/trailing-garbage input, and on a tag this build does
|
||||
// not read — a future version's record is refused rather than half-parsed. Round-trips.
|
||||
std::optional<BakeRequest> decodeBakeRequest(const std::string& wire);
|
||||
std::optional<BakeOutcome> decodeBakeOutcome(const std::string& wire);
|
||||
|
||||
} // namespace reasampler::wire
|
||||
Reference in New Issue
Block a user