pS remediation: legacy lift terminates on stale-id proof; displayName in v10 refs; refs-reader range fallbacks; load path keeps owned refs
This commit is contained in:
@@ -2124,7 +2124,8 @@ static void testReconcileBrowseSequenceNoShadowing() {
|
||||
|
||||
static SampleRefEntry refEntry(const std::string& id, const std::string& rel, int root,
|
||||
bool hasLoop = false, std::int64_t loopStart = 0,
|
||||
std::int64_t loopEnd = 0, int channels = 0) {
|
||||
std::int64_t loopEnd = 0, int channels = 0,
|
||||
const std::string& name = "") {
|
||||
SampleRefEntry e;
|
||||
e.sampleId = id;
|
||||
e.ref.relativePath = rel;
|
||||
@@ -2133,6 +2134,7 @@ static SampleRefEntry refEntry(const std::string& id, const std::string& rel, in
|
||||
e.ref.loop.start = loopStart;
|
||||
e.ref.loop.end = loopEnd;
|
||||
e.ref.channelCount = channels;
|
||||
e.displayName = name;
|
||||
return e;
|
||||
}
|
||||
|
||||
@@ -2145,7 +2147,8 @@ static void testSampleRefsRoundTrip() {
|
||||
s.channelModeExplicit = true;
|
||||
s.masterGainLinear = 0.5;
|
||||
s.sampleRefs.push_back(refEntry("kick", "reasampler_bank/kick.wav", 36,
|
||||
/*hasLoop=*/true, 100, 500, /*channels=*/2));
|
||||
/*hasLoop=*/true, 100, 500, /*channels=*/2,
|
||||
/*name=*/"Kick Drum"));
|
||||
s.sampleRefs.push_back(refEntry("pad", "reasampler_bank/pad.wav", 60,
|
||||
/*hasLoop=*/false, 0, 0, /*channels=*/1));
|
||||
s.map.zones.push_back(zone("pad", 48, 72));
|
||||
@@ -2158,9 +2161,11 @@ static void testSampleRefsRoundTrip() {
|
||||
CHECK(back.sampleRefs.size() == 2 && back.sampleRefs[0].ref.loop.hasLoop &&
|
||||
back.sampleRefs[0].ref.loop.start == 100 && back.sampleRefs[0].ref.loop.end == 500);
|
||||
CHECK(back.sampleRefs.size() == 2 && back.sampleRefs[0].ref.channelCount == 2);
|
||||
CHECK(back.sampleRefs.size() == 2 && back.sampleRefs[0].displayName == "Kick Drum");
|
||||
CHECK(back.sampleRefs.size() == 2 && back.sampleRefs[1].sampleId == "pad");
|
||||
CHECK(back.sampleRefs.size() == 2 && !back.sampleRefs[1].ref.loop.hasLoop);
|
||||
CHECK(back.sampleRefs.size() == 2 && back.sampleRefs[1].ref.channelCount == 1);
|
||||
CHECK(back.sampleRefs.size() == 2 && back.sampleRefs[1].displayName.empty());
|
||||
// Envelope neighbours undisturbed (the refs read consumed exactly its own bytes).
|
||||
CHECK(back.selectionId == "kick");
|
||||
CHECK(back.map.zones.size() == 1);
|
||||
@@ -2296,13 +2301,16 @@ static void testRefreshRefsFromBankUpsertAndOwnership() {
|
||||
CHECK(refs.size() == 1 && refs[0].sampleId == "a" && refs[0].ref.rootNote == 36);
|
||||
CHECK(refs.size() == 1 && refs[0].ref.relativePath == "b/a.wav");
|
||||
CHECK(refs.size() == 1 && refs[0].ref.channelCount == 2);
|
||||
// Recapture-style bank edit: path + root changed -> the owned copy refreshes.
|
||||
refreshRefsFromBank(refs, bookJson({makeSample("a", "Kick", "b/a2.wav", 40)}, {}), {"a"});
|
||||
CHECK(refs.size() == 1 && refs[0].displayName == "Kick"); // name copied with the ref
|
||||
// Recapture-style bank edit: path + root + name changed -> the owned copy refreshes.
|
||||
refreshRefsFromBank(refs, bookJson({makeSample("a", "Kick 2", "b/a2.wav", 40)}, {}), {"a"});
|
||||
CHECK(refs.size() == 1 && refs[0].ref.rootNote == 40);
|
||||
CHECK(refs.size() == 1 && refs[0].ref.relativePath == "b/a2.wav");
|
||||
CHECK(refs.size() == 1 && refs[0].displayName == "Kick 2"); // rename sync
|
||||
// Bank deletion: the id no longer resolves -> the OWNED copy survives untouched.
|
||||
refreshRefsFromBank(refs, bookJson({makeSample("x", "Other", "b/x.wav", 60)}, {}), {"a"});
|
||||
CHECK(refs.size() == 1 && refs[0].ref.rootNote == 40);
|
||||
CHECK(refs.size() == 1 && refs[0].displayName == "Kick 2");
|
||||
// Malformed / empty blobs: no-op.
|
||||
refreshRefsFromBank(refs, "{garbage", {"a"});
|
||||
refreshRefsFromBank(refs, "", {"a"});
|
||||
@@ -2333,8 +2341,9 @@ static void testSampleRefsTruncatedMidEntry() {
|
||||
s.sampleRefs.push_back(refEntry("pad", "b/p.wav", 60));
|
||||
std::vector<std::uint8_t> bytes = serializeComponentState(s);
|
||||
// The tail after the refs table is idLen(4) + "kick"(4) + the empty-map zones payload
|
||||
// (marker 4 + version 4 + count 4) = 20 bytes; entry 2 is 43 bytes (4+3 id, 4+7 path,
|
||||
// 4 root, 1+8+8 loop, 4 channels). Cutting 40 bytes lands 20 bytes into entry 2.
|
||||
// (marker 4 + version 4 + count 4) = 20 bytes; entry 2 is 47 bytes (4+3 id, 4+7 path,
|
||||
// 4 root, 1+8+8 loop, 4 channels, 4+0 name). Cutting 40 bytes lands 27 bytes into
|
||||
// entry 2 (inside loop.start).
|
||||
CHECK(bytes.size() > 40);
|
||||
bytes.resize(bytes.size() - 40);
|
||||
const ComponentState back = deserializeComponentState(bytes, 44100.0);
|
||||
@@ -2344,6 +2353,47 @@ static void testSampleRefsTruncatedMidEntry() {
|
||||
CHECK(back.map.zones.empty());
|
||||
}
|
||||
|
||||
static void testSampleRefsReaderRangeFallbacks() {
|
||||
// Corrupt-blob posture for the refs intrinsics (the refs table is the ONLY copy on the
|
||||
// play path, so a bad field must degrade to its default, never poison playback): an
|
||||
// out-of-MIDI-range rootNote falls back to the middle-C default distill() uses; a
|
||||
// negative channelCount falls back to 0 = unknown (the GA auto-default then skips it).
|
||||
// The fallback is per-field — in-range neighbours pass through untouched.
|
||||
ComponentState s;
|
||||
s.sampleRefs.push_back(refEntry("hi", "b/h.wav", /*root=*/999, false, 0, 0,
|
||||
/*channels=*/-3));
|
||||
s.sampleRefs.push_back(refEntry("lo", "b/l.wav", /*root=*/-5, false, 0, 0,
|
||||
/*channels=*/1));
|
||||
s.sampleRefs.push_back(refEntry("ok", "b/o.wav", /*root=*/36, false, 0, 0,
|
||||
/*channels=*/2));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||
CHECK(back.sampleRefs.size() == 3);
|
||||
CHECK(back.sampleRefs.size() == 3 && back.sampleRefs[0].ref.rootNote == 60);
|
||||
CHECK(back.sampleRefs.size() == 3 && back.sampleRefs[0].ref.channelCount == 0);
|
||||
CHECK(back.sampleRefs.size() == 3 && back.sampleRefs[1].ref.rootNote == 60);
|
||||
CHECK(back.sampleRefs.size() == 3 && back.sampleRefs[1].ref.channelCount == 1);
|
||||
CHECK(back.sampleRefs.size() == 3 && back.sampleRefs[2].ref.rootNote == 36);
|
||||
CHECK(back.sampleRefs.size() == 3 && back.sampleRefs[2].ref.channelCount == 2);
|
||||
}
|
||||
|
||||
static void testLegacyLiftDecision() {
|
||||
// The #A terminating guard, pure: Retry while the blob is not readable YET (absent,
|
||||
// empty, malformed — the project's ext-state may simply not have parsed); Lift when a
|
||||
// referenced id resolves (a lift attempt makes progress); Stale — the shell latches
|
||||
// permanently — when the blob PARSES and knows none of the referenced ids (an empty
|
||||
// id list included), so a stale-id pre-v10 lift STOPS instead of churning every tick.
|
||||
const std::string json = bookJson({makeSample("a", "Kick", "b/a.wav", 36)}, {});
|
||||
const std::vector<std::string> ids{"a"};
|
||||
CHECK(legacyLiftDecision(std::nullopt, ids) == LegacyLiftDecision::Retry);
|
||||
CHECK(legacyLiftDecision(std::string(), ids) == LegacyLiftDecision::Retry);
|
||||
CHECK(legacyLiftDecision(std::string("{garbage"), ids) == LegacyLiftDecision::Retry);
|
||||
CHECK(legacyLiftDecision(json, ids) == LegacyLiftDecision::Lift);
|
||||
// One resolvable id among stale ones is still progress (the lift copies what it can).
|
||||
CHECK(legacyLiftDecision(json, {"ghost", "a"}) == LegacyLiftDecision::Lift);
|
||||
CHECK(legacyLiftDecision(json, {"ghost"}) == LegacyLiftDecision::Stale);
|
||||
CHECK(legacyLiftDecision(json, {}) == LegacyLiftDecision::Stale);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testSelectByIdHit();
|
||||
testSelectEmptyIdIsSilence();
|
||||
@@ -2478,6 +2528,8 @@ int main() {
|
||||
testRefreshRefsFromBankUpsertAndOwnership();
|
||||
testRetainRefsFiltersToPlayedSet();
|
||||
testSampleRefsTruncatedMidEntry();
|
||||
testSampleRefsReaderRangeFallbacks();
|
||||
testLegacyLiftDecision();
|
||||
|
||||
if (g_fail == 0) std::printf("sample_map: all tests passed\n");
|
||||
return g_fail != 0;
|
||||
|
||||
Reference in New Issue
Block a user