feat(capture): bindable capture family — master/tracks/items/razor (M7)
Four wet capture actions route to OfflineRenderBackend (snapshot/restore RENDER_*, 32-bit float), produce a Sample, add to the bank, persist. Pure render_settings maps source mode to RENDER_SETTINGS and parses P_RAZOREDITS (tested). No arrange insertion. True dry deferred to M10.
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
// 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 <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 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<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);
|
||||
}
|
||||
|
||||
// --- 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<std::string> 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;
|
||||
}
|
||||
Reference in New Issue
Block a user