// realtime_record.cpp — pure logic for the realtime-record backend (M8). See header. // NO REAPER types; unit-tested by tests/test_realtime_record.cpp. #include "realtime_record.h" namespace reasampler { 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--" 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