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
+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 +
// 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
// restore. Item scope folds an empty FX identity (take/item FX are not enumerable
// via TrackFX_*); the drift signal then keys on scope+range, which is honest.
// restore. Item scope reads the active take's TakeFX chain (via TakeFX_*) per
// selected item, combined in item order; Track scope reads the track FX chain.
static std::optional<reasampler::Provenance> buildCaptureProvenance(
const reasampler::CaptureRequest& req,
reasampler::CaptureScope scope,
@@ -472,15 +472,27 @@ static std::optional<reasampler::Provenance> buildCaptureProvenance(
recipe.sampleRate = req.sampleRate;
recipe.channelCount = req.channelCount;
recipe.trackGuids = req.trackGuids;
// The in-scope FX-chain identity is the per-track chains combined in track order
// (Track scope), length-prefixed so distinct partitions never collide. Item scope
// has no readable take-FX chain, so each track folds to an empty identity and the
// combined result stays stable/honest (drift then keys on scope + range).
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);
// The in-scope FX-chain identity:
// Track scope — per-track chains combined in track order (TrackFX_*).
// Item scope — per-item active-take chains combined in item order (TakeFX_*);
// the owning track's FX chain is OUT OF SCOPE for an item capture and must
// not be fingerprinted here (it is bypassed during render, not heard).
if (scope == reasampler::CaptureScope::Item) {
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);
}
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;
prov.parentSampleId = *parentId;
@@ -883,12 +895,26 @@ static void RunRecaptureFromSource()
req.trackGuids = recipe->trackGuids;
// Read the CURRENT source FX-chain identity BEFORE the render bypasses it, to
// compare against the recorded identity for drift reporting.
std::vector<std::string> perTrackNow;
perTrackNow.reserve(sourceTracks.size());
for (MediaTrack* tr : sourceTracks)
perTrackNow.push_back(reasampler::fxChainIdentityForTrack(tr));
const std::string currentIdentity = reasampler::combineChainIdentities(perTrackNow);
// compare against the recorded identity for drift reporting. Mirror the same
// scope split as buildCaptureProvenance: item scope reads take FX via TakeFX_*;
// track scope reads the track FX chain via TrackFX_*.
std::string currentIdentity;
if (scope == reasampler::CaptureScope::Item) {
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);
// Render (bank-only; renderOffline never touches the timeline).
@@ -918,6 +944,10 @@ static void RunRecaptureFromSource()
updated.captureTempo = res.sample.captureTempo;
updated.trackGuids = res.sample.trackGuids;
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;
prov.parentSampleId = recordedParentId;
prov.fxChainSnapshot = reasampler::buildFingerprint(refreshed);