// Standalone tests for reasampler::instrument::ui::loop_marks — no VST3, no REAPER, no // framework. Asserts the loop enable's whole state machine: the resolve's park rule and its two // OFF states, the write's collapse fold and crossfade retention, and the four gestures that // reach `hasLoop` composed end to end (resolve -> edit -> apply -> resolve), which is exactly // how the editor drives it. #include "../src/core/instrument/ui/loop_marks.h" #include "../src/core/instrument/engine/loop/loop_span.h" // defaultLoopBounds (the park target) #include using namespace reasampler::instrument::ui; using reasampler::SampleLoop; using reasampler::instrument::engine::loop::LoopBounds; using reasampler::instrument::engine::loop::defaultLoopBounds; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) static constexpr std::int64_t kFrames = 10000; // The editor's own round trip: what the band shows after a marker set is written back. static LoopMarks writeThenRead(const LoopMarks& edited, std::int64_t frames = kFrames) { const LoopWrite w = applyLoopMarks(edited); StoredLoop s; s.override_ = w.loop; s.crossfade = w.crossfade; s.startPoint = w.start; return resolveLoopMarks(s, frames); } static StoredLoop storedSpan(bool on, std::int64_t start, std::int64_t end, std::int64_t crossfade) { StoredLoop s; s.override_ = SampleLoop{on, start, end}; s.crossfade = crossfade; return s; } // --- resolve: the two OFF states ----------------------------------------------- static void testNothingSetParksOnTheDefaultBoundsAndReadsAsNeverSet() { const LoopMarks m = resolveLoopMarks(StoredLoop{}, kFrames); const LoopBounds d = defaultLoopBounds(kFrames); CHECK(!m.hasLoop); CHECK(m.parked); // the "DRAG TO SET LOOP" state CHECK(m.loopStart == d.start && m.loopEnd == d.end); } static void testAValidSpanSwitchedOffKeepsItsOwnPositions() { const LoopMarks m = resolveLoopMarks(storedSpan(false, 4000, 6000, 300), kFrames); CHECK(!m.hasLoop); CHECK(!m.parked); // the "LOOP OFF" state — there is nothing to "set" CHECK(m.loopStart == 4000 && m.loopEnd == 6000); CHECK(m.crossfade == 300); // And it is NOT the park position, which is what the off->on restore depends on. const LoopBounds d = defaultLoopBounds(kFrames); CHECK(!(m.loopStart == d.start && m.loopEnd == d.end)); } static void testAnUnusableSpanParksWhateverTheEnableSays() { const LoopBounds d = defaultLoopBounds(kFrames); // Collapsed, inverted, past the PCM, and negative — the four the engine refuses. const StoredLoop bad[4] = {storedSpan(true, 500, 500, 0), storedSpan(true, 900, 400, 0), storedSpan(true, 500, kFrames + 1, 0), storedSpan(true, -5, 400, 0)}; for (const StoredLoop& s : bad) { const LoopMarks m = resolveLoopMarks(s, kFrames); CHECK(!m.hasLoop); CHECK(m.parked); CHECK(m.loopStart == d.start && m.loopEnd == d.end); } } static void testTheOverrideSupersedesTheBankIntrinsic() { StoredLoop s; s.intrinsic = SampleLoop{true, 100, 200}; s.override_ = SampleLoop{true, 4000, 6000}; const LoopMarks m = resolveLoopMarks(s, kFrames); CHECK(m.hasLoop && m.loopStart == 4000 && m.loopEnd == 6000); // With no override the intrinsic is what the band shows. s.override_.reset(); const LoopMarks i = resolveLoopMarks(s, kFrames); CHECK(i.hasLoop && i.loopStart == 100 && i.loopEnd == 200); // An intrinsic that itself says "no loop" is not a span to adopt. s.intrinsic = SampleLoop{false, 100, 200}; CHECK(resolveLoopMarks(s, kFrames).parked); } // --- the four gestures onto hasLoop -------------------------------------------- // Gesture 1: the enable clicked ON. Span and crossfade retained as-is. static void testEnableOnKeepsTheSpanAndCrossfade() { LoopMarks m = resolveLoopMarks(storedSpan(false, 4000, 6000, 300), kFrames); m.hasLoop = true; const LoopMarks after = writeThenRead(m); CHECK(after.hasLoop); CHECK(after.loopStart == 4000 && after.loopEnd == 6000); CHECK(after.crossfade == 300); } // Gesture 2: the enable clicked OFF. Span and crossfade retained — the whole point of it being // a toggle rather than a delete button. static void testEnableOffRetainsTheSpanAndCrossfade() { LoopMarks m = resolveLoopMarks(storedSpan(true, 4000, 6000, 300), kFrames); m.hasLoop = false; const LoopMarks after = writeThenRead(m); CHECK(!after.hasLoop); CHECK(!after.parked); CHECK(after.loopStart == 4000 && after.loopEnd == 6000); CHECK(after.crossfade == 300); } // The acceptance criterion in one assertion: off then on restores the loop EXACTLY. static void testOffThenOnRestoresTheLoopExactly() { const LoopMarks before = resolveLoopMarks(storedSpan(true, 4000, 6000, 300), kFrames); LoopMarks off = before; off.hasLoop = false; LoopMarks mid = writeThenRead(off); mid.hasLoop = true; const LoopMarks back = writeThenRead(mid); CHECK(back.hasLoop == before.hasLoop); CHECK(back.loopStart == before.loopStart && back.loopEnd == before.loopEnd); CHECK(back.crossfade == before.crossfade); CHECK(back.parked == before.parked); } // Gesture 3: the span collapsed onto itself. OFF, span destroyed and re-parked, crossfade zeroed. static void testCollapsingTheSpanTurnsItOffReparksAndZeroesTheCrossfade() { LoopMarks m = resolveLoopMarks(storedSpan(true, 4000, 6000, 300), kFrames); m.loopEnd = m.loopStart; // the drag that lands one mark on the other const LoopWrite w = applyLoopMarks(m); CHECK(!w.loop.hasLoop); CHECK(w.crossfade == 0); const LoopMarks after = writeThenRead(m); const LoopBounds d = defaultLoopBounds(kFrames); CHECK(!after.hasLoop && after.parked); CHECK(after.loopStart == d.start && after.loopEnd == d.end); CHECK(after.crossfade == 0); } // Gesture 4: dragging a loop mark while OFF turns it on, in BOTH off-states. static void testDraggingALoopMarkWhileOffTurnsItOn() { // Never set: the pair is parked, and the drag takes it off the park. LoopMarks parked = resolveLoopMarks(StoredLoop{}, kFrames); CHECK(parked.parked && !parked.hasLoop); parked.loopStart = 3000; // the drag parked.hasLoop = true; const LoopMarks fromParked = writeThenRead(parked); CHECK(fromParked.hasLoop && !fromParked.parked); CHECK(fromParked.loopStart == 3000); // Span retained: the drag turns it on at the dragged positions, crossfade retained. LoopMarks retained = resolveLoopMarks(storedSpan(false, 4000, 6000, 300), kFrames); CHECK(!retained.parked && !retained.hasLoop); retained.loopEnd = 7000; retained.hasLoop = true; const LoopMarks fromRetained = writeThenRead(retained); CHECK(fromRetained.hasLoop); CHECK(fromRetained.loopStart == 4000 && fromRetained.loopEnd == 7000); CHECK(fromRetained.crossfade == 300); } // --- the write's own rules ------------------------------------------------------ // Dragging the START marker with the enable off must not turn the loop on, and must not // destroy the crossfade travelling with the retained span. static void testEditingTheStartMarkerWhileOffLeavesTheEnableAndCrossfadeAlone() { LoopMarks m = resolveLoopMarks(storedSpan(false, 4000, 6000, 300), kFrames); m.start = 512; const LoopWrite w = applyLoopMarks(m); CHECK(!w.loop.hasLoop); CHECK(w.loop.start == 4000 && w.loop.end == 6000); CHECK(w.crossfade == 300); CHECK(w.start == 512); } static void testANegativeCrossfadeNeverReachesTheStore() { LoopMarks m = resolveLoopMarks(storedSpan(true, 4000, 6000, 0), kFrames); m.crossfade = -1; CHECK(applyLoopMarks(m).crossfade == 0); StoredLoop s = storedSpan(true, 4000, 6000, -1); CHECK(resolveLoopMarks(s, kFrames).crossfade == 0); } int main() { testNothingSetParksOnTheDefaultBoundsAndReadsAsNeverSet(); testAValidSpanSwitchedOffKeepsItsOwnPositions(); testAnUnusableSpanParksWhateverTheEnableSays(); testTheOverrideSupersedesTheBankIntrinsic(); testEnableOnKeepsTheSpanAndCrossfade(); testEnableOffRetainsTheSpanAndCrossfade(); testOffThenOnRestoresTheLoopExactly(); testCollapsingTheSpanTurnsItOffReparksAndZeroesTheCrossfade(); testDraggingALoopMarkWhileOffTurnsItOn(); testEditingTheStartMarkerWhileOffLeavesTheEnableAndCrossfadeAlone(); testANegativeCrossfadeNeverReachesTheStore(); if (g_fail == 0) { std::printf("loop_marks: all tests passed\n"); return 0; } std::printf("loop_marks: %d failure(s)\n", g_fail); return 1; }