// 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 mapping heart: bank // blob -> selected capture (through the SHARED bank_book JSON parse), the channel policy, // the one parameter set's override-beats-intrinsic fold, and the SampleData build. // // The ComponentState wire ladder lives in test_component_state_io.cpp — its own module, its // own suite, since the Q-W2v split. // // 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 / resolve / build values are checked against independently // computed expectations. // // Covers: selectSample by-id hit (across pool + named banks), the policy reversal (empty / // stale id -> SILENCE nullopt, not the first sample), empty & malformed blob -> nullopt, // zero-samples -> nullopt, rootNote/loop/channel-count intrinsic threading incl. the // middle-C default; channelModeFor's auto-default rule; listSamples ordinal order + the // card metadata + empty/malformed; listBanks ordinal order (pool first); downmixToMono / // extractChannel / decodeChannels across both channel modes; the instance-owned refs // helpers (findRef / referencedSampleIds / refreshRefsFromBank / retainRefs) and the // legacy-lift decision; resolvePlay's seconds->frames conversion at the live rate; // resolveCapture's override-beats-intrinsic fold and the bank/refs paths' agreement; // buildSampleData's threading, per-decode rate resolution, and channel handling; the // selection-state round-trip; and the wav_trim -> extractFloatFrames -> downmixToMono // integration, which locks the interleave-stride contract across that seam. #include "../src/core/instrument/map/sample_map.h" #include "../src/core/instrument/map/component_state_io.h" // serializeSelection (the v1 blob) #include #include #include #include #include #include "../src/core/model/bank_book.h" #include "../src/core/model/bank_model.h" using namespace reasampler; using namespace reasampler::instrument::engine; using namespace reasampler::instrument::map; using namespace reasampler::capture; // wav_trim (WavLayout) using namespace reasampler::model; 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 // BankModel::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"); BankModel* 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 testSelectChannelCountThreaded() { // GA: the bank's capture channel-count intrinsic rides SelectedSample so the shell can // auto-default the channel mode (stereo capture -> Stereo). An entry without the // intrinsic yields 0 (unknown — the auto-default skips it). Sample st = makeSample("st", "Wide", "reasampler_bank/st.wav", 60); st.channelCount = 2; Sample mo = makeSample("mo", "Narrow", "reasampler_bank/mo.wav", 60); mo.channelCount = 1; const std::string json = bookJson({st, mo, makeSample("un", "Old", "reasampler_bank/un.wav", 60)}, {}); auto selSt = selectSample(json, "st"); CHECK(selSt && selSt->channelCount == 2); auto selMo = selectSample(json, "mo"); CHECK(selMo && selMo->channelCount == 1); auto selUn = selectSample(json, "un"); CHECK(selUn && selUn->channelCount == 0); // unstamped -> unknown, never a guess } // --- channelModeFor (GA auto-default rule) ------------------------------------------- static void testChannelModeForExplicitIsNeverFought() { // An explicit user choice is ALWAYS returned unchanged, regardless of channelCount. CHECK(channelModeFor(2, ChannelMode::Mono, true) == ChannelMode::Mono); CHECK(channelModeFor(1, ChannelMode::Stereo, true) == ChannelMode::Stereo); CHECK(channelModeFor(0, ChannelMode::Stereo, true) == ChannelMode::Stereo); } static void testChannelModeForUnknownCountIsNoOp() { // An unknown channel count (0 — an older bank entry) leaves the current mode unchanged. CHECK(channelModeFor(0, ChannelMode::Mono, false) == ChannelMode::Mono); CHECK(channelModeFor(0, ChannelMode::Stereo, false) == ChannelMode::Stereo); } static void testChannelModeForStereoCapture() { // A capture with channelCount >= 2 selects Stereo (regardless of current mode). CHECK(channelModeFor(2, ChannelMode::Mono, false) == ChannelMode::Stereo); CHECK(channelModeFor(2, ChannelMode::Stereo, false) == ChannelMode::Stereo); CHECK(channelModeFor(6, ChannelMode::Mono, false) == ChannelMode::Stereo); } static void testChannelModeForMonoCapture() { // A capture with channelCount == 1 selects Mono (ingest-imported mono files only). CHECK(channelModeFor(1, ChannelMode::Stereo, false) == ChannelMode::Mono); CHECK(channelModeFor(1, ChannelMode::Mono, false) == ChannelMode::Mono); } 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 } // --- 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)); } // --- 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 DecodedPcm 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 DecodedPcm 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 DecodedPcm 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 } // --- pS self-contained playback: the instance-owned sample refs (envelope v10) --------------- static SampleRefEntry refEntry(const std::string& id, const std::string& rel, int root, bool hasLoop = false, std::int64_t loopStart = 0, std::int64_t loopEnd = 0, int channels = 0, const std::string& name = "") { SampleRefEntry e; e.sampleId = id; e.ref.relativePath = rel; e.ref.rootNote = root; e.ref.loop.hasLoop = hasLoop; e.ref.loop.start = loopStart; e.ref.loop.end = loopEnd; e.ref.channelCount = channels; e.displayName = name; return e; } static void testReferencedSampleIdsIsTheLoadedCapture() { // One capture = at most one referenced id. An empty selection contributes nothing (no // phantom "" id can reach the refs table). const std::vector ids = referencedSampleIds("b"); CHECK(ids.size() == 1); CHECK(ids.size() == 1 && ids[0] == "b"); CHECK(referencedSampleIds("").empty()); } static void testFindRefLooksUpTheOwnedCopy() { SampleRefs refs; refs.push_back(refEntry("a", "b/a.wav", 36)); refs.push_back(refEntry("b", "b/b.wav", 48)); const SelectedSample* a = findRef(refs, "a"); CHECK(a != nullptr && a->rootNote == 36 && a->relativePath == "b/a.wav"); CHECK(findRef(refs, "ghost") == nullptr); CHECK(findRef(refs, "") == nullptr); // an empty id never matches an entry CHECK(findRef(SampleRefs{}, "a") == nullptr); } static void testRefreshRefsFromBankUpsertAndOwnership() { // Upsert: a resolvable id copies in (the selectSample distillation); a re-refresh after // a bank edit UPDATES the owned copy (S9 recapture sync); a bank MISS never strips the // owned entry (a bank deletion cannot silence a self-contained instance); an empty or // malformed blob is a no-op. Sample s1 = makeSample("a", "Kick", "b/a.wav", 36); s1.channelCount = 2; const std::string json1 = bookJson({s1}, {}); SampleRefs refs; refreshRefsFromBank(refs, json1, {"a", "ghost"}); CHECK(refs.size() == 1); // "ghost" does not resolve -> no entry minted CHECK(refs.size() == 1 && refs[0].sampleId == "a" && refs[0].ref.rootNote == 36); CHECK(refs.size() == 1 && refs[0].ref.relativePath == "b/a.wav"); CHECK(refs.size() == 1 && refs[0].ref.channelCount == 2); CHECK(refs.size() == 1 && refs[0].displayName == "Kick"); // name copied with the ref // Recapture-style bank edit: path + root + name changed -> the owned copy refreshes. refreshRefsFromBank(refs, bookJson({makeSample("a", "Kick 2", "b/a2.wav", 40)}, {}), {"a"}); CHECK(refs.size() == 1 && refs[0].ref.rootNote == 40); CHECK(refs.size() == 1 && refs[0].ref.relativePath == "b/a2.wav"); CHECK(refs.size() == 1 && refs[0].displayName == "Kick 2"); // rename sync // Bank deletion: the id no longer resolves -> the OWNED copy survives untouched. refreshRefsFromBank(refs, bookJson({makeSample("x", "Other", "b/x.wav", 60)}, {}), {"a"}); CHECK(refs.size() == 1 && refs[0].ref.rootNote == 40); CHECK(refs.size() == 1 && refs[0].displayName == "Kick 2"); // Malformed / empty blobs: no-op. refreshRefsFromBank(refs, "{garbage", {"a"}); refreshRefsFromBank(refs, "", {"a"}); CHECK(refs.size() == 1 && refs[0].ref.rootNote == 40); } static void testSameDecodeSourceTracksEveryDecodeInput() { // The predicate a resumed (already-decoded) instrument is gated on: every field that // changes what buildSampleData produces must read as different, and the display-only // name must not. SelectedSample a; a.relativePath = "b/a.wav"; a.rootNote = 36; a.channelCount = 2; a.loop.hasLoop = true; a.loop.start = 100; a.loop.end = 900; CHECK(sameDecodeSource(a, a)); SelectedSample recaptured = a; recaptured.relativePath = "b/a2.wav"; // the recapture case: a new file behind one id CHECK(!sameDecodeSource(a, recaptured)); SelectedSample reRooted = a; reRooted.rootNote = 40; CHECK(!sameDecodeSource(a, reRooted)); SelectedSample reChanneled = a; reChanneled.channelCount = 1; // drives the channel-mode auto-default, hence the decode CHECK(!sameDecodeSource(a, reChanneled)); SelectedSample loopOff = a; loopOff.loop.hasLoop = false; CHECK(!sameDecodeSource(a, loopOff)); SelectedSample loopMoved = a; loopMoved.loop.start = 101; CHECK(!sameDecodeSource(a, loopMoved)); loopMoved = a; loopMoved.loop.end = 901; CHECK(!sameDecodeSource(a, loopMoved)); } static void testRetainRefsFiltersToPlayedSet() { // getState hygiene: only the entries the instance currently plays persist — the table // cannot grow with browsing history. Order of survivors is preserved. SampleRefs refs; refs.push_back(refEntry("a", "b/a.wav", 36)); refs.push_back(refEntry("b", "b/b.wav", 48)); refs.push_back(refEntry("c", "b/c.wav", 60)); retainRefs(refs, {"c", "a"}); CHECK(refs.size() == 2); CHECK(refs.size() == 2 && refs[0].sampleId == "a" && refs[1].sampleId == "c"); retainRefs(refs, {}); CHECK(refs.empty()); } static void testLegacyLiftDecision() { // The #A terminating guard, pure: Retry while the blob is not readable YET (absent, // empty, malformed — the project's ext-state may simply not have parsed); Lift when a // referenced id resolves (a lift attempt makes progress); Stale — the shell latches // permanently — when the blob PARSES and knows none of the referenced ids (an empty // id list included), so a stale-id pre-v10 lift STOPS instead of churning every tick. const std::string json = bookJson({makeSample("a", "Kick", "b/a.wav", 36)}, {}); const std::vector ids{"a"}; CHECK(legacyLiftDecision(std::nullopt, ids) == LegacyLiftDecision::Retry); CHECK(legacyLiftDecision(std::string(), ids) == LegacyLiftDecision::Retry); CHECK(legacyLiftDecision(std::string("{garbage"), ids) == LegacyLiftDecision::Retry); CHECK(legacyLiftDecision(json, ids) == LegacyLiftDecision::Lift); // One resolvable id among stale ones is still progress (the lift copies what it can). CHECK(legacyLiftDecision(json, {"ghost", "a"}) == LegacyLiftDecision::Lift); CHECK(legacyLiftDecision(json, {"ghost"}) == LegacyLiftDecision::Stale); CHECK(legacyLiftDecision(json, {}) == LegacyLiftDecision::Stale); } // --- resolvePlay: stored SECONDS -> engine FRAMES at the live rate -------------- static void testResolvePlayConvertsWallClockAtTheRate() { // Wall-clock times convert at the LIVE rate; the Trigger %-length, every hold FRACTION and // every curve exponent are rate-free and carry through untouched, as do levels and depths. PlaySeconds st; st.playMode = PlayMode::Trigger; st.adsr.attackSeconds = 0.01; st.adsr.holdSeconds = 0.05; st.adsr.decaySeconds = 0.02; st.adsr.sustainLevel = 0.8; st.adsr.releaseSeconds = 0.15; st.adsr.attackCurve = 2.5; st.adsr.decayCurve = 0.4; st.adsr.releaseCurve = 3.5; st.trigger.lengthFraction = 0.75; st.trigAhd.attackSeconds = 0.01; st.trigAhd.decaySeconds = 0.02; st.trigAhd.holdFraction = 0.6; st.trigAhd.attackCurve = 1.75; st.trigAhd.decayCurve = 0.8; st.pitchEngine = PitchEngine::Preserve; st.pitchEnv.enabled = true; st.pitchEnv.shape.attackSeconds = 0.02; st.pitchEnv.shape.decaySeconds = 0.03; st.pitchEnv.shape.holdFraction = 0.25; st.pitchEnv.peakSemitones = 5.0; const PlayParams at48 = resolvePlay(st, 48000); CHECK(at48.playMode == PlayMode::Trigger); CHECK(at48.adsr.attackFrames == 480); CHECK(at48.adsr.holdFrames == 2400); CHECK(at48.adsr.decayFrames == 960); CHECK(at48.adsr.sustainLevel == 0.8); // a level, not a time CHECK(at48.adsr.releaseFrames == 7200); CHECK(at48.adsr.attackCurve == 2.5); // dimensionless CHECK(at48.adsr.decayCurve == 0.4); CHECK(at48.adsr.releaseCurve == 3.5); CHECK(at48.trigger.lengthFraction == 0.75); // source-timeline, unconverted CHECK(at48.trigAhd.attackFrames == 480); CHECK(at48.trigAhd.decayFrames == 960); CHECK(at48.trigAhd.holdFraction == 0.6); // a fraction, not a time CHECK(at48.trigAhd.attackCurve == 1.75); CHECK(at48.trigAhd.decayCurve == 0.8); CHECK(at48.pitchEngine == PitchEngine::Preserve); CHECK(at48.pitchEnv.enabled); CHECK(at48.pitchEnv.shape.attackFrames == 960); CHECK(at48.pitchEnv.shape.decayFrames == 1440); CHECK(at48.pitchEnv.shape.holdFraction == 0.25); CHECK(at48.pitchEnv.peakSemitones == 5.0); // a depth, not a time // THE no-hardcoded-rate contract: the SAME stored seconds yield different frame counts // at a different rate. A baked-in rate would make these equal. const PlayParams at96 = resolvePlay(st, 96000); CHECK(at96.adsr.attackFrames == 960); CHECK(at96.adsr.holdFrames == 4800); CHECK(at96.adsr.releaseFrames == 14400); CHECK(at96.pitchEnv.shape.attackFrames == 1920); CHECK(at96.trigAhd.attackFrames == 960); CHECK(at96.trigAhd.holdFraction == 0.6); // still unconverted CHECK(at96.adsr.attackCurve == 2.5); } static void testResolvePlayCarriesTheFilterAndResolvesOnlyItsEnvelope() { // The filter's control positions are already rate-free, so only its envelope crosses the // seconds->frames boundary. A converted norm would be a bug in the other direction: the // same preset must sound identical at 48k and 96k. PlaySeconds st; st.filter.enabled = true; st.filter.settings.cutoffNorm = 0.25f; st.filter.settings.resonanceNorm = 0.75f; st.filter.settings.morphNorm = 0.5f; st.filter.settings.driveNorm = 0.125f; st.filter.settings.morphLaw = reasampler::instrument::engine::filter::MorphLaw::HighNotchLow; st.filter.modAmount = -0.5; st.filter.velAmount = -0.375; // distinct from every neighbouring field, so a mis-wire shows st.filter.velocityCurve = VelocityCurve::fromPoints( {{0.0, 0.0}, {127.0, 0.25}}, reasampler::instrument::engine::CurveDomain::Bipolar); st.filter.keyTrack = 1.25; st.filter.env.attackSeconds = 0.01; st.filter.env.holdSeconds = 0.02; st.filter.env.decaySeconds = 0.03; st.filter.env.sustainLevel = 0.4; st.filter.env.releaseSeconds = 0.05; const PlayParams at48 = resolvePlay(st, 48000); CHECK(at48.filter.enabled); CHECK(at48.filter.settings.cutoffNorm == 0.25f); CHECK(at48.filter.settings.resonanceNorm == 0.75f); CHECK(at48.filter.settings.morphNorm == 0.5f); CHECK(at48.filter.settings.driveNorm == 0.125f); CHECK(at48.filter.settings.morphLaw == reasampler::instrument::engine::filter::MorphLaw::HighNotchLow); CHECK(at48.filter.modAmount == -0.5); CHECK(at48.filter.velAmount == -0.375); // The transfer curve is dimensionless, so it crosses unchanged — asserted against the // straight line the two stored knots describe, not against the stored object. CHECK(at48.filter.velocityCurve.eval(0.0) == 0.0); CHECK(approx(at48.filter.velocityCurve.eval(127.0), 0.25)); CHECK(approx(at48.filter.velocityCurve.eval(63.5), 0.125)); CHECK(at48.filter.keyTrack == 1.25); CHECK(at48.filter.env.attackFrames == 480); CHECK(at48.filter.env.holdFrames == 960); CHECK(at48.filter.env.decayFrames == 1440); CHECK(at48.filter.env.sustainLevel == 0.4); // a level, not a time CHECK(at48.filter.env.releaseFrames == 2400); const PlayParams at96 = resolvePlay(st, 96000); CHECK(at96.filter.env.attackFrames == 960); CHECK(at96.filter.env.releaseFrames == 4800); CHECK(at96.filter.settings.cutoffNorm == 0.25f); // rate-free: unchanged // Off by default, and the default envelope is a flat unity so a disengaged filter has // nothing to modulate with either. const PlayParams bare = resolvePlay(PlaySeconds{}, 48000); CHECK(!bare.filter.enabled); CHECK(bare.filter.modAmount == 0.0); CHECK(bare.filter.velAmount == 0.0); for (int v = 0; v <= 127; ++v) CHECK(bare.filter.velocityCurve.eval(v) == 0.0); CHECK(bare.filter.keyTrack == 0.0); CHECK(bare.filter.env.sustainLevel == 1.0); // The velocity->pitch curve rides the same boundary and is off by the same default. for (int v = 0; v <= 127; ++v) CHECK(bare.pitchVelocityCurve.eval(v) == 0.0); } // The velocity->pitch curve is dimensionless like the filter's, so resolvePlay carries it // across the seconds->frames boundary untouched at any rate. static void testResolvePlayCarriesThePitchVelocityCurve() { PlaySeconds st; st.pitchVelocityCurve = VelocityCurve::fromPoints( {{0.0, -1.0}, {127.0, 1.0}}, reasampler::instrument::engine::CurveDomain::Bipolar); for (const int rate : {44100, 96000}) { const PlayParams p = resolvePlay(st, rate); CHECK(p.pitchVelocityCurve.eval(0.0) == -1.0); CHECK(p.pitchVelocityCurve.eval(127.0) == 1.0); CHECK(approx(p.pitchVelocityCurve.eval(63.5), 0.0)); } } static void testResolvePlayRoundsAndFloorsNegatives() { PlaySeconds st; st.adsr.attackSeconds = 0.0001; // 4.41 frames at 44.1k -> rounds to 4 st.adsr.decaySeconds = 0.00012; // 5.292 -> rounds to 5 st.adsr.releaseSeconds = -1.0; // negative is floored to 0, never a negative count const PlayParams p = resolvePlay(st, 44100); CHECK(p.adsr.attackFrames == 4); CHECK(p.adsr.decayFrames == 5); CHECK(p.adsr.releaseFrames == 0); } // The retired Trigger fade pair was SOURCE frames; the AHD that replaced it stores wall-clock // seconds, and the codec's lift can only divide by the PROJECT rate. This is the far end of // that seam: the build multiplies by the DECODE rate, so a migrated fade comes back scaled by // decodeRate/projectRate whenever a file's own rate differs from the project's. The bound is // documented at the lift in component_state_io.h; this is its measured size. static void testMigratedFadeStretchesWhenTheDecodeRateDiffersFromTheProjectRate() { PlaySeconds st; st.trigAhd.attackSeconds = 441.0 / 44100.0; // a 441-SOURCE-frame fade lifted at 44.1k st.trigAhd.decaySeconds = 882.0 / 44100.0; // Matched rates are EXACT: the round trip through seconds loses nothing. const PlayParams matched = resolvePlay(st, 44100); CHECK(matched.trigAhd.attackFrames == 441); CHECK(matched.trigAhd.decayFrames == 882); // resolvePlay's second argument is the DECODE rate; the seconds above were lifted (divided) // at the PROJECT rate 44100 — so this is a 48 kHz file opened in a 44.1 kHz project (the // mirror of component_state_io.h's worked example): 441 * 48000/44100 = 480 source frames, // ~8.8% longer than the fade the saved instance actually had. const PlayParams stretched = resolvePlay(st, 48000); CHECK(stretched.trigAhd.attackFrames == 480); CHECK(stretched.trigAhd.decayFrames == 960); } // --- resolveCapture: the ONE override-beats-intrinsic fold --------------------- static SelectedSample ref(const std::string& rel, int root, bool hasLoop = false, std::int64_t loopStart = 0, std::int64_t loopEnd = 0) { SelectedSample s; s.relativePath = rel; s.rootNote = root; s.loop.hasLoop = hasLoop; s.loop.start = loopStart; s.loop.end = loopEnd; return s; } static void testResolveCaptureUsesIntrinsicsWhenNoOverride() { const ResolvedCapture r = resolveCapture(ref("b/a.wav", 40, true, 200, 800), InstrumentParams{}); CHECK(r.relativePath == "b/a.wav"); CHECK(r.rootNote == 40); // the capture's own root CHECK(r.loop.hasLoop && r.loop.start == 200 && r.loop.end == 800); CHECK(r.startFrame == 0); // absent start point -> frame 0 CHECK(r.keyTrack == 1.0); } static void testResolveCaptureOverridesBeatIntrinsics() { InstrumentParams p; p.rootOverride = 72; SampleLoop lp; lp.hasLoop = true; lp.start = 10; lp.end = 90; p.loopOverride = lp; p.startPoint = 512; p.keyTrack = 0.5; p.play.playMode = PlayMode::Trigger; const ResolvedCapture r = resolveCapture(ref("b/a.wav", 40, true, 200, 800), p); CHECK(r.rootNote == 72); // override beats the intrinsic CHECK(r.loop.hasLoop && r.loop.start == 10 && r.loop.end == 90); CHECK(r.startFrame == 512); CHECK(r.keyTrack == 0.5); CHECK(r.play.playMode == PlayMode::Trigger); CHECK(r.relativePath == "b/a.wav"); // the path is always the capture's } static void testResolveCaptureLoopOverrideCanDisableTheLoop() { // A loop override with hasLoop=false is how the user turns a looping capture into a // one-shot — it must beat the intrinsic rather than falling back to it. InstrumentParams p; p.loopOverride = SampleLoop{}; // hasLoop == false const ResolvedCapture r = resolveCapture(ref("b/a.wav", 40, true, 200, 800), p); CHECK(!r.loop.hasLoop); } static void testResolveFromBankAndRefsCannotDrift() { // Both resolution paths share ONE fold, so the same parameter set resolved via the bank // blob and via a refs table refreshed FROM that bank yields identical results. Sample s1 = makeSample("a", "Pad", "b/a.wav", 40); s1.loop = LoopPoints{200, 800}; const std::string json = bookJson({s1}, {}); InstrumentParams p; p.rootOverride = 72; p.startPoint = 512; SampleRefs refs; refreshRefsFromBank(refs, json, referencedSampleIds("a")); const std::optional viaBank = resolveFromBank(json, "a", p); const std::optional viaRefs = resolveFromRefs(refs, "a", p); CHECK(viaBank.has_value() && viaRefs.has_value()); if (viaBank && viaRefs) { CHECK(viaRefs->relativePath == viaBank->relativePath); CHECK(viaRefs->rootNote == viaBank->rootNote); // 72 (override) CHECK(viaRefs->loop.hasLoop == viaBank->loop.hasLoop); CHECK(viaRefs->loop.start == viaBank->loop.start); // 200 (intrinsic) CHECK(viaRefs->loop.end == viaBank->loop.end); CHECK(viaRefs->startFrame == viaBank->startFrame); // 512 } } static void testResolveNoPickAndStaleIdAreSilence() { const std::string json = bookJson({makeSample("a", "Kick", "b/a.wav", 36)}, {}); SampleRefs refs; refs.push_back(refEntry("a", "b/a.wav", 36)); // No pick -> nothing to resolve; a stale id -> the SAME defined no-play, never a // substituted first sample. CHECK(!resolveFromBank(json, "", InstrumentParams{}).has_value()); CHECK(!resolveFromBank(json, "ghost", InstrumentParams{}).has_value()); CHECK(!resolveFromRefs(refs, "", InstrumentParams{}).has_value()); CHECK(!resolveFromRefs(refs, "ghost", InstrumentParams{}).has_value()); // And an unreadable bank blob resolves to nothing rather than throwing. CHECK(!resolveFromBank("", "a", InstrumentParams{}).has_value()); CHECK(!resolveFromBank("{garbage", "a", InstrumentParams{}).has_value()); } static void testResolveFromRefsNeedsNoBankAtAll() { // The self-contained play path: an instance with owned refs resolves with NO bank blob // anywhere in the call — this is what an instance does when the extension is absent. SampleRefs refs; refs.push_back(refEntry("a", "b/a.wav", 36, /*hasLoop=*/true, 100, 500)); const std::optional r = resolveFromRefs(refs, "a", InstrumentParams{}); CHECK(r.has_value()); CHECK(r && r->relativePath == "b/a.wav"); CHECK(r && r->rootNote == 36); CHECK(r && r->loop.hasLoop && r->loop.start == 100 && r->loop.end == 500); } // --- buildSampleData ----------------------------------------------------------- static void testBuildSampleDataThreadsEverything() { InstrumentParams p; p.rootOverride = 40; SampleLoop lp; lp.hasLoop = true; lp.start = 10; lp.end = 90; p.loopOverride = lp; p.startPoint = 7; p.keyTrack = 0.25; p.play.adsr.attackSeconds = 0.01; const SampleData sd = buildSampleData(resolveCapture(ref("b/a.wav", 60), p), DecodedPcm{{0.1f, 0.2f, 0.3f}, 48000, {}}); CHECK(sd.playable()); CHECK(sd.frames.size() == 3); CHECK(sd.sampleRate == 48000); CHECK(sd.rootNote == 40); // the override, not the capture's 60 CHECK(sd.loop.hasLoop && sd.loop.start == 10 && sd.loop.end == 90); CHECK(sd.startFrame == 7); CHECK(sd.keyTrack == 0.25); CHECK(sd.channelCount() == 1); // Wall-clock seconds resolve to frames at THIS decode's rate. CHECK(sd.play.adsr.attackFrames == 480); } static void testBuildSampleDataResolvesSecondsAtTheDecodeRate() { // The rate that governs the conversion is the DECODE's, not a baked constant: the same // parameter set built against two decodes yields two different frame counts. InstrumentParams p; p.play.adsr.attackSeconds = 0.1; p.play.adsr.releaseSeconds = 0.25; const ResolvedCapture rc = resolveCapture(ref("b/a.wav", 60), p); const SampleData at44 = buildSampleData(rc, DecodedPcm{{0.1f}, 44100, {}}); const SampleData at96 = buildSampleData(rc, DecodedPcm{{0.1f}, 96000, {}}); CHECK(at44.play.adsr.attackFrames == 4410); CHECK(at44.play.adsr.releaseFrames == 11025); CHECK(at96.play.adsr.attackFrames == 9600); CHECK(at96.play.adsr.releaseFrames == 24000); } static void testBuildSampleDataCarriesTheSecondChannel() { const SampleData sd = buildSampleData( resolveCapture(ref("b/a.wav", 60), InstrumentParams{}), DecodedPcm{{0.1f, 0.2f}, 44100, {0.9f, 0.8f}}); CHECK(sd.channelCount() == 2); CHECK(sd.framesR.size() == 2 && approx(sd.framesR[0], 0.9) && approx(sd.framesR[1], 0.8)); } static void testBuildSampleDataDropsMismatchedSecondChannel() { // A malformed pair must fall back to MONO rather than half-playing. const SampleData sd = buildSampleData( resolveCapture(ref("b/a.wav", 60), InstrumentParams{}), DecodedPcm{{0.1f, 0.2f, 0.3f}, 44100, {0.9f}}); CHECK(sd.channelCount() == 1); CHECK(sd.framesR.empty()); } static void testBuildSampleDataEmptyPcmIsUnplayable() { // An unreadable/missing WAV decodes to empty PCM: the build yields an UNPLAYABLE // SampleData (silence), never a voice started on an empty read span. (A non-positive // rate is a programming error the build asserts on, so it is not exercised here.) const ResolvedCapture rc = resolveCapture(ref("b/a.wav", 60), InstrumentParams{}); const SampleData sd = buildSampleData(rc, DecodedPcm{{}, 44100, {}}); CHECK(!sd.playable()); CHECK(sd.frames.empty()); } // Major-1 remediation: buildSampleData (sample_map.cpp:333-334) is the ONE call site wiring // load-time detection to SampleData; detectPeriod's own unit coverage (test_period_detect.cpp) // never exercises this call, so a deleted wire passed the gated suite unnoticed. Goes through // the real build, not a direct detectPeriod call. static void testBuildSampleDataDetectsThirtyHertzSourcePeriod() { const int rate = 44100; const std::size_t frames = 30000; std::vector pcm(frames); for (std::size_t i = 0; i < frames; ++i) { pcm[i] = static_cast( std::sin(2.0 * 3.14159265358979323846 * 30.0 * static_cast(i) / rate)); } const SampleData sd = buildSampleData(resolveCapture(ref("b/a.wav", 60), InstrumentParams{}), DecodedPcm{pcm, rate, {}}); CHECK(std::fabs(sd.sourcePeriodFrames - 1470.0) < 2.0); // 44100 / 30 Hz } // The span half of the same wire: buildSampleData must hand detection the LOOP region when the // capture carries one, not the whole decoded PCM. Asserted through the real build for the same // reason as the test above — period_detect's own coverage cannot see which span the loader picks. static void testBuildSampleDataDetectsOverTheSustainLoopNotTheWholeSource() { const int rate = 44100; const std::size_t frames = 120000; const std::int64_t loopStart = 60000; const double kPi = 3.14159265358979323846; const double loopPeriod = static_cast(rate) / 30.0; // 1470 frames // Head at 147 Hz, looped tail at 30 Hz: analysed whole, the probes split two-and-two and // detection correctly refuses. Analysed over the loop, the 30 Hz sustain is unambiguous. std::vector pcm(frames); double phase = 0.0; for (std::size_t i = 0; i < frames; ++i) { phase += 2.0 * kPi / (i < static_cast(loopStart) ? 300.0 : loopPeriod); pcm[i] = static_cast(std::sin(phase)); } InstrumentParams noLoop; const SampleData bare = buildSampleData(resolveCapture(ref("b/a.wav", 60), noLoop), DecodedPcm{pcm, rate, {}}); CHECK(bare.sourcePeriodFrames == 0.0); // no loop -> whole source -> no ONE period InstrumentParams looped; looped.loopOverride = SampleLoop{true, loopStart, static_cast(frames)}; const SampleData sd = buildSampleData(resolveCapture(ref("b/a.wav", 60), looped), DecodedPcm{pcm, rate, {}}); CHECK(std::fabs(sd.sourcePeriodFrames - loopPeriod) < 2.0); // A loop too short to host the full search band falls back to the whole source rather than // to none — here that whole source has no one period, so the answer is none. Asserted // against the literal, not against `bare`: the two agreeing would also hold if both // regressed together, which is no evidence that the fallback ran. InstrumentParams shortLoop; shortLoop.loopOverride = SampleLoop{true, 118000, static_cast(frames)}; const SampleData shortSd = buildSampleData(resolveCapture(ref("b/a.wav", 60), shortLoop), DecodedPcm{pcm, rate, {}}); CHECK(shortSd.sourcePeriodFrames == 0.0); } static void testBuildSampleDataCarriesTheVelocityCurve() { InstrumentParams p; p.velocityCurve = VelocityCurve::linear(); const SampleData sd = buildSampleData(resolveCapture(ref("b/a.wav", 60), p), DecodedPcm{{0.1f}, 44100, {}}); // The curve reaches the engine's own copy: a mid velocity maps to ~half gain, which the // flat default would not do. CHECK(std::fabs(sd.velocityCurve.eval(64.0) - 64.0 / 127.0) < 1e-6); } int main() { testSelectByIdHit(); testSelectEmptyIdIsSilence(); testSelectUnknownIdIsSilence(); testSelectRootNoteDefault(); testSelectLoopThreaded(); testSelectNoLoopIsAbsent(); testSelectChannelCountThreaded(); testChannelModeForExplicitIsNeverFought(); testChannelModeForUnknownCountIsNoOp(); testChannelModeForStereoCapture(); testChannelModeForMonoCapture(); testSelectEmptyBlob(); testSelectMalformedBlob(); testSelectZeroSamples(); testListSamplesOrdinalOrder(); testListSamplesCarriesCardMetadata(); testListSamplesEmptyAndMalformed(); testListBanksOrdinalOrder(); testListBanksEmptyAndMalformed(); testDownmixMonoPassthrough(); testDownmixStereoAverages(); testDownmixThreeChannelAverages(); testDownmixDegenerate(); testSelectionStateRoundTrip(); testSelectionStateEmptyId(); testSelectionStateWrongVersion(); testSelectionStateTruncated(); testWavTrimToDownmixPipelineStereo(); testWavTrimToDownmixPipelineMono(); testExtractChannelStereo(); testExtractChannelClampsToLast(); testDecodeChannelsMonoModeDownmixes(); testDecodeChannelsStereoModeStereoSource(); testDecodeChannelsStereoModeMonoSourceDualMono(); testReferencedSampleIdsIsTheLoadedCapture(); testFindRefLooksUpTheOwnedCopy(); testRefreshRefsFromBankUpsertAndOwnership(); testSameDecodeSourceTracksEveryDecodeInput(); testRetainRefsFiltersToPlayedSet(); testLegacyLiftDecision(); testResolvePlayConvertsWallClockAtTheRate(); testResolvePlayCarriesTheFilterAndResolvesOnlyItsEnvelope(); testResolvePlayCarriesThePitchVelocityCurve(); testResolvePlayRoundsAndFloorsNegatives(); testMigratedFadeStretchesWhenTheDecodeRateDiffersFromTheProjectRate(); testResolveCaptureUsesIntrinsicsWhenNoOverride(); testResolveCaptureOverridesBeatIntrinsics(); testResolveCaptureLoopOverrideCanDisableTheLoop(); testResolveFromBankAndRefsCannotDrift(); testResolveNoPickAndStaleIdAreSilence(); testResolveFromRefsNeedsNoBankAtAll(); testBuildSampleDataThreadsEverything(); testBuildSampleDataResolvesSecondsAtTheDecodeRate(); testBuildSampleDataCarriesTheSecondChannel(); testBuildSampleDataDropsMismatchedSecondChannel(); testBuildSampleDataEmptyPcmIsUnplayable(); testBuildSampleDataCarriesTheVelocityCurve(); testBuildSampleDataDetectsThirtyHertzSourcePeriod(); testBuildSampleDataDetectsOverTheSustainLoopNotTheWholeSource(); if (g_fail == 0) std::printf("sample_map: all tests passed\n"); return g_fail != 0; }