2a9ab65944
Corrected the Render-dialog source name the refusal's evidence depends on, added a by-ear content check and a file-size channel proxy, and stopped two comments from overclaiming.
234 lines
10 KiB
C++
234 lines
10 KiB
C++
// capture_realtime_finalize.cpp — recorded-file discovery, move-into-bank, the
|
|
// Auto-tail decay-scan trim, and finished-Sample population. See the header.
|
|
//
|
|
// Compiled into the reaper_reasampler module. Includes reaper_plugin_functions.h
|
|
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
|
|
// pointers; here they are extern.
|
|
|
|
#include "shell/capture/capture_realtime_finalize.h"
|
|
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "core/audio/peaks.h" // lastFrameAboveThreshold
|
|
#include "core/capture/capture_realtime.h" // RecordedCapture, sampleFromRecordedCapture
|
|
#include "core/capture/render_settings.h" // autoTrimEndRatio
|
|
#include "core/capture/wav_codec.h" // parseWavLayout, extractFloatFrames, planWavTruncate, patchU32LE
|
|
#include "core/util/file_bytes.h" // shared whole-file loader
|
|
|
|
#define REAPERAPI_MINIMAL
|
|
#define REAPERAPI_WANT_GetTrackNumMediaItems
|
|
#define REAPERAPI_WANT_GetTrackMediaItem
|
|
#define REAPERAPI_WANT_GetMediaItemTake
|
|
#define REAPERAPI_WANT_GetMediaItemTake_Source
|
|
#define REAPERAPI_WANT_GetMediaSourceFileName
|
|
#include "reaper_plugin_functions.h"
|
|
|
|
namespace reasampler::capture {
|
|
|
|
namespace {
|
|
|
|
std::string normSlashes(std::string s) {
|
|
for (char& c : s) if (c == '\\') c = '/';
|
|
if (s.size() > 1 && s.back() == '/') s.pop_back();
|
|
return s;
|
|
}
|
|
|
|
// Auto-mode PCM decay-scan trim (docs/product/capture-tail.md §realtime): after the
|
|
// recorded file is moved into the bank (the file we OWN, never the project), scan
|
|
// the tail (frames after the original range end) backward for the last frame above
|
|
// -72 dB and truncate there. Rules:
|
|
// * no tail frame above -72 dB -> trim back to the range end
|
|
// * signal never drops below -72 dB -> keep the full window (cap did its job)
|
|
// * otherwise -> trim one frame past the last audible
|
|
//
|
|
// Returns the trimmed length in seconds, or negative for "no trim applied". Any
|
|
// unreadable/unknown/short file skips the trim rather than risk corrupting the
|
|
// capture — this is a convenience path, not a correctness one.
|
|
//
|
|
// Assumes the recorded file is a canonical 32-bit float WAV, fully flushed/closed
|
|
// before this runs (tick()'s Finalizing size-stable wait guarantees that on the
|
|
// normal path; abort()'s best-effort finalize can race it).
|
|
double trimAutoTailInPlace(const std::string& path,
|
|
double rangeStartSeconds,
|
|
double rangeEndSeconds) {
|
|
constexpr double kNoTrim = -1.0;
|
|
|
|
std::vector<std::uint8_t> bytes = util::readFileBytes(path);
|
|
if (bytes.empty()) return kNoTrim;
|
|
|
|
const WavLayout layout = parseWavLayout(bytes);
|
|
if (!layout.valid || layout.sampleRate == 0) return kNoTrim; // not a WAV we trim
|
|
|
|
const std::size_t totalFrames = layout.frameCount();
|
|
if (totalFrames == 0) return kNoTrim;
|
|
|
|
// Range end as a frame index (frame 0 == start), using the file's own sample
|
|
// rate (authoritative — the request rate may be 0 = follow project). Clamped to
|
|
// the file so a rounding overshoot cannot exceed it.
|
|
const double rangeSeconds = rangeEndSeconds - rangeStartSeconds;
|
|
if (rangeSeconds <= 0.0) return kNoTrim;
|
|
std::size_t rangeEndFrame = static_cast<std::size_t>(
|
|
rangeSeconds * static_cast<double>(layout.sampleRate) + 0.5);
|
|
if (rangeEndFrame > totalFrames) rangeEndFrame = totalFrames;
|
|
|
|
if (rangeEndFrame >= totalFrames) return kNoTrim; // tail window was empty
|
|
|
|
// Scan only the tail region — the trim never eats into the range body.
|
|
const std::size_t tailFrames = totalFrames - rangeEndFrame;
|
|
const std::vector<AudioSample> tailPcm =
|
|
extractFloatFrames(bytes, layout, rangeEndFrame, tailFrames);
|
|
if (tailPcm.empty()) return kNoTrim;
|
|
|
|
const float threshold = static_cast<float>(autoTrimEndRatio());
|
|
const std::size_t lastAbove = audio::lastFrameAboveThreshold(
|
|
tailPcm, layout.channelCount, tailFrames, threshold);
|
|
|
|
// keptFrames: the trimmed file's total frame count. No audible tail frame -> trim
|
|
// back to rangeEndFrame; an audible frame at idx -> keep through that frame. The
|
|
// "never drops below threshold" case falls out naturally: lastAbove is the final
|
|
// tail frame, so keptFrames == totalFrames.
|
|
std::size_t keptFrames;
|
|
if (lastAbove == audio::kNoFrameAboveThreshold) {
|
|
keptFrames = rangeEndFrame;
|
|
} else {
|
|
keptFrames = rangeEndFrame + (lastAbove + 1);
|
|
}
|
|
if (keptFrames >= totalFrames) return kNoTrim; // full window kept -> no truncate
|
|
|
|
const WavTruncatePlan plan = planWavTruncate(layout, keptFrames);
|
|
if (!plan.valid) return kNoTrim;
|
|
|
|
// Patch RIFF + data size fields to the kept frame count (wav_codec's patch
|
|
// primitive — the one RIFF owner), then rewrite the file as exactly the first
|
|
// newFileByteLength bytes. A single truncating write avoids a separate resize
|
|
// step and any partial-write window where on-disk sizes and length disagree.
|
|
patchU32LE(bytes, plan.dataSizeFieldOffset, plan.newDataSize);
|
|
patchU32LE(bytes, plan.riffSizeFieldOffset, plan.newRiffSize);
|
|
|
|
// A mid-write failure (full disk, yanked drive) would leave a short file while we
|
|
// return kNoTrim, overstating the Sample length. Vanishingly unlikely for a
|
|
// just-recorded local file, and this is a convenience path, so a temp-file+
|
|
// atomic-rename isn't warranted; flagged rather than built.
|
|
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
|
if (!out) return kNoTrim; // could not reopen to rewrite — leave the full file
|
|
out.write(reinterpret_cast<const char*>(bytes.data()),
|
|
static_cast<std::streamsize>(plan.newFileByteLength));
|
|
if (!out) return kNoTrim;
|
|
out.close();
|
|
|
|
// The trimmed length in seconds for the Sample metadata.
|
|
return static_cast<double>(keptFrames) / static_cast<double>(layout.sampleRate);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::string recordedFilePath(MediaTrack* temp) {
|
|
if (!temp) return {};
|
|
if (GetTrackNumMediaItems(temp) <= 0) return {};
|
|
MediaItem* item = GetTrackMediaItem(temp, 0);
|
|
if (!item) return {};
|
|
MediaItem_Take* take = GetMediaItemTake(item, 0);
|
|
if (!take) return {};
|
|
PCM_source* src = GetMediaItemTake_Source(take);
|
|
if (!src) return {};
|
|
std::vector<char> buf(4096, '\0');
|
|
GetMediaSourceFileName(src, buf.data(), static_cast<int>(buf.size()));
|
|
return normSlashes(std::string(buf.data()));
|
|
}
|
|
|
|
CaptureResult finalizeRecording(ReaProject* proj, MediaTrack* temp,
|
|
const CaptureRequest& request,
|
|
const BankPaths& paths,
|
|
const std::string& uniqueTag,
|
|
double recordWindowEnd) {
|
|
CaptureResult result;
|
|
|
|
const std::string recorded = recordedFilePath(temp);
|
|
if (recorded.empty() || !std::filesystem::exists(recorded)) {
|
|
result.status = CaptureStatus::RenderFailed;
|
|
result.message = "Realtime record produced no file (check transport/record "
|
|
"settings in the DAW).";
|
|
return result;
|
|
}
|
|
|
|
std::error_code ec;
|
|
std::filesystem::create_directories(paths.absoluteDir, ec);
|
|
const std::string destPath = paths.absoluteDir + "/" + paths.fileName;
|
|
std::filesystem::rename(recorded, destPath, ec);
|
|
if (ec) {
|
|
// Cross-volume rename can fail; fall back to copy+remove.
|
|
ec.clear();
|
|
std::filesystem::copy_file(
|
|
recorded, destPath,
|
|
std::filesystem::copy_options::overwrite_existing, ec);
|
|
if (ec) {
|
|
result.status = CaptureStatus::RenderFailed;
|
|
result.message = "Recorded file could not be moved into the bank: " +
|
|
ec.message();
|
|
return result;
|
|
}
|
|
std::error_code rmEc;
|
|
std::filesystem::remove(recorded, rmEc); // best-effort
|
|
}
|
|
|
|
// Only Auto trims; None recorded exact bounds and Manual is a fixed window
|
|
// (spec §The realtime path).
|
|
double trimmedLenSeconds = -1.0;
|
|
if (request.tailMode == TailMode::Auto) {
|
|
trimmedLenSeconds = trimAutoTailInPlace(destPath,
|
|
request.startSeconds,
|
|
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.
|
|
const MonoCollapseOutcome collapseOutcome =
|
|
collapseCapturedFileToMono(destPath, "ReaSampler realtime capture");
|
|
|
|
// Pure recorded-capture -> Sample mapping (identity, bounds echo, tier).
|
|
RecordedCapture cap;
|
|
cap.relativePath = paths.relativePath;
|
|
cap.uniqueTag = uniqueTag;
|
|
cap.sourceMode = SourceMode::Realtime;
|
|
cap.startSeconds = request.startSeconds;
|
|
cap.endSeconds = request.endSeconds;
|
|
cap.wetDry = request.wetDry;
|
|
cap.displayName = request.label();
|
|
cap.trackGuids = request.trackGuids;
|
|
// 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);
|
|
|
|
// Shared finished-capture stamp. timeSigProj = proj: the realtime path pins the
|
|
// record's own project (offline reads the active project instead) — the
|
|
// divergence is kept caller-visible here.
|
|
stampCaptureSample(result.sample, request, /*rateProj=*/proj,
|
|
/*timeSigProj=*/proj, destPath);
|
|
|
|
// The recorded length differs from the request range when a tail was recorded,
|
|
// so lengthSeconds must reflect the file, not the range: trimmed length if Auto
|
|
// trimmed, else the full recorded window (recordWindowEnd - start; equals the
|
|
// exact range when tailMode is None).
|
|
if (trimmedLenSeconds >= 0.0) {
|
|
result.sample.lengthSeconds = trimmedLenSeconds;
|
|
} else {
|
|
result.sample.lengthSeconds = recordWindowEnd - request.startSeconds;
|
|
}
|
|
|
|
result.message = "Realtime-captured [" +
|
|
std::to_string(request.startSeconds) + "s, " +
|
|
std::to_string(request.endSeconds) + "s] (recorded " +
|
|
std::to_string(result.sample.lengthSeconds) + "s) -> " +
|
|
paths.relativePath + monoCollapseSuffix(collapseOutcome);
|
|
return result;
|
|
}
|
|
|
|
} // namespace reasampler::capture
|