Render in place: a track's output to a new sibling, source to the bench

This commit is contained in:
2026-08-02 14:17:55 -04:00
parent a0fd931dcb
commit 5c0f5f1591
21 changed files with 727 additions and 32 deletions
+13
View File
@@ -86,4 +86,17 @@ CaptureName composeCaptureName(const CaptureNameInputs& in) {
return out;
}
std::string captureTrackName(const std::string& sourceName) {
const std::string prefix(kCaptureTrackPrefix);
// A source with no readable name yields the bare word rather than a trailing
// space; both spellings are fixed points, which is what makes the whole function
// one (a track named exactly "Capture" must not become "Capture Capture").
const std::string bare = prefix.substr(0, prefix.size() - 1);
if (sourceName.empty()) return bare;
if (sourceName == bare) return sourceName;
if (sourceName.rfind(prefix, 0) == 0) return sourceName;
return prefix + sourceName;
}
} // namespace reasampler::capture
+12
View File
@@ -59,4 +59,16 @@ std::string formatCaptureStamp(const CaptureStamp& stamp);
CaptureName composeCaptureName(const CaptureNameInputs& in);
// Prefixed onto a source track's name to name the track a render-in-place created.
// A display convention, not a persisted key — unlike a lane prefix or an action-id
// suffix, changing it later strands nothing.
inline constexpr const char* kCaptureTrackPrefix = "Capture ";
// The new track's name for a render of `sourceName`. IDEMPOTENT — a fixed point on
// its own output, so a second render over a result track yields "Capture MONEY"
// again rather than "Capture Capture MONEY". A counter suffix is deliberately not
// offered: REAPER does not uniquify track names either, and what distinguishes two
// renders of one source is their position, not their name.
std::string captureTrackName(const std::string& sourceName);
} // namespace reasampler::capture
+22 -12
View File
@@ -45,16 +45,25 @@ std::string sanitizeStem(const std::string& baseName) {
return out;
}
BankPaths deriveBankPaths(const std::string& projectDir,
const std::string& baseName,
const std::string& uniqueTag) {
const std::string dir = normalizeSlashes(projectDir);
RenderPaths deriveRenderPaths(const std::string& absoluteDir,
const std::string& baseName,
const std::string& uniqueTag) {
std::string stem = sanitizeStem(baseName);
if (!uniqueTag.empty()) {
stem += "_" + sanitizeStem(uniqueTag);
}
const std::string fileName = stem + ".wav";
RenderPaths r;
r.fileStem = stem; // stem only — REAPER appends the extension
r.fileName = stem + ".wav";
r.absoluteDir = normalizeSlashes(absoluteDir);
return r;
}
BankPaths deriveBankPaths(const std::string& projectDir,
const std::string& baseName,
const std::string& uniqueTag) {
const std::string dir = normalizeSlashes(projectDir);
// Precondition: caller must resolve a non-empty project directory — an
// empty one would otherwise fall back to a bare relative path (forbidden).
@@ -62,18 +71,19 @@ BankPaths deriveBankPaths(const std::string& projectDir,
// ignores it fails at the render/stat step, not silently onto CWD.
assert(!dir.empty() && "deriveBankPaths: projectDir must not be empty");
const RenderPaths r = deriveRenderPaths(
dir.empty() ? std::string{} : dir + "/" + kBankSubfolder, baseName, uniqueTag);
BankPaths p;
p.fileStem = stem; // stem only — REAPER appends extension
p.fileName = fileName;
p.relativePath = std::string(kBankSubfolder) + "/" + fileName;
p.absoluteDir = dir.empty() ? std::string{}
: dir + "/" + kBankSubfolder;
p.fileStem = r.fileStem;
p.fileName = r.fileName;
p.relativePath = bankRelativeForName(r.fileName);
p.absoluteDir = r.absoluteDir;
return p;
}
std::string bankRelativeForName(const std::string& fileName) {
if (fileName.empty()) return {};
// Same expression deriveBankPaths uses, so the two spellings can't drift.
return std::string(kBankSubfolder) + "/" + fileName;
}
+23 -3
View File
@@ -36,9 +36,29 @@ std::string normalizeSlashes(const std::string& path);
// "capture" if nothing usable remains. Deterministic.
std::string sanitizeStem(const std::string& baseName);
// Derives the bank paths for one capture: baseName is the sanitized file-stem
// source, uniqueTag an optional sanitized disambiguator (timestamp/counter) so
// repeated captures don't collide. Produces "<stem>[_<tag>].wav".
// Where one render writes, with no index spelling at all: the directory REAPER is
// told to render into plus the stem/file name it produces there. `absoluteDir` is
// taken as given (normalized only) rather than derived, because a render that never
// enters the bank has no bank subfolder to append — the render-in-place verb points
// this at the project's own recording path.
struct RenderPaths {
std::string absoluteDir; // RENDER_FILE (forward slash, no trailing slash)
std::string fileName; // <stem>.wav
std::string fileStem; // <stem> (RENDER_PATTERN — REAPER appends the extension)
};
// The file-stem spelling for one render: baseName is the sanitized file-stem source,
// uniqueTag an optional sanitized disambiguator (timestamp/counter) so repeated
// renders don't collide. Produces "<stem>[_<tag>].wav". THE one owner of that
// spelling — deriveBankPaths is expressed over it, and bankRelativeForName depends
// on the bank's spelling never drifting from it.
RenderPaths deriveRenderPaths(const std::string& absoluteDir,
const std::string& baseName,
const std::string& uniqueTag);
// Derives the bank paths for one capture: the same stem spelling as
// deriveRenderPaths, in the bank subfolder, plus the project-relative path the
// index stores.
BankPaths deriveBankPaths(const std::string& projectDir,
const std::string& baseName,
const std::string& uniqueTag);
+34
View File
@@ -25,4 +25,38 @@ std::vector<int> directChildIndices(const std::vector<int>& folderDepths,
return children;
}
SiblingPlacement siblingPlacement(const std::vector<int>& folderDepths, int srcIndex) {
const int count = static_cast<int>(folderDepths.size());
if (count == 0) return SiblingPlacement{};
const int src = srcIndex < 0 ? 0 : (srcIndex >= count ? count - 1 : srcIndex);
// levels[i] is track i's absolute nesting depth; levels[count] is the depth the
// list closes at (0 in a well-formed project). Negative is unrepresentable, so a
// malformed over-closing delta clamps here rather than propagating.
std::vector<int> levels(static_cast<std::size_t>(count) + 1, 0);
for (int i = 0; i < count; ++i) {
const int next = levels[static_cast<std::size_t>(i)] +
folderDepths[static_cast<std::size_t>(i)];
levels[static_cast<std::size_t>(i) + 1] = next < 0 ? 0 : next;
}
const int L = levels[static_cast<std::size_t>(src)];
int p = src + 1;
if (folderDepths[static_cast<std::size_t>(src)] >= 1) {
p = count; // an unterminated folder swallows the rest of the list
for (int j = src + 1; j <= count; ++j) {
if (levels[static_cast<std::size_t>(j)] == L) { p = j; break; }
}
}
SiblingPlacement out;
out.insertIndex = p;
out.precedingIndex = p - 1;
out.precedingDepth = L - levels[static_cast<std::size_t>(p - 1)];
out.newDepth = levels[static_cast<std::size_t>(p)] - L;
return out;
}
} // namespace reasampler::capture
+33 -3
View File
@@ -1,8 +1,8 @@
#pragma once
// track_topology — pure folder arithmetic over a project's track list: which tracks
// are the DIRECT children of a folder parent, derived from the I_FOLDERDEPTH deltas
// alone. NO REAPER types (the shell reads the deltas); unit-tested by
// tests/test_track_topology.cpp.
// are the DIRECT children of a folder parent, and where a new SIBLING of a given
// track goes, both derived from the I_FOLDERDEPTH deltas alone. NO REAPER types
// (the shell reads the deltas); unit-tested by tests/test_track_topology.cpp.
#include <vector>
@@ -21,4 +21,34 @@ namespace reasampler::capture {
std::vector<int> directChildIndices(const std::vector<int>& folderDepths,
int parentIndex);
// Where a new track goes so it is a SIBLING of `srcIndex` — same nesting level, same
// folder — and the two I_FOLDERDEPTH writes that put it there.
struct SiblingPlacement {
int insertIndex = 0; // the index the new track occupies after insertion
// The track that will PRECEDE the new one (insertIndex - 1), and its rewritten
// delta. -1 only for a degenerate empty list, where there is nothing to write.
int precedingIndex = -1;
int precedingDepth = 0;
int newDepth = 0; // the new track's own I_FOLDERDEPTH
};
// Both naive answers are audibly wrong, which is why this is arithmetic and not
// `srcIndex + 1`: inserting straight after a folder PARENT makes the new track that
// folder's first child (its audio re-enters the parent's FX and fader), and inserting
// straight after the folder's LAST track steals that track's closing delta and drops
// the new one outside the folder entirely (its audio bypasses the folder bus).
//
// Levels are absolute nesting depths recovered from the deltas (level[0] = 0,
// level[i+1] = level[i] + depth[i]). A folder parent's insert point is the first
// following track back at the source's own level — i.e. after the whole folder;
// everything else inserts directly below the source. The two writes preserve the
// total delta sum, so no track after the insertion changes level.
//
// A malformed list (deltas not summing to zero, an out-of-range srcIndex) CLAMPS to
// the nearest legal placement rather than asserting: the failure mode of a corrupt
// project must be a track at the wrong nesting level, never a crash.
SiblingPlacement siblingPlacement(const std::vector<int>& folderDepths, int srcIndex);
} // namespace reasampler::capture