// Standalone tests for reasampler::vst::trigger_seam — no VST3, no REAPER, no framework. // Same fast assert loop as the sibling pure tests. // // Covers: triggerPlayLength (zero play length, startFrame set, startFrame past frameCount, // rounding); framesToFadeFraction (zero play length, basic ratio); fadeFractionToFrames // (zero play length, rounding); round-trip fidelity; the Finding 1 regression (start-point // set — the case that was broken before this module existed). #include "../src/vst/trigger_seam.h" #include using namespace reasampler::vst; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // --- triggerPlayLength -------------------------------------------------------- static void testPlayLengthNoStartPoint() { // startFrame == 0 (no start marker): postStart = frameCount. // 1000 frames, lengthFraction 1.0 -> 1000. CHECK(triggerPlayLength(1.0, 1000, 0) == 1000); // 1000 frames, lengthFraction 0.5 -> round(500.0) = 500. CHECK(triggerPlayLength(0.5, 1000, 0) == 500); // 1000 frames, lengthFraction 0.333 -> round(333.0) = 333. CHECK(triggerPlayLength(0.333, 1000, 0) == 333); } static void testPlayLengthWithStartPoint() { // The Finding 1 regression: startFrame set, play length must be shorter. // frameCount=1000, startFrame=200 -> postStart=800. // lengthFraction 1.0 -> 800 (NOT 1000 as the pre-fix code produced). CHECK(triggerPlayLength(1.0, 1000, 200) == 800); // lengthFraction 0.5 -> round(400.0) = 400. CHECK(triggerPlayLength(0.5, 1000, 200) == 400); } static void testPlayLengthZeroFrameCount() { // No decoded audio -> 0. CHECK(triggerPlayLength(1.0, 0, 0) == 0); } static void testPlayLengthStartFramePastEnd() { // startFrame >= frameCount -> postStart clamped to 0 -> play length 0. CHECK(triggerPlayLength(1.0, 500, 500) == 0); CHECK(triggerPlayLength(1.0, 500, 600) == 0); } static void testPlayLengthRounding() { // round(0.5) = 1 (round half up via +0.5 truncation: 0.5 + 0.5 = 1.0 -> 1). CHECK(triggerPlayLength(0.5, 1, 0) == 1); // round(lengthFraction * 3): 0.4 * 3 = 1.2 -> 1. CHECK(triggerPlayLength(0.4, 3, 0) == 1); // 0.6 * 3 = 1.8 -> 2. CHECK(triggerPlayLength(0.6, 3, 0) == 2); } // --- framesToFadeFraction ----------------------------------------------------- static void testFramesToFadeFractionBasic() { // 100 frames fade over 1000 play length -> 0.1. const double frac = framesToFadeFraction(100, 1000); CHECK(frac > 0.0999 && frac < 0.1001); } static void testFramesToFadeFractionZeroPlayLength() { // Degenerate: zero play length -> 0.0 (no division by zero). CHECK(framesToFadeFraction(100, 0) == 0.0); CHECK(framesToFadeFraction(0, 0) == 0.0); } static void testFramesToFadeFractionFullSpan() { // fadeFrames == playLength -> fraction 1.0. const double frac = framesToFadeFraction(500, 500); CHECK(frac > 0.9999 && frac < 1.0001); } // --- fadeFractionToFrames ----------------------------------------------------- static void testFadeFractionToFramesBasic() { // 0.1 of 1000 play length -> round(100.0) = 100. CHECK(fadeFractionToFrames(0.1, 1000) == 100); } static void testFadeFractionToFramesZeroPlayLength() { // Degenerate: play length 0 -> 0 frames. CHECK(fadeFractionToFrames(0.5, 0) == 0); } static void testFadeFractionToFramesRounding() { // 0.333... of 3 -> round(1.0) = 1. CHECK(fadeFractionToFrames(1.0 / 3.0, 3) == 1); // 0.5 of 3 -> round(1.5) = 2. CHECK(fadeFractionToFrames(0.5, 3) == 2); } // --- Round-trip --------------------------------------------------------------- static void testRoundTripNoStartPoint() { // Pack then unpack: fadeInFrames should survive (within 1 frame of rounding). // frameCount=44100, startFrame=0, lengthFraction=1.0 -> playLength=44100. // fadeInFrames = 2205 (5% of 44100). const std::int64_t fadeIn = 2205; const std::int64_t playLen = triggerPlayLength(1.0, 44100, 0); const double frac = framesToFadeFraction(fadeIn, playLen); const std::int64_t recovered = fadeFractionToFrames(frac, playLen); // Should be exact (2205 / 44100 * 44100 = 2205.0). CHECK(recovered == fadeIn); } static void testRoundTripWithStartPoint() { // The Finding 1 case: startFrame set. frameCount=44100, startFrame=8820 (20%). // postStart=35280, lengthFraction=1.0 -> playLength=35280. // fadeInFrames = 1764 (5% of 35280). const std::int64_t frameCount = 44100; const std::int64_t startFrame = 8820; const std::int64_t fadeIn = 1764; const std::int64_t playLen = triggerPlayLength(1.0, frameCount, startFrame); CHECK(playLen == 35280); const double frac = framesToFadeFraction(fadeIn, playLen); const std::int64_t recovered = fadeFractionToFrames(frac, playLen); CHECK(recovered == fadeIn); } static void testRoundTripFadeGreaterThanSpan() { // fadeFrames > playLength -> fraction > 1 (returned unclamped; the overlay clamps at draw). // The shell is responsible for clamping before writing AmpEnvelope. const std::int64_t playLen = 100; const std::int64_t fadeIn = 150; const double frac = framesToFadeFraction(fadeIn, playLen); CHECK(frac > 1.0); // intentionally unclamped from this module's perspective // The round-trip still recovers the original fade, so the shell can clamp after. const std::int64_t recovered = fadeFractionToFrames(frac, playLen); CHECK(recovered == fadeIn); } int main() { testPlayLengthNoStartPoint(); testPlayLengthWithStartPoint(); testPlayLengthZeroFrameCount(); testPlayLengthStartFramePastEnd(); testPlayLengthRounding(); testFramesToFadeFractionBasic(); testFramesToFadeFractionZeroPlayLength(); testFramesToFadeFractionFullSpan(); testFadeFractionToFramesBasic(); testFadeFractionToFramesZeroPlayLength(); testFadeFractionToFramesRounding(); testRoundTripNoStartPoint(); testRoundTripWithStartPoint(); testRoundTripFadeGreaterThanSpan(); if (g_fail == 0) std::printf("trigger_seam: all tests passed\n"); else std::printf("trigger_seam: %d FAILED\n", g_fail); return g_fail == 0 ? 0 : 1; }