Ψ-W2-T2: collapse a capture whose channels are bit-identical to one lossless mono channel, index value measured off the landed file
This commit is contained in:
@@ -51,7 +51,7 @@ detail not covered there:
|
||||
|
||||
## Modules
|
||||
|
||||
- `capture` — two CONCRETE backends with deliberately different lifecycles (no shared interface — the former `ICaptureBackend` was deleted in Q-W3, T4-26: one deriver, zero polymorphic call sites): `OfflineRenderBackend` (deterministic default, synchronous) and `RealtimeRecordBackend` (async begin/tick/abort). Input: `CaptureRequest`. Output: finished file + populated `Sample` handed to `bank_model`.
|
||||
- `capture` — two CONCRETE backends with deliberately different lifecycles (no shared interface — the former `ICaptureBackend` was deleted in Q-W3, T4-26: one deriver, zero polymorphic call sites): `OfflineRenderBackend` (deterministic default, synchronous) and `RealtimeRecordBackend` (async begin/tick/abort). Input: `CaptureRequest`. Output: finished file + populated `Sample` handed to `bank_model`. It also owns the two file-side steps both backends share: `collapseCapturedFileToMono` (the lossless mono collapse, applied to the landed file) and `stampCaptureSample`, which measures the channel count off that same file so the entry and the audio cannot disagree.
|
||||
- `scope_resolve` (`shell/capture`) — scope/source resolution shared by every capture entry point (Q-W3 hoist out of `main.cpp`): razor-else-time range inference, selected-track/selected-item-owning-track collection with canonical GUIDs, and the M10 provenance-assembly inputs (read BEFORE the FX-bypass guard neutralizes the in-scope chain).
|
||||
- `render_selection` (`shell/capture`) — the transient track selection a selected-tracks render (`&128`) requires, as a stack RAII guard: REAPER prints whatever tracks are selected, so `renderOffline` makes the request's own tracks BE the selection for the render's duration and restores the user's set on every exit path. Engaged ONLY for that source mode, which leaves a stated residual: a `&32` selected-items render still prints whatever ITEMS the user has selected. Live captures are unaffected (that selection is the source), but a recipe replay of a `SelectedItems` capture renders against whatever happens to be selected then — the recipe stores tracks and a range, never item GUIDs, so this guard cannot close it. Filed in `docs/TODO.md`.
|
||||
- `render_isolation` (`shell/capture`) — the transient upstream silencing a ranged ITEM render needs, as a stack RAII guard alongside the two above: the selected-tracks source prints everything flowing INTO the track, so each direct folder child's `B_MAINSEND` and each of the track's receives' `B_MUTE` are cut for the render and restored on every exit path. Direct children only — a grandchild reaches the track through the child that owns it. The child-set walk is pure (`core/capture/track_topology`).
|
||||
|
||||
@@ -201,11 +201,27 @@ std::string makeUniqueTag(const std::string& prefix) {
|
||||
std::to_string(++counter);
|
||||
}
|
||||
|
||||
void collapseCapturedFileToMono(const std::string& absolutePath) {
|
||||
const std::vector<std::uint8_t> bytes = util::readFileBytes(absolutePath);
|
||||
if (bytes.empty()) return;
|
||||
|
||||
const MonoCollapse collapse = collapseToMono(bytes);
|
||||
if (!collapse.collapsed) return;
|
||||
|
||||
// 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()));
|
||||
}
|
||||
|
||||
void stampCaptureSample(Sample& s, const CaptureRequest& req,
|
||||
ReaProject* rateProj, ReaProject* timeSigProj,
|
||||
const std::string& absolutePath) {
|
||||
// Track GUIDs + channel count: echoed from the request (the caller resolved
|
||||
// the selection; the backends stay source-agnostic).
|
||||
// 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.
|
||||
s.trackGuids = req.trackGuids;
|
||||
s.channelCount = req.channelCount;
|
||||
|
||||
@@ -238,6 +254,10 @@ void stampCaptureSample(Sample& s, const CaptureRequest& req,
|
||||
const std::vector<std::uint8_t> fileBytes = util::readFileBytes(absolutePath);
|
||||
if (!fileBytes.empty()) {
|
||||
s.contentHash = hashWavContent(fileBytes);
|
||||
// The one authority for the entry's channel count is the file's own `fmt`
|
||||
// — never the render request, which asks for 2 on every capture path.
|
||||
const WavLayout layout = parseWavLayout(fileBytes);
|
||||
if (layout.valid) s.channelCount = static_cast<int>(layout.channelCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,6 +467,14 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Lossless mono collapse, deliberately AFTER the bounds gate: the gate measures
|
||||
// REAPER's own render against the requested window, so nothing of ours may sit
|
||||
// between the render and that measurement, and a refusal must delete the
|
||||
// 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);
|
||||
|
||||
// Record the request's own bounds (exact) rather than re-measuring the file.
|
||||
Sample s;
|
||||
// Same uniqueTag that named the file — calling makeUniqueTag() again could
|
||||
|
||||
@@ -53,6 +53,9 @@ struct CaptureRequest {
|
||||
|
||||
// 0 sampleRate => follow project rate.
|
||||
int sampleRate = 0;
|
||||
// What the RENDER is asked for (RENDER_CHANNELS / the realtime record mode), not
|
||||
// what the capture lands as: a dual-mono render is collapsed to 1 channel after
|
||||
// the fact, and the Sample's count comes from the produced file.
|
||||
int channelCount = 2;
|
||||
WavBitDepth bitDepth = WavBitDepth::Float32;
|
||||
|
||||
@@ -100,8 +103,16 @@ public:
|
||||
// backend's family marker ("" offline, "rt-" realtime).
|
||||
std::string makeUniqueTag(const std::string& prefix);
|
||||
|
||||
// Stamps the metadata shared by both backends onto `s`: trackGuids + channelCount
|
||||
// (echoed from the request), resolved sampleRate (request rate, else PROJECT_SRATE
|
||||
// Rewrites a just-captured WAV in place as a 1-channel file when its channels are
|
||||
// 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);
|
||||
|
||||
// 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
|
||||
// sampleRate (request rate, else PROJECT_SRATE
|
||||
// from `rateProj`), captureTempo, the capture-start time signature
|
||||
// (TimeMap_GetTimeSigAtTime against `timeSigProj` — offline passes nullptr for the
|
||||
// active project, realtime pins the record's own project), the WAV-aware
|
||||
|
||||
@@ -184,6 +184,10 @@ CaptureResult finalizeRecording(ReaProject* proj, MediaTrack* temp,
|
||||
request.endSeconds);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Pure recorded-capture -> Sample mapping (identity, bounds echo, tier).
|
||||
RecordedCapture cap;
|
||||
cap.relativePath = paths.relativePath;
|
||||
@@ -194,7 +198,9 @@ CaptureResult finalizeRecording(ReaProject* proj, MediaTrack* temp,
|
||||
cap.wetDry = request.wetDry;
|
||||
cap.displayName = request.baseName;
|
||||
cap.trackGuids = request.trackGuids;
|
||||
cap.channelCount = request.channelCount;
|
||||
// channelCount deliberately left unset here: stampCaptureSample measures it from
|
||||
// the file below. Echoing the request was this path's own defect — it parsed the
|
||||
// recorded layout for the trim and still reported the requested 2.
|
||||
|
||||
result.status = CaptureStatus::Ok;
|
||||
result.sample = sampleFromRecordedCapture(cap);
|
||||
|
||||
@@ -148,8 +148,9 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
// no-play — no crash, no retry loop.
|
||||
if (const SelectedSample* sel = findRef(refs, selId)) {
|
||||
// Auto-default: channelModeFor computes the mode from the loaded capture's channel
|
||||
// count (always 2 for extension captures; mono only for ingest-imported mono files).
|
||||
// An unknown count (0) or explicit user choice keeps the mode.
|
||||
// count — 1 for an ingested mono file or a capture whose channels came out
|
||||
// bit-identical and collapsed, 2 otherwise. An unknown count (0) or an explicit
|
||||
// user choice keeps the mode.
|
||||
{
|
||||
std::lock_guard<std::mutex> cm(channelModeMutex_);
|
||||
channelMode_ = channelModeFor(sel->channelCount, channelMode_,
|
||||
|
||||
Reference in New Issue
Block a user