Refuse every multi-track selected-tracks render, both scopes; make a failed mono collapse observable

This commit is contained in:
2026-08-01 22:40:54 -04:00
parent 6e6f4a6a15
commit f69c4bf6bf
14 changed files with 366 additions and 114 deletions
+21 -5
View File
@@ -112,11 +112,27 @@ SourceMode sourceModeForScope(CaptureScope scope, bool itemExtentIsWindow) {
return SourceMode::SelectedTracks;
}
bool isMultiTrackRangedItemRender(CaptureScope scope, SourceMode mode,
int sourceTrackCount) {
return scope == CaptureScope::Item
&& mode == SourceMode::SelectedTracks
&& sourceTrackCount > 1;
bool isMultiTrackStemRender(SourceMode mode, int sourceTrackCount) {
return mode == SourceMode::SelectedTracks && sourceTrackCount > 1;
}
std::string multiTrackRefusalMessage(CaptureScope scope) {
switch (scope) {
case CaptureScope::Item:
return "This range is narrower than the selected items, so it renders "
"through their tracks -- and those items span more than one track, "
"which this shape cannot land as a single file. Capture one track's "
"items at a time, or make the range match the items' extent.";
case CaptureScope::Track:
return "A track capture renders the selected tracks through the master, "
"and more than one track cannot land as a single file. Capture one "
"track at a time, or route them into a folder/bus track and capture "
"that (a folder's own output is its children summed).";
}
// Unreachable for a valid enum; a refusal with no way out is still better than a
// silent one, so fail closed to the scope-agnostic half of the message.
return "This selection spans more than one track, which cannot land as a single "
"file. Capture one track at a time.";
}
RangeSource inferRangeSource(bool hasRazorArea) {
+16 -12
View File
@@ -124,19 +124,23 @@ enum class CaptureScope {
// track ITSELF is deliberately not isolated; see src/shell/capture/CLAUDE.md.
SourceMode sourceModeForScope(CaptureScope scope, bool itemExtentIsWindow);
// True for the one render shape that cannot land as a single capture: an item-scope
// capture re-sourced to the selected-tracks render (its window is not the item
// extent) whose selected items span more than one track. That source is read as
// rendering one file per selected track — the single-file bit is documented for
// item/razor sources only (SDK header ~3041), which is the whole basis for the
// reading and is DAW-unverified. If it holds, N tracks collapse N stems onto one
// literal render pattern and whichever file survived would land as a successful
// capture carrying one track's audio. The caller refuses instead.
// True for the one render shape that cannot land as a single capture: a selected-tracks
// render covering more than one track — a ranged item capture whose items span several
// tracks, or any multi-track track capture. That source is read as rendering one file
// per selected track — the single-file bit is documented for item/razor sources only
// (SDK header ~3041), which is the whole basis for the reading and is DAW-unverified.
// If it holds, N tracks collapse N stems onto one literal render pattern and whichever
// file survived would land as a successful capture carrying one track's audio. The
// caller refuses instead.
//
// Track scope is deliberately NOT covered here even though it renders through the
// same source — see src/shell/capture/CLAUDE.md §Gotchas.
bool isMultiTrackRangedItemRender(CaptureScope scope, SourceMode mode,
int sourceTrackCount);
// Scope is deliberately NOT a parameter: the exposure comes from the render SOURCE,
// which both scopes reach.
bool isMultiTrackStemRender(SourceMode mode, int sourceTrackCount);
// The refusal text for the shape above. Keyed on scope because only the way OUT differs:
// an item capture can also widen its range to the items' own extent, which a track
// capture has no analog for. Kept beside the predicate so the two read as siblings.
std::string multiTrackRefusalMessage(CaptureScope scope);
// --- Range inference: razor-else-time (orthogonal to scope) -------------------
//
+10
View File
@@ -300,6 +300,16 @@ MonoCollapse collapseToMono(const std::vector<std::uint8_t>& bytes) {
return out;
}
std::string monoCollapseSuffix(MonoCollapseOutcome outcome) {
switch (outcome) {
case MonoCollapseOutcome::Declined: return {};
case MonoCollapseOutcome::Collapsed: return " (collapsed to mono)";
case MonoCollapseOutcome::Failed:
return " (mono collapse failed -- left as captured)";
}
return {}; // unreachable for a valid enum; claim nothing rather than a wrong outcome
}
std::string hashBytes(const std::uint8_t* data, std::size_t len) {
// FNV-1a 64-bit: deterministic, no dependencies, adequate for dedup identity.
std::uint64_t h = kFnvOffsetBasis;
+15
View File
@@ -117,6 +117,21 @@ struct MonoCollapse {
// including the bext/source-position consequence beyond hashing.
MonoCollapse collapseToMono(const std::vector<std::uint8_t>& bytes);
// How applying the collapse to a captured FILE ended. `Declined` is collapseToMono's own
// "nothing to do"; `Failed` is a read that never happened or a warranted rewrite that did
// not land. The capture is intact and correctly measured in every case — only the report
// tells them apart, which is why the two must not share one value.
enum class MonoCollapseOutcome {
Declined,
Collapsed,
Failed,
};
// The capture message's collapse suffix — empty for Declined, so a capture that had
// nothing to collapse reads exactly as it did before the collapse existed. Shared by
// both backends so one outcome cannot be reported two ways.
std::string monoCollapseSuffix(MonoCollapseOutcome outcome);
// --- Content identity (dedup hashes) -----------------------------------------
// Deterministic FNV-1a 64-bit content hash over `len` bytes, as 16-char lowercase