test: S7xS11 compose — stereo+startFrame+loop share one read head; v4 stereo envelope + v2 zone overrides round-trip together
This commit is contained in:
@@ -941,6 +941,44 @@ static void testComponentStateV4TruncatedModeByte() {
|
||||
CHECK(back.selectionId.empty() && back.map.zones.empty());
|
||||
}
|
||||
|
||||
static void testComponentStateV4StereoWithZoneOverridesRoundTrip() {
|
||||
// The MERGE composition property (S7 v4 envelope x S11 v2 zones payload): a v4 blob carrying
|
||||
// channelMode = STEREO AND zones with loopOverride + startPoint must round-trip ALL of it
|
||||
// losslessly. The channel-mode byte lives on the envelope; the loop/start overrides live in
|
||||
// the self-versioned zones payload — the two tracks are orthogonal, so both survive one
|
||||
// serialize/deserialize. (V4RoundTripStereo covers mode with a bare zone; LoopStartRoundTrip
|
||||
// covers overrides at the default mono mode; this asserts them TOGETHER.)
|
||||
ComponentState s;
|
||||
s.selectionId = "pick";
|
||||
s.channelMode = ChannelMode::Stereo;
|
||||
PerformanceZone z0 = zone("z0", 12, 48, /*override=*/36);
|
||||
SampleLoop lp0; lp0.hasLoop = true; lp0.start = 500; lp0.end = 9000;
|
||||
z0.loopOverride = lp0;
|
||||
z0.startPoint = 128;
|
||||
PerformanceZone z1 = zone("z1", 49, 127); // second zone: no overrides (mixed payload)
|
||||
s.map.zones.push_back(z0);
|
||||
s.map.zones.push_back(z1);
|
||||
|
||||
const ComponentState back = deserializeComponentState(serializeComponentState(s));
|
||||
CHECK(back.channelMode == ChannelMode::Stereo); // envelope field survives
|
||||
CHECK(back.selectionId == "pick");
|
||||
CHECK(back.map.zones.size() == 2);
|
||||
CHECK(back.map.zones.size() == 2 && back.map.zones[0].sampleId == "z0" &&
|
||||
back.map.zones[0].lowNote == 12 && back.map.zones[0].highNote == 48);
|
||||
CHECK(back.map.zones.size() == 2 && back.map.zones[0].rootOverride.has_value() &&
|
||||
*back.map.zones[0].rootOverride == 36);
|
||||
CHECK(back.map.zones.size() == 2 && back.map.zones[0].loopOverride.has_value() &&
|
||||
back.map.zones[0].loopOverride->hasLoop &&
|
||||
back.map.zones[0].loopOverride->start == 500 &&
|
||||
back.map.zones[0].loopOverride->end == 9000);
|
||||
CHECK(back.map.zones.size() == 2 && back.map.zones[0].startPoint.has_value() &&
|
||||
*back.map.zones[0].startPoint == 128);
|
||||
// The override-free second zone stays override-free (the payload framing per zone is intact).
|
||||
CHECK(back.map.zones.size() == 2 && back.map.zones[1].sampleId == "z1" &&
|
||||
!back.map.zones[1].loopOverride.has_value() &&
|
||||
!back.map.zones[1].startPoint.has_value());
|
||||
}
|
||||
|
||||
int main() {
|
||||
testSelectByIdHit();
|
||||
testSelectEmptyIdIsSilence();
|
||||
@@ -1010,6 +1048,7 @@ int main() {
|
||||
testComponentStateV3LiftsToMono();
|
||||
testComponentStateV1V2LiftToMono();
|
||||
testComponentStateV4TruncatedModeByte();
|
||||
testComponentStateV4StereoWithZoneOverridesRoundTrip();
|
||||
|
||||
if (g_fail == 0) std::printf("sample_map: all tests passed\n");
|
||||
return g_fail != 0;
|
||||
|
||||
@@ -783,6 +783,52 @@ static void testStereoRenderNullBufferIsNoOp() {
|
||||
for (float v : buf) CHECK(approx(v, 0.0, 1e-9)); // untouched
|
||||
}
|
||||
|
||||
static void testStereoStartFrameLoopShareOneReadHead() {
|
||||
// S7 x S11 compose: a STEREO sample with a startFrame AND a sustain loop must read BOTH
|
||||
// channels from the SAME single read head — one offset, one loop wrap, applied to L and R
|
||||
// identically (only the sampled value differs). A per-frame L/R ramp that is a fixed offset
|
||||
// apart (R = L + 0.5) pins the read position on both channels: if the stereo path ever gave
|
||||
// L and R independent heads, the constant L->R offset would break at the start jump or the
|
||||
// loop seam.
|
||||
SampleData s;
|
||||
s.frames.resize(40);
|
||||
s.framesR.resize(40);
|
||||
for (int i = 0; i < 40; ++i) {
|
||||
s.frames[i] = static_cast<float>(i) * 0.01f; // L: 0.00 .. 0.39
|
||||
s.framesR[i] = static_cast<float>(i) * 0.01f + 0.5f; // R: L + 0.5, everywhere
|
||||
}
|
||||
s.rootNote = 60;
|
||||
s.startFrame = 10; // begin BOTH channels at frame 10
|
||||
s.loop.hasLoop = true;
|
||||
s.loop.start = 20;
|
||||
s.loop.end = 30; // loop [20,30): frames 20..29
|
||||
CHECK(s.channelCount() == 2);
|
||||
Keymap km = Keymap::singleSampleChromatic(std::move(s));
|
||||
VoiceEngine eng(1, km, flatAdsr());
|
||||
eng.noteOn(60, 127); // unity ratio, full velocity, flat gain
|
||||
|
||||
std::vector<AudioSample> left(200, 0.f), right(200, 0.f);
|
||||
eng.render(left.data(), right.data(), 200);
|
||||
|
||||
// First frame: both channels start at frame 10 (L=0.10, R=0.60) — the shared start offset.
|
||||
CHECK(approx(left[0], 0.10, 1e-4));
|
||||
CHECK(approx(right[0], 0.60, 1e-4));
|
||||
// The loop sustains the voice indefinitely.
|
||||
CHECK(eng.activeVoiceCount() == 1);
|
||||
// At every rendered frame R - L == 0.5 exactly: both channels read the SAME frame index
|
||||
// (one read head) through the start jump and every loop wrap. A per-channel head drift would
|
||||
// break this invariant at the seam.
|
||||
for (std::size_t i = 0; i < left.size(); ++i) {
|
||||
CHECK(approx(right[i] - left[i], 0.5, 1e-4));
|
||||
}
|
||||
// Once fully inside the loop (start=10 -> reaches loop.start=20 within a handful of unity-ratio
|
||||
// frames), every L value sits in the loop band [0.20, 0.30): the shared head is sustaining the
|
||||
// loop region on both channels, never running off the sample end.
|
||||
for (std::size_t i = 15; i < left.size(); ++i) {
|
||||
CHECK(left[i] >= 0.20 - 1e-4 && left[i] < 0.30 + 1e-4);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
testChromaticSingleRoot();
|
||||
testZonedRangesBoundaries();
|
||||
@@ -815,6 +861,7 @@ int main() {
|
||||
testStereoRenderAdvancesLikeMonoRepitch();
|
||||
testStereoRenderSumsVoicesPerChannel();
|
||||
testStereoRenderNullBufferIsNoOp();
|
||||
testStereoStartFrameLoopShareOneReadHead();
|
||||
|
||||
if (g_fail == 0) {
|
||||
std::printf("all sampler_core tests passed\n");
|
||||
|
||||
Reference in New Issue
Block a user