fix(m10): item-scope take-FX fingerprint; Windows path case-fold; comments

Item scope now reads the active take's FX chain via TakeFX_* (new
fxChainIdentityForItems helper), not the owning track's chain.
normalizeSlashes lowercases on _WIN32 for detectParent. Stale
comments corrected; case-fold tests added.
This commit is contained in:
2026-07-26 18:03:20 -04:00
parent 20018c1df2
commit 7d0fcc49ac
8 changed files with 177 additions and 38 deletions
+3 -2
View File
@@ -44,8 +44,9 @@ struct SourceRange {
}; };
// Present only when a sample was resampled FROM another sample. Carries the // Present only when a sample was resampled FROM another sample. Carries the
// parent's id and the FX-chain snapshot string captured at resample time, so the // parent's id and the FX-chain snapshot string (a thin drift fingerprint, NOT a
// null-test / re-capture-from-source action (M10) can reconstruct the chain. // restorable chunk) captured at resample time; the re-capture-from-source action
// (M10) uses it to detect chain drift and replay the original capture request.
struct Provenance { struct Provenance {
std::string parentSampleId; std::string parentSampleId;
std::string fxChainSnapshot; std::string fxChainSnapshot;
+8
View File
@@ -1,6 +1,7 @@
#include "capture_paths.h" #include "capture_paths.h"
#include <cassert> #include <cassert>
#include <cctype>
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstring> // std::memcmp #include <cstring> // std::memcmp
@@ -124,6 +125,13 @@ std::string normalizeSlashes(const std::string& path) {
if (out.size() > 1 && out.back() == '/') { if (out.size() > 1 && out.back() == '/') {
out.pop_back(); out.pop_back();
} }
#ifdef _WIN32
// Windows paths are case-insensitive. Fold to lowercase so that two paths
// that differ only in drive-letter or component casing compare equal (e.g.
// "C:/Foo/BAR.wav" == "c:/foo/bar.wav"). On macOS/Linux, exact case is
// preserved (the filesystem is case-sensitive; folding would be wrong).
for (char& c : out) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
#endif
return out; return out;
} }
+4
View File
@@ -68,6 +68,10 @@ std::string hashWavContent(const std::vector<std::uint8_t>& bytes);
// Normalizes a path to forward slashes and strips any trailing slash. Empty in // Normalizes a path to forward slashes and strips any trailing slash. Empty in
// -> empty out. Pure string transform (does not consult the filesystem). // -> empty out. Pure string transform (does not consult the filesystem).
// Platform case rule: on Windows (_WIN32) the result is also lowercased so that
// paths differing only in drive-letter or component casing compare equal (Windows
// paths are case-insensitive). On macOS/Linux the case is preserved exactly (those
// filesystems are case-sensitive).
std::string normalizeSlashes(const std::string& path); std::string normalizeSlashes(const std::string& path);
// Sanitizes a caller-supplied base name into a filesystem-safe stem: keeps // Sanitizes a caller-supplied base name into a filesystem-safe stem: keeps
+47 -17
View File
@@ -438,8 +438,8 @@ static reasampler::ProvenanceScope provenanceScopeFor(reasampler::CaptureScope s
// fingerprint (P1=a) — scope + source mode + exact range + tail + rate + channels + // fingerprint (P1=a) — scope + source mode + exact range + tail + rate + channels +
// source track GUIDs + the in-scope source FX-chain identity — so "re-capture from // source track GUIDs + the in-scope source FX-chain identity — so "re-capture from
// source" can replay the request and report drift. NEVER a serialized chain to // source" can replay the request and report drift. NEVER a serialized chain to
// restore. Item scope folds an empty FX identity (take/item FX are not enumerable // restore. Item scope reads the active take's TakeFX chain (via TakeFX_*) per
// via TrackFX_*); the drift signal then keys on scope+range, which is honest. // selected item, combined in item order; Track scope reads the track FX chain.
static std::optional<reasampler::Provenance> buildCaptureProvenance( static std::optional<reasampler::Provenance> buildCaptureProvenance(
const reasampler::CaptureRequest& req, const reasampler::CaptureRequest& req,
reasampler::CaptureScope scope, reasampler::CaptureScope scope,
@@ -472,15 +472,27 @@ static std::optional<reasampler::Provenance> buildCaptureProvenance(
recipe.sampleRate = req.sampleRate; recipe.sampleRate = req.sampleRate;
recipe.channelCount = req.channelCount; recipe.channelCount = req.channelCount;
recipe.trackGuids = req.trackGuids; recipe.trackGuids = req.trackGuids;
// The in-scope FX-chain identity is the per-track chains combined in track order // The in-scope FX-chain identity:
// (Track scope), length-prefixed so distinct partitions never collide. Item scope // Track scope — per-track chains combined in track order (TrackFX_*).
// has no readable take-FX chain, so each track folds to an empty identity and the // Item scope — per-item active-take chains combined in item order (TakeFX_*);
// combined result stays stable/honest (drift then keys on scope + range). // the owning track's FX chain is OUT OF SCOPE for an item capture and must
std::vector<std::string> perTrack; // not be fingerprinted here (it is bypassed during render, not heard).
perTrack.reserve(src.sourceTracks.size()); if (scope == reasampler::CaptureScope::Item) {
for (MediaTrack* tr : src.sourceTracks) const int n = CountSelectedMediaItems(nullptr);
perTrack.push_back(reasampler::fxChainIdentityForTrack(tr)); std::vector<MediaItem*> items;
recipe.fxChainIdentity = reasampler::combineChainIdentities(perTrack); items.reserve(static_cast<std::size_t>(n < 0 ? 0 : n));
for (int i = 0; i < n; ++i) {
MediaItem* it = GetSelectedMediaItem(nullptr, i);
if (it) items.push_back(it);
}
recipe.fxChainIdentity = reasampler::fxChainIdentityForItems(items);
} else {
std::vector<std::string> perTrack;
perTrack.reserve(src.sourceTracks.size());
for (MediaTrack* tr : src.sourceTracks)
perTrack.push_back(reasampler::fxChainIdentityForTrack(tr));
recipe.fxChainIdentity = reasampler::combineChainIdentities(perTrack);
}
reasampler::Provenance prov; reasampler::Provenance prov;
prov.parentSampleId = *parentId; prov.parentSampleId = *parentId;
@@ -883,12 +895,26 @@ static void RunRecaptureFromSource()
req.trackGuids = recipe->trackGuids; req.trackGuids = recipe->trackGuids;
// Read the CURRENT source FX-chain identity BEFORE the render bypasses it, to // Read the CURRENT source FX-chain identity BEFORE the render bypasses it, to
// compare against the recorded identity for drift reporting. // compare against the recorded identity for drift reporting. Mirror the same
std::vector<std::string> perTrackNow; // scope split as buildCaptureProvenance: item scope reads take FX via TakeFX_*;
perTrackNow.reserve(sourceTracks.size()); // track scope reads the track FX chain via TrackFX_*.
for (MediaTrack* tr : sourceTracks) std::string currentIdentity;
perTrackNow.push_back(reasampler::fxChainIdentityForTrack(tr)); if (scope == reasampler::CaptureScope::Item) {
const std::string currentIdentity = reasampler::combineChainIdentities(perTrackNow); const int n = CountSelectedMediaItems(nullptr);
std::vector<MediaItem*> items;
items.reserve(static_cast<std::size_t>(n < 0 ? 0 : n));
for (int i = 0; i < n; ++i) {
MediaItem* it = GetSelectedMediaItem(nullptr, i);
if (it) items.push_back(it);
}
currentIdentity = reasampler::fxChainIdentityForItems(items);
} else {
std::vector<std::string> perTrackNow;
perTrackNow.reserve(sourceTracks.size());
for (MediaTrack* tr : sourceTracks)
perTrackNow.push_back(reasampler::fxChainIdentityForTrack(tr));
currentIdentity = reasampler::combineChainIdentities(perTrackNow);
}
const bool drifted = (currentIdentity != recipe->fxChainIdentity); const bool drifted = (currentIdentity != recipe->fxChainIdentity);
// Render (bank-only; renderOffline never touches the timeline). // Render (bank-only; renderOffline never touches the timeline).
@@ -918,6 +944,10 @@ static void RunRecaptureFromSource()
updated.captureTempo = res.sample.captureTempo; updated.captureTempo = res.sample.captureTempo;
updated.trackGuids = res.sample.trackGuids; updated.trackGuids = res.sample.trackGuids;
updated.createdTimestamp = res.sample.createdTimestamp; updated.createdTimestamp = res.sample.createdTimestamp;
// NOTE: levels, clipped, and lengthBeats are carried from the original (via the
// *orig copy above) because the offline backend does not populate them today
// (res.sample leaves them at defaults). If a later milestone populates these
// fields at capture time, refresh them here from res.sample instead.
reasampler::Provenance prov; reasampler::Provenance prov;
prov.parentSampleId = recordedParentId; prov.parentSampleId = recordedParentId;
prov.fxChainSnapshot = reasampler::buildFingerprint(refreshed); prov.fxChainSnapshot = reasampler::buildFingerprint(refreshed);
+9 -7
View File
@@ -46,13 +46,14 @@ enum class ProvenanceScope {
Track, Track,
}; };
// One FX-chain entry as the shell reads it from REAPER (TrackFX_GetFXName / // One FX-chain entry as the shell reads it from REAPER. For Track scope the shell
// TrackFX_GetFXGUID / TrackFX_GetEnabled). Plain data — the shell fills it, the // uses TrackFX_GetFXName/GetFXGUID/GetEnabled; for Item scope it uses the TakeFX_*
// equivalents over the active take's FX chain. Plain data — the shell fills it, the
// pure fold turns the vector into the identity string. // pure fold turns the vector into the identity string.
struct FxIdentityEntry { struct FxIdentityEntry {
std::string name; // TrackFX_GetFXName std::string name; // TrackFX_GetFXName / TakeFX_GetFXName
std::string guid; // TrackFX_GetFXGUID -> guidToString (per-instance identity) std::string guid; // TrackFX_GetFXGUID / TakeFX_GetFXGUID -> guidToString
bool enabled; // TrackFX_GetEnabled bool enabled; // TrackFX_GetEnabled / TakeFX_GetEnabled
}; };
// The recorded capture recipe + source FX-chain identity — the thin fingerprint. // The recorded capture recipe + source FX-chain identity — the thin fingerprint.
@@ -138,8 +139,9 @@ struct BankFileRef {
// //
// Returns the parent sample id, or nullopt when the capture is not a genuine // Returns the parent sample id, or nullopt when the capture is not a genuine
// resample-from-sample. Comparison is exact path identity; the caller normalizes // resample-from-sample. Comparison is exact path identity; the caller normalizes
// both sides identically so a slash/case difference never spuriously matches or // both sides identically via normalizeSlashes (which lowercases on Windows) so a
// misses (case handling is the caller's normalization contract, not decided here). // slash/case difference never spuriously matches or misses. On Windows both sides
// are lowercased before they reach here; on macOS/Linux they are case-exact.
std::optional<std::string> detectParent( std::optional<std::string> detectParent(
const std::vector<std::string>& sourceItemFiles, const std::vector<std::string>& sourceItemFiles,
const std::vector<BankFileRef>& bankFiles); const std::vector<BankFileRef>& bankFiles);
+39
View File
@@ -8,6 +8,10 @@
// * TrackFX_GetFXName(MediaTrack*, int, char*, int) -> bool (~7356) // * TrackFX_GetFXName(MediaTrack*, int, char*, int) -> bool (~7356)
// * TrackFX_GetFXGUID(MediaTrack*, int) -> GUID* (~7348) // * TrackFX_GetFXGUID(MediaTrack*, int) -> GUID* (~7348)
// * TrackFX_GetEnabled(MediaTrack*, int) -> bool (~7291) // * TrackFX_GetEnabled(MediaTrack*, int) -> bool (~7291)
// * TakeFX_GetCount(MediaItem_Take*) (~6710)
// * TakeFX_GetFXName(MediaItem_Take*, int, char*, int) -> bool (~6758)
// * TakeFX_GetFXGUID(MediaItem_Take*, int) -> GUID* (~6750)
// * TakeFX_GetEnabled(MediaItem_Take*, int) -> bool (~6718)
// * CountSelectedMediaItems / GetSelectedMediaItem (selection reads) // * CountSelectedMediaItems / GetSelectedMediaItem (selection reads)
// * GetActiveTake(MediaItem*) -> MediaItem_Take* (active take) // * GetActiveTake(MediaItem*) -> MediaItem_Take* (active take)
// * GetMediaItemTake_Source(MediaItem_Take*) -> PCM_source* (~2053) // * GetMediaItemTake_Source(MediaItem_Take*) -> PCM_source* (~2053)
@@ -28,6 +32,10 @@
#define REAPERAPI_WANT_TrackFX_GetFXName #define REAPERAPI_WANT_TrackFX_GetFXName
#define REAPERAPI_WANT_TrackFX_GetFXGUID #define REAPERAPI_WANT_TrackFX_GetFXGUID
#define REAPERAPI_WANT_TrackFX_GetEnabled #define REAPERAPI_WANT_TrackFX_GetEnabled
#define REAPERAPI_WANT_TakeFX_GetCount
#define REAPERAPI_WANT_TakeFX_GetFXName
#define REAPERAPI_WANT_TakeFX_GetFXGUID
#define REAPERAPI_WANT_TakeFX_GetEnabled
#define REAPERAPI_WANT_CountSelectedMediaItems #define REAPERAPI_WANT_CountSelectedMediaItems
#define REAPERAPI_WANT_GetSelectedMediaItem #define REAPERAPI_WANT_GetSelectedMediaItem
#define REAPERAPI_WANT_CountTrackMediaItems #define REAPERAPI_WANT_CountTrackMediaItems
@@ -67,6 +75,37 @@ std::string fxChainIdentityForTrack(MediaTrack* tr) {
return fxChainIdentity(rows); return fxChainIdentity(rows);
} }
std::string fxChainIdentityForItems(const std::vector<MediaItem*>& items) {
// For Item scope the in-scope chain is each item's active take's FX chain, NOT
// the owning track's FX chain (the track chain is out-of-scope and is bypassed
// during render). TakeFX_* is the correct family here.
std::vector<std::string> perItem;
perItem.reserve(items.size());
for (MediaItem* it : items) {
if (!it) { perItem.push_back(fxChainIdentity({})); continue; }
MediaItem_Take* take = GetActiveTake(it);
if (!take) { perItem.push_back(fxChainIdentity({})); continue; }
std::vector<FxIdentityEntry> rows;
const int n = TakeFX_GetCount(take);
rows.reserve(static_cast<std::size_t>(n < 0 ? 0 : n));
for (int i = 0; i < n; ++i) {
FxIdentityEntry e;
char nameBuf[512] = {0};
if (TakeFX_GetFXName(take, i, nameBuf, static_cast<int>(sizeof(nameBuf))))
e.name = nameBuf;
if (GUID* g = TakeFX_GetFXGUID(take, i)) {
char gb[64] = {0};
guidToString(g, gb);
e.guid = gb;
}
e.enabled = TakeFX_GetEnabled(take, i);
rows.push_back(std::move(e));
}
perItem.push_back(fxChainIdentity(rows));
}
return combineChainIdentities(perItem);
}
namespace { namespace {
// The active take source file of one item, normalized. Empty if unresolvable. // The active take source file of one item, normalized. Empty if unresolvable.
std::string itemSourceFile(MediaItem* it) { std::string itemSourceFile(MediaItem* it) {
+13 -7
View File
@@ -22,20 +22,26 @@
#include "provenance.h" #include "provenance.h"
class MediaTrack; class MediaTrack;
class MediaItem;
namespace reasampler { namespace reasampler {
class BankBook; class BankBook;
// The in-scope FX-chain identity of a source track, folded to the pure // The in-scope FX-chain identity of a source track (Track scope), folded to the
// provenance string. For TRACK scope this is the track's own FX chain; for ITEM // pure provenance string. Reads the track's own FX chain via TrackFX_GetCount /
// scope the take/item FX are the in-scope chain — but item/take FX are not // TrackFX_GetFXName / TrackFX_GetFXGUID / TrackFX_GetEnabled in chain order.
// enumerable via the TrackFX_* family, so an item-scope capture folds an EMPTY
// chain identity (the drift signal then keys on scope + range only, which is
// honest: we do not claim to fingerprint take FX we cannot read). TRACK scope reads
// TrackFX_GetCount / GetFXName / GetFXGUID / GetEnabled in chain order.
std::string fxChainIdentityForTrack(MediaTrack* tr); std::string fxChainIdentityForTrack(MediaTrack* tr);
// The in-scope FX-chain identity for Item scope: enumerates each item's active
// take FX chain via TakeFX_GetCount / TakeFX_GetFXName / TakeFX_GetFXGUID /
// TakeFX_GetEnabled, in item order then FX order, combined with
// combineChainIdentities so distinct per-item partitions never collide. Returns
// the combined identity string (empty combined identity for a no-FX or no-item
// set). The items vector is the same source-item set the shell collected for the
// item-scope capture (selected items whose owning tracks were also collected).
std::string fxChainIdentityForItems(const std::vector<MediaItem*>& items);
// Reads the media-file path of every SELECTED media item's active take source // Reads the media-file path of every SELECTED media item's active take source
// (GetMediaItemTake_Source -> GetMediaSourceFileName), normalized to forward-slash. // (GetMediaItemTake_Source -> GetMediaSourceFileName), normalized to forward-slash.
// Unresolvable items (no take / no source / empty name) are omitted — never an // Unresolvable items (no take / no source / empty name) are omitted — never an
+54 -5
View File
@@ -18,12 +18,40 @@ static int g_fail = 0;
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
static void testNormalizeSlashes() { static void testNormalizeSlashes() {
#ifdef _WIN32
// On Windows paths are lowercased for case-insensitive comparison.
CHECK(normalizeSlashes("C:\\a\\b") == "c:/a/b");
CHECK(normalizeSlashes("a/b/c") == "a/b/c");
CHECK(normalizeSlashes("a/b/") == "a/b"); // trailing slash stripped
CHECK(normalizeSlashes("a\\b\\") == "a/b"); // backslash + trailing
CHECK(normalizeSlashes("/") == "/"); // lone root preserved
CHECK(normalizeSlashes("") == ""); // empty stays empty
#else
CHECK(normalizeSlashes("C:\\a\\b") == "C:/a/b"); CHECK(normalizeSlashes("C:\\a\\b") == "C:/a/b");
CHECK(normalizeSlashes("a/b/c") == "a/b/c"); CHECK(normalizeSlashes("a/b/c") == "a/b/c");
CHECK(normalizeSlashes("a/b/") == "a/b"); // trailing slash stripped CHECK(normalizeSlashes("a/b/") == "a/b");
CHECK(normalizeSlashes("a\\b\\") == "a/b"); // backslash + trailing CHECK(normalizeSlashes("a\\b\\") == "a/b");
CHECK(normalizeSlashes("/") == "/"); // lone root preserved CHECK(normalizeSlashes("/") == "/");
CHECK(normalizeSlashes("") == ""); // empty stays empty CHECK(normalizeSlashes("") == "");
#endif
}
// Windows case-folding: paths differing only in casing must compare equal after
// normalizeSlashes, since Windows paths are case-insensitive. On non-Windows the
// function is case-preserving (filesystem is case-sensitive).
static void testNormalizeSlashesCaseFolding() {
#ifdef _WIN32
// Drive letter and component casing differences are neutralized.
CHECK(normalizeSlashes("C:/Foo/BAR.wav") == normalizeSlashes("c:/foo/bar.wav"));
CHECK(normalizeSlashes("C:/Foo/BAR.wav") == "c:/foo/bar.wav");
// Mixed-case input produces consistently lowercase output.
CHECK(normalizeSlashes("C:\\Users\\Daniel\\Proj\\File.WAV")
== "c:/users/daniel/proj/file.wav");
#else
// Non-Windows: case is preserved exactly (case-sensitive filesystem).
CHECK(normalizeSlashes("C:/Foo/BAR.wav") != normalizeSlashes("c:/foo/bar.wav"));
CHECK(normalizeSlashes("C:/Foo/BAR.wav") == "C:/Foo/BAR.wav");
#endif
} }
static void testSanitizeStem() { static void testSanitizeStem() {
@@ -57,7 +85,12 @@ static void testDeriveRelativePathIsProjectRelative() {
static void testDeriveAbsoluteDirJoinsProjectDir() { static void testDeriveAbsoluteDirJoinsProjectDir() {
BankPaths p = deriveBankPaths("C:\\Users\\d\\proj", "kick", ""); BankPaths p = deriveBankPaths("C:\\Users\\d\\proj", "kick", "");
// Backslashes normalized; bank subfolder appended; no trailing slash. // Backslashes normalized; bank subfolder appended; no trailing slash.
// On Windows the drive-letter + components are lowercased by normalizeSlashes.
#ifdef _WIN32
CHECK(p.absoluteDir == "c:/users/d/proj/reasampler_bank");
#else
CHECK(p.absoluteDir == "C:/Users/d/proj/reasampler_bank"); CHECK(p.absoluteDir == "C:/Users/d/proj/reasampler_bank");
#endif
// No unique tag -> stem has no trailing "_". // No unique tag -> stem has no trailing "_".
CHECK(p.fileName == "kick.wav"); CHECK(p.fileName == "kick.wav");
CHECK(p.relativePath == "reasampler_bank/kick.wav"); CHECK(p.relativePath == "reasampler_bank/kick.wav");
@@ -116,7 +149,15 @@ static void testFileStem() {
static void testResolveBankFileAgainstProjectDir() { static void testResolveBankFileAgainstProjectDir() {
// A relative index entry resolves to <projectDir>/<relativePath>, forward- // A relative index entry resolves to <projectDir>/<relativePath>, forward-
// slashed, regardless of the input slash style. // slashed, regardless of the input slash style. On Windows the result is also
// lowercased (Windows paths are case-insensitive; normalizeSlashes folds them).
#ifdef _WIN32
CHECK(resolveBankFile("C:\\Users\\d\\proj", "reasampler_bank/kick.wav")
== "c:/users/d/proj/reasampler_bank/kick.wav");
// Backslashes in the stored relative path are normalized on resolution.
CHECK(resolveBankFile("C:/p", "reasampler_bank\\a.wav")
== "c:/p/reasampler_bank/a.wav");
#else
CHECK(resolveBankFile("C:\\Users\\d\\proj", "reasampler_bank/kick.wav") CHECK(resolveBankFile("C:\\Users\\d\\proj", "reasampler_bank/kick.wav")
== "C:/Users/d/proj/reasampler_bank/kick.wav"); == "C:/Users/d/proj/reasampler_bank/kick.wav");
CHECK(resolveBankFile("/home/d/proj", "reasampler_bank/mix.wav") CHECK(resolveBankFile("/home/d/proj", "reasampler_bank/mix.wav")
@@ -127,6 +168,7 @@ static void testResolveBankFileAgainstProjectDir() {
// Backslashes in the stored relative path are normalized on resolution. // Backslashes in the stored relative path are normalized on resolution.
CHECK(resolveBankFile("/p", "reasampler_bank\\a.wav") CHECK(resolveBankFile("/p", "reasampler_bank\\a.wav")
== "/p/reasampler_bank/a.wav"); == "/p/reasampler_bank/a.wav");
#endif
} }
static void testResolveBankFileRejectsEmptyInputs() { static void testResolveBankFileRejectsEmptyInputs() {
@@ -159,10 +201,16 @@ static void testResolveAgainstNewProjectDirAfterSaveAs() {
static void testRelocationPlanForSaveAs() { static void testRelocationPlanForSaveAs() {
// Save-As to a different directory: relocation is needed; both bank dirs are // Save-As to a different directory: relocation is needed; both bank dirs are
// <projectDir>/reasampler_bank, forward-slashed, no trailing slash. // <projectDir>/reasampler_bank, forward-slashed, no trailing slash.
// On Windows the drive-letter and path components are lowercased.
BankRelocation r = deriveRelocationPlan("C:\\old\\proj", "C:/new/proj"); BankRelocation r = deriveRelocationPlan("C:\\old\\proj", "C:/new/proj");
CHECK(r.needed); CHECK(r.needed);
#ifdef _WIN32
CHECK(r.oldBankDir == "c:/old/proj/reasampler_bank");
CHECK(r.newBankDir == "c:/new/proj/reasampler_bank");
#else
CHECK(r.oldBankDir == "C:/old/proj/reasampler_bank"); CHECK(r.oldBankDir == "C:/old/proj/reasampler_bank");
CHECK(r.newBankDir == "C:/new/proj/reasampler_bank"); CHECK(r.newBankDir == "C:/new/proj/reasampler_bank");
#endif
} }
static void testRelocationPlanNotNeededForSaveInPlace() { static void testRelocationPlanNotNeededForSaveInPlace() {
@@ -501,6 +549,7 @@ static void testHashWavContentDomainSeparationFromWholeFile() {
int main() { int main() {
testNormalizeSlashes(); testNormalizeSlashes();
testNormalizeSlashesCaseFolding();
testSanitizeStem(); testSanitizeStem();
testDeriveRelativePathIsProjectRelative(); testDeriveRelativePathIsProjectRelative();
testDeriveAbsoluteDirJoinsProjectDir(); testDeriveAbsoluteDirJoinsProjectDir();