fix(vst): zone-bleed (3a) — reconcile the Sample-face zone on load so the loaded sample zone is the one first-match resolve plays
This commit is contained in:
@@ -1665,6 +1665,108 @@ static void testBuildTier0KeymapResolvesSecondsAt48k() {
|
||||
CHECK(a.releaseFrames == 2880); // round(0.060 * 48000)
|
||||
}
|
||||
|
||||
// --- single-capture zone lifecycle: reconcileSingleCaptureZones (zone-bleed fix, 3a) ---
|
||||
|
||||
// The bled state: two full-range Sample-face zones. Reconcile on load keeps only the
|
||||
// selected sample's zone, params intact (a return to that sample restores its edits).
|
||||
static void testReconcileKeepsOnlySelectedFullRangeZone() {
|
||||
PerformanceMap m;
|
||||
m.zones.push_back(zone("a", 0, 127));
|
||||
PerformanceZone b = zone("b", 0, 127);
|
||||
b.keyTrack = 0.5; // distinctive param — must survive the reconcile
|
||||
m.zones.push_back(b);
|
||||
CHECK(reconcileSingleCaptureZones(m, "b"));
|
||||
CHECK(m.zones.size() == 1);
|
||||
CHECK(m.zones.size() == 1 && m.zones[0].sampleId == "b");
|
||||
CHECK(m.zones.size() == 1 && m.zones[0].keyTrack == 0.5);
|
||||
}
|
||||
|
||||
// Loading a sample with no zone yet empties a Sample-face-shaped map — the shell then
|
||||
// plays the selection via the Tier-0 fast path (product defaults), never the stale zone.
|
||||
static void testReconcileClearsWhenSelectionUnzoned() {
|
||||
PerformanceMap m;
|
||||
m.zones.push_back(zone("a", 0, 127));
|
||||
CHECK(reconcileSingleCaptureZones(m, "b"));
|
||||
CHECK(m.zones.empty());
|
||||
}
|
||||
|
||||
// Any narrow key range marks Zone-view authorship: the map (including a legitimate
|
||||
// full-range fallback zone) is untouched — first-match order is load-bearing there.
|
||||
static void testReconcileLeavesAuthoredMapUntouched() {
|
||||
PerformanceMap m;
|
||||
m.zones.push_back(zone("a", 60, 72)); // authored narrow range
|
||||
m.zones.push_back(zone("b", 0, 127)); // authored full-range fallback layer
|
||||
CHECK(!reconcileSingleCaptureZones(m, "c"));
|
||||
CHECK(m.zones.size() == 2);
|
||||
CHECK(m.zones.size() == 2 && m.zones[0].sampleId == "a" && m.zones[1].sampleId == "b");
|
||||
}
|
||||
|
||||
// A map already holding exactly the selection's one zone is coherent — no change reported,
|
||||
// so callers do not republish/reload needlessly.
|
||||
static void testReconcileNoOpWhenAlreadyCoherent() {
|
||||
PerformanceMap m;
|
||||
m.zones.push_back(zone("a", 0, 127));
|
||||
CHECK(!reconcileSingleCaptureZones(m, "a"));
|
||||
CHECK(m.zones.size() == 1 && m.zones[0].sampleId == "a");
|
||||
}
|
||||
|
||||
// Guards: an empty map and an empty selection both leave the map untouched.
|
||||
static void testReconcileGuards() {
|
||||
PerformanceMap empty;
|
||||
CHECK(!reconcileSingleCaptureZones(empty, "a"));
|
||||
PerformanceMap m;
|
||||
m.zones.push_back(zone("a", 0, 127));
|
||||
m.zones.push_back(zone("b", 0, 127));
|
||||
CHECK(!reconcileSingleCaptureZones(m, "")); // no selection -> never mutate
|
||||
CHECK(m.zones.size() == 2);
|
||||
}
|
||||
|
||||
// The reported browse sequence, end to end at the pure layer: edit sample A (full-range
|
||||
// zone materialized), browse-load B, edit B (zone appended AFTER A's). First proves the
|
||||
// bug — first-match resolve plays A's zone while B is loaded — then proves the reconcile
|
||||
// at the load step makes the loaded sample's zone the one resolve() returns.
|
||||
static void testReconcileBrowseSequenceNoShadowing() {
|
||||
const std::string json = bookJson(
|
||||
{makeSample("a", "A", "reasampler_bank/a.wav", 60),
|
||||
makeSample("b", "B", "reasampler_bank/b.wav", 60)}, {});
|
||||
|
||||
PerformanceMap m;
|
||||
m.zones.push_back(zone("a", 0, 127)); // edit on A materializes A's zone
|
||||
m.zones.push_back(zone("b", 0, 127)); // browse to B (pre-fix: no reconcile) + edit B
|
||||
|
||||
// PCM markers: A decodes to 0.75, B to 0.25 — which zone resolve() picked is audible
|
||||
// in frames[0] of the resolved sample.
|
||||
const auto keymapFor = [&](const PerformanceMap& map) {
|
||||
const ResolvedPerformance r = resolvePerformance(json, map);
|
||||
std::vector<DecodedZonePcm> decoded;
|
||||
for (const ResolvedZone& rz : r.zones) {
|
||||
decoded.push_back(DecodedZonePcm{
|
||||
{rz.relativePath == "reasampler_bank/a.wav" ? 0.75f : 0.25f}, 44100});
|
||||
}
|
||||
return buildZonedKeymap(r.zones, decoded);
|
||||
};
|
||||
|
||||
// The bled map: the engine resolves A's zone (index 0) — the shadowing bug.
|
||||
const Keymap bled = keymapFor(m);
|
||||
CHECK(bled.zones.size() == 2);
|
||||
const ZoneResolution shadow = bled.resolve(60, 100);
|
||||
CHECK(shadow.matched);
|
||||
CHECK(shadow.matched &&
|
||||
bled.samples[bled.zones[shadow.zoneIndex].sampleIndex].frames[0] == 0.75f);
|
||||
|
||||
// The fix at the load step: reconcile on the selection change keeps only B's zone —
|
||||
// the loaded sample's zone IS the zone resolve() returns, at every note.
|
||||
CHECK(reconcileSingleCaptureZones(m, "b"));
|
||||
const Keymap fixed = keymapFor(m);
|
||||
CHECK(fixed.zones.size() == 1);
|
||||
for (int note : {0, 60, 127}) {
|
||||
const ZoneResolution r = fixed.resolve(note, 100);
|
||||
CHECK(r.matched);
|
||||
CHECK(r.matched &&
|
||||
fixed.samples[fixed.zones[r.zoneIndex].sampleIndex].frames[0] == 0.25f);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
testSelectByIdHit();
|
||||
testSelectEmptyIdIsSilence();
|
||||
@@ -1763,6 +1865,12 @@ int main() {
|
||||
testComponentStateV6TruncatedVelocity();
|
||||
testV5EnvelopeWithMarkerAndPlayParamsRoundTrip();
|
||||
testV4BlobWithPlayParamsLiftsMarkerZeroKeepsPlay();
|
||||
testReconcileKeepsOnlySelectedFullRangeZone();
|
||||
testReconcileClearsWhenSelectionUnzoned();
|
||||
testReconcileLeavesAuthoredMapUntouched();
|
||||
testReconcileNoOpWhenAlreadyCoherent();
|
||||
testReconcileGuards();
|
||||
testReconcileBrowseSequenceNoShadowing();
|
||||
|
||||
if (g_fail == 0) std::printf("sample_map: all tests passed\n");
|
||||
return g_fail != 0;
|
||||
|
||||
Reference in New Issue
Block a user