Prove the render bounds at the boundary they cross, and name a short render whose count is exactly a millisecond-floored window
No truncation exists on our side of that boundary, so the read-back is the only evidence available for whether REAPER kept the window — and it fires on every tail mode, where only None was ever judged.
This commit is contained in:
@@ -52,7 +52,7 @@ Detail specific to these pure modules:
|
||||
- `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 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.
|
||||
- `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 two 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) and `describeBoundsDrift` (the sentence the offline backend prints when `RENDER_STARTPOS`/`RENDER_ENDPOS` do not read back as they were written).
|
||||
- `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.
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "core/capture/render_window.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
namespace reasampler::capture {
|
||||
|
||||
@@ -15,6 +16,22 @@ long long frameIndexAt(double seconds, int sampleRate) {
|
||||
return std::llround(seconds * static_cast<double>(sampleRate));
|
||||
}
|
||||
|
||||
// See the header for why whole milliseconds get a tolerance and why it is this small.
|
||||
double floorToMilliseconds(double seconds) {
|
||||
const double ms = seconds * 1000.0;
|
||||
const double nearest = std::nearbyint(ms);
|
||||
if (std::fabs(ms - nearest) < 1e-6) return nearest / 1000.0;
|
||||
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) {
|
||||
@@ -41,4 +58,29 @@ bool itemExtentPrintsWindow(double reqStart, double reqEnd,
|
||||
&& frameIndexAt(reqEnd, sampleRate) == frameIndexAt(itemEnd, sampleRate);
|
||||
}
|
||||
|
||||
long long msFlooredEndFrameCount(double startSeconds, double endSeconds,
|
||||
int sampleRate) {
|
||||
return frameCountFor(startSeconds, floorToMilliseconds(endSeconds), sampleRate);
|
||||
}
|
||||
|
||||
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 {};
|
||||
|
||||
std::string s = "REAPER did not keep the render bounds 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,9 +1,12 @@
|
||||
#pragma once
|
||||
// render_window — pure frame arithmetic for a capture's requested window: the
|
||||
// frame count a project-time range occupies, and whether a render whose bounds
|
||||
// come from the selected items' own extent already prints that window.
|
||||
// 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 two
|
||||
// diagnostics that say where a short render lost its frames.
|
||||
// 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
|
||||
@@ -42,4 +45,28 @@ bool itemExtentPrintsWindow(double reqStart, double reqEnd,
|
||||
double itemStart, double itemEnd,
|
||||
int sampleRate);
|
||||
|
||||
// --- 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. Two live short renders (48 kHz, TailMode::None)
|
||||
// matched this count to the frame, which is the entire reason it exists.
|
||||
//
|
||||
// A COINCIDENCE OF COUNTS, not a claim about how anything resolved the end: nothing
|
||||
// renders from this number and no capture path asks for it. Whole-millisecond values
|
||||
// are recognized within a nanosecond, because a decimal millisecond is not always one
|
||||
// in binary (0.029 * 1000 lands just below 29) and a bare floor would drop a
|
||||
// millisecond from a window already on the grid. A nanosecond is far under one frame
|
||||
// at any rate we render, so a real sub-millisecond remainder still floors.
|
||||
long long msFlooredEndFrameCount(double startSeconds, double endSeconds,
|
||||
int sampleRate);
|
||||
|
||||
// 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