Fix drag-out losing audio and FX-container drops losing the capture; re-home ingest under shell/actions
This commit is contained in:
@@ -0,0 +1,485 @@
|
||||
// ingest.cpp — see ingest.h. main.cpp owns the API pointers; this TU gets them extern.
|
||||
// REAPER-facing, DAW-verified; the pure serialization it drives (assignment_request)
|
||||
// is CTest-tested.
|
||||
|
||||
#include "shell/actions/ingest.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/version/app_version.h" // channelCommandId / channelActionName
|
||||
#include "core/wire/assignment_request.h" // pure (bankId, sampleId, generation) encode
|
||||
#include "core/model/bank_book.h" // BankBook, Bank, activeBankId / activeIndex
|
||||
#include "core/model/bank_model.h" // Sample, AddResult, findByHash
|
||||
#include "shell/panel/panel_input.h" // bankPanelRefresh
|
||||
#include "core/capture/capture_paths.h" // deriveBankPaths / projectDirOfRpp
|
||||
#include "core/util/file_bytes.h" // shared whole-file loader
|
||||
#include "core/wire/instrument_drop.h" // pure buildInstrumentDropPreset (.vstpreset image for a sampleId)
|
||||
#include "shell/actions/instrument_drop_win.h" // shell loadInstrumentOntoTrack (FX add+apply, no own undo block)
|
||||
#include "shell/persist/session.h" // ReaSamplerSession
|
||||
|
||||
#include "core/capture/wav_codec.h" // parseWavLayout (32f fast-path validator), buildFloat32Wav, hashWavContent
|
||||
|
||||
#include "reaper_plugin.h" // reaper_plugin_info_t, gaccel_register_t (full defs)
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_ShowConsoleMsg
|
||||
#define REAPERAPI_WANT_MediaExplorerGetLastPlayedFileInfo
|
||||
#define REAPERAPI_WANT_PCM_Source_CreateFromFile
|
||||
#define REAPERAPI_WANT_PCM_Source_Destroy
|
||||
#define REAPERAPI_WANT_GetMediaSourceNumChannels
|
||||
#define REAPERAPI_WANT_GetMediaSourceSampleRate
|
||||
#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 {
|
||||
|
||||
using capture::BankPaths;
|
||||
using capture::buildFloat32Wav;
|
||||
using capture::deriveBankPaths;
|
||||
using capture::hashWavContent;
|
||||
using capture::parseWavLayout;
|
||||
using capture::projectDirOfRpp;
|
||||
using capture::WavLayout;
|
||||
using util::readFileBytes;
|
||||
using version::channelActionName;
|
||||
using version::channelCommandId;
|
||||
using wire::AssignmentRequest;
|
||||
using wire::buildInstrumentDropPreset;
|
||||
using wire::encodeAssignmentRequest;
|
||||
|
||||
namespace {
|
||||
|
||||
// Not owned here (main.cpp owns g_session).
|
||||
ReaSamplerSession* g_session = nullptr;
|
||||
|
||||
// FOREVER-STABLE suffix — NEVER change after ship. Only the Media-Explorer import
|
||||
// registers here — the arrange capture+assign action lives in the capture family in
|
||||
// main.cpp, and the drop path is a panel callback (ingestDroppedFiles), not a
|
||||
// bindable action.
|
||||
constexpr const char* kIdImportMediaExplorer = "INGEST_IMPORT_MEDIA_EXPLORER";
|
||||
|
||||
int g_cmdImportMediaExplorer = 0;
|
||||
gaccel_register_t g_accelImportMediaExplorer{};
|
||||
|
||||
// c_str() pointers are handed to REAPER at register and re-presented at unregister,
|
||||
// so these strings must not be mutated after registration.
|
||||
std::string g_idImportStr;
|
||||
std::string g_labelImportStr;
|
||||
|
||||
// Forward-slashed, no trailing slash. Empty for an unsaved/no-active project, which
|
||||
// makes the import refuse to place a file (relative-paths invariant, no fallback).
|
||||
std::string currentProjectDir() {
|
||||
std::vector<char> buf(4096, '\0');
|
||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||
return projectDirOfRpp(std::string(buf.data()));
|
||||
}
|
||||
|
||||
// Writes a byte buffer to a file. Returns true on success. The caller is responsible for
|
||||
// ensuring the directory exists before calling.
|
||||
bool writeFileBytes(const std::string& path, const std::vector<std::uint8_t>& bytes) {
|
||||
std::ofstream f(path, std::ios::binary | std::ios::trunc);
|
||||
if (!f) return false;
|
||||
f.write(reinterpret_cast<const char*>(bytes.data()),
|
||||
static_cast<std::streamsize>(bytes.size()));
|
||||
return f.good();
|
||||
}
|
||||
|
||||
// buildFloat32Wav (wav_codec) takes the interleaved ReaSample (double) frames
|
||||
// decoded below and yields the canonical bank-format bytes — the double->float
|
||||
// narrowing is the intentional bank contract.
|
||||
|
||||
// Returns empty on a zero-length or silent source. The caller has already queried
|
||||
// channelCount/sampleRate from the same source (passed in to avoid re-querying
|
||||
// after GetSamples mutates decoder state).
|
||||
std::vector<ReaSample> decodePcmSource(PCM_source* src, int nch, double sampleRate,
|
||||
double lengthSeconds) {
|
||||
if (!src || nch <= 0 || sampleRate < 1.0 || lengthSeconds <= 0.0) return {};
|
||||
|
||||
const std::size_t totalFrames =
|
||||
static_cast<std::size_t>(lengthSeconds * sampleRate + 0.5);
|
||||
if (totalFrames == 0) return {};
|
||||
|
||||
std::vector<ReaSample> out;
|
||||
out.reserve(totalFrames * static_cast<std::size_t>(nch));
|
||||
|
||||
constexpr int kBlockFrames = 4096;
|
||||
std::vector<ReaSample> block(static_cast<std::size_t>(kBlockFrames * nch));
|
||||
|
||||
PCM_source_transfer_t t{};
|
||||
t.samplerate = sampleRate;
|
||||
t.nch = nch;
|
||||
t.time_s = 0.0;
|
||||
t.midi_events = nullptr;
|
||||
|
||||
while (true) {
|
||||
t.samples = block.data();
|
||||
t.length = kBlockFrames;
|
||||
t.samples_out = 0;
|
||||
src->GetSamples(&t);
|
||||
if (t.samples_out <= 0) break;
|
||||
const std::size_t got = static_cast<std::size_t>(t.samples_out) *
|
||||
static_cast<std::size_t>(nch);
|
||||
out.insert(out.end(), block.data(), block.data() + got);
|
||||
t.time_s += static_cast<double>(t.samples_out) / sampleRate;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
struct ImportResult {
|
||||
std::string sampleId; // "" on failure (nothing to assign)
|
||||
bool added = false; // true iff a NEW index entry was created (not a collapse)
|
||||
std::string message; // human-readable outcome for the console
|
||||
};
|
||||
|
||||
// Imports one OS-native source file into the ACTIVE bank: convert-if-needed to
|
||||
// 32-bit-float WAV (the bank contract — a verbatim copy of anything else would be
|
||||
// unplayable), write to the project-relative bank folder, index-add, hash-dedup.
|
||||
//
|
||||
// DEDUP ORDERING: the content hash is taken from the CONVERTED bytes AFTER building
|
||||
// the buffer but BEFORE writing to disk, so a re-import of the same source (or of a
|
||||
// WAV matching a captured file's content) collapses without a redundant disk write.
|
||||
// Hashing the raw source bytes instead would miss this for non-WAV sources, since
|
||||
// their bytes differ from the converted WAV bytes.
|
||||
//
|
||||
// NON-DESTRUCTIVE: the source file is only read. Does NOT persist or open an undo
|
||||
// point — the caller batches that (a multi-file drop is one undo point, one persist).
|
||||
ImportResult importFileIntoActiveBank(const std::string& absoluteSourcePath) {
|
||||
ImportResult out;
|
||||
|
||||
if (absoluteSourcePath.empty()) {
|
||||
out.message = "empty file path";
|
||||
return out;
|
||||
}
|
||||
namespace fs = std::filesystem;
|
||||
std::error_code ec;
|
||||
if (!fs::exists(absoluteSourcePath, ec) || ec) {
|
||||
out.message = "file not found: " + absoluteSourcePath;
|
||||
return out;
|
||||
}
|
||||
|
||||
const std::string projectDir = currentProjectDir();
|
||||
if (projectDir.empty()) {
|
||||
out.message = "no saved project, so the bank has no location -- save the "
|
||||
"project first";
|
||||
return out;
|
||||
}
|
||||
|
||||
const std::vector<std::uint8_t> srcBytes = readFileBytes(absoluteSourcePath);
|
||||
if (srcBytes.empty()) {
|
||||
out.message = "file is empty or unreadable: " + absoluteSourcePath;
|
||||
return out;
|
||||
}
|
||||
|
||||
// A file REAPER cannot open leaves geometry at zero — the sample still imports
|
||||
// if the WAV-fast-path succeeds; the geometry is simply unknown, the honest default.
|
||||
int channelCount = 0;
|
||||
int sampleRate = 0;
|
||||
double lengthSeconds = 0.0;
|
||||
PCM_source* srcHandle = PCM_Source_CreateFromFile(absoluteSourcePath.c_str());
|
||||
if (srcHandle) {
|
||||
channelCount = GetMediaSourceNumChannels(srcHandle);
|
||||
sampleRate = GetMediaSourceSampleRate(srcHandle);
|
||||
bool isQN = false;
|
||||
lengthSeconds = GetMediaSourceLength(srcHandle, &isQN);
|
||||
if (isQN) lengthSeconds = 0.0; // QN-length source has no seconds length to store
|
||||
}
|
||||
|
||||
// parseWavLayout validates a canonical 32-bit-float RIFF/WAVE; any other format
|
||||
// (mp3, aiff, integer PCM, 16-bit WAV, etc.) takes the decode+rewrite path.
|
||||
const WavLayout layout = parseWavLayout(srcBytes);
|
||||
const bool isFloat32Wav = layout.valid;
|
||||
|
||||
std::vector<std::uint8_t> bankBytes;
|
||||
if (isFloat32Wav) {
|
||||
bankBytes = srcBytes;
|
||||
if (srcHandle) PCM_Source_Destroy(srcHandle);
|
||||
} else {
|
||||
std::vector<ReaSample> decoded;
|
||||
if (srcHandle && channelCount > 0 && sampleRate > 0 && lengthSeconds > 0.0) {
|
||||
decoded = decodePcmSource(srcHandle, channelCount,
|
||||
static_cast<double>(sampleRate), lengthSeconds);
|
||||
}
|
||||
if (srcHandle) PCM_Source_Destroy(srcHandle);
|
||||
|
||||
if (decoded.empty()) {
|
||||
// e.g. a MIDI file, zero-length audio, or an unsupported format. Fail
|
||||
// loudly rather than write a silent WAV and pretend the import succeeded.
|
||||
out.message = "could not decode audio samples from: " +
|
||||
fs::path(absoluteSourcePath).filename().string() +
|
||||
" (unsupported format or no audio data)";
|
||||
return out;
|
||||
}
|
||||
|
||||
const std::size_t frameCount =
|
||||
decoded.size() / static_cast<std::size_t>(channelCount > 0 ? channelCount : 1);
|
||||
bankBytes = buildFloat32Wav(channelCount,
|
||||
static_cast<std::uint32_t>(sampleRate),
|
||||
frameCount, decoded);
|
||||
}
|
||||
|
||||
// WAV-aware hash so a re-import deduplicates against a previously-captured or
|
||||
// previously-imported sample with identical audio content, even if non-audio
|
||||
// RIFF chunks differ. Empty (unhashable) is "not dedupable" — copies + adds
|
||||
// rather than silently collapsing onto an unrelated entry.
|
||||
const std::string contentHash = hashWavContent(bankBytes);
|
||||
|
||||
BankBook& book = g_session->book();
|
||||
|
||||
// Dedup-before-disk: skip the write entirely if the active bank already holds
|
||||
// this content.
|
||||
if (!contentHash.empty()) {
|
||||
if (const Sample* existing = book.activeIndex().findByHash(contentHash)) {
|
||||
out.sampleId = existing->id;
|
||||
out.added = false; // already present — no index mutation, no undo point
|
||||
out.message = "already in the active bank (assigned existing sample)";
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
// A timestamp uniqueTag avoids collision with a prior import of a same-named file.
|
||||
const std::string sourceStem = fs::path(absoluteSourcePath).stem().string();
|
||||
const std::int64_t nowSec = static_cast<std::int64_t>(std::time(nullptr));
|
||||
const std::string uniqueTag = std::to_string(nowSec);
|
||||
const BankPaths paths = deriveBankPaths(projectDir, sourceStem, uniqueTag);
|
||||
|
||||
fs::create_directories(paths.absoluteDir, ec); // idempotent; ec ignored (write reports)
|
||||
const std::string destPath = paths.absoluteDir + "/" + paths.fileName;
|
||||
if (!writeFileBytes(destPath, bankBytes)) {
|
||||
out.message = "could not write converted file to the bank folder";
|
||||
return out;
|
||||
}
|
||||
|
||||
// Import is NOT a capture — capture-only fields stay at defaults. rootNote/loop
|
||||
// stay empty: an imported file is not a single played note, so we do not guess.
|
||||
Sample s;
|
||||
s.id = "imp-" + uniqueTag + "-" + paths.fileName;
|
||||
s.displayName = sourceStem.empty() ? std::string("import") : sourceStem;
|
||||
s.relativePath = paths.relativePath; // project-relative (invariant)
|
||||
s.channelCount = channelCount;
|
||||
s.sampleRate = sampleRate;
|
||||
s.lengthSeconds = lengthSeconds;
|
||||
s.tier = Tier::Scratch; // imports land in scratch, like captures
|
||||
s.contentHash = contentHash;
|
||||
s.createdTimestamp = nowSec;
|
||||
|
||||
const AddResult r = book.activeIndex().add(s);
|
||||
// Record as owned regardless of outcome — the tool WROTE the file, so prune must
|
||||
// attribute it even in the narrow Collapsed race below.
|
||||
g_session->owned().add(paths.relativePath);
|
||||
|
||||
switch (r) {
|
||||
case AddResult::Added:
|
||||
out.sampleId = s.id;
|
||||
out.added = true;
|
||||
out.message = (isFloat32Wav ? "imported -> " : "converted + imported -> ") +
|
||||
paths.relativePath;
|
||||
break;
|
||||
case AddResult::Collapsed: {
|
||||
// A race against the pre-write dedup check (or an empty-hash edge).
|
||||
const Sample* existing =
|
||||
contentHash.empty() ? nullptr : book.activeIndex().findByHash(contentHash);
|
||||
out.sampleId = existing ? existing->id : std::string{};
|
||||
out.added = false;
|
||||
out.message = "collapsed onto an existing bank sample";
|
||||
break;
|
||||
}
|
||||
case AddResult::RejectedAbsolutePath:
|
||||
case AddResult::RejectedEmptyId:
|
||||
// Unreachable in practice (deriveBankPaths always yields a relative path
|
||||
// and non-empty id) — reported honestly rather than silently.
|
||||
out.message = "index rejected the import (internal path/id error)";
|
||||
break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Imports the Media Explorer's last-played/selected file into the active bank, then
|
||||
// adds a ReaSampler 9000 instrument to the FIRST SELECTED TRACK pre-loaded with that
|
||||
// sound — no new track, no routing changes. No assignment_request write.
|
||||
//
|
||||
// Single-file, pull-on-action (MediaExplorerGetLastPlayedFileInfo — no
|
||||
// enumerate-selected API). The selection RANGE it reports is deliberately IGNORED:
|
||||
// an import brings the whole file in ([0,1] fraction fields are a preview hint, not
|
||||
// seconds); a sub-range user captures via the arrange path instead.
|
||||
//
|
||||
// LOAD-BEARING: NEVER inserts a timeline item, NEVER creates a track. Persist
|
||||
// ordering is critical — the fresh instance's setState -> reloadInstrument reads the
|
||||
// bank from project ext-state, so the sample MUST be persisted BEFORE
|
||||
// loadInstrumentOntoTrack adds the FX, or the instance cannot resolve the sampleId.
|
||||
void doImportFromMediaExplorer() {
|
||||
// Only the filename is used; selstart/selend are [0,1] fractions (a preview
|
||||
// hint, not a bank-relevant range), extrainfo is documented "currently unused".
|
||||
std::vector<char> nameBuf(4096, '\0');
|
||||
int filemode = 0;
|
||||
double selStart = 0.0, selEnd = 0.0;
|
||||
double pitch = 0.0, vol = 0.0, rate = 0.0, srcbpm = 0.0;
|
||||
std::vector<char> extra(256, '\0'); // documented unused; sized generously to be safe
|
||||
const bool ok = MediaExplorerGetLastPlayedFileInfo(
|
||||
nameBuf.data(), static_cast<int>(nameBuf.size()), &filemode, &selStart, &selEnd,
|
||||
&pitch, &vol, &rate, &srcbpm, extra.data(), static_cast<int>(extra.size()));
|
||||
|
||||
const std::string path(nameBuf.data());
|
||||
if (!ok || path.empty()) {
|
||||
ShowConsoleMsg("ReaSampler ingest: no Media Explorer file to import -- open the "
|
||||
"Media Explorer and select (or preview) a file first.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
const ImportResult r = importFileIntoActiveBank(path);
|
||||
if (r.sampleId.empty()) {
|
||||
ShowConsoleMsg(("ReaSampler ingest: Media Explorer import failed -- " + r.message +
|
||||
".\n").c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// GetSelectedTrack ignores the master; null means nothing selected — existing
|
||||
// track only, never alter the graph.
|
||||
MediaTrack* target = GetSelectedTrack(nullptr, 0);
|
||||
if (!target) {
|
||||
// Bank import is kept (sound is in the bank browser); 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;
|
||||
}
|
||||
|
||||
// Valid for BOTH the fresh import and the dedup case (added == false but a real
|
||||
// sampleId is sufficient to pre-select the sound).
|
||||
const std::vector<std::uint8_t> preset = buildInstrumentDropPreset(r.sampleId);
|
||||
|
||||
// Persist happens INSIDE the block and BEFORE the FX add so the new instance's
|
||||
// setState -> reloadInstrument sees the just-persisted sample.
|
||||
Undo_BeginBlock2(nullptr);
|
||||
bool persisted = true; // true when nothing needed persisting (dedup)
|
||||
if (r.added) {
|
||||
g_session->bumpBankGeneration();
|
||||
persisted = g_session->saveToActiveProject();
|
||||
}
|
||||
const bool placed = loadInstrumentOntoTrack(target, preset);
|
||||
if (placed && persisted)
|
||||
Undo_EndBlock2(nullptr, "ReaSampler: import from Media Explorer into selected track",
|
||||
UNDO_STATE_MISCCFG);
|
||||
else
|
||||
// Either the FX add/inject failed (already rolled back, no orphan) or the
|
||||
// project was unsaved (persist no-op): discard so no empty point is recorded.
|
||||
Undo_EndBlock2(nullptr, "", 0);
|
||||
|
||||
bankPanelRefresh();
|
||||
if (placed)
|
||||
ShowConsoleMsg(("ReaSampler ingest: " + r.message +
|
||||
" (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 add the instrument to the selected track.\n").c_str());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ingestAssignActiveInstance(const std::string& bankId, const std::string& sampleId) {
|
||||
if (!g_session || sampleId.empty()) return; // nothing to assign
|
||||
|
||||
AssignmentRequest req;
|
||||
req.bankId = bankId;
|
||||
req.sampleId = sampleId;
|
||||
// Monotonic wall-clock stamp so the reader tells a fresh assign (even
|
||||
// re-assigning the SAME id) from a stale value — self-contained to the request,
|
||||
// not the bank-generation counter.
|
||||
req.generation = static_cast<std::int64_t>(std::time(nullptr));
|
||||
|
||||
g_session->writeAssignmentRequest(encodeAssignmentRequest(req));
|
||||
}
|
||||
|
||||
// Bank-fill only; no assignment_request is written (the drop has no effect on what
|
||||
// any live instance plays). Batch the persist + undo point: many imports are ONE
|
||||
// undo entry.
|
||||
void ingestDroppedFiles(const std::vector<std::string>& absolutePaths) {
|
||||
if (!g_session || absolutePaths.empty()) return;
|
||||
|
||||
int importedNew = 0;
|
||||
int importedTotal = 0;
|
||||
std::string lastFailure;
|
||||
|
||||
for (const std::string& path : absolutePaths) {
|
||||
const ImportResult r = importFileIntoActiveBank(path);
|
||||
if (r.sampleId.empty()) {
|
||||
lastFailure = r.message;
|
||||
continue;
|
||||
}
|
||||
++importedTotal;
|
||||
if (r.added) ++importedNew;
|
||||
}
|
||||
|
||||
// One undo point for the whole drop, opened only if a NEW index entry was created.
|
||||
if (importedNew > 0) {
|
||||
Undo_BeginBlock2(nullptr);
|
||||
g_session->bumpBankGeneration();
|
||||
const bool persisted = g_session->saveToActiveProject();
|
||||
if (persisted)
|
||||
Undo_EndBlock2(nullptr, "ReaSampler: import dropped file(s)", UNDO_STATE_MISCCFG);
|
||||
else
|
||||
Undo_EndBlock2(nullptr, "", 0);
|
||||
bankPanelRefresh();
|
||||
const std::string msg =
|
||||
"ReaSampler ingest: imported " + std::to_string(importedTotal) +
|
||||
(importedTotal == 1 ? " file" : " files") + " into the bank.\n";
|
||||
ShowConsoleMsg(msg.c_str());
|
||||
} else if (importedTotal > 0) {
|
||||
bankPanelRefresh();
|
||||
ShowConsoleMsg("ReaSampler ingest: all dropped files already in the bank.\n");
|
||||
} else {
|
||||
ShowConsoleMsg(("ReaSampler ingest: nothing imported from the drop -- " +
|
||||
(lastFailure.empty() ? std::string("no usable files") : lastFailure) +
|
||||
".\n").c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void ingestRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session) {
|
||||
g_session = 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 selected track");
|
||||
g_accelImportMediaExplorer.accel.cmd = g_cmdImportMediaExplorer;
|
||||
g_accelImportMediaExplorer.desc = g_labelImportStr.c_str();
|
||||
rec->Register("gaccel", (void*)&g_accelImportMediaExplorer);
|
||||
}
|
||||
}
|
||||
|
||||
bool ingestHandleCommand(int command) {
|
||||
if (command == 0 || !g_session) return false;
|
||||
if (command == g_cmdImportMediaExplorer) { doImportFromMediaExplorer(); return true; }
|
||||
return false; // not ours — caller's hookcommand keeps looking
|
||||
}
|
||||
|
||||
void ingestUnregisterActions(reaper_plugin_info_t* rec) {
|
||||
// '-command_id' re-presents the SAME interned id used at register (g_idImportStr).
|
||||
rec->Register("-gaccel", (void*)&g_accelImportMediaExplorer);
|
||||
rec->Register("-command_id", (void*)g_idImportStr.c_str());
|
||||
g_session = nullptr;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
Reference in New Issue
Block a user