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:
+147
-18
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user