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.
|
- `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`.
|
- `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_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.
|
- `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.
|
- `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 "core/capture/render_window.h"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
namespace reasampler::capture {
|
namespace reasampler::capture {
|
||||||
|
|
||||||
@@ -15,6 +16,22 @@ long long frameIndexAt(double seconds, int sampleRate) {
|
|||||||
return std::llround(seconds * static_cast<double>(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
|
} // namespace
|
||||||
|
|
||||||
long long frameCountFor(double startSeconds, double endSeconds, int sampleRate) {
|
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);
|
&& 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
|
} // namespace reasampler::capture
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// render_window — pure frame arithmetic for a capture's requested window: the
|
// 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
|
// frame count a project-time range occupies, whether a render whose bounds come
|
||||||
// come from the selected items' own extent already prints that window.
|
// 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.
|
// NO REAPER types; unit-tested by tests/test_render_window.cpp.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace reasampler::capture {
|
namespace reasampler::capture {
|
||||||
|
|
||||||
// Frames the [startSeconds, endSeconds) window occupies at `sampleRate`. Both
|
// Frames the [startSeconds, endSeconds) window occupies at `sampleRate`. Both
|
||||||
@@ -42,4 +45,28 @@ bool itemExtentPrintsWindow(double reqStart, double reqEnd,
|
|||||||
double itemStart, double itemEnd,
|
double itemStart, double itemEnd,
|
||||||
int sampleRate);
|
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
|
} // namespace reasampler::capture
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
#include "core/capture/wav_codec.h" // hashWavContent / collapseToMono — the one WAV/RIFF owner
|
#include "core/capture/wav_codec.h" // hashWavContent / collapseToMono — the one WAV/RIFF owner
|
||||||
#include "core/util/file_bytes.h"
|
#include "core/util/file_bytes.h"
|
||||||
#include "core/capture/render_settings.h"
|
#include "core/capture/render_settings.h"
|
||||||
|
#include "core/capture/render_window.h" // describeBoundsDrift — the read-back's verdict
|
||||||
#include "shell/capture/render_bounds_gate.h" // the exact-bounds verdict on the landed render
|
#include "shell/capture/render_bounds_gate.h" // the exact-bounds verdict on the landed render
|
||||||
|
|
||||||
#define REAPERAPI_MINIMAL
|
#define REAPERAPI_MINIMAL
|
||||||
@@ -424,6 +425,12 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
|||||||
GetSetProjectInfo(proj, "RENDER_STARTPOS", request.startSeconds, true);
|
GetSetProjectInfo(proj, "RENDER_STARTPOS", request.startSeconds, true);
|
||||||
GetSetProjectInfo(proj, "RENDER_ENDPOS", request.endSeconds, true);
|
GetSetProjectInfo(proj, "RENDER_ENDPOS", request.endSeconds, true);
|
||||||
|
|
||||||
|
// The requested window crosses out of this process HERE and nowhere else, so the
|
||||||
|
// read-back is the only evidence available on this side of that boundary for
|
||||||
|
// whether REAPER kept it. Reported below, once the project rate is known.
|
||||||
|
const double storedStart = GetSetProjectInfo(proj, "RENDER_STARTPOS", 0.0, false);
|
||||||
|
const double storedEnd = GetSetProjectInfo(proj, "RENDER_ENDPOS", 0.0, false);
|
||||||
|
|
||||||
// TAILFLAG/TAILMS/NORMALIZE/TRIMEND from the pure mapping: None -> exact
|
// TAILFLAG/TAILMS/NORMALIZE/TRIMEND from the pure mapping: None -> exact
|
||||||
// bounds + disable-all normalize; Auto -> 8s tail + surgical trim-end
|
// bounds + disable-all normalize; Auto -> 8s tail + surgical trim-end
|
||||||
// normalize + -72 dB TRIMEND; Manual -> clamped fixed tail + disable-all, no
|
// normalize + -72 dB TRIMEND; Manual -> clamped fixed tail + disable-all, no
|
||||||
@@ -453,6 +460,18 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
|||||||
GetSetProjectInfo(proj, "RENDER_SRATE",
|
GetSetProjectInfo(proj, "RENDER_SRATE",
|
||||||
static_cast<double>(effectiveSampleRate), true);
|
static_cast<double>(effectiveSampleRate), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Silent unless a bound came back changed. Fires on EVERY tail mode on purpose:
|
||||||
|
// only None is judged against its window after the render, so this is the sole
|
||||||
|
// signal an Auto/Manual capture was shortened before it ever started.
|
||||||
|
{
|
||||||
|
const std::string drift =
|
||||||
|
describeBoundsDrift(request.startSeconds, request.endSeconds,
|
||||||
|
storedStart, storedEnd, effectiveSampleRate);
|
||||||
|
if (!drift.empty())
|
||||||
|
ShowConsoleMsg(("ReaSampler capture: " + drift + "\n").c_str());
|
||||||
|
}
|
||||||
|
|
||||||
GetSetProjectInfo(proj, "RENDER_CHANNELS",
|
GetSetProjectInfo(proj, "RENDER_CHANNELS",
|
||||||
static_cast<double>(request.channelCount), true);
|
static_cast<double>(request.channelCount), true);
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,17 @@ BoundsVerdict checkRenderedBounds(const std::string& renderedPath,
|
|||||||
const long long actualFrames = static_cast<long long>(layout.frameCount());
|
const long long actualFrames = static_cast<long long>(layout.frameCount());
|
||||||
if (renderHonoredBounds(expectedFrames, actualFrames)) return v;
|
if (renderHonoredBounds(expectedFrames, actualFrames)) return v;
|
||||||
|
|
||||||
|
// Says whether this shortfall has the one shape two live short renders already
|
||||||
|
// matched to the frame, so every refusal from here on adds to (or breaks) that
|
||||||
|
// evidence instead of needing the arithmetic done by hand. A count coincidence
|
||||||
|
// only — it does not establish how the render resolved anything.
|
||||||
|
const std::string msNote =
|
||||||
|
actualFrames == msFlooredEndFrameCount(request.startSeconds,
|
||||||
|
request.endSeconds, rate)
|
||||||
|
? " Those are exactly the frames this window holds with its end floored to"
|
||||||
|
" the millisecond -- a match on the count, not a measured cause."
|
||||||
|
: std::string();
|
||||||
|
|
||||||
v.refused = true;
|
v.refused = true;
|
||||||
v.message = "Render produced " + std::to_string(actualFrames) +
|
v.message = "Render produced " + std::to_string(actualFrames) +
|
||||||
" frames but the requested range is " + std::to_string(expectedFrames) +
|
" frames but the requested range is " + std::to_string(expectedFrames) +
|
||||||
@@ -108,7 +119,7 @@ BoundsVerdict checkRenderedBounds(const std::string& renderedPath,
|
|||||||
std::to_string(request.endSeconds) + "s) -> frame indices [" +
|
std::to_string(request.endSeconds) + "s) -> frame indices [" +
|
||||||
std::to_string(std::llround(request.startSeconds * rate)) + ", " +
|
std::to_string(std::llround(request.startSeconds * rate)) + ", " +
|
||||||
std::to_string(std::llround(request.endSeconds * rate)) + ")." +
|
std::to_string(std::llround(request.endSeconds * rate)) + ")." +
|
||||||
retainRefusedRender(renderedPath, projectDir);
|
msNote + retainRefusedRender(renderedPath, projectDir);
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
// Standalone tests for reasampler::render_window — no REAPER, no framework.
|
// Standalone tests for reasampler::render_window — no REAPER, no framework.
|
||||||
// Covers the bounds-equality number (a window's exact frame count at the project
|
// Covers the bounds-equality number (a window's exact frame count at the project
|
||||||
// rate), the verdict the offline backend refuses a capture on, and the predicate
|
// rate), the verdict the offline backend refuses a capture on, the predicate
|
||||||
// that decides whether REAPER's selected-items render source can express a
|
// that decides whether REAPER's selected-items render source can express a
|
||||||
// requested window at all.
|
// requested window at all, and the two short-render diagnostics.
|
||||||
|
|
||||||
#include "../src/core/capture/render_window.h"
|
#include "../src/core/capture/render_window.h"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
using namespace reasampler::capture;
|
using namespace reasampler::capture;
|
||||||
|
|
||||||
@@ -15,6 +16,10 @@ static int g_fail = 0;
|
|||||||
#define CHECK(cond) do { if(!(cond)) { \
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
static bool contains(const std::string& haystack, const std::string& needle) {
|
||||||
|
return haystack.find(needle) != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
// --- frameCountFor: the bounds equality, stated as a number ------------------
|
// --- frameCountFor: the bounds equality, stated as a number ------------------
|
||||||
|
|
||||||
static void testFrameCountIsExactNotRounded() {
|
static void testFrameCountIsExactNotRounded() {
|
||||||
@@ -226,6 +231,131 @@ static void testMultiItemUnionExtent() {
|
|||||||
CHECK(!itemExtentPrintsWindow(1.0, 4.0, 1.0, 9.0, 48000));
|
CHECK(!itemExtentPrintsWindow(1.0, 4.0, 1.0, 9.0, 48000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- msFlooredEndFrameCount: the shape both live short renders had ------------
|
||||||
|
|
||||||
|
static void testMillisecondFlooredEndReproducesBothShortRenders() {
|
||||||
|
// Both DAW observations, as arithmetic. 48 kHz, TailMode::None, start at 0: the
|
||||||
|
// requested window's count, and the count its end floored to the millisecond
|
||||||
|
// holds — which is what each render actually printed.
|
||||||
|
CHECK(frameCountFor(0.0, 4.067797, 48000) == 195254);
|
||||||
|
CHECK(msFlooredEndFrameCount(0.0, 4.067797, 48000) == 195216);
|
||||||
|
CHECK(frameCountFor(0.0, 4.067797, 48000) -
|
||||||
|
msFlooredEndFrameCount(0.0, 4.067797, 48000) == 38);
|
||||||
|
|
||||||
|
CHECK(frameCountFor(0.0, 1.655172, 48000) == 79448);
|
||||||
|
CHECK(msFlooredEndFrameCount(0.0, 1.655172, 48000) == 79440);
|
||||||
|
CHECK(frameCountFor(0.0, 1.655172, 48000) -
|
||||||
|
msFlooredEndFrameCount(0.0, 1.655172, 48000) == 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testTheSixDecimalDisplayDidNotCreateTheEffect() {
|
||||||
|
// Both reported ends were printed to six decimals by the refusal. Each is one 4/4
|
||||||
|
// bar — at 59 BPM and at 145 BPM — so the full-precision doubles behind them are
|
||||||
|
// 240/59 and 240/145. Same counts either way: the display rounding is not what
|
||||||
|
// produces the shortfall.
|
||||||
|
CHECK(frameCountFor(0.0, 240.0 / 59.0, 48000) == 195254);
|
||||||
|
CHECK(msFlooredEndFrameCount(0.0, 240.0 / 59.0, 48000) == 195216);
|
||||||
|
CHECK(frameCountFor(0.0, 240.0 / 145.0, 48000) == 79448);
|
||||||
|
CHECK(msFlooredEndFrameCount(0.0, 240.0 / 145.0, 48000) == 79440);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testWindowAlreadyOnTheMillisecondGridLosesNothing() {
|
||||||
|
// The "sometimes it works" case: a bar at 120 BPM is exactly 2 s.
|
||||||
|
CHECK(msFlooredEndFrameCount(0.0, 2.0, 48000) == frameCountFor(0.0, 2.0, 48000));
|
||||||
|
|
||||||
|
// The binary-representation trap a bare floor would fall into. The premise, not an
|
||||||
|
// outcome: 1.007 s is a whole millisecond that really does land BELOW 1007 ms in
|
||||||
|
// double, so flooring it without a tolerance drops a millisecond from a window
|
||||||
|
// already on the grid.
|
||||||
|
CHECK(1.007 * 1000.0 < 1007.0);
|
||||||
|
CHECK(frameCountFor(0.0, 1.007, 48000) == 48336);
|
||||||
|
CHECK(msFlooredEndFrameCount(0.0, 1.007, 48000) == 48336);
|
||||||
|
// Same end reached from a non-zero start, so nothing here rests on the window
|
||||||
|
// beginning at 0.
|
||||||
|
CHECK(msFlooredEndFrameCount(0.5, 1.007, 48000) ==
|
||||||
|
frameCountFor(0.5, 1.007, 48000));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testOneFrameOfRemainderStillFloors() {
|
||||||
|
// The whole-millisecond tolerance must sit far below a frame, or it would swallow
|
||||||
|
// the very remainder this diagnostic exists to find. One frame at 48 kHz is 20.8 us
|
||||||
|
// — four orders of magnitude above the nanosecond tolerance.
|
||||||
|
const double oneFrame = 1.0 / 48000.0;
|
||||||
|
CHECK(frameCountFor(0.0, 1.0 + oneFrame, 48000) == 48001);
|
||||||
|
CHECK(msFlooredEndFrameCount(0.0, 1.0 + oneFrame, 48000) == 48000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testMillisecondFloorAt44100WhereAMillisecondIsNotWholeFrames() {
|
||||||
|
// 44.1 kHz: a millisecond is 44.1 frames, so a floored end cannot be described as
|
||||||
|
// dropping a whole number of frames — the count still resolves exactly.
|
||||||
|
CHECK(frameCountFor(0.0, 0.0105, 44100) == 463);
|
||||||
|
CHECK(msFlooredEndFrameCount(0.0, 0.0105, 44100) == 441);
|
||||||
|
// And a window that IS on the millisecond grid there is untouched, even though its
|
||||||
|
// edge is not on a frame boundary.
|
||||||
|
CHECK(frameCountFor(0.0, 0.010, 44100) == 441);
|
||||||
|
CHECK(msFlooredEndFrameCount(0.0, 0.010, 44100) == 441);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testASubMillisecondStartWouldNotHideItself() {
|
||||||
|
// Both observations started at 0.000000s, the one value that hides a start-side
|
||||||
|
// truncation. A window whose START carries a sub-millisecond remainder counts from
|
||||||
|
// that exact start...
|
||||||
|
const double start = 1.0001724, end = 2.0001724;
|
||||||
|
CHECK(frameCountFor(start, end, 48000) == 48000);
|
||||||
|
// ...so a start floored to the millisecond would print a DIFFERENT count — 8 frames
|
||||||
|
// more, the same remainder the second observation lost off its end. A start-side
|
||||||
|
// truncation is therefore visible to the same frame-count gate, not silent.
|
||||||
|
CHECK(frameCountFor(1.000, end, 48000) == 48008);
|
||||||
|
CHECK(!renderHonoredBounds(frameCountFor(start, end, 48000),
|
||||||
|
frameCountFor(1.000, end, 48000)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- describeBoundsDrift: the read-back's verdict ------------------------------
|
||||||
|
|
||||||
|
static void testBoundsThatReadBackUnchangedDescribeNothing() {
|
||||||
|
// The answer that proves the request crossed into REAPER intact — including for a
|
||||||
|
// window whose end is nowhere near a millisecond boundary.
|
||||||
|
CHECK(describeBoundsDrift(0.0, 4.067797, 0.0, 4.067797, 48000).empty());
|
||||||
|
CHECK(describeBoundsDrift(1.0001724, 2.0001724, 1.0001724, 2.0001724, 48000).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testADriftedEndNamesBothWindowsAndBothCounts() {
|
||||||
|
const std::string s =
|
||||||
|
describeBoundsDrift(0.0, 4.067797, 0.0, 4.067, 48000);
|
||||||
|
CHECK(!s.empty());
|
||||||
|
// Both counts as literals from the DAW observation, not re-derived from the same
|
||||||
|
// functions the sentence was built with.
|
||||||
|
CHECK(contains(s, "195254")); // what the request asks for
|
||||||
|
CHECK(contains(s, "195216")); // what the drifted window would hold
|
||||||
|
CHECK(contains(s, "48000 Hz"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testTheReportPrintsEnoughDigitsToShowTheDrift() {
|
||||||
|
// A report whose two numbers print identically is evidence of nothing. Two ends a
|
||||||
|
// single ULP apart — far under the sixth decimal a shorter rendering would stop at
|
||||||
|
// — must still read as two different numbers.
|
||||||
|
const double asked = 4.067797;
|
||||||
|
const double stored = std::nextafter(asked, 5.0);
|
||||||
|
const std::string s = describeBoundsDrift(0.0, asked, 0.0, stored, 48000);
|
||||||
|
CHECK(!s.empty());
|
||||||
|
CHECK(!contains(s, "4.067797s, read back [0s, 4.067797s)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testADriftedStartIsCaughtToo() {
|
||||||
|
// The edge both observations could not test.
|
||||||
|
const std::string s = describeBoundsDrift(1.0001724, 2.0, 1.000, 2.0, 48000);
|
||||||
|
CHECK(!s.empty());
|
||||||
|
CHECK(contains(s, "1.0001724"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testAnUnknownRateStillReportsTheDriftWithoutFrames() {
|
||||||
|
// A project that never pinned a rate reads 0. The drift is still worth saying; a
|
||||||
|
// frame count over an unknown rate is not.
|
||||||
|
const std::string s = describeBoundsDrift(0.0, 4.067797, 0.0, 4.067, 0);
|
||||||
|
CHECK(!s.empty());
|
||||||
|
CHECK(!contains(s, "frames"));
|
||||||
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
testFrameCountIsExactNotRounded();
|
testFrameCountIsExactNotRounded();
|
||||||
testFrameCountIsADifferenceOfIndicesNotADuration();
|
testFrameCountIsADifferenceOfIndicesNotADuration();
|
||||||
@@ -244,6 +374,17 @@ int main() {
|
|||||||
testSubFrameDriftStillPrintsTheSameFrames();
|
testSubFrameDriftStillPrintsTheSameFrames();
|
||||||
testUnknownRateFallsBackToExactEquality();
|
testUnknownRateFallsBackToExactEquality();
|
||||||
testMultiItemUnionExtent();
|
testMultiItemUnionExtent();
|
||||||
|
testMillisecondFlooredEndReproducesBothShortRenders();
|
||||||
|
testTheSixDecimalDisplayDidNotCreateTheEffect();
|
||||||
|
testWindowAlreadyOnTheMillisecondGridLosesNothing();
|
||||||
|
testOneFrameOfRemainderStillFloors();
|
||||||
|
testMillisecondFloorAt44100WhereAMillisecondIsNotWholeFrames();
|
||||||
|
testASubMillisecondStartWouldNotHideItself();
|
||||||
|
testBoundsThatReadBackUnchangedDescribeNothing();
|
||||||
|
testADriftedEndNamesBothWindowsAndBothCounts();
|
||||||
|
testTheReportPrintsEnoughDigitsToShowTheDrift();
|
||||||
|
testADriftedStartIsCaughtToo();
|
||||||
|
testAnUnknownRateStillReportsTheDriftWithoutFrames();
|
||||||
|
|
||||||
if (g_fail) { std::printf("%d check(s) FAILED\n", g_fail); return 1; }
|
if (g_fail) { std::printf("%d check(s) FAILED\n", g_fail); return 1; }
|
||||||
std::printf("render_window: all checks passed\n");
|
std::printf("render_window: all checks passed\n");
|
||||||
|
|||||||
Reference in New Issue
Block a user