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
+147 -18
View File
@@ -9,13 +9,15 @@
// and the selection / downmix / keymap / state values are checked against independently
// computed expectations.
//
// Covers: selectSample by-id hit (across pool + named banks), first-sample fallback for
// an empty / unknown id, empty & malformed blob -> nullopt, zero-samples -> nullopt,
// rootNote/loop intrinsic threading incl. the middle-C default; listSamples ordinal
// order + empty/malformed; downmixToMono mono passthrough / stereo average / 3-ch
// average / zero-stride / empty; buildTier0Keymap single full-keyboard zone with the
// root + loop + rate threaded and rate defaulting; selection state round-trip + empty id
// + wrong-version / truncated -> "".
// Covers: selectSample by-id hit (across pool + named banks), the S10 policy reversal
// (empty / stale id -> SILENCE nullopt, not the first sample), empty & malformed blob ->
// nullopt, zero-samples -> nullopt, rootNote/loop intrinsic threading incl. the middle-C
// default; listSamples ordinal order + the card metadata (rootNote/key/bankId) + empty/
// malformed; listBanks ordinal order (pool first) + empty/malformed; downmixToMono mono
// passthrough / stereo average / 3-ch average / zero-stride / empty; buildTier0Keymap single
// full-keyboard zone with the root + loop + rate threaded and rate defaulting; selection
// state round-trip + empty id + wrong-version / truncated -> ""; component state (v3)
// round-trip + v1/v2 back-compat lift + empty/unknown -> empty.
// wav_trim -> extractFloatFrames -> downmixToMono integration: locks the interleave-
// stride contract across the seam (that the byte stride wav_trim reports matches the
// channel-count stride downmixToMono divides by).
@@ -78,24 +80,25 @@ static void testSelectByIdHit() {
CHECK(sel && sel->rootNote == 38);
}
static void testSelectFirstSampleFallbackOnEmptyId() {
static void testSelectEmptyIdIsSilence() {
const std::string json = bookJson(
{makeSample("a", "Kick", "reasampler_bank/a.wav", 36)},
{makeSample("b", "Snare", "reasampler_bank/b.wav", 38)});
// No stored selection -> the FIRST sample in ordinal order (pool first).
// POLICY REVERSAL (S10): no stored selection resolves to SILENCE (nullopt), NOT the
// bank's first sample. A fresh instance plays nothing and shows the "pick a capture"
// empty state — the deliberate reversal of the S4 first-sample auto-play.
auto sel = selectSample(json, "");
CHECK(sel.has_value());
CHECK(sel && sel->relativePath == "reasampler_bank/a.wav");
CHECK(sel && sel->rootNote == 36);
CHECK(!sel.has_value());
}
static void testSelectFirstSampleFallbackOnUnknownId() {
static void testSelectUnknownIdIsSilence() {
const std::string json = bookJson(
{makeSample("a", "Kick", "reasampler_bank/a.wav", 36)}, {});
// A stored id that no longer resolves falls back to the first sample, not silence.
// A stale stored id (deleted/moved-out sample) resolves to SILENCE, not a substituted
// first sample — the editor reflects the missing pick with its empty state rather than
// masking it with a mystery sample.
auto sel = selectSample(json, "deleted-id");
CHECK(sel.has_value());
CHECK(sel && sel->relativePath == "reasampler_bank/a.wav");
CHECK(!sel.has_value());
}
static void testSelectRootNoteDefault() {
@@ -155,12 +158,53 @@ static void testListSamplesOrdinalOrder() {
CHECK(list.size() == 3 && list[2].id == "b" && list[2].displayName == "Snare");
}
static void testListSamplesCarriesCardMetadata() {
// The browser card needs rootNote/key badge + the bank id (for the filter). A pool sample
// reports the pool bank id; a named-bank sample reports "drums-id"; an un-rooted sample
// reports no rootNote (the badge shows "root —", never a guessed value).
Sample rooted = makeSample("a", "Kick", "reasampler_bank/a.wav", 36);
rooted.key = "Cm";
Sample unrooted = makeSample("u", "Loop", "reasampler_bank/u.wav", std::nullopt);
const std::string json = bookJson({rooted, unrooted},
{makeSample("b", "Snare", "reasampler_bank/b.wav", 38)});
const std::vector<SampleChoice> list = listSamples(json);
CHECK(list.size() == 3);
// Pool sample "a": rooted + keyed, pool bank id.
CHECK(list[0].id == "a" && list[0].rootNote.has_value() && *list[0].rootNote == 36);
CHECK(list[0].key.has_value() && *list[0].key == "Cm");
CHECK(!list[0].bankId.empty()); // the pool has an id; the filter matches on it
// Pool sample "u": no root intrinsic -> no rootNote (badge shows "root —").
CHECK(list[1].id == "u" && !list[1].rootNote.has_value());
// Named-bank sample "b": its bank id distinguishes it from the pool for the filter.
CHECK(list[2].id == "b" && list[2].bankId == "drums-id");
CHECK(list[2].bankId != list[0].bankId); // pool vs. named bank differ (filterable apart)
}
static void testListSamplesEmptyAndMalformed() {
CHECK(listSamples("").empty());
CHECK(listSamples("{garbage").empty());
CHECK(listSamples(bookJson({}, {})).empty());
}
static void testListBanksOrdinalOrder() {
const std::string json = bookJson(
{makeSample("a", "Kick", "reasampler_bank/a.wav", 36)},
{makeSample("b", "Snare", "reasampler_bank/b.wav", 38)});
const std::vector<BankChoice> banks = listBanks(json);
// Pool first (bank-zero), then the named bank "Drums". Both ids are present so the filter
// tab strip can key on them.
CHECK(banks.size() == 2);
CHECK(banks.size() == 2 && banks[1].id == "drums-id" && banks[1].displayName == "Drums");
CHECK(banks.size() == 2 && !banks[0].id.empty()); // the pool bank has an id too
}
static void testListBanksEmptyAndMalformed() {
CHECK(listBanks("").empty());
CHECK(listBanks("{garbage").empty());
// A valid book with no samples still has the pool bank -> one entry.
CHECK(listBanks(bookJson({}, {})).size() == 1);
}
// --- downmixToMono ------------------------------------------------------------
static bool approx(double a, double b) { return std::fabs(a - b) < 1e-6; }
@@ -558,10 +602,86 @@ static void testPerformanceStateNegativeNotesRoundTrip() {
*back.zones[0].rootOverride == 0);
}
// --- Combined component state (v3, S10) --------------------------------------
static void testComponentStateRoundTrip() {
// The v3 state carries the single-capture selection AND the opt-in zones, distinctly.
ComponentState s;
s.selectionId = "picked-capture";
s.map.zones.push_back(zone("z0", 0, 59, /*override=*/std::nullopt));
s.map.zones.push_back(zone("z1", 60, 127, /*override=*/48));
const ComponentState back = deserializeComponentState(serializeComponentState(s));
CHECK(back.selectionId == "picked-capture");
CHECK(back.map.zones.size() == 2);
CHECK(back.map.zones.size() == 2 && back.map.zones[0].sampleId == "z0" &&
back.map.zones[0].highNote == 59 && !back.map.zones[0].rootOverride.has_value());
CHECK(back.map.zones.size() == 2 && back.map.zones[1].sampleId == "z1" &&
back.map.zones[1].rootOverride.has_value() && *back.map.zones[1].rootOverride == 48);
}
static void testComponentStateSelectionOnlyNoZones() {
// A single-capture instance: a pick, no zones. Must restore the pick with an empty map
// (NOT synthesize a zone) — the default face is one capture, zones are opt-in.
ComponentState s;
s.selectionId = "just-a-pick";
const ComponentState back = deserializeComponentState(serializeComponentState(s));
CHECK(back.selectionId == "just-a-pick");
CHECK(back.map.zones.empty());
}
static void testComponentStateEmptyIsEmpty() {
// No pick, no zones -> restores EMPTY (the S10 silent empty state), never a first sample.
const ComponentState s; // selectionId "", empty map
const ComponentState back = deserializeComponentState(serializeComponentState(s));
CHECK(back.selectionId.empty());
CHECK(back.map.zones.empty());
}
static void testComponentStateV1BackCompat() {
// A v1 S4 blob (single-selection) lifts to {id, one full-keyboard zone} so an old pick
// survives as BOTH the selection and a one-zone map.
const std::vector<std::uint8_t> v1 = serializeSelection("legacy-id");
const ComponentState back = deserializeComponentState(v1);
CHECK(back.selectionId == "legacy-id");
CHECK(back.map.zones.size() == 1);
CHECK(back.map.zones.size() == 1 && back.map.zones[0].sampleId == "legacy-id" &&
back.map.zones[0].lowNote == 0 && back.map.zones[0].highNote == 127);
// A v1 blob with an EMPTY id -> empty state (no selection, no zone).
const ComponentState empty = deserializeComponentState(serializeSelection(""));
CHECK(empty.selectionId.empty() && empty.map.zones.empty());
}
static void testComponentStateV2BackCompat() {
// A v2 S5 blob (zones-only) lifts to {"", zones}: that instance had zones but no separate
// single-capture selection.
PerformanceMap m;
m.zones.push_back(zone("s", 12, 24, /*override=*/std::nullopt));
const std::vector<std::uint8_t> v2 = serializePerformance(m);
const ComponentState back = deserializeComponentState(v2);
CHECK(back.selectionId.empty());
CHECK(back.map.zones.size() == 1 && back.map.zones[0].sampleId == "s" &&
back.map.zones[0].lowNote == 12 && back.map.zones[0].highNote == 24);
}
static void testComponentStateGarbage() {
// Empty / unknown version -> empty (never throws across the host).
CHECK(deserializeComponentState({}).selectionId.empty());
CHECK(deserializeComponentState({}).map.zones.empty());
const std::vector<std::uint8_t> unknown{0xAA, 0xBB, 0xCC, 0xDD};
CHECK(deserializeComponentState(unknown).map.zones.empty());
CHECK(deserializeComponentState(unknown).selectionId.empty());
// A v3 header claiming a longer id than the blob holds -> empty (bounded read).
std::vector<std::uint8_t> t;
t.push_back(3); t.push_back(0); t.push_back(0); t.push_back(0); // version 3
t.push_back(200); t.push_back(0); t.push_back(0); t.push_back(0); // id length 200 (absent)
CHECK(deserializeComponentState(t).selectionId.empty());
CHECK(deserializeComponentState(t).map.zones.empty());
}
int main() {
testSelectByIdHit();
testSelectFirstSampleFallbackOnEmptyId();
testSelectFirstSampleFallbackOnUnknownId();
testSelectEmptyIdIsSilence();
testSelectUnknownIdIsSilence();
testSelectRootNoteDefault();
testSelectLoopThreaded();
testSelectNoLoopIsAbsent();
@@ -569,7 +689,10 @@ int main() {
testSelectMalformedBlob();
testSelectZeroSamples();
testListSamplesOrdinalOrder();
testListSamplesCarriesCardMetadata();
testListSamplesEmptyAndMalformed();
testListBanksOrdinalOrder();
testListBanksEmptyAndMalformed();
testDownmixMonoPassthrough();
testDownmixStereoAverages();
testDownmixThreeChannelAverages();
@@ -597,6 +720,12 @@ int main() {
testPerformanceStateV1BackCompat();
testPerformanceStateGarbage();
testPerformanceStateNegativeNotesRoundTrip();
testComponentStateRoundTrip();
testComponentStateSelectionOnlyNoZones();
testComponentStateEmptyIsEmpty();
testComponentStateV1BackCompat();
testComponentStateV2BackCompat();
testComponentStateGarbage();
if (g_fail == 0) std::printf("sample_map: all tests passed\n");
return g_fail != 0;