S10: capture-first ReaSampler 9000 editor — browser, single-capture setup, zones toggle

Reverse S4 auto-select (empty=silence+empty state), add peak-thumbnail capture
browser + bank filter + keyboard-strip drag machine, v3 component state (selection
+ zones), and the S-NAME-1 filename/display rename (UID locked). MSVC min/max
macro collisions fixed post-implementation; 26/26 tests green.
This commit is contained in:
2026-07-26 20:36:36 -04:00
parent 991c190bb8
commit 7151e19432
16 changed files with 1906 additions and 325 deletions
+99 -31
View File
@@ -39,28 +39,23 @@ SelectedSample distill(const Sample& s) {
std::optional<SelectedSample> selectSample(const std::string& banksJson,
const std::string& sampleId) {
// POLICY REVERSAL (S10): an empty selection is SILENCE, not the first sample. Short-
// circuit before parsing — no stored id resolves to nothing to play by design.
if (sampleId.empty()) return std::nullopt;
if (banksJson.empty()) return std::nullopt;
std::optional<BankBook> book = BankBook::deserialize(banksJson);
if (!book) return std::nullopt; // malformed -> nothing to play (never throw)
// Search every bank (pool first, then named — banks() is ordinal order) for the
// stored id. A sample lives in exactly one bank, so first hit wins.
if (!sampleId.empty()) {
for (const Bank& b : book->banks()) {
if (const Sample* s = b.index.query(sampleId)) {
return distill(*s);
}
}
}
// No stored id, or the id no longer resolves (the sample was deleted/moved out):
// fall back to the FIRST sample in ordinal order so a fresh instance plays.
for (const Bank& b : book->banks()) {
if (!b.index.all().empty()) {
return distill(b.index.all().front());
if (const Sample* s = b.index.query(sampleId)) {
return distill(*s);
}
}
return std::nullopt; // bank has zero samples anywhere
// A stale stored id (no longer resolves) is SILENCE, not a substituted first sample:
// the editor reflects the missing pick with its empty state rather than masking it.
return std::nullopt;
}
std::vector<SampleChoice> listSamples(const std::string& banksJson) {
@@ -70,12 +65,23 @@ std::vector<SampleChoice> listSamples(const std::string& banksJson) {
if (!book) return out;
for (const Bank& b : book->banks()) {
for (const Sample& s : b.index.all()) {
out.push_back(SampleChoice{s.id, s.displayName});
out.push_back(SampleChoice{s.id, s.displayName, s.rootNote, s.key, b.id});
}
}
return out;
}
std::vector<BankChoice> listBanks(const std::string& banksJson) {
std::vector<BankChoice> out;
if (banksJson.empty()) return out;
std::optional<BankBook> book = BankBook::deserialize(banksJson);
if (!book) return out;
for (const Bank& b : book->banks()) {
out.push_back(BankChoice{b.id, b.displayName});
}
return out;
}
std::vector<AudioSample> downmixToMono(const std::vector<AudioSample>& interleaved,
int channelCount) {
std::vector<AudioSample> out;
@@ -211,11 +217,9 @@ struct ByteReader {
int i32() { return static_cast<int>(static_cast<std::int32_t>(u32())); }
};
} // namespace
std::vector<std::uint8_t> serializePerformance(const PerformanceMap& map) {
std::vector<std::uint8_t> out;
putU32le(out, kPerformanceStateVersion);
// Append the zones payload (zone count + per-zone records) — the shared body of the v2
// performance blob and the v3 component blob, so both write zones identically.
void putZonesPayload(std::vector<std::uint8_t>& out, const PerformanceMap& map) {
putU32le(out, static_cast<std::uint32_t>(map.zones.size()));
for (const PerformanceZone& z : map.zones) {
putU32le(out, static_cast<std::uint32_t>(z.sampleId.size()));
@@ -228,6 +232,33 @@ std::vector<std::uint8_t> serializePerformance(const PerformanceMap& map) {
static_cast<std::uint32_t>(static_cast<std::int32_t>(*z.rootOverride)));
}
}
}
// Read a zones payload (zone count + per-zone records) from `r` into `map`. Shared by the
// v2 performance parse and the v3 component parse. A truncated mid-zone read keeps the zones
// that parsed cleanly and drops the rest; the reader position is left after the last byte
// read successfully.
void readZonesPayload(ByteReader& r, PerformanceMap& map) {
const std::uint32_t count = r.u32();
for (std::uint32_t i = 0; i < count && r.ok; ++i) {
PerformanceZone z;
const std::uint32_t idLen = r.u32();
z.sampleId = r.str(idLen);
z.lowNote = r.i32();
z.highNote = r.i32();
const std::uint8_t hasOverride = r.u8();
if (hasOverride) z.rootOverride = r.i32();
if (!r.ok) break; // truncated mid-zone -> keep what parsed cleanly, drop the rest
map.zones.push_back(std::move(z));
}
}
} // namespace
std::vector<std::uint8_t> serializePerformance(const PerformanceMap& map) {
std::vector<std::uint8_t> out;
putU32le(out, kPerformanceStateVersion);
putZonesPayload(out, map);
return out;
}
@@ -252,21 +283,58 @@ PerformanceMap deserializePerformance(const std::vector<std::uint8_t>& bytes) {
}
if (version != kPerformanceStateVersion) return map; // unknown -> empty
const std::uint32_t count = r.u32();
for (std::uint32_t i = 0; i < count && r.ok; ++i) {
PerformanceZone z;
const std::uint32_t idLen = r.u32();
z.sampleId = r.str(idLen);
z.lowNote = r.i32();
z.highNote = r.i32();
const std::uint8_t hasOverride = r.u8();
if (hasOverride) z.rootOverride = r.i32();
if (!r.ok) break; // truncated mid-zone -> keep what parsed cleanly, drop the rest
map.zones.push_back(std::move(z));
}
readZonesPayload(r, map);
return map;
}
// --- Combined component state (v3, S10) --------------------------------------
std::vector<std::uint8_t> serializeComponentState(const ComponentState& state) {
std::vector<std::uint8_t> out;
putU32le(out, kComponentStateVersion);
// Length-prefixed selection id (it precedes the zones payload, so it MUST be framed —
// unlike the v1 selection blob where the id ran to end-of-stream).
putU32le(out, static_cast<std::uint32_t>(state.selectionId.size()));
out.insert(out.end(), state.selectionId.begin(), state.selectionId.end());
putZonesPayload(out, state.map);
return out;
}
ComponentState deserializeComponentState(const std::vector<std::uint8_t>& bytes) {
ComponentState out;
ByteReader r(bytes);
const std::uint32_t version = r.u32();
if (!r.ok) return out; // no version tag -> empty (the S10 silent empty state)
// BACK-COMPAT: an older blob predates the v3 {selection, zones} split.
// * v1 (S4 single-selection: version 1 + id-to-end): restore {id, one full-keyboard
// zone} so the old pick survives as BOTH the selection and a one-zone map.
// * v2 (S5 zones-only): restore {"", zones} — that instance had zones but no separate
// single-capture selection.
if (version == kSelectionStateVersion) {
out.selectionId = deserializeSelection(bytes);
if (!out.selectionId.empty()) {
PerformanceZone z;
z.sampleId = out.selectionId;
z.lowNote = 0;
z.highNote = 127;
out.map.zones.push_back(std::move(z));
}
return out;
}
if (version == kPerformanceStateVersion) {
readZonesPayload(r, out.map); // v2 body starts right after the version tag
return out;
}
if (version != kComponentStateVersion) return out; // unknown -> empty
const std::uint32_t idLen = r.u32();
out.selectionId = r.str(idLen);
if (!r.ok) { out.selectionId.clear(); return out; } // truncated id -> empty
readZonesPayload(r, out.map);
return out;
}
std::vector<std::uint8_t> serializeSelection(const std::string& sampleId) {
std::vector<std::uint8_t> out;
out.resize(4 + sampleId.size());