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:
2026-07-27 03:07:52 -04:00
parent a54af277e4
commit 082c9b82c2
6 changed files with 144 additions and 92 deletions
+74 -56
View File
@@ -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();