Render in place: a track's output to a new sibling, source to the bench
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user