// render_in_place.cpp — see render_in_place.h. // // 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/render_in_place.h" #include #include #include "core/capture/capture_name.h" // captureTrackName #include "core/capture/insert_plan.h" // computeInsertMode / InsertOptions #include "core/capture/render_settings.h" // CaptureScope #include "core/capture/tail_control.h" // TailSetting #include "core/capture/track_topology.h" // siblingPlacement #include "core/view/view_mode_model.h" // kArrangeModeId / kDesignModeId #include "shell/capture/capture.h" #include "shell/capture/capture_orchestrator.h" // renderOffline #include "shell/capture/item_read.h" // itemGuid #include "shell/capture/scope_resolve.h" // ResolveScopeSource / trackName #include "shell/capture/track_guid.h" // guidString #include "shell/panel/panel_input.h" // bankPanelTailSetting #include "shell/persist/session.h" #include "shell/view/view.h" // applyMode / mintManagedLanes #include "shell/view/view_fx_park.h" // drainDeferredFxParks #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_CountTrackMediaItems #define REAPERAPI_WANT_CountTracks #define REAPERAPI_WANT_EnumProjects #define REAPERAPI_WANT_GetCursorPosition #define REAPERAPI_WANT_GetMediaTrackInfo_Value #define REAPERAPI_WANT_GetProjectPathEx #define REAPERAPI_WANT_GetSetMediaTrackInfo_String #define REAPERAPI_WANT_GetTrack #define REAPERAPI_WANT_GetTrackColor #define REAPERAPI_WANT_GetTrackMediaItem #define REAPERAPI_WANT_InsertMedia #define REAPERAPI_WANT_InsertTrackInProject #define REAPERAPI_WANT_SetEditCurPos #define REAPERAPI_WANT_SetMediaTrackInfo_Value #define REAPERAPI_WANT_SetOnlyTrackSelected #define REAPERAPI_WANT_ShowConsoleMsg #define REAPERAPI_WANT_TrackList_AdjustWindows #define REAPERAPI_WANT_Undo_BeginBlock2 #define REAPERAPI_WANT_Undo_EndBlock2 #include "reaper_plugin_functions.h" namespace reasampler::capture { namespace { void refuse(const std::string& why) { ShowConsoleMsg(("ReaSampler render in place: " + why + "\n").c_str()); } // Every track's I_FOLDERDEPTH in track order — the flat delta list the pure // sibling arithmetic reads. std::vector folderDepths(ReaProject* proj, int count) { std::vector depths; depths.reserve(static_cast(count < 0 ? 0 : count)); for (int i = 0; i < count; ++i) { MediaTrack* tr = GetTrack(proj, i); depths.push_back(tr ? static_cast( GetMediaTrackInfo_Value(tr, "I_FOLDERDEPTH")) : 0); } return depths; } int indexOfTrack(ReaProject* proj, int count, MediaTrack* wanted) { for (int i = 0; i < count; ++i) if (GetTrack(proj, i) == wanted) return i; return -1; } void setTrackName(MediaTrack* tr, const std::string& name) { // GetSetMediaTrackInfo_String takes a writable buffer even on the set path. std::vector buf(name.begin(), name.end()); buf.push_back('\0'); GetSetMediaTrackInfo_String(tr, "P_NAME", buf.data(), true); } } // namespace void RunRenderTrackInPlace(ReaSamplerSession& session) { ResolvedSource src; std::string why; if (!ResolveScopeSource(CaptureScope::Track, src, why)) { refuse(why); return; } if (src.sourceTracks.empty() || !src.sourceTracks.front()) { refuse("no source track resolved"); return; } const TailSetting tail = bankPanelTailSetting(); const CaptureName name = captureNameFor(src.trackNames, /*ordinal=*/0, "capture"); CaptureRequest req; req.sourceMode = SourceMode::SelectedTracks; req.startSeconds = src.startSeconds; // exact bounds — no rounding req.endSeconds = src.endSeconds; req.wetDry = 1.0; req.tailMode = tail.mode; req.tailMs = tail.manualMs; req.sampleRate = 0; // follow project rate req.channelCount = 2; req.bitDepth = WavBitDepth::Float32; req.baseName = name.stemBase; req.displayName = name.label; req.destination = CaptureDestination::ProjectMedia; // trackGuids left empty: they exist to stamp provenance onto a Sample this verb // discards. A multi-track selection is refused inside renderOffline, keyed on the // render source, so there is no check to add here. const CaptureResult res = renderOffline(CaptureScope::Track, src.sourceTracks, req); if (res.status != CaptureStatus::Ok) { refuse(res.message); return; } MediaTrack* source = src.sourceTracks.front(); ReaProject* proj = EnumProjects(-1, nullptr, 0); const int trackCount = CountTracks(proj); const int srcIndex = indexOfTrack(proj, trackCount, source); if (srcIndex < 0) { refuse("the source track is no longer in the project; the render landed at " + res.absolutePath + " but was not placed."); return; } const SiblingPlacement place = siblingPlacement(folderDepths(proj, trackCount), srcIndex); // Read ONCE, and only to reapply the mode / decide whether the result landed // visible — never to choose a tag. Both tags below are absolute. const std::string activeMode = session.view().activeModeId(); Undo_BeginBlock2(nullptr); // flags = 0, never 1: flags&1 adds default envelopes/FX, and a default chain // would process a render that already carries the source's FX a second time. InsertTrackInProject(proj, place.insertIndex, /*flags=*/0); MediaTrack* fresh = GetTrack(proj, place.insertIndex); if (!fresh) { // InsertTrackInProject already mutated the project by this point, so the // "no ext-state write -> discard" idiom does not apply here — a discard would // leave the orphaned track un-undoable. Undo_EndBlock2(nullptr, "ReaSampler: render in place (failed to create result track)", -1); refuse("could not create the result track; the render landed at " + res.absolutePath + " but was not placed."); return; } // Both writes or none — one alone lands the new track at the wrong nesting level, // which is audible in both directions (see siblingPlacement). if (place.precedingIndex >= 0) { if (MediaTrack* preceding = GetTrack(proj, place.precedingIndex)) SetMediaTrackInfo_Value(preceding, "I_FOLDERDEPTH", static_cast(place.precedingDepth)); } SetMediaTrackInfo_Value(fresh, "I_FOLDERDEPTH", static_cast(place.newDepth)); // GetTrackColor returns the colour already OR'd with 0x1000000 and 0 for "no // colour set", which I_CUSTOMCOLOR reads as unused — so one line clones a colour // and the absence of one, with no branch. SetMediaTrackInfo_Value(fresh, "I_CUSTOMCOLOR", static_cast(GetTrackColor(source))); const std::string freshName = captureTrackName(trackName(source)); setTrackName(fresh, freshName); // After every attribute write, per the SDK header's manual-panel-update caveat. TrackList_AdjustWindows(false); // Unsnapped and unrounded, deliberately: this placement IS the null test performed // automatically, so snapping it to the grid would move the audio off the position // it was rendered from. InsertOptions{} defaults give native length and no conform. const double cursorPos = GetCursorPosition(); SetOnlyTrackSelected(fresh); SetEditCurPos(src.startSeconds, false, false); // InsertMedia's int return isn't SDK-documented; treated conservatively as // 0 = failure, matching performArrangeDrop — an empty result track would // otherwise be a silent no-op, which is exactly what this verb must not produce. const bool placed = InsertMedia(res.absolutePath.c_str(), computeInsertMode(InsertOptions{})) != 0; SetEditCurPos(cursorPos, false, false); // The new track is left selected, alone — in the headline case the source is being // parked out of sight in the same gesture, so restoring the selection would leave // the user selecting an invisible track. // Absolute, not mode-following: the source parks on the bench, the result is an // Arrange member whatever mode was active. Explicit records rather than untag(), // because the record is what the panel's auto-tag detector defers to. MembershipIndex& membership = session.view().membership(); membership.tag(guidString(source), kDesignModeId); membership.tag(guidString(fresh), kArrangeModeId); // The track is brand new, so its items are exactly the ones just placed. An // untagged item would be handed to the detector, which tags to the active mode. const int itemCount = CountTrackMediaItems(fresh); for (int i = 0; i < itemCount; ++i) { if (MediaItem* it = GetTrackMediaItem(fresh, i)) { const std::string ig = itemGuid(it); if (!ig.empty()) membership.tag(ig, kArrangeModeId); } } mintManagedLanes(session.view(), nullptr); applyMode(session.view(), activeMode, nullptr); // a reapply, never a switch Undo_EndBlock2(nullptr, "ReaSampler: render selected track to a new track", -1); // Persist outside the block. The offline render's own save gate already forced a // saved project, so the Save-As-guarded persist the Design View actions need // cannot have anything to prompt for here. drainDeferredFxParks(); // the reapply above may have deferred a restore — see the contract there session.saveToActiveProject(); if (!placed) { refuse("the render landed at " + res.absolutePath + " but REAPER refused to place it — the new track is empty."); return; } // Silent on success — the new track is the feedback. Except when it is not: fired // outside Arrange the result track is parked, so a silent success would be // indistinguishable from a no-op. if (activeMode != kArrangeModeId) { ShowConsoleMsg(("ReaSampler render in place: created \"" + freshName + "\" in Arrange (switch to Arrange to see it).\n") .c_str()); } } } // namespace reasampler::capture