// Standalone tests for reasampler::sample_map — no VST3, no REAPER, no test framework. // Same fast assert loop as the sibling pure tests. This module is the S4 mapping heart: // bank blob -> selected sample (through the SHARED bank_book JSON parse), interleaved -> // mono downmix (the Tier-0 channel policy), the Tier-0 chromatic keymap build, and the // selected-sample instance-state (de)serialization. // // Every assertion is written to FAIL if the mapping were wrong: the bank blobs are built // by serializing a real BankBook (so we exercise the shared parse, not a fixture string), // and the selection / downmix / keymap / state values are checked against independently // computed expectations. // // Covers: selectSample by-id hit (across pool + named banks), the S10 policy reversal // (empty / stale id -> SILENCE nullopt, not the first sample), empty & malformed blob -> // nullopt, zero-samples -> nullopt, rootNote/loop intrinsic threading incl. the middle-C // default; listSamples ordinal order + the card metadata (rootNote/key/bankId) + empty/ // malformed; listBanks ordinal order (pool first) + empty/malformed; downmixToMono mono // passthrough / stereo average / 3-ch average / zero-stride / empty; buildTier0Keymap single // full-keyboard zone with the root + loop + rate threaded and rate defaulting; selection // state round-trip + empty id + wrong-version / truncated -> ""; component state (v3) // round-trip + v1/v2 back-compat lift + empty/unknown -> empty. // wav_trim -> extractFloatFrames -> downmixToMono integration: locks the interleave- // stride contract across the seam (that the byte stride wav_trim reports matches the // channel-count stride downmixToMono divides by). #include "../src/vst/sample_map.h" #include #include #include #include #include #include "../src/bank_book.h" #include "../src/bank_model.h" using namespace reasampler; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // Build a Sample with the fields sample_map reads. Relative path is required by // BankIndex::add (relative-only invariant); a content hash is set so dedup does not // collapse distinct entries. static Sample makeSample(const std::string& id, const std::string& name, const std::string& rel, std::optional root) { Sample s; s.id = id; s.displayName = name; s.relativePath = rel; s.contentHash = "hash-" + id; s.rootNote = root; return s; } // A serialized BankBook: the pool carries `poolSamples`, and one named bank "Drums" // carries `drumSamples`. Returns the JSON the instrument would read from ext-state. static std::string bookJson(const std::vector& poolSamples, const std::vector& drumSamples) { BankBook book; for (const Sample& s : poolSamples) book.pool().index.add(s); if (!drumSamples.empty()) { book.createBank("drums-id", "Drums"); BankIndex* di = book.index("drums-id"); for (const Sample& s : drumSamples) di->add(s); } return book.serialize(); } // --- selectSample ------------------------------------------------------------- static void testSelectByIdHit() { const std::string json = bookJson( {makeSample("a", "Kick", "reasampler_bank/a.wav", 36)}, {makeSample("b", "Snare", "reasampler_bank/b.wav", 38)}); // A sample in the NAMED bank resolves by id (search spans every bank). auto sel = selectSample(json, "b"); CHECK(sel.has_value()); CHECK(sel && sel->relativePath == "reasampler_bank/b.wav"); CHECK(sel && sel->rootNote == 38); } static void testSelectEmptyIdIsSilence() { const std::string json = bookJson( {makeSample("a", "Kick", "reasampler_bank/a.wav", 36)}, {makeSample("b", "Snare", "reasampler_bank/b.wav", 38)}); // POLICY REVERSAL (S10): no stored selection resolves to SILENCE (nullopt), NOT the // bank's first sample. A fresh instance plays nothing and shows the "pick a capture" // empty state — the deliberate reversal of the S4 first-sample auto-play. auto sel = selectSample(json, ""); CHECK(!sel.has_value()); } static void testSelectUnknownIdIsSilence() { const std::string json = bookJson( {makeSample("a", "Kick", "reasampler_bank/a.wav", 36)}, {}); // A stale stored id (deleted/moved-out sample) resolves to SILENCE, not a substituted // first sample — the editor reflects the missing pick with its empty state rather than // masking it with a mystery sample. auto sel = selectSample(json, "deleted-id"); CHECK(!sel.has_value()); } static void testSelectRootNoteDefault() { const std::string json = bookJson( {makeSample("a", "Loop", "reasampler_bank/a.wav", std::nullopt)}, {}); // A sample with no root-note intrinsic defaults to middle C (60). auto sel = selectSample(json, "a"); CHECK(sel.has_value()); CHECK(sel && sel->rootNote == 60); } static void testSelectLoopThreaded() { Sample s = makeSample("a", "Pad", "reasampler_bank/a.wav", 60); s.loop = LoopPoints{100, 500}; const std::string json = bookJson({s}, {}); auto sel = selectSample(json, "a"); CHECK(sel.has_value()); CHECK(sel && sel->loop.hasLoop); CHECK(sel && sel->loop.start == 100 && sel->loop.end == 500); } static void testSelectNoLoopIsAbsent() { const std::string json = bookJson( {makeSample("a", "OneShot", "reasampler_bank/a.wav", 60)}, {}); auto sel = selectSample(json, "a"); CHECK(sel.has_value()); CHECK(sel && !sel->loop.hasLoop); // absent loop -> hasLoop false (not a zero loop) } static void testSelectEmptyBlob() { CHECK(!selectSample("", "a").has_value()); } static void testSelectMalformedBlob() { CHECK(!selectSample("{not valid json", "a").has_value()); } static void testSelectZeroSamples() { // A valid book with NO samples anywhere -> nothing to play. const std::string json = bookJson({}, {}); CHECK(!selectSample(json, "").has_value()); CHECK(!selectSample(json, "anything").has_value()); } // --- listSamples -------------------------------------------------------------- static void testListSamplesOrdinalOrder() { const std::string json = bookJson( {makeSample("a", "Kick", "reasampler_bank/a.wav", 36), makeSample("c", "Hat", "reasampler_bank/c.wav", 42)}, {makeSample("b", "Snare", "reasampler_bank/b.wav", 38)}); const std::vector list = listSamples(json); // Pool samples (insertion order) come before the named bank's. CHECK(list.size() == 3); CHECK(list.size() == 3 && list[0].id == "a" && list[0].displayName == "Kick"); CHECK(list.size() == 3 && list[1].id == "c"); CHECK(list.size() == 3 && list[2].id == "b" && list[2].displayName == "Snare"); } static void testListSamplesCarriesCardMetadata() { // The browser card needs rootNote/key badge + the bank id (for the filter). A pool sample // reports the pool bank id; a named-bank sample reports "drums-id"; an un-rooted sample // reports no rootNote (the badge shows "root —", never a guessed value). Sample rooted = makeSample("a", "Kick", "reasampler_bank/a.wav", 36); rooted.key = "Cm"; Sample unrooted = makeSample("u", "Loop", "reasampler_bank/u.wav", std::nullopt); const std::string json = bookJson({rooted, unrooted}, {makeSample("b", "Snare", "reasampler_bank/b.wav", 38)}); const std::vector list = listSamples(json); CHECK(list.size() == 3); // Pool sample "a": rooted + keyed, pool bank id. CHECK(list[0].id == "a" && list[0].rootNote.has_value() && *list[0].rootNote == 36); CHECK(list[0].key.has_value() && *list[0].key == "Cm"); CHECK(!list[0].bankId.empty()); // the pool has an id; the filter matches on it // Pool sample "u": no root intrinsic -> no rootNote (badge shows "root —"). CHECK(list[1].id == "u" && !list[1].rootNote.has_value()); // Named-bank sample "b": its bank id distinguishes it from the pool for the filter. CHECK(list[2].id == "b" && list[2].bankId == "drums-id"); CHECK(list[2].bankId != list[0].bankId); // pool vs. named bank differ (filterable apart) } static void testListSamplesEmptyAndMalformed() { CHECK(listSamples("").empty()); CHECK(listSamples("{garbage").empty()); CHECK(listSamples(bookJson({}, {})).empty()); } static void testListBanksOrdinalOrder() { const std::string json = bookJson( {makeSample("a", "Kick", "reasampler_bank/a.wav", 36)}, {makeSample("b", "Snare", "reasampler_bank/b.wav", 38)}); const std::vector banks = listBanks(json); // Pool first (bank-zero), then the named bank "Drums". Both ids are present so the filter // tab strip can key on them. CHECK(banks.size() == 2); CHECK(banks.size() == 2 && banks[1].id == "drums-id" && banks[1].displayName == "Drums"); CHECK(banks.size() == 2 && !banks[0].id.empty()); // the pool bank has an id too } static void testListBanksEmptyAndMalformed() { CHECK(listBanks("").empty()); CHECK(listBanks("{garbage").empty()); // A valid book with no samples still has the pool bank -> one entry. CHECK(listBanks(bookJson({}, {})).size() == 1); } // --- downmixToMono ------------------------------------------------------------ static bool approx(double a, double b) { return std::fabs(a - b) < 1e-6; } static void testDownmixMonoPassthrough() { const std::vector in{0.1f, -0.2f, 0.3f}; const std::vector out = downmixToMono(in, 1); CHECK(out.size() == 3); CHECK(out.size() == 3 && approx(out[0], 0.1) && approx(out[1], -0.2) && approx(out[2], 0.3)); } static void testDownmixStereoAverages() { // Two frames, stereo interleaved: frame0 = (1.0, 0.0) -> 0.5; frame1 = (0.4, 0.6) -> 0.5. const std::vector in{1.0f, 0.0f, 0.4f, 0.6f}; const std::vector out = downmixToMono(in, 2); CHECK(out.size() == 2); CHECK(out.size() == 2 && approx(out[0], 0.5) && approx(out[1], 0.5)); } static void testDownmixThreeChannelAverages() { // One 3-channel frame (0.3, 0.3, 0.6) -> 0.4. const std::vector in{0.3f, 0.3f, 0.6f}; const std::vector out = downmixToMono(in, 3); CHECK(out.size() == 1); CHECK(out.size() == 1 && approx(out[0], 0.4)); } static void testDownmixDegenerate() { CHECK(downmixToMono({}, 2).empty()); // empty input CHECK(downmixToMono({0.1f, 0.2f}, 0).empty()); // zero stride CHECK(downmixToMono({0.1f, 0.2f}, -1).empty()); // negative stride } // --- buildTier0Keymap --------------------------------------------------------- static void testBuildKeymapSingleFullZone() { SampleLoop loop; loop.hasLoop = true; loop.start = 10; loop.end = 90; const Keymap km = buildTier0Keymap({0.1f, 0.2f, 0.3f}, 48000, 40, loop); // One sample, one zone spanning the whole keyboard, rooted at 40. CHECK(km.samples.size() == 1); CHECK(km.zones.size() == 1); CHECK(km.zones.size() == 1 && km.zones[0].lowNote == 0 && km.zones[0].highNote == 127); CHECK(km.zones.size() == 1 && km.zones[0].rootNote == 40); CHECK(km.samples.size() == 1 && km.samples[0].sampleRate == 48000); CHECK(km.samples.size() == 1 && km.samples[0].rootNote == 40); CHECK(km.samples.size() == 1 && km.samples[0].frames.size() == 3); CHECK(km.samples.size() == 1 && km.samples[0].loop.hasLoop && km.samples[0].loop.start == 10 && km.samples[0].loop.end == 90); // Resolution: any note lands in the single zone. CHECK(km.resolve(0, 100).matched); CHECK(km.resolve(127, 100).matched); } // --- selection state (setState/getState) -------------------------------------- static void testSelectionStateRoundTrip() { const std::string id = "sample-guid-123"; const std::vector bytes = serializeSelection(id); // Versioned: 4-byte tag + the id bytes. CHECK(bytes.size() == 4 + id.size()); CHECK(deserializeSelection(bytes) == id); } static void testSelectionStateEmptyId() { const std::vector bytes = serializeSelection(""); CHECK(bytes.size() == 4); // just the version tag CHECK(deserializeSelection(bytes) == ""); } static void testSelectionStateWrongVersion() { std::vector bytes = serializeSelection("id"); bytes[0] = 0xEE; // corrupt the version tag CHECK(deserializeSelection(bytes) == ""); // unknown version -> no selection } static void testSelectionStateTruncated() { CHECK(deserializeSelection({}) == ""); // empty CHECK(deserializeSelection({1, 0, 0}) == ""); // fewer than 4 bytes (no tag) } // --- wav_trim -> extractFloatFrames -> downmixToMono integration --------------- // // Locks the interleave-stride contract at the seam between wav_trim and sample_map: // wav_trim reports channelCount, extractFloatFrames yields interleaved samples with // that stride, and downmixToMono divides by that same stride. If either module // changed its understanding of the layout (e.g. extractFloatFrames started packing // differently, or downmixToMono changed its stride divisor), this test catches it. static void putU16sm(std::vector& b, std::uint16_t v) { b.push_back(static_cast(v & 0xFF)); b.push_back(static_cast((v >> 8) & 0xFF)); } static void putU32sm(std::vector& b, std::uint32_t v) { b.push_back(static_cast(v & 0xFF)); b.push_back(static_cast((v >> 8) & 0xFF)); b.push_back(static_cast((v >> 16) & 0xFF)); b.push_back(static_cast((v >> 24) & 0xFF)); } static void putTagsm(std::vector& b, const char* t) { for (int i = 0; i < 4; ++i) b.push_back(static_cast(t[i])); } static void putFloatsm(std::vector& b, float f) { std::uint8_t tmp[4]; std::memcpy(tmp, &f, 4); for (int i = 0; i < 4; ++i) b.push_back(tmp[i]); } // Build a 32-bit-float WAV byte buffer. Samples: frame f, channel c = value(f, c). template static std::vector buildWav(std::uint16_t channels, std::uint32_t sampleRate, std::size_t frames, Fn value) { const std::uint32_t dataBytes = static_cast(frames * channels * 4u); std::vector chunks; putTagsm(chunks, "fmt "); putU32sm(chunks, 16); putU16sm(chunks, 3); // IEEE float putU16sm(chunks, channels); putU32sm(chunks, sampleRate); putU32sm(chunks, sampleRate * channels * 4u); putU16sm(chunks, static_cast(channels * 4)); putU16sm(chunks, 32); putTagsm(chunks, "data"); putU32sm(chunks, dataBytes); for (std::size_t f = 0; f < frames; ++f) for (std::uint16_t c = 0; c < channels; ++c) putFloatsm(chunks, value(f, c)); std::vector wav; putTagsm(wav, "RIFF"); putU32sm(wav, static_cast(4 + chunks.size())); putTagsm(wav, "WAVE"); wav.insert(wav.end(), chunks.begin(), chunks.end()); return wav; } static void testWavTrimToDownmixPipelineStereo() { // Stereo WAV: frame f, L = f * 0.1f, R = f * 0.1f + 0.5f. Expected mono average: // (f * 0.1f + f * 0.1f + 0.5f) / 2 = f * 0.1f + 0.25f. const std::size_t kFrames = 4; auto wav = buildWav(2, 48000, kFrames, [](std::size_t f, std::uint16_t c) { return static_cast(f) * 0.1f + (c == 1 ? 0.5f : 0.0f); }); WavLayout layout = parseWavLayout(wav); CHECK(layout.valid); CHECK(layout.channelCount == 2); CHECK(layout.frameCount() == kFrames); const std::vector interleaved = extractFloatFrames(wav, layout, 0, layout.frameCount()); CHECK(interleaved.size() == kFrames * 2); const std::vector mono = downmixToMono(interleaved, layout.channelCount); CHECK(mono.size() == kFrames); for (std::size_t f = 0; f < kFrames; ++f) { const float expected = static_cast(f) * 0.1f + 0.25f; CHECK(approx(mono[f], expected)); } } static void testWavTrimToDownmixPipelineMono() { // Mono WAV: extractFloatFrames -> downmixToMono with channelCount==1 is a passthrough. const std::size_t kFrames = 3; auto wav = buildWav(1, 44100, kFrames, [](std::size_t f, std::uint16_t) { return static_cast(f) * 0.5f; }); WavLayout layout = parseWavLayout(wav); CHECK(layout.valid); CHECK(layout.channelCount == 1); const std::vector interleaved = extractFloatFrames(wav, layout, 0, layout.frameCount()); CHECK(interleaved.size() == kFrames); const std::vector mono = downmixToMono(interleaved, layout.channelCount); CHECK(mono.size() == kFrames); CHECK(approx(mono[0], 0.0) && approx(mono[1], 0.5) && approx(mono[2], 1.0)); } // --- performance map: resolvePerformance -------------------------------------- static PerformanceZone zone(const std::string& id, int lo, int hi, std::optional rootOverride = std::nullopt) { PerformanceZone z; z.sampleId = id; z.lowNote = lo; z.highNote = hi; z.rootOverride = rootOverride; return z; } static void testResolveEmptyMap() { // An empty performance map resolves to nothing (the shell falls back to Tier 0). const std::string json = bookJson({makeSample("a", "Kick", "b/a.wav", 36)}, {}); const ResolvedPerformance r = resolvePerformance(json, PerformanceMap{}); CHECK(r.zones.empty()); CHECK(r.droppedSampleIds.empty()); } static void testResolveEmptyBlob() { PerformanceMap m; m.zones.push_back(zone("a", 0, 127)); CHECK(resolvePerformance("", m).zones.empty()); // no bank CHECK(resolvePerformance("{garbage", m).zones.empty()); // malformed } static void testResolveMultiZoneAcrossBanks() { const std::string json = bookJson( {makeSample("a", "Kick", "b/a.wav", 36)}, {makeSample("b", "Snare", "b/b.wav", 38)}); PerformanceMap m; m.zones.push_back(zone("a", 36, 47)); m.zones.push_back(zone("b", 48, 59)); const ResolvedPerformance r = resolvePerformance(json, m); CHECK(r.zones.size() == 2); CHECK(r.droppedSampleIds.empty()); // Order preserved; paths + ranges threaded. CHECK(r.zones.size() == 2 && r.zones[0].relativePath == "b/a.wav"); CHECK(r.zones.size() == 2 && r.zones[0].lowNote == 36 && r.zones[0].highNote == 47); CHECK(r.zones.size() == 2 && r.zones[1].relativePath == "b/b.wav"); CHECK(r.zones.size() == 2 && r.zones[1].lowNote == 48 && r.zones[1].highNote == 59); } static void testResolveStaleIdDropsZone() { // STALE-ID POLICY: a zone naming a deleted sample is dropped, its id reported; the // surviving zone still resolves (the whole map is NOT abandoned). const std::string json = bookJson({makeSample("a", "Kick", "b/a.wav", 36)}, {}); PerformanceMap m; m.zones.push_back(zone("a", 0, 59)); m.zones.push_back(zone("ghost", 60, 127)); // no such sample const ResolvedPerformance r = resolvePerformance(json, m); CHECK(r.zones.size() == 1); CHECK(r.zones.size() == 1 && r.zones[0].relativePath == "b/a.wav"); CHECK(r.droppedSampleIds.size() == 1); CHECK(r.droppedSampleIds.size() == 1 && r.droppedSampleIds[0] == "ghost"); } static void testResolveRootPrecedence() { // Override beats bank intrinsic beats middle-C default. const std::string json = bookJson( {makeSample("rooted", "R", "b/r.wav", 40), // bank intrinsic 40 makeSample("unrooted", "U", "b/u.wav", std::nullopt)}, // no intrinsic {}); PerformanceMap m; m.zones.push_back(zone("rooted", 0, 42)); // no override -> 40 m.zones.push_back(zone("rooted", 43, 84, /*override=*/72)); // override -> 72 m.zones.push_back(zone("unrooted", 85, 127)); // no intrinsic -> 60 const ResolvedPerformance r = resolvePerformance(json, m); CHECK(r.zones.size() == 3); CHECK(r.zones.size() == 3 && r.zones[0].rootNote == 40); // bank intrinsic CHECK(r.zones.size() == 3 && r.zones[1].rootNote == 72); // override wins CHECK(r.zones.size() == 3 && r.zones[2].rootNote == 60); // middle-C default } static void testResolveLoopThreaded() { Sample s = makeSample("a", "Pad", "b/a.wav", 60); s.loop = LoopPoints{200, 800}; const std::string json = bookJson({s}, {}); PerformanceMap m; m.zones.push_back(zone("a", 0, 127)); const ResolvedPerformance r = resolvePerformance(json, m); CHECK(r.zones.size() == 1); CHECK(r.zones.size() == 1 && r.zones[0].loop.hasLoop); CHECK(r.zones.size() == 1 && r.zones[0].loop.start == 200 && r.zones[0].loop.end == 800); // No loop override + no startPoint -> effective start is 0 (S11 default). CHECK(r.zones.size() == 1 && r.zones[0].startFrame == 0); } static void testResolveLoopOverrideWins() { // S11: the instrument's per-zone loopOverride beats the bank's S2 loop intrinsic, and the // startPoint feeds the effective startFrame — without mutating the bank. Sample s = makeSample("a", "Pad", "b/a.wav", 60); s.loop = LoopPoints{200, 800}; // bank intrinsic const std::string json = bookJson({s}, {}); PerformanceMap m; PerformanceZone z = zone("a", 0, 127); SampleLoop over; over.hasLoop = true; over.start = 1000; over.end = 4000; z.loopOverride = over; // instrument override z.startPoint = 512; // start offset m.zones.push_back(z); const ResolvedPerformance r = resolvePerformance(json, m); CHECK(r.zones.size() == 1 && r.zones[0].loop.hasLoop); CHECK(r.zones.size() == 1 && r.zones[0].loop.start == 1000 && r.zones[0].loop.end == 4000); CHECK(r.zones.size() == 1 && r.zones[0].startFrame == 512); } static void testResolveLoopOverrideDisablesLoop() { // A loopOverride with hasLoop=false explicitly REMOVES the bank's loop for this instance // (override present-but-empty wins over the intrinsic — a deliberate "no loop here"). Sample s = makeSample("a", "Pad", "b/a.wav", 60); s.loop = LoopPoints{200, 800}; const std::string json = bookJson({s}, {}); PerformanceMap m; PerformanceZone z = zone("a", 0, 127); z.loopOverride = SampleLoop{}; // hasLoop=false, start=end=0 m.zones.push_back(z); const ResolvedPerformance r = resolvePerformance(json, m); CHECK(r.zones.size() == 1 && !r.zones[0].loop.hasLoop); } // --- performance map: buildZonedKeymap ---------------------------------------- static void testBuildZonedKeymapMultiZone() { std::vector zones; ResolvedZone z0; z0.lowNote = 36; z0.highNote = 47; z0.rootNote = 36; zones.push_back(z0); ResolvedZone z1; z1.lowNote = 48; z1.highNote = 59; z1.rootNote = 48; zones.push_back(z1); std::vector decoded; decoded.push_back(DecodedZonePcm{{0.1f, 0.2f}, 44100}); decoded.push_back(DecodedZonePcm{{0.3f, 0.4f, 0.5f}, 48000}); const Keymap km = buildZonedKeymap(zones, decoded); CHECK(km.samples.size() == 2); CHECK(km.zones.size() == 2); // Zone 0 -> sample 0, rooted 36, range 36..47; zone 1 -> sample 1, rooted 48. CHECK(km.zones.size() == 2 && km.zones[0].sampleIndex == 0 && km.zones[0].rootNote == 36); CHECK(km.zones.size() == 2 && km.zones[0].lowNote == 36 && km.zones[0].highNote == 47); CHECK(km.zones.size() == 2 && km.zones[1].sampleIndex == 1 && km.zones[1].rootNote == 48); CHECK(km.samples.size() == 2 && km.samples[1].sampleRate == 48000); CHECK(km.samples.size() == 2 && km.samples[1].frames.size() == 3); // Resolution: a note in each range lands in the right zone. CHECK(km.resolve(40, 100).matched && km.resolve(40, 100).zoneIndex == 0); CHECK(km.resolve(52, 100).matched && km.resolve(52, 100).zoneIndex == 1); // A note outside every zone does not match (no-play, not zone 0). CHECK(!km.resolve(24, 100).matched); } static void testBuildZonedKeymapThreadsLoopAndStart() { // S11: the effective loop + start on a ResolvedZone reach the core's SampleData so the // voice honors them at note-on. std::vector zones; ResolvedZone z0; z0.lowNote = 0; z0.highNote = 127; z0.rootNote = 60; z0.loop.hasLoop = true; z0.loop.start = 3; z0.loop.end = 7; z0.startFrame = 2; zones.push_back(z0); std::vector decoded; decoded.push_back(DecodedZonePcm{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 44100}); const Keymap km = buildZonedKeymap(zones, decoded); CHECK(km.samples.size() == 1); CHECK(km.samples.size() == 1 && km.samples[0].loop.hasLoop && km.samples[0].loop.start == 3 && km.samples[0].loop.end == 7); CHECK(km.samples.size() == 1 && km.samples[0].startFrame == 2); } static void testBuildZonedKeymapDropsEmptyPcm() { // A zone whose decoded WAV is empty is dropped; the other zone survives, and the // survivor's sampleIndex points at ITS sample (not the dropped one's slot). std::vector zones; ResolvedZone z0; z0.lowNote = 0; z0.highNote = 63; z0.rootNote = 60; zones.push_back(z0); ResolvedZone z1; z1.lowNote = 64; z1.highNote = 127; z1.rootNote = 72; zones.push_back(z1); std::vector decoded; decoded.push_back(DecodedZonePcm{{}, 44100}); // empty -> dropped decoded.push_back(DecodedZonePcm{{0.9f}, 44100}); // survives const Keymap km = buildZonedKeymap(zones, decoded); CHECK(km.samples.size() == 1); CHECK(km.zones.size() == 1); CHECK(km.zones.size() == 1 && km.zones[0].sampleIndex == 0); // remapped to slot 0 CHECK(km.zones.size() == 1 && km.zones[0].lowNote == 64 && km.zones[0].rootNote == 72); } static void testBuildZonedKeymapOverlapFirstWins() { // OVERLAP POLICY: two zones share keys; the FIRST in order wins the contested note // (mirrors the S3 core's first-match resolve). std::vector zones; ResolvedZone z0; z0.lowNote = 0; z0.highNote = 127; z0.rootNote = 60; zones.push_back(z0); ResolvedZone z1; z1.lowNote = 60; z1.highNote = 72; z1.rootNote = 48; zones.push_back(z1); std::vector decoded; decoded.push_back(DecodedZonePcm{{0.1f}, 44100}); decoded.push_back(DecodedZonePcm{{0.2f}, 44100}); const Keymap km = buildZonedKeymap(zones, decoded); CHECK(km.zones.size() == 2); // Note 64 is in both zones; first-match resolves to zone 0. CHECK(km.resolve(64, 100).matched && km.resolve(64, 100).zoneIndex == 0); } static void testBuildZonedKeymapEmpty() { // No zones -> empty keymap (silence). const Keymap km = buildZonedKeymap({}, {}); CHECK(km.samples.empty() && km.zones.empty()); CHECK(!km.resolve(60, 100).matched); } // --- performance-map state: serialize / deserialize --------------------------- static void testPerformanceStateRoundTrip() { PerformanceMap m; 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 bytes = serializePerformance(m); 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); CHECK(back.zones.size() == 2 && !back.zones[0].rootOverride.has_value()); CHECK(back.zones.size() == 2 && back.zones[1].sampleId == "snare"); CHECK(back.zones.size() == 2 && back.zones[1].rootOverride.has_value() && *back.zones[1].rootOverride == 50); } static void testPerformanceStateEmpty() { const std::vector bytes = serializePerformance(PerformanceMap{}); // Envelope version (4) + zones-payload marker (4) + payload version (4) + zero count (4). CHECK(bytes.size() == 16); CHECK(deserializePerformance(bytes, 44100.0).zones.empty()); } static void testPerformanceStateLoopStartRoundTrip() { // S11: the per-zone loopOverride + startPoint survive the payload-v2 round trip. PerformanceMap m; PerformanceZone z = zone("pad", 24, 96, /*override=*/64); SampleLoop lp; lp.hasLoop = true; lp.start = 12345; lp.end = 67890; z.loopOverride = lp; z.startPoint = 4096; 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), 44100.0); CHECK(back.zones.size() == 2); CHECK(back.zones.size() == 2 && back.zones[0].rootOverride.has_value() && *back.zones[0].rootOverride == 64); CHECK(back.zones.size() == 2 && back.zones[0].loopOverride.has_value() && back.zones[0].loopOverride->hasLoop && back.zones[0].loopOverride->start == 12345 && back.zones[0].loopOverride->end == 67890); CHECK(back.zones.size() == 2 && back.zones[0].startPoint.has_value() && *back.zones[0].startPoint == 4096); // Zone 1: no overrides -> all optionals absent after round trip. CHECK(back.zones.size() == 2 && !back.zones[1].loopOverride.has_value()); CHECK(back.zones.size() == 2 && !back.zones[1].startPoint.has_value()); } static void testPerformanceStateV1PayloadBackCompat() { // A pre-S11 PAYLOAD v1 blob (no format marker: envelope v2 + bare count + short records) // parses cleanly with the loop/start overrides defaulting absent. Hand-build the exact // shipped shape to prove the reader still accepts the marker-less payload. std::vector b; auto u32 = [&](std::uint32_t v) { b.push_back(v & 0xFF); b.push_back((v >> 8) & 0xFF); b.push_back((v >> 16) & 0xFF); b.push_back((v >> 24) & 0xFF); }; u32(2); // envelope version 2 u32(1); // zone count 1 (NOT the marker -> payload v1) const std::string id = "legacy"; u32(static_cast(id.size())); b.insert(b.end(), id.begin(), id.end()); u32(10); // lowNote u32(40); // highNote b.push_back(0); // hasRootOverride = 0 (record ends here in v1) 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()); CHECK(back.zones.size() == 1 && !back.zones[0].startPoint.has_value()); } static void testComponentStateLoopStartRoundTrip() { // The overrides also round-trip through the v3 ComponentState envelope (zones nest inside // it), so the processor's live getState/setState preserves them — the composition property. ComponentState s; s.selectionId = "pick"; PerformanceZone z = zone("pick", 0, 127); SampleLoop lp; lp.hasLoop = true; lp.start = 500; lp.end = 9000; z.loopOverride = lp; z.startPoint = 128; s.map.zones.push_back(z); 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 && back.map.zones[0].loopOverride->end == 9000); CHECK(back.map.zones.size() == 1 && back.map.zones[0].startPoint.has_value() && *back.map.zones[0].startPoint == 128); } static void testPerformanceStateV1BackCompat() { // A v1 blob (the S4 single-selection format) lifts to a single full-keyboard zone. const std::vector v1 = serializeSelection("legacy-sample-id"); 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(""), 44100.0).zones.empty()); } static void testPerformanceStateGarbage() { // Unknown version / truncated / empty -> empty map (never throws). 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 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, 44100.0).zones.empty()); } static void testPerformanceStateNegativeNotesRoundTrip() { // Notes are clamped in the UI, but the wire format must survive the full int range so // 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), 44100.0); CHECK(back.zones.size() == 1 && back.zones[0].rootOverride.has_value() && *back.zones[0].rootOverride == 0); } // --- Combined component state (v3, S10) -------------------------------------- static void testComponentStateRoundTrip() { // The v3 state carries the single-capture selection AND the opt-in zones, distinctly. ComponentState s; 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), 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" && back.map.zones[0].highNote == 59 && !back.map.zones[0].rootOverride.has_value()); CHECK(back.map.zones.size() == 2 && back.map.zones[1].sampleId == "z1" && back.map.zones[1].rootOverride.has_value() && *back.map.zones[1].rootOverride == 48); } static void testComponentStateSelectionOnlyNoZones() { // A single-capture instance: a pick, no zones. Must restore the pick with an empty map // (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), 44100.0); CHECK(back.selectionId == "just-a-pick"); CHECK(back.map.zones.empty()); } 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), 44100.0); CHECK(back.selectionId.empty()); CHECK(back.map.zones.empty()); } 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 v1 = serializeSelection("legacy-id"); 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(""), 44100.0); CHECK(empty.selectionId.empty() && empty.map.zones.empty()); } static void testComponentStateV2BackCompat() { // A v2 S5 blob (zones-only) lifts to {"", zones}: that instance had zones but no separate // single-capture selection. PerformanceMap m; m.zones.push_back(zone("s", 12, 24, /*override=*/std::nullopt)); const std::vector v2 = serializePerformance(m); 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); } static void testComponentStateGarbage() { // Empty / unknown version -> empty (never throws across the host). CHECK(deserializeComponentState({}, 44100.0).selectionId.empty()); CHECK(deserializeComponentState({}, 44100.0).map.zones.empty()); const std::vector unknown{0xAA, 0xBB, 0xCC, 0xDD}; 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 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, 44100.0).selectionId.empty()); CHECK(deserializeComponentState(t, 44100.0).map.zones.empty()); } // --- S7: extractChannel / decodeChannels (cross-mode channel policy) ---------- static void testExtractChannelStereo() { // Interleaved stereo [L0,R0,L1,R1,...]; extract channel 0 -> L's, channel 1 -> R's. const std::vector in{0.1f, 0.9f, 0.2f, 0.8f, 0.3f, 0.7f}; const std::vector l = extractChannel(in, 2, 0); const std::vector r = extractChannel(in, 2, 1); CHECK(l.size() == 3 && approx(l[0], 0.1) && approx(l[1], 0.2) && approx(l[2], 0.3)); CHECK(r.size() == 3 && approx(r[0], 0.9) && approx(r[1], 0.8) && approx(r[2], 0.7)); } static void testExtractChannelClampsToLast() { // A mono source asked for channel 1 yields channel 0 (clamp to last) — the dual-mono block. const std::vector mono{0.1f, 0.2f, 0.3f}; const std::vector ch1 = extractChannel(mono, 1, 1); CHECK(ch1.size() == 3 && approx(ch1[0], 0.1) && approx(ch1[2], 0.3)); // == channel 0 CHECK(extractChannel({}, 2, 0).empty()); // empty in CHECK(extractChannel({0.1f}, 0, 0).empty()); // zero stride } static void testDecodeChannelsMonoModeDownmixes() { // MONO mode: a stereo source averages to one channel (the existing policy), framesR empty. const std::vector stereo{1.0f, 0.0f, 0.4f, 0.6f}; // frames (1,0) and (0.4,0.6) const DecodedZonePcm d = decodeChannels(stereo, 2, ChannelMode::Mono, 48000); CHECK(d.monoFrames.size() == 2 && approx(d.monoFrames[0], 0.5) && approx(d.monoFrames[1], 0.5)); CHECK(d.framesR.empty()); // mono mode -> single channel CHECK(d.sampleRate == 48000); } static void testDecodeChannelsStereoModeStereoSource() { // STEREO mode + stereo source: channels taken as-is (L/R), both present + distinct. const std::vector stereo{0.1f, 0.9f, 0.2f, 0.8f}; const DecodedZonePcm d = decodeChannels(stereo, 2, ChannelMode::Stereo, 44100); CHECK(d.monoFrames.size() == 2 && approx(d.monoFrames[0], 0.1) && approx(d.monoFrames[1], 0.2)); CHECK(d.framesR.size() == 2 && approx(d.framesR[0], 0.9) && approx(d.framesR[1], 0.8)); } static void testDecodeChannelsStereoModeMonoSourceDualMono() { // STEREO mode + mono source: dual-mono — framesR duplicates channel 0 (centered, not silent). const std::vector mono{0.3f, 0.6f, 0.9f}; const DecodedZonePcm d = decodeChannels(mono, 1, ChannelMode::Stereo, 44100); CHECK(d.monoFrames.size() == 3); CHECK(d.framesR.size() == 3); for (std::size_t i = 0; i < 3; ++i) CHECK(approx(d.monoFrames[i], d.framesR[i])); // R == L } // --- S7: buildTier0Keymap stereo threading ------------------------------------ static void testBuildKeymapStereoCarriesSecondChannel() { const Keymap km = buildTier0Keymap({0.1f, 0.2f}, 48000, 60, SampleLoop{}, {0.9f, 0.8f}); CHECK(km.samples.size() == 1); CHECK(km.samples.size() == 1 && km.samples[0].channelCount() == 2); CHECK(km.samples.size() == 1 && km.samples[0].framesR.size() == 2 && approx(km.samples[0].framesR[0], 0.9) && approx(km.samples[0].framesR[1], 0.8)); } static void testBuildKeymapMonoWhenNoSecondChannel() { // No framesR passed -> mono SampleData (byte-identical to the pre-S7 build). const Keymap km = buildTier0Keymap({0.1f, 0.2f}, 48000, 60, SampleLoop{}); CHECK(km.samples.size() == 1 && km.samples[0].channelCount() == 1); CHECK(km.samples.size() == 1 && km.samples[0].framesR.empty()); } static void testBuildKeymapDropsMismatchedSecondChannel() { // A framesR whose length mismatches frames is dropped -> mono (a bad pair never half-plays). const Keymap km = buildTier0Keymap({0.1f, 0.2f, 0.3f}, 48000, 60, SampleLoop{}, {0.9f}); CHECK(km.samples.size() == 1 && km.samples[0].channelCount() == 1); } static void testBuildZonedKeymapCarriesSecondChannel() { // The zoned build threads each zone's framesR when it length-matches channel 0. std::vector zones; ResolvedZone z; z.lowNote = 0; z.highNote = 127; z.rootNote = 60; zones.push_back(z); std::vector decoded; DecodedZonePcm d; d.monoFrames = {0.1f, 0.2f}; d.sampleRate = 44100; d.framesR = {0.9f, 0.8f}; decoded.push_back(d); const Keymap km = buildZonedKeymap(zones, decoded); CHECK(km.samples.size() == 1 && km.samples[0].channelCount() == 2); CHECK(km.samples.size() == 1 && km.samples[0].framesR.size() == 2 && approx(km.samples[0].framesR[1], 0.8)); } // --- S7: component state v4 (channel mode) ------------------------------------ static void testComponentStateV4RoundTripStereo() { ComponentState s; 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), 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"); } static void testComponentStateV4RoundTripMono() { ComponentState s; s.selectionId = "pick"; s.channelMode = ChannelMode::Mono; 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{}), 44100.0); CHECK(back.channelMode == ChannelMode::Mono); CHECK(back.selectionId.empty() && back.map.zones.empty()); } static void testComponentStateV3LiftsToMono() { // A pre-S7 v3 blob (selection + zones, no mode byte) lifts to channelMode = mono, with the // selection and zones intact. Build a v3 blob by hand: tag 3, id length + id, zones payload. std::vector v3; v3.push_back(3); v3.push_back(0); v3.push_back(0); v3.push_back(0); // version 3 const std::string id = "legacy"; v3.push_back(static_cast(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, 44100.0); CHECK(back.selectionId == "legacy"); CHECK(back.channelMode == ChannelMode::Mono); // pre-S7 default CHECK(back.map.zones.empty()); } 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"), 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), 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 t{4, 0, 0, 0}; // version 4, nothing after const ComponentState back = deserializeComponentState(t, 44100.0); CHECK(back.channelMode == ChannelMode::Mono); 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), 44100.0); 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()); } // --- v5 component state: the S8/S9 last-consumed-assignment marker ------------- static void testComponentStateV5MarkerRoundTrip() { // The consumed-assignment generation (S8 reader marker) round-trips through the v5 envelope // alongside selection + mode + zones. A non-zero, > 32-bit value proves the 8-byte LE field. ComponentState s; s.selectionId = "pick"; 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), 44100.0); CHECK(back.lastConsumedAssignGeneration == 1700000123456LL); // marker survives CHECK(back.channelMode == ChannelMode::Stereo); CHECK(back.selectionId == "pick"); CHECK(back.map.zones.size() == 1 && back.map.zones[0].sampleId == "z0"); } 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{}), 44100.0); CHECK(back.lastConsumedAssignGeneration == 0); } static void testComponentStateV4LiftsMarkerToZero() { // A GENUINE v4 blob (version tag 4: mode byte, then id + zones — NO 8-byte marker) must lift // with lastConsumedAssignGeneration = 0 and its mode/selection/zones intact. Build it by hand // (serializeComponentState now emits v5, so we cannot use it to make a v4 blob). This proves // an already-saved pre-S8/S9 instance restores cleanly and its first assign still applies. std::vector v4; v4.push_back(4); v4.push_back(0); v4.push_back(0); v4.push_back(0); // version 4 v4.push_back(1); // channel mode = stereo const std::string id = "saved"; v4.push_back(static_cast(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, 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"); CHECK(back.map.zones.empty()); } 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 t{5, 0, 0, 0, 1, 0xAA, 0xBB}; // version 5, mode byte, 2 marker bytes const ComponentState back = deserializeComponentState(t, 44100.0); CHECK(back.lastConsumedAssignGeneration == 0); CHECK(back.selectionId.empty() && back.map.zones.empty()); } // --- v6 component state: the S-VIEW-4 preview-trigger velocity ------------------ static void testComponentStatePreviewVelocityRoundTrip() { // The preview velocity round-trips through the v6 envelope alongside selection + mode + marker // + zones. A non-default value (not 64) proves the byte is actually read back, not defaulted. ComponentState s; s.selectionId = "pick"; s.channelMode = ChannelMode::Stereo; s.lastConsumedAssignGeneration = 1700000123456LL; s.previewVelocity = 111; // non-default s.map.zones.push_back(zone("z0", 0, 127, /*override=*/std::nullopt)); const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0); CHECK(back.previewVelocity == 111); // velocity survives CHECK(back.channelMode == ChannelMode::Stereo); // envelope neighbours intact CHECK(back.lastConsumedAssignGeneration == 1700000123456LL); CHECK(back.selectionId == "pick"); CHECK(back.map.zones.size() == 1 && back.map.zones[0].sampleId == "z0"); } static void testComponentStateDefaultPreviewVelocityIsMid() { // A default-constructed state carries the mid velocity default and round-trips it. const ComponentState back = deserializeComponentState(serializeComponentState(ComponentState{}), 44100.0); CHECK(back.previewVelocity == kPreviewVelocityDefault); CHECK(kPreviewVelocityDefault == 64); } static void testComponentStatePreviewVelocityExtremes() { // The full MIDI velocity range round-trips: 1 (softest audible) and 127 (max) both survive the // single-byte field without clamping or overflow. for (std::uint8_t v : {std::uint8_t{1}, std::uint8_t{127}}) { ComponentState s; s.previewVelocity = v; const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0); CHECK(back.previewVelocity == v); } } static void testComponentStateV5LiftsVelocityToMid() { // A GENUINE v5 blob (version tag 5: mode byte, 8-byte marker, then id + zones — NO velocity // byte) must lift previewVelocity to kPreviewVelocityDefault, its mode/marker/selection/zones // intact. Build it by hand (serializeComponentState now emits v6, so it cannot make a v5 blob). // This proves an already-saved pre-S-VIEW-4 instance restores at the mid default. std::vector v5; v5.push_back(5); v5.push_back(0); v5.push_back(0); v5.push_back(0); // version 5 v5.push_back(1); // channel mode = stereo for (int i = 0; i < 8; ++i) v5.push_back(0); // marker = 0 const std::string id = "saved"; v5.push_back(static_cast(id.size())); v5.push_back(0); v5.push_back(0); v5.push_back(0); v5.insert(v5.end(), id.begin(), id.end()); v5.push_back(0); v5.push_back(0); v5.push_back(0); v5.push_back(0); // zone count 0 const ComponentState back = deserializeComponentState(v5, 44100.0); CHECK(back.previewVelocity == kPreviewVelocityDefault); // no velocity byte in v5 -> mid default CHECK(back.channelMode == ChannelMode::Stereo); // v5 mode byte honored CHECK(back.selectionId == "saved"); CHECK(back.map.zones.empty()); } static void testComponentStateV4LiftsVelocityToMid() { // A pre-S8/S9 v4 blob (mode byte, then id + zones — no marker, no velocity) also lifts // previewVelocity to the mid default. Proves the older-than-v5 lift path defaults the field too. std::vector v4; v4.push_back(4); v4.push_back(0); v4.push_back(0); v4.push_back(0); // version 4 v4.push_back(0); // channel mode = mono v4.push_back(0); v4.push_back(0); v4.push_back(0); v4.push_back(0); // id length 0 v4.push_back(0); v4.push_back(0); v4.push_back(0); v4.push_back(0); // zone count 0 const ComponentState back = deserializeComponentState(v4, 44100.0); CHECK(back.previewVelocity == kPreviewVelocityDefault); } static void testComponentStateV6TruncatedVelocity() { // A v6 blob truncated inside the header before the velocity byte (mode + full marker present, // velocity byte cut) -> empty, with the mid velocity default holding (bounded read, never throws). std::vector t{6, 0, 0, 0, 1}; // version 6, mode byte for (int i = 0; i < 8; ++i) t.push_back(0); // full marker, no velocity byte const ComponentState back = deserializeComponentState(t, 44100.0); CHECK(back.previewVelocity == kPreviewVelocityDefault); CHECK(back.selectionId.empty() && back.map.zones.empty()); } // --- v7 component state: the Phase S voice-system fields (count / mode / trigger) ------------- static void testComponentStateVoiceSystemRoundTrip() { // Non-default values on all three fields prove the bytes are read back, not defaulted; the // envelope neighbours (mode, marker, velocity, selection, zones) ride alongside intact. ComponentState s; s.selectionId = "pick"; s.channelMode = ChannelMode::Stereo; s.lastConsumedAssignGeneration = 42; s.previewVelocity = 99; s.voiceCount = 5; s.voiceMode = VoiceMode::Mono; s.monoTrigger = MonoTrigger::Legato; s.map.zones.push_back(zone("z0", 0, 127, /*override=*/std::nullopt)); const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0); CHECK(back.voiceCount == 5); CHECK(back.voiceMode == VoiceMode::Mono); CHECK(back.monoTrigger == MonoTrigger::Legato); CHECK(back.channelMode == ChannelMode::Stereo); CHECK(back.lastConsumedAssignGeneration == 42); CHECK(back.previewVelocity == 99); CHECK(back.selectionId == "pick"); CHECK(back.map.zones.size() == 1 && back.map.zones[0].sampleId == "z0"); } static void testComponentStateVoiceDefaultsRoundTrip() { // A default-constructed state carries {16, Poly, Retrigger} — the pre-Phase-S behavior — // and round-trips it. Locks the constants the engine + editor share. const ComponentState back = deserializeComponentState(serializeComponentState(ComponentState{}), 44100.0); CHECK(back.voiceCount == kDefaultVoiceCount); CHECK(kDefaultVoiceCount == 16 && kMinVoiceCount == 1 && kMaxVoiceCount == 32); CHECK(back.voiceMode == VoiceMode::Poly); CHECK(back.monoTrigger == MonoTrigger::Retrigger); } static void testComponentStateVoiceCountExtremesRoundTrip() { // Both range edges survive the single-byte field exactly. for (int vc : {kMinVoiceCount, kMaxVoiceCount}) { ComponentState s; s.voiceCount = vc; const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0); CHECK(back.voiceCount == vc); } } static void testComponentStateVoiceCountWriterClamps() { // The WRITER never emits an out-of-range byte: above-max clamps to max; a nonsensical // below-min value (a programming error upstream) falls back to the default. ComponentState hi; hi.voiceCount = 99; CHECK(deserializeComponentState(serializeComponentState(hi), 44100.0).voiceCount == kMaxVoiceCount); ComponentState lo; lo.voiceCount = 0; CHECK(deserializeComponentState(serializeComponentState(lo), 44100.0).voiceCount == kDefaultVoiceCount); } static void testComponentStateV6LiftsVoiceDefaults() { // A GENUINE v6 blob (version tag 6: mode, marker, velocity, id, zones — NO voice bytes) // lifts to the Phase S voice defaults {16, Poly, Retrigger}, its other fields intact. // Hand-built (serializeComponentState now emits v7, so it cannot make a v6 blob). This // proves an already-saved pre-Phase-S instance restores playing exactly as it did. std::vector v6; v6.push_back(6); v6.push_back(0); v6.push_back(0); v6.push_back(0); // version 6 v6.push_back(1); // channel mode = stereo for (int i = 0; i < 8; ++i) v6.push_back(0); // marker = 0 v6.push_back(111); // preview velocity const std::string id = "saved"; v6.push_back(static_cast(id.size())); v6.push_back(0); v6.push_back(0); v6.push_back(0); v6.insert(v6.end(), id.begin(), id.end()); v6.push_back(0); v6.push_back(0); v6.push_back(0); v6.push_back(0); // zone count 0 const ComponentState back = deserializeComponentState(v6, 44100.0); CHECK(back.voiceCount == kDefaultVoiceCount); CHECK(back.voiceMode == VoiceMode::Poly); CHECK(back.monoTrigger == MonoTrigger::Retrigger); CHECK(back.previewVelocity == 111); // the v6 byte still honored CHECK(back.channelMode == ChannelMode::Stereo); CHECK(back.selectionId == "saved"); CHECK(back.map.zones.empty()); } static void testComponentStateV7CorruptVoiceBytesFallBack() { // Out-of-range voice bytes in a v7 blob fall back to each field's DEFAULT (the // previewVelocity corrupt-byte precedent) — a corrupt blob never silences or distorts the // instance to an edge the user never chose. Build v7 by serializing, then vandalize the // three voice bytes in place (offsets: 4 version + 1 mode + 8 marker + 1 velocity = 14). ComponentState s; s.voiceCount = 7; s.voiceMode = VoiceMode::Mono; s.monoTrigger = MonoTrigger::Legato; std::vector bytes = serializeComponentState(s); bytes[14] = 0; // voice count 0: below kMinVoiceCount bytes[15] = 7; // voice mode: not a legal {0,1} value bytes[16] = 9; // mono trigger: not a legal {0,1} value const ComponentState back = deserializeComponentState(bytes, 44100.0); CHECK(back.voiceCount == kDefaultVoiceCount); CHECK(back.voiceMode == VoiceMode::Poly); // non-1 mode byte -> Poly default CHECK(back.monoTrigger == MonoTrigger::Retrigger); } static void testComponentStateV7TruncatedVoiceBytes() { // A v7 blob cut INSIDE the three voice bytes -> empty, defaults holding (bounded read). std::vector t{7, 0, 0, 0, 1}; // version 7, mode byte for (int i = 0; i < 8; ++i) t.push_back(0); // full marker t.push_back(64); // velocity byte t.push_back(16); // voice count only — const ComponentState back = deserializeComponentState(t, 44100.0); // mode/trigger cut CHECK(back.voiceCount == kDefaultVoiceCount); CHECK(back.voiceMode == VoiceMode::Poly); CHECK(back.monoTrigger == MonoTrigger::Retrigger); CHECK(back.selectionId.empty() && back.map.zones.empty()); } // --- MERGE COMPOSITION (S9 v5 marker envelope x S15/S16 v3 play-param payload) ---------------- // // The merge of ps-w9-t1-sync (envelope v5, adds the consumed-assignment marker) and // ps-w9-t2-modes (payload v3, adds the per-zone play params) makes THREE combinations first // reachable. Each pre-existing suite covers one axis in isolation; these lock the axes together. static void testV5EnvelopeWithMarkerAndPlayParamsRoundTrip() { // (a) The full v5 face: channelMode + the S8/S9 consumed marker (envelope) AND zones carrying // S15/S16 play params (payload) must ALL survive one serialize/deserialize. The two extensions // sit on orthogonal tracks (envelope vs self-versioned payload); this proves they compose with // no field cross-talk — neither the marker read nor the play-param read consumes the other's bytes. ComponentState s; s.selectionId = "pick"; s.channelMode = ChannelMode::Stereo; s.lastConsumedAssignGeneration = 1700000123456LL; // > INT32_MAX -> exercises the full 8-byte field PerformanceZone z = zone("z0", 0, 127, /*override=*/48); z.play.playMode = PlayMode::Trigger; z.play.adsr.holdSeconds = 0.093; z.play.trigger.lengthFraction = 0.625; z.play.trigger.fadeInFrames = 32; z.play.trigger.fadeOutFrames = 96; z.play.pitchEngine = PitchEngine::Varispeed; z.play.pitchEnv.enabled = true; z.play.pitchEnv.attackSeconds = 0.00018; z.play.pitchEnv.decaySeconds = 0.0145; z.play.pitchEnv.peakSemitones = 12.5; s.map.zones.push_back(z); 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"); CHECK(back.map.zones.size() == 1); if (back.map.zones.size() != 1) return; const ZonePlaySeconds& p = back.map.zones[0].play; // payload: play params CHECK(p.playMode == PlayMode::Trigger); CHECK(p.adsr.holdSeconds == 0.093); CHECK(p.trigger.lengthFraction == 0.625); CHECK(p.trigger.fadeInFrames == 32 && p.trigger.fadeOutFrames == 96); CHECK(p.pitchEngine == PitchEngine::Varispeed); CHECK(p.pitchEnv.enabled && p.pitchEnv.attackSeconds == 0.00018 && p.pitchEnv.decaySeconds == 0.0145 && p.pitchEnv.peakSemitones == 12.5); } // Hand-build ONE v3 zone record (marker-versioned payload body) for a single-zone map. Emits the // exact on-wire order the header's PAYLOAD v3 spec + putZonesPayload write: id, lo/hi, no root/loop/ // start overrides, then the always-present S15/S16 play tail. Used to synthesize the two v4 blobs // below WITHOUT serializeComponentState (which now emits v5) — so the reader's widened accept-chain // is exercised against a genuine, older-envelope byte layout rather than a self-produced buffer. static std::vector handBuildV3PayloadOneZone(const std::string& id) { std::vector b; auto u32 = [&](std::uint32_t v) { b.push_back(v & 0xFF); b.push_back((v >> 8) & 0xFF); b.push_back((v >> 16) & 0xFF); b.push_back((v >> 24) & 0xFF); }; auto u64 = [&](std::uint64_t v) { for (int i = 0; i < 8; ++i) b.push_back(static_cast((v >> (i * 8)) & 0xFF)); }; auto dbl = [&](double d) { std::uint64_t bits; std::memcpy(&bits, &d, 8); u64(bits); }; u32(kZonesFormatMarker); u32(3); // PAYLOAD VERSION 3 (S15/S16 play tail present) u32(1); // zone count 1 u32(static_cast(id.size())); b.insert(b.end(), id.begin(), id.end()); u32(10); // lowNote u32(70); // highNote b.push_back(0); // hasRootOverride = 0 b.push_back(0); // hasLoopOverride = 0 b.push_back(0); // hasStartPoint = 0 // Always-present v3 play tail: Trigger, hold, lengthFraction, fades, Varispeed, env off. b.push_back(1); // playMode = Trigger u64(static_cast(2048)); // adsr.holdFrames dbl(0.5); // trigger.lengthFraction u64(static_cast(16)); // trigger.fadeInFrames u64(static_cast(48)); // trigger.fadeOutFrames b.push_back(0); // pitchEngine = Varispeed b.push_back(0); // pitchEnv.enabled = 0 u64(0); // pitchEnv.attackFrames u64(0); // pitchEnv.decayFrames dbl(0.0); // pitchEnv.peakSemitones return b; } static void testV4BlobWithPlayParamsLiftsMarkerZeroKeepsPlay() { // (b) + (c) unified: a GENUINE v4 ENVELOPE blob (version tag 4: mode byte, id, then the zones // payload — NO 8-byte marker) whose zones payload is PAYLOAD v3 (the exact shape an S15-test-build // save produced). Under the widened accept-chain it must (b) lift lastConsumedAssignGeneration to // 0 AND (c) deserialize its payload-v3 play params intact. This is the precise blob a user who // saved on the S15 test build (envelope v4 + payload v3) would hold; the v4 lift branch delegates // zones to readZonesPayload, which self-selects the v3 record shape from the payload marker — so // the two v4 layouts (S7-era payload-v2, S15-era payload-v3) are UNAMBIGUOUS, distinguished // inside the payload, not on the envelope. std::vector v4; v4.push_back(4); v4.push_back(0); v4.push_back(0); v4.push_back(0); // ENVELOPE version 4 v4.push_back(1); // channel mode = stereo const std::string id = "s15saved"; v4.push_back(static_cast(id.size())); v4.push_back(0); v4.push_back(0); v4.push_back(0); // idLen (LE) v4.insert(v4.end(), id.begin(), id.end()); const std::vector payload = handBuildV3PayloadOneZone("zv3"); v4.insert(v4.end(), payload.begin(), payload.end()); 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"); CHECK(back.map.zones.size() == 1); // (c) payload-v3 zone parsed under widened check if (back.map.zones.size() != 1) return; CHECK(back.map.zones[0].sampleId == "zv3"); 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 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); CHECK(p.pitchEnv.enabled == false); } // --- S15/S16 zone-payload v3: per-zone play params round-trip + back-compat lift ------------- static void testPlayParamsRoundTrip() { // A zone carrying explicit S15/S16 play params (Trigger mode, hold seconds, source-frame fades, // Varispeed engine, pitch env on) must round-trip ALL fields losslessly through the v5 tail. PerformanceMap m; PerformanceZone z = zone("lead", 20, 100, /*override=*/55); z.play.playMode = PlayMode::Trigger; z.play.adsr.holdSeconds = 0.028; // wall-clock seconds z.play.trigger.lengthFraction = 0.375; z.play.trigger.fadeInFrames = 64; // source frames z.play.trigger.fadeOutFrames = 128; z.play.pitchEngine = PitchEngine::Varispeed; z.play.pitchEnv.enabled = true; z.play.pitchEnv.attackSeconds = 0.0002; // wall-clock seconds z.play.pitchEnv.decaySeconds = 0.011; z.play.pitchEnv.peakSemitones = -7.5; m.zones.push_back(z); 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; CHECK(p.playMode == PlayMode::Trigger); CHECK(p.adsr.holdSeconds == 0.028); // exact double round-trip CHECK(p.trigger.lengthFraction == 0.375); // exact double round-trip CHECK(p.trigger.fadeInFrames == 64); CHECK(p.trigger.fadeOutFrames == 128); CHECK(p.pitchEngine == PitchEngine::Varispeed); CHECK(p.pitchEnv.enabled == true); CHECK(p.pitchEnv.attackSeconds == 0.0002); CHECK(p.pitchEnv.decaySeconds == 0.011); CHECK(p.pitchEnv.peakSemitones == -7.5); // exact double round-trip } static void testPlayParamsComposeWithLoopStart() { // S11 (loop/start) x S15/S16 (play params) tails co-exist per zone: both round-trip together. PerformanceMap m; PerformanceZone z = zone("pad", 0, 60); SampleLoop lp; lp.hasLoop = true; lp.start = 111; lp.end = 222; z.loopOverride = lp; z.startPoint = 333; z.play.playMode = PlayMode::Gate; z.play.adsr.holdSeconds = 0.0225; z.play.pitchEngine = PitchEngine::Preserve; m.zones.push_back(z); 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() && back.zones[0].loopOverride->start == 111 && back.zones[0].loopOverride->end == 222); CHECK(back.zones[0].startPoint.has_value() && *back.zones[0].startPoint == 333); CHECK(back.zones[0].play.adsr.holdSeconds == 0.0225); CHECK(back.zones[0].play.pitchEngine == PitchEngine::Preserve); } // --- S-VIEW-6 key-tracking scalar: v6 round-trip + resolve-through + back-compat lift ---------- static void testKeyTrackRoundTrip() { // A per-zone keyTrack survives the payload-v6 round trip losslessly (exact double). A second // zone left at the default proves the field is per-record and the default is 1.0. PerformanceMap m; PerformanceZone z = zone("lead", 20, 100, /*override=*/55); z.keyTrack = 0.5; m.zones.push_back(z); m.zones.push_back(zone("pad", 0, 19)); // default keyTrack (1.0) const PerformanceMap back = deserializePerformance(serializePerformance(m), 44100.0); CHECK(back.zones.size() == 2); if (back.zones.size() != 2) return; CHECK(back.zones[0].keyTrack == 0.5); // exact double round-trip CHECK(back.zones[1].keyTrack == 1.0); // untouched zone keeps the 100% default } static void testKeyTrackThroughComponentEnvelope() { // keyTrack round-trips through the ComponentState envelope too (the composition property: // the zones payload is envelope-independent, so it carries the v6 tail unchanged). ComponentState s; s.selectionId = "pick"; PerformanceZone z = zone("pick", 0, 127); z.keyTrack = 2.0; s.map.zones.push_back(z); const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0); CHECK(back.map.zones.size() == 1); if (back.map.zones.size() != 1) return; CHECK(back.map.zones[0].keyTrack == 2.0); } static void testKeyTrackResolvesToZone() { // resolvePerformance carries keyTrack from the PerformanceZone through to the ResolvedZone, // so the keymap build (and thus the repitch engine) sees the authored scalar. const std::string json = bookJson({makeSample("a", "Kick", "b/a.wav", 36)}, {}); PerformanceMap m; PerformanceZone z = zone("a", 0, 127); z.keyTrack = 0.0; // no tracking m.zones.push_back(z); const ResolvedPerformance r = resolvePerformance(json, m); CHECK(r.zones.size() == 1); if (r.zones.size() != 1) return; CHECK(r.zones[0].keyTrack == 0.0); } static void testKeyTrackV5BackCompatLiftsToUnity() { // A v5 PAYLOAD blob (marker + version 5 + full play tail but NO keyTrack field) lifts every // zone to keyTrack == 1.0 (the PerformanceZone default) — so an instance saved BEFORE S-VIEW-6 // repitches BIT-IDENTICALLY (100% ET). Hand-build the exact v5 record shape. std::vector b; auto u32 = [&](std::uint32_t v) { b.push_back(v & 0xFF); b.push_back((v >> 8) & 0xFF); b.push_back((v >> 16) & 0xFF); b.push_back((v >> 24) & 0xFF); }; auto f64 = [&](double d) { std::uint64_t bits; std::memcpy(&bits, &d, sizeof(bits)); for (int i = 0; i < 8; ++i) b.push_back(static_cast((bits >> (i * 8)) & 0xFF)); }; auto i64 = [&](std::int64_t v) { std::uint64_t bits = static_cast(v); for (int i = 0; i < 8; ++i) b.push_back(static_cast((bits >> (i * 8)) & 0xFF)); }; u32(kPerformanceStateVersion); // envelope version (2) u32(kZonesFormatMarker); // marker -> a versioned payload u32(5); // PAYLOAD VERSION 5 (pre-S-VIEW-6, no keyTrack tail) u32(1); // zone count 1 const std::string id = "v5saved"; u32(static_cast(id.size())); b.insert(b.end(), id.begin(), id.end()); u32(10); u32(70); // low/high b.push_back(0); // hasRootOverride = 0 b.push_back(0); // hasLoopOverride = 0 b.push_back(0); // hasStartPoint = 0 // v5 play tail (order matches putZonesPayload): playMode, hold, len, fadeIn, fadeOut, engine, // envEnabled, envAttack, envDecay, peak, attack, decay, sustain, release. b.push_back(0); // playMode = Gate f64(0.0); // adsr.holdSeconds f64(1.0); // trigger.lengthFraction i64(0); i64(0); // trigger fades (source frames) b.push_back(1); // pitchEngine = Preserve b.push_back(0); // pitchEnv.enabled = false f64(0.0); f64(0.0); f64(0.0); // pitchEnv attack/decay/peak f64(0.003); f64(0.0); f64(1.0); f64(0.060); // adsr A/D/S/R (tier-0 seconds) const PerformanceMap back = deserializePerformance(b, 44100.0); CHECK(back.zones.size() == 1); if (back.zones.size() != 1) return; CHECK(back.zones[0].sampleId == "v5saved"); CHECK(back.zones[0].keyTrack == 1.0); // no keyTrack tail -> default 1.0 (bit-identical repitch) } // --- S-VIEW-9 velocity->amp curve: v7 round-trip + resolve-through + v6 back-compat lift --------- static void testVelocityCurveRoundTrip() { // A per-zone velocity curve survives the payload-v7 round trip losslessly (exact point coords). // A second zone left at the flat default proves the field is per-record and defaults to flat y=1. PerformanceMap m; PerformanceZone z = zone("lead", 20, 100); z.velocityCurve = vst::VelocityCurve::linear(); z.velocityCurve.addPoint(60.0, 0.3); // an interior knot to exercise multi-point round-trip m.zones.push_back(z); m.zones.push_back(zone("pad", 0, 19)); // default flat curve const PerformanceMap back = deserializePerformance(serializePerformance(m), 44100.0); CHECK(back.zones.size() == 2); if (back.zones.size() != 2) return; CHECK(back.zones[0].velocityCurve.equals(z.velocityCurve)); // exact point round-trip CHECK(back.zones[1].velocityCurve.equals(vst::VelocityCurve::flat())); // default preserved // And the flat default really is unity everywhere (R10-F1 Option A), not the old linear ramp. CHECK(back.zones[1].velocityCurve.eval(1.0) == 1.0); CHECK(back.zones[1].velocityCurve.eval(64.0) == 1.0); } static void testVelocityCurveThroughComponentEnvelope() { // The curve round-trips through the ComponentState envelope too (zones-payload is envelope- // independent, so it carries the v7 tail unchanged). ComponentState s; s.selectionId = "pick"; PerformanceZone z = zone("pick", 0, 127); z.velocityCurve = vst::VelocityCurve::linear(); s.map.zones.push_back(z); const ComponentState back = deserializeComponentState(serializeComponentState(s), 44100.0); CHECK(back.map.zones.size() == 1); if (back.map.zones.size() != 1) return; CHECK(back.map.zones[0].velocityCurve.equals(vst::VelocityCurve::linear())); } static void testVelocityCurveResolvesToZone() { // resolvePerformance carries the curve from PerformanceZone through to ResolvedZone, so the // keymap build (and thus the voice engine at start()) sees the authored curve. const std::string json = bookJson({makeSample("a", "Kick", "b/a.wav", 36)}, {}); PerformanceMap m; PerformanceZone z = zone("a", 0, 127); z.velocityCurve = vst::VelocityCurve::linear(); m.zones.push_back(z); const ResolvedPerformance r = resolvePerformance(json, m); CHECK(r.zones.size() == 1); if (r.zones.size() != 1) return; CHECK(r.zones[0].velocityCurve.equals(vst::VelocityCurve::linear())); } // FA1 bug 3a — the COMPOSED end-to-end regression, mirroring the processor's reload composition // exactly: an authored curve survives the component-state round-trip (the save/load seam), then // resolvePerformance -> buildZonedKeymap -> VoiceEngine (constructed with a Preserve window, the // DAW configuration) -> render, and the rendered level tracks velocity through the curve. This // is the full pure slice of the click-to-sound path; only the bridge read + WAV decode (shell // I/O) are outside it. A y=x curve at velocity 1 must be near-silent — NOT max volume. static void testVelocityCurveEndToEndThroughReloadComposition() { // 1. The instrument's own state: one full-keyboard zone with a LINEAR curve (the exact edit // Daniel made), round-tripped through the v7 component-state wire (save -> load). ComponentState s; s.selectionId = "a"; PerformanceZone z = zone("a", 0, 127); z.velocityCurve = vst::VelocityCurve::linear(); s.map.zones.push_back(z); const ComponentState back = deserializeComponentState(serializeComponentState(s), 48000.0); CHECK(back.map.zones.size() == 1); if (back.map.zones.size() != 1) return; // 2. Resolve against a live bank blob (the shared bank_book parse, root 60 intrinsic). const std::string json = bookJson({makeSample("a", "Kick", "reasampler_bank/a.wav", 60)}, {}); const ResolvedPerformance rp = resolvePerformance(json, back.map); CHECK(rp.zones.size() == 1); if (rp.zones.size() != 1) return; // The round-tripped zone still runs the PRESERVE product default (the DAW engine config). CHECK(rp.zones[0].play.pitchEngine == PitchEngine::Preserve); // 3. Build the zoned keymap from decoded DC-1 PCM and play it through an engine constructed // the way reloadFromBank constructs it (Preserve voices pre-sized to a real window). auto steadyLevelAt = [&](int vel) -> double { DecodedZonePcm pcm; pcm.monoFrames.assign(4000, 1.0f); pcm.sampleRate = 48000; const Keymap km = buildZonedKeymap(rp.zones, {pcm}); VoiceEngine eng(16, km, /*preserveCap=*/8, /*window=*/256); eng.noteOn(62, vel); // transposed: the genuine OLA shifter path std::vector out; eng.render(out, 1000); return static_cast(out[900]); // steady state (ring fully DC past the window) }; CHECK(approx(steadyLevelAt(127), 1.0)); CHECK(approx(steadyLevelAt(64), 64.0 / 127.0)); CHECK(steadyLevelAt(1) < 0.02); // velocity 1 through y=x: near-silent, never max volume } static void testVelocityCurveV6BackCompatLiftsToFlat() { // A v6 PAYLOAD blob (marker + version 6 + full play tail + keyTrack, but NO velocity-curve field) // lifts every zone to VelocityCurve::flat() (R10-F1 Option A — flat y=1). This is the DELIBERATE // non-back-compat behavior change: an instance saved BEFORE S-VIEW-9 now plays every velocity at // unity, NOT the old linear velocity/127. Hand-build the exact v6 record shape. std::vector b; auto u32 = [&](std::uint32_t v) { b.push_back(v & 0xFF); b.push_back((v >> 8) & 0xFF); b.push_back((v >> 16) & 0xFF); b.push_back((v >> 24) & 0xFF); }; auto f64 = [&](double d) { std::uint64_t bits; std::memcpy(&bits, &d, sizeof(bits)); for (int i = 0; i < 8; ++i) b.push_back(static_cast((bits >> (i * 8)) & 0xFF)); }; auto i64 = [&](std::int64_t v) { std::uint64_t bits = static_cast(v); for (int i = 0; i < 8; ++i) b.push_back(static_cast((bits >> (i * 8)) & 0xFF)); }; u32(kPerformanceStateVersion); // envelope version (2) u32(kZonesFormatMarker); // marker -> a versioned payload u32(6); // PAYLOAD VERSION 6 (pre-S-VIEW-9, keyTrack but no curve) u32(1); // zone count 1 const std::string id = "v6saved"; u32(static_cast(id.size())); b.insert(b.end(), id.begin(), id.end()); u32(10); u32(70); // low/high b.push_back(0); // hasRootOverride = 0 b.push_back(0); // hasLoopOverride = 0 b.push_back(0); // hasStartPoint = 0 // v5 play tail. b.push_back(0); // playMode = Gate f64(0.0); // adsr.holdSeconds f64(1.0); // trigger.lengthFraction i64(0); i64(0); // trigger fades b.push_back(1); // pitchEngine = Preserve b.push_back(0); // pitchEnv.enabled = false f64(0.0); f64(0.0); f64(0.0); // pitchEnv attack/decay/peak f64(0.003); f64(0.0); f64(1.0); f64(0.060); // adsr A/D/S/R f64(0.5); // v6 keyTrack (0.5) — present, but no curve tail follows const PerformanceMap back = deserializePerformance(b, 44100.0); CHECK(back.zones.size() == 1); if (back.zones.size() != 1) return; CHECK(back.zones[0].sampleId == "v6saved"); CHECK(back.zones[0].keyTrack == 0.5); // the v6 field still read correctly // No curve tail -> flat y=1 default (the deliberate behavior change). CHECK(back.zones[0].velocityCurve.equals(vst::VelocityCurve::flat())); CHECK(back.zones[0].velocityCurve.eval(20.0) == 1.0); // a soft hit now plays at unity } static void testPlayParamsV2BackCompatLiftsToDefaults() { // A pre-S15 PAYLOAD v2 blob (marker + version 2 + record with the S11 tail but NO play tail) // lifts each zone to the PRODUCT defaults: Gate + Preserve (S16-F1) + no fades + env off — the // deliberate behavior change for already-saved instruments. Hand-build a v2 record exactly. std::vector b; auto u32 = [&](std::uint32_t v) { b.push_back(v & 0xFF); b.push_back((v >> 8) & 0xFF); b.push_back((v >> 16) & 0xFF); b.push_back((v >> 24) & 0xFF); }; u32(kPerformanceStateVersion); // envelope version (2) u32(kZonesFormatMarker); // marker -> a versioned payload u32(2); // PAYLOAD VERSION 2 (S11, no play tail) u32(1); // zone count 1 const std::string id = "old"; u32(static_cast(id.size())); b.insert(b.end(), id.begin(), id.end()); u32(5); // lowNote u32(80); // highNote 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, 44100.0); CHECK(back.zones.size() == 1); if (back.zones.size() != 1) return; CHECK(back.zones[0].sampleId == "old"); // Lifted to product defaults: Gate play mode, PRESERVE engine (the S16-F1 default), env off. CHECK(back.zones[0].play.playMode == PlayMode::Gate); CHECK(back.zones[0].play.pitchEngine == kDefaultPitchEngine); // == Preserve CHECK(back.zones[0].play.pitchEnv.enabled == false); CHECK(back.zones[0].play.adsr.holdSeconds == 0.0); } static void testPlayParamsThroughComponentEnvelope() { // The play params round-trip through the v4 COMPONENT envelope too (the composition property: // the zones payload is envelope-independent, so v4 {channelMode, selection, zones} carries them). ComponentState s; s.selectionId = "pick"; s.channelMode = ChannelMode::Stereo; PerformanceZone z = zone("z", 0, 127); z.play.playMode = PlayMode::Trigger; z.play.trigger.lengthFraction = 0.9; z.play.pitchEngine = PitchEngine::Varispeed; s.map.zones.push_back(z); 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; CHECK(back.map.zones[0].play.playMode == PlayMode::Trigger); CHECK(back.map.zones[0].play.trigger.lengthFraction == 0.9); CHECK(back.map.zones[0].play.pitchEngine == PitchEngine::Varispeed); } // --- S12 domain fix: wall-clock ADSR stored as SECONDS, resolved to frames at the live rate. --- // // These tests replace the R1/R2 flag/nominal-frame tests. The stored domain is seconds (rate-free); // the keymap build resolves seconds -> frames against whatever WAV rate is live. The lift -> // commit -> reload sequence must stay rate-correct at every rate (the R2 blocker). // All five AHDSR fields round-trip through the v5 payload as SECONDS (exact double round-trip). static void testFullAdsrSecondsRoundTrip() { PerformanceMap m; PerformanceZone z = zone("pad", 0, 127); z.play.playMode = PlayMode::Gate; z.play.adsr.attackSeconds = 0.01; z.play.adsr.holdSeconds = 0.02; z.play.adsr.decaySeconds = 0.1; z.play.adsr.sustainLevel = 0.7; z.play.adsr.releaseSeconds = 0.2; z.play.pitchEngine = PitchEngine::Preserve; m.zones.push_back(z); 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; CHECK(a.attackSeconds == 0.01); CHECK(a.holdSeconds == 0.02); CHECK(a.decaySeconds == 0.1); CHECK(a.sustainLevel == 0.7); // exact double round-trip via bit-cast CHECK(a.releaseSeconds == 0.2); CHECK(back.zones[0].play.pitchEngine == PitchEngine::Preserve); } // A legacy PAYLOAD v3 blob (Daniel's beta projects — has holdFrames but no A/D/S/R) lifts the // absent A/D/S/R to the tier-0 SECONDS defaults (0.003 / 0 / 1.0 / 0.060), NO rate involved: they // were always the seconds constants. holdSeconds converts from the v3 44.1k-nominal frame count. static void testV3BlobLiftsAdsrToSecondsDefaults() { std::vector 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 payload = handBuildV3PayloadOneZone("old"); blob.insert(blob.end(), payload.begin(), payload.end()); 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 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 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 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. static void testKeymapBuildResolvesSecondsToFramesAtEachRate() { const auto rnd = [](double s, int rate) { return static_cast(s * static_cast(rate) + 0.5); }; for (int rate : {44100, 48000, 96000}) { // (a) DEFAULT zone (round-tripped through serialize/deserialize) -> tier-0 seconds. { PerformanceMap m; m.zones.push_back(zone("def", 0, 127)); // product-default play (tier-0 AHDSR seconds) 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; rz.play = back.zones[0].play; const DecodedZonePcm pcm{{0.5f}, rate}; const Keymap km = buildZonedKeymap({rz}, {pcm}); CHECK(km.samples.size() == 1); if (km.samples.empty()) continue; const AdsrParams& a = km.samples[0].play.adsr; CHECK(a.attackFrames == rnd(0.003, rate)); // tier-0 attack at this rate CHECK(a.decayFrames == 0); CHECK(a.sustainLevel == 1.0); // level, never rate-scaled CHECK(a.releaseFrames == rnd(0.060, rate)); // tier-0 release at this rate } // (b) AUTHORED zone -> round(seconds * rate) at this rate. { PerformanceMap m; PerformanceZone z = zone("auth", 0, 127); z.play.adsr.attackSeconds = 0.01; z.play.adsr.decaySeconds = 0.1; z.play.adsr.sustainLevel = 0.7; z.play.adsr.releaseSeconds = 0.2; m.zones.push_back(z); 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; rz.play = back.zones[0].play; const DecodedZonePcm pcm{{0.5f}, rate}; const Keymap km = buildZonedKeymap({rz}, {pcm}); CHECK(km.samples.size() == 1); if (km.samples.empty()) continue; const AdsrParams& a = km.samples[0].play.adsr; CHECK(a.attackFrames == rnd(0.01, rate)); CHECK(a.decayFrames == rnd(0.1, rate)); CHECK(a.sustainLevel == 0.7); // level, never rate-scaled CHECK(a.releaseFrames == rnd(0.2, rate)); } } } // buildTier0Keymap resolves the default (seconds) play arg to frames at the WAV's rate — the // single-capture fast path. At 48k the tier-0 attack is round(0.003*48000)=144, release // round(0.060*48000)=2880 — identical wall-clock to any rate, no baked constant. static void testBuildTier0KeymapResolvesSecondsAt48k() { const Keymap km = buildTier0Keymap({0.5f}, 48000, 60, SampleLoop{}); CHECK(km.samples.size() == 1); if (km.samples.empty()) return; const AdsrParams& a = km.samples[0].play.adsr; CHECK(a.attackFrames == 144); // round(0.003 * 48000) CHECK(a.decayFrames == 0); CHECK(a.sustainLevel == 1.0); // level, not a time CHECK(a.releaseFrames == 2880); // round(0.060 * 48000) } // --- single-capture zone lifecycle: reconcileSingleCaptureZones (zone-bleed fix, 3a) --- // The bled state: two full-range Sample-face zones. Reconcile on load keeps only the // selected sample's zone, params intact (a return to that sample restores its edits). static void testReconcileKeepsOnlySelectedFullRangeZone() { PerformanceMap m; m.zones.push_back(zone("a", 0, 127)); PerformanceZone b = zone("b", 0, 127); b.keyTrack = 0.5; // distinctive param — must survive the reconcile m.zones.push_back(b); CHECK(reconcileSingleCaptureZones(m, "b")); CHECK(m.zones.size() == 1); CHECK(m.zones.size() == 1 && m.zones[0].sampleId == "b"); CHECK(m.zones.size() == 1 && m.zones[0].keyTrack == 0.5); } // Loading a sample with no zone yet empties a Sample-face-shaped map — the shell then // plays the selection via the Tier-0 fast path (product defaults), never the stale zone. static void testReconcileClearsWhenSelectionUnzoned() { PerformanceMap m; m.zones.push_back(zone("a", 0, 127)); CHECK(reconcileSingleCaptureZones(m, "b")); CHECK(m.zones.empty()); } // Any narrow key range marks Zone-view authorship: the map (including a legitimate // full-range fallback zone) is untouched — first-match order is load-bearing there. static void testReconcileLeavesAuthoredMapUntouched() { PerformanceMap m; m.zones.push_back(zone("a", 60, 72)); // authored narrow range m.zones.push_back(zone("b", 0, 127)); // authored full-range fallback layer CHECK(!reconcileSingleCaptureZones(m, "c")); CHECK(m.zones.size() == 2); CHECK(m.zones.size() == 2 && m.zones[0].sampleId == "a" && m.zones[1].sampleId == "b"); } // A map already holding exactly the selection's one zone is coherent — no change reported, // so callers do not republish/reload needlessly. static void testReconcileNoOpWhenAlreadyCoherent() { PerformanceMap m; m.zones.push_back(zone("a", 0, 127)); CHECK(!reconcileSingleCaptureZones(m, "a")); CHECK(m.zones.size() == 1 && m.zones[0].sampleId == "a"); } // Guards: an empty map and an empty selection both leave the map untouched. static void testReconcileGuards() { PerformanceMap empty; CHECK(!reconcileSingleCaptureZones(empty, "a")); PerformanceMap m; m.zones.push_back(zone("a", 0, 127)); m.zones.push_back(zone("b", 0, 127)); CHECK(!reconcileSingleCaptureZones(m, "")); // no selection -> never mutate CHECK(m.zones.size() == 2); } // The reported browse sequence, end to end at the pure layer: edit sample A (full-range // zone materialized), browse-load B, edit B (zone appended AFTER A's). First proves the // bug — first-match resolve plays A's zone while B is loaded — then proves the reconcile // at the load step makes the loaded sample's zone the one resolve() returns. static void testReconcileBrowseSequenceNoShadowing() { const std::string json = bookJson( {makeSample("a", "A", "reasampler_bank/a.wav", 60), makeSample("b", "B", "reasampler_bank/b.wav", 60)}, {}); PerformanceMap m; m.zones.push_back(zone("a", 0, 127)); // edit on A materializes A's zone m.zones.push_back(zone("b", 0, 127)); // browse to B (pre-fix: no reconcile) + edit B // PCM markers: A decodes to 0.75, B to 0.25 — which zone resolve() picked is audible // in frames[0] of the resolved sample. const auto keymapFor = [&](const PerformanceMap& map) { const ResolvedPerformance r = resolvePerformance(json, map); std::vector decoded; for (const ResolvedZone& rz : r.zones) { decoded.push_back(DecodedZonePcm{ {rz.relativePath == "reasampler_bank/a.wav" ? 0.75f : 0.25f}, 44100}); } return buildZonedKeymap(r.zones, decoded); }; // The bled map: the engine resolves A's zone (index 0) — the shadowing bug. const Keymap bled = keymapFor(m); CHECK(bled.zones.size() == 2); const ZoneResolution shadow = bled.resolve(60, 100); CHECK(shadow.matched); CHECK(shadow.matched && bled.samples[bled.zones[shadow.zoneIndex].sampleIndex].frames[0] == 0.75f); // The fix at the load step: reconcile on the selection change keeps only B's zone — // the loaded sample's zone IS the zone resolve() returns, at every note. CHECK(reconcileSingleCaptureZones(m, "b")); const Keymap fixed = keymapFor(m); CHECK(fixed.zones.size() == 1); for (int note : {0, 60, 127}) { const ZoneResolution r = fixed.resolve(note, 100); CHECK(r.matched); CHECK(r.matched && fixed.samples[fixed.zones[r.zoneIndex].sampleIndex].frames[0] == 0.25f); } } int main() { testSelectByIdHit(); testSelectEmptyIdIsSilence(); testSelectUnknownIdIsSilence(); testSelectRootNoteDefault(); testSelectLoopThreaded(); testSelectNoLoopIsAbsent(); testSelectEmptyBlob(); testSelectMalformedBlob(); testSelectZeroSamples(); testListSamplesOrdinalOrder(); testListSamplesCarriesCardMetadata(); testListSamplesEmptyAndMalformed(); testListBanksOrdinalOrder(); testListBanksEmptyAndMalformed(); testDownmixMonoPassthrough(); testDownmixStereoAverages(); testDownmixThreeChannelAverages(); testDownmixDegenerate(); testBuildKeymapSingleFullZone(); testSelectionStateRoundTrip(); testSelectionStateEmptyId(); testSelectionStateWrongVersion(); testSelectionStateTruncated(); testWavTrimToDownmixPipelineStereo(); testWavTrimToDownmixPipelineMono(); testResolveEmptyMap(); testResolveEmptyBlob(); testResolveMultiZoneAcrossBanks(); testResolveStaleIdDropsZone(); testResolveRootPrecedence(); testResolveLoopThreaded(); testResolveLoopOverrideWins(); testResolveLoopOverrideDisablesLoop(); testBuildZonedKeymapMultiZone(); testBuildZonedKeymapThreadsLoopAndStart(); testBuildZonedKeymapDropsEmptyPcm(); testBuildZonedKeymapOverlapFirstWins(); testBuildZonedKeymapEmpty(); testPerformanceStateRoundTrip(); testPerformanceStateEmpty(); testPerformanceStateLoopStartRoundTrip(); testPerformanceStateV1PayloadBackCompat(); testPerformanceStateV1BackCompat(); testPerformanceStateGarbage(); testPerformanceStateNegativeNotesRoundTrip(); testPlayParamsRoundTrip(); testPlayParamsComposeWithLoopStart(); testKeyTrackRoundTrip(); testKeyTrackThroughComponentEnvelope(); testKeyTrackResolvesToZone(); testKeyTrackV5BackCompatLiftsToUnity(); testVelocityCurveRoundTrip(); testVelocityCurveThroughComponentEnvelope(); testVelocityCurveResolvesToZone(); testVelocityCurveEndToEndThroughReloadComposition(); testVelocityCurveV6BackCompatLiftsToFlat(); testPlayParamsV2BackCompatLiftsToDefaults(); testPlayParamsThroughComponentEnvelope(); testFullAdsrSecondsRoundTrip(); testV3BlobLiftsAdsrToSecondsDefaults(); testV3BlobLiftsAdsrAt96k(); testKeymapBuildResolvesSecondsToFramesAtEachRate(); testBuildTier0KeymapResolvesSecondsAt48k(); testComponentStateRoundTrip(); testComponentStateLoopStartRoundTrip(); testComponentStateSelectionOnlyNoZones(); testComponentStateEmptyIsEmpty(); testComponentStateV1BackCompat(); testComponentStateV2BackCompat(); testComponentStateGarbage(); testExtractChannelStereo(); testExtractChannelClampsToLast(); testDecodeChannelsMonoModeDownmixes(); testDecodeChannelsStereoModeStereoSource(); testDecodeChannelsStereoModeMonoSourceDualMono(); testBuildKeymapStereoCarriesSecondChannel(); testBuildKeymapMonoWhenNoSecondChannel(); testBuildKeymapDropsMismatchedSecondChannel(); testBuildZonedKeymapCarriesSecondChannel(); testComponentStateV4RoundTripStereo(); testComponentStateV4RoundTripMono(); testComponentStateV4DefaultIsMono(); testComponentStateV3LiftsToMono(); testComponentStateV1V2LiftToMono(); testComponentStateV4TruncatedModeByte(); testComponentStateV4StereoWithZoneOverridesRoundTrip(); testComponentStateV5MarkerRoundTrip(); testComponentStateDefaultMarkerIsZero(); testComponentStateV4LiftsMarkerToZero(); testComponentStateV5TruncatedMarker(); testComponentStatePreviewVelocityRoundTrip(); testComponentStateDefaultPreviewVelocityIsMid(); testComponentStatePreviewVelocityExtremes(); testComponentStateV5LiftsVelocityToMid(); testComponentStateV4LiftsVelocityToMid(); testComponentStateV6TruncatedVelocity(); testComponentStateVoiceSystemRoundTrip(); testComponentStateVoiceDefaultsRoundTrip(); testComponentStateVoiceCountExtremesRoundTrip(); testComponentStateVoiceCountWriterClamps(); testComponentStateV6LiftsVoiceDefaults(); testComponentStateV7CorruptVoiceBytesFallBack(); testComponentStateV7TruncatedVoiceBytes(); testV5EnvelopeWithMarkerAndPlayParamsRoundTrip(); testV4BlobWithPlayParamsLiftsMarkerZeroKeepsPlay(); testReconcileKeepsOnlySelectedFullRangeZone(); testReconcileClearsWhenSelectionUnzoned(); testReconcileLeavesAuthoredMapUntouched(); testReconcileNoOpWhenAlreadyCoherent(); testReconcileGuards(); testReconcileBrowseSequenceNoShadowing(); if (g_fail == 0) std::printf("sample_map: all tests passed\n"); return g_fail != 0; }