Merge ME-import → new RS9k instrument
Repurpose INGEST_IMPORT_MEDIA_EXPLORER to stand up a fresh instrument on a new track pre-loaded with the imported sound, instead of force-selecting on the active instance.
This commit is contained in:
+59
-30
@@ -22,6 +22,8 @@
|
|||||||
#include "bank_model.h" // Sample, AddResult, findByHash
|
#include "bank_model.h" // Sample, AddResult, findByHash
|
||||||
#include "bank_panel.h" // bankPanelRefresh
|
#include "bank_panel.h" // bankPanelRefresh
|
||||||
#include "capture_paths.h" // deriveBankPaths / projectDirOfRpp / hashWavContent
|
#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 "persist.h" // ReaSamplerSession
|
||||||
|
|
||||||
#include "wav_trim.h" // parseWavLayout — 32f-float WAV validator for the fast path
|
#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 --------------------------------------------
|
// --- Media-Explorer import action --------------------------------------------
|
||||||
|
|
||||||
// Import the Media Explorer's current last-played/selected file into the active bank and
|
// Import the Media Explorer's current last-played/selected file into the active bank, then
|
||||||
// assign it to the active instance (S8 surface 2). Single-file, pull-on-action:
|
// stand up a NEW ReaSampler 9000 instrument on a NEW track pre-loaded with JUST that sound
|
||||||
// MediaExplorerGetLastPlayedFileInfo returns the ONE last-played file (the whole ME
|
// (S8 surface 2, RS5k "load into a new sampler" parity). This is the ADD-a-new-sound workflow:
|
||||||
// contract — no enumerate-selected API). The selection RANGE it reports is deliberately
|
// it NEVER touches (nor force-selects on) an existing live instance — no assignment_request is
|
||||||
// IGNORED here: an import brings the whole file into the bank (the range is a preview
|
// written on this path. Single-file, pull-on-action: MediaExplorerGetLastPlayedFileInfo returns
|
||||||
// hint, and the fields are [0,1] fractions, not seconds — see the DAW-verify note); a
|
// the ONE last-played file (the whole ME contract — no enumerate-selected API). The selection
|
||||||
// user wanting a sub-range captures it via the arrange path instead. Undo-wrapped.
|
// 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() {
|
void doImportFromMediaExplorer() {
|
||||||
// filemode/sel/pitch/vol/rate/bpm/extrainfo are read but only the filename is used for
|
// 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
|
// 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);
|
const ImportResult r = importFileIntoActiveBank(path);
|
||||||
if (r.sampleId.empty()) {
|
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 +
|
ShowConsoleMsg(("ReaSampler ingest: Media Explorer import failed -- " + r.message +
|
||||||
".\n").c_str());
|
".\n").c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist the bank add AND the assign request inside ONE undo block so Ctrl-Z rolls
|
// The new track's name follows the imported sound (RS5k parity). The source file stem is
|
||||||
// back both keys atomically: undo restores `banks` (removing the new sample) AND
|
// what importFileIntoActiveBank uses for the Sample's displayName, so it names the track
|
||||||
// clears the `assign_request` that named it, so no stale request can survive.
|
// consistently for both a fresh import and a dedup hit.
|
||||||
// The block is opened only when the index mutated (a dedup collapse changed nothing).
|
const std::string trackName =
|
||||||
// If saveToActiveProject() no-ops (unsaved project), we close with an empty label +
|
std::filesystem::path(path).stem().string();
|
||||||
// zero flag so REAPER discards the undo entry (the house pattern from actions.cpp).
|
|
||||||
if (r.added) {
|
// 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);
|
Undo_BeginBlock2(nullptr);
|
||||||
// S9: an ingest import adds a sample to the active bank -> bump inside the block so
|
bool persisted = true; // true when nothing needed persisting (dedup) — governs the label path
|
||||||
// the stamped generation refreshes the assigned instance hands-free (and undo rolls
|
if (r.added) {
|
||||||
// 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();
|
g_session->bumpBankGeneration();
|
||||||
const bool persisted = g_session->saveToActiveProject();
|
persisted = g_session->saveToActiveProject();
|
||||||
// Assign request inside the same block: undo rolls back both keys together.
|
}
|
||||||
ingestAssignActiveInstance(g_session->book().activeBankId(), r.sampleId);
|
MediaTrack* newTrack = createTrackWithInstrument(trackName, chunk);
|
||||||
if (persisted)
|
if (newTrack && persisted)
|
||||||
Undo_EndBlock2(nullptr, "ReaSampler: import from Media Explorer",
|
Undo_EndBlock2(nullptr, "ReaSampler: import from Media Explorer into new instrument",
|
||||||
UNDO_STATE_MISCCFG);
|
UNDO_STATE_MISCCFG);
|
||||||
else
|
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);
|
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);
|
|
||||||
}
|
|
||||||
bankPanelRefresh();
|
bankPanelRefresh();
|
||||||
ShowConsoleMsg(("ReaSampler ingest: " + r.message + " (assigned to the active "
|
if (newTrack)
|
||||||
"instance).\n").c_str());
|
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
|
} // namespace
|
||||||
@@ -572,7 +601,7 @@ void ingestRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session
|
|||||||
g_idImportStr = channelCommandId(kIdImportMediaExplorer);
|
g_idImportStr = channelCommandId(kIdImportMediaExplorer);
|
||||||
g_cmdImportMediaExplorer = rec->Register("command_id", (void*)g_idImportStr.c_str());
|
g_cmdImportMediaExplorer = rec->Register("command_id", (void*)g_idImportStr.c_str());
|
||||||
if (g_cmdImportMediaExplorer) {
|
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.accel.cmd = g_cmdImportMediaExplorer;
|
||||||
g_accelImportMediaExplorer.desc = g_labelImportStr.c_str();
|
g_accelImportMediaExplorer.desc = g_labelImportStr.c_str();
|
||||||
rec->Register("gaccel", (void*)&g_accelImportMediaExplorer);
|
rec->Register("gaccel", (void*)&g_accelImportMediaExplorer);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "instrument_drop_win.h"
|
#include "instrument_drop_win.h"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "app_version.h" // vstPluginName() — the CHANNEL-correct FX name (stable/beta pairing)
|
#include "app_version.h" // vstPluginName() — the CHANNEL-correct FX name (stable/beta pairing)
|
||||||
#include "instrument_drop.h" // infoNamesFxHotspot — the PURE, unit-tested hotspot classifier
|
#include "instrument_drop.h" // infoNamesFxHotspot — the PURE, unit-tested hotspot classifier
|
||||||
@@ -19,6 +20,12 @@
|
|||||||
#define REAPERAPI_WANT_TrackFX_SetNamedConfigParm
|
#define REAPERAPI_WANT_TrackFX_SetNamedConfigParm
|
||||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
#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"
|
#include "reaper_plugin_functions.h"
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
@@ -39,7 +46,7 @@ FxDropTarget resolveFxDropTarget(int screenX, int screenY) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool performInstrumentDrop(MediaTrack* track, const std::string& chunkBase64) {
|
bool loadInstrumentOntoTrack(MediaTrack* track, const std::string& chunkBase64) {
|
||||||
if (!track || chunkBase64.empty()) return false;
|
if (!track || chunkBase64.empty()) return false;
|
||||||
|
|
||||||
// The CHANNEL-correct FX name: "VST3:ReaSampler 9000" on stable, "VST3:ReaSampler 9000
|
// The CHANNEL-correct FX name: "VST3:ReaSampler 9000" on stable, "VST3:ReaSampler 9000
|
||||||
@@ -48,32 +55,74 @@ bool performInstrumentDrop(MediaTrack* track, const std::string& chunkBase64) {
|
|||||||
// drops the beta VST, a stable extension the stable VST — with no literal to drift.
|
// drops the beta VST, a stable extension the stable VST — with no literal to drift.
|
||||||
const std::string fxName = "VST3:" + vstPluginName();
|
const std::string fxName = "VST3:" + vstPluginName();
|
||||||
|
|
||||||
// One undo point for the whole gesture (mirrors the bank-verb undo discipline). Both the
|
|
||||||
// FX add and the state write are REAPER-undoable, so Ctrl-Z removes the instance cleanly.
|
|
||||||
Undo_BeginBlock2(nullptr);
|
|
||||||
|
|
||||||
// Negative `instantiate` => always create a NEW instance (verified in the header). recFX
|
// Negative `instantiate` => always create a NEW instance (verified in the header). recFX
|
||||||
// = false: a normal track FX chain instance, not a record/monitoring FX.
|
// = false: a normal track FX chain instance, not a record/monitoring FX.
|
||||||
const int fxIndex = TrackFX_AddByName(track, fxName.c_str(), /*recFX=*/false,
|
const int fxIndex = TrackFX_AddByName(track, fxName.c_str(), /*recFX=*/false,
|
||||||
/*instantiate=*/-1);
|
/*instantiate=*/-1);
|
||||||
bool ok = false;
|
if (fxIndex < 0) return false;
|
||||||
if (fxIndex >= 0) {
|
|
||||||
// Inject the instrument's OWN component-state blob (the dragged capture pre-selected)
|
// Inject the instrument's OWN component-state blob (the dragged capture pre-selected)
|
||||||
// via the documented vst_chunk write-parm. The blob was built by the shared writer
|
// via the documented vst_chunk write-parm. The blob was built by the shared writer
|
||||||
// (instrument_drop::buildInstrumentDropChunk -> sample_map::serializeComponentState),
|
// (instrument_drop::buildInstrumentDropChunk -> sample_map::serializeComponentState),
|
||||||
// so these bytes are exactly what ReaSampler 9000's setState accepts.
|
// so these bytes are exactly what ReaSampler 9000's setState accepts.
|
||||||
ok = TrackFX_SetNamedConfigParm(track, fxIndex, "vst_chunk", chunkBase64.c_str());
|
const bool ok =
|
||||||
|
TrackFX_SetNamedConfigParm(track, fxIndex, "vst_chunk", chunkBase64.c_str());
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
// All-or-nothing: if the chunk write fails, remove the empty FX instance we just
|
// All-or-nothing: if the chunk write fails, remove the empty FX instance we just
|
||||||
// added so the track is left exactly as it was. TrackFX_Delete signature (verified
|
// added so the track is left exactly as it was. TrackFX_Delete signature (verified
|
||||||
// in reaper_plugin_functions.h:7236): bool TrackFX_Delete(MediaTrack*, int fx).
|
// in reaper_plugin_functions.h:7236): bool TrackFX_Delete(MediaTrack*, int fx).
|
||||||
TrackFX_Delete(track, fxIndex);
|
TrackFX_Delete(track, fxIndex);
|
||||||
}
|
}
|
||||||
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool performInstrumentDrop(MediaTrack* track, const std::string& chunkBase64) {
|
||||||
|
if (!track || chunkBase64.empty()) return false;
|
||||||
|
|
||||||
|
// One undo point for the whole gesture (mirrors the bank-verb undo discipline). Both the
|
||||||
|
// FX add and the state write are REAPER-undoable, so Ctrl-Z removes the instance cleanly.
|
||||||
|
Undo_BeginBlock2(nullptr);
|
||||||
|
const bool ok = loadInstrumentOntoTrack(track, chunkBase64);
|
||||||
// The undo label reflects the placement-of-the-player framing (not a capture, not an insert).
|
// The undo label reflects the placement-of-the-player framing (not a capture, not an insert).
|
||||||
Undo_EndBlock2(nullptr, "ReaSampler: drop capture onto FX chain", -1);
|
Undo_EndBlock2(nullptr, "ReaSampler: drop capture onto FX chain", -1);
|
||||||
return ok;
|
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
|
} // namespace reasampler
|
||||||
|
|||||||
@@ -54,4 +54,24 @@ FxDropTarget resolveFxDropTarget(int screenX, int screenY);
|
|||||||
// undoable.
|
// undoable.
|
||||||
bool performInstrumentDrop(MediaTrack* track, const std::string& chunkBase64);
|
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
|
||||||
|
// 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
|
} // namespace reasampler
|
||||||
|
|||||||
Reference in New Issue
Block a user