Files
reasampler/tests/test_render_settings.cpp
T
daniel 4ef41cf705 refactor(capture): drop master scope; realtime taps selected track
Capture is now item + track only (master removed as a scope; still bypassed
as out-of-scope chain). Realtime records the selected track's own output via
per-track post-fader sends into a hidden temp track, fixing the silent file.
2026-07-23 16:34:02 -04:00

199 lines
8.2 KiB
C++

// Standalone tests for reasampler::render_settings — no REAPER, no framework.
// Covers the pure pieces behind the two-scope capture family: the source-mode ->
// RENDER_SETTINGS bit mapping, P_RAZOREDITS parsing -> ranges + union, scope ->
// source mode, range inference (razor-else-time), the FX-bypass plan (corrects
// the "items captured through parent FX" defect), and the capture-action taxonomy
// table (stable ids, one row per scope).
#include "../src/render_settings.h"
#include <cstdio>
#include <set>
#include <string>
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 (all three-scope capture 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
// razor captures render track audio only. 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<RazorRange> 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);
}
// --- sourceModeForScope: scope -> render source mode -------------------------
static void testScopeSourceModes() {
// Each scope drives a distinct render source. Item -> items, Track -> tracks.
// (There is no master scope — to capture the master you render a track.) These
// feed renderSettingsFor and must be supported.
CHECK(sourceModeForScope(CaptureScope::Item) == SourceMode::SelectedItems);
CHECK(sourceModeForScope(CaptureScope::Track) == SourceMode::SelectedTracks);
// Every scope's source mode is an offline-supported render source.
CHECK(renderSettingsFor(sourceModeForScope(CaptureScope::Item), 1.0).supported);
CHECK(renderSettingsFor(sourceModeForScope(CaptureScope::Track), 1.0).supported);
}
// --- inferRangeSource: razor-else-time (orthogonal to scope) -----------------
static void testRangeInference() {
// Razor present -> razor union wins; no razor -> time selection.
CHECK(inferRangeSource(true) == RangeSource::Razor);
CHECK(inferRangeSource(false) == RangeSource::TimeSelection);
}
// --- fxBypassPlanFor: the FX-scope invariant ----------------------------------
static void testItemScopeBypassesEverythingButTake() {
// Item = take/item FX ONLY. Bypass the item's own track FX, its ancestors, and
// the master. (If this returned bypassSelfFx=false the M7 defect — items heard
// through the track's FX — would recur; the assertion pins the fix.)
FxBypassPlan p = fxBypassPlanFor(CaptureScope::Item);
CHECK(p.bypassSelfFx);
CHECK(p.bypassAncestorFx);
CHECK(p.bypassMaster);
}
static void testTrackScopeKeepsSelfBypassesAncestorsAndMaster() {
// Track = item FX + the track's OWN FX. Keep self FX; bypass ancestors + master.
// Master stays a bypass target even though it is no longer a capture scope.
FxBypassPlan p = fxBypassPlanFor(CaptureScope::Track);
CHECK(!p.bypassSelfFx); // the whole point: the track's own FX stays live
CHECK(p.bypassAncestorFx); // no parent/folder FX
CHECK(p.bypassMaster); // no master FX
}
// --- captureActionTable: the two-scope taxonomy ------------------------------
static void testTableHasTwoScopeRows() {
const auto& table = captureActionTable();
// Exactly 2 scope rows: item, track. There is no master scope.
CHECK(table.size() == 2);
std::set<std::string> ids;
bool sawItem = false, sawTrack = 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 scope resolves to a supported offline source.
CHECK(renderSettingsFor(sourceModeForScope(def.scope), 1.0).supported);
if (def.scope == CaptureScope::Item) sawItem = true;
if (def.scope == CaptureScope::Track) sawTrack = true;
}
CHECK(sawItem);
CHECK(sawTrack);
}
int main() {
testMasterMixWet();
testSelectedTracksWet();
testSelectedItemsSingleFile();
testRazorSingleFile();
testRealtimeIsUnsupportedOffline();
testParseSingleTrackAudioArea();
testParseMultipleAreas();
testParseSkipsEnvelopeLaneAreas();
testParseEmptyAndMalformed();
testRazorUnionBounds();
testScopeSourceModes();
testRangeInference();
testItemScopeBypassesEverythingButTake();
testTrackScopeKeepsSelfBypassesAncestorsAndMaster();
testTableHasTwoScopeRows();
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;
}