feat(S8): ingest through the bank — capture/import/drop into bank + assign to active instance

This commit is contained in:
2026-07-26 21:29:13 -04:00
parent bf1bf5283c
commit 8074e21057
11 changed files with 1009 additions and 6 deletions
+80
View File
@@ -0,0 +1,80 @@
#pragma once
// assignment_request — the pure core of the S8 ingest assignment-request seam.
//
// PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO VST3,
// NO vendor/ includes. Standard library only. Unit-tested outside the DAW — the same
// "small pure type + length-prefixed round-trip" pattern as provenance / owned_manifest.
//
// -- What it is --------------------------------------------------------------
//
// When the EXTENSION ingests a sample (S8: arrange capture / Media-Explorer import /
// drop-onto-panel) it writes an ASSIGNMENT REQUEST to its own "reasampler" ext-state
// namespace: "the active sampler instance should now play THIS sample." The value
// names the ingested sample by (bankId, sampleId) plus a monotonic `generation` the
// reader compares to decide the request is NEW (a fresh ingest, even of the same id).
//
// This module owns ONLY the value's WIRE FORMAT — build/parse round-trip. Writing it
// to ext-state is the persist shell's job; READING it is the instrument's job in a
// LATER dispatch (S8 instrument-side follow-up, after S10 merges). This is why the
// format is documented here in the header, not just in code: the reader lands elsewhere
// and must decode exactly what this writer produced.
//
// -- The data-ownership boundary (load-bearing) ------------------------------
//
// The EXTENSION writes this; the instrument only READS it. That does not violate the
// instrument's read-only-over-the-bank rule: the assignment request is the extension
// writing its OWN namespace (a request FROM the extension TO the instrument), never the
// instrument writing back into the bank. The instrument, on reading a new generation,
// updates its OWN component-state selection (the same selection S4 persists) and reloads.
//
// -- Why `generation` -------------------------------------------------------
//
// Instances reference sample IDs, so re-assigning the SAME id (e.g. a recapture, or a
// re-drop of the same file) would be indistinguishable from a stale value without a
// changing field. `generation` is a monotonic disambiguator (the ingest writer supplies
// a wall-clock unix-epoch stamp today — see the writer shell) so the reader can tell
// "assigned again just now" from "already saw this." It is DELIBERATELY the same shape
// the S9 bank-generation counter will use, but it is NOT that counter — S9 is a separate
// point; this field is self-contained to the request and does not depend on S9 landing.
#include <cstdint>
#include <optional>
#include <string>
namespace reasampler {
// One assignment request: the ingested sample's identity + a monotonic disambiguator.
// bankId — the bank the sample was ingested into (the active/target bank).
// sampleId — the ingested Sample's stable id (BankIndex key).
// generation — a monotonic value the reader compares to detect a NEW request. The
// writer supplies a unix-epoch-seconds stamp; the reader treats it as an
// opaque "did this change?" token, not a wall-clock it interprets.
struct AssignmentRequest {
std::string bankId;
std::string sampleId;
std::int64_t generation = 0;
bool operator==(const AssignmentRequest& o) const {
return bankId == o.bankId && sampleId == o.sampleId &&
generation == o.generation;
}
bool operator!=(const AssignmentRequest& o) const { return !(*this == o); }
};
// Encode an assignment request to the wire string. Length-prefixed fields behind a
// magic+version tag ("rsassign1"), so arbitrary bytes in an id (a GUID, a display-
// derived id) round-trip whole with no escaping ambiguity — the same idiom provenance
// uses. Deterministic: the same request always yields the same string.
//
// FORMAT (documented for the LATER instrument-side reader):
// "rsassign1" <len>':'<bankId> <len>':'<sampleId> <len>':'<generation-decimal>
// where each <len> is the decimal byte length of the field that follows the ':'.
std::string encodeAssignmentRequest(const AssignmentRequest& req);
// Parse a wire string produced by encodeAssignmentRequest. std::nullopt on any
// malformed / truncated / trailing-garbage input (never UB, never a partial value) —
// the reader shell treats absence/malformed as "no pending request." Round-trips:
// decodeAssignmentRequest(encodeAssignmentRequest(x)) == x.
std::optional<AssignmentRequest> decodeAssignmentRequest(const std::string& wire);
} // namespace reasampler