feat(ingest): ME import loads a new instrument on a new track

Repurpose INGEST_IMPORT_MEDIA_EXPLORER (id unchanged) to stand up a
fresh ReaSampler 9000 on a new named track pre-loaded with the imported
sound, instead of force-selecting on the active instance. One undo point.
This commit is contained in:
2026-07-27 13:39:18 -04:00
parent 3b9b78b82c
commit 835bddadda
3 changed files with 149 additions and 51 deletions
+61 -32
View File
@@ -22,6 +22,8 @@
#include "bank_model.h" // Sample, AddResult, findByHash
#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 "persist.h" // ReaSamplerSession
#include "wav_trim.h" // parseWavLayout — 32f-float WAV validator for the fast path
@@ -416,13 +418,21 @@ ImportResult importFileIntoActiveBank(const std::string& absoluteSourcePath) {
// --- Media-Explorer import action --------------------------------------------
// Import the Media Explorer's current last-played/selected file into the active bank and
// assign it to the active instance (S8 surface 2). 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. Undo-wrapped.
// 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.
//
// 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.
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
@@ -445,39 +455,58 @@ 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).
ShowConsoleMsg(("ReaSampler ingest: Media Explorer import failed -- " + r.message +
".\n").c_str());
return;
}
// Persist the bank add AND the assign request inside ONE undo block so Ctrl-Z rolls
// back both keys atomically: undo restores `banks` (removing the new sample) AND
// clears the `assign_request` that named it, so no stale request can survive.
// The block is opened only when the index mutated (a dedup collapse changed nothing).
// If saveToActiveProject() no-ops (unsaved project), we close with an empty label +
// zero flag so REAPER discards the undo entry (the house pattern from actions.cpp).
// 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();
// 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
// player, and a valid sampleId is sufficient to pre-select the sound.
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.
// 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
// REAPER discards the undo entry (the house pattern from actions.cpp). importFileIntoActiveBank
// already refuses on an unsaved project, so in practice the persist here succeeds.
Undo_BeginBlock2(nullptr);
bool persisted = true; // true when nothing needed persisting (dedup) — governs the label path
if (r.added) {
Undo_BeginBlock2(nullptr);
// S9: an ingest import adds a sample to the active bank -> bump inside the block so
// the stamped generation refreshes the assigned instance hands-free (and undo rolls
// the generation back with the banks/assign_request keys).
// S9: a new sample landed in the active bank -> bump inside the block so the stamped
// generation is what the fresh instance (and any other live instances) resolve against,
// and undo rolls the generation back with the banks key.
g_session->bumpBankGeneration();
const bool persisted = g_session->saveToActiveProject();
// Assign request inside the same block: undo rolls back both keys together.
ingestAssignActiveInstance(g_session->book().activeBankId(), r.sampleId);
if (persisted)
Undo_EndBlock2(nullptr, "ReaSampler: import from Media Explorer",
UNDO_STATE_MISCCFG);
else
Undo_EndBlock2(nullptr, "", 0);
} else {
// Dedup collapse: index unchanged, no undo point. Assign request still written
// (the user explicitly re-imported; they want the instance updated).
ingestAssignActiveInstance(g_session->book().activeBankId(), r.sampleId);
persisted = g_session->saveToActiveProject();
}
MediaTrack* newTrack = createTrackWithInstrument(trackName, chunk);
if (newTrack && persisted)
Undo_EndBlock2(nullptr, "ReaSampler: import from Media Explorer into new instrument",
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.
Undo_EndBlock2(nullptr, "", 0);
bankPanelRefresh();
ShowConsoleMsg(("ReaSampler ingest: " + r.message + " (assigned to the active "
"instance).\n").c_str());
if (newTrack)
ShowConsoleMsg(("ReaSampler ingest: " + r.message +
" (loaded into a new instrument on a new 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());
}
} // namespace
@@ -572,7 +601,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 bank + assign");
g_labelImportStr = channelActionName("import Media Explorer file into new instrument");
g_accelImportMediaExplorer.accel.cmd = g_cmdImportMediaExplorer;
g_accelImportMediaExplorer.desc = g_labelImportStr.c_str();
rec->Register("gaccel", (void*)&g_accelImportMediaExplorer);