s12: thread project rate through v3 legacy lift; remove 44100 literals
kLegacyV3NominalRate removed. readZonesPayload, deserializePerformance, deserializeComponentState now take a projectRate for v3 frames→seconds. Rate fields default 0 (invalid). Four 44100 fallbacks replaced with assert+safe-return. 96k v3-lift test added.
This commit is contained in:
+74
-56
@@ -262,12 +262,6 @@ static void testBuildKeymapSingleFullZone() {
|
||||
CHECK(km.resolve(127, 100).matched);
|
||||
}
|
||||
|
||||
static void testBuildKeymapRateDefault() {
|
||||
// A zero/invalid rate defaults to 44100 rather than producing a divide-by-zero-shaped
|
||||
// sample rate downstream.
|
||||
const Keymap km = buildTier0Keymap({0.1f}, 0, 60, SampleLoop{});
|
||||
CHECK(km.samples.size() == 1 && km.samples[0].sampleRate == 44100);
|
||||
}
|
||||
|
||||
// --- selection state (setState/getState) --------------------------------------
|
||||
|
||||
@@ -279,6 +273,7 @@ static void testSelectionStateRoundTrip() {
|
||||
CHECK(deserializeSelection(bytes) == id);
|
||||
}
|
||||
|
||||
|
||||
static void testSelectionStateEmptyId() {
|
||||
const std::vector<std::uint8_t> bytes = serializeSelection("");
|
||||
CHECK(bytes.size() == 4); // just the version tag
|
||||
@@ -603,7 +598,7 @@ static void testPerformanceStateRoundTrip() {
|
||||
m.zones.push_back(zone("kick", 36, 47)); // no override
|
||||
m.zones.push_back(zone("snare", 48, 59, /*override=*/50)); // with override
|
||||
const std::vector<std::uint8_t> bytes = serializePerformance(m);
|
||||
const PerformanceMap back = deserializePerformance(bytes);
|
||||
const PerformanceMap back = deserializePerformance(bytes, 44100.0);
|
||||
CHECK(back.zones.size() == 2);
|
||||
CHECK(back.zones.size() == 2 && back.zones[0].sampleId == "kick");
|
||||
CHECK(back.zones.size() == 2 && back.zones[0].lowNote == 36 && back.zones[0].highNote == 47);
|
||||
@@ -617,7 +612,7 @@ static void testPerformanceStateEmpty() {
|
||||
const std::vector<std::uint8_t> bytes = serializePerformance(PerformanceMap{});
|
||||
// Envelope version (4) + zones-payload marker (4) + payload version (4) + zero count (4).
|
||||
CHECK(bytes.size() == 16);
|
||||
CHECK(deserializePerformance(bytes).zones.empty());
|
||||
CHECK(deserializePerformance(bytes, 44100.0).zones.empty());
|
||||
}
|
||||
|
||||
static void testPerformanceStateLoopStartRoundTrip() {
|
||||
@@ -630,7 +625,7 @@ static void testPerformanceStateLoopStartRoundTrip() {
|
||||
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));
|
||||
const PerformanceMap back = deserializePerformance(serializePerformance(m), 44100.0);
|
||||
CHECK(back.zones.size() == 2);
|
||||
CHECK(back.zones.size() == 2 && back.zones[0].rootOverride.has_value() &&
|
||||
*back.zones[0].rootOverride == 64);
|
||||
@@ -662,7 +657,7 @@ static void testPerformanceStateV1PayloadBackCompat() {
|
||||
u32(10); // lowNote
|
||||
u32(40); // highNote
|
||||
b.push_back(0); // hasRootOverride = 0 (record ends here in v1)
|
||||
const PerformanceMap back = deserializePerformance(b);
|
||||
const PerformanceMap back = deserializePerformance(b, 44100.0);
|
||||
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());
|
||||
@@ -679,7 +674,7 @@ static void testComponentStateLoopStartRoundTrip() {
|
||||
z.loopOverride = lp;
|
||||
z.startPoint = 128;
|
||||
s.map.zones.push_back(z);
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||
CHECK(back.selectionId == "pick");
|
||||
CHECK(back.map.zones.size() == 1 && back.map.zones[0].loopOverride.has_value() &&
|
||||
back.map.zones[0].loopOverride->start == 500 &&
|
||||
@@ -691,25 +686,25 @@ static void testComponentStateLoopStartRoundTrip() {
|
||||
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");
|
||||
const PerformanceMap back = deserializePerformance(v1);
|
||||
const PerformanceMap back = deserializePerformance(v1, 44100.0);
|
||||
CHECK(back.zones.size() == 1);
|
||||
CHECK(back.zones.size() == 1 && back.zones[0].sampleId == "legacy-sample-id");
|
||||
CHECK(back.zones.size() == 1 && back.zones[0].lowNote == 0 && back.zones[0].highNote == 127);
|
||||
CHECK(back.zones.size() == 1 && !back.zones[0].rootOverride.has_value());
|
||||
// A v1 blob with an EMPTY id lifts to an empty map (no zone for "no selection").
|
||||
CHECK(deserializePerformance(serializeSelection("")).zones.empty());
|
||||
CHECK(deserializePerformance(serializeSelection(""), 44100.0).zones.empty());
|
||||
}
|
||||
|
||||
static void testPerformanceStateGarbage() {
|
||||
// Unknown version / truncated / empty -> empty map (never throws).
|
||||
CHECK(deserializePerformance({}).zones.empty());
|
||||
CHECK(deserializePerformance({0xAA, 0xBB, 0xCC, 0xDD}).zones.empty()); // unknown version
|
||||
CHECK(deserializePerformance({}, 44100.0).zones.empty());
|
||||
CHECK(deserializePerformance({0xAA, 0xBB, 0xCC, 0xDD}, 44100.0).zones.empty()); // unknown version
|
||||
// Truncated mid-zone: valid v2 header claiming 1 zone but no zone bytes -> empty.
|
||||
std::vector<std::uint8_t> t;
|
||||
t.push_back(2); t.push_back(0); t.push_back(0); t.push_back(0); // version 2
|
||||
t.push_back(1); t.push_back(0); t.push_back(0); t.push_back(0); // count 1
|
||||
// (no zone payload)
|
||||
CHECK(deserializePerformance(t).zones.empty());
|
||||
CHECK(deserializePerformance(t, 44100.0).zones.empty());
|
||||
}
|
||||
|
||||
static void testPerformanceStateNegativeNotesRoundTrip() {
|
||||
@@ -717,7 +712,7 @@ static void testPerformanceStateNegativeNotesRoundTrip() {
|
||||
// a hand-set/legacy value round-trips without corruption (two's-complement on the wire).
|
||||
PerformanceMap m;
|
||||
m.zones.push_back(zone("s", 0, 127, /*override=*/0));
|
||||
const PerformanceMap back = deserializePerformance(serializePerformance(m));
|
||||
const PerformanceMap back = deserializePerformance(serializePerformance(m), 44100.0);
|
||||
CHECK(back.zones.size() == 1 && back.zones[0].rootOverride.has_value() &&
|
||||
*back.zones[0].rootOverride == 0);
|
||||
}
|
||||
@@ -730,7 +725,7 @@ static void testComponentStateRoundTrip() {
|
||||
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));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||
CHECK(back.selectionId == "picked-capture");
|
||||
CHECK(back.map.zones.size() == 2);
|
||||
CHECK(back.map.zones.size() == 2 && back.map.zones[0].sampleId == "z0" &&
|
||||
@@ -744,7 +739,7 @@ static void testComponentStateSelectionOnlyNoZones() {
|
||||
// (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));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||
CHECK(back.selectionId == "just-a-pick");
|
||||
CHECK(back.map.zones.empty());
|
||||
}
|
||||
@@ -752,7 +747,7 @@ static void testComponentStateSelectionOnlyNoZones() {
|
||||
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));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||
CHECK(back.selectionId.empty());
|
||||
CHECK(back.map.zones.empty());
|
||||
}
|
||||
@@ -761,13 +756,13 @@ 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);
|
||||
const ComponentState back = deserializeComponentState(v1, 44100.0);
|
||||
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(""));
|
||||
const ComponentState empty = deserializeComponentState(serializeSelection(""), 44100.0);
|
||||
CHECK(empty.selectionId.empty() && empty.map.zones.empty());
|
||||
}
|
||||
|
||||
@@ -777,7 +772,7 @@ static void testComponentStateV2BackCompat() {
|
||||
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);
|
||||
const ComponentState back = deserializeComponentState(v2, 44100.0);
|
||||
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);
|
||||
@@ -785,17 +780,17 @@ static void testComponentStateV2BackCompat() {
|
||||
|
||||
static void testComponentStateGarbage() {
|
||||
// Empty / unknown version -> empty (never throws across the host).
|
||||
CHECK(deserializeComponentState({}).selectionId.empty());
|
||||
CHECK(deserializeComponentState({}).map.zones.empty());
|
||||
CHECK(deserializeComponentState({}, 44100.0).selectionId.empty());
|
||||
CHECK(deserializeComponentState({}, 44100.0).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());
|
||||
CHECK(deserializeComponentState(unknown, 44100.0).map.zones.empty());
|
||||
CHECK(deserializeComponentState(unknown, 44100.0).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());
|
||||
CHECK(deserializeComponentState(t, 44100.0).selectionId.empty());
|
||||
CHECK(deserializeComponentState(t, 44100.0).map.zones.empty());
|
||||
}
|
||||
|
||||
// --- S7: extractChannel / decodeChannels (cross-mode channel policy) ----------
|
||||
@@ -887,7 +882,7 @@ static void testComponentStateV4RoundTripStereo() {
|
||||
s.selectionId = "pick";
|
||||
s.channelMode = ChannelMode::Stereo;
|
||||
s.map.zones.push_back(zone("z0", 0, 127, /*override=*/std::nullopt));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||
CHECK(back.selectionId == "pick");
|
||||
CHECK(back.channelMode == ChannelMode::Stereo); // mode round-trips
|
||||
CHECK(back.map.zones.size() == 1 && back.map.zones[0].sampleId == "z0");
|
||||
@@ -897,14 +892,14 @@ static void testComponentStateV4RoundTripMono() {
|
||||
ComponentState s;
|
||||
s.selectionId = "pick";
|
||||
s.channelMode = ChannelMode::Mono;
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||
CHECK(back.selectionId == "pick");
|
||||
CHECK(back.channelMode == ChannelMode::Mono);
|
||||
}
|
||||
|
||||
static void testComponentStateV4DefaultIsMono() {
|
||||
// A default-constructed state serializes with mono and restores mono (preserves behavior).
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(ComponentState{}));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(ComponentState{}), 44100.0);
|
||||
CHECK(back.channelMode == ChannelMode::Mono);
|
||||
CHECK(back.selectionId.empty() && back.map.zones.empty());
|
||||
}
|
||||
@@ -918,7 +913,7 @@ static void testComponentStateV3LiftsToMono() {
|
||||
v3.push_back(static_cast<std::uint8_t>(id.size())); v3.push_back(0); v3.push_back(0); v3.push_back(0);
|
||||
v3.insert(v3.end(), id.begin(), id.end());
|
||||
v3.push_back(0); v3.push_back(0); v3.push_back(0); v3.push_back(0); // zone count 0
|
||||
const ComponentState back = deserializeComponentState(v3);
|
||||
const ComponentState back = deserializeComponentState(v3, 44100.0);
|
||||
CHECK(back.selectionId == "legacy");
|
||||
CHECK(back.channelMode == ChannelMode::Mono); // pre-S7 default
|
||||
CHECK(back.map.zones.empty());
|
||||
@@ -926,17 +921,17 @@ static void testComponentStateV3LiftsToMono() {
|
||||
|
||||
static void testComponentStateV1V2LiftToMono() {
|
||||
// The older lifts (v1 single-selection, v2 zones-only) also default to mono under v4 read.
|
||||
const ComponentState v1 = deserializeComponentState(serializeSelection("old"));
|
||||
const ComponentState v1 = deserializeComponentState(serializeSelection("old"), 44100.0);
|
||||
CHECK(v1.channelMode == ChannelMode::Mono && v1.selectionId == "old");
|
||||
PerformanceMap m; m.zones.push_back(zone("s", 12, 24));
|
||||
const ComponentState v2 = deserializeComponentState(serializePerformance(m));
|
||||
const ComponentState v2 = deserializeComponentState(serializePerformance(m), 44100.0);
|
||||
CHECK(v2.channelMode == ChannelMode::Mono && v2.map.zones.size() == 1);
|
||||
}
|
||||
|
||||
static void testComponentStateV4TruncatedModeByte() {
|
||||
// A v4 blob truncated right after the version tag (no mode byte) -> empty, mono default holds.
|
||||
std::vector<std::uint8_t> t{4, 0, 0, 0}; // version 4, nothing after
|
||||
const ComponentState back = deserializeComponentState(t);
|
||||
const ComponentState back = deserializeComponentState(t, 44100.0);
|
||||
CHECK(back.channelMode == ChannelMode::Mono);
|
||||
CHECK(back.selectionId.empty() && back.map.zones.empty());
|
||||
}
|
||||
@@ -959,7 +954,7 @@ static void testComponentStateV4StereoWithZoneOverridesRoundTrip() {
|
||||
s.map.zones.push_back(z0);
|
||||
s.map.zones.push_back(z1);
|
||||
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||
CHECK(back.channelMode == ChannelMode::Stereo); // envelope field survives
|
||||
CHECK(back.selectionId == "pick");
|
||||
CHECK(back.map.zones.size() == 2);
|
||||
@@ -989,7 +984,7 @@ static void testComponentStateV5MarkerRoundTrip() {
|
||||
s.channelMode = ChannelMode::Stereo;
|
||||
s.lastConsumedAssignGeneration = 1700000123456LL; // > INT32_MAX
|
||||
s.map.zones.push_back(zone("z0", 0, 127, /*override=*/std::nullopt));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||
CHECK(back.lastConsumedAssignGeneration == 1700000123456LL); // marker survives
|
||||
CHECK(back.channelMode == ChannelMode::Stereo);
|
||||
CHECK(back.selectionId == "pick");
|
||||
@@ -999,7 +994,7 @@ static void testComponentStateV5MarkerRoundTrip() {
|
||||
static void testComponentStateDefaultMarkerIsZero() {
|
||||
// A default-constructed state has marker 0 and round-trips 0 — a fresh instance's first
|
||||
// assign (generation >= 1) must not be swallowed by a non-zero default.
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(ComponentState{}));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(ComponentState{}), 44100.0);
|
||||
CHECK(back.lastConsumedAssignGeneration == 0);
|
||||
}
|
||||
|
||||
@@ -1015,7 +1010,7 @@ static void testComponentStateV4LiftsMarkerToZero() {
|
||||
v4.push_back(static_cast<std::uint8_t>(id.size())); v4.push_back(0); v4.push_back(0); v4.push_back(0);
|
||||
v4.insert(v4.end(), id.begin(), id.end());
|
||||
v4.push_back(0); v4.push_back(0); v4.push_back(0); v4.push_back(0); // zone count 0
|
||||
const ComponentState back = deserializeComponentState(v4);
|
||||
const ComponentState back = deserializeComponentState(v4, 44100.0);
|
||||
CHECK(back.lastConsumedAssignGeneration == 0); // no marker in v4 -> default 0
|
||||
CHECK(back.channelMode == ChannelMode::Stereo); // v4 mode byte still honored
|
||||
CHECK(back.selectionId == "saved");
|
||||
@@ -1026,7 +1021,7 @@ static void testComponentStateV5TruncatedMarker() {
|
||||
// A v5 blob truncated inside the 8-byte marker (mode byte present, marker cut short) -> empty,
|
||||
// mono + marker 0 default holds (bounded read, never throws across the host).
|
||||
std::vector<std::uint8_t> t{5, 0, 0, 0, 1, 0xAA, 0xBB}; // version 5, mode byte, 2 marker bytes
|
||||
const ComponentState back = deserializeComponentState(t);
|
||||
const ComponentState back = deserializeComponentState(t, 44100.0);
|
||||
CHECK(back.lastConsumedAssignGeneration == 0);
|
||||
CHECK(back.selectionId.empty() && back.map.zones.empty());
|
||||
}
|
||||
@@ -1059,7 +1054,7 @@ static void testV5EnvelopeWithMarkerAndPlayParamsRoundTrip() {
|
||||
z.play.pitchEnv.peakSemitones = 12.5;
|
||||
s.map.zones.push_back(z);
|
||||
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||
CHECK(back.channelMode == ChannelMode::Stereo); // envelope: mode
|
||||
CHECK(back.lastConsumedAssignGeneration == 1700000123456LL); // envelope: marker
|
||||
CHECK(back.selectionId == "pick");
|
||||
@@ -1133,7 +1128,7 @@ static void testV4BlobWithPlayParamsLiftsMarkerZeroKeepsPlay() {
|
||||
const std::vector<std::uint8_t> payload = handBuildV3PayloadOneZone("zv3");
|
||||
v4.insert(v4.end(), payload.begin(), payload.end());
|
||||
|
||||
const ComponentState back = deserializeComponentState(v4);
|
||||
const ComponentState back = deserializeComponentState(v4, 44100.0);
|
||||
CHECK(back.lastConsumedAssignGeneration == 0); // (b) no marker in v4 -> default 0
|
||||
CHECK(back.channelMode == ChannelMode::Stereo); // v4 envelope mode honored
|
||||
CHECK(back.selectionId == "s15saved");
|
||||
@@ -1143,8 +1138,8 @@ static void testV4BlobWithPlayParamsLiftsMarkerZeroKeepsPlay() {
|
||||
CHECK(back.map.zones[0].lowNote == 10 && back.map.zones[0].highNote == 70);
|
||||
const ZonePlaySeconds& p = back.map.zones[0].play;
|
||||
CHECK(p.playMode == PlayMode::Trigger); // (c) play params survive the v4 envelope
|
||||
// Legacy v3 wall-clock frames (44.1k-nominal) convert to seconds at the v3 authoring rate.
|
||||
CHECK(approx(p.adsr.holdSeconds, 2048.0 / kLegacyV3NominalRate));
|
||||
// Legacy v3 wall-clock frames convert to seconds at the passed project rate (44100.0 here).
|
||||
CHECK(approx(p.adsr.holdSeconds, 2048.0 / 44100.0));
|
||||
CHECK(p.trigger.lengthFraction == 0.5);
|
||||
CHECK(p.trigger.fadeInFrames == 16 && p.trigger.fadeOutFrames == 48); // source frames, as-is
|
||||
CHECK(p.pitchEngine == PitchEngine::Varispeed);
|
||||
@@ -1169,7 +1164,7 @@ static void testPlayParamsRoundTrip() {
|
||||
z.play.pitchEnv.decaySeconds = 0.011;
|
||||
z.play.pitchEnv.peakSemitones = -7.5;
|
||||
m.zones.push_back(z);
|
||||
const PerformanceMap back = deserializePerformance(serializePerformance(m));
|
||||
const PerformanceMap back = deserializePerformance(serializePerformance(m), 44100.0);
|
||||
CHECK(back.zones.size() == 1);
|
||||
if (back.zones.size() != 1) return;
|
||||
const ZonePlaySeconds& p = back.zones[0].play;
|
||||
@@ -1196,7 +1191,7 @@ static void testPlayParamsComposeWithLoopStart() {
|
||||
z.play.adsr.holdSeconds = 0.0225;
|
||||
z.play.pitchEngine = PitchEngine::Preserve;
|
||||
m.zones.push_back(z);
|
||||
const PerformanceMap back = deserializePerformance(serializePerformance(m));
|
||||
const PerformanceMap back = deserializePerformance(serializePerformance(m), 44100.0);
|
||||
CHECK(back.zones.size() == 1);
|
||||
if (back.zones.size() != 1) return;
|
||||
CHECK(back.zones[0].loopOverride.has_value() &&
|
||||
@@ -1227,7 +1222,7 @@ static void testPlayParamsV2BackCompatLiftsToDefaults() {
|
||||
b.push_back(0); // hasRootOverride = 0
|
||||
b.push_back(0); // hasLoopOverride = 0
|
||||
b.push_back(0); // hasStartPoint = 0 (record ends here in v2)
|
||||
const PerformanceMap back = deserializePerformance(b);
|
||||
const PerformanceMap back = deserializePerformance(b, 44100.0);
|
||||
CHECK(back.zones.size() == 1);
|
||||
if (back.zones.size() != 1) return;
|
||||
CHECK(back.zones[0].sampleId == "old");
|
||||
@@ -1249,7 +1244,7 @@ static void testPlayParamsThroughComponentEnvelope() {
|
||||
z.play.trigger.lengthFraction = 0.9;
|
||||
z.play.pitchEngine = PitchEngine::Varispeed;
|
||||
s.map.zones.push_back(z);
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s));
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0);
|
||||
CHECK(back.channelMode == ChannelMode::Stereo);
|
||||
CHECK(back.map.zones.size() == 1);
|
||||
if (back.map.zones.size() != 1) return;
|
||||
@@ -1276,7 +1271,7 @@ static void testFullAdsrSecondsRoundTrip() {
|
||||
z.play.adsr.releaseSeconds = 0.2;
|
||||
z.play.pitchEngine = PitchEngine::Preserve;
|
||||
m.zones.push_back(z);
|
||||
const PerformanceMap back = deserializePerformance(serializePerformance(m));
|
||||
const PerformanceMap back = deserializePerformance(serializePerformance(m), 44100.0);
|
||||
CHECK(back.zones.size() == 1);
|
||||
if (back.zones.size() != 1) return;
|
||||
const AdsrSeconds& a = back.zones[0].play.adsr;
|
||||
@@ -1300,18 +1295,41 @@ static void testV3BlobLiftsAdsrToSecondsDefaults() {
|
||||
u32(kPerformanceStateVersion); // envelope version 2 header
|
||||
const std::vector<std::uint8_t> payload = handBuildV3PayloadOneZone("old");
|
||||
blob.insert(blob.end(), payload.begin(), payload.end());
|
||||
const PerformanceMap back = deserializePerformance(blob);
|
||||
const PerformanceMap back = deserializePerformance(blob, 44100.0);
|
||||
CHECK(back.zones.size() == 1);
|
||||
if (back.zones.size() != 1) return;
|
||||
const AdsrSeconds& a = back.zones[0].play.adsr;
|
||||
// hold converts from the v3 record's 44.1k-nominal frames; A/D/S/R lift to the seconds defaults.
|
||||
CHECK(approx(a.holdSeconds, 2048.0 / kLegacyV3NominalRate)); // from the hand-built v3 record
|
||||
// hold converts from the v3 record's frames at the passed project rate (44100.0 here).
|
||||
CHECK(approx(a.holdSeconds, 2048.0 / 44100.0)); // from the hand-built v3 record
|
||||
CHECK(a.attackSeconds == AdsrSeconds{}.attackSeconds); // 0.003 (tier-0 default seconds)
|
||||
CHECK(a.decaySeconds == AdsrSeconds{}.decaySeconds); // 0.0
|
||||
CHECK(a.sustainLevel == AdsrSeconds{}.sustainLevel); // 1.0
|
||||
CHECK(a.releaseSeconds == AdsrSeconds{}.releaseSeconds); // 0.060
|
||||
}
|
||||
|
||||
// A legacy PAYLOAD v3 blob decoded at 96k: the hold frame count (2048) converts using the
|
||||
// PASSED project rate, not a baked 44100 constant. At 96000 the seconds value is 2048/96000.
|
||||
static void testV3BlobLiftsAdsrAt96k() {
|
||||
std::vector<std::uint8_t> blob;
|
||||
auto u32 = [&](std::uint32_t v) {
|
||||
blob.push_back(v & 0xFF); blob.push_back((v >> 8) & 0xFF);
|
||||
blob.push_back((v >> 16) & 0xFF); blob.push_back((v >> 24) & 0xFF);
|
||||
};
|
||||
u32(kPerformanceStateVersion); // envelope version 2 header
|
||||
const std::vector<std::uint8_t> payload = handBuildV3PayloadOneZone("old96k");
|
||||
blob.insert(blob.end(), payload.begin(), payload.end());
|
||||
const PerformanceMap back = deserializePerformance(blob, 96000.0);
|
||||
CHECK(back.zones.size() == 1);
|
||||
if (back.zones.size() != 1) return;
|
||||
const AdsrSeconds& a = back.zones[0].play.adsr;
|
||||
// 2048 frames at 96000 Hz -> 2048/96000 seconds (not 2048/44100).
|
||||
CHECK(approx(a.holdSeconds, 2048.0 / 96000.0));
|
||||
CHECK(a.attackSeconds == AdsrSeconds{}.attackSeconds);
|
||||
CHECK(a.decaySeconds == AdsrSeconds{}.decaySeconds);
|
||||
CHECK(a.sustainLevel == AdsrSeconds{}.sustainLevel);
|
||||
CHECK(a.releaseSeconds == AdsrSeconds{}.releaseSeconds);
|
||||
}
|
||||
|
||||
// The lift -> commit -> reload sequence must stay rate-correct at 44.1k / 48k / 96k. A DEFAULT zone
|
||||
// resolves to the tier-0 wall-clock durations at each rate (round(0.003*rate), round(0.060*rate));
|
||||
// an AUTHORED zone resolves to round(seconds*rate). This is the R2 blocker, pinned across rates.
|
||||
@@ -1324,7 +1342,7 @@ static void testKeymapBuildResolvesSecondsToFramesAtEachRate() {
|
||||
{
|
||||
PerformanceMap m;
|
||||
m.zones.push_back(zone("def", 0, 127)); // product-default play (tier-0 AHDSR seconds)
|
||||
const PerformanceMap back = deserializePerformance(serializePerformance(m));
|
||||
const PerformanceMap back = deserializePerformance(serializePerformance(m), 44100.0);
|
||||
CHECK(back.zones.size() == 1);
|
||||
if (back.zones.size() != 1) continue;
|
||||
ResolvedZone rz; rz.lowNote = 0; rz.highNote = 127; rz.rootNote = 60;
|
||||
@@ -1348,7 +1366,7 @@ static void testKeymapBuildResolvesSecondsToFramesAtEachRate() {
|
||||
z.play.adsr.sustainLevel = 0.7;
|
||||
z.play.adsr.releaseSeconds = 0.2;
|
||||
m.zones.push_back(z);
|
||||
const PerformanceMap back = deserializePerformance(serializePerformance(m));
|
||||
const PerformanceMap back = deserializePerformance(serializePerformance(m), 44100.0);
|
||||
CHECK(back.zones.size() == 1);
|
||||
if (back.zones.size() != 1) continue;
|
||||
ResolvedZone rz; rz.lowNote = 0; rz.highNote = 127; rz.rootNote = 60;
|
||||
@@ -1400,7 +1418,6 @@ int main() {
|
||||
testDownmixThreeChannelAverages();
|
||||
testDownmixDegenerate();
|
||||
testBuildKeymapSingleFullZone();
|
||||
testBuildKeymapRateDefault();
|
||||
testSelectionStateRoundTrip();
|
||||
testSelectionStateEmptyId();
|
||||
testSelectionStateWrongVersion();
|
||||
@@ -1433,6 +1450,7 @@ int main() {
|
||||
testPlayParamsThroughComponentEnvelope();
|
||||
testFullAdsrSecondsRoundTrip();
|
||||
testV3BlobLiftsAdsrToSecondsDefaults();
|
||||
testV3BlobLiftsAdsrAt96k();
|
||||
testKeymapBuildResolvesSecondsToFramesAtEachRate();
|
||||
testBuildTier0KeymapResolvesSecondsAt48k();
|
||||
testComponentStateRoundTrip();
|
||||
|
||||
Reference in New Issue
Block a user