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:
+18
-12
@@ -765,20 +765,26 @@ these refusals — but it means a future one- or two-frame refusal may be ours,
|
||||
the tolerance was not widened on speculation. Widening it is a precision-invariant
|
||||
decision, not a bug fix.
|
||||
|
||||
## `capture.cpp` is over the ~600-line ceiling — documented, not split mid-experiment
|
||||
## `capture.cpp` is over the ~600-line ceiling — the seam is identified, taking it is blocked
|
||||
|
||||
**Context.** The bounds-channel live experiment (`RenderBoundsChannel`, this same
|
||||
section above) added the time-selection guard/read-back plumbing and the always-on
|
||||
verdict print to `OfflineRenderBackend::capture`, landing the file at 697 lines against
|
||||
root `CLAUDE.md`'s ~600-line ceiling. The named seam: the drift/verdict instrumentation
|
||||
block (`ScopedTimeSelection`/read-back/drift-report/verdict-print, roughly
|
||||
`capture.cpp:449-655`).
|
||||
**Context.** Removing the settled bounds experiment's instrumentation (the console
|
||||
verdict and the three-checkpoint `RENDER_STARTPOS`/`ENDPOS` read-back) brought the file
|
||||
from 697 to **622 measured lines**, against root `CLAUDE.md`'s ~600-line ceiling. The
|
||||
seam that entry originally named is gone with the instrumentation; nothing left in the
|
||||
file is bisectable without cutting load-bearing why.
|
||||
|
||||
**Deferred, not silent.** ≈60 of the added lines are temporary probe instrumentation
|
||||
with a known removal date (the experiment closes when `docs/VERIFICATION.md` §Capture
|
||||
range and bounds comes back), and splitting the file mid-experiment risks moving the
|
||||
exact code the smoke run is measuring. Split after the experiment closes, onto the seam
|
||||
named above.
|
||||
**The remaining seam is a real responsibility boundary**, and the file header already
|
||||
names it as two things: `OfflineRenderBackend::capture` (the offline render driver)
|
||||
versus the four helpers BOTH backends share — `makeUniqueTag`, `captureNameFor`,
|
||||
`collapseCapturedFileToMono`, `stampCaptureSample` — consumed by `capture_batch`,
|
||||
`capture_orchestrator`, `capture_realtime_shell`, `capture_realtime_finalize` and
|
||||
`render_in_place`. Lifting those four into their own TU takes the driver under the
|
||||
ceiling and gives the cross-backend steps their own home.
|
||||
|
||||
**Why not taken.** `src/shell/capture/` has no `CMakeLists.txt` of its own — its sources
|
||||
are listed in `src/app/CMakeLists.txt`, so a new TU needs an edit there. Forcing the
|
||||
four helpers into an existing TU instead (orchestrator, realtime finalize) would put
|
||||
them in a wrong home to dodge one build-file line, which is worse than the overshoot.
|
||||
|
||||
## bext TimeReference read-back is not a floor detector (dead end, recorded so it is not re-litigated)
|
||||
|
||||
|
||||
@@ -26,11 +26,11 @@ 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`)
|
||||
- [ ] 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`)
|
||||
- [ ] **The millisecond floor — what to expect.** A custom-bounds render is known to floor its window's END to the millisecond and write the floored value back over `RENDER_ENDPOS`. Mechanism, why two `RENDER_BOUNDSFLAG` channels exist, and the two live observations behind this: `src/core/capture/render_settings.h`'s `RenderBoundsChannel` and `docs/TODO.md` "An offline capture can be refused...". **Both live observations started at `0s`, on the grid, so nothing is known about the START edge**
|
||||
- [ ] **The one experiment — does another bounds mode escape the floor?** This build renders on the TIME SELECTION channel (`RENDER_BOUNDSFLAG=2`, window handed over via `GetSet_LoopTimeRange`) instead of custom time bounds. On every return past the point a bounds channel is chosen (the format/mode/empty-range/no-project refusals answer earlier and print nothing), one line beginning `ReaSampler capture -- bounds channel:` prints — including the two paths that answer before any bounds are judged (unsupported format, no output file), which print `NOT JUDGED` rather than staying silent. Read the verdict: **the floor's signature is ONLY a sentence naming "floored to the millisecond"** — 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). **(WITHIN TOLERANCE)** on a SHORT/LONG is the gate's ordinary ±1-frame edge-convention slack (`render_window.h`), not the floor — don't read it as either result, and it can mask a floor: a window whose start and end sit in the same millisecond bucket makes the floored count equal the exact one, so a real one-frame floor there reads as a bare SHORT (WITHIN TOLERANCE) with no floor sentence at all. **EXACT is the fix only when the line carries no further caveat.** The verdict checks the observed count against every millisecond-floored model of the window (start floored alone, end floored alone, both together) and names any that reproduce it — grid membership on either edge is a proxy for that collision, not the test itself, so the caveat can fire even when NEITHER edge sits on the millisecond grid (sub-millisecond remainders on the two edges can cancel under a full floor — a dragged, fixed-length time selection is the reproducible case). Re-run with a window the caveat doesn't name before trusting EXACT. **NOT JUDGED naming a bounds channel** means that capture answered nothing (tail mode was not None, the render was empty, the render never even reached a bounds check, or the window rounds to 0 frames at this rate) OR the render source itself is INFERRED (not SDK-confirmed) to derive its own bounds and never consult the channel — selected-items captures always read this way; razor edits never do today, because no offline capture path assigns `SourceMode::RazorArea` (razor is a range source resolved through track/item scope, not a render source of its own) — so pick a window narrower than the selected item(s) to route through the time-bounded source instead
|
||||
- [ ] **Same run, the START edge.** The verdict line also says whether the run tested the start. Capture a range whose start is NOT a whole millisecond (set View → time unit to Samples, then nudge the selection start off the grid) so the line reads `The START edge IS tested here`. Report that line verbatim — it is the only evidence available for whether the start floors too, and a start floor is the case that would break the null test silently rather than loudly
|
||||
- [ ] **Auto and Manual tail.** Repeat the off-grid-start/off-grid-end capture once with the panel tail toggle at **Auto** and once at **Manual**. Neither is judged against a frame count, so the evidence is the `after render` drift lines: report whether either channel's bounds read back changed. `[verify — DAW]` A tail is assumed to render PAST the window end — the SDK header (`:3048`) confirms only that `RENDER_TAILMS` is a length in ms, not that it extends past the end. If that assumption is wrong, an end floor could be costing Auto/Manual real content with no detector (`checkRenderedBounds` returns immediately for `tailMode != None`) — so also report by ear/measurement whether either tail capture comes up short against the source, not only whether the bounds fields drifted
|
||||
- [ ] Whichever way the experiment lands, the refused render is still kept for diagnosis at `<project folder>/reasampler_refused/` (the refusal line names the path; a failed move leaves it unindexed in the bank folder and says so). Delete the folder when done — nothing in the bank references it
|
||||
- [ ] **The millisecond floor — SETTLED, nothing to re-run for `TailMode::None`.** The floor lives in the custom-time-bounds field (`RENDER_BOUNDSFLAG=0`), not in the render engine. Two live 48 kHz `TailMode::None` renders on `RENDER_BOUNDSFLAG=2` (time selection, handed over via `GetSet_LoopTimeRange`) came back exact — 97627 frames against 97627 — the second over a window whose START carried a sub-millisecond remainder, with no floored model of that window able to reproduce the count. Time selection is now the only bounds mode a capture can reach; the console verdict line and the `RENDER_STARTPOS`/`ENDPOS` read-back probe that answered this are gone. Full observation: `src/core/capture/render_settings.h`'s `kRenderBoundsTimeSelection`
|
||||
- [ ] **Still open — Auto and Manual tail.** `checkRenderedBounds` judges `TailMode::None` only (Auto/Manual add frames by design), so the settled result covers those two by INFERENCE, not observation: the floor applied to the bounds identically on all three tail modes, and all three now hand the window over the same way. What would establish it: repeat an off-grid-start capture at **Manual** over a source that is loud right to the window's end, and check the landed file's frames against window + `tailMs` — a floored edge shows up in that count. **Auto** cannot be checked by count (it trims trailing silence), so it needs the null test by ear/inversion against the source instead
|
||||
- [ ] `[verify — DAW]` A tail is assumed to render PAST the window end — the SDK header (`:3048`) confirms only that `RENDER_TAILMS` is a length in ms, not that it extends past the end. If that assumption is wrong, a tail capture is silently SHORTER than its window with no detector at all. Report whether either tail capture comes up short against the source
|
||||
- [ ] A refused render is kept for diagnosis at `<project folder>/reasampler_refused/` (the refusal line names the path; a failed move leaves it unindexed in the bank folder and says so). Delete the folder when done — nothing in the bank references it
|
||||
- [ ] **If a capture is refused for a short render**, report the refusal line verbatim. A message naming `floored to the millisecond` means the floor is back on a mode measured escaping it; a shortfall of one or two frames with no such sentence may be the gate's own edge-convention tolerance rather than the render (`render_window.h`'s `renderHonoredBounds`)
|
||||
|
||||
## 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_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
|
||||
|
||||
@@ -36,14 +36,14 @@ detail not covered there:
|
||||
- **`renderOffline` is the one seam both a fresh capture and a recipe replay
|
||||
cross**, which is why the refusal and both transient guards live there rather
|
||||
than in the action bodies — anything placed in `ResolveScopeSource` alone would
|
||||
miss `RunRecaptureFromSource` entirely. The bounds channel is inside the backend
|
||||
miss `RunRecaptureFromSource` entirely. The bounds mode is inside the backend
|
||||
that seam calls, for the same reason: a replay must hand its window over exactly
|
||||
the way a fresh capture does.
|
||||
- **The bounds channel is under live experiment**, and `capture.cpp`'s
|
||||
`kBoundsChannel` is its single switch. On the time-selection channel the render
|
||||
window travels in the project's own time selection, so `capture` snapshots and
|
||||
restores that selection like any other state it borrows. Why there are two
|
||||
channels: `src/core/capture/render_settings.h`'s `RenderBoundsChannel`.
|
||||
- **The render window travels in the project's own TIME SELECTION**
|
||||
(`RENDER_BOUNDSFLAG=2`), so `capture` snapshots and restores that selection on every
|
||||
exit path like any other state it borrows. The custom-bounds field floors the window
|
||||
to the millisecond and must not come back — why, in
|
||||
`src/core/capture/render_settings.h`'s `kRenderBoundsTimeSelection`.
|
||||
- **FX-bypass guard ordering.** `scope_resolve` reads the M10 provenance-assembly
|
||||
inputs (track/item selection, FX-chain identity) BEFORE the FX-bypass guard
|
||||
neutralizes the in-scope chain — provenance must see the chain as it really is,
|
||||
|
||||
+21
-121
@@ -6,8 +6,8 @@
|
||||
// the one TU that defines the API pointers; here they are extern.
|
||||
//
|
||||
// Drives the RENDER_* project settings via GetSetProjectInfo/_String (source-
|
||||
// selection bits come from the pure render_settings mapping) plus, on the
|
||||
// time-selection bounds channel, the project time selection; snapshots and restores
|
||||
// selection bits come from the pure render_settings mapping) plus the project time
|
||||
// selection, which is where the render window itself travels; snapshots and restores
|
||||
// every one of them, triggers a render, then populates a Sample.
|
||||
// Source-agnostic: never reads the DAW selection itself, only the CaptureRequest
|
||||
// the caller resolved. RENDER_ADDTOPROJ&1 is cleared on every path — never
|
||||
@@ -35,7 +35,6 @@
|
||||
#include "core/capture/wav_codec.h" // hashWavContent / collapseToMono — the one WAV/RIFF owner
|
||||
#include "core/util/file_bytes.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
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
@@ -63,14 +62,6 @@ namespace {
|
||||
// project — why we set them all explicitly first.
|
||||
constexpr int kActionRenderUsingMostRecentSettings = 42230;
|
||||
|
||||
// The bounds channel this build hands the render window over on. Why there are two,
|
||||
// and the open DAW question this selection exists to answer, are stated once on
|
||||
// RenderBoundsChannel (core/capture/render_settings.h) — flipping this constant back
|
||||
// to CustomTimeBounds is the whole revert.
|
||||
constexpr RenderBoundsChannel kBoundsChannel = RenderBoundsChannel::TimeSelection;
|
||||
constexpr bool kUsesTimeSelectionBounds =
|
||||
(kBoundsChannel == RenderBoundsChannel::TimeSelection);
|
||||
|
||||
// RENDER_TAILFLAG/TAILMS/NORMALIZE/TRIMEND are driven from the pure
|
||||
// tailRenderSettingsFor mapping (render_settings.h) in the tail-driving block below.
|
||||
|
||||
@@ -182,21 +173,17 @@ void restoreRenderSettings(const RenderSettingsSnapshot& s) {
|
||||
GetSetProjectInfo(s.proj, "RENDER_TRIMEND", s.trimEnd, true);
|
||||
}
|
||||
|
||||
// The project time selection, snapshotted and restored around a render that uses it
|
||||
// as its bounds channel. Separate from ScopedRenderSettings because it is project
|
||||
// state rather than a RENDER_* setting, and only one channel touches it.
|
||||
// The project time selection, snapshotted and restored around the render that carries
|
||||
// its window in it. Separate from ScopedRenderSettings because it is project state
|
||||
// rather than a RENDER_* setting.
|
||||
// GetSet_LoopTimeRange has no project parameter (SDK header ~2670) — it acts on the
|
||||
// active project, which is the one capture() already resolved and renders into.
|
||||
struct ScopedTimeSelection {
|
||||
bool engaged;
|
||||
double start = 0.0;
|
||||
double end = 0.0;
|
||||
|
||||
explicit ScopedTimeSelection(bool engage) : engaged(engage) {
|
||||
if (engaged) GetSet_LoopTimeRange(false, false, &start, &end, false);
|
||||
}
|
||||
ScopedTimeSelection() { GetSet_LoopTimeRange(false, false, &start, &end, false); }
|
||||
~ScopedTimeSelection() {
|
||||
if (!engaged) return;
|
||||
// Copies: the setter takes non-const pointers, so the snapshot must not be
|
||||
// what it writes through.
|
||||
double s = start, e = end;
|
||||
@@ -469,46 +456,35 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
}
|
||||
|
||||
ScopedRenderSettings guard(proj);
|
||||
ScopedTimeSelection tsGuard(kUsesTimeSelectionBounds);
|
||||
ScopedTimeSelection tsGuard;
|
||||
|
||||
// The window goes over the selected channel's own store so the rendered length
|
||||
// equals the requested range with NO rounding and NO added silence (unless a tail
|
||||
// was explicitly requested). RENDER_STARTPOS/ENDPOS are written on BOTH channels:
|
||||
// they are documented as applying to mode 0 only (SDK header ~3045-3046), so under
|
||||
// the time-selection channel they are inert, and their read-back below then reports
|
||||
// that field independently of the one actually carrying the window.
|
||||
// The window travels in the project's own time selection, which is what makes the
|
||||
// rendered length the requested range with NO rounding and NO added silence (unless
|
||||
// a tail was explicitly requested) — the custom-bounds field floors it to the
|
||||
// millisecond (render_settings.h's kRenderBoundsTimeSelection).
|
||||
//
|
||||
// RENDER_STARTPOS/ENDPOS are written anyway, to the same window. The header
|
||||
// (~3045-3046) documents them as mode-0-only, but the DAW run that settled this
|
||||
// channel had both stores holding the identical window, so it cannot distinguish
|
||||
// "mode 2 ignored them" from "mode 2 read them and they happened to agree". Writing
|
||||
// them keeps the two stores agreeing rather than resting exactness on that
|
||||
// distinction; a stale leftover here could only ever misalign a render silently.
|
||||
GetSetProjectInfo(proj, "RENDER_BOUNDSFLAG",
|
||||
static_cast<double>(renderBoundsFlagFor(kBoundsChannel)), true);
|
||||
static_cast<double>(kRenderBoundsTimeSelection), true);
|
||||
GetSetProjectInfo(proj, "RENDER_STARTPOS", request.startSeconds, true);
|
||||
GetSetProjectInfo(proj, "RENDER_ENDPOS", request.endSeconds, true);
|
||||
if (kUsesTimeSelectionBounds) {
|
||||
{
|
||||
double s = request.startSeconds, e = request.endSeconds;
|
||||
GetSet_LoopTimeRange(true, false, &s, &e, false);
|
||||
}
|
||||
|
||||
// The time-selection channel's read-back, so each checkpoint below reads the store
|
||||
// that actually carried the window rather than the inert RENDER_* pair.
|
||||
auto readTimeSelection = [](double& s, double& e) {
|
||||
s = 0.0;
|
||||
e = 0.0;
|
||||
GetSet_LoopTimeRange(false, false, &s, &e, false);
|
||||
};
|
||||
|
||||
// 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);
|
||||
double storedTsStart = 0.0, storedTsEnd = 0.0;
|
||||
if (kUsesTimeSelectionBounds) readTimeSelection(storedTsStart, storedTsEnd);
|
||||
|
||||
// TAILFLAG/TAILMS/NORMALIZE/TRIMEND from the pure mapping: None -> exact
|
||||
// bounds + disable-all normalize; Auto -> 8s tail + surgical trim-end
|
||||
// normalize + -72 dB TRIMEND; Manual -> clamped fixed tail + disable-all, no
|
||||
// trim. NORMALIZE is driven here (not the determinism block below) so the
|
||||
// Auto surgical value isn't clobbered.
|
||||
const TailRenderSettings tail =
|
||||
tailRenderSettingsFor(request.tailMode, request.tailMs, kBoundsChannel);
|
||||
tailRenderSettingsFor(request.tailMode, request.tailMs);
|
||||
GetSetProjectInfo(proj, "RENDER_TAILFLAG",
|
||||
static_cast<double>(tail.tailFlag), true);
|
||||
GetSetProjectInfo(proj, "RENDER_TAILMS", tail.tailMs, true);
|
||||
@@ -532,33 +508,6 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
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. Named by
|
||||
// checkpoint so a DAW observation is self-locating: three reads bracket the two
|
||||
// places REAPER could quantize — the store, and the render itself.
|
||||
auto reportDrift = [&](const char* checkpoint, double atStart, double atEnd) {
|
||||
const std::string drift =
|
||||
describeBoundsDrift(request.startSeconds, request.endSeconds,
|
||||
atStart, atEnd, effectiveSampleRate);
|
||||
if (!drift.empty())
|
||||
ShowConsoleMsg(("ReaSampler capture (" + std::string(checkpoint) + "): " +
|
||||
drift + "\n").c_str());
|
||||
};
|
||||
|
||||
// The same checkpoint on the other channel. No-op unless that channel is the one
|
||||
// carrying the window, so the console gains nothing on the custom-bounds build.
|
||||
auto reportTimeSelectionDrift = [&](const char* checkpoint) {
|
||||
if (!kUsesTimeSelectionBounds) return;
|
||||
double s = 0.0, e = 0.0;
|
||||
readTimeSelection(s, e);
|
||||
reportDrift(checkpoint, s, e);
|
||||
};
|
||||
|
||||
reportDrift("at store, custom-bounds fields", storedStart, storedEnd);
|
||||
if (kUsesTimeSelectionBounds)
|
||||
reportDrift("at store, time selection", storedTsStart, storedTsEnd);
|
||||
|
||||
GetSetProjectInfo(proj, "RENDER_CHANNELS",
|
||||
static_cast<double>(request.channelCount), true);
|
||||
|
||||
@@ -580,37 +529,10 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
setProjString(proj, "RENDER_FILE", paths.absoluteDir);
|
||||
setProjString(proj, "RENDER_PATTERN", paths.fileStem);
|
||||
|
||||
// Which bounds channel carried this window and what the render did with it — printed
|
||||
// on EVERY tail mode and on EVERY return past this point (refusals included),
|
||||
// because a verdict that appeared only on some outcomes would read its own absence
|
||||
// on the rest as a pass. Auto/Manual are not judged against a frame count (they add
|
||||
// frames by design) and the sentence says so rather than comparing anyway.
|
||||
// SelectedItems/RazorArea are INFERRED (not SDK-confirmed) to derive their bounds
|
||||
// from content and never consult the channel at all
|
||||
// (render_settings::sourceBypassesBoundsChannel) — the batch-item path renders
|
||||
// through SelectedItems on every capture, so without this the verdict would print an
|
||||
// EXACT/SHORT/LONG claim about a channel that was never in play.
|
||||
const char* boundsBypassLabel = sourceBypassesBoundsChannel(request.sourceMode)
|
||||
? renderSourceLabel(request.sourceMode)
|
||||
: nullptr;
|
||||
auto printBoundsVerdict = [&](long long frames, int rate) {
|
||||
ShowConsoleMsg(("ReaSampler capture -- " +
|
||||
describeBoundsExperiment(renderBoundsChannelLabel(kBoundsChannel),
|
||||
request.startSeconds, request.endSeconds,
|
||||
frames, rate, boundsBypassLabel) +
|
||||
"\n").c_str());
|
||||
};
|
||||
auto reportExperiment = [&](const BoundsVerdict& v) {
|
||||
printBoundsVerdict(v.measuredFrames, v.measuredRate);
|
||||
};
|
||||
|
||||
// Int16/Int24 have no captured ground-truth blob — fail explicitly rather
|
||||
// than silently mis-render at the wrong bit depth.
|
||||
const char* fmtBase64 = wavSinkConfigBase64(request.bitDepth);
|
||||
if (!fmtBase64) {
|
||||
// Nothing was rendered yet — frames/rate 0 reads as NOT JUDGED, same as any
|
||||
// other capture that answered nothing.
|
||||
printBoundsVerdict(0, 0);
|
||||
result.status = CaptureStatus::UnsupportedFormat;
|
||||
result.message = "Requested bit depth has no verified RENDER_FORMAT blob "
|
||||
"(Float32 only; Int16/Int24 not yet supported).";
|
||||
@@ -618,32 +540,12 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
}
|
||||
setProjString(proj, "RENDER_FORMAT", fmtBase64);
|
||||
|
||||
// Read again right here: a mismatch against the store-time read-back above
|
||||
// means something between the two writes and this line moved the bounds,
|
||||
// before the render ever ran. Both channels get the same three checkpoints, or a
|
||||
// store-versus-render distinction would only be available on one of them.
|
||||
reportDrift("before render, custom-bounds fields",
|
||||
GetSetProjectInfo(proj, "RENDER_STARTPOS", 0.0, false),
|
||||
GetSetProjectInfo(proj, "RENDER_ENDPOS", 0.0, false));
|
||||
reportTimeSelectionDrift("before render, time selection");
|
||||
|
||||
Main_OnCommand(kActionRenderUsingMostRecentSettings, 0);
|
||||
|
||||
// And once more here, while the guard above is still live and before it restores
|
||||
// anything: only the gap between this read and the one immediately above can be
|
||||
// the render itself.
|
||||
reportDrift("after render, custom-bounds fields",
|
||||
GetSetProjectInfo(proj, "RENDER_STARTPOS", 0.0, false),
|
||||
GetSetProjectInfo(proj, "RENDER_ENDPOS", 0.0, false));
|
||||
reportTimeSelectionDrift("after render, time selection");
|
||||
|
||||
// Main_OnCommand returns void, so a failed render is silent — stat the
|
||||
// expected output path to detect it.
|
||||
const std::string expectedPath = paths.absoluteDir + "/" + paths.fileName;
|
||||
if (!std::filesystem::exists(expectedPath)) {
|
||||
// Main_OnCommand ran but produced nothing measurable — NOT JUDGED, same as
|
||||
// the format refusal above.
|
||||
printBoundsVerdict(0, 0);
|
||||
result.status = CaptureStatus::RenderFailed;
|
||||
result.message = "Render produced no output file (expected: " +
|
||||
expectedPath + "). Check the REAPER console for errors.";
|
||||
@@ -657,7 +559,6 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
const BoundsVerdict emptyVerdict =
|
||||
checkRenderedFileNotEmpty(expectedPath, projectDir, request.destination);
|
||||
if (emptyVerdict.refused) {
|
||||
reportExperiment(emptyVerdict);
|
||||
result.status = CaptureStatus::BoundsMismatch;
|
||||
result.message = emptyVerdict.message;
|
||||
return result;
|
||||
@@ -671,7 +572,6 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
// on an already-warm file, judged acceptable.)
|
||||
const BoundsVerdict bounds =
|
||||
checkRenderedBounds(expectedPath, projectDir, request);
|
||||
reportExperiment(bounds);
|
||||
if (bounds.refused) {
|
||||
result.status = CaptureStatus::BoundsMismatch;
|
||||
result.message = bounds.message;
|
||||
|
||||
@@ -231,8 +231,8 @@ CaptureResult renderOffline(CaptureScope scope,
|
||||
// caller can report success/failure. Load-bearing principle holds: writes a file +
|
||||
// a bank index entry ONLY; never touches the arrange/timeline. Non-destructive: the
|
||||
// out-of-scope FX/fader/pan chain is fully restored on every path (FxBypassGuard),
|
||||
// and the backend restores every RENDER_* setting it changed plus, on the
|
||||
// time-selection bounds channel, the project time selection it borrowed.
|
||||
// and the backend restores every RENDER_* setting it changed plus the project time
|
||||
// selection it borrowed to carry the render window.
|
||||
//
|
||||
// On success, res.sample.id carries the LANDED bank-index id (S8): the newly-added id
|
||||
// on a fresh add, or the EXISTING entry's id on a hash-dedup collapse — so the S8
|
||||
|
||||
@@ -117,8 +117,6 @@ BoundsVerdict checkRenderedBounds(const std::string& renderedPath,
|
||||
const long long expectedFrames =
|
||||
frameCountFor(request.startSeconds, request.endSeconds, rate);
|
||||
const long long actualFrames = static_cast<long long>(layout.frameCount());
|
||||
v.measuredFrames = actualFrames;
|
||||
v.measuredRate = rate;
|
||||
if (renderHonoredBounds(expectedFrames, actualFrames)) return v;
|
||||
|
||||
// Says whether this shortfall has the known shape: the END alone floored to the
|
||||
|
||||
@@ -18,13 +18,6 @@ namespace reasampler::capture {
|
||||
struct BoundsVerdict {
|
||||
bool refused = false;
|
||||
std::string message; // console text; meaningful only when refused
|
||||
|
||||
// What the landed file measured, when this verdict measured it at all. A rate of 0
|
||||
// means it did not — a tail mode is not judged here, the parse failed, or this is
|
||||
// the emptiness check, which counts no frames. Carried so the backend's bounds
|
||||
// verdict can report the count without opening the file again.
|
||||
long long measuredFrames = 0;
|
||||
int measuredRate = 0;
|
||||
};
|
||||
|
||||
// Judges `renderedPath` against `request`'s window. Refuses on two counts: the file's
|
||||
|
||||
@@ -111,12 +111,8 @@ static void testLabelsSeparateExactlyWhatTheRenderSeparates() {
|
||||
|
||||
// --- tail: TailMode -> RENDER_* mapping (docs/product/capture-tail.md) --------
|
||||
|
||||
// The tail assertions below are about the MODE's mapping; the one value that also
|
||||
// depends on the bounds channel has its own test, so they all pin the channel that
|
||||
// every shipped capture rendered on.
|
||||
static TailRenderSettings tailFor(TailMode mode, double manualTailMs) {
|
||||
return tailRenderSettingsFor(mode, manualTailMs,
|
||||
RenderBoundsChannel::CustomTimeBounds);
|
||||
return tailRenderSettingsFor(mode, manualTailMs);
|
||||
}
|
||||
|
||||
static void testTailNoneIsExactBounds() {
|
||||
@@ -136,11 +132,11 @@ static void testTailNoneIsExactBounds() {
|
||||
}
|
||||
|
||||
static void testTailAutoIsSurgicalTrim() {
|
||||
// Auto -> custom-bounds tail bit, 8 s cap, SURGICAL normalize (ONLY &32768), and
|
||||
// the -72 dB TRIMEND ratio. The disable-all bit must NOT be set (it is semantically
|
||||
// opposed to trim — this assertion catches a regression to the None normalize).
|
||||
// Auto -> the time-selection tail bit, 8 s cap, SURGICAL normalize (ONLY &32768),
|
||||
// and the -72 dB TRIMEND ratio. The disable-all bit must NOT be set (it is
|
||||
// semantically opposed to trim — this catches a regression to the None normalize).
|
||||
TailRenderSettings t = tailFor(TailMode::Auto, 0.0);
|
||||
CHECK(t.tailFlag == kTailFlagCustomBounds); // &1
|
||||
CHECK(t.tailFlag == kTailFlagTimeSelection); // &4
|
||||
CHECK(t.tailMs == kMaxTailMs); // 8000
|
||||
CHECK(t.normalize == kNormalizeTrimEnd); // exactly 32768, nothing else
|
||||
CHECK((t.normalize & kNormalizeDisableAll) == 0); // disable-all is NOT set
|
||||
@@ -161,11 +157,11 @@ static void testAutoTrimRatioDerivesFromDb() {
|
||||
}
|
||||
|
||||
static void testTailManualFixedNoTrim() {
|
||||
// Manual -> custom-bounds tail, the requested ms (within cap), disable-all
|
||||
// Manual -> the time-selection tail bit, the requested ms (within cap), disable-all
|
||||
// normalize (no trim). A Manual capture is a fixed tail, so it keeps today's
|
||||
// disable-all exactly like the no-tail path.
|
||||
TailRenderSettings t = tailFor(TailMode::Manual, 2500.0);
|
||||
CHECK(t.tailFlag == kTailFlagCustomBounds);
|
||||
CHECK(t.tailFlag == kTailFlagTimeSelection);
|
||||
CHECK(t.tailMs == 2500.0);
|
||||
CHECK(t.normalize == kNormalizeDisableAll);
|
||||
CHECK(t.trimEnd == 0.0);
|
||||
@@ -181,59 +177,29 @@ static void testTailManualClampsToCap() {
|
||||
CHECK(tailFor(TailMode::Manual, -50.0).tailMs == 0.0);
|
||||
}
|
||||
|
||||
// --- bounds channel: RENDER_BOUNDSFLAG mode + the tail bit it drags along ------
|
||||
// --- bounds mode: RENDER_BOUNDSFLAG + the tail bit paired with it ---------------
|
||||
|
||||
static void testEachChannelNamesItsOwnBoundsFlagMode() {
|
||||
// The two RENDER_BOUNDSFLAG values, as literals from the SDK header — 0 = custom
|
||||
// time bounds, 2 = time selection. Pinned as numbers so a renumbering of the enum
|
||||
// cannot silently point a capture at "entire project" or "selected media items".
|
||||
CHECK(renderBoundsFlagFor(RenderBoundsChannel::CustomTimeBounds) == 0);
|
||||
CHECK(renderBoundsFlagFor(RenderBoundsChannel::TimeSelection) == 2);
|
||||
static void testTheBoundsModeIsTheTimeSelectionAndItsTailBitIsPairedWithIt() {
|
||||
// Literals from the SDK header, pinned as numbers so neither can drift onto
|
||||
// another bounds mode's value: RENDER_BOUNDSFLAG 2 = time selection (~3042), and
|
||||
// RENDER_TAILFLAG's bits are keyed per bounds mode, &4 = time selection (~3047).
|
||||
// The custom-bounds pair (0 / &1) is DELIBERATELY absent — that mode floors the
|
||||
// window to the millisecond (render_settings.h) and must not come back.
|
||||
CHECK(kRenderBoundsTimeSelection == 2);
|
||||
CHECK(kTailFlagTimeSelection == 4);
|
||||
CHECK(kTailFlagNone == 0);
|
||||
}
|
||||
|
||||
static void testTailBitFollowsTheBoundsChannel() {
|
||||
// RENDER_TAILFLAG's bits are per-bounds-mode: &1 covers custom time bounds, &4
|
||||
// covers the time selection. A tail set under the other channel's bit renders no
|
||||
// tail at all, which is why the mapping takes the channel rather than trusting a
|
||||
// caller to OR the right one in.
|
||||
CHECK(tailFlagBitFor(RenderBoundsChannel::CustomTimeBounds) == 1);
|
||||
CHECK(tailFlagBitFor(RenderBoundsChannel::TimeSelection) == 4);
|
||||
static void testEveryTailModeSetsTheBitTheBoundsModeReads() {
|
||||
// A tail set under a different bounds mode's bit renders no tail at all, so both
|
||||
// tail-bearing modes must carry &4 — a fix applied to Auto alone would leave
|
||||
// Manual silently tailless.
|
||||
for (TailMode mode : {TailMode::Auto, TailMode::Manual})
|
||||
CHECK(tailRenderSettingsFor(mode, 2500.0).tailFlag == kTailFlagTimeSelection);
|
||||
|
||||
// Both tail-bearing modes follow it — a fix applied to Auto alone would leave
|
||||
// Manual rendering under a bit the bounds mode does not read.
|
||||
for (TailMode mode : {TailMode::Auto, TailMode::Manual}) {
|
||||
CHECK(tailRenderSettingsFor(mode, 2500.0,
|
||||
RenderBoundsChannel::CustomTimeBounds)
|
||||
.tailFlag == kTailFlagCustomBounds);
|
||||
CHECK(tailRenderSettingsFor(mode, 2500.0,
|
||||
RenderBoundsChannel::TimeSelection)
|
||||
.tailFlag == kTailFlagTimeSelection);
|
||||
}
|
||||
}
|
||||
|
||||
static void testNoneSetsNoTailBitOnEitherChannel() {
|
||||
// None is exact bounds on every channel: no tail bit, so no channel's bit either.
|
||||
CHECK(tailRenderSettingsFor(TailMode::None, 5000.0,
|
||||
RenderBoundsChannel::CustomTimeBounds)
|
||||
.tailFlag == kTailFlagNone);
|
||||
CHECK(tailRenderSettingsFor(TailMode::None, 5000.0,
|
||||
RenderBoundsChannel::TimeSelection)
|
||||
.tailFlag == kTailFlagNone);
|
||||
}
|
||||
|
||||
static void testTheChannelLabelNamesTheModeAndItsStore() {
|
||||
// The console verdict is read by someone deciding which channel to keep, so the
|
||||
// label has to name both the mode number and where the window actually went.
|
||||
const std::string custom = renderBoundsChannelLabel(RenderBoundsChannel::CustomTimeBounds);
|
||||
CHECK(custom.find("RENDER_BOUNDSFLAG=0") != std::string::npos);
|
||||
CHECK(custom.find("RENDER_STARTPOS") != std::string::npos);
|
||||
|
||||
const std::string ts = renderBoundsChannelLabel(RenderBoundsChannel::TimeSelection);
|
||||
CHECK(ts.find("RENDER_BOUNDSFLAG=2") != std::string::npos);
|
||||
CHECK(ts.find("GetSet_LoopTimeRange") != std::string::npos);
|
||||
|
||||
// Two channels that read alike in the console would make the experiment unreadable.
|
||||
CHECK(custom != ts);
|
||||
// None is exact bounds: no tail bit at all, whatever ms it is handed.
|
||||
CHECK(tailRenderSettingsFor(TailMode::None, 5000.0).tailFlag == kTailFlagNone);
|
||||
CHECK(tailRenderSettingsFor(TailMode::None, 0.0).tailFlag == kTailFlagNone);
|
||||
}
|
||||
|
||||
// --- realtimeRecordWindowEnd: the T2 record-window extension -----------------
|
||||
@@ -384,23 +350,6 @@ static void testMultiTrackStemRenderIsNamedForRefusal() {
|
||||
CHECK(!isMultiTrackStemRender(sourceModeForScope(CaptureScope::Item, true), 2));
|
||||
}
|
||||
|
||||
static void testSourceBypassesBoundsChannelOnlyForContentDerivedSources() {
|
||||
// SelectedItems (&32) and RazorArea (&4096) derive their bounds from the
|
||||
// selected items'/areas' own extents -- RENDER_BOUNDSFLAG is never consulted, so
|
||||
// a bounds-channel verdict is not evidence for either (the regression this
|
||||
// predicate exists to catch: RunBatchCaptureItems always renders through
|
||||
// SelectedItems, so this false-EXACT would fire on every batch-item capture).
|
||||
CHECK(sourceBypassesBoundsChannel(SourceMode::SelectedItems));
|
||||
CHECK(sourceBypassesBoundsChannel(SourceMode::RazorArea));
|
||||
|
||||
// Every other source is genuinely time-bounded through RENDER_STARTPOS/ENDPOS or
|
||||
// the time selection, so the channel IS the evidence for these.
|
||||
CHECK(!sourceBypassesBoundsChannel(SourceMode::MasterMix));
|
||||
CHECK(!sourceBypassesBoundsChannel(SourceMode::TimeSelection));
|
||||
CHECK(!sourceBypassesBoundsChannel(SourceMode::SelectedTracks));
|
||||
CHECK(!sourceBypassesBoundsChannel(SourceMode::Realtime));
|
||||
}
|
||||
|
||||
static void testRefusalMessagesAreSiblingsWithDistinctExits() {
|
||||
const std::string item = multiTrackRefusalMessage(CaptureScope::Item);
|
||||
const std::string track = multiTrackRefusalMessage(CaptureScope::Track);
|
||||
@@ -528,10 +477,8 @@ int main() {
|
||||
testAutoTrimRatioDerivesFromDb();
|
||||
testTailManualFixedNoTrim();
|
||||
testTailManualClampsToCap();
|
||||
testEachChannelNamesItsOwnBoundsFlagMode();
|
||||
testTailBitFollowsTheBoundsChannel();
|
||||
testNoneSetsNoTailBitOnEitherChannel();
|
||||
testTheChannelLabelNamesTheModeAndItsStore();
|
||||
testTheBoundsModeIsTheTimeSelectionAndItsTailBitIsPairedWithIt();
|
||||
testEveryTailModeSetsTheBitTheBoundsModeReads();
|
||||
testRealtimeWindowNoneIsExact();
|
||||
testRealtimeWindowAutoAddsCap();
|
||||
testRealtimeWindowManualAddsClampedLength();
|
||||
@@ -543,7 +490,6 @@ int main() {
|
||||
testScopeSourceModes();
|
||||
testRangedItemScopeRendersTimeBounded();
|
||||
testMultiTrackStemRenderIsNamedForRefusal();
|
||||
testSourceBypassesBoundsChannelOnlyForContentDerivedSources();
|
||||
testRefusalMessagesAreSiblingsWithDistinctExits();
|
||||
testRefusalMessagesMatchGoldenLiterals();
|
||||
testRangeInference();
|
||||
|
||||
+74
-319
@@ -2,13 +2,12 @@
|
||||
// Covers the bounds-equality number (a window's exact frame count at the project
|
||||
// rate), the verdict the offline backend refuses a capture on, the predicate
|
||||
// that decides whether REAPER's selected-items render source can express a
|
||||
// requested window at all, and the two short-render diagnostics.
|
||||
// requested window at all, and the millisecond-floor shape a refusal quotes.
|
||||
|
||||
#include "../src/core/capture/render_window.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
using namespace reasampler::capture;
|
||||
|
||||
@@ -16,10 +15,6 @@ static int g_fail = 0;
|
||||
#define CHECK(cond) do { if(!(cond)) { \
|
||||
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 ------------------
|
||||
|
||||
static void testFrameCountIsExactNotRounded() {
|
||||
@@ -358,308 +353,86 @@ static void testOffGridRecognizesASubMillisecondRemainder() {
|
||||
CHECK(!isOnMillisecondGrid(1.0 - 1.0 / 48000.0));
|
||||
}
|
||||
|
||||
// --- describeBoundsExperiment: the console verdict on a bounds channel --------
|
||||
// --- the settled time-selection observations, as pure arithmetic ---------------
|
||||
//
|
||||
// Two live 48 kHz TailMode::None renders on RENDER_BOUNDSFLAG=2 came back EXACT at
|
||||
// 97627 frames. The console printed run TWO's start verbatim (2.0338983050847457s);
|
||||
// run ONE started at 0s and its end was never printed, so the value below is a
|
||||
// reconstruction from run two's own printed start — it pins the count, not which
|
||||
// double REAPER was handed.
|
||||
|
||||
static void testAnExactRenderReadsExactAndNamesItsChannel() {
|
||||
const std::string s =
|
||||
describeBoundsExperiment("time selection (RENDER_BOUNDSFLAG=2)",
|
||||
0.0, 1.6551724137931001, 79448, 48000);
|
||||
CHECK(contains(s, "EXACT"));
|
||||
CHECK(!contains(s, "SHORT"));
|
||||
CHECK(contains(s, "time selection (RENDER_BOUNDSFLAG=2)"));
|
||||
CHECK(contains(s, "79448"));
|
||||
CHECK(contains(s, "48000 Hz"));
|
||||
// The END here carries a sub-millisecond remainder, so this run DID test it --
|
||||
// the END-untested caveat must not fire on a window it didn't apply to.
|
||||
CHECK(!contains(s, "END edge is UNTESTED"));
|
||||
static void testTheSettledExactRenderOnTheOnGridStart() {
|
||||
CHECK(frameCountFor(0.0, 2.0338983050847457, 48000) == 97627);
|
||||
// Run one could not test the START: 0s is on the grid, which floor, ceil and round
|
||||
// all leave alone, so a start-flooring render prints the identical count.
|
||||
CHECK(isOnMillisecondGrid(0.0));
|
||||
// Its END, though, WAS under test — a floored end would have printed 43 frames fewer.
|
||||
CHECK(msFlooredEndFrameCount(0.0, 2.0338983050847457, 48000) == 97584);
|
||||
CHECK(!renderHonoredBounds(97627, 97584));
|
||||
}
|
||||
|
||||
static void testTheLiveShortfallReadsShortAndNamesTheMillisecondShape() {
|
||||
// The observation, replayed through the verdict: 79440 produced against 79448.
|
||||
const std::string s =
|
||||
describeBoundsExperiment("custom time bounds (RENDER_BOUNDSFLAG=0)",
|
||||
0.0, 1.6551724137931001, 79440, 48000);
|
||||
CHECK(contains(s, "SHORT"));
|
||||
CHECK(!contains(s, "EXACT"));
|
||||
CHECK(contains(s, "79440"));
|
||||
CHECK(contains(s, "79448"));
|
||||
// 79440 IS the ms-floored count, so the verdict has to say the floor did not move.
|
||||
CHECK(contains(s, "floored to the millisecond"));
|
||||
}
|
||||
|
||||
static void testAShortfallThatIsNotTheMillisecondShapeClaimsNothingAboutIt() {
|
||||
// A render 3 frames short is short, but 79445 is not the floored count — the
|
||||
// millisecond sentence must not appear, or it would assert a shape that is absent.
|
||||
CHECK(msFlooredEndFrameCount(0.0, 1.6551724137931001, 48000) != 79445);
|
||||
const std::string s =
|
||||
describeBoundsExperiment("custom time bounds", 0.0, 1.6551724137931001,
|
||||
79445, 48000);
|
||||
CHECK(contains(s, "SHORT"));
|
||||
CHECK(!contains(s, "floored to the millisecond"));
|
||||
}
|
||||
|
||||
static void testARenderPastTheWindowReadsLong() {
|
||||
// The whole-item widening, through the verdict: 30 s printed for a 1 s window.
|
||||
const std::string s =
|
||||
describeBoundsExperiment("custom time bounds", 5.0, 6.0, 30 * 48000, 48000);
|
||||
CHECK(contains(s, "LONG"));
|
||||
CHECK(contains(s, "1440000 frames"));
|
||||
CHECK(contains(s, "the 48000 the window asks for"));
|
||||
}
|
||||
|
||||
static void testAWindowAlreadyOnTheGridIsUnaffectedByTheChannelSwitch() {
|
||||
// A window whose end is a whole millisecond has nothing for a floor to take: the
|
||||
// exact count and the floored count are the same number, so an exact render reads
|
||||
// EXACT and the millisecond sentence never fires.
|
||||
CHECK(frameCountFor(0.0, 2.0, 48000) == msFlooredEndFrameCount(0.0, 2.0, 48000));
|
||||
const std::string s =
|
||||
describeBoundsExperiment("time selection", 0.0, 2.0, 96000, 48000);
|
||||
CHECK(contains(s, "EXACT"));
|
||||
CHECK(contains(s, "96000"));
|
||||
CHECK(!contains(s, "floored to the millisecond"));
|
||||
// The false positive this window is the shape of: a render that floored either edge
|
||||
// alone, or both together, would have printed this identical EXACT count (every
|
||||
// edge here is on the grid) -- the line has to say this run cannot rule any of them
|
||||
// out rather than reading EXACT as settled.
|
||||
CHECK(contains(s, "EXACT here is not proof"));
|
||||
CHECK(contains(s, "floors the START edge alone"));
|
||||
CHECK(contains(s, "floors the END edge alone"));
|
||||
CHECK(contains(s, "floors START and END together"));
|
||||
}
|
||||
|
||||
static void testEqualRemaindersCancelUnderAFullFloorEvenOffGrid() {
|
||||
// C1: a dragged, fixed-length time selection reproduces this. Neither edge sits on
|
||||
// the millisecond grid (isOnMillisecondGrid is false for both), but the START and
|
||||
// END frame-rounding remainders are EQUAL (rs == re == 8 frames), so a render that
|
||||
// floors both edges together lands on the identical count -- the grid predicate on
|
||||
// either edge alone would have missed this collision entirely.
|
||||
const double start = 1.0001724, end = 2.0001724;
|
||||
static void testTheSettledExactRenderTestedBothEdges() {
|
||||
// Run two: both edges carry a sub-millisecond remainder, and the render still
|
||||
// printed the window's exact count.
|
||||
const double start = 2.0338983050847457, end = 4.0677966101694913;
|
||||
CHECK(!isOnMillisecondGrid(start));
|
||||
CHECK(!isOnMillisecondGrid(end));
|
||||
const long long expected = frameCountFor(start, end, 48000);
|
||||
CHECK(expected == 48000);
|
||||
// The both-edges-floored render lands on the SAME count as the exact one.
|
||||
CHECK(frameCountFor(1.000, 2.000, 48000) == expected);
|
||||
// Neither edge floored ALONE reproduces it -- only the combined floor does.
|
||||
CHECK(frameCountFor(1.000, end, 48000) != expected);
|
||||
CHECK(frameCountFor(start, 2.000, 48000) != expected);
|
||||
CHECK(frameCountFor(start, end, 48000) == 97627);
|
||||
|
||||
const std::string s =
|
||||
describeBoundsExperiment("time selection", start, end, expected, 48000);
|
||||
CHECK(contains(s, "EXACT"));
|
||||
CHECK(contains(s, "EXACT here is not proof"));
|
||||
CHECK(contains(s, "floors START and END together"));
|
||||
CHECK(!contains(s, "floors the START edge alone"));
|
||||
CHECK(!contains(s, "floors the END edge alone"));
|
||||
// What makes that EXACT proof rather than a coincidence: NO millisecond-floored
|
||||
// model of this window reproduces 97627, and every one of them sits outside the
|
||||
// gate's one-frame tolerance. This is the assertion the whole experiment rests on.
|
||||
const long long startAlone = frameCountFor(2.033, end, 48000);
|
||||
const long long endAlone = frameCountFor(start, 4.067, 48000);
|
||||
const long long bothTogether = frameCountFor(2.033, 4.067, 48000);
|
||||
CHECK(startAlone == 97670);
|
||||
CHECK(endAlone == 97589);
|
||||
CHECK(bothTogether == 97632);
|
||||
CHECK(!renderHonoredBounds(97627, startAlone));
|
||||
CHECK(!renderHonoredBounds(97627, endAlone));
|
||||
CHECK(!renderHonoredBounds(97627, bothTogether));
|
||||
}
|
||||
|
||||
static void testEndOffGridByUnderHalfAFrameStillCollidesWithAFlooredEnd() {
|
||||
// C1's second live shape: isOnMillisecondGrid reads this END as off-grid, but the
|
||||
// remainder is under half a frame at 48 kHz, so flooring it doesn't move its frame
|
||||
// index -- a grid test on the edge alone would still miss this collision.
|
||||
const double start = 0.0, end = 1.000005;
|
||||
CHECK(!isOnMillisecondGrid(end));
|
||||
const long long expected = frameCountFor(start, end, 48000);
|
||||
CHECK(expected == 48000);
|
||||
CHECK(frameCountFor(start, 1.000, 48000) == expected); // the floored-end model matches
|
||||
static void testOnAndOffGridWindowsAreHonoredIdentically() {
|
||||
// Nothing on the settled path may treat a grid-aligned window differently from one
|
||||
// carrying a remainder — the whole point of leaving the flooring channel behind.
|
||||
const double onStart = 1.000, onEnd = 2.000;
|
||||
const double offStart = 1.0001724, offEnd = 2.0001724;
|
||||
CHECK(isOnMillisecondGrid(onStart));
|
||||
CHECK(isOnMillisecondGrid(onEnd));
|
||||
CHECK(!isOnMillisecondGrid(offStart));
|
||||
CHECK(!isOnMillisecondGrid(offEnd));
|
||||
|
||||
const std::string s =
|
||||
describeBoundsExperiment("time selection", start, end, expected, 48000);
|
||||
CHECK(contains(s, "EXACT"));
|
||||
CHECK(contains(s, "EXACT here is not proof"));
|
||||
CHECK(contains(s, "floors the END edge alone"));
|
||||
const long long on = frameCountFor(onStart, onEnd, 48000);
|
||||
const long long off = frameCountFor(offStart, offEnd, 48000);
|
||||
CHECK(on == 48000);
|
||||
CHECK(off == 48000);
|
||||
|
||||
// The discriminating half: the off-grid window is one a flooring render WOULD get
|
||||
// wrong (47992 against 48000) while the on-grid one is untouched by a floor. The
|
||||
// gate's verdict must not notice that difference at any delta.
|
||||
CHECK(msFlooredEndFrameCount(offStart, offEnd, 48000) == 47992);
|
||||
CHECK(msFlooredEndFrameCount(onStart, onEnd, 48000) == on);
|
||||
for (long long delta = -3; delta <= 3; ++delta)
|
||||
CHECK(renderHonoredBounds(on, on + delta) ==
|
||||
renderHonoredBounds(off, off + delta));
|
||||
}
|
||||
|
||||
static void testALongVerdictNeverCarriesTheFloorSentence() {
|
||||
// A floor only removes frames, so LONG can never be its signature -- the sentence
|
||||
// must not appear even though the delta here is a "clean" one-frame LONG.
|
||||
const std::string s =
|
||||
describeBoundsExperiment("time selection", 5.0, 6.0, 48001, 48000);
|
||||
CHECK(contains(s, "LONG"));
|
||||
CHECK(!contains(s, "floored to the millisecond"));
|
||||
}
|
||||
|
||||
static void testASubFrameWindowIsNotJudgedNotExact() {
|
||||
// A window under one frame at this rate rounds to 0 expected frames. A 0-frame
|
||||
// render against that is a 0-vs-0 coincidence of degenerate inputs, not a match --
|
||||
// it must read NOT JUDGED, never EXACT.
|
||||
const double oneTenthOfAFrame = 1.0 / (48000.0 * 10.0);
|
||||
const long long expected = frameCountFor(0.0, oneTenthOfAFrame, 48000);
|
||||
CHECK(expected == 0);
|
||||
const std::string s =
|
||||
describeBoundsExperiment("time selection", 0.0, oneTenthOfAFrame, 0, 48000);
|
||||
CHECK(contains(s, "NOT JUDGED"));
|
||||
CHECK(!contains(s, "EXACT"));
|
||||
}
|
||||
|
||||
static void testAWithinToleranceDeltaIsTaggedNotFloorShaped() {
|
||||
// One frame off frameCountFor is the gate's own edge-convention slack
|
||||
// (render_window.h), not the millisecond floor -- the verdict must say so rather
|
||||
// than reading like a genuine miss or like the floor was escaped.
|
||||
const std::string shortByOne =
|
||||
describeBoundsExperiment("time selection", 0.0, 4.067797, 195253, 48000);
|
||||
CHECK(contains(shortByOne, "SHORT"));
|
||||
CHECK(contains(shortByOne, "WITHIN TOLERANCE"));
|
||||
CHECK(!contains(shortByOne, "floored to the millisecond"));
|
||||
|
||||
const std::string longByOne =
|
||||
describeBoundsExperiment("time selection", 0.0, 4.067797, 195255, 48000);
|
||||
CHECK(contains(longByOne, "LONG"));
|
||||
CHECK(contains(longByOne, "WITHIN TOLERANCE"));
|
||||
|
||||
// A genuine miss (outside the tolerance) carries no such tag.
|
||||
const std::string shortByThree =
|
||||
describeBoundsExperiment("time selection", 0.0, 4.067797, 195251, 48000);
|
||||
CHECK(contains(shortByThree, "SHORT"));
|
||||
CHECK(!contains(shortByThree, "WITHIN TOLERANCE"));
|
||||
}
|
||||
|
||||
static void testABypassingSourceReadsNotJudgedAndNamesTheSourceNotTheChannel() {
|
||||
// SelectedItems/RazorArea derive their own bounds from content -- the channel
|
||||
// named by channelLabel was never consulted, so a matching frame count here would
|
||||
// be a coincidence, not evidence the channel escaped the floor.
|
||||
const std::string s =
|
||||
describeBoundsExperiment("time selection", 0.0, 1.6551724137931001,
|
||||
79448, 48000, "selected media items");
|
||||
CHECK(contains(s, "NOT JUDGED"));
|
||||
CHECK(contains(s, "selected media items"));
|
||||
CHECK(!contains(s, "EXACT"));
|
||||
// The channel is still named at the top of the line -- only the verdict changes.
|
||||
CHECK(contains(s, "time selection"));
|
||||
}
|
||||
|
||||
static void testANullOrEmptyBypassLabelFallsBackToTheOrdinaryVerdict() {
|
||||
// Off-grid, non-cancelling edges (see testEqualRemaindersCancelUnderAFullFloorEvenOffGrid
|
||||
// for the window shape that WOULD trip the collision caveat, whose own text also
|
||||
// contains "EXACT") so this assertion is pinned to the verdict word itself, not to a
|
||||
// caveat sentence that happens to contain the same substring.
|
||||
const double start = 1.0001724, end = 2.0009724;
|
||||
const long long expected = frameCountFor(start, end, 48000);
|
||||
const std::string withNull =
|
||||
describeBoundsExperiment("time selection", start, end, expected, 48000, nullptr);
|
||||
CHECK(contains(withNull, "EXACT"));
|
||||
CHECK(!contains(withNull, "EXACT here is not proof"));
|
||||
CHECK(contains(describeBoundsExperiment("time selection", start, end, expected, 48000,
|
||||
""),
|
||||
"EXACT"));
|
||||
}
|
||||
|
||||
static void testAnOnGridStartSaysTheStartEdgeIsUntested() {
|
||||
// Both live observations started at 0 s — the value that hides a start-side floor.
|
||||
const std::string s =
|
||||
describeBoundsExperiment("time selection", 0.0, 1.6551724137931001, 79448, 48000);
|
||||
CHECK(contains(s, "UNTESTED"));
|
||||
CHECK(contains(s, "millisecond grid"));
|
||||
}
|
||||
|
||||
static void testAnOffGridStartSaysTheStartEdgeIsTested() {
|
||||
// The run that would genuinely settle the start question: a start carrying its own
|
||||
// remainder, paired with an end whose remainder does NOT cancel it (unlike
|
||||
// testEqualRemaindersCancelUnderAFullFloorEvenOffGrid's window, where the same shape
|
||||
// of start value pairs with an end that cancels it and the collision caveat fires
|
||||
// instead). No floored model reproduces this count, so EXACT here is unqualified.
|
||||
const double start = 1.0001724, end = 2.0009724;
|
||||
const long long expected = frameCountFor(start, end, 48000);
|
||||
const std::string s =
|
||||
describeBoundsExperiment("time selection", start, end, expected, 48000);
|
||||
CHECK(contains(s, "IS tested"));
|
||||
CHECK(!contains(s, "UNTESTED"));
|
||||
CHECK(contains(s, "EXACT"));
|
||||
CHECK(!contains(s, "EXACT here is not proof"));
|
||||
// A start-floored-alone render would have printed a DIFFERENT count here, so a
|
||||
// mismatch against `expected` on a re-run is real evidence, not ambiguous.
|
||||
CHECK(frameCountFor(1.000, end, 48000) != expected);
|
||||
CHECK(contains(describeBoundsExperiment("time selection", start, end,
|
||||
frameCountFor(1.000, end, 48000), 48000),
|
||||
"LONG"));
|
||||
}
|
||||
|
||||
static void testAt44100WhereAMillisecondIsNotAWholeNumberOfFrames() {
|
||||
// 44.1 kHz: the window is 463 frames, the ms-floored one 441 (both pinned in
|
||||
// testMillisecondFloorAt44100WhereAMillisecondIsNotWholeFrames). The verdict has to
|
||||
// reach the same two numbers at a rate where a millisecond is 44.1 frames.
|
||||
const std::string exact =
|
||||
describeBoundsExperiment("time selection", 0.0, 0.0105, 463, 44100);
|
||||
CHECK(contains(exact, "EXACT"));
|
||||
CHECK(contains(exact, "44100 Hz"));
|
||||
|
||||
const std::string floored =
|
||||
describeBoundsExperiment("custom time bounds", 0.0, 0.0105, 441, 44100);
|
||||
CHECK(contains(floored, "SHORT"));
|
||||
CHECK(contains(floored, "floored to the millisecond"));
|
||||
}
|
||||
|
||||
static void testAnUnmeasuredRenderAnswersNothingRatherThanPassing() {
|
||||
// Auto/Manual are not judged against a frame count, and an empty render has none.
|
||||
// The line must still print and must not read as a pass — its silence would.
|
||||
const std::string s =
|
||||
describeBoundsExperiment("time selection", 0.0, 1.6551724137931001, 0, 0);
|
||||
CHECK(!s.empty());
|
||||
CHECK(contains(s, "NOT JUDGED"));
|
||||
CHECK(!contains(s, "EXACT"));
|
||||
CHECK(contains(s, "time selection"));
|
||||
}
|
||||
|
||||
static void testAnUnnamedChannelStillProducesAReadableLine() {
|
||||
CHECK(contains(describeBoundsExperiment(nullptr, 0.0, 1.0, 48000, 48000),
|
||||
"unnamed"));
|
||||
CHECK(contains(describeBoundsExperiment("", 0.0, 1.0, 48000, 48000), "unnamed"));
|
||||
}
|
||||
|
||||
// --- 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. Pinned as the actual %.17g literals
|
||||
// (not the needle the two ends share, "s)", which occurs at every precision and so
|
||||
// proves nothing): a report that regressed to a shorter format like %.6g would
|
||||
// print the same six significant digits for both ends, and these two `contains`
|
||||
// checks would then fail.
|
||||
const double asked = 4.067797;
|
||||
const double stored = std::nextafter(asked, 5.0);
|
||||
char askedBuf[32], storedBuf[32];
|
||||
std::snprintf(askedBuf, sizeof(askedBuf), "%.17g", asked);
|
||||
std::snprintf(storedBuf, sizeof(storedBuf), "%.17g", stored);
|
||||
CHECK(std::string(askedBuf) != std::string(storedBuf));
|
||||
|
||||
const std::string s = describeBoundsDrift(0.0, asked, 0.0, stored, 48000);
|
||||
CHECK(!s.empty());
|
||||
CHECK(contains(s, askedBuf));
|
||||
CHECK(contains(s, storedBuf));
|
||||
}
|
||||
|
||||
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"));
|
||||
static void testOnAndOffGridAt44100WhereAMillisecondIsNotWholeFrames() {
|
||||
// 44.1 kHz: a millisecond is 44.1 frames, so a grid-aligned window's edges are NOT
|
||||
// frame-aligned. The exact counts must still be exact and the two must still be
|
||||
// judged identically.
|
||||
const double onStart = 1.000, onEnd = 2.000;
|
||||
const double offStart = 1.0001724, offEnd = 2.0001724;
|
||||
const long long on = frameCountFor(onStart, onEnd, 44100);
|
||||
const long long off = frameCountFor(offStart, offEnd, 44100);
|
||||
CHECK(on == 44100);
|
||||
CHECK(off == 44100);
|
||||
CHECK(msFlooredEndFrameCount(offStart, offEnd, 44100) == 44092);
|
||||
CHECK(msFlooredEndFrameCount(onStart, onEnd, 44100) == on);
|
||||
for (long long delta = -3; delta <= 3; ++delta)
|
||||
CHECK(renderHonoredBounds(on, on + delta) ==
|
||||
renderHonoredBounds(off, off + delta));
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -689,28 +462,10 @@ int main() {
|
||||
testTheTwoLiveShortRendersPinnedAtFullPrecision();
|
||||
testOnGridRecognizesWholeMillisecondsIncludingTheBinaryTrap();
|
||||
testOffGridRecognizesASubMillisecondRemainder();
|
||||
testAnExactRenderReadsExactAndNamesItsChannel();
|
||||
testTheLiveShortfallReadsShortAndNamesTheMillisecondShape();
|
||||
testAShortfallThatIsNotTheMillisecondShapeClaimsNothingAboutIt();
|
||||
testARenderPastTheWindowReadsLong();
|
||||
testAWindowAlreadyOnTheGridIsUnaffectedByTheChannelSwitch();
|
||||
testEqualRemaindersCancelUnderAFullFloorEvenOffGrid();
|
||||
testEndOffGridByUnderHalfAFrameStillCollidesWithAFlooredEnd();
|
||||
testALongVerdictNeverCarriesTheFloorSentence();
|
||||
testASubFrameWindowIsNotJudgedNotExact();
|
||||
testAWithinToleranceDeltaIsTaggedNotFloorShaped();
|
||||
testABypassingSourceReadsNotJudgedAndNamesTheSourceNotTheChannel();
|
||||
testANullOrEmptyBypassLabelFallsBackToTheOrdinaryVerdict();
|
||||
testAnOnGridStartSaysTheStartEdgeIsUntested();
|
||||
testAnOffGridStartSaysTheStartEdgeIsTested();
|
||||
testAt44100WhereAMillisecondIsNotAWholeNumberOfFrames();
|
||||
testAnUnmeasuredRenderAnswersNothingRatherThanPassing();
|
||||
testAnUnnamedChannelStillProducesAReadableLine();
|
||||
testBoundsThatReadBackUnchangedDescribeNothing();
|
||||
testADriftedEndNamesBothWindowsAndBothCounts();
|
||||
testTheReportPrintsEnoughDigitsToShowTheDrift();
|
||||
testADriftedStartIsCaughtToo();
|
||||
testAnUnknownRateStillReportsTheDriftWithoutFrames();
|
||||
testTheSettledExactRenderOnTheOnGridStart();
|
||||
testTheSettledExactRenderTestedBothEdges();
|
||||
testOnAndOffGridWindowsAreHonoredIdentically();
|
||||
testOnAndOffGridAt44100WhereAMillisecondIsNotWholeFrames();
|
||||
|
||||
if (g_fail) { std::printf("%d check(s) FAILED\n", g_fail); return 1; }
|
||||
std::printf("render_window: all checks passed\n");
|
||||
|
||||
Reference in New Issue
Block a user