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:
+99
-31
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user