Q-W3: main.cpp → pointers+entry+dispatch via 4 capture hoists; one pure wav_codec RIFF owner; ICaptureBackend deleted; capture_realtime rename + finalize split; shared stampCaptureSample; makeUniqueTag gains monotonic counter (fixes same-second batch collisions). 60/60 green.
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
// capture_realtime.cpp — pure logic for the realtime-record backend (M8). See
|
||||
// header. NO REAPER types; unit-tested by tests/test_capture_realtime.cpp.
|
||||
// (Renamed from realtime_record.cpp in Q-W3 — the Q-9 naming rider.)
|
||||
|
||||
#include "core/capture/capture_realtime.h"
|
||||
|
||||
namespace reasampler::capture {
|
||||
|
||||
RecordModePlan recordModePlanFor(int channelCount, OutputTap tap) {
|
||||
RecordModePlan p;
|
||||
|
||||
// Stereo vs mono output recording, latency-compensated either way so the
|
||||
// recorded file lines up with the source. A request asking for <= 1 channel
|
||||
// records mono-out; anything else records stereo-out. (Higher channel counts
|
||||
// still record stereo-out here — REAPER's output-record modes are mono/stereo
|
||||
// only; a >2-channel realtime capture is out of scope for this increment.)
|
||||
p.recMode = (channelCount <= 1) ? kRecModeMonoOutLatComp
|
||||
: kRecModeStereoOutLatComp;
|
||||
|
||||
switch (tap) {
|
||||
case OutputTap::PostFader: p.recModeFlags = kRecOutPostFader; break;
|
||||
case OutputTap::PreFx: p.recModeFlags = kRecOutPreFx; break;
|
||||
case OutputTap::PostFxPreFader: p.recModeFlags = kRecOutPostFxPreFader; break;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
OutputTap outputTapForWetDry(double wetDry) {
|
||||
// Fully wet (1.0) taps post-fader; any dry-ward value taps pre-FX — the true
|
||||
// pre-FX dry that offline render cannot produce (the realtime backend's whole
|
||||
// reason to exist for the M10 null test). PostFxPreFader is an explicit future
|
||||
// option, not reachable from the wet/dry axis, so it is not returned here.
|
||||
return (wetDry >= 1.0) ? OutputTap::PostFader : OutputTap::PreFx;
|
||||
}
|
||||
|
||||
Sample sampleFromRecordedCapture(const RecordedCapture& cap) {
|
||||
Sample s;
|
||||
// Same id shape as the offline path: "cap-<tag>-<fileName>" would need the file
|
||||
// name; here the recorded file name is the tail of relativePath. Keep the id
|
||||
// stable + unique via the tag, and include the relative path tail so two
|
||||
// captures with the same tag (impossible in practice) still differ.
|
||||
s.id = "cap-" + cap.uniqueTag + "-" + cap.relativePath;
|
||||
s.displayName = cap.displayName;
|
||||
s.relativePath = cap.relativePath; // project-relative (invariant)
|
||||
s.sourceMode = cap.sourceMode;
|
||||
s.sourceRange.startSeconds = cap.startSeconds;
|
||||
s.sourceRange.endSeconds = cap.endSeconds;
|
||||
// PPQ/beats deferred (musical-placement concern) — identical to the offline path.
|
||||
s.wetDry = cap.wetDry;
|
||||
s.trackGuids = cap.trackGuids;
|
||||
s.channelCount = cap.channelCount;
|
||||
s.sampleRate = cap.sampleRate; // 0 when project rate was unknown
|
||||
s.lengthSeconds = cap.endSeconds - cap.startSeconds;
|
||||
s.captureTempo = cap.captureTempo;
|
||||
s.captureTimeSigNum = cap.captureTimeSigNum; // L7 F1 meter stamp (0/0 = unstamped)
|
||||
s.captureTimeSigDenom = cap.captureTimeSigDenom;
|
||||
s.tier = Tier::Scratch; // captures land in scratch by default
|
||||
// contentHash set by the caller (capture_realtime.cpp) after the file is
|
||||
// finalized and on disk — the hash is over the finished file bytes. Left empty
|
||||
// here because sampleFromRecordedCapture runs before the file exists (the
|
||||
// mapping is pure / DAW-free); the shell patches it in after the move+trim.
|
||||
// Phase S seam fields (rootNote / loop) left empty (D-B) — same reasoning as the
|
||||
// offline path: a realtime record of wet output is not a single played note, so
|
||||
// no root note is derivable; loop points are set by a later explicit action.
|
||||
s.createdTimestamp = cap.createdTimestamp;
|
||||
return s;
|
||||
}
|
||||
|
||||
RecordPhase advanceRecordPhase(RecordPhase current,
|
||||
const RecordTickInputs& inputs,
|
||||
double rangeStartSeconds,
|
||||
double rangeEndSeconds) {
|
||||
switch (current) {
|
||||
case RecordPhase::Recording: {
|
||||
// Transport stopped while we still expected to be recording -> the user
|
||||
// (or REAPER) stopped early. Move to the flush wait and finalize whatever
|
||||
// was captured up to the stop.
|
||||
if (!inputs.transport.recording) return RecordPhase::Finalizing;
|
||||
|
||||
// Reached the range end (latency-compensated play position). >= (not >)
|
||||
// so a cursor landing exactly on the end completes.
|
||||
if (inputs.transport.playPosition >= rangeEndSeconds)
|
||||
return RecordPhase::Finalizing;
|
||||
|
||||
// Self-defense (review §3): the transport is running but the play cursor
|
||||
// is not advancing to the end (stuck / looping). Without this the machine
|
||||
// stays in Recording forever, leaking the temp track + armed sink. Force
|
||||
// the flush wait once wall-clock exceeds the nominal duration + margin.
|
||||
const double ceiling =
|
||||
(rangeEndSeconds - rangeStartSeconds) + kRecordMarginSeconds;
|
||||
if (inputs.elapsedSeconds > ceiling) return RecordPhase::Finalizing;
|
||||
|
||||
return RecordPhase::Recording;
|
||||
}
|
||||
|
||||
case RecordPhase::Finalizing: {
|
||||
// The transport is stopped; wait for REAPER to flush/close the recorded
|
||||
// take on the audio thread. Finalize (move + Sample) only once the file
|
||||
// exists AND is stable (review §2) — moving it early races the flush and
|
||||
// yields a truncated / missing capture.
|
||||
if (inputs.fileReady) return RecordPhase::Done;
|
||||
|
||||
// Bound the wait: a file that never stabilizes fails cleanly rather than
|
||||
// hanging the in-flight state for the session.
|
||||
if (inputs.finalizingSeconds > kFinalizeFlushCeilingSeconds)
|
||||
return RecordPhase::Failed;
|
||||
|
||||
return RecordPhase::Finalizing;
|
||||
}
|
||||
|
||||
// Terminal phases are sticky: once the verdict is in, a later tick (a stray
|
||||
// extra call before the shell has finished tearing down) must not flip it.
|
||||
case RecordPhase::Done:
|
||||
case RecordPhase::Failed:
|
||||
default:
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
bool isStopRequested(RecordPhase phase) {
|
||||
return phase != RecordPhase::Recording;
|
||||
}
|
||||
|
||||
bool isTerminalPhase(RecordPhase phase) {
|
||||
return phase == RecordPhase::Done || phase == RecordPhase::Failed;
|
||||
}
|
||||
|
||||
} // namespace reasampler::capture
|
||||
Reference in New Issue
Block a user