Ξ-W2-T1: the resample bake chain — instrument renders, extension banks, one click re-points and resets
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
// See instrument_bake.h.
|
||||
|
||||
#include "shell/instrument/instrument_bake.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/capture/wav_codec.h" // buildFloat32Wav (the bank byte format)
|
||||
#include "core/instrument/bake/bake_plan.h"
|
||||
#include "core/instrument/bake/bake_render.h"
|
||||
#include "core/instrument/bake/bake_reset.h"
|
||||
#include "core/instrument/note/tempo.h"
|
||||
#include "core/wire/bake_wire.h"
|
||||
#include "ext_keys.h"
|
||||
#include "shell/instrument/reaper_bridge.h"
|
||||
#include "shell/instrument/reasampler_processor.h"
|
||||
|
||||
namespace reasampler::vst {
|
||||
|
||||
using capture::buildFloat32Wav;
|
||||
using instrument::bake::defaultBakeProgram;
|
||||
using instrument::bake::planBake;
|
||||
using instrument::bake::renderBake;
|
||||
using instrument::bake::resetAfterBake;
|
||||
using instrument::map::SampleRefEntry;
|
||||
using instrument::map::SelectedSample;
|
||||
using instrument::note::Tempo;
|
||||
using instrument::note::resolveNote;
|
||||
using wire::BakeOutcome;
|
||||
using wire::BakeRequest;
|
||||
using wire::BakeStatus;
|
||||
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Deletes the staged file on EVERY exit path, success or failure — the same stack-RAII
|
||||
// discipline the capture shell's FX-bypass guard follows. On success the extension has
|
||||
// already COPIED the bytes into the bank, so the delete here is what keeps the temp from
|
||||
// outliving the click. Best-effort: a file already gone is not an error.
|
||||
//
|
||||
// Orphan policy: a crash between the write and the invoke leaves one file in the OS temp
|
||||
// directory, which is exactly what that directory is swept for. Nothing here scans or
|
||||
// deletes files it did not itself create.
|
||||
class StagedFileGuard {
|
||||
public:
|
||||
explicit StagedFileGuard(std::string path) : path_(std::move(path)) {}
|
||||
~StagedFileGuard() {
|
||||
if (path_.empty()) return;
|
||||
std::error_code ec;
|
||||
fs::remove(path_, ec);
|
||||
}
|
||||
StagedFileGuard(const StagedFileGuard&) = delete;
|
||||
StagedFileGuard& operator=(const StagedFileGuard&) = delete;
|
||||
|
||||
private:
|
||||
std::string path_;
|
||||
};
|
||||
|
||||
// Clears the request key on every exit path. A key left holding a request would be picked
|
||||
// up by the next bake's landing pass and re-run against a temp file that no longer exists.
|
||||
class RequestKeyGuard {
|
||||
public:
|
||||
RequestKeyGuard(ReaperBridge& bridge, std::string key)
|
||||
: bridge_(bridge), key_(std::move(key)) {}
|
||||
~RequestKeyGuard() { bridge_.writeBakeExtState(key_, ""); }
|
||||
RequestKeyGuard(const RequestKeyGuard&) = delete;
|
||||
RequestKeyGuard& operator=(const RequestKeyGuard&) = delete;
|
||||
|
||||
private:
|
||||
ReaperBridge& bridge_;
|
||||
std::string key_;
|
||||
};
|
||||
|
||||
BakeChainResult fail(std::string message) {
|
||||
return BakeChainResult{false, std::move(message)};
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool bakeAvailable(ReaperBridge& bridge) {
|
||||
return bridge.isConnected() &&
|
||||
bridge.extensionActionAvailable(wire::bakeActionLookupName());
|
||||
}
|
||||
|
||||
BakeChainResult runBake(ReaSamplerProcessor& processor) {
|
||||
ReaperBridge& bridge = processor.bridge();
|
||||
if (!bakeAvailable(bridge))
|
||||
return fail("resample needs the ReaSampler extension loaded");
|
||||
|
||||
const std::string selectionId = processor.selectedSampleId();
|
||||
if (selectionId.empty()) return fail("nothing loaded to resample");
|
||||
|
||||
// The whole ref entry, not just its SelectedSample: the display name that the new
|
||||
// capture's is derived from sits beside the intrinsics.
|
||||
const instrument::map::SampleRefs refs = processor.sampleRefs();
|
||||
const SampleRefEntry* sourceEntry = nullptr;
|
||||
for (const SampleRefEntry& e : refs)
|
||||
if (e.sampleId == selectionId) { sourceEntry = &e; break; }
|
||||
if (!sourceEntry) return fail("the loaded capture has no resolvable file");
|
||||
const SelectedSample* source = &sourceEntry->ref;
|
||||
|
||||
const int sampleRate = static_cast<int>(processor.sampleRate());
|
||||
if (sampleRate <= 0) return fail("the host has not reported a sample rate yet");
|
||||
|
||||
const std::optional<Tempo> tempo = Tempo::fromBpm(bridge.projectTempoBpm());
|
||||
if (!tempo) return fail("the project tempo could not be read");
|
||||
|
||||
const InstrumentParams dialed = processor.instrumentParams();
|
||||
const int rootNote = dialed.rootOverride ? *dialed.rootOverride : source->rootNote;
|
||||
const auto plan = planBake(resolveNote(defaultBakeProgram(), *tempo), sampleRate,
|
||||
rootNote);
|
||||
if (!plan) return fail("the programmed capture window is empty");
|
||||
|
||||
std::optional<SampleData> snapshot = processor.bakeSnapshot();
|
||||
if (!snapshot) return fail("the loaded capture could not be decoded for the render");
|
||||
|
||||
const instrument::bake::BakeAudio audio = renderBake(std::move(*snapshot), *plan);
|
||||
if (audio.empty()) return fail("the offline pass produced no audio");
|
||||
|
||||
// buildFloat32Wav takes doubles and narrows; the narrowing back to float is the bank's
|
||||
// own 32-bit-float contract, so the round trip is exact.
|
||||
std::vector<double> interleaved(audio.interleaved.begin(), audio.interleaved.end());
|
||||
const std::vector<std::uint8_t> bytes =
|
||||
buildFloat32Wav(audio.channelCount, static_cast<std::uint32_t>(audio.sampleRate),
|
||||
static_cast<std::size_t>(audio.frameCount()), interleaved);
|
||||
|
||||
const std::string instanceGuid = processor.usageInstanceGuid();
|
||||
const std::int64_t stamp = static_cast<std::int64_t>(std::time(nullptr));
|
||||
|
||||
// OUTSIDE the bank folder, always: the bank holds indexed captures only, and a stray
|
||||
// file there would read as a prune orphan.
|
||||
std::error_code ec;
|
||||
const fs::path stagedPath =
|
||||
fs::temp_directory_path(ec) /
|
||||
("reasampler_bake_" + instanceGuid + "_" + std::to_string(stamp) + ".wav");
|
||||
if (ec) return fail("no writable temp directory for the staged render");
|
||||
const std::string staged = stagedPath.string();
|
||||
StagedFileGuard stagedGuard(staged);
|
||||
if (!writeFileBytes(staged, bytes)) return fail("could not stage the rendered file");
|
||||
|
||||
BakeRequest request;
|
||||
request.instanceGuid = instanceGuid;
|
||||
request.stagedFilePath = staged;
|
||||
request.sourceSampleId = selectionId;
|
||||
request.sourceRelativePath = source->relativePath;
|
||||
request.sourceDisplayName = sourceEntry->displayName;
|
||||
request.ownUsageKey = usageKeyFor(instanceGuid);
|
||||
request.rootNote = plan->note;
|
||||
request.generation = stamp;
|
||||
|
||||
const std::string key = bakeKeyFor(instanceGuid);
|
||||
RequestKeyGuard keyGuard(bridge, key);
|
||||
if (!bridge.writeBakeExtState(key, wire::encodeBakeRequest(request)))
|
||||
return fail("could not publish the bake request");
|
||||
|
||||
// Synchronous: the extension's landing runs to completion inside this call and writes
|
||||
// its outcome back over the same key before returning.
|
||||
if (!bridge.invokeExtensionAction(wire::bakeActionLookupName()))
|
||||
return fail("the ReaSampler extension's bake action is not registered");
|
||||
|
||||
const std::optional<std::string> raw = bridge.readReasamplerExtState(key);
|
||||
const std::optional<BakeOutcome> outcome =
|
||||
raw ? wire::decodeBakeOutcome(*raw) : std::nullopt;
|
||||
// No outcome at all means the action never reached our request — an older extension
|
||||
// that registers the id but does not read this key, or an invocation REAPER deferred.
|
||||
if (!outcome) return fail("the extension did not answer the bake request");
|
||||
if (outcome->generation != request.generation)
|
||||
return fail("the extension answered a different bake request");
|
||||
if (outcome->status != BakeStatus::Ok)
|
||||
return fail(outcome->message.empty() ? std::string("the bake was refused")
|
||||
: outcome->message);
|
||||
|
||||
SampleRefEntry entry;
|
||||
entry.sampleId = outcome->sampleId;
|
||||
entry.displayName = outcome->displayName;
|
||||
entry.ref.relativePath = outcome->relativePath;
|
||||
entry.ref.rootNote = outcome->rootNote;
|
||||
entry.ref.channelCount = outcome->channelCount;
|
||||
// No loop: the loop points shaped the render and are meaningless against the new file
|
||||
// (bake_reset owns that rule for the parameter set; this is its bank-intrinsic peer).
|
||||
|
||||
const instrument::bake::BakeReset reset = resetAfterBake(dialed);
|
||||
processor.adoptBakedCapture(entry, reset.params, reset.masterGainLinear);
|
||||
|
||||
// The extension's own wording, which distinguishes the three landings (replaced, added,
|
||||
// and pointed at an identical existing entry) more precisely than this side can.
|
||||
return BakeChainResult{true, "resampled -- " + outcome->message};
|
||||
}
|
||||
|
||||
} // namespace reasampler::vst
|
||||
Reference in New Issue
Block a user