S4 Tier 0: the bank plays — VST3 marshals MIDI to the S3 core, reads the live bank + resolves WAV the M4 way, mono downmix, lock-free load handoff, LICE sample-pick
This commit is contained in:
@@ -2,16 +2,64 @@
|
||||
|
||||
#include "reasampler_processor.h"
|
||||
|
||||
#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.
|
||||
@@ -23,7 +71,7 @@ tresult PLUGIN_API ReaSamplerProcessor::initialize(FUnknown* context) {
|
||||
if (result != kResultOk) return result;
|
||||
|
||||
// Connect the REAPER bridge. Non-fatal if it fails (non-REAPER host): the
|
||||
// instrument still loads, the editor just shows "no bridge".
|
||||
// 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
|
||||
@@ -35,46 +83,204 @@ tresult PLUGIN_API ReaSamplerProcessor::initialize(FUnknown* context) {
|
||||
}
|
||||
|
||||
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*/) {
|
||||
// Nothing to allocate/free in the silent skeleton; S4 will size voice buffers here
|
||||
// against the setupProcessing block size.
|
||||
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::process(ProcessData& data) {
|
||||
// Silent skeleton: emit silence on the output bus so the instrument runs cleanly in
|
||||
// REAPER's render/record path without a null buffer. S4 marshals MIDI->core->audio.
|
||||
if (data.numOutputs > 0 && data.outputs && data.numSamples > 0) {
|
||||
AudioBusBuffers& out = data.outputs[0];
|
||||
for (int32 ch = 0; ch < out.numChannels; ++ch) {
|
||||
if (data.symbolicSampleSize == kSample32) {
|
||||
if (float* buf = out.channelBuffers32[ch]) {
|
||||
for (int32 i = 0; i < data.numSamples; ++i) buf[i] = 0.f;
|
||||
}
|
||||
} else if (data.symbolicSampleSize == kSample64) {
|
||||
if (double* buf = out.channelBuffers64[ch]) {
|
||||
for (int32 i = 0; i < data.numSamples; ++i) buf[i] = 0.0;
|
||||
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);
|
||||
if (got < static_cast<int32>(sizeof(chunk))) break;
|
||||
}
|
||||
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()) {
|
||||
state->write(const_cast<std::uint8_t*>(bytes.data()),
|
||||
static_cast<int32>(bytes.size()), nullptr);
|
||||
}
|
||||
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_);
|
||||
|
||||
// 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_));
|
||||
// Record which id actually resolved so a first-sample fallback
|
||||
// (empty stored id) becomes the concrete selection.
|
||||
resolvedId = selectedSampleId();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Flag output silence so the host can optimize (nothing plays yet).
|
||||
}
|
||||
|
||||
// 4. Publish. Atomically install the new instrument; the DISPLACED one goes to the
|
||||
// graveyard (process may still be reading it this block — it is reclaimed only when
|
||||
// process is stopped, in setActive(false)/terminate). 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.
|
||||
LoadedInstrument* prev = live_.exchange(built.release());
|
||||
if (prev) graveyard_.emplace_back(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).
|
||||
LoadedInstrument* inst = live_.load(std::memory_order_acquire);
|
||||
|
||||
// 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(&bridge_);
|
||||
return new ReaSamplerEditor(this);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user