Settle the render window on the time selection and delete the experiment that proved it
The millisecond floor lives in the custom-bounds field, not the engine, so RENDER_BOUNDSFLAG=2 is now the only bounds mode: the two-position type, the console verdict and the STARTPOS/ENDPOS drift probe all go. capture.cpp 697 -> 622.
This commit is contained in:
@@ -51,8 +51,8 @@ Detail specific to these pure modules:
|
||||
- `capture_paths` — the REAPER-free path arithmetic behind offline capture: bank-subfolder + unique-filename derivation (`deriveBankPaths`, forward-slash form, no filesystem touch), the absolute-render-dir vs. project-relative-index-path split (`BankPaths`), the persist-side inverse (`resolveBankFile`, `projectDirOfRpp`), the Save-As bank-relocation plan (`deriveRelocationPlan`), and the GUID-primary project-identity classifier (`classifyProjectTransition` → `NoOp`/`Load`/`SaveAsRelocate`) the persist-poll timer drives.
|
||||
- `capture_name` — the REAPER-free composition of one capture's label + file-stem base from its source-track name(s), a local-calendar discriminator (`MM-DD HHMM`, from the shell's clock read), and an optional batch ordinal. The label and the stem deliberately diverge: the stem still passes through `capture_paths::sanitizeStem` (so a name that sanitizes to nothing files as `capture`), while the label keeps the source name verbatim. Stem uniqueness stays entirely `makeUniqueTag`'s — this module never disambiguates.
|
||||
- `insert_plan` — the REAPER-free logic behind the `insert` shell (M6): computes the `InsertMedia` `mode` bitmask from an `InsertOptions` struct (placement target, tempo-conform ratio, preserve-pitch flag), guaranteeing the &4 stretch-to-time-selection bit is never set and that no tempo bits are set when `conform == None`.
|
||||
- `render_settings` — the REAPER-free logic behind the capture action family: `SourceMode` → `RENDER_SETTINGS` bit mapping, `P_RAZOREDITS` string parsing + range-union bounds, razor-else-time range inference, the FX-scope bypass plan (`fxBypassPlanFor`), the bounds channel a capture hands its window over on (`RenderBoundsChannel`/`renderBoundsFlagFor`/`renderBoundsChannelLabel`), the tail-mode → `RENDER_TAILFLAG`/`RENDER_NORMALIZE`/`RENDER_TRIMEND` mapping (`tailRenderSettingsFor`) and its realtime-window analog (`realtimeRecordWindowEnd`), the capture-action taxonomy table (`captureActionTable`) `main.cpp` iterates to register the CAPTURE_ITEM/CAPTURE_TRACK family, and `renderSourceLabel` (the source named in the offline backend's bounds refusal).
|
||||
- `render_window` — the REAPER-free frame arithmetic behind exact capture bounds: `frameCountFor` (the frame count a project-time window occupies at the project rate — the number the offline backend checks the rendered file against before landing it, so a render that printed something other than the window is refused rather than banked), `renderHonoredBounds` (the gate's verdict and the sole home of its one-frame tolerance, which is empirical rather than proven — the header states which renderer models it covers and which it does not), and `itemExtentPrintsWindow`, the predicate `render_settings::sourceModeForScope` consults to decide whether REAPER's selected-items render source can express a requested window at all. It also owns the short-render diagnostics: `msFlooredEndFrameCount` (the frames a window holds with its end floored to the millisecond — the shape two live short renders matched, quoted by the refusal as a count coincidence and nothing more), `describeBoundsDrift` (the sentence the offline backend prints when a channel's stored bounds do not read back as they were written), `isOnMillisecondGrid` (whether an observed edge can speak to a rounding question at all — an on-grid edge cannot), and `describeBoundsExperiment` (the always-printed verdict naming which bounds channel carried a capture's window and what the landed file measured).
|
||||
- `render_settings` — the REAPER-free logic behind the capture action family: `SourceMode` → `RENDER_SETTINGS` bit mapping, `P_RAZOREDITS` string parsing + range-union bounds, razor-else-time range inference, the FX-scope bypass plan (`fxBypassPlanFor`), the one bounds mode a capture hands its window over on (`kRenderBoundsTimeSelection`) and the tail bit paired with it (`kTailFlagTimeSelection`), the tail-mode → `RENDER_TAILFLAG`/`RENDER_NORMALIZE`/`RENDER_TRIMEND` mapping (`tailRenderSettingsFor`) and its realtime-window analog (`realtimeRecordWindowEnd`), the capture-action taxonomy table (`captureActionTable`) `main.cpp` iterates to register the CAPTURE_ITEM/CAPTURE_TRACK family, and `renderSourceLabel` (the source named in the offline backend's bounds refusal).
|
||||
- `render_window` — the REAPER-free frame arithmetic behind exact capture bounds: `frameCountFor` (the frame count a project-time window occupies at the project rate — the number the offline backend checks the rendered file against before landing it, so a render that printed something other than the window is refused rather than banked), `renderHonoredBounds` (the gate's verdict and the sole home of its one-frame tolerance, which is empirical rather than proven — the header states which renderer models it covers and which it does not), and `itemExtentPrintsWindow`, the predicate `render_settings::sourceModeForScope` consults to decide whether REAPER's selected-items render source can express a requested window at all. It also owns the one short-render diagnostic: `msFlooredEndFrameCount` (the frames a window holds with its end floored to the millisecond — the shape two live short renders matched on the retired custom-bounds mode, quoted by a refusal as a count coincidence and nothing more) and `isOnMillisecondGrid`, the whole-millisecond tolerance that count depends on.
|
||||
- `track_topology` — the REAPER-free folder arithmetic over a project's flat `I_FOLDERDEPTH` delta list: `directChildIndices` names a folder parent's DIRECT children, the set `shell/capture/render_isolation` silences so a ranged item capture does not print its track's children. Grandchildren are excluded by construction — they reach the parent only through the child that owns them.
|
||||
- `tail_control` — the REAPER-free logic behind the docked `bank_panel`'s tail-mode toggle: the cycle order (None → Auto → Manual → None), the Manual-length clamp/scroll-wheel fine-adjust (`clampManualMs`/`adjustManualMs`, 250 ms/notch, 2000 ms default), the toggle's label text (e.g. "Tail: Manual 2.0s"), and the `TailSetting` JSON round-trip persist stores per-project.
|
||||
|
||||
@@ -79,12 +79,11 @@ Detail specific to these pure modules:
|
||||
that with a transient silencing (`shell/capture/render_isolation`) whose child-set
|
||||
walk lives here in `track_topology`; the item-vs-track asymmetry behind it is in
|
||||
`src/shell/capture/CLAUDE.md`.
|
||||
- **The render window floors to the millisecond at render time.** Measured cause,
|
||||
why two bounds channels exist, and the live experiment: `render_settings.h`'s
|
||||
`RenderBoundsChannel` — the one narrative home; this bullet is a pointer, not a
|
||||
retelling. The one fact worth keeping local: every observation to date started at
|
||||
`0s`, on the grid, so **nothing is known about whether the start floors too** —
|
||||
assume neither.
|
||||
- **The custom-time-bounds field floors the render window to the millisecond; the
|
||||
time selection does not.** Both observations and why only one bounds mode is
|
||||
reachable: `render_settings.h`'s `kRenderBoundsTimeSelection` — the one narrative
|
||||
home; this bullet is a pointer, not a retelling. Do not reintroduce
|
||||
`RENDER_BOUNDSFLAG=0`.
|
||||
- `kRenderPreFaderStems` (&8192) is deliberately **not** used — REAPER offline
|
||||
render has no true pre-FX "dry" bit; FX scoping is done entirely by the
|
||||
FX-bypass-around-render mechanism, never by a render bit.
|
||||
|
||||
@@ -14,36 +14,7 @@ double autoTrimEndRatio() {
|
||||
return std::pow(10.0, kAutoTrimThresholdDb / 20.0);
|
||||
}
|
||||
|
||||
int renderBoundsFlagFor(RenderBoundsChannel channel) {
|
||||
switch (channel) {
|
||||
case RenderBoundsChannel::CustomTimeBounds: return 0;
|
||||
case RenderBoundsChannel::TimeSelection: return 2;
|
||||
}
|
||||
// Unreachable for a valid enum; fail closed to the channel every shipped capture
|
||||
// rendered on, never to a mode that bounds itself off something else entirely.
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tailFlagBitFor(RenderBoundsChannel channel) {
|
||||
switch (channel) {
|
||||
case RenderBoundsChannel::CustomTimeBounds: return kTailFlagCustomBounds;
|
||||
case RenderBoundsChannel::TimeSelection: return kTailFlagTimeSelection;
|
||||
}
|
||||
return kTailFlagCustomBounds; // paired with renderBoundsFlagFor's fallback
|
||||
}
|
||||
|
||||
const char* renderBoundsChannelLabel(RenderBoundsChannel channel) {
|
||||
switch (channel) {
|
||||
case RenderBoundsChannel::CustomTimeBounds:
|
||||
return "custom time bounds (RENDER_BOUNDSFLAG=0, RENDER_STARTPOS/RENDER_ENDPOS)";
|
||||
case RenderBoundsChannel::TimeSelection:
|
||||
return "time selection (RENDER_BOUNDSFLAG=2, GetSet_LoopTimeRange)";
|
||||
}
|
||||
return "unnamed bounds channel";
|
||||
}
|
||||
|
||||
TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs,
|
||||
RenderBoundsChannel channel) {
|
||||
TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs) {
|
||||
TailRenderSettings t;
|
||||
switch (mode) {
|
||||
case TailMode::None:
|
||||
@@ -59,7 +30,7 @@ TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs,
|
||||
// postprocessing bit clear. A fixed-threshold trim scales/limits/fades
|
||||
// nothing, so identical requests trim at the identical sample -> holds
|
||||
// the bit-identical-repeats invariant.
|
||||
t.tailFlag = tailFlagBitFor(channel);
|
||||
t.tailFlag = kTailFlagTimeSelection;
|
||||
t.tailMs = kMaxTailMs;
|
||||
t.normalize = kNormalizeTrimEnd;
|
||||
t.trimEnd = autoTrimEndRatio();
|
||||
@@ -67,7 +38,7 @@ TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs,
|
||||
|
||||
case TailMode::Manual:
|
||||
// Clamped to the cap regardless of source; negative floors to 0.
|
||||
t.tailFlag = tailFlagBitFor(channel);
|
||||
t.tailFlag = kTailFlagTimeSelection;
|
||||
t.tailMs = std::clamp(manualTailMs, 0.0, kMaxTailMs);
|
||||
t.normalize = kNormalizeDisableAll;
|
||||
t.trimEnd = 0.0;
|
||||
@@ -143,10 +114,6 @@ const char* renderSourceLabel(SourceMode mode) {
|
||||
return "unknown"; // unreachable for a valid enum; never claim a source
|
||||
}
|
||||
|
||||
bool sourceBypassesBoundsChannel(SourceMode mode) {
|
||||
return mode == SourceMode::SelectedItems || mode == SourceMode::RazorArea;
|
||||
}
|
||||
|
||||
SourceMode sourceModeForScope(CaptureScope scope, bool itemExtentIsWindow) {
|
||||
switch (scope) {
|
||||
case CaptureScope::Item:
|
||||
|
||||
@@ -27,51 +27,34 @@ inline constexpr int kRenderRazorEdits = 4096; // &4096 render razor e
|
||||
// render wet; the scope decides which FX remain enabled.
|
||||
inline constexpr int kRenderSingleFile = (4 << 16); // items/razor -> one file
|
||||
|
||||
// --- Render bounds channel ----------------------------------------------------
|
||||
// --- Render bounds mode -------------------------------------------------------
|
||||
//
|
||||
// The RENDER_BOUNDSFLAG mode a capture hands its window over on (values verbatim,
|
||||
// header ~3042: 0 = custom time bounds, 2 = time selection). RENDER_STARTPOS /
|
||||
// RENDER_ENDPOS apply to mode 0 ONLY (header ~3045-3046), so the TimeSelection
|
||||
// channel carries the window in the project's own time selection instead — a
|
||||
// different store. That difference is the whole reason two channels exist: REAPER
|
||||
// resolved a custom-bounds window on a whole-millisecond grid, floored the end, wrote
|
||||
// the floored value back over RENDER_ENDPOS, and rendered exactly the floored frame
|
||||
// count — twice, to the frame (docs/TODO.md "An offline capture can be refused..."
|
||||
// records the observations). The same read-back at store time and immediately before
|
||||
// the render was silent, so the field itself holds full double precision and the floor
|
||||
// happens at render time. Whether that floor sits in the custom-bounds channel or
|
||||
// downstream in the render engine (where no bounds mode escapes it) cannot be settled
|
||||
// from the SDK header, only in a DAW. The offline backend therefore names the channel
|
||||
// it used and what the landed file measured (render_window::describeBoundsExperiment)
|
||||
// so one smoke run answers it. This is the one narrative home for why two channels
|
||||
// exist; other sites point here rather than retelling it.
|
||||
enum class RenderBoundsChannel {
|
||||
CustomTimeBounds,
|
||||
TimeSelection,
|
||||
};
|
||||
|
||||
// The RENDER_BOUNDSFLAG value for a channel.
|
||||
int renderBoundsFlagFor(RenderBoundsChannel channel);
|
||||
|
||||
// The channel in words, for the console verdict.
|
||||
const char* renderBoundsChannelLabel(RenderBoundsChannel channel);
|
||||
// A capture hands its window over on RENDER_BOUNDSFLAG=2 — the project's own TIME
|
||||
// SELECTION (value verbatim, header ~3042), written through GetSet_LoopTimeRange.
|
||||
//
|
||||
// Custom time bounds (RENDER_BOUNDSFLAG=0, RENDER_STARTPOS/RENDER_ENDPOS, header
|
||||
// ~3045-3046) must NOT be reintroduced: REAPER resolved a custom-bounds window on a
|
||||
// whole-millisecond grid AT RENDER TIME, floored the end, wrote the floored value back
|
||||
// over RENDER_ENDPOS, and rendered exactly the floored frame count — twice, to the
|
||||
// frame. Re-rendering on this mode came back exact on both edges, including a start
|
||||
// carrying a sub-millisecond remainder, which is what locates the floor in the
|
||||
// custom-bounds field rather than downstream in the render engine. This is the one
|
||||
// narrative home for that; other sites point here.
|
||||
inline constexpr int kRenderBoundsTimeSelection = 2;
|
||||
|
||||
// --- Tail: RENDER_NORMALIZE / RENDER_TRIMEND bits + named constants ----------
|
||||
//
|
||||
// RENDER_TAILFLAG's bits are keyed PER BOUNDS MODE (header ~3047), so the bit a
|
||||
// tail mode has to set follows the bounds channel the window went over — a tail
|
||||
// set under the other channel's bit renders no tail at all. RENDER_NORMALIZE
|
||||
// (verbatim, header ~3051): &32768 = trim ending silence (Auto path);
|
||||
// &(4<<16) = disable all render postprocessing (None/Manual path).
|
||||
// RENDER_NORMALIZE (verbatim, header ~3051): &32768 = trim ending silence (Auto
|
||||
// path); &(4<<16) = disable all render postprocessing (None/Manual path).
|
||||
inline constexpr int kNormalizeTrimEnd = 32768; // &32768 trim ending silence
|
||||
inline constexpr int kNormalizeDisableAll = (4 << 16); // &(4<<16) = 262144, disable all
|
||||
|
||||
inline constexpr int kTailFlagNone = 0;
|
||||
inline constexpr int kTailFlagCustomBounds = 1; // &1, header ~3047
|
||||
inline constexpr int kTailFlagTimeSelection = 4; // &4, header ~3047
|
||||
inline constexpr int kTailFlagNone = 0;
|
||||
|
||||
// The RENDER_TAILFLAG bit that applies to a channel's bounds mode.
|
||||
int tailFlagBitFor(RenderBoundsChannel channel);
|
||||
// RENDER_TAILFLAG's bits are keyed PER BOUNDS MODE (header ~3047): &4 is the
|
||||
// time-selection mode's bit, the pair of kRenderBoundsTimeSelection above. A tail set
|
||||
// under a different mode's bit renders no tail at all, so these two move together.
|
||||
inline constexpr int kTailFlagTimeSelection = 4;
|
||||
|
||||
// Auto-trim trailing-silence threshold; single source of truth (RENDER_TRIMEND
|
||||
// ratio derives from this dB, never the reverse). Daniel-set.
|
||||
@@ -99,18 +82,15 @@ enum class TailMode {
|
||||
// normalize bit is set (Auto). The backend reads these straight onto
|
||||
// GetSetProjectInfo.
|
||||
struct TailRenderSettings {
|
||||
int tailFlag = kTailFlagNone; // RENDER_TAILFLAG (0 or the channel's bit)
|
||||
int tailFlag = kTailFlagNone; // RENDER_TAILFLAG (0 or the bounds mode's bit)
|
||||
double tailMs = 0.0; // RENDER_TAILMS
|
||||
int normalize = kNormalizeDisableAll; // RENDER_NORMALIZE
|
||||
double trimEnd = 0.0; // RENDER_TRIMEND (only used when trim bit set)
|
||||
};
|
||||
|
||||
// Maps a tail mode (+ requested manual tail ms, used only for Manual) to its
|
||||
// RENDER_* values. Manual is clamped to kMaxTailMs regardless of source. `channel`
|
||||
// is a parameter rather than a caller-side OR so a bounds-channel change cannot
|
||||
// leave Auto/Manual setting a tail bit the render no longer reads.
|
||||
TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs,
|
||||
RenderBoundsChannel channel);
|
||||
// RENDER_* values. Manual is clamped to kMaxTailMs regardless of source.
|
||||
TailRenderSettings tailRenderSettingsFor(TailMode mode, double manualTailMs);
|
||||
|
||||
// The realtime record-window end (project seconds): realtime does NOT drive
|
||||
// RENDER_*, it records a generous window and trims later, so this is where the
|
||||
@@ -137,26 +117,13 @@ RenderSettingsChoice renderSettingsFor(SourceMode mode, double wetDry);
|
||||
// bounds refusal: the two ways a render can miss its window — a source that
|
||||
// derives its own bounds (selected items, razor edits) versus a time-bounded
|
||||
// render that came up short — are indistinguishable from a frame count alone,
|
||||
// and naming the source is what tells them apart in a bug report. Quoted verbatim
|
||||
// in docs/VERIFICATION.md, which asks for this exact line back.
|
||||
// and naming the source is what tells them apart in a bug report.
|
||||
//
|
||||
// MasterMix and TimeSelection deliberately answer the SAME words: they map to the
|
||||
// same RENDER_SETTINGS value and every capture renders custom-time-bounded, so
|
||||
// naming them apart would assert a render distinction that does not exist.
|
||||
// same RENDER_SETTINGS value and render identically, so naming them apart would
|
||||
// assert a render distinction that does not exist.
|
||||
const char* renderSourceLabel(SourceMode mode);
|
||||
|
||||
// True for a render source INFERRED (not SDK-confirmed) to derive its bounds from
|
||||
// content rather than RENDER_BOUNDSFLAG: SelectedItems (&32), per the observed-defect
|
||||
// inference in src/core/capture/CLAUDE.md §Gotchas, and RazorArea (&4096) by analogy to
|
||||
// it — the SDK header (~3042) actually separates bounds (RENDER_BOUNDSFLAG, its own
|
||||
// value 4 = selected media items) from source (&32), which leans the other way.
|
||||
// RazorArea stays in the set on that inference even though no offline capture path
|
||||
// assigns it today — razor is a RANGE source (capture_batch.cpp's razor units render
|
||||
// through track scope), not a render source of its own. A capture on either never
|
||||
// consults RenderBoundsChannel, so describeBoundsExperiment's verdict must not be read
|
||||
// as evidence about the channel — the caller names the source instead.
|
||||
bool sourceBypassesBoundsChannel(SourceMode mode);
|
||||
|
||||
// --- Capture scope: the FX-scope invariant ------------------------------------
|
||||
//
|
||||
// See src/core/capture/CLAUDE.md for the scope contract. There is NO master
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "core/capture/render_window.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
namespace reasampler::capture {
|
||||
|
||||
@@ -23,14 +22,6 @@ double floorToMilliseconds(double seconds) {
|
||||
return std::floor(ms) / 1000.0;
|
||||
}
|
||||
|
||||
// Full round-trip precision: a drift report whose two numbers print identically
|
||||
// would be evidence of nothing.
|
||||
std::string exactly(double seconds) {
|
||||
char buf[32];
|
||||
std::snprintf(buf, sizeof(buf), "%.17g", seconds);
|
||||
return buf;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
long long frameCountFor(double startSeconds, double endSeconds, int sampleRate) {
|
||||
@@ -67,125 +58,4 @@ long long msFlooredEndFrameCount(double startSeconds, double endSeconds,
|
||||
return frameCountFor(startSeconds, floorToMilliseconds(endSeconds), sampleRate);
|
||||
}
|
||||
|
||||
std::string describeBoundsExperiment(const char* channelLabel,
|
||||
double reqStart, double reqEnd,
|
||||
long long actualFrames, int sampleRate,
|
||||
const char* bypassingSourceLabel) {
|
||||
std::string s = "bounds channel: ";
|
||||
s += (channelLabel && channelLabel[0]) ? channelLabel : "unnamed bounds channel";
|
||||
s += ". ";
|
||||
|
||||
if (bypassingSourceLabel && bypassingSourceLabel[0]) {
|
||||
s += "NOT JUDGED -- this capture's render source is " +
|
||||
std::string(bypassingSourceLabel) +
|
||||
", INFERRED (not SDK-confirmed) to derive its bounds from content rather"
|
||||
" than consult this channel; this capture is not evidence either way about it.";
|
||||
return s;
|
||||
}
|
||||
|
||||
if (sampleRate <= 0) {
|
||||
s += "NOT JUDGED -- this capture's frames were never counted against the "
|
||||
"window, so this capture is not evidence either way about the channel.";
|
||||
return s;
|
||||
}
|
||||
|
||||
const long long expected = frameCountFor(reqStart, reqEnd, sampleRate);
|
||||
const long long delta = actualFrames - expected;
|
||||
|
||||
// A window under a frame at this rate has nothing to compare: a 0-frame render
|
||||
// against a 0-frame window is a coincidence of degenerate inputs, not a match.
|
||||
if (expected == 0 && actualFrames == 0) {
|
||||
s += "NOT JUDGED -- the requested window rounds to 0 frames at this rate, so a "
|
||||
"0-frame render is not evidence either way about the channel.";
|
||||
return s;
|
||||
}
|
||||
|
||||
// Within the gate's own edge-convention slack (render_window.h): its normal
|
||||
// tolerance, not evidence the millisecond floor was escaped or hit.
|
||||
const bool withinTolerance =
|
||||
delta != 0 && renderHonoredBounds(expected, actualFrames);
|
||||
s += (delta == 0) ? "EXACT" : (delta < 0 ? "SHORT" : "LONG");
|
||||
if (withinTolerance) s += " (WITHIN TOLERANCE)";
|
||||
s += " -- the landed render holds " + std::to_string(actualFrames) +
|
||||
" frames against the " + std::to_string(expected) +
|
||||
" the window asks for at " + std::to_string(sampleRate) + " Hz.";
|
||||
|
||||
// The shape both live short renders matched to the frame. A match says this channel
|
||||
// produced a floored window; it does not locate where inside REAPER the floor is. A
|
||||
// floor only removes frames, so this can only ever match a SHORT, never a LONG.
|
||||
if (delta < 0) {
|
||||
const long long msFloored =
|
||||
msFlooredEndFrameCount(reqStart, reqEnd, sampleRate);
|
||||
if (msFloored > 0 && actualFrames == msFloored)
|
||||
s += " That is exactly the count this window holds with its end floored to"
|
||||
" the millisecond -- the shape REAPER's render was measured producing.";
|
||||
}
|
||||
|
||||
s += isOnMillisecondGrid(reqStart)
|
||||
? " The START edge is UNTESTED here: " + exactly(reqStart) +
|
||||
"s is already on the millisecond grid, which floor, ceil and round all leave"
|
||||
" alone. Re-run over a range starting off the grid to test it."
|
||||
: " The START edge IS tested here: " + exactly(reqStart) +
|
||||
"s carries a sub-millisecond remainder.";
|
||||
|
||||
// EXACT is proof only when no millisecond-floored model of this window could have
|
||||
// produced the same count. Grid membership on an edge is a PROXY for that collision,
|
||||
// not the test itself: sub-millisecond remainders on the two edges can cancel under
|
||||
// a full floor even when neither edge is on the grid (a dragged, fixed-length time
|
||||
// selection reproduces this), and a remainder under half a frame collides with a
|
||||
// floored edge without ever registering as off-grid. Enumerate every floored model
|
||||
// directly rather than inferring from grid membership.
|
||||
if (delta == 0) {
|
||||
const double flooredStart = floorToMilliseconds(reqStart);
|
||||
const double flooredEnd = floorToMilliseconds(reqEnd);
|
||||
const bool startAlone =
|
||||
actualFrames == frameCountFor(flooredStart, reqEnd, sampleRate);
|
||||
const bool endAlone =
|
||||
actualFrames == frameCountFor(reqStart, flooredEnd, sampleRate);
|
||||
const bool bothTogether =
|
||||
actualFrames == frameCountFor(flooredStart, flooredEnd, sampleRate);
|
||||
|
||||
if (startAlone || endAlone || bothTogether) {
|
||||
std::string models;
|
||||
auto addModel = [&](const char* label) {
|
||||
if (!models.empty()) models += ", or ";
|
||||
models += label;
|
||||
};
|
||||
if (startAlone) addModel("floors the START edge alone");
|
||||
if (endAlone) addModel("floors the END edge alone");
|
||||
if (bothTogether) addModel("floors START and END together");
|
||||
|
||||
s += " EXACT here is not proof: a render that " + models +
|
||||
" to the millisecond would print this identical count -- re-run over a"
|
||||
" window where a floored edge would show a different count before"
|
||||
" reading EXACT as the fix.";
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string describeBoundsDrift(double reqStart, double reqEnd,
|
||||
double storedStart, double storedEnd,
|
||||
int sampleRate) {
|
||||
// Bit equality, deliberately: the caller wrote these exact doubles and read them
|
||||
// straight back, so anything but the same bits is a value REAPER changed.
|
||||
if (storedStart == reqStart && storedEnd == reqEnd) return {};
|
||||
|
||||
// Says only that the two differ, not why -- a legitimate clamp (negative start,
|
||||
// end past project end) reads back differently for the same reason a precision
|
||||
// defect would, and this sentence cannot tell those apart.
|
||||
std::string s = "REAPER read back different render bounds than it was handed -- "
|
||||
"asked for [" +
|
||||
exactly(reqStart) + "s, " + exactly(reqEnd) + "s), read back [" +
|
||||
exactly(storedStart) + "s, " + exactly(storedEnd) + "s).";
|
||||
if (sampleRate > 0) {
|
||||
s += " The stored window is " +
|
||||
std::to_string(frameCountFor(storedStart, storedEnd, sampleRate)) +
|
||||
" frames against the " +
|
||||
std::to_string(frameCountFor(reqStart, reqEnd, sampleRate)) +
|
||||
" the request asks for, at " + std::to_string(sampleRate) + " Hz.";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace reasampler::capture
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
#pragma once
|
||||
// render_window — pure frame arithmetic for a capture's requested window: the
|
||||
// frame count a project-time range occupies, whether a render whose bounds come
|
||||
// from the selected items' own extent already prints that window, and the
|
||||
// diagnostics that bound a short render: whether the stored bounds round-tripped,
|
||||
// whether the shortfall matches a millisecond-floor coincidence, and the verdict on
|
||||
// which bounds channel a capture used and what it produced.
|
||||
// render_window — pure frame arithmetic for a capture's requested window: the frame
|
||||
// count a project-time range occupies, whether a render whose bounds come from the
|
||||
// selected items' own extent already prints that window, and the one diagnostic a
|
||||
// refused render quotes — whether its shortfall matches a millisecond-floor coincidence.
|
||||
// NO REAPER types; unit-tested by tests/test_render_window.cpp.
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace reasampler::capture {
|
||||
|
||||
// Frames the [startSeconds, endSeconds) window occupies at `sampleRate`. Both
|
||||
@@ -30,8 +26,7 @@ long long frameCountFor(double startSeconds, double endSeconds, int sampleRate);
|
||||
// end edge by DIFFERENT conventions can legitimately sit TWO frames from this answer
|
||||
// (tests/test_render_window.cpp pins both facts). Which model REAPER uses is
|
||||
// unverified, so a refusal one or two frames wide may be this gate's fault rather than
|
||||
// the render's — the open DAW question in docs/VERIFICATION.md §Capture range and
|
||||
// bounds. Widening past one frame retires the exact-bounds invariant rather than
|
||||
// the render's. Widening past one frame retires the exact-bounds invariant rather than
|
||||
// relaxing it, and is not a fix to reach for before that question is answered.
|
||||
bool renderHonoredBounds(long long expectedFrames, long long actualFrames);
|
||||
|
||||
@@ -50,9 +45,11 @@ bool itemExtentPrintsWindow(double reqStart, double reqEnd,
|
||||
// --- Diagnostics: where a short render lost its frames ------------------------
|
||||
|
||||
// The frames this window would hold if its END were resolved on a whole-millisecond
|
||||
// grid, floored, instead of exactly. That is what REAPER's offline render does: two
|
||||
// live short renders (48 kHz, TailMode::None) printed this count to the frame, and the
|
||||
// after-render read-back showed REAPER's own resolved end floored to the same value.
|
||||
// grid, floored, instead of exactly. That is what REAPER's offline render did on the
|
||||
// retired custom-time-bounds mode (render_settings.h's kRenderBoundsTimeSelection states
|
||||
// the whole observation): two live short renders (48 kHz, TailMode::None) printed this
|
||||
// count to the frame. Kept as the refusal's shape check — a refused render matching it
|
||||
// says the floor is back, on a mode that was measured escaping it.
|
||||
//
|
||||
// Still a DESCRIPTION, never a request: nothing renders from this number and no capture
|
||||
// path asks for it — a refusal quotes it to say the shortfall has the known shape, which
|
||||
@@ -64,70 +61,15 @@ bool itemExtentPrintsWindow(double reqStart, double reqEnd,
|
||||
//
|
||||
// The tolerance is ours, not REAPER's: on a `1.007`-class grid point, a REAPER floor
|
||||
// that does NOT carry the same epsilon would miss this shape entirely, and a real
|
||||
// floored render would then read as an unmatched SHORT rather than the known one —
|
||||
// silence here is not proof the floor didn't happen (docs/TODO.md records why this
|
||||
// floored render would then read as an unmatched short render rather than the known one
|
||||
// — silence here is not proof the floor didn't happen (docs/TODO.md records why this
|
||||
// premise needs a DAW measurement before anything is built on it).
|
||||
long long msFlooredEndFrameCount(double startSeconds, double endSeconds,
|
||||
int sampleRate);
|
||||
|
||||
// True when `seconds` sits on a whole-millisecond boundary, under the same nanosecond
|
||||
// tolerance msFlooredEndFrameCount uses and for the same reason (stated there).
|
||||
//
|
||||
// Load-bearing for reading a bounds observation: an on-grid edge is left alone by
|
||||
// floor, ceil and round alike, so a window whose START is on the grid can say nothing
|
||||
// about whether REAPER resolves the start edge the way it resolves the end.
|
||||
// True when `seconds` sits on a whole-millisecond boundary, under the nanosecond
|
||||
// tolerance msFlooredEndFrameCount depends on and for the reason stated there. Public so
|
||||
// that premise is testable directly rather than only through the count it feeds.
|
||||
bool isOnMillisecondGrid(double seconds);
|
||||
|
||||
// The one-line verdict on what a capture's bounds channel did with its window: which
|
||||
// channel carried it (or, when the render source defines the window itself, which
|
||||
// source bypassed the channel entirely), the frames the landed file holds against the
|
||||
// frames the window asks for, and whether this run could test the START and END edges
|
||||
// at all. Always non-empty — a capture that answered nothing has to say so, or its
|
||||
// silence reads as a pass.
|
||||
//
|
||||
// EXACT never stands alone as proof: the observed count is checked against every
|
||||
// millisecond-floored model of the same window (start floored alone, end floored alone,
|
||||
// both together), and any model that reproduces it is named in the sentence. Grid
|
||||
// membership on an edge is a PROXY for that collision, not the test itself — remainders
|
||||
// on the two edges can cancel under a full floor even when NEITHER edge sits on the
|
||||
// grid, and a remainder under half a frame collides with a floored edge without ever
|
||||
// registering as off-grid at all. Checking the models directly is what a grid test on
|
||||
// either edge alone cannot do.
|
||||
//
|
||||
// A non-zero delta that still falls inside the gate's own tolerance (renderHonoredBounds)
|
||||
// is tagged "(WITHIN TOLERANCE)" — that is the gate's ordinary edge-convention slack, not
|
||||
// evidence of the millisecond floor. The floor's signature is ONLY the "floored to the
|
||||
// millisecond" sentence (a delta matching msFlooredEndFrameCount exactly); a bare
|
||||
// SHORT with no such sentence means the shortfall's cause is unestablished, and LONG can
|
||||
// never be the floor's signature — a floor only removes frames, never adds them. A
|
||||
// window whose start and end sit in the same millisecond bucket makes msFlooredEndFrameCount
|
||||
// equal the exact count, so a real one-frame floor there reads as a bare SHORT (WITHIN
|
||||
// TOLERANCE) with no floor sentence at all — that combination is not evidence the floor
|
||||
// didn't happen, just a case this diagnostic can't see into.
|
||||
//
|
||||
// `sampleRate <= 0` means the landed file was never measured: a tail mode adds frames by
|
||||
// design and is not judged, an empty render has none, and a render whose layout failed to
|
||||
// parse or declared no sample rate is refused before it can be judged either — the
|
||||
// sentence then says the run answered nothing rather than inventing a comparison. A
|
||||
// window that rounds to 0 frames at this rate reads NOT JUDGED the same way: a 0-frame
|
||||
// render against a 0-frame window is not a comparison either.
|
||||
//
|
||||
// `bypassingSourceLabel`, when non-null and non-empty, means the render source itself
|
||||
// defined the window (render_settings::sourceBypassesBoundsChannel) — `channelLabel` was
|
||||
// never consulted, so the verdict names the source instead and reads NOT JUDGED
|
||||
// regardless of how the frame counts compare.
|
||||
std::string describeBoundsExperiment(const char* channelLabel,
|
||||
double reqStart, double reqEnd,
|
||||
long long actualFrames, int sampleRate,
|
||||
const char* bypassingSourceLabel = nullptr);
|
||||
|
||||
// The sentence a capture prints when the render bounds it handed REAPER did not read
|
||||
// back unchanged — the requested window, what came back, and both frame counts at
|
||||
// `sampleRate` (omitted when the rate is unknown). EMPTY when both edges read back
|
||||
// bit-identical, which is the only answer proving the request crossed into REAPER
|
||||
// intact; a caller prints this only when it is non-empty.
|
||||
std::string describeBoundsDrift(double reqStart, double reqEnd,
|
||||
double storedStart, double storedEnd,
|
||||
int sampleRate);
|
||||
|
||||
} // namespace reasampler::capture
|
||||
|
||||
Reference in New Issue
Block a user