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).
This commit is contained in:
2026-07-27 14:18:10 -04:00
parent 8576832c84
commit 3a95ce61fe
3 changed files with 58 additions and 89 deletions
+57 -32
View File
@@ -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);
-44
View File
@@ -6,7 +6,6 @@
#include "instrument_drop_win.h"
#include <string>
#include <vector>
#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<char> 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
+1 -13
View File
@@ -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