b4b64ce68f
Tag LoadedInstrument with installedAt; process() publishes that field (not a re-read of reloadGeneration_) so the pruner's displacedAt <= seen bound is airtight. Also fixes sampleRowHitTest canvas.bottom over-read and adds interleave-stride + canvas-clip tests.
318 lines
14 KiB
C++
318 lines
14 KiB
C++
// reasampler_processor.cpp — see reasampler_processor.h.
|
|
|
|
#include "reasampler_processor.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <fstream>
|
|
#include <vector>
|
|
|
|
#include "pluginterfaces/base/ibstream.h"
|
|
#include "pluginterfaces/vst/ivstaudioprocessor.h"
|
|
#include "pluginterfaces/vst/ivstevents.h"
|
|
#include "pluginterfaces/vst/vstspeaker.h"
|
|
|
|
#include "capture_paths.h" // resolveBankFile (shared M4 path resolution)
|
|
#include "ext_keys.h" // kProjExtBanksKey (shared wire contract)
|
|
#include "reasampler_editor.h"
|
|
#include "sample_map.h" // selectSample, downmixToMono, buildTier0Keymap, state (de)ser
|
|
#include "wav_trim.h" // parseWavLayout, extractFloatFrames (shared WAV parse)
|
|
|
|
using namespace Steinberg;
|
|
using namespace Steinberg::Vst;
|
|
|
|
namespace reasampler::vst {
|
|
|
|
namespace {
|
|
|
|
// Tier-0 fixed instrument shape (Tier 2 makes these editable). A gentle amp envelope so
|
|
// notes neither click on nor cut off abruptly; sustain at unity (velocity does the
|
|
// dynamics), a short release for a natural tail. Times are in seconds, converted to
|
|
// frames against the live sample rate at build time.
|
|
constexpr double kAttackSeconds = 0.003;
|
|
constexpr double kDecaySeconds = 0.0;
|
|
constexpr double kSustainLevel = 1.0;
|
|
constexpr double kReleaseSeconds = 0.060;
|
|
constexpr std::size_t kMaxVoices = 16;
|
|
|
|
AdsrParams tier0Adsr(double sampleRate) {
|
|
const double sr = sampleRate > 0.0 ? sampleRate : 44100.0;
|
|
AdsrParams p;
|
|
p.attackFrames = static_cast<std::int64_t>(kAttackSeconds * sr);
|
|
p.decayFrames = static_cast<std::int64_t>(kDecaySeconds * sr);
|
|
p.sustainLevel = kSustainLevel;
|
|
p.releaseFrames = static_cast<std::int64_t>(kReleaseSeconds * sr);
|
|
return p;
|
|
}
|
|
|
|
// Read a whole file into a byte buffer. Off-thread only (blocking file I/O). Empty on
|
|
// any failure — the caller treats an unreadable WAV as "nothing to play".
|
|
std::vector<std::uint8_t> readFileBytes(const std::string& path) {
|
|
std::vector<std::uint8_t> bytes;
|
|
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
|
if (!f) return bytes;
|
|
const std::streamoff size = f.tellg();
|
|
if (size <= 0) return bytes;
|
|
f.seekg(0, std::ios::beg);
|
|
bytes.resize(static_cast<std::size_t>(size));
|
|
if (!f.read(reinterpret_cast<char*>(bytes.data()), size)) bytes.clear();
|
|
return bytes;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
FUnknown* ReaSamplerProcessor::createInstance(void* /*context*/) {
|
|
// The host owns the returned reference. Cast up to the combined interface the SDK
|
|
// exposes (IAudioProcessor) so the FUnknown refcount is correctly rooted.
|
|
return static_cast<IAudioProcessor*>(new ReaSamplerProcessor());
|
|
}
|
|
|
|
tresult PLUGIN_API ReaSamplerProcessor::initialize(FUnknown* context) {
|
|
tresult result = SingleComponentEffect::initialize(context);
|
|
if (result != kResultOk) return result;
|
|
|
|
// Connect the REAPER bridge. Non-fatal if it fails (non-REAPER host): the
|
|
// instrument still loads, it just has no live bank to play.
|
|
bridge_.connect(context);
|
|
|
|
// Instrument bus topology: one event input (MIDI in, 16 channels), one stereo audio
|
|
// output, no audio input. This is the standard VSTi arrangement.
|
|
addEventInput(STR16("MIDI In"), 16);
|
|
addAudioOutput(STR16("Stereo Out"), SpeakerArr::kStereo);
|
|
|
|
return kResultOk;
|
|
}
|
|
|
|
tresult PLUGIN_API ReaSamplerProcessor::terminate() {
|
|
// process() is not running at terminate. Free the live instrument and drain the
|
|
// graveyard. Take the pointer out of the atomic first so nothing else races it.
|
|
std::lock_guard<std::mutex> lock(reloadMutex_);
|
|
delete live_.exchange(nullptr);
|
|
graveyard_.clear();
|
|
return SingleComponentEffect::terminate();
|
|
}
|
|
|
|
tresult PLUGIN_API ReaSamplerProcessor::setActive(TBool state) {
|
|
// Activating: build the instrument from the currently-selected sample so the first
|
|
// block after activation can play. Deactivating: process is now GUARANTEED stopped by
|
|
// the host, so this is the safe point to reclaim the graveyard (the displaced engines
|
|
// no reload could free while active). The build/drain are off the audio thread —
|
|
// setActive is a main/UI-thread call.
|
|
if (state) {
|
|
reloadFromBank();
|
|
} else {
|
|
std::lock_guard<std::mutex> lock(reloadMutex_);
|
|
graveyard_.clear();
|
|
}
|
|
return kResultOk;
|
|
}
|
|
|
|
tresult PLUGIN_API ReaSamplerProcessor::setupProcessing(ProcessSetup& setup) {
|
|
sampleRate_ = setup.sampleRate;
|
|
maxBlockSize_ = setup.maxSamplesPerBlock;
|
|
return SingleComponentEffect::setupProcessing(setup);
|
|
}
|
|
|
|
tresult PLUGIN_API ReaSamplerProcessor::setState(IBStream* state) {
|
|
if (!state) return kResultFalse;
|
|
// Read the whole component-state blob (the selected sample id, versioned). The blob
|
|
// is small; read in one shot into a growable buffer.
|
|
std::vector<std::uint8_t> bytes;
|
|
std::uint8_t chunk[256];
|
|
int32 got = 0;
|
|
while (state->read(chunk, sizeof(chunk), &got) == kResultOk && got > 0) {
|
|
bytes.insert(bytes.end(), chunk, chunk + got);
|
|
}
|
|
setSelectedSampleId(deserializeSelection(bytes));
|
|
// Rebuild from the restored selection (off-thread — setState is a load-time call).
|
|
reloadFromBank();
|
|
return kResultOk;
|
|
}
|
|
|
|
tresult PLUGIN_API ReaSamplerProcessor::getState(IBStream* state) {
|
|
if (!state) return kResultFalse;
|
|
const std::vector<std::uint8_t> bytes = serializeSelection(selectedSampleId());
|
|
if (!bytes.empty()) {
|
|
const tresult wr = state->write(const_cast<std::uint8_t*>(bytes.data()),
|
|
static_cast<int32>(bytes.size()), nullptr);
|
|
if (wr != kResultOk) return wr;
|
|
}
|
|
return kResultOk;
|
|
}
|
|
|
|
std::string ReaSamplerProcessor::selectedSampleId() {
|
|
std::lock_guard<std::mutex> lock(selectionMutex_);
|
|
return selectedSampleId_;
|
|
}
|
|
|
|
void ReaSamplerProcessor::setSelectedSampleId(const std::string& id) {
|
|
std::lock_guard<std::mutex> lock(selectionMutex_);
|
|
selectedSampleId_ = id;
|
|
}
|
|
|
|
std::string ReaSamplerProcessor::reloadFromBank() {
|
|
// OFF THE AUDIO THREAD. Serialize concurrent reloads (editor click + setState) so
|
|
// the retired-slot free is single-writer. This mutex is NEVER taken on the audio
|
|
// thread — process() only touches the atomic.
|
|
std::lock_guard<std::mutex> lock(reloadMutex_);
|
|
|
|
// Mint this reload's generation number first so we can stamp the built instrument
|
|
// with it before publishing. Under reloadMutex_ no other reload races here.
|
|
const std::uint64_t gen = reloadGeneration_.fetch_add(1, std::memory_order_relaxed) + 1;
|
|
|
|
// 1. Read the live bank + resolve the project dir over the bridge (allocates,
|
|
// calls REAPER — fine here, off-thread).
|
|
std::optional<std::string> banksJson =
|
|
bridge_.readReasamplerExtState(kProjExtBanksKey);
|
|
const std::string projectDir = bridge_.activeProjectDir();
|
|
|
|
std::string resolvedId;
|
|
std::unique_ptr<LoadedInstrument> built;
|
|
|
|
if (banksJson) {
|
|
// 2. Pick the sample (shared bank_book JSON parse — NOT a second parser).
|
|
std::optional<SelectedSample> sel =
|
|
selectSample(*banksJson, selectedSampleId());
|
|
if (sel) {
|
|
// 3. Resolve the project-relative WAV path the M4 way persist does, read +
|
|
// decode it (file I/O off-thread), downmix to the core's mono contract.
|
|
const std::string abs = resolveBankFile(projectDir, sel->relativePath);
|
|
if (!abs.empty()) {
|
|
const std::vector<std::uint8_t> bytes = readFileBytes(abs);
|
|
const WavLayout layout = parseWavLayout(bytes);
|
|
if (layout.valid) {
|
|
const std::size_t frames = layout.frameCount();
|
|
std::vector<AudioSample> interleaved =
|
|
extractFloatFrames(bytes, layout, 0, frames);
|
|
std::vector<AudioSample> mono =
|
|
downmixToMono(interleaved, layout.channelCount);
|
|
if (!mono.empty()) {
|
|
Keymap km = buildTier0Keymap(
|
|
std::move(mono),
|
|
static_cast<int>(layout.sampleRate), sel->rootNote,
|
|
sel->loop);
|
|
built = std::make_unique<LoadedInstrument>(
|
|
std::move(km), kMaxVoices, tier0Adsr(sampleRate_), gen);
|
|
// Record which id actually resolved so a first-sample fallback
|
|
// (empty stored id) becomes the concrete selection.
|
|
resolvedId = selectedSampleId();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Publish. Atomically install the new instrument; the DISPLACED one goes to the
|
|
// graveyard tagged with this generation (process may still be mid-block reading
|
|
// it). A null `built` (no bank / unreadable WAV) installs silence.
|
|
// `built` is heap-owned; release() hands ownership to the atomic, and the
|
|
// exchanged pointer is re-owned by the graveyard.
|
|
//
|
|
// Bounded reclaim: prune graveyard entries where displacedAt <= seen, where seen
|
|
// is the last generation process() published. process() publishes inst->installedAt
|
|
// (not a re-read of reloadGeneration_), so seen == D means process holds the
|
|
// instrument installed at gen D. An entry with displacedAt == D was displaced by
|
|
// reload D, which installed that very successor — process cannot be holding the
|
|
// displaced entry. The pruning condition is therefore <= (see header for the full
|
|
// proof). Remaining entries drain at setActive(false) / terminate() when process
|
|
// is guaranteed stopped.
|
|
const std::uint64_t seen = processGeneration_.load(std::memory_order_acquire);
|
|
graveyard_.erase(
|
|
std::remove_if(graveyard_.begin(), graveyard_.end(),
|
|
[seen](const GraveyardEntry& e) { return e.displacedAt <= seen; }),
|
|
graveyard_.end());
|
|
LoadedInstrument* prev = live_.exchange(built.release());
|
|
if (prev) graveyard_.push_back({gen, std::unique_ptr<LoadedInstrument>(prev)});
|
|
return resolvedId;
|
|
}
|
|
|
|
tresult PLUGIN_API ReaSamplerProcessor::process(ProcessData& data) {
|
|
// REAL-TIME: no allocation, no IO, no locks. Load the live instrument once for the
|
|
// whole block (a single atomic acquire), then publish inst->installedAt so the off-
|
|
// thread graveyard pruner knows exactly which generation this block is holding.
|
|
//
|
|
// We publish installedAt — not a fresh re-read of reloadGeneration_ — to close an
|
|
// ordering race: reading reloadGeneration_ after live_ could observe a generation
|
|
// newer than the pointer we actually hold, causing the pruner to free an instrument
|
|
// process is still reading. installedAt was set on the reload path before the atomic
|
|
// exchange that made the instrument visible, so it is always <= the generation of any
|
|
// instrument that could have been loaded after our acquire above.
|
|
LoadedInstrument* inst = live_.load(std::memory_order_acquire);
|
|
const std::uint64_t heldGen = inst ? inst->installedAt : 0;
|
|
processGeneration_.store(heldGen, std::memory_order_release);
|
|
|
|
// Marshal MIDI note-on/off from the event input into the voice engine. Tier 0 maps
|
|
// events at block granularity (no per-event sample-offset split) — audible timing is
|
|
// within one block, adequate for Tier 0; sample-accurate scheduling is a later tier.
|
|
if (inst && data.inputEvents) {
|
|
const int32 count = data.inputEvents->getEventCount();
|
|
for (int32 i = 0; i < count; ++i) {
|
|
Event e;
|
|
if (data.inputEvents->getEvent(i, e) != kResultOk) continue;
|
|
if (e.type == Event::kNoteOnEvent) {
|
|
// A note-on with velocity 0 is a note-off by MIDI convention.
|
|
const int vel = static_cast<int>(e.noteOn.velocity * 127.0f + 0.5f);
|
|
if (vel <= 0) {
|
|
inst->engine.noteOff(e.noteOn.pitch);
|
|
} else {
|
|
inst->engine.noteOn(e.noteOn.pitch, vel);
|
|
}
|
|
} else if (e.type == Event::kNoteOffEvent) {
|
|
inst->engine.noteOff(e.noteOff.pitch);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (data.numOutputs <= 0 || !data.outputs || data.numSamples <= 0) {
|
|
return kResultOk;
|
|
}
|
|
AudioBusBuffers& out = data.outputs[0];
|
|
const int32 frames = data.numSamples;
|
|
|
|
// 64-bit host processing is not supported by the mono float core; emit silence
|
|
// rather than mis-render. REAPER runs 32-bit float by default.
|
|
if (data.symbolicSampleSize != kSample32) {
|
|
for (int32 ch = 0; ch < out.numChannels; ++ch) {
|
|
if (double* buf = out.channelBuffers64[ch]) {
|
|
for (int32 i = 0; i < frames; ++i) buf[i] = 0.0;
|
|
}
|
|
}
|
|
out.silenceFlags = (out.numChannels >= 64)
|
|
? ~0ULL
|
|
: ((1ULL << out.numChannels) - 1);
|
|
return kResultOk;
|
|
}
|
|
|
|
// Render mono into channel 0's buffer, then replicate to the other channels (the
|
|
// core is mono-per-sample). Clear channel 0 first (render ADDS), then mix.
|
|
float* ch0 = out.numChannels > 0 ? out.channelBuffers32[0] : nullptr;
|
|
if (ch0) {
|
|
for (int32 i = 0; i < frames; ++i) ch0[i] = 0.f;
|
|
if (inst) {
|
|
inst->engine.render(ch0, static_cast<std::size_t>(frames));
|
|
}
|
|
// Duplicate the mono render across the remaining output channels.
|
|
for (int32 ch = 1; ch < out.numChannels; ++ch) {
|
|
if (float* buf = out.channelBuffers32[ch]) {
|
|
for (int32 i = 0; i < frames; ++i) buf[i] = ch0[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Report silence only when nothing is loaded (lets the host optimize when idle).
|
|
// With an instrument loaded we clear the flag so a ringing voice is not skipped.
|
|
out.silenceFlags = inst ? 0 : ((out.numChannels >= 64)
|
|
? ~0ULL
|
|
: ((1ULL << out.numChannels) - 1));
|
|
return kResultOk;
|
|
}
|
|
|
|
IPlugView* PLUGIN_API ReaSamplerProcessor::createView(FIDString name) {
|
|
if (name && FIDStringsEqual(name, ViewType::kEditor)) {
|
|
return new ReaSamplerEditor(this);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
} // namespace reasampler::vst
|