373948c18a
Non-WAV sources decode via PCM_source::GetSamples and land as canonical 32f RIFF/WAVE; hash taken post-conversion so re-imports dedup. Undo block covers bank mutation + assign_request atomically. Overflow guards + adversarial tests.
90 lines
5.0 KiB
C++
90 lines
5.0 KiB
C++
#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.
|
|
//
|
|
// READER REQUIREMENT (instrument-side, S8 follow-up dispatch): after successfully
|
|
// decoding a request, the reader MUST verify that (bankId, sampleId) resolves to an
|
|
// existing sample before acting on it. An undo on the extension side rolls back the
|
|
// `banks` ext-state key (removing the sample) but cannot atomically clear the
|
|
// `assign_request` key if the write happened outside the undo block. Even with the
|
|
// undo-grouping fix (Major 2), the reader must guard against this: treat an
|
|
// unresolvable (bankId, sampleId) pair as a stale/no-op request and discard it
|
|
// silently, never crashing or selecting a nonexistent entry.
|
|
std::optional<AssignmentRequest> decodeAssignmentRequest(const std::string& wire);
|
|
|
|
} // namespace reasampler
|