From 3a95ce61fe852d7b5a2d14f3821affdd5155c155 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Mon, 27 Jul 2026 14:18:10 -0400 Subject: [PATCH] refactor(ingest): ME-import adds RS9k to selected track, not a new track No track is created; no routing changes. No selected track -> bank import proceeds, instrument placement is refused with a console message explaining why. createTrackWithInstrument removed (was sole caller). --- src/ingest.cpp | 89 ++++++++++++++++++++++++------------- src/instrument_drop_win.cpp | 44 ------------------ src/instrument_drop_win.h | 14 +----- 3 files changed, 58 insertions(+), 89 deletions(-) diff --git a/src/ingest.cpp b/src/ingest.cpp index cac3177..7ff4eae 100644 --- a/src/ingest.cpp +++ b/src/ingest.cpp @@ -23,7 +23,7 @@ #include "bank_panel.h" // bankPanelRefresh #include "capture_paths.h" // deriveBankPaths / projectDirOfRpp / hashWavContent #include "instrument_drop.h" // pure buildInstrumentDropChunk (vst_chunk blob for a sampleId) -#include "instrument_drop_win.h" // shell createTrackWithInstrument (new track + pre-loaded RS9k) +#include "instrument_drop_win.h" // shell loadInstrumentOntoTrack (FX add+inject, no own undo block) #include "persist.h" // ReaSamplerSession #include "wav_trim.h" // parseWavLayout — 32f-float WAV validator for the fast path @@ -41,6 +41,7 @@ #define REAPERAPI_WANT_GetMediaSourceLength #define REAPERAPI_WANT_Undo_BeginBlock2 #define REAPERAPI_WANT_Undo_EndBlock2 +#define REAPERAPI_WANT_GetSelectedTrack #include "reaper_plugin_functions.h" namespace reasampler { @@ -419,20 +420,25 @@ ImportResult importFileIntoActiveBank(const std::string& absoluteSourcePath) { // --- Media-Explorer import action -------------------------------------------- // Import the Media Explorer's current last-played/selected file into the active bank, then -// stand up a NEW ReaSampler 9000 instrument on a NEW track pre-loaded with JUST that sound -// (S8 surface 2, RS5k "load into a new sampler" parity). This is the ADD-a-new-sound workflow: -// it NEVER touches (nor force-selects on) an existing live instance — no assignment_request is -// written on this path. Single-file, pull-on-action: MediaExplorerGetLastPlayedFileInfo returns -// the ONE last-played file (the whole ME contract — no enumerate-selected API). The selection -// RANGE it reports is deliberately IGNORED here: an import brings the whole file into the bank -// (the range is a preview hint, and the fields are [0,1] fractions, not seconds — see the -// DAW-verify note); a user wanting a sub-range captures it via the arrange path instead. +// add a ReaSampler 9000 instrument to the FIRST SELECTED TRACK pre-loaded with that sound. +// No new track is created; no routing changes are made — "new sound, existing track." +// No assignment_request is written on this path. // -// LOAD-BEARING (CLAUDE.md): this creates a track + one FX instance ONLY. It NEVER inserts a -// timeline item. Persist ordering is critical — the fresh instance's setState -> reloadFromBank -// reads the bank from project ext-state, so the sample MUST be persisted (generation bumped when -// something new landed) BEFORE createTrackWithInstrument adds the FX, or the instance cannot -// resolve the sampleId. Undo-wrapped: track-create + persist + FX-add + inject = one Ctrl-Z. +// Single-file, pull-on-action: MediaExplorerGetLastPlayedFileInfo returns the ONE last-played +// file (the whole ME contract — no enumerate-selected API). The selection RANGE it reports is +// deliberately IGNORED here: an import brings the whole file into the bank (the range is a +// preview hint, and the fields are [0,1] fractions, not seconds — see the DAW-verify note); +// a user wanting a sub-range captures it via the arrange path instead. +// +// LOAD-BEARING (CLAUDE.md): this adds ONE FX instance to the user's existing selected track. +// It NEVER inserts a timeline item and NEVER creates a track. Persist ordering is critical — +// the fresh instance's setState -> reloadFromBank reads the bank from project ext-state, so +// the sample MUST be persisted (generation bumped when something new landed) BEFORE +// loadInstrumentOntoTrack adds the FX, or the instance cannot resolve the sampleId. +// Undo-wrapped: persist + FX-add + inject = one Ctrl-Z. +// +// No selected track: the bank import still proceeds (sound is now in the bank), but no +// instrument is placed and a clear console message explains why. void doImportFromMediaExplorer() { // filemode/sel/pitch/vol/rate/bpm/extrainfo are read but only the filename is used for // the import. selstart/selend are [0,1] fractions (SDK header) — a preview hint, not a @@ -456,18 +462,37 @@ void doImportFromMediaExplorer() { const ImportResult r = importFileIntoActiveBank(path); if (r.sampleId.empty()) { // Import refused (unsaved project / undecodable / write failure). Report and stop — - // no track is created, so there is no orphan to clean up (createTrackWithInstrument was - // never reached). + // no instrument is placed. ShowConsoleMsg(("ReaSampler ingest: Media Explorer import failed -- " + r.message + ".\n").c_str()); return; } - // The new track's name follows the imported sound (RS5k parity). The source file stem is - // what importFileIntoActiveBank uses for the Sample's displayName, so it names the track - // consistently for both a fresh import and a dedup hit. - const std::string trackName = - std::filesystem::path(path).stem().string(); + // Resolve the first selected track. GetSelectedTrack(nullptr, 0): proj=nullptr=active + // project, seltrackidx=0=first selected (ignores master). Returns null when nothing is + // selected — directive: existing track only, never alter the graph. + MediaTrack* target = GetSelectedTrack(nullptr, 0); + if (!target) { + // Sound landed in the bank; no instrument placed because there is no selected track. + // The bank import is kept (sound is available in the bank browser) and generation is + // bumped so any open VST3 browser instances refresh to show the new sound. + if (r.added) { + Undo_BeginBlock2(nullptr); + g_session->bumpBankGeneration(); + const bool persisted = g_session->saveToActiveProject(); + if (persisted) + Undo_EndBlock2(nullptr, "ReaSampler: import Media Explorer file into bank", + UNDO_STATE_MISCCFG); + else + Undo_EndBlock2(nullptr, "", 0); + bankPanelRefresh(); + } + ShowConsoleMsg(("ReaSampler ingest: " + r.message + + " -- select a track first, then import into it " + "(sound is in the bank but no instrument was placed because " + "no track was selected).\n").c_str()); + return; + } // Build the pre-loaded instrument blob for the resolved sampleId. Valid for BOTH the fresh // import and the dedup case (added == false but a real sampleId) — the user asked for a @@ -475,7 +500,7 @@ void doImportFromMediaExplorer() { const std::string chunk = buildInstrumentDropChunk(r.sampleId); // One undo point for the whole gesture. Persist happens INSIDE the block and BEFORE the - // track/FX so the new instance's setState -> reloadFromBank sees the just-persisted sample. + // FX add so the new instance's setState -> reloadFromBank sees the just-persisted sample. // The generation is bumped only when something NEW landed (a dedup collapse mutated nothing, // so it needs neither a bump nor a persist to resolve — the sample is already in ext-state). // If saveToActiveProject() no-ops (unsaved project), close with an empty label + zero flag so @@ -490,23 +515,23 @@ void doImportFromMediaExplorer() { g_session->bumpBankGeneration(); persisted = g_session->saveToActiveProject(); } - MediaTrack* newTrack = createTrackWithInstrument(trackName, chunk); - if (newTrack && persisted) - Undo_EndBlock2(nullptr, "ReaSampler: import from Media Explorer into new instrument", + const bool placed = loadInstrumentOntoTrack(target, chunk); + if (placed && persisted) + Undo_EndBlock2(nullptr, "ReaSampler: import from Media Explorer into selected track", UNDO_STATE_MISCCFG); else - // Either the instrument-side add/inject failed (createTrackWithInstrument already rolled - // its own track back — no orphan) or the project was unsaved (persist no-op): discard the - // undo entry so no empty point is recorded. + // Either the FX add/inject failed (loadInstrumentOntoTrack already rolled the FX back — + // no orphan) or the project was unsaved (persist no-op): discard the undo entry so no + // empty point is recorded. Undo_EndBlock2(nullptr, "", 0); bankPanelRefresh(); - if (newTrack) + if (placed) ShowConsoleMsg(("ReaSampler ingest: " + r.message + - " (loaded into a new instrument on a new track).\n").c_str()); + " (loaded into a new instrument on the selected track).\n").c_str()); else ShowConsoleMsg(("ReaSampler ingest: imported to the bank (" + r.message + - ") but could not create the new instrument track.\n").c_str()); + ") but could not add the instrument to the selected track.\n").c_str()); } } // namespace @@ -587,7 +612,7 @@ void ingestRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session g_idImportStr = channelCommandId(kIdImportMediaExplorer); g_cmdImportMediaExplorer = rec->Register("command_id", (void*)g_idImportStr.c_str()); if (g_cmdImportMediaExplorer) { - g_labelImportStr = channelActionName("import Media Explorer file into new instrument"); + g_labelImportStr = channelActionName("import Media Explorer file into selected track"); g_accelImportMediaExplorer.accel.cmd = g_cmdImportMediaExplorer; g_accelImportMediaExplorer.desc = g_labelImportStr.c_str(); rec->Register("gaccel", (void*)&g_accelImportMediaExplorer); diff --git a/src/instrument_drop_win.cpp b/src/instrument_drop_win.cpp index 2c38b73..9a1d487 100644 --- a/src/instrument_drop_win.cpp +++ b/src/instrument_drop_win.cpp @@ -6,7 +6,6 @@ #include "instrument_drop_win.h" #include -#include #include "app_version.h" // vstPluginName() — the CHANNEL-correct FX name (stable/beta pairing) #include "instrument_drop.h" // infoNamesFxHotspot — the PURE, unit-tested hotspot classifier @@ -20,12 +19,6 @@ #define REAPERAPI_WANT_TrackFX_SetNamedConfigParm #define REAPERAPI_WANT_Undo_BeginBlock2 #define REAPERAPI_WANT_Undo_EndBlock2 -#define REAPERAPI_WANT_CountTracks -#define REAPERAPI_WANT_InsertTrackInProject -#define REAPERAPI_WANT_GetTrack -#define REAPERAPI_WANT_DeleteTrack -#define REAPERAPI_WANT_GetSetMediaTrackInfo_String -#define REAPERAPI_WANT_TrackList_AdjustWindows #include "reaper_plugin_functions.h" namespace reasampler { @@ -88,41 +81,4 @@ bool performInstrumentDrop(MediaTrack* track, const std::string& chunkBase64) { return ok; } -MediaTrack* createTrackWithInstrument(const std::string& trackName, - const std::string& chunkBase64) { - if (chunkBase64.empty()) return nullptr; - - // Append a new track at the end of the active project (proj=0). flags=0: no default FX/ - // envelopes — we add exactly the one instrument ourselves. InsertTrackInProject returns - // void, so re-fetch the appended track by its (now-last) zero-based index. The index BEFORE - // insertion equals the new track's index AFTER insertion (append at the tail). - const int newIndex = CountTracks(nullptr); - InsertTrackInProject(nullptr, newIndex, /*flags=*/0); - MediaTrack* track = GetTrack(nullptr, newIndex); - if (!track) return nullptr; // insertion did not yield a fetchable track — nothing to clean up - - // Name the track after the imported sound (RS5k parity). P_NAME writes are propagated to - // the panels by TrackList_AdjustWindows below. GetSetMediaTrackInfo_String takes a non-const - // buffer even when setting, so copy into a mutable vector. - if (!trackName.empty()) { - std::vector nameBuf(trackName.begin(), trackName.end()); - nameBuf.push_back('\0'); - GetSetMediaTrackInfo_String(track, "P_NAME", nameBuf.data(), /*setNewValue=*/true); - } - - // Add + inject the pre-loaded instrument. No inner undo block — the caller owns the grouping - // so track-create + persist + FX collapse to one Ctrl-Z. All-or-nothing on failure. - if (!loadInstrumentOntoTrack(track, chunkBase64)) { - // Roll back the track we just created so the project is left exactly as it was (no - // orphaned empty track). DeleteTrack removes the track and everything on it. - DeleteTrack(track); - TrackList_AdjustWindows(false); - return nullptr; - } - - // Repaint the TCP/MCP so the new named track + its FX appear immediately. - TrackList_AdjustWindows(false); - return track; -} - } // namespace reasampler diff --git a/src/instrument_drop_win.h b/src/instrument_drop_win.h index 1034ced..11ea7d2 100644 --- a/src/instrument_drop_win.h +++ b/src/instrument_drop_win.h @@ -57,21 +57,9 @@ bool performInstrumentDrop(MediaTrack* track, const std::string& chunkBase64); // Add a fresh ReaSampler 9000 instance to `track` and inject `chunkBase64` as its component // state. Same all-or-nothing add+inject contract as performInstrumentDrop (rolls the FX back // via TrackFX_Delete on inject failure), but does NOT open its own undo block — the caller owns -// the undo grouping so the whole gesture (track-create + persist + FX-add + inject) collapses to +// the undo grouping so the whole gesture (persist + FX-add + inject) collapses to // one Ctrl-Z. This is the shared inner half performInstrumentDrop wraps in its own block. // Returns true on success, false on any failure. NEVER inserts a timeline item. bool loadInstrumentOntoTrack(MediaTrack* track, const std::string& chunkBase64); -// Create a NEW track at the end of the active project's track list, name it `trackName`, add a -// fresh ReaSampler 9000 instance pre-loaded with `chunkBase64` (RS5k-parity "load into a new -// sampler"), and return the created track. Does NOT open an undo block — the caller owns the -// undo grouping (track-create + the caller's bank persist collapse to one Ctrl-Z). All-or- -// nothing on the instrument side: if the FX add or chunk inject fails, the freshly-created -// track is deleted before returning nullptr, leaving the project exactly as it was (no orphaned -// empty track). NEVER inserts a timeline item; the ONLY mutations are one track + one FX -// instance + that instance's state, all REAPER-undoable. Returns the new track on success, -// nullptr on any failure. -MediaTrack* createTrackWithInstrument(const std::string& trackName, - const std::string& chunkBase64); - } // namespace reasampler