1b4d0e67b7
Also corrects the cap-area, em-dash, glyph-overhang and heuristic-comment findings noted in review.
225 lines
9.3 KiB
C++
225 lines
9.3 KiB
C++
// 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 <cstdio>
|
|
|
|
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);
|
|
}
|
|
|
|
// Dragging the START marker while parked (never set) must not turn "DRAG TO SET LOOP" into
|
|
// "LOOP OFF" — the ordinary gesture that exposed the bug, distinct from the retained-span case
|
|
// above.
|
|
static void testEditingTheStartMarkerWhileParkedStaysParked() {
|
|
LoopMarks m = resolveLoopMarks(StoredLoop{}, kFrames);
|
|
CHECK(m.parked && !m.hasLoop);
|
|
m.start = 512;
|
|
const LoopMarks after = writeThenRead(m);
|
|
CHECK(!after.hasLoop);
|
|
CHECK(after.parked);
|
|
const LoopBounds d = defaultLoopBounds(kFrames);
|
|
CHECK(after.loopStart == d.start && after.loopEnd == d.end);
|
|
CHECK(after.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();
|
|
testEditingTheStartMarkerWhileParkedStaysParked();
|
|
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;
|
|
}
|