Loop: an explicit enable, four named marks with grabbable caps, and the crossfade painted where it is actually heard
hasLoop becomes user-owned with the gestures as shortcuts onto it; no format change. START uses overlay/trace, not accent/primary, which is the waveform's own fill.
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
// 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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user