S11: waveform view with draggable start/loop markers + zero-crossing snap
Pure waveform_view module (frame<->pixel markers, zero-crossing snap); per-zone loop/start overrides on PerformanceZone with self-versioning zone payload; core start-point read offset; editor waveform surface with drag machine.
This commit is contained in:
+128
-2
@@ -479,6 +479,40 @@ static void testResolveLoopThreaded() {
|
||||
CHECK(r.zones.size() == 1);
|
||||
CHECK(r.zones.size() == 1 && r.zones[0].loop.hasLoop);
|
||||
CHECK(r.zones.size() == 1 && r.zones[0].loop.start == 200 && r.zones[0].loop.end == 800);
|
||||
// No loop override + no startPoint -> effective start is 0 (S11 default).
|
||||
CHECK(r.zones.size() == 1 && r.zones[0].startFrame == 0);
|
||||
}
|
||||
|
||||
static void testResolveLoopOverrideWins() {
|
||||
// S11: the instrument's per-zone loopOverride beats the bank's S2 loop intrinsic, and the
|
||||
// startPoint feeds the effective startFrame — without mutating the bank.
|
||||
Sample s = makeSample("a", "Pad", "b/a.wav", 60);
|
||||
s.loop = LoopPoints{200, 800}; // bank intrinsic
|
||||
const std::string json = bookJson({s}, {});
|
||||
PerformanceMap m;
|
||||
PerformanceZone z = zone("a", 0, 127);
|
||||
SampleLoop over; over.hasLoop = true; over.start = 1000; over.end = 4000;
|
||||
z.loopOverride = over; // instrument override
|
||||
z.startPoint = 512; // start offset
|
||||
m.zones.push_back(z);
|
||||
const ResolvedPerformance r = resolvePerformance(json, m);
|
||||
CHECK(r.zones.size() == 1 && r.zones[0].loop.hasLoop);
|
||||
CHECK(r.zones.size() == 1 && r.zones[0].loop.start == 1000 && r.zones[0].loop.end == 4000);
|
||||
CHECK(r.zones.size() == 1 && r.zones[0].startFrame == 512);
|
||||
}
|
||||
|
||||
static void testResolveLoopOverrideDisablesLoop() {
|
||||
// A loopOverride with hasLoop=false explicitly REMOVES the bank's loop for this instance
|
||||
// (override present-but-empty wins over the intrinsic — a deliberate "no loop here").
|
||||
Sample s = makeSample("a", "Pad", "b/a.wav", 60);
|
||||
s.loop = LoopPoints{200, 800};
|
||||
const std::string json = bookJson({s}, {});
|
||||
PerformanceMap m;
|
||||
PerformanceZone z = zone("a", 0, 127);
|
||||
z.loopOverride = SampleLoop{}; // hasLoop=false, start=end=0
|
||||
m.zones.push_back(z);
|
||||
const ResolvedPerformance r = resolvePerformance(json, m);
|
||||
CHECK(r.zones.size() == 1 && !r.zones[0].loop.hasLoop);
|
||||
}
|
||||
|
||||
// --- performance map: buildZonedKeymap ----------------------------------------
|
||||
@@ -506,6 +540,24 @@ static void testBuildZonedKeymapMultiZone() {
|
||||
CHECK(!km.resolve(24, 100).matched);
|
||||
}
|
||||
|
||||
static void testBuildZonedKeymapThreadsLoopAndStart() {
|
||||
// S11: the effective loop + start on a ResolvedZone reach the core's SampleData so the
|
||||
// voice honors them at note-on.
|
||||
std::vector<ResolvedZone> zones;
|
||||
ResolvedZone z0;
|
||||
z0.lowNote = 0; z0.highNote = 127; z0.rootNote = 60;
|
||||
z0.loop.hasLoop = true; z0.loop.start = 3; z0.loop.end = 7;
|
||||
z0.startFrame = 2;
|
||||
zones.push_back(z0);
|
||||
std::vector<DecodedZonePcm> decoded;
|
||||
decoded.push_back(DecodedZonePcm{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 44100});
|
||||
const Keymap km = buildZonedKeymap(zones, decoded);
|
||||
CHECK(km.samples.size() == 1);
|
||||
CHECK(km.samples.size() == 1 && km.samples[0].loop.hasLoop &&
|
||||
km.samples[0].loop.start == 3 && km.samples[0].loop.end == 7);
|
||||
CHECK(km.samples.size() == 1 && km.samples[0].startFrame == 2);
|
||||
}
|
||||
|
||||
static void testBuildZonedKeymapDropsEmptyPcm() {
|
||||
// A zone whose decoded WAV is empty is dropped; the other zone survives, and the
|
||||
// survivor's sampleIndex points at ITS sample (not the dropped one's slot).
|
||||
@@ -563,11 +615,79 @@ static void testPerformanceStateRoundTrip() {
|
||||
|
||||
static void testPerformanceStateEmpty() {
|
||||
const std::vector<std::uint8_t> bytes = serializePerformance(PerformanceMap{});
|
||||
// Just the version + zero-count header.
|
||||
CHECK(bytes.size() == 8);
|
||||
// Envelope version (4) + zones-payload marker (4) + payload version (4) + zero count (4).
|
||||
CHECK(bytes.size() == 16);
|
||||
CHECK(deserializePerformance(bytes).zones.empty());
|
||||
}
|
||||
|
||||
static void testPerformanceStateLoopStartRoundTrip() {
|
||||
// S11: the per-zone loopOverride + startPoint survive the payload-v2 round trip.
|
||||
PerformanceMap m;
|
||||
PerformanceZone z = zone("pad", 24, 96, /*override=*/64);
|
||||
SampleLoop lp; lp.hasLoop = true; lp.start = 12345; lp.end = 67890;
|
||||
z.loopOverride = lp;
|
||||
z.startPoint = 4096;
|
||||
m.zones.push_back(z);
|
||||
// A second zone with NO overrides proves the optional tail is per-record.
|
||||
m.zones.push_back(zone("kick", 0, 23));
|
||||
const PerformanceMap back = deserializePerformance(serializePerformance(m));
|
||||
CHECK(back.zones.size() == 2);
|
||||
CHECK(back.zones.size() == 2 && back.zones[0].rootOverride.has_value() &&
|
||||
*back.zones[0].rootOverride == 64);
|
||||
CHECK(back.zones.size() == 2 && back.zones[0].loopOverride.has_value() &&
|
||||
back.zones[0].loopOverride->hasLoop &&
|
||||
back.zones[0].loopOverride->start == 12345 &&
|
||||
back.zones[0].loopOverride->end == 67890);
|
||||
CHECK(back.zones.size() == 2 && back.zones[0].startPoint.has_value() &&
|
||||
*back.zones[0].startPoint == 4096);
|
||||
// Zone 1: no overrides -> all optionals absent after round trip.
|
||||
CHECK(back.zones.size() == 2 && !back.zones[1].loopOverride.has_value());
|
||||
CHECK(back.zones.size() == 2 && !back.zones[1].startPoint.has_value());
|
||||
}
|
||||
|
||||
static void testPerformanceStateV1PayloadBackCompat() {
|
||||
// A pre-S11 PAYLOAD v1 blob (no format marker: envelope v2 + bare count + short records)
|
||||
// parses cleanly with the loop/start overrides defaulting absent. Hand-build the exact
|
||||
// shipped shape to prove the reader still accepts the marker-less payload.
|
||||
std::vector<std::uint8_t> b;
|
||||
auto u32 = [&](std::uint32_t v) {
|
||||
b.push_back(v & 0xFF); b.push_back((v >> 8) & 0xFF);
|
||||
b.push_back((v >> 16) & 0xFF); b.push_back((v >> 24) & 0xFF);
|
||||
};
|
||||
u32(2); // envelope version 2
|
||||
u32(1); // zone count 1 (NOT the marker -> payload v1)
|
||||
const std::string id = "legacy";
|
||||
u32(static_cast<std::uint32_t>(id.size()));
|
||||
b.insert(b.end(), id.begin(), id.end());
|
||||
u32(10); // lowNote
|
||||
u32(40); // highNote
|
||||
b.push_back(0); // hasRootOverride = 0 (record ends here in v1)
|
||||
const PerformanceMap back = deserializePerformance(b);
|
||||
CHECK(back.zones.size() == 1 && back.zones[0].sampleId == "legacy");
|
||||
CHECK(back.zones.size() == 1 && back.zones[0].lowNote == 10 && back.zones[0].highNote == 40);
|
||||
CHECK(back.zones.size() == 1 && !back.zones[0].loopOverride.has_value());
|
||||
CHECK(back.zones.size() == 1 && !back.zones[0].startPoint.has_value());
|
||||
}
|
||||
|
||||
static void testComponentStateLoopStartRoundTrip() {
|
||||
// The overrides also round-trip through the v3 ComponentState envelope (zones nest inside
|
||||
// it), so the processor's live getState/setState preserves them — the composition property.
|
||||
ComponentState s;
|
||||
s.selectionId = "pick";
|
||||
PerformanceZone z = zone("pick", 0, 127);
|
||||
SampleLoop lp; lp.hasLoop = true; lp.start = 500; lp.end = 9000;
|
||||
z.loopOverride = lp;
|
||||
z.startPoint = 128;
|
||||
s.map.zones.push_back(z);
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s));
|
||||
CHECK(back.selectionId == "pick");
|
||||
CHECK(back.map.zones.size() == 1 && back.map.zones[0].loopOverride.has_value() &&
|
||||
back.map.zones[0].loopOverride->start == 500 &&
|
||||
back.map.zones[0].loopOverride->end == 9000);
|
||||
CHECK(back.map.zones.size() == 1 && back.map.zones[0].startPoint.has_value() &&
|
||||
*back.map.zones[0].startPoint == 128);
|
||||
}
|
||||
|
||||
static void testPerformanceStateV1BackCompat() {
|
||||
// A v1 blob (the S4 single-selection format) lifts to a single full-keyboard zone.
|
||||
const std::vector<std::uint8_t> v1 = serializeSelection("legacy-sample-id");
|
||||
@@ -711,16 +831,22 @@ int main() {
|
||||
testResolveStaleIdDropsZone();
|
||||
testResolveRootPrecedence();
|
||||
testResolveLoopThreaded();
|
||||
testResolveLoopOverrideWins();
|
||||
testResolveLoopOverrideDisablesLoop();
|
||||
testBuildZonedKeymapMultiZone();
|
||||
testBuildZonedKeymapThreadsLoopAndStart();
|
||||
testBuildZonedKeymapDropsEmptyPcm();
|
||||
testBuildZonedKeymapOverlapFirstWins();
|
||||
testBuildZonedKeymapEmpty();
|
||||
testPerformanceStateRoundTrip();
|
||||
testPerformanceStateEmpty();
|
||||
testPerformanceStateLoopStartRoundTrip();
|
||||
testPerformanceStateV1PayloadBackCompat();
|
||||
testPerformanceStateV1BackCompat();
|
||||
testPerformanceStateGarbage();
|
||||
testPerformanceStateNegativeNotesRoundTrip();
|
||||
testComponentStateRoundTrip();
|
||||
testComponentStateLoopStartRoundTrip();
|
||||
testComponentStateSelectionOnlyNoZones();
|
||||
testComponentStateEmptyIsEmpty();
|
||||
testComponentStateV1BackCompat();
|
||||
|
||||
Reference in New Issue
Block a user