// component_state_io unit tests — the codec's whole suite: the current envelope + params // payload round-trip, the frozen prefix bytes, the ENVELOPE ladder (v1..v11) with each // version's documented lift, and the RETIRED-ZONE-PAYLOAD migration ladder (payload v1..v7 // -> the one parameter set, adopting zone one). The codec's own executable is also the // STRUCTURAL PROOF it links WITHOUT the voice engine: it links component_state_io + // velocity_curve + master_gain only, so a sampler_core/pitch_shift symbol reaching this // link is a regression. #include "../src/core/instrument/map/component_state_io.h" #include "../src/core/instrument/engine/master_gain.h" // masterGainMaxLinear (the v8 wire cap) #include #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 writer for the RETIRED zone-list payloads ----------------------------- // // The shipping codec no longer EMITS a zone list, so the migration ladder can only be // tested against bytes this suite lays out itself. These helpers mirror the frozen v1..v7 // record shapes documented in component_state_io.h; if they and the reader ever disagree, // the migration tests below fail — which is the point. namespace legacy { static void u8v(std::vector& out, std::uint8_t v) { out.push_back(v); } static void u32v(std::vector& out, std::uint32_t v) { for (int i = 0; i < 4; ++i) out.push_back(static_cast((v >> (8 * i)) & 0xFF)); } static void i64v(std::vector& out, std::int64_t v) { const auto u = static_cast(v); for (int i = 0; i < 8; ++i) out.push_back(static_cast((u >> (8 * i)) & 0xFF)); } static void f64v(std::vector& out, double v) { std::uint64_t bits = 0; std::memcpy(&bits, &v, sizeof(bits)); for (int i = 0; i < 8; ++i) out.push_back(static_cast((bits >> (8 * i)) & 0xFF)); } static void strv(std::vector& out, const std::string& s) { u32v(out, static_cast(s.size())); out.insert(out.end(), s.begin(), s.end()); } // One zone's worth of the retired per-zone record, in the v7 (fullest) shape. struct Zone { std::string sampleId; int lowNote = 0; int highNote = 127; int rootOverride = -1; // < 0 = absent bool hasLoopOverride = false; std::int64_t loopStart = 0; std::int64_t loopEnd = 0; std::int64_t startPoint = -1; // < 0 = absent bool trigger = false; double holdSeconds = 0.0; double lengthFraction = 1.0; std::int64_t fadeIn = 0; std::int64_t fadeOut = 0; bool preserve = false; bool pitchEnvEnabled = false; double pitchAttack = 0.0; double pitchDecay = 0.0; double peakSemis = 0.0; double attackSeconds = 0.003; double decaySeconds = 0.0; double sustainLevel = 1.0; double releaseSeconds = 0.060; double keyTrack = 1.0; std::vector curve; // empty -> the flat endpoints }; static void putZone(std::vector& out, const Zone& z, std::uint32_t pv) { strv(out, z.sampleId); u32v(out, static_cast(z.lowNote)); u32v(out, static_cast(z.highNote)); u8v(out, z.rootOverride >= 0 ? 1 : 0); if (z.rootOverride >= 0) u32v(out, static_cast(z.rootOverride)); if (pv >= 2) { u8v(out, z.hasLoopOverride ? 1 : 0); if (z.hasLoopOverride) { u8v(out, 1); i64v(out, z.loopStart); i64v(out, z.loopEnd); } u8v(out, z.startPoint >= 0 ? 1 : 0); if (z.startPoint >= 0) i64v(out, z.startPoint); } if (pv >= 5) { u8v(out, z.trigger ? 1 : 0); f64v(out, z.holdSeconds); f64v(out, z.lengthFraction); i64v(out, z.fadeIn); i64v(out, z.fadeOut); u8v(out, z.preserve ? 1 : 0); u8v(out, z.pitchEnvEnabled ? 1 : 0); f64v(out, z.pitchAttack); f64v(out, z.pitchDecay); f64v(out, z.peakSemis); f64v(out, z.attackSeconds); f64v(out, z.decaySeconds); f64v(out, z.sustainLevel); f64v(out, z.releaseSeconds); } if (pv >= 6) f64v(out, z.keyTrack); if (pv >= 7) { const std::vector pts = z.curve.empty() ? std::vector{{0.0, 1.0}, {127.0, 1.0}} : z.curve; u32v(out, static_cast(pts.size())); for (const VelocityPoint& p : pts) { f64v(out, p.velocity); f64v(out, p.amp); } } } // The envelope fields, in wire order. A builder at version N emits only the prefix fields // version N carried, so each lift can be asserted against a blob shaped exactly as that // version's writer produced. struct Envelope { std::uint32_t version = kComponentStateVersion; std::uint8_t modeByte = 0; // v4+ 0 mono / 1 stereo std::int64_t assignGeneration = 0; // v5+ std::uint8_t previewVelocity = kPreviewVelocityDefault; // v6+ std::uint8_t voiceCount = static_cast(kDefaultVoiceCount); // v7+ std::uint8_t voiceMode = 0; // v7+ 0 poly / 1 mono std::uint8_t monoTrigger = 0; // v7+ 0 retrigger / 1 legato double masterGain = 1.0; // v8+ std::uint8_t channelModeExplicit = 0; // v9+ std::string instanceGuid; // v11+ std::string selectionId; // v3+ }; // A complete envelope at `env.version` whose tail is a RETIRED zone-list payload at version // `pv`. `env.selectionId` is the envelope's own stored pick — which the adoption rule // overrides when the payload carries a zone. The sample-refs table (v10+) is always empty: // its own shape is covered by the round-trip test. static std::vector envelopeWithZones(const Envelope& env, const std::vector& zones, std::uint32_t pv) { std::vector out; const std::uint32_t v = env.version; u32v(out, v); if (v >= 4) u8v(out, env.modeByte); if (v >= 5) i64v(out, env.assignGeneration); if (v >= 6) u8v(out, env.previewVelocity); if (v >= 7) { u8v(out, env.voiceCount); u8v(out, env.voiceMode); u8v(out, env.monoTrigger); } if (v >= 8) f64v(out, env.masterGain); if (v >= 9) u8v(out, env.channelModeExplicit); if (v >= 10) u32v(out, 0); // sample-refs: empty table if (v >= 11) strv(out, env.instanceGuid); if (v >= 3) strv(out, env.selectionId); // v2 was zones-only, no selection if (pv >= 2) { u32v(out, kParamsFormatMarker); u32v(out, pv); } u32v(out, static_cast(zones.size())); for (const Zone& z : zones) putZone(out, z, pv); return out; } // Shorthand for the common case: the CURRENT envelope version carrying a zone payload. static std::vector envelopeWithZones(const std::string& selectionId, const std::vector& zones, std::uint32_t pv) { Envelope env; env.selectionId = selectionId; return envelopeWithZones(env, zones, pv); } } // namespace legacy // --- The current format ------------------------------------------------------- // A full round-trip through the CURRENT envelope (v11) + params payload (v8): every field // survives. This is the "one parameter set round-trips save/reload intact" contract. 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); in.params.rootOverride = 61; SampleLoop lp; lp.hasLoop = true; lp.start = 7; lp.end = 900; in.params.loopOverride = lp; in.params.startPoint = 5; in.params.keyTrack = 1.5; in.params.velocityCurve = reasampler::instrument::engine::VelocityCurve::fromPoints( {VelocityPoint{0.0, 0.2}, VelocityPoint{64.0, 0.6}, VelocityPoint{127.0, 1.0}}); in.params.play.playMode = PlayMode::Trigger; in.params.play.adsr.attackSeconds = 0.01; in.params.play.adsr.holdSeconds = 0.05; in.params.play.adsr.decaySeconds = 0.02; in.params.play.adsr.sustainLevel = 0.8; in.params.play.adsr.releaseSeconds = 0.15; in.params.play.trigger.lengthFraction = 0.75; in.params.play.trigger.fadeInFrames = 441; in.params.play.trigger.fadeOutFrames = 882; in.params.play.pitchEngine = PitchEngine::Preserve; in.params.play.pitchEnv.enabled = true; in.params.play.pitchEnv.attackSeconds = 0.02; in.params.play.pitchEnv.decaySeconds = 0.03; in.params.play.pitchEnv.peakSemitones = 5.0; 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"); } const InstrumentParams& p = out.params; CHECK(p.rootOverride && *p.rootOverride == 61); CHECK(p.loopOverride && p.loopOverride->hasLoop); CHECK(p.loopOverride && p.loopOverride->start == 7 && p.loopOverride->end == 900); CHECK(p.startPoint && *p.startPoint == 5); CHECK(p.keyTrack == 1.5); CHECK(p.velocityCurve.points().size() == 3); CHECK(p.play.playMode == PlayMode::Trigger); CHECK(p.play.adsr.attackSeconds == 0.01); CHECK(p.play.adsr.holdSeconds == 0.05); CHECK(p.play.adsr.decaySeconds == 0.02); CHECK(p.play.adsr.sustainLevel == 0.8); CHECK(p.play.adsr.releaseSeconds == 0.15); CHECK(p.play.trigger.lengthFraction == 0.75); CHECK(p.play.trigger.fadeInFrames == 441); CHECK(p.play.trigger.fadeOutFrames == 882); CHECK(p.play.pitchEngine == PitchEngine::Preserve); CHECK(p.play.pitchEnv.enabled); CHECK(p.play.pitchEnv.attackSeconds == 0.02); CHECK(p.play.pitchEnv.decaySeconds == 0.03); CHECK(p.play.pitchEnv.peakSemitones == 5.0); } // A DEFAULT parameter set must round-trip to defaults — the "no pick, nothing configured" // blob restores as the silent empty state, not as a set of accidental values. static void testDefaultStateRoundTripsToDefaults() { const ComponentState out = deserializeComponentState(serializeComponentState(ComponentState{}), 48000.0); CHECK(out.selectionId.empty()); CHECK(!out.params.rootOverride); CHECK(!out.params.loopOverride); CHECK(!out.params.startPoint); CHECK(out.params.keyTrack == 1.0); CHECK(out.params.play.playMode == PlayMode::Gate); CHECK(out.params.play.pitchEngine == kDefaultPitchEngine); CHECK(!out.params.play.pitchEnv.enabled); CHECK(out.params.play.adsr.attackSeconds == AdsrSeconds{}.attackSeconds); CHECK(out.params.play.adsr.releaseSeconds == AdsrSeconds{}.releaseSeconds); } // 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). // Also pins the payload version + marker as SEMANTIC constants, so a bump has to be // deliberate rather than incidental. static void testEnvelopePrefixBytesFrozen() { ComponentState in; // defaults: mono, implicit, no refs, no selection, default params 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(kParamsPayloadVersion == 8); CHECK(kParamsFormatMarker == 0xFFFFFF00u); } // The WRITER emits the CURRENT payload version, and the marker + version sit at the head of // the payload — the self-describing property every legacy branch depends on. Asserted // against the semantic constants, not literals. static void testWriterEmitsCurrentPayloadVersion() { ComponentState in; in.selectionId = "id"; const std::vector bytes = serializeComponentState(in); // Scan for the marker; the four bytes after it are the payload version. bool found = false; for (std::size_t i = 0; i + 8 <= bytes.size(); ++i) { const std::uint32_t m = static_cast(bytes[i]) | (static_cast(bytes[i + 1]) << 8) | (static_cast(bytes[i + 2]) << 16) | (static_cast(bytes[i + 3]) << 24); if (m != kParamsFormatMarker) continue; const std::uint32_t v = static_cast(bytes[i + 4]) | (static_cast(bytes[i + 5]) << 8) | (static_cast(bytes[i + 6]) << 16) | (static_cast(bytes[i + 7]) << 24); CHECK(v == kParamsPayloadVersion); found = true; break; } CHECK(found); } // --- The retired-zone-payload migration ladder -------------------------------- // SINGLE-ZONE LIFT IS LOSSLESS: a single-capture instance saved under the zone model // restores with the same capture, the same root, and the same parameters. static void testSingleZoneMigrationIsLossless() { legacy::Zone z; z.sampleId = "kick"; z.lowNote = 0; z.highNote = 127; z.rootOverride = 36; z.hasLoopOverride = true; z.loopStart = 1000; z.loopEnd = 5000; z.startPoint = 250; z.trigger = true; z.holdSeconds = 0.05; z.lengthFraction = 0.75; z.fadeIn = 100; z.fadeOut = 200; z.preserve = true; z.pitchEnvEnabled = true; z.pitchAttack = 0.02; z.pitchDecay = 0.03; z.peakSemis = 5.0; z.attackSeconds = 0.01; z.decaySeconds = 0.02; z.sustainLevel = 0.8; z.releaseSeconds = 0.15; z.keyTrack = 0.5; z.curve = {VelocityPoint{0.0, 0.2}, VelocityPoint{64.0, 0.6}, VelocityPoint{127.0, 1.0}}; const ComponentState out = deserializeComponentState(legacy::envelopeWithZones("kick", {z}, 7), 48000.0); CHECK(out.selectionId == "kick"); // same capture const InstrumentParams& p = out.params; CHECK(p.rootOverride && *p.rootOverride == 36); // same root CHECK(p.loopOverride && p.loopOverride->start == 1000 && p.loopOverride->end == 5000); CHECK(p.startPoint && *p.startPoint == 250); CHECK(p.keyTrack == 0.5); CHECK(p.velocityCurve.points().size() == 3); CHECK(p.play.playMode == PlayMode::Trigger); CHECK(p.play.adsr.attackSeconds == 0.01); CHECK(p.play.adsr.holdSeconds == 0.05); CHECK(p.play.adsr.decaySeconds == 0.02); CHECK(p.play.adsr.sustainLevel == 0.8); CHECK(p.play.adsr.releaseSeconds == 0.15); CHECK(p.play.trigger.lengthFraction == 0.75); CHECK(p.play.trigger.fadeInFrames == 100); CHECK(p.play.trigger.fadeOutFrames == 200); CHECK(p.play.pitchEngine == PitchEngine::Preserve); CHECK(p.play.pitchEnv.enabled); CHECK(p.play.pitchEnv.attackSeconds == 0.02); CHECK(p.play.pitchEnv.decaySeconds == 0.03); CHECK(p.play.pitchEnv.peakSemitones == 5.0); } // A lifted single-zone instance RE-SAVES in the current format and survives a second // round-trip unchanged — the lift is a one-way door, not a per-open re-derivation. static void testLiftedStateReSavesInCurrentFormat() { legacy::Zone z; z.sampleId = "kick"; z.rootOverride = 36; z.keyTrack = 0.5; z.releaseSeconds = 0.4; const ComponentState lifted = deserializeComponentState(legacy::envelopeWithZones("kick", {z}, 7), 48000.0); const ComponentState again = deserializeComponentState(serializeComponentState(lifted), 48000.0); CHECK(again.selectionId == "kick"); CHECK(again.params.rootOverride && *again.params.rootOverride == 36); CHECK(again.params.keyTrack == 0.5); CHECK(again.params.play.adsr.releaseSeconds == 0.4); } // MULTI-ZONE LIFT ADOPTS ZONE ONE: its capture AND its parameters win; every later zone // drops. No error, no empty state. static void testMultiZoneMigrationAdoptsFirstZone() { legacy::Zone first; first.sampleId = "kick"; first.lowNote = 0; first.highNote = 59; first.rootOverride = 36; first.keyTrack = 0.5; first.releaseSeconds = 0.4; legacy::Zone second; second.sampleId = "snare"; second.lowNote = 60; second.highNote = 127; second.rootOverride = 38; second.keyTrack = 2.0; second.releaseSeconds = 0.9; legacy::Zone third; third.sampleId = "hat"; third.rootOverride = 42; const ComponentState out = deserializeComponentState( legacy::envelopeWithZones("", {first, second, third}, 7), 48000.0); CHECK(out.selectionId == "kick"); // zone one's capture CHECK(out.params.rootOverride && *out.params.rootOverride == 36); CHECK(out.params.keyTrack == 0.5); // zone one's parameters CHECK(out.params.play.adsr.releaseSeconds == 0.4); // Zones two and three left no trace anywhere. CHECK(out.selectionId != "snare" && out.selectionId != "hat"); CHECK(out.params.keyTrack != 2.0); } // The FIRST zone supersedes the envelope's own stored selection — that zone is what // first-match resolve actually played, so adopting it is what keeps the sound identical. static void testFirstZoneSupersedesStoredSelection() { legacy::Zone z; z.sampleId = "actually-playing"; const ComponentState out = deserializeComponentState( legacy::envelopeWithZones("stale-selection", {z}, 7), 48000.0); CHECK(out.selectionId == "actually-playing"); } // An EMPTY zone list leaves the envelope's selection alone (a picked-but-never-edited // instance) and yields default parameters. static void testEmptyZoneListKeepsTheStoredSelection() { const ComponentState out = deserializeComponentState(legacy::envelopeWithZones("picked", {}, 7), 48000.0); CHECK(out.selectionId == "picked"); CHECK(!out.params.rootOverride); CHECK(out.params.keyTrack == 1.0); } // EVERY older payload version takes the migration path, and each lifts the fields its own // shape carries while defaulting the ones it predates. static void testEveryOlderPayloadVersionMigrates() { for (std::uint32_t pv : {1u, 2u, 5u, 6u, 7u}) { legacy::Zone z; z.sampleId = "kick"; z.rootOverride = 36; z.keyTrack = 0.5; z.releaseSeconds = 0.4; const ComponentState out = deserializeComponentState(legacy::envelopeWithZones("", {z}, pv), 48000.0); CHECK(out.selectionId == "kick"); // every version CHECK(out.params.rootOverride && *out.params.rootOverride == 36); // v1 onward // keyTrack arrived at v6; older payloads lift to 100% ET (bit-identical repitch). CHECK(out.params.keyTrack == (pv >= 6 ? 0.5 : 1.0)); // The full A/D/S/R tail arrived at v5; older payloads keep the tier-0 defaults. CHECK(out.params.play.adsr.releaseSeconds == (pv >= 5 ? 0.4 : AdsrSeconds{}.releaseSeconds)); } // And the CURRENT version does NOT take the migration path: it reads its own record. CHECK(kParamsPayloadVersion == 8); } // The LEGACY v3 payload's wall-clock frame counts convert to seconds at the READ boundary // using the project rate threaded in — no baked constant. static void testLegacyV3FramesConvertAtTheProjectRate() { // v3's own tail shape differs from v5's, so lay it out directly here. std::vector out; legacy::u32v(out, kComponentStateVersion); legacy::u8v(out, 0); legacy::i64v(out, 0); legacy::u8v(out, kPreviewVelocityDefault); legacy::u8v(out, static_cast(kDefaultVoiceCount)); legacy::u8v(out, 0); legacy::u8v(out, 0); legacy::f64v(out, 1.0); legacy::u8v(out, 0); legacy::u32v(out, 0); legacy::strv(out, ""); legacy::strv(out, ""); legacy::u32v(out, kParamsFormatMarker); legacy::u32v(out, 3); legacy::u32v(out, 1); // one zone legacy::strv(out, "kick"); legacy::u32v(out, 0); // lowNote legacy::u32v(out, 127); // highNote legacy::u8v(out, 0); // no root override legacy::u8v(out, 0); // no loop override legacy::u8v(out, 0); // no start point legacy::u8v(out, 0); // playMode: Gate legacy::i64v(out, 2400); // holdFrames -> 0.05 s at 48 kHz legacy::f64v(out, 1.0); // lengthFraction legacy::i64v(out, 0); // fadeIn legacy::i64v(out, 0); // fadeOut legacy::u8v(out, 1); // pitchEngine: Preserve legacy::u8v(out, 1); // pitchEnv enabled legacy::i64v(out, 960); // pitchEnv attackFrames -> 0.02 s legacy::i64v(out, 1440); // pitchEnv decayFrames -> 0.03 s legacy::f64v(out, 5.0); // peakSemitones const ComponentState st = deserializeComponentState(out, 48000.0); CHECK(st.selectionId == "kick"); CHECK(st.params.play.adsr.holdSeconds == 0.05); CHECK(st.params.play.pitchEnv.attackSeconds == 0.02); CHECK(st.params.play.pitchEnv.decaySeconds == 0.03); CHECK(st.params.play.pitchEnv.peakSemitones == 5.0); // A/D/S/R are absent in v3 -> the tier-0 seconds defaults hold. CHECK(st.params.play.adsr.attackSeconds == AdsrSeconds{}.attackSeconds); CHECK(st.params.play.adsr.releaseSeconds == AdsrSeconds{}.releaseSeconds); // The SAME bytes at a different project rate convert to different seconds — proof the // rate is a read-time parameter, not a baked constant. const ComponentState at96k = deserializeComponentState(out, 96000.0); CHECK(at96k.params.play.adsr.holdSeconds == 0.025); } // --- The ENVELOPE ladder (v2..v11) ------------------------------------------- // EVERY envelope version restores the fields it carried and lifts the ones it predates to // their documented defaults. One table over the whole ladder, so a new envelope field // cannot be added without deciding what each older version lifts it to. static void testEnvelopeLadderLiftsEachVersion() { for (std::uint32_t v : {3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u, 11u}) { legacy::Envelope env; env.version = v; env.selectionId = "kick"; env.modeByte = 1; // stereo env.assignGeneration = 4242; env.previewVelocity = 99; env.voiceCount = 7; env.voiceMode = 1; // mono env.monoTrigger = 1; // legato env.masterGain = 0.5; env.channelModeExplicit = 1; env.instanceGuid = "guid-abc"; const ComponentState out = deserializeComponentState(legacy::envelopeWithZones(env, {}, 7), 48000.0); CHECK(out.selectionId == "kick"); // v3 onward all carry the selection // v4 added the channel mode; older blobs lift to MONO. CHECK(out.channelMode == (v >= 4 ? ChannelMode::Stereo : ChannelMode::Mono)); // v5 added the consumed-assignment marker; older blobs lift to 0, so a genuinely // new first assign (generation >= 1) still applies to a pre-marker instance. CHECK(out.lastConsumedAssignGeneration == (v >= 5 ? 4242 : 0)); // v6 added the preview velocity; older blobs lift to the mid default. CHECK(out.previewVelocity == (v >= 6 ? 99 : kPreviewVelocityDefault)); // v7 added the voice system; older blobs lift to {16, Poly, Retrigger} — the // pre-voice-system behavior, byte-identically. CHECK(out.voiceCount == (v >= 7 ? 7 : kDefaultVoiceCount)); CHECK(out.voiceMode == (v >= 7 ? VoiceMode::Mono : VoiceMode::Poly)); CHECK(out.monoTrigger == (v >= 7 ? MonoTrigger::Legato : MonoTrigger::Retrigger)); // v8 added the master gain; older blobs lift to unity. CHECK(out.masterGainLinear == (v >= 8 ? 0.5 : 1.0)); // v9 added the channel-mode EXPLICIT flag; older blobs lift to implicit, so the // auto-default may follow the loaded capture. CHECK(out.channelModeExplicit == (v >= 9 ? true : false)); // v10 added the refs table (always empty here), v11 the instance guid; a pre-v11 // blob lifts to an empty guid, which the processor mints on first publish. CHECK(out.instanceGuid == (v >= 11 ? "guid-abc" : "")); CHECK(out.sampleRefs.empty()); } } // A v2 blob is ZONES-ONLY — no stored selection at all — so the adopted first zone supplies // BOTH the capture and the parameters. static void testV2ZonesOnlyBlobAdoptsBothFromZoneOne() { legacy::Zone z; z.sampleId = "kick"; z.rootOverride = 36; std::vector out; legacy::u32v(out, kPerformanceStateVersion); // == 2, the zones-only envelope legacy::u32v(out, kParamsFormatMarker); legacy::u32v(out, 7); legacy::u32v(out, 1); legacy::putZone(out, z, 7); const ComponentState st = deserializeComponentState(out, 48000.0); CHECK(st.selectionId == "kick"); CHECK(st.params.rootOverride && *st.params.rootOverride == 36); CHECK(st.channelMode == ChannelMode::Mono); // a v2 blob predates the mode byte } // A CORRUPT field falls back to its own DEFAULT rather than clamping to an edge the user // never chose (or, for the gain, silencing/blasting the instance). static void testCorruptFieldsFallBackToDefaults() { legacy::Envelope env; env.selectionId = "kick"; env.previewVelocity = 0; // 0 is a note-off by convention — out of the 1..127 spec env.voiceCount = 200; // past kMaxVoiceCount env.masterGain = 1e9; // far past the +24 dB cap const ComponentState out = deserializeComponentState(legacy::envelopeWithZones(env, {}, 7), 48000.0); CHECK(out.previewVelocity == kPreviewVelocityDefault); CHECK(out.voiceCount == kDefaultVoiceCount); CHECK(out.masterGainLinear == 1.0); } // The WRITER never emits an out-of-range voice count or master gain, so a blob this codec // produced always re-reads as itself. static void testWriterClampsOutOfRangeFields() { ComponentState in; in.voiceCount = 999; in.masterGainLinear = 1e9; const ComponentState out = deserializeComponentState(serializeComponentState(in), 48000.0); CHECK(out.voiceCount >= kMinVoiceCount && out.voiceCount <= kMaxVoiceCount); CHECK(out.masterGainLinear <= reasampler::instrument::engine::masterGainMaxLinear() * (1.0 + 1e-9)); // Zero gain is TRUE silence and a legal stored value — it must not be "corrected". ComponentState silent; silent.masterGainLinear = 0.0; CHECK(deserializeComponentState(serializeComponentState(silent), 48000.0) .masterGainLinear == 0.0); } // An UNKNOWN envelope version yields the empty state rather than a misparse. static void testUnknownEnvelopeVersionIsEmpty() { legacy::Envelope env; env.version = 99; env.selectionId = "kick"; const ComponentState out = deserializeComponentState(legacy::envelopeWithZones(env, {}, 7), 48000.0); CHECK(out.selectionId.empty()); CHECK(!out.params.rootOverride); } // A v1 selection blob lifts to {id, default params} — 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.params.rootOverride); CHECK(out.params.keyTrack == 1.0); } // Truncation degrades to a partial/empty parse — never out-of-bounds, never throws. Run // over BOTH the current format and a retired zone-list blob, since the migration path has // its own bounded-read walk. static void testTruncationDegradesCleanly() { ComponentState in; in.selectionId = "smp-2"; in.params.rootOverride = 61; const std::vector current = serializeComponentState(in); legacy::Zone z; z.sampleId = "smp-2"; const std::vector retired = legacy::envelopeWithZones("smp-2", {z, z}, 7); for (const std::vector* blob : {¤t, &retired}) { for (std::size_t cut = 0; cut < blob->size(); ++cut) { const std::vector part(blob->begin(), blob->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); } int main() { testComponentStateRoundTrip(); testDefaultStateRoundTripsToDefaults(); testEnvelopePrefixBytesFrozen(); testWriterEmitsCurrentPayloadVersion(); testSingleZoneMigrationIsLossless(); testLiftedStateReSavesInCurrentFormat(); testMultiZoneMigrationAdoptsFirstZone(); testFirstZoneSupersedesStoredSelection(); testEmptyZoneListKeepsTheStoredSelection(); testEveryOlderPayloadVersionMigrates(); testLegacyV3FramesConvertAtTheProjectRate(); testEnvelopeLadderLiftsEachVersion(); testV2ZonesOnlyBlobAdoptsBothFromZoneOne(); testCorruptFieldsFallBackToDefaults(); testWriterClampsOutOfRangeFields(); testUnknownEnvelopeVersionIsEmpty(); testV1SelectionLift(); testTruncationDegradesCleanly(); 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; }