104 lines
4.1 KiB
C++
104 lines
4.1 KiB
C++
// capture_realtime.cpp — pure logic for the realtime-record backend. See header.
|
|
// Unit-tested by tests/test_capture_realtime.cpp.
|
|
|
|
#include "core/capture/capture_realtime.h"
|
|
|
|
namespace reasampler::capture {
|
|
|
|
RecordModePlan recordModePlanFor(int channelCount, OutputTap tap) {
|
|
RecordModePlan p;
|
|
|
|
// REAPER's output-record modes are mono/stereo only; >2 channels still
|
|
// records stereo-out (a >2-channel realtime capture is out of scope).
|
|
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) {
|
|
return (wetDry >= 1.0) ? OutputTap::PostFader : OutputTap::PreFx;
|
|
}
|
|
|
|
Sample sampleFromRecordedCapture(const RecordedCapture& cap) {
|
|
Sample s;
|
|
// Relative path tail included so two same-tag captures (shouldn't happen) still differ.
|
|
s.id = "cap-" + cap.uniqueTag + "-" + cap.relativePath;
|
|
s.displayName = cap.displayName;
|
|
s.relativePath = cap.relativePath;
|
|
s.sourceMode = cap.sourceMode;
|
|
s.sourceRange.startSeconds = cap.startSeconds;
|
|
s.sourceRange.endSeconds = cap.endSeconds;
|
|
// PPQ/beats deferred (musical-placement concern), as offline.
|
|
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; // 0/0 = unstamped
|
|
s.captureTimeSigDenom = cap.captureTimeSigDenom;
|
|
s.tier = Tier::Scratch;
|
|
// contentHash is left empty: this mapping runs before the file exists on
|
|
// disk; the shell patches the hash in after the move+trim.
|
|
// rootNote/loop left empty: a realtime record of wet output isn't a single
|
|
// played note, so no root note is derivable; loop points are a later action.
|
|
s.createdTimestamp = cap.createdTimestamp;
|
|
return s;
|
|
}
|
|
|
|
RecordPhase advanceRecordPhase(RecordPhase current,
|
|
const RecordTickInputs& inputs,
|
|
double rangeStartSeconds,
|
|
double rangeEndSeconds) {
|
|
switch (current) {
|
|
case RecordPhase::Recording: {
|
|
// Stopped early (user or REAPER) -> finalize what was captured so far.
|
|
if (!inputs.transport.recording) return RecordPhase::Finalizing;
|
|
|
|
// >= (not >): a cursor landing exactly on the end completes.
|
|
if (inputs.transport.playPosition >= rangeEndSeconds)
|
|
return RecordPhase::Finalizing;
|
|
|
|
// Self-defense: a stuck/looping transport that never reaches end would
|
|
// otherwise stay in Recording forever, leaking the temp track + armed sink.
|
|
const double ceiling =
|
|
(rangeEndSeconds - rangeStartSeconds) + kRecordMarginSeconds;
|
|
if (inputs.elapsedSeconds > ceiling) return RecordPhase::Finalizing;
|
|
|
|
return RecordPhase::Recording;
|
|
}
|
|
|
|
case RecordPhase::Finalizing: {
|
|
// Moving the file before it's stable would race REAPER's flush and
|
|
// yield a truncated/missing capture.
|
|
if (inputs.fileReady) return RecordPhase::Done;
|
|
|
|
if (inputs.finalizingSeconds > kFinalizeFlushCeilingSeconds)
|
|
return RecordPhase::Failed;
|
|
|
|
return RecordPhase::Finalizing;
|
|
}
|
|
|
|
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
|