capture: name the render source in the exact-bounds refusal, and put its one-frame tolerance under test
The tolerance is unchanged and now derived, not assumed: frameCountFor lands in {floor(L), ceil(L)}, so a non-frame-aligned window can never miss by more than a frame. Naming the source is what tells a self-bounding render from a short one.
This commit is contained in:
@@ -26,6 +26,8 @@ Checks for Θ, Ξ, and Ψ work that no unit test can close. Build **Release**, i
|
|||||||
- [ ] Same source: track scope × time selection, and track scope × razor — same exact window (`PLAN.md:2136`)
|
- [ ] Same source: track scope × time selection, and track scope × razor — same exact window (`PLAN.md:2136`)
|
||||||
- [ ] One razor-union case (two disjoint areas, one track) — lands the requested window, no `ReaSampler capture failed:` line (`PLAN.md:2137`)
|
- [ ] One razor-union case (two disjoint areas, one track) — lands the requested window, no `ReaSampler capture failed:` line (`PLAN.md:2137`)
|
||||||
- [ ] Capture an item whose extent already equals the window — still lands, unchanged (the byte-identity regression floor) (`docs/COMPLETED.md:829`)
|
- [ ] Capture an item whose extent already equals the window — still lands, unchanged (the byte-identity regression floor) (`docs/COMPLETED.md:829`)
|
||||||
|
- [ ] **Open blocker.** A live capture refused with a 38-frame shortfall (195216 of 195254 at 48 kHz). Set View → time unit to Samples, then over the same range run **track** scope and **item** scope in turn and report the refusal's `Render source:` line plus both frame counts, and whether the media under the range ends before the range does
|
||||||
|
- [ ] Same range extended ~1 s past all media, track scope — a full-length file with trailing silence means postprocessing is off; a short file means a trailing-silence trim is firing despite `RENDER_NORMALIZE &(4<<16)`
|
||||||
|
|
||||||
## Names and channels
|
## Names and channels
|
||||||
|
|
||||||
|
|||||||
@@ -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_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.
|
- `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`), and the capture-action taxonomy table (`captureActionTable`) `main.cpp` iterates to register the CAPTURE_ITEM/CAPTURE_TRACK family.
|
- `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 widened render is refused rather than banked) 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 and the derivation behind it), and `itemExtentPrintsWindow`, the predicate `render_settings::sourceModeForScope` consults to decide whether REAPER's selected-items render source can express a requested window at all.
|
||||||
- `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.
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,18 @@ RenderSettingsChoice renderSettingsFor(SourceMode mode, double /*wetDry*/) {
|
|||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char* renderSourceLabel(SourceMode mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case SourceMode::MasterMix: return "master mix";
|
||||||
|
case SourceMode::TimeSelection: return "master mix (time selection)";
|
||||||
|
case SourceMode::SelectedTracks: return "selected tracks via master";
|
||||||
|
case SourceMode::SelectedItems: return "selected media items";
|
||||||
|
case SourceMode::RazorArea: return "razor edits";
|
||||||
|
case SourceMode::Realtime: return "realtime record";
|
||||||
|
}
|
||||||
|
return "unknown"; // unreachable for a valid enum; never claim a source
|
||||||
|
}
|
||||||
|
|
||||||
SourceMode sourceModeForScope(CaptureScope scope, bool itemExtentIsWindow) {
|
SourceMode sourceModeForScope(CaptureScope scope, bool itemExtentIsWindow) {
|
||||||
switch (scope) {
|
switch (scope) {
|
||||||
case CaptureScope::Item:
|
case CaptureScope::Item:
|
||||||
|
|||||||
@@ -96,6 +96,13 @@ struct RenderSettingsChoice {
|
|||||||
// SelectedItems -> &32|single-file; RazorArea -> &4096|single-file.
|
// SelectedItems -> &32|single-file; RazorArea -> &4096|single-file.
|
||||||
RenderSettingsChoice renderSettingsFor(SourceMode mode, double wetDry);
|
RenderSettingsChoice renderSettingsFor(SourceMode mode, double wetDry);
|
||||||
|
|
||||||
|
// The render source a mode drives, in words. Exists for the offline backend's
|
||||||
|
// 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.
|
||||||
|
const char* renderSourceLabel(SourceMode mode);
|
||||||
|
|
||||||
// --- Capture scope: the FX-scope invariant ------------------------------------
|
// --- Capture scope: the FX-scope invariant ------------------------------------
|
||||||
//
|
//
|
||||||
// See src/core/capture/CLAUDE.md for the scope contract. There is NO master
|
// See src/core/capture/CLAUDE.md for the scope contract. There is NO master
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ long long frameCountFor(double startSeconds, double endSeconds, int sampleRate)
|
|||||||
return frames > 0 ? frames : 0;
|
return frames > 0 ? frames : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool renderHonoredBounds(long long expectedFrames, long long actualFrames) {
|
||||||
|
const long long delta = actualFrames > expectedFrames
|
||||||
|
? actualFrames - expectedFrames
|
||||||
|
: expectedFrames - actualFrames;
|
||||||
|
return delta <= 1;
|
||||||
|
}
|
||||||
|
|
||||||
bool itemExtentPrintsWindow(double reqStart, double reqEnd,
|
bool itemExtentPrintsWindow(double reqStart, double reqEnd,
|
||||||
double itemStart, double itemEnd,
|
double itemStart, double itemEnd,
|
||||||
int sampleRate) {
|
int sampleRate) {
|
||||||
|
|||||||
@@ -18,6 +18,18 @@ namespace reasampler::capture {
|
|||||||
// whether the equality is exact or off by a frame.
|
// whether the equality is exact or off by a frame.
|
||||||
long long frameCountFor(double startSeconds, double endSeconds, int sampleRate);
|
long long frameCountFor(double startSeconds, double endSeconds, int sampleRate);
|
||||||
|
|
||||||
|
// True when a landed render's frame count is consistent with `frameCountFor`'s
|
||||||
|
// answer for the same window. Tolerates a one-frame difference, and exactly one:
|
||||||
|
// frameCountFor rounds EACH edge, so it sits within a frame of the window's
|
||||||
|
// real-valued length (end-start)*rate — and a renderer that floors, ceils or
|
||||||
|
// rounds that same length sits within a frame of it too, so two integers derived
|
||||||
|
// that way can never be more than one apart. A window whose edges do not land on
|
||||||
|
// frame boundaries therefore cannot produce a larger difference; anything larger
|
||||||
|
// is a render that printed something other than the window asked for, whatever
|
||||||
|
// the alignment. Widening this past one frame retires the exact-bounds invariant
|
||||||
|
// rather than relaxing it — do not.
|
||||||
|
bool renderHonoredBounds(long long expectedFrames, long long actualFrames);
|
||||||
|
|
||||||
// True when a render bounded by the selected items' own extent
|
// True when a render bounded by the selected items' own extent
|
||||||
// [itemStart, itemEnd) already prints exactly the requested
|
// [itemStart, itemEnd) already prints exactly the requested
|
||||||
// [reqStart, reqEnd) window — the one case where REAPER's selected-items render
|
// [reqStart, reqEnd) window — the one case where REAPER's selected-items render
|
||||||
|
|||||||
@@ -35,7 +35,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" // frameCountFor — the exact-bounds number
|
#include "core/capture/render_window.h" // frameCountFor / renderHonoredBounds — the exact-bounds gate
|
||||||
|
|
||||||
#define REAPERAPI_MINIMAL
|
#define REAPERAPI_MINIMAL
|
||||||
#define REAPERAPI_WANT_EnumProjects
|
#define REAPERAPI_WANT_EnumProjects
|
||||||
@@ -499,9 +499,10 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exact bounds, made structural: with no tail requested the file must contain
|
// Exact bounds, made structural: with no tail requested the file must contain
|
||||||
// (within a tolerance, see below) the requested window's frames, so a source
|
// the requested window's frames (renderHonoredBounds owns the tolerance and the
|
||||||
// mode that silently widened the render fails loudly here instead of landing as
|
// reasoning behind it), so a render that printed something other than the window
|
||||||
// a successful capture. Auto and Manual add frames by design and are skipped.
|
// fails loudly here instead of landing as a successful capture. Auto and Manual
|
||||||
|
// add frames by design and are skipped.
|
||||||
// (On TailMode::None the landed file is read three times on this path — this gate,
|
// (On TailMode::None the landed file is read three times on this path — this gate,
|
||||||
// the mono collapse, and stampCaptureSample — plus one rewrite when the collapse
|
// the mono collapse, and stampCaptureSample — plus one rewrite when the collapse
|
||||||
// fires; Auto/Manual skip this gate entirely, so they read it twice. A
|
// fires; Auto/Manual skip this gate entirely, so they read it twice. A
|
||||||
@@ -518,24 +519,20 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
|||||||
static_cast<int>(layout.sampleRate))
|
static_cast<int>(layout.sampleRate))
|
||||||
: 0;
|
: 0;
|
||||||
const long long actualFrames = static_cast<long long>(layout.frameCount());
|
const long long actualFrames = static_cast<long long>(layout.frameCount());
|
||||||
// frameCountFor is a difference of frame indices, not a rounded duration
|
if (expectedFrames > 0 &&
|
||||||
// (see render_window.h) — REAPER's own edge-rounding can legitimately land
|
!renderHonoredBounds(expectedFrames, actualFrames)) {
|
||||||
// one frame off that, so the gate tolerates +/-1 rather than exact equality.
|
|
||||||
// The defect this refuses is a whole-item widening (seconds of extra audio,
|
|
||||||
// thousands of frames), which a 1-frame tolerance still catches with
|
|
||||||
// certainty. Tightening to exact equality needs a DAW pass confirming REAPER
|
|
||||||
// resolves the window's two edges to frame indices the same way this does.
|
|
||||||
const long long frameDelta = actualFrames > expectedFrames
|
|
||||||
? actualFrames - expectedFrames
|
|
||||||
: expectedFrames - actualFrames;
|
|
||||||
if (expectedFrames > 0 && frameDelta > 1) {
|
|
||||||
result.status = CaptureStatus::BoundsMismatch;
|
result.status = CaptureStatus::BoundsMismatch;
|
||||||
|
// Naming the render source is load-bearing, not decoration: a source that
|
||||||
|
// derives its own bounds and a time-bounded render that came up short
|
||||||
|
// produce the same frame count, and only one of them is a routing defect.
|
||||||
result.message = "Render produced " + std::to_string(actualFrames) +
|
result.message = "Render produced " + std::to_string(actualFrames) +
|
||||||
" frames but the requested range is " +
|
" frames but the requested range is " +
|
||||||
std::to_string(expectedFrames) + " at " +
|
std::to_string(expectedFrames) + " at " +
|
||||||
std::to_string(layout.sampleRate) +
|
std::to_string(layout.sampleRate) +
|
||||||
" Hz -- the render did not honor the requested bounds. "
|
" Hz -- the render did not honor the requested bounds. "
|
||||||
"Requested [" + std::to_string(request.startSeconds) +
|
"Render source: " +
|
||||||
|
renderSourceLabel(request.sourceMode) +
|
||||||
|
". Requested [" + std::to_string(request.startSeconds) +
|
||||||
"s, " + std::to_string(request.endSeconds) +
|
"s, " + std::to_string(request.endSeconds) +
|
||||||
"s) -> frame indices [" +
|
"s) -> frame indices [" +
|
||||||
std::to_string(std::llround(request.startSeconds *
|
std::to_string(std::llround(request.startSeconds *
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
@@ -63,6 +64,28 @@ static void testRealtimeIsUnsupportedOffline() {
|
|||||||
CHECK(!renderSettingsFor(SourceMode::Realtime, 1.0).supported);
|
CHECK(!renderSettingsFor(SourceMode::Realtime, 1.0).supported);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void testEveryRenderSourceIsNameable() {
|
||||||
|
// The bounds refusal quotes this label to say WHICH render source missed the
|
||||||
|
// window, so every mode must name itself and no two may read alike.
|
||||||
|
const SourceMode all[] = {
|
||||||
|
SourceMode::MasterMix, SourceMode::TimeSelection, SourceMode::SelectedTracks,
|
||||||
|
SourceMode::SelectedItems, SourceMode::RazorArea, SourceMode::Realtime,
|
||||||
|
};
|
||||||
|
for (std::size_t i = 0; i < sizeof(all) / sizeof(all[0]); ++i) {
|
||||||
|
const char* label = renderSourceLabel(all[i]);
|
||||||
|
CHECK(label != nullptr && label[0] != '\0');
|
||||||
|
CHECK(std::strcmp(label, "unknown") != 0);
|
||||||
|
for (std::size_t j = i + 1; j < sizeof(all) / sizeof(all[0]); ++j)
|
||||||
|
CHECK(std::strcmp(label, renderSourceLabel(all[j])) != 0);
|
||||||
|
}
|
||||||
|
// The two the refusal must tell apart: a source that derives its own bounds vs
|
||||||
|
// the time-bounded render.
|
||||||
|
CHECK(std::strcmp(renderSourceLabel(SourceMode::SelectedItems),
|
||||||
|
"selected media items") == 0);
|
||||||
|
CHECK(std::strcmp(renderSourceLabel(SourceMode::SelectedTracks),
|
||||||
|
"selected tracks via master") == 0);
|
||||||
|
}
|
||||||
|
|
||||||
// --- tail: TailMode -> RENDER_* mapping (docs/product/capture-tail.md) --------
|
// --- tail: TailMode -> RENDER_* mapping (docs/product/capture-tail.md) --------
|
||||||
|
|
||||||
static void testTailNoneIsExactBounds() {
|
static void testTailNoneIsExactBounds() {
|
||||||
@@ -395,6 +418,7 @@ int main() {
|
|||||||
testSelectedItemsSingleFile();
|
testSelectedItemsSingleFile();
|
||||||
testRazorSingleFile();
|
testRazorSingleFile();
|
||||||
testRealtimeIsUnsupportedOffline();
|
testRealtimeIsUnsupportedOffline();
|
||||||
|
testEveryRenderSourceIsNameable();
|
||||||
testTailNoneIsExactBounds();
|
testTailNoneIsExactBounds();
|
||||||
testTailAutoIsSurgicalTrim();
|
testTailAutoIsSurgicalTrim();
|
||||||
testAutoTrimRatioDerivesFromDb();
|
testAutoTrimRatioDerivesFromDb();
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
// 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) and the predicate that decides whether REAPER's selected-items render
|
// rate), the verdict the offline backend refuses a capture on, and the predicate
|
||||||
// source can express a requested window at all.
|
// that decides whether REAPER's selected-items render source can express a
|
||||||
|
// requested window at all.
|
||||||
|
|
||||||
#include "../src/core/capture/render_window.h"
|
#include "../src/core/capture/render_window.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
|
||||||
using namespace reasampler::capture;
|
using namespace reasampler::capture;
|
||||||
@@ -42,6 +44,74 @@ static void testFrameCountRefusesEmptyInvertedAndUnknownRate() {
|
|||||||
CHECK(frameCountFor(1.0, 2.0, -1) == 0); // rate nonsensical
|
CHECK(frameCountFor(1.0, 2.0, -1) == 0); // rate nonsensical
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void testWindowStartingAtExactlyZero() {
|
||||||
|
CHECK(frameCountFor(0.0, 1.0, 48000) == 48000);
|
||||||
|
// The window from the reported blocker: it starts at 0 and its end lands a
|
||||||
|
// quarter of a frame off the grid at 48 kHz.
|
||||||
|
CHECK(frameCountFor(0.0, 4.067797, 48000) == 195254);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- renderHonoredBounds: the gate's verdict ---------------------------------
|
||||||
|
|
||||||
|
static void testNonFrameAlignedWindowAcceptsEveryEdgeConvention() {
|
||||||
|
// 4.067797 s at 48 kHz is 195254.26 frames — not a frame boundary. A correct
|
||||||
|
// render lands on 195254, and the neighbours a different edge convention would
|
||||||
|
// produce are inside the gate.
|
||||||
|
const long long expected = frameCountFor(0.0, 4.067797, 48000);
|
||||||
|
CHECK(expected == 195254);
|
||||||
|
CHECK(renderHonoredBounds(expected, 195254));
|
||||||
|
CHECK(renderHonoredBounds(expected, 195255));
|
||||||
|
CHECK(renderHonoredBounds(expected, 195253));
|
||||||
|
// The shortfall actually reported from the DAW is 38 frames — far outside any
|
||||||
|
// alignment slack, so it is a render that missed the window, and is refused.
|
||||||
|
CHECK(!renderHonoredBounds(expected, 195216));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testNonAlignmentCanNeverExceedOneFrame() {
|
||||||
|
// The provable content of the one-frame tolerance: frameCountFor rounds each
|
||||||
|
// edge, so it lands within a frame of the window's real length — and so does a
|
||||||
|
// renderer that floors, ceils or rounds that same length. Sweep both edges over
|
||||||
|
// every eighth of a frame; no pairing may fall outside the gate. The renderer's
|
||||||
|
// count is derived from the length here, independently of frameCountFor.
|
||||||
|
const int rate = 48000;
|
||||||
|
for (int s = 0; s < 8; ++s) {
|
||||||
|
for (int e = 0; e < 8; ++e) {
|
||||||
|
const double start = 3.0 + s / (8.0 * rate);
|
||||||
|
const double end = 7.5 + e / (8.0 * rate);
|
||||||
|
const long long expected = frameCountFor(start, end, rate);
|
||||||
|
const double length = (end - start) * rate;
|
||||||
|
CHECK(renderHonoredBounds(expected,
|
||||||
|
static_cast<long long>(std::floor(length))));
|
||||||
|
CHECK(renderHonoredBounds(expected,
|
||||||
|
static_cast<long long>(std::ceil(length))));
|
||||||
|
CHECK(renderHonoredBounds(expected, std::llround(length)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testWholeItemWideningIsStillRefused() {
|
||||||
|
// The defect the gate was built for: a 1 s window inside a 30 s item printing
|
||||||
|
// the whole item.
|
||||||
|
const long long expected = frameCountFor(5.0, 6.0, 48000);
|
||||||
|
CHECK(expected == 48000);
|
||||||
|
CHECK(!renderHonoredBounds(expected, 30 * 48000));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testLargeShortfallIsStillRefused() {
|
||||||
|
const long long expected = frameCountFor(0.0, 4.067797, 48000);
|
||||||
|
CHECK(!renderHonoredBounds(expected, 190000));
|
||||||
|
// Two frames is the smallest miss outside the tolerance, in both directions —
|
||||||
|
// the tolerance is one frame and stays one frame.
|
||||||
|
CHECK(!renderHonoredBounds(expected, expected - 2));
|
||||||
|
CHECK(!renderHonoredBounds(expected, expected + 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testEmptyRenderIsRefusedAgainstARealWindow() {
|
||||||
|
// A render that produced nothing is a bounds miss like any other; the backend's
|
||||||
|
// own "did the file parse" guard is what keeps an unreadable render out of here.
|
||||||
|
CHECK(!renderHonoredBounds(48000, 0));
|
||||||
|
}
|
||||||
|
|
||||||
// --- itemExtentPrintsWindow: can the selected-items source express this? -----
|
// --- itemExtentPrintsWindow: can the selected-items source express this? -----
|
||||||
|
|
||||||
static void testRangeInsideItemCannotBeExpressed() {
|
static void testRangeInsideItemCannotBeExpressed() {
|
||||||
@@ -106,6 +176,12 @@ int main() {
|
|||||||
testFrameCountIsExactNotRounded();
|
testFrameCountIsExactNotRounded();
|
||||||
testFrameCountIsADifferenceOfIndicesNotADuration();
|
testFrameCountIsADifferenceOfIndicesNotADuration();
|
||||||
testFrameCountRefusesEmptyInvertedAndUnknownRate();
|
testFrameCountRefusesEmptyInvertedAndUnknownRate();
|
||||||
|
testWindowStartingAtExactlyZero();
|
||||||
|
testNonFrameAlignedWindowAcceptsEveryEdgeConvention();
|
||||||
|
testNonAlignmentCanNeverExceedOneFrame();
|
||||||
|
testWholeItemWideningIsStillRefused();
|
||||||
|
testLargeShortfallIsStillRefused();
|
||||||
|
testEmptyRenderIsRefusedAgainstARealWindow();
|
||||||
testRangeInsideItemCannotBeExpressed();
|
testRangeInsideItemCannotBeExpressed();
|
||||||
testRangeWiderThanItemCannotBeExpressedEither();
|
testRangeWiderThanItemCannotBeExpressedEither();
|
||||||
testEachEdgeAloneDisqualifies();
|
testEachEdgeAloneDisqualifies();
|
||||||
|
|||||||
Reference in New Issue
Block a user