// 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