// component_state_io unit tests (Q-W2v). The HISTORICAL codec suite — the full // envelope/payload version ladder, every legacy lift, the golden byte fixtures — // lives in test_sample_map.cpp and runs unmodified against the split module; this // target exists as the module's OWN executable (house rule: every pure module has // one) and as the STRUCTURAL PROOF the codec links WITHOUT the voice engine // (T2-07): it links component_state_io + velocity_curve + master_gain only — a // sampler_core/pitch_shift symbol reaching this link is a regression. #include "../src/core/instrument/map/component_state_io.h" #include #include #include using namespace reasampler; using namespace reasampler::instrument::map; static int failures = 0; #define CHECK(cond) \ do { \ if (!(cond)) { \ std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \ ++failures; \ } \ } while (0) // A full round-trip through the CURRENT envelope (v11): every field survives. static void testComponentStateRoundTrip() { ComponentState in; in.selectionId = "smp-1"; in.channelMode = ChannelMode::Stereo; in.channelModeExplicit = true; in.lastConsumedAssignGeneration = 42; in.previewVelocity = 99; in.voiceCount = 7; in.voiceMode = VoiceMode::Mono; in.monoTrigger = MonoTrigger::Legato; in.masterGainLinear = 0.5; in.instanceGuid = "0123456789abcdef0123456789abcdef"; SampleRefEntry e; e.sampleId = "smp-1"; e.ref.relativePath = "bank/smp-1.wav"; e.ref.rootNote = 64; e.ref.loop.hasLoop = true; e.ref.loop.start = 100; e.ref.loop.end = 2000; e.ref.channelCount = 2; e.displayName = "My Capture"; in.sampleRefs.push_back(e); PerformanceZone z; z.sampleId = "smp-1"; z.lowNote = 30; z.highNote = 90; z.rootOverride = 61; z.startPoint = 5; z.keyTrack = 1.5; z.play.playMode = PlayMode::Trigger; z.play.trigger.lengthFraction = 0.75; z.play.trigger.fadeInFrames = 441; z.play.trigger.fadeOutFrames = 882; in.map.zones.push_back(z); const std::vector bytes = serializeComponentState(in); const ComponentState out = deserializeComponentState(bytes, 48000.0); CHECK(out.selectionId == "smp-1"); CHECK(out.channelMode == ChannelMode::Stereo); CHECK(out.channelModeExplicit); CHECK(out.lastConsumedAssignGeneration == 42); CHECK(out.previewVelocity == 99); CHECK(out.voiceCount == 7); CHECK(out.voiceMode == VoiceMode::Mono); CHECK(out.monoTrigger == MonoTrigger::Legato); CHECK(out.masterGainLinear == 0.5); CHECK(out.instanceGuid == "0123456789abcdef0123456789abcdef"); CHECK(out.sampleRefs.size() == 1); if (out.sampleRefs.size() == 1) { CHECK(out.sampleRefs[0].sampleId == "smp-1"); CHECK(out.sampleRefs[0].ref.relativePath == "bank/smp-1.wav"); CHECK(out.sampleRefs[0].ref.rootNote == 64); CHECK(out.sampleRefs[0].ref.loop.hasLoop); CHECK(out.sampleRefs[0].ref.loop.start == 100); CHECK(out.sampleRefs[0].ref.loop.end == 2000); CHECK(out.sampleRefs[0].ref.channelCount == 2); CHECK(out.sampleRefs[0].displayName == "My Capture"); } CHECK(out.map.zones.size() == 1); if (out.map.zones.size() == 1) { const PerformanceZone& oz = out.map.zones[0]; CHECK(oz.sampleId == "smp-1"); CHECK(oz.lowNote == 30); CHECK(oz.highNote == 90); CHECK(oz.rootOverride && *oz.rootOverride == 61); CHECK(oz.startPoint && *oz.startPoint == 5); CHECK(oz.keyTrack == 1.5); CHECK(oz.play.playMode == PlayMode::Trigger); CHECK(oz.play.trigger.lengthFraction == 0.75); CHECK(oz.play.trigger.fadeInFrames == 441); CHECK(oz.play.trigger.fadeOutFrames == 882); } } // GOLDEN FULL-BLOB FIXTURE (reviewer follow-up, Q-W2v). testEnvelopePrefixBytesFrozen below // only pins the first 5 bytes of a near-EMPTY blob; it cannot catch a drift anywhere past the // mode byte (a field re-ordered or dropped inside the voice/gain/refs/guid/zone tail would // still pass it). This test builds a canonical v11 ComponentState that exercises EVERY field // family at once (two zones — one Trigger with every optional override set, one Gate with all // optionals absent — a two-entry sample-refs table, non-default voice/gain/channel-mode // fields, and a non-flat velocity curve) and asserts the encoded bytes equal an EXACT expected // vector. The vector below is the current writer's PROVABLY-CORRECT output (proven by the // round-trip test above) captured as the golden — so the byte layout itself becomes // un-driftable, not just its first 5 bytes. static void testGoldenFullBlobFixture() { ComponentState in; in.selectionId = "kick"; in.channelMode = ChannelMode::Stereo; in.channelModeExplicit = true; in.lastConsumedAssignGeneration = 12345; in.previewVelocity = 100; in.voiceCount = 24; in.voiceMode = VoiceMode::Mono; in.monoTrigger = MonoTrigger::Legato; in.masterGainLinear = 2.0; in.instanceGuid = "guid-1234-5678-abcd"; SampleRefEntry kickRef; kickRef.sampleId = "kick"; kickRef.ref.relativePath = "bank/kick.wav"; kickRef.ref.rootNote = 36; kickRef.ref.loop.hasLoop = true; kickRef.ref.loop.start = 1000; kickRef.ref.loop.end = 5000; kickRef.ref.channelCount = 2; kickRef.displayName = "Kick Drum"; in.sampleRefs.push_back(kickRef); SampleRefEntry snareRef; snareRef.sampleId = "snare"; snareRef.ref.relativePath = "bank/snare.wav"; snareRef.ref.rootNote = 38; snareRef.ref.loop.hasLoop = false; snareRef.ref.loop.start = 0; snareRef.ref.loop.end = 0; snareRef.ref.channelCount = 1; snareRef.displayName = "Snare"; in.sampleRefs.push_back(snareRef); // Zone A: every optional field present, Trigger mode, non-flat velocity curve. PerformanceZone zoneA; zoneA.sampleId = "kick"; zoneA.lowNote = 24; zoneA.highNote = 60; zoneA.rootOverride = 36; SampleLoop loopA; loopA.hasLoop = true; loopA.start = 1000; loopA.end = 5000; zoneA.loopOverride = loopA; zoneA.startPoint = 250; zoneA.keyTrack = 0.5; zoneA.velocityCurve = reasampler::instrument::engine::VelocityCurve::fromPoints( {VelocityPoint{0.0, 0.2}, VelocityPoint{64.0, 0.6}, VelocityPoint{127.0, 1.0}}); zoneA.play.playMode = PlayMode::Trigger; zoneA.play.adsr.attackSeconds = 0.01; zoneA.play.adsr.holdSeconds = 0.05; zoneA.play.adsr.decaySeconds = 0.02; zoneA.play.adsr.sustainLevel = 0.8; zoneA.play.adsr.releaseSeconds = 0.15; zoneA.play.trigger.lengthFraction = 0.75; zoneA.play.trigger.fadeInFrames = 100; zoneA.play.trigger.fadeOutFrames = 200; zoneA.play.pitchEngine = PitchEngine::Preserve; zoneA.play.pitchEnv.enabled = true; zoneA.play.pitchEnv.attackSeconds = 0.02; zoneA.play.pitchEnv.decaySeconds = 0.03; zoneA.play.pitchEnv.peakSemitones = 5.0; in.map.zones.push_back(zoneA); // Zone B: every optional field absent, Gate mode, default flat velocity curve. PerformanceZone zoneB; zoneB.sampleId = "snare"; zoneB.lowNote = 61; zoneB.highNote = 90; zoneB.keyTrack = 2.0; zoneB.play.playMode = PlayMode::Gate; zoneB.play.adsr.attackSeconds = 0.005; zoneB.play.adsr.holdSeconds = 0.0; zoneB.play.adsr.decaySeconds = 0.1; zoneB.play.adsr.sustainLevel = 0.5; zoneB.play.adsr.releaseSeconds = 0.2; zoneB.play.pitchEngine = PitchEngine::Varispeed; in.map.zones.push_back(zoneB); const std::vector bytes = serializeComponentState(in); // clang-format off static const std::uint8_t kGolden[] = { 0x0b,0x00,0x00,0x00,0x01,0x39,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x64,0x18,0x01, 0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x01,0x02,0x00,0x00,0x00,0x04,0x00, 0x00,0x00,0x6b,0x69,0x63,0x6b,0x0d,0x00,0x00,0x00,0x62,0x61,0x6e,0x6b,0x2f,0x6b, 0x69,0x63,0x6b,0x2e,0x77,0x61,0x76,0x24,0x00,0x00,0x00,0x01,0xe8,0x03,0x00,0x00, 0x00,0x00,0x00,0x00,0x88,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x00,0x00, 0x09,0x00,0x00,0x00,0x4b,0x69,0x63,0x6b,0x20,0x44,0x72,0x75,0x6d,0x05,0x00,0x00, 0x00,0x73,0x6e,0x61,0x72,0x65,0x0e,0x00,0x00,0x00,0x62,0x61,0x6e,0x6b,0x2f,0x73, 0x6e,0x61,0x72,0x65,0x2e,0x77,0x61,0x76,0x26,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00, 0x00,0x05,0x00,0x00,0x00,0x53,0x6e,0x61,0x72,0x65,0x13,0x00,0x00,0x00,0x67,0x75, 0x69,0x64,0x2d,0x31,0x32,0x33,0x34,0x2d,0x35,0x36,0x37,0x38,0x2d,0x61,0x62,0x63, 0x64,0x04,0x00,0x00,0x00,0x6b,0x69,0x63,0x6b,0x00,0xff,0xff,0xff,0x07,0x00,0x00, 0x00,0x02,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x6b,0x69,0x63,0x6b,0x18,0x00,0x00, 0x00,0x3c,0x00,0x00,0x00,0x01,0x24,0x00,0x00,0x00,0x01,0x01,0xe8,0x03,0x00,0x00, 0x00,0x00,0x00,0x00,0x88,0x13,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0xfa,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x01,0x9a,0x99,0x99,0x99,0x99,0x99,0xa9,0x3f,0x00,0x00, 0x00,0x00,0x00,0x00,0xe8,0x3f,0x64,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xc8,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x01,0x7b,0x14,0xae,0x47,0xe1,0x7a,0x94,0x3f, 0xb8,0x1e,0x85,0xeb,0x51,0xb8,0x9e,0x3f,0x00,0x00,0x00,0x00,0x00,0x00,0x14,0x40, 0x7b,0x14,0xae,0x47,0xe1,0x7a,0x84,0x3f,0x7b,0x14,0xae,0x47,0xe1,0x7a,0x94,0x3f, 0x9a,0x99,0x99,0x99,0x99,0x99,0xe9,0x3f,0x33,0x33,0x33,0x33,0x33,0x33,0xc3,0x3f, 0x00,0x00,0x00,0x00,0x00,0x00,0xe0,0x3f,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x9a,0x99,0x99,0x99,0x99,0x99,0xc9,0x3f,0x00,0x00,0x00,0x00, 0x00,0x00,0x50,0x40,0x33,0x33,0x33,0x33,0x33,0x33,0xe3,0x3f,0x00,0x00,0x00,0x00, 0x00,0xc0,0x5f,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0xf0,0x3f,0x05,0x00,0x00,0x00, 0x73,0x6e,0x61,0x72,0x65,0x3d,0x00,0x00,0x00,0x5a,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xf0, 0x3f,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7b,0x14,0xae,0x47,0xe1, 0x7a,0x74,0x3f,0x9a,0x99,0x99,0x99,0x99,0x99,0xb9,0x3f,0x00,0x00,0x00,0x00,0x00, 0x00,0xe0,0x3f,0x9a,0x99,0x99,0x99,0x99,0x99,0xc9,0x3f,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x40,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0xf0,0x3f,0x00,0x00,0x00,0x00,0x00,0xc0,0x5f,0x40,0x00, 0x00,0x00,0x00,0x00,0x00,0xf0,0x3f, }; // clang-format on CHECK(bytes.size() == sizeof(kGolden)); if (bytes.size() == sizeof(kGolden)) { bool same = true; for (std::size_t i = 0; i < bytes.size(); ++i) { if (bytes[i] != kGolden[i]) { same = false; break; } } CHECK(same); } } // The FROZEN envelope prefix: version tag v11 LE, then the mode byte — a drift in // either is a byte-format break the round-trip alone can't prove (both sides could // drift together). Pins the writer's absolute bytes. static void testEnvelopePrefixBytesFrozen() { ComponentState in; // defaults: mono, implicit, no refs, no selection, no zones const std::vector bytes = serializeComponentState(in); CHECK(bytes.size() > 5); if (bytes.size() > 5) { CHECK(bytes[0] == 11 && bytes[1] == 0 && bytes[2] == 0 && bytes[3] == 0); CHECK(bytes[4] == 0); // ChannelMode::Mono } CHECK(kComponentStateVersion == 11); CHECK(kZonesPayloadVersion == 7); CHECK(kZonesFormatMarker == 0xFFFFFF00u); } // A v1 selection blob lifts to {id, one full-keyboard zone} — the oldest live lift. static void testV1SelectionLift() { const std::vector v1 = serializeSelection("old-pick"); const ComponentState out = deserializeComponentState(v1, 48000.0); CHECK(out.selectionId == "old-pick"); CHECK(out.map.zones.size() == 1); if (out.map.zones.size() == 1) { CHECK(out.map.zones[0].sampleId == "old-pick"); CHECK(out.map.zones[0].lowNote == 0); CHECK(out.map.zones[0].highNote == 127); } } // Truncation degrades to a partial/empty parse — never out-of-bounds, never throws. static void testTruncationDegradesCleanly() { ComponentState in; in.selectionId = "smp-2"; PerformanceZone z; z.sampleId = "smp-2"; in.map.zones.push_back(z); const std::vector bytes = serializeComponentState(in); for (std::size_t cut = 0; cut < bytes.size(); ++cut) { const std::vector part(bytes.begin(), bytes.begin() + static_cast(cut)); const ComponentState out = deserializeComponentState(part, 48000.0); (void)out; // reaching here without UB/throw is the contract under test } CHECK(true); } // serializePerformance/deserializePerformance round-trip through the v2 envelope. static void testPerformanceRoundTrip() { PerformanceMap in; PerformanceZone z; z.sampleId = "zone-a"; z.lowNote = 10; z.highNote = 20; in.zones.push_back(z); const PerformanceMap out = deserializePerformance(serializePerformance(in), 48000.0); CHECK(out.zones.size() == 1); if (out.zones.size() == 1) { CHECK(out.zones[0].sampleId == "zone-a"); CHECK(out.zones[0].lowNote == 10); CHECK(out.zones[0].highNote == 20); } } int main() { testComponentStateRoundTrip(); testGoldenFullBlobFixture(); testEnvelopePrefixBytesFrozen(); testV1SelectionLift(); testTruncationDegradesCleanly(); testPerformanceRoundTrip(); if (failures == 0) { std::printf("component_state_io_tests: all tests passed\n"); return 0; } std::printf("component_state_io_tests: %d FAILURE(S)\n", failures); return 1; }