// Standalone tests for reasampler::render_settings — no REAPER, no framework. // Covers the three pure pieces behind the M7 capture family: the source-mode -> // RENDER_SETTINGS bit mapping (wet-only), P_RAZOREDITS parsing -> ranges + union, // and the capture-action taxonomy table (stable ids, coverage of every mode). #include "../src/render_settings.h" #include #include #include using namespace reasampler; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // --- renderSettingsFor: wet-only bit mapping --------------------------------- static void testMasterMixWet() { // Master mix -> value 0 (no source bits), supported. RenderSettingsChoice c = renderSettingsFor(SourceMode::MasterMix, 1.0); CHECK(c.settings == kRenderMasterMix); CHECK(c.supported); // TimeSelection aliases master mix — same result. CHECK(renderSettingsFor(SourceMode::TimeSelection, 1.0).settings == kRenderMasterMix); // wetDry argument is irrelevant for M7 (all actions are wet); passing 0.0 // must still yield the same wet master-mix bits. CHECK(renderSettingsFor(SourceMode::MasterMix, 0.0).settings == kRenderMasterMix); } static void testSelectedTracksWet() { // Selected tracks -> via master (&128), header-confirmed. RenderSettingsChoice c = renderSettingsFor(SourceMode::SelectedTracks, 1.0); CHECK(c.settings == kRenderSelTracksViaMaster); CHECK(c.supported); } static void testSelectedItemsSingleFile() { // Items render to ONE file (single-file bit set) so N items -> 1 bank entry. RenderSettingsChoice c = renderSettingsFor(SourceMode::SelectedItems, 1.0); CHECK((c.settings & kRenderSelItems) != 0); CHECK((c.settings & kRenderSingleFile) != 0); CHECK(c.supported); } static void testRazorSingleFile() { // Razor edits render to ONE file (same single-file rationale as items). RenderSettingsChoice c = renderSettingsFor(SourceMode::RazorArea, 1.0); CHECK((c.settings & kRenderRazorEdits) != 0); CHECK((c.settings & kRenderSingleFile) != 0); CHECK(c.supported); } static void testRealtimeIsUnsupportedOffline() { // The realtime mode is not an offline-render source — must report unsupported // so the offline backend refuses it rather than rendering the master mix. CHECK(!renderSettingsFor(SourceMode::Realtime, 1.0).supported); } // --- parseRazorEdits: P_RAZOREDITS string -> ranges -------------------------- static void testParseSingleTrackAudioArea() { // One track-audio triple: start end "" (empty quoted GUID = track audio). auto r = parseRazorEdits("1.5 3.25 \"\""); CHECK(r.size() == 1); CHECK(r[0].startSeconds == 1.5); CHECK(r[0].endSeconds == 3.25); } static void testParseMultipleAreas() { auto r = parseRazorEdits("0.0 1.0 \"\" 2.0 4.0 \"\""); CHECK(r.size() == 2); CHECK(r[0].startSeconds == 0.0 && r[0].endSeconds == 1.0); CHECK(r[1].startSeconds == 2.0 && r[1].endSeconds == 4.0); } static void testParseSkipsEnvelopeLaneAreas() { // A triple whose GUID is a real {…} is an ENVELOPE-lane area — skipped, since // M7 renders track audio. Only the track-audio triple survives. auto r = parseRazorEdits( "1.0 2.0 \"\" 3.0 4.0 {AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE}"); CHECK(r.size() == 1); CHECK(r[0].startSeconds == 1.0 && r[0].endSeconds == 2.0); } static void testParseEmptyAndMalformed() { CHECK(parseRazorEdits("").empty()); // Empty/inverted range dropped (end <= start). CHECK(parseRazorEdits("5.0 5.0 \"\"").empty()); CHECK(parseRazorEdits("5.0 1.0 \"\"").empty()); // Trailing garbage token in a time field -> that triple dropped, not a crash. CHECK(parseRazorEdits("1.0x 2.0 \"\"").empty()); // A dangling partial triple (missing GUID token) is ignored. CHECK(parseRazorEdits("1.0 2.0").empty()); } static void testRazorUnionBounds() { // Union = min start .. max end across all areas (the exact render window). std::vector ranges = {{2.0, 3.0}, {0.5, 1.0}, {4.0, 6.5}}; RazorRange u = razorUnionBounds(ranges); CHECK(u.startSeconds == 0.5); CHECK(u.endSeconds == 6.5); // Empty -> {0,0} sentinel (caller treats as "no razor area"). RazorRange empty = razorUnionBounds({}); CHECK(empty.startSeconds == 0.0 && empty.endSeconds == 0.0); } // --- captureActionTable: the taxonomy ---------------------------------------- static void testTableHasFourWetOnlyRows() { const auto& table = captureActionTable(); // Exactly 4 wet-only rows: MASTER, TRACKS_WET, ITEMS_WET, RAZOR_WET. CHECK(table.size() == 4); std::set ids; bool sawMaster = false, sawTracks = false, sawItems = false, sawRazor = false; for (const auto& def : table) { // Every id is a non-empty CEREBELLUM_REASAMPLER_ string and is UNIQUE // (duplicate ids would collide on registration). std::string id = def.commandString; CHECK(id.rfind("CEREBELLUM_REASAMPLER_", 0) == 0); CHECK(ids.insert(id).second); // false if duplicate // Every row is wet (>=0.5) and a real offline source. CHECK(def.wetDry >= 0.5); CHECK(renderSettingsFor(def.sourceMode, def.wetDry).supported); if (def.sourceMode == SourceMode::MasterMix) sawMaster = true; if (def.sourceMode == SourceMode::SelectedTracks) sawTracks = true; if (def.sourceMode == SourceMode::SelectedItems) sawItems = true; if (def.sourceMode == SourceMode::RazorArea) sawRazor = true; } CHECK(sawMaster); CHECK(sawTracks); CHECK(sawItems); CHECK(sawRazor); } static void testNoDryRowsInTable() { // M7 ships wet-only. No table row must have wetDry < 0.5. for (const auto& def : captureActionTable()) CHECK(def.wetDry >= 0.5); } int main() { testMasterMixWet(); testSelectedTracksWet(); testSelectedItemsSingleFile(); testRazorSingleFile(); testRealtimeIsUnsupportedOffline(); testParseSingleTrackAudioArea(); testParseMultipleAreas(); testParseSkipsEnvelopeLaneAreas(); testParseEmptyAndMalformed(); testRazorUnionBounds(); testTableHasFourWetOnlyRows(); testNoDryRowsInTable(); if (g_fail == 0) std::printf("render_settings: all tests passed\n"); else std::printf("render_settings: %d CHECK(s) FAILED\n", g_fail); return g_fail == 0 ? 0 : 1; }