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());
|
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() {
|
int main() {
|
||||||
testSelectByIdHit();
|
testSelectByIdHit();
|
||||||
testSelectEmptyIdIsSilence();
|
testSelectEmptyIdIsSilence();
|
||||||
@@ -1010,6 +1048,7 @@ int main() {
|
|||||||
testComponentStateV3LiftsToMono();
|
testComponentStateV3LiftsToMono();
|
||||||
testComponentStateV1V2LiftToMono();
|
testComponentStateV1V2LiftToMono();
|
||||||
testComponentStateV4TruncatedModeByte();
|
testComponentStateV4TruncatedModeByte();
|
||||||
|
testComponentStateV4StereoWithZoneOverridesRoundTrip();
|
||||||
|
|
||||||
if (g_fail == 0) std::printf("sample_map: all tests passed\n");
|
if (g_fail == 0) std::printf("sample_map: all tests passed\n");
|
||||||
return g_fail != 0;
|
return g_fail != 0;
|
||||||
|
|||||||
@@ -783,6 +783,52 @@ static void testStereoRenderNullBufferIsNoOp() {
|
|||||||
for (float v : buf) CHECK(approx(v, 0.0, 1e-9)); // untouched
|
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() {
|
int main() {
|
||||||
testChromaticSingleRoot();
|
testChromaticSingleRoot();
|
||||||
testZonedRangesBoundaries();
|
testZonedRangesBoundaries();
|
||||||
@@ -815,6 +861,7 @@ int main() {
|
|||||||
testStereoRenderAdvancesLikeMonoRepitch();
|
testStereoRenderAdvancesLikeMonoRepitch();
|
||||||
testStereoRenderSumsVoicesPerChannel();
|
testStereoRenderSumsVoicesPerChannel();
|
||||||
testStereoRenderNullBufferIsNoOp();
|
testStereoRenderNullBufferIsNoOp();
|
||||||
|
testStereoStartFrameLoopShareOneReadHead();
|
||||||
|
|
||||||
if (g_fail == 0) {
|
if (g_fail == 0) {
|
||||||
std::printf("all sampler_core tests passed\n");
|
std::printf("all sampler_core tests passed\n");
|
||||||
|
|||||||
Reference in New Issue
Block a user