Ψ-W2-T2 remediation: atomic temp+rename collapse write, honest unknown-channel fallback, [verify — DAW] markers, corrected+filed bake-collapse deferral, observable collapse message, quiet-NaN test

This commit is contained in:
2026-08-01 22:08:39 -04:00
parent 4fa3c1dd15
commit 8b191e3379
12 changed files with 151 additions and 31 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ detail not covered there:
- `realtime_lifecycle` (`shell/capture`) — the in-flight realtime-capture state machine + globals (Q-W3 hoist): the action starts it, `OnTimer` drives it per tick via `DriveRealtimeCapture` (a single-pointer-test idle fast path — load-bearing hot-path guardrail), `CommitRealtimeResult` lands a finished capture in the bank, `AbortRealtimeCaptureForUnload` tears down cleanly on extension unload.
- `capture_realtime_shell` (`shell/capture`) — the async realtime-record backend surface (Q-W6 split of the former fat `capture.h`): `RealtimeRecordBackend::begin`/`tick`/`abort`, transport-driven across timer ticks (a realtime record cannot block REAPER's UI for its own duration). Deliberately shares NO interface with the offline backend — the lifecycles genuinely differ (the former `ICaptureBackend` interface was deleted in Q-W3, T4-26).
- `capture_realtime_finalize` (`shell/capture`) — the file-side half of the realtime-record shell (Q-W3, T4-08): discovers the file REAPER actually recorded, moves it into the bank, runs the Auto-tail PCM decay-scan trim, and populates the finished `Sample`.
- `insert` — placement via `InsertMedia`. **Conform-to-project-tempo is an explicit opt-in flag, never silent stretching.**
- `insert` — placement via `InsertMedia`. **Conform-to-project-tempo is an explicit opt-in flag, never silent stretching.** The mono collapse needs no change here: `insert.cpp` passes only a path to `InsertMedia`, and REAPER derives the item's channel count from the file itself — a 1-channel WAV yields a mono item for free.
- `provenance_shell` — FX-chain identity queries via `TrackFX_*`/`TakeFX_*` APIs; feeds the pure `provenance` fingerprint builder. Stamps `Sample.provenance` on capture; ambiguous/mixed cases record nothing conservatively.
- `track_guid` — shared `MediaTrack*` → canonical GUID-string formatter; single source of truth for membership keys.
- `item_read` — the ONE place a `MediaItem*` is read for its canonical GUID string (`itemGuid`) and for the durable `P_LANENAME` of the fixed lane it sits on (`itemLaneName`); extracted from previously-duplicated `itemGuid`/`itemLaneName` pairs in `view.cpp` and `bank_panel.cpp` — the item-read analog of `track_guid`'s single `MediaTrack*`→GUID-key formatter. Callers must already know the track is fixed-lane (`I_FREEMODE==2`) before calling `itemLaneName`; the pure `isOnManualLane` predicate handles the non-fixed-lane case separately.
+42 -16
View File
@@ -1,5 +1,5 @@
// REAPER-facing offline-render backend (OfflineRenderBackend) plus the shared
// backend helpers (makeUniqueTag / stampCaptureSample).
// backend helpers (makeUniqueTag / collapseCapturedFileToMono / stampCaptureSample).
//
// Includes reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT — main.cpp is
// the one TU that defines the API pointers; here they are extern.
@@ -31,7 +31,7 @@
#include <vector>
#include "core/capture/capture_paths.h"
#include "core/capture/wav_codec.h" // hashWavContent — the one WAV/RIFF owner
#include "core/capture/wav_codec.h" // hashWavContent / collapseToMono — the one WAV/RIFF owner
#include "core/util/file_bytes.h"
#include "core/capture/render_settings.h"
#include "core/capture/render_window.h" // frameCountFor — the exact-bounds number
@@ -201,29 +201,54 @@ std::string makeUniqueTag(const std::string& prefix) {
std::to_string(++counter);
}
void collapseCapturedFileToMono(const std::string& absolutePath) {
bool collapseCapturedFileToMono(const std::string& absolutePath) {
const std::vector<std::uint8_t> bytes = util::readFileBytes(absolutePath);
if (bytes.empty()) return;
if (bytes.empty()) return false;
const MonoCollapse collapse = collapseToMono(bytes);
if (!collapse.collapsed) return;
if (!collapse.collapsed) return false;
// One truncating write the same shape, and the same accepted mid-write residual,
// as the realtime Auto-tail trim (capture_realtime_finalize.cpp).
std::ofstream out(absolutePath, std::ios::binary | std::ios::trunc);
if (!out) return;
out.write(reinterpret_cast<const char*>(collapse.bytes.data()),
static_cast<std::streamsize>(collapse.bytes.size()));
// Sibling temp + rename, NOT an in-place truncating write: this runs unconditionally
// on the deterministic offline path (which never reopened its render for write before
// this step existed), so a mid-write failure here must not land a truncated file that
// stampCaptureSample then hashes as a false CaptureStatus::Ok. rename() replaces the
// destination in one step, so the original bytes are never destroyed until the
// replacement is known-complete; a failed write or rename leaves the original file
// untouched and self-cleans the temp rather than littering it.
const std::string tempPath = absolutePath + ".moncollapse.tmp";
{
std::ofstream out(tempPath, std::ios::binary | std::ios::trunc);
if (!out) return false;
out.write(reinterpret_cast<const char*>(collapse.bytes.data()),
static_cast<std::streamsize>(collapse.bytes.size()));
const bool wroteOk = static_cast<bool>(out);
out.close();
if (!wroteOk) {
std::error_code ec;
std::filesystem::remove(tempPath, ec);
return false;
}
}
std::error_code ec;
std::filesystem::rename(tempPath, absolutePath, ec);
if (ec) {
std::filesystem::remove(tempPath, ec); // don't leave litter on a failed rename
return false;
}
return true;
}
void stampCaptureSample(Sample& s, const CaptureRequest& req,
ReaProject* rateProj, ReaProject* timeSigProj,
const std::string& absolutePath) {
// Track GUIDs echoed from the request (the caller resolved the selection; the
// backends stay source-agnostic). channelCount starts at the request value only
// as the fallback for an unparseable file — the produced FILE overrides it below.
// backends stay source-agnostic). channelCount starts at 0 (unknown, the same
// sentinel bank_model already uses for a pre-field entry) rather than the
// request's value — the request always asks for 2, so echoing it would claim a
// measurement that never happened for the unparseable-file case below. The
// produced FILE overrides it below whenever it parses.
s.trackGuids = req.trackGuids;
s.channelCount = req.channelCount;
s.channelCount = 0;
// PROJECT_SRATE can read 0 on a project that never pinned a rate — stays 0
// (honest "unknown") rather than a bogus literal.
@@ -473,7 +498,7 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
// renderer's file rather than one this step had already rewritten. The collapse
// preserves the frame count, so the two are order-independent in outcome — only
// in what each is measuring.
collapseCapturedFileToMono(expectedPath);
const bool collapsedToMono = collapseCapturedFileToMono(expectedPath);
// Record the request's own bounds (exact) rather than re-measuring the file.
Sample s;
@@ -501,7 +526,8 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
result.message = "Captured [" +
std::to_string(request.startSeconds) + "s, " +
std::to_string(request.endSeconds) + "s] -> " +
paths.relativePath;
paths.relativePath +
(collapsedToMono ? " (collapsed to mono)" : "");
return result;
}
+6 -3
View File
@@ -107,11 +107,14 @@ std::string makeUniqueTag(const std::string& prefix);
// bit-identical (the pure `collapseToMono` decides). Every other file is left
// untouched, byte for byte, so the not-collapsed path is exactly what the backend
// produced. Must run BEFORE stampCaptureSample, which measures the landed file.
void collapseCapturedFileToMono(const std::string& absolutePath);
// Returns whether the file was actually rewritten (collapsed AND the write landed) —
// callers use it to make the collapse observable in the reported CaptureResult.
bool collapseCapturedFileToMono(const std::string& absolutePath);
// Stamps the metadata shared by both backends onto `s`: trackGuids (echoed from the
// request) + channelCount (measured from the produced file's `fmt`; the request
// value only as the fallback for a file that cannot be parsed), resolved
// request) + channelCount (measured from the produced file's `fmt`; 0/unknown as the
// fallback for a file that cannot be parsed — never the request's value, which is
// always 2 and was never actually measured), resolved
// sampleRate (request rate, else PROJECT_SRATE
// from `rateProj`), captureTempo, the capture-start time signature
// (TimeMap_GetTimeSigAtTime against `timeSigProj` — offline passes nullptr for the
@@ -186,7 +186,7 @@ CaptureResult finalizeRecording(ReaProject* proj, MediaTrack* temp,
// Channel-domain rewrite, after the frame-domain trim so it acts on the final
// frame set; it preserves the frame count, so the trimmed length above still holds.
collapseCapturedFileToMono(destPath);
const bool collapsedToMono = collapseCapturedFileToMono(destPath);
// Pure recorded-capture -> Sample mapping (identity, bounds echo, tier).
RecordedCapture cap;
@@ -225,7 +225,8 @@ CaptureResult finalizeRecording(ReaProject* proj, MediaTrack* temp,
std::to_string(request.startSeconds) + "s, " +
std::to_string(request.endSeconds) + "s] (recorded " +
std::to_string(result.sample.lengthSeconds) + "s) -> " +
paths.relativePath;
paths.relativePath +
(collapsedToMono ? " (collapsed to mono)" : "");
return result;
}