feat: VST FX rack per-track + masterbus render via native_bridge

This commit is contained in:
2026-08-17 21:31:57 +07:00
parent 8fbacd1d2b
commit b261eb0d7e
18 changed files with 1805 additions and 14 deletions
@@ -475,7 +475,11 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
float* spR = alignF(scratchR_, numSamples);
std::memset(spL, 0, numSamples * sizeof(float));
std::memset(spR, 0, numSamples * sizeof(float));
#ifdef _WIN32
DWORD sehCode = 0;
#else
uint32_t sehCode = 0;
#endif
if (!SafeProcessChannel(inst.get(), spL, spR, numSamples, &sehCode)) {
// G3.3: plugin fault — mute this channel only, keep the bridge up.
if (!crashedCh_[ch]) {
+928
View File
@@ -0,0 +1,928 @@
// native_bridge/src/RenderFxJob.cpp
// Offline audio-FX render mode (Phase 0+1 of PLAN_MASTERBUS_FX_RACK_VST.md):
// `daw_vst_bridge --render-fx <job.json> --in <input.wav> --out <output.wav>`
// processes an existing WAV through a VST3 effect chain (audio-in/audio-out,
// no MIDI) plus builtin slots (gain/normalize). Also `--scan <dir>` which
// classifies VST3 modules as instrument/effect.
//
// Same JSON/b64/WAV helpers as RenderJob.cpp (sheredom/json.h vendored with
// the VST3 SDK). The VST3 host pattern is the Vst3Instrument.cpp one minus
// MIDI/GUI — component + controller, audio buses, processData with input
// buffers copied directly (setChannelBuffers is unusable: prepare() owns them).
#include "RenderFxJob.h"
#include "vst3sdk/public.sdk/source/vst/moduleinfo/json.h"
#ifdef HAVE_VST3SDK
#include "public.sdk/source/vst/hosting/module.h"
#include "public.sdk/source/vst/hosting/hostclasses.h"
#include "public.sdk/source/vst/hosting/processdata.h"
#include "public.sdk/source/vst/vstpresetfile.h"
#include "public.sdk/source/common/memorystream.h"
#include "pluginterfaces/vst/ivstaudioprocessor.h"
#include "pluginterfaces/vst/ivstcomponent.h"
#include "pluginterfaces/vst/ivsteditcontroller.h"
#include "pluginterfaces/vst/ivstprocesscontext.h"
#include "pluginterfaces/vst/ivstmessage.h"
#endif
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <memory>
#include <string>
#include <vector>
namespace {
// --- sheredom/json.h helpers (same as RenderJob.cpp) ------------------------
const json_object_element_s* member(const json_object_s* o, const char* key) {
for (const json_object_element_s* e = o ? o->start : nullptr; e; e = e->next)
if (e->name && e->name->string && std::strcmp(e->name->string, key) == 0)
return e;
return nullptr;
}
const json_value_s* memberValue(const json_object_s* o, const char* key) {
const json_object_element_s* m = member(o, key);
return m ? m->value : nullptr;
}
std::string memberString(const json_object_s* o, const char* key, const std::string& def) {
const json_value_s* v = memberValue(o, key);
if (v && v->type == json_type_string) {
const auto* s = static_cast<const json_string_s*>(v->payload);
return std::string(s->string, s->string_size);
}
return def;
}
bool memberNumber(const json_object_s* o, const char* key, double& out) {
const json_value_s* v = memberValue(o, key);
if (v && v->type == json_type_number) {
out = std::atof(static_cast<const json_number_s*>(v->payload)->number);
return true;
}
return false;
}
int64_t memberInt(const json_object_s* o, const char* key, int64_t def) {
double d;
return memberNumber(o, key, d) ? (int64_t)d : def;
}
bool memberBool(const json_object_s* o, const char* key, bool def) {
const json_value_s* v = memberValue(o, key);
if (v && v->type == json_type_true) return true;
if (v && v->type == json_type_false) return false;
return def;
}
// --- WAV writer (stdlib only, 16-bit PCM stereo, little-endian) --------------
void writeU16(std::ofstream& f, uint16_t v) {
char b[2] = { (char)(v & 0xFF), (char)((v >> 8) & 0xFF) };
f.write(b, 2);
}
void writeU32(std::ofstream& f, uint32_t v) {
char b[4] = { (char)(v & 0xFF), (char)((v >> 8) & 0xFF),
(char)((v >> 16) & 0xFF), (char)((v >> 24) & 0xFF) };
f.write(b, 4);
}
void writeWavHeader(std::ofstream& f, uint32_t sampleRate) {
f.write("RIFF", 4);
writeU32(f, 36);
f.write("WAVE", 4);
f.write("fmt ", 4);
writeU32(f, 16);
writeU16(f, 1);
writeU16(f, 2);
writeU32(f, sampleRate);
writeU32(f, sampleRate * 4);
writeU16(f, 4);
writeU16(f, 16);
f.write("data", 4);
writeU32(f, 0);
}
void finishWav(std::ofstream& f, uint64_t dataBytes) {
f.seekp(4);
writeU32(f, (uint32_t)(36 + dataBytes));
f.seekp(40);
writeU32(f, (uint32_t)dataBytes);
f.flush();
}
void writeFrames(std::ofstream& f, const float* L, const float* R, uint32_t n) {
for (uint32_t i = 0; i < n; ++i) {
auto cl = [](float v) -> int {
if (v > 1.0f) v = 1.0f;
else if (v < -1.0f) v = -1.0f;
return (int)(v * 32767.0f);
};
writeU16(f, (uint16_t)cl(L[i]));
writeU16(f, (uint16_t)cl(R[i]));
}
}
// --- WAV reader (stdlib only): PCM 16/24/32-bit + IEEE float32, mono/stereo
// → stereo float32. Returns false on any parse error (job/argument error). ---
struct WavIn {
uint32_t sampleRate = 0;
uint16_t channels = 0;
uint16_t bits = 0;
std::vector<float> L, R; // same length
};
uint32_t readU32le(const uint8_t* p) {
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
uint16_t readU16le(const uint8_t* p) {
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
bool readWav(const std::string& path, WavIn& out) {
std::ifstream f(path, std::ios::binary);
if (!f) { std::cerr << "[RenderFx] cannot read input: " << path << std::endl; return false; }
std::vector<uint8_t> buf((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
if (buf.size() < 44) return false;
if (std::memcmp(buf.data(), "RIFF", 4) != 0 || std::memcmp(buf.data() + 8, "WAVE", 4) != 0)
return false;
uint16_t fmtTag = 0, channels = 0, bits = 0;
uint32_t sampleRate = 0;
size_t dataOff = 0, dataLen = 0;
size_t p = 12;
bool foundFmt = false, foundData = false;
while (p + 8 <= buf.size()) {
const char* id = reinterpret_cast<const char*>(buf.data() + p);
uint32_t sz = readU32le(buf.data() + p + 4);
size_t chunkStart = p + 8;
if (chunkStart + sz > buf.size()) break; // truncated chunk — stop
if (std::memcmp(id, "fmt ", 4) == 0 && sz >= 16) {
fmtTag = readU16le(buf.data() + chunkStart);
channels = readU16le(buf.data() + chunkStart + 2);
sampleRate = readU32le(buf.data() + chunkStart + 4);
bits = readU16le(buf.data() + chunkStart + 14);
foundFmt = true;
} else if (std::memcmp(id, "data", 4) == 0) {
dataOff = chunkStart;
dataLen = sz;
foundData = true;
break;
}
p = chunkStart + sz + (sz & 1);
}
if (!foundFmt || !foundData) return false;
// Supported: PCM 16/24/32 and IEEE float 32; mono or stereo.
const bool isFloat = (fmtTag == 3);
if (!(fmtTag == 1 || isFloat)) {
std::cerr << "[RenderFx] unsupported WAV format tag " << fmtTag << " (need PCM/float)" << std::endl;
return false;
}
if (channels < 1 || channels > 2) {
std::cerr << "[RenderFx] unsupported WAV channel count " << channels << std::endl;
return false;
}
const uint16_t bytes = (bits + 7) / 8;
if (bytes != 2 && bytes != 3 && bytes != 4) return false;
const size_t frameBytes = (size_t)channels * bytes;
const size_t nFrames = dataLen / frameBytes;
out.sampleRate = sampleRate;
out.channels = channels;
out.bits = bits;
out.L.assign(nFrames, 0.0f);
out.R.assign(nFrames, 0.0f);
const uint8_t* d = buf.data() + dataOff;
for (size_t i = 0; i < nFrames; ++i) {
float l = 0.f, r = 0.f;
for (uint16_t c = 0; c < channels; ++c) {
const uint8_t* s = d + (i * channels + c) * bytes;
float v = 0.f;
if (isFloat && bytes == 4) {
uint32_t u = readU32le(s);
float fv;
std::memcpy(&fv, &u, 4);
v = fv;
} else if (bytes == 2) {
v = (float)(int16_t)((uint16_t)s[0] | ((uint16_t)s[1] << 8)) / 32768.f;
} else if (bytes == 3) {
int32_t iv = (int32_t)s[0] | ((int32_t)s[1] << 8) | ((int32_t)s[2] << 16);
if (iv & 0x800000) iv |= ~0xFFFFFF; // sign extend
v = (float)iv / 8388608.f;
} else { // bytes == 4, PCM int32
int32_t iv = (int32_t)readU32le(s);
v = (float)iv / 2147483648.f;
}
if (c == 0) l = v; else r = v;
}
out.L[i] = l;
out.R[i] = (channels == 1) ? l : r;
}
return true;
}
// --- base64 (RFC 4648) — same helpers as RenderJob.cpp ----------------------
static const char* kBase64Tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static std::string base64Encode(const uint8_t* data, size_t len) {
std::string out;
out.reserve(((len + 2) / 3) * 4);
for (size_t i = 0; i < len; i += 3) {
uint32_t n = (uint32_t)data[i] << 16;
if (i + 1 < len) n |= (uint32_t)data[i + 1] << 8;
if (i + 2 < len) n |= (uint32_t)data[i + 2];
out += kBase64Tbl[(n >> 18) & 63];
out += kBase64Tbl[(n >> 12) & 63];
out += (i + 1 < len) ? kBase64Tbl[(n >> 6) & 63] : '=';
out += (i + 2 < len) ? kBase64Tbl[n & 63] : '=';
}
return out;
}
static int b64val(unsigned char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
return -1;
}
static bool base64Decode(const std::string& in, std::vector<uint8_t>& out) {
out.clear();
out.reserve((in.size() / 4) * 3);
uint32_t acc = 0;
int bits = 0;
for (unsigned char ch : in) {
if (ch == '=' || ch == '\n' || ch == '\r' || ch == ' ') continue;
int v = b64val(ch);
if (v < 0) return false;
acc = (acc << 6) | (uint32_t)v;
bits += 6;
if (bits >= 8) {
bits -= 8;
out.push_back((uint8_t)((acc >> bits) & 0xFF));
}
}
return true;
}
// --- VST3 audio-effect host (offline; no MIDI, no GUI) ----------------------
#ifdef HAVE_VST3SDK
using Steinberg::tresult;
using Steinberg::kResultOk;
using Steinberg::kResultTrue;
using Steinberg::kResultFalse;
using Steinberg::kNoInterface;
using Steinberg::FUnknownPtr;
using Steinberg::IPtr;
using Steinberg::owned;
using Steinberg::FIDString;
using Steinberg::int32;
using Steinberg::uint32;
using Steinberg::Vst::IComponent;
using Steinberg::Vst::IEditController;
using Steinberg::Vst::IAudioProcessor;
using Steinberg::Vst::IComponentHandler;
using Steinberg::Vst::IConnectionPoint;
using Steinberg::Vst::ProcessSetup;
using Steinberg::Vst::ProcessContext;
using Steinberg::Vst::HostProcessData;
using Steinberg::Vst::HostApplication;
using Steinberg::Vst::BusInfo;
using Steinberg::Vst::kAudio;
using Steinberg::Vst::kEvent;
using Steinberg::Vst::kInput;
using Steinberg::Vst::kOutput;
using Steinberg::Vst::kRealtime;
using Steinberg::Vst::kSample32;
class HostComponentHandlerFx : public IComponentHandler {
public:
tresult queryInterface(const char*, void** v) override { *v = nullptr; return kNoInterface; }
Steinberg::uint32 addRef() override { return 1; }
Steinberg::uint32 release() override { return 1; }
tresult beginEdit(Steinberg::Vst::ParamID) override { return kResultOk; }
tresult performEdit(Steinberg::Vst::ParamID, Steinberg::Vst::ParamValue) override { return kResultOk; }
tresult endEdit(Steinberg::Vst::ParamID) override { return kResultOk; }
tresult restartComponent(Steinberg::int32) override { return kResultOk; }
};
struct Vst3FxState {
VST3::Hosting::Module::Ptr module; // destroyed LAST (owns the factory)
IPtr<IComponent> component;
IPtr<IEditController> controller;
IPtr<HostApplication> hostApp;
HostProcessData processData;
ProcessContext processContext;
HostComponentHandlerFx componentHandler;
int32 inputChannels = 2;
int32 outputChannels = 2;
bool controllerIsComponent = false;
};
// Class selection for FX: any kVstAudioEffectClass whose subcategory does NOT
// contain "Instrument" (Ozone/ScalerAudio et al.). Pick the first match.
bool pickFxClass(const VST3::Hosting::PluginFactory& factory,
VST3::Hosting::ClassInfo& out) {
auto infos = factory.classInfos(); // temporary vector — copy, never keep pointer
for (const auto& ci : infos) {
if (ci.category() != kVstAudioEffectClass) continue;
if (ci.subCategoriesString().find("Instrument") != std::string::npos) continue;
out = ci;
return true;
}
return false;
}
// Must live OUTSIDE any __try (C2712: no C++ unwinding inside SEH frames).
bool vst3FxLoadInner(Vst3FxState* s, const std::string& path, double sampleRate,
int32 maxBlockSize, std::string& err) {
using namespace VST3::Hosting;
Module::Ptr module = Module::create(path, err);
if (!module) return false;
const PluginFactory& factory = module->getFactory();
ClassInfo chosen;
if (!pickFxClass(factory, chosen)) {
err = "no audio-effect class in " + path;
return false;
}
IPtr<IComponent> component = factory.createInstance<IComponent>(chosen.ID());
if (!component) { err = "createInstance<IComponent> failed"; return false; }
IPtr<HostApplication> hostApp = owned(new HostApplication());
FUnknownPtr<Steinberg::IPluginBase> plugBase(component.get());
if (!plugBase || plugBase->initialize(hostApp) != kResultOk) {
err = "component initialize failed"; return false;
}
IPtr<IEditController> controller;
bool isSingle = false;
if (component->queryInterface(IEditController::iid, (void**)&controller) == kResultTrue) {
isSingle = true;
} else {
Steinberg::TUID cid = {};
tresult cidRes = component->getControllerClassId(cid);
if (cidRes == kResultTrue || cidRes == kResultOk) {
controller = factory.createInstance<IEditController>(VST3::UID(cid));
if (controller) {
FUnknownPtr<Steinberg::IPluginBase> ctrlBase(controller.get());
if (!ctrlBase || ctrlBase->initialize(hostApp) != kResultOk) controller = nullptr;
}
}
}
if (!controller) { err = "no edit controller"; return false; }
s->controllerIsComponent = isSingle;
controller->setComponentHandler(&s->componentHandler);
FUnknownPtr<IConnectionPoint> compCP(component);
FUnknownPtr<IConnectionPoint> ctrlCP(controller);
if (compCP && ctrlCP) { compCP->connect(ctrlCP); ctrlCP->connect(compCP); }
int32 numAudioInputs = component->getBusCount(kAudio, kInput);
for (int32 i = 0; i < numAudioInputs; ++i) component->activateBus(kAudio, kInput, i, true);
int32 numAudioOutputs = component->getBusCount(kAudio, kOutput);
for (int32 i = 0; i < numAudioOutputs; ++i) component->activateBus(kAudio, kOutput, i, true);
int32 numEventInputs = component->getBusCount(kEvent, kInput);
for (int32 i = 0; i < numEventInputs; ++i) component->activateBus(kEvent, kInput, i, true);
int32 numEventOutputs = component->getBusCount(kEvent, kOutput);
for (int32 i = 0; i < numEventOutputs; ++i) component->activateBus(kEvent, kOutput, i, true);
if (numAudioInputs < 1 || numAudioOutputs < 1) {
err = "plugin has no audio in/out (instrument?)"; return false;
}
FUnknownPtr<IAudioProcessor> processor(component);
if (!processor) { err = "no IAudioProcessor"; return false; }
ProcessSetup setup{kRealtime, kSample32, maxBlockSize, sampleRate};
if (processor->setupProcessing(setup) != kResultOk) { err = "setupProcessing failed"; return false; }
if (component->setActive(true) != kResultOk) { err = "setActive failed"; return false; }
processor->setProcessing(true);
if (!s->processData.prepare(*component, maxBlockSize, kSample32)) {
err = "processData.prepare failed"; return false;
}
BusInfo bi = {};
if (component->getBusInfo(kAudio, kInput, 0, bi) == kResultOk && bi.channelCount > 0)
s->inputChannels = bi.channelCount;
bi = {};
if (component->getBusInfo(kAudio, kOutput, 0, bi) == kResultOk && bi.channelCount > 0)
s->outputChannels = bi.channelCount;
s->module = std::move(module);
s->component = std::move(component);
s->controller = std::move(controller);
s->hostApp = std::move(hostApp);
s->processContext.sampleRate = sampleRate;
s->processContext.tempo = 120.0;
s->processContext.timeSigNumerator = 4;
s->processContext.timeSigDenominator = 4;
s->processContext.state = ProcessContext::kPlaying | ProcessContext::kTempoValid |
ProcessContext::kTimeSigValid | ProcessContext::kProjectTimeMusicValid;
std::cout << "[RenderFx] VST3 FX loaded " << path << " (" << chosen.name()
<< ") in=" << s->inputChannels << " out=" << s->outputChannels << std::endl;
return true;
}
#endif // HAVE_VST3SDK
class Vst3Fx {
public:
Vst3Fx() = default;
~Vst3Fx() {
#ifdef HAVE_VST3SDK
if (!state_) return;
auto* s = static_cast<Vst3FxState*>(state_);
if (s->component) {
FUnknownPtr<IAudioProcessor> processor(s->component);
if (processor) processor->setProcessing(false);
s->component->setActive(false);
s->component->terminate();
}
if (s->controller && !s->controllerIsComponent) s->controller->terminate();
s->processData.unprepare();
delete s;
state_ = nullptr;
#endif
}
bool load(const std::string& path, double sampleRate, int32 maxBlockSize) {
#ifndef HAVE_VST3SDK
(void)path; (void)sampleRate; (void)maxBlockSize;
return false;
#else
if (state_) return true;
auto* s = new Vst3FxState();
std::string err;
if (!vst3FxLoadInner(s, path, sampleRate, maxBlockSize, err)) {
std::cerr << "[RenderFx] VST3 FX load FAILED: " << err << std::endl;
delete s;
return false;
}
state_ = s;
return true;
#endif
}
// Audio in → audio out, one block. Must be SEH-wrapped by the caller.
void processAudio(const float* inL, const float* inR,
float* outL, float* outR, uint32_t n) {
#ifndef HAVE_VST3SDK
(void)inL; (void)inR; (void)outL; (void)outR; (void)n;
#else
auto* s = static_cast<Vst3FxState*>(state_);
if (!s || !s->component || n == 0) return;
FUnknownPtr<IAudioProcessor> processor(s->component);
if (!processor) return;
s->processData.processMode = kRealtime;
s->processData.numSamples = (int32)n;
s->processData.inputEvents = nullptr;
s->processData.inputParameterChanges = nullptr;
s->processData.processContext = &s->processContext;
s->processContext.projectTimeSamples += n;
s->processContext.projectTimeMusic =
(double)s->processContext.projectTimeSamples / s->processContext.sampleRate *
(s->processContext.tempo / 60.0);
// Copy input into the prepared (owned) input buffers — direct write,
// setChannelBuffers is unusable (channelBufferOwner=true after prepare).
if (s->processData.numInputs > 0) {
const Steinberg::Vst::AudioBusBuffers& ib = s->processData.inputs[0];
for (int32 c = 0; c < ib.numChannels; ++c) {
float* dst = ib.channelBuffers32[c];
if (!dst) continue;
const float* src = (c == 0) ? inL : (c == 1 ? inR : inL);
std::memcpy(dst, src, n * sizeof(float));
}
}
// Host buffers must be zeroed before process (plugins skip output
// leave garbage otherwise — same rule as Vst3Instrument).
if (s->processData.numOutputs > 0) {
for (int32 b = 0; b < s->processData.numOutputs; ++b) {
const Steinberg::Vst::AudioBusBuffers& ob = s->processData.outputs[b];
for (int32 c = 0; c < ob.numChannels; ++c)
if (ob.channelBuffers32[c])
std::memset(ob.channelBuffers32[c], 0, n * sizeof(float));
}
}
tresult pr = processor->process(s->processData);
if (pr >= 0 && s->processData.numOutputs > 0) {
const Steinberg::Vst::AudioBusBuffers& out = s->processData.outputs[0];
const float* buf0 = out.numChannels > 0 ? out.channelBuffers32[0] : nullptr;
const float* buf1 = out.numChannels > 1 ? out.channelBuffers32[1] : nullptr;
if (buf0) std::memcpy(outL, buf0, n * sizeof(float));
else std::memset(outL, 0, n * sizeof(float));
if (buf1) std::memcpy(outR, buf1, n * sizeof(float));
else if (buf0) std::memcpy(outR, buf0, n * sizeof(float)); // mono → stereo
else std::memset(outR, 0, n * sizeof(float));
} else {
std::memset(outL, 0, n * sizeof(float));
std::memset(outR, 0, n * sizeof(float));
}
#endif
}
// Apply preset: base64 of a raw .vstpreset FILE (VST3 magic + chunk list)
// OR of the bridge state blob [4B BE compLen][comp][4B BE ctrlLen][ctrl].
bool applyPreset(const std::string& b64) {
#ifndef HAVE_VST3SDK
(void)b64;
return false;
#else
std::vector<uint8_t> bytes;
if (!base64Decode(b64, bytes) || bytes.empty()) return false;
std::vector<uint8_t> comp, ctrl;
if (bytes.size() >= 4 && std::memcmp(bytes.data(), "VST3", 4) == 0) {
// Raw .vstpreset file: parse with the SDK PresetFile.
Steinberg::MemoryStream stream;
if (stream.write(bytes.data(), (int32)bytes.size(), nullptr) != Steinberg::kResultOk) return false;
stream.seek(0, Steinberg::IBStream::kIBSeekSet, nullptr);
Steinberg::Vst::PresetFile pf(&stream);
if (!pf.readChunkList()) return false;
auto readChunk = [&](Steinberg::Vst::ChunkType which, std::vector<uint8_t>& outc) -> bool {
const Steinberg::Vst::PresetFile::Entry* e = pf.getEntry(which);
if (!e) return true;
bool seeked = (which == Steinberg::Vst::kComponentState)
? pf.seekToComponentState() : pf.seekToControllerState();
if (!seeked) return false;
outc.resize((size_t)e->size);
if (e->size == 0) return true;
int32 got = 0;
return stream.read(outc.data(), (int32)e->size, &got) == Steinberg::kResultOk &&
got == e->size;
};
if (!readChunk(Steinberg::Vst::kComponentState, comp) ||
!readChunk(Steinberg::Vst::kControllerState, ctrl))
return false;
} else {
// Bridge state blob.
auto read32 = [&bytes](size_t off) -> uint32_t {
return ((uint32_t)bytes[off] << 24) | ((uint32_t)bytes[off + 1] << 16) |
((uint32_t)bytes[off + 2] << 8) | (uint32_t)bytes[off + 3];
};
if (bytes.size() < 8) return false;
size_t off = 0;
uint32_t clen = read32(off); off += 4;
if (off + clen > bytes.size()) return false;
comp.assign(bytes.begin() + off, bytes.begin() + off + clen);
off += clen;
if (off + 4 > bytes.size()) return false;
uint32_t klen = read32(off); off += 4;
if (off + klen > bytes.size()) return false;
ctrl.assign(bytes.begin() + off, bytes.begin() + off + klen);
}
auto* s = static_cast<Vst3FxState*>(state_);
if (!s) return false;
if (!comp.empty()) {
Steinberg::MemoryStream stream;
stream.write(comp.data(), (int32)comp.size(), nullptr);
stream.seek(0, Steinberg::IBStream::kIBSeekSet, nullptr);
if (!s->component || s->component->setState(&stream) != kResultOk) return false;
}
if (!ctrl.empty()) {
Steinberg::MemoryStream stream;
stream.write(ctrl.data(), (int32)ctrl.size(), nullptr);
stream.seek(0, Steinberg::IBStream::kIBSeekSet, nullptr);
if (!s->controller || s->controller->setState(&stream) != kResultOk) return false;
}
std::cout << "[RenderFx] preset applied component=" << comp.size()
<< " controller=" << ctrl.size() << std::endl;
return true;
#endif
}
bool loaded() const { return state_ != nullptr; }
private:
void* state_ = nullptr;
};
// One FX slot from the job. builtin id: "gain" | "normalize".
struct FxSlot {
bool vst = false; // true = VST3, false = builtin
std::string path; // vst only
std::string presetB64; // vst only
bool bypass = false;
std::string builtinId; // builtin only
double db = 0.0; // gain param
double peak = 0.95; // normalize param
std::unique_ptr<Vst3Fx> fx;
};
} // namespace
// SEH-guarded single-slot process call. Kept OUT of run_render_fx_job's frame
// (C2712: __try cannot live in a function whose C++ objects need unwinding).
// This frame holds only references/raw pointers — nothing to unwind.
static bool runFxSlotSafe(const FxSlot& sl, const float* inL, const float* inR,
float* outL, float* outR, uint32_t n, uint32_t* crashCode) {
#ifdef _WIN32
__try {
sl.fx->processAudio(inL, inR, outL, outR, n);
return true;
} __except (*crashCode = (uint32_t)GetExceptionCode(), EXCEPTION_EXECUTE_HANDLER) {
return false;
}
#else
sl.fx->processAudio(inL, inR, outL, outR, n);
return true;
#endif
}
int run_render_fx_job(const std::string& jobPath, const std::string& inPath,
const std::string& outPath) {
#ifdef _WIN32
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
struct ComGuard { ~ComGuard() { CoUninitialize(); } } comGuard;
#endif
int rc = 1;
std::ofstream wav;
auto fail = [&](const std::string& msg) {
std::cerr << "[RenderFx] " << msg << std::endl;
if (wav.is_open()) wav.close();
std::remove(outPath.c_str());
};
// 1. Read input WAV.
WavIn in;
if (!readWav(inPath, in)) { fail("cannot read input WAV: " + inPath); return rc; }
if (in.L.empty()) { fail("input WAV is empty"); return rc; }
// 2. Parse job.
std::ifstream jf(jobPath, std::ios::binary);
if (!jf) { fail("cannot read job file: " + jobPath); return rc; }
std::string data((std::istreambuf_iterator<char>(jf)), std::istreambuf_iterator<char>());
json_parse_result_s pres = {};
json_value_s* root = json_parse_ex(data.data(), data.size(), json_parse_flags_default,
nullptr, nullptr, &pres);
if (!root) { fail("job JSON parse error (code " + std::to_string(pres.error) + ")"); return rc; }
struct RootHolder { json_value_s* p = nullptr; ~RootHolder() { if (p) std::free(p); } } rootH;
rootH.p = root;
if (root->type != json_type_object) { fail("job root must be a JSON object"); return rc; }
const json_object_s* job = static_cast<const json_object_s*>(root->payload);
double srD = (double)in.sampleRate;
memberNumber(job, "sample_rate", srD);
if (srD < 8000.0 || srD > 192000.0) { fail("bad sample_rate: " + std::to_string(srD)); return rc; }
const uint32_t sampleRate = (uint32_t)srD;
if (in.sampleRate != sampleRate) {
fail("input WAV sr (" + std::to_string(in.sampleRate) + ") != job sample_rate ("
+ std::to_string(sampleRate) + ")"); return rc;
}
int64_t block = memberInt(job, "block_size", 512);
block = std::max<int64_t>(32, std::min<int64_t>(block, 4096));
const uint32_t chunk = (uint32_t)block;
// 3. Build slot list.
std::vector<FxSlot> slots;
const json_value_s* chainV = memberValue(job, "fx_chain");
if (chainV && chainV->type == json_type_array) {
const json_array_s* chain = static_cast<const json_array_s*>(chainV->payload);
for (const json_array_element_s* el = chain->start; el; el = el->next) {
const json_value_s* v = el->value;
if (!v || v->type != json_type_object) continue;
const json_object_s* o = static_cast<const json_object_s*>(v->payload);
const std::string type = memberString(o, "type", "");
FxSlot sl;
sl.bypass = memberBool(o, "bypass", false);
if (type == "vst3" || type == "vst") {
sl.vst = true;
sl.path = memberString(o, "path", "");
sl.presetB64 = memberString(o, "preset_b64", "");
} else if (type == "builtin") {
sl.vst = false;
sl.builtinId = memberString(o, "id", "");
const json_value_s* pv = memberValue(o, "params");
if (pv && pv->type == json_type_object) {
const json_object_s* po = static_cast<const json_object_s*>(pv->payload);
double d = sl.db; memberNumber(po, "db", d); sl.db = d;
double pk = sl.peak; memberNumber(po, "peak", pk); sl.peak = pk;
}
} else {
fail("unknown fx_chain slot type: " + type); return rc;
}
slots.push_back(std::move(sl));
}
}
if (slots.empty()) { fail("fx_chain is empty — nothing to render"); return rc; }
// 4. Load VST3 slots (bypassed slots stay unloaded → passthrough).
for (auto& sl : slots) {
if (!sl.vst || sl.bypass) continue;
if (sl.path.empty()) { fail("vst3 slot missing path"); return rc; }
auto fx = std::make_unique<Vst3Fx>();
if (!fx->load(sl.path, sampleRate, (int32)chunk)) { rc = 2; fail("VST3 FX load failed: " + sl.path); return rc; }
if (!sl.presetB64.empty() && !fx->applyPreset(sl.presetB64)) {
fail("preset import FAILED for " + sl.path); return rc;
}
sl.fx = std::move(fx);
}
// 5. Process chain block-wise. Double-buffered: cur ← slot → nxt, swap.
std::vector<float> L0(chunk), R0(chunk), L1(chunk), R1(chunk);
std::vector<float> curL(chunk), curR(chunk);
bool normalizePending = false;
double normalizePeak = 0.95;
uint32_t crashCode = 0;
for (const auto& sl : slots) {
if (sl.bypass) continue;
if (sl.vst && !sl.fx) continue; // defensive: bypassed/unloaded
if (!sl.vst && sl.builtinId == "normalize") {
normalizePending = true;
normalizePeak = std::max(0.01, std::min(1.0, sl.peak));
continue; // global post-pass
}
const bool isVst = sl.vst;
const double gainLin = sl.vst ? 1.0 : std::pow(10.0, sl.db / 20.0);
for (uint64_t pos = 0; pos < in.L.size(); pos += chunk) {
const uint32_t n = (uint32_t)std::min<uint64_t>(chunk, in.L.size() - pos);
std::memcpy(curL.data(), in.L.data() + pos, n * sizeof(float));
std::memcpy(curR.data(), in.R.data() + pos, n * sizeof(float));
if (isVst) {
bool ok = runFxSlotSafe(sl, curL.data(), curR.data(), L0.data(), R0.data(),
n, &crashCode);
if (!ok) { rc = 3; fail("VST3 FX crashed inside processAudio (SEH code=0x"
+ std::to_string(crashCode) + ")"); return rc; }
std::memcpy(in.L.data() + pos, L0.data(), n * sizeof(float));
std::memcpy(in.R.data() + pos, R0.data(), n * sizeof(float));
} else {
for (uint32_t i = 0; i < n; ++i) {
in.L[pos + i] = curL[i] * (float)gainLin;
in.R[pos + i] = curR[i] * (float)gainLin;
}
}
}
if (isVst) std::cout << "[RenderFx] slot processed: " << sl.path << std::endl;
else std::cout << "[RenderFx] slot processed: builtin " << sl.builtinId << std::endl;
}
// 6. Global normalize pass (only reduces when peak exceeds target — mirrors
// render_project's clip guard; the final mastering slot by convention).
if (normalizePending) {
float peak = 0.f;
for (float v : in.L) peak = std::max(peak, std::fabs(v));
for (float v : in.R) peak = std::max(peak, std::fabs(v));
if (peak > (float)normalizePeak && peak > 1e-9f) {
const float k = (float)normalizePeak / peak;
for (float& v : in.L) v *= k;
for (float& v : in.R) v *= k;
std::cout << "[RenderFx] normalize: peak " << peak << " → " << normalizePeak << std::endl;
} else {
std::cout << "[RenderFx] normalize: peak " << peak << " ≤ " << normalizePeak << " (no-op)" << std::endl;
}
}
// 7. Write output WAV.
wav.open(outPath, std::ios::binary);
if (!wav) { fail("cannot open output: " + outPath); return rc; }
writeWavHeader(wav, sampleRate);
uint64_t dataBytes = 0;
for (uint64_t pos = 0; pos < in.L.size(); pos += chunk) {
const uint32_t n = (uint32_t)std::min<uint64_t>(chunk, in.L.size() - pos);
writeFrames(wav, in.L.data() + pos, in.R.data() + pos, n);
dataBytes += (uint64_t)n * 4;
}
finishWav(wav, dataBytes);
wav.close();
std::cout << "[RenderFx] wrote " << outPath << " (" << in.L.size() << " frames)" << std::endl;
rc = 0;
return rc;
}
// --- --scan: classify VST3 modules as instrument/effect ----------------------
namespace {
std::string jsonEscape(const std::string& s) {
std::string o;
o.reserve(s.size() + 8);
for (char ch : s) {
switch (ch) {
case '"': o += "\\\""; break;
case '\\': o += "\\\\"; break;
case '\n': o += "\\n"; break;
case '\r': o += "\\r"; break;
case '\t': o += "\\t"; break;
default: o += ch;
}
}
return o;
}
#ifdef HAVE_VST3SDK
struct ScanEntry {
std::string path;
std::string name;
bool isFx = false;
bool isInstrument = false;
};
// C++ locals live here (no __try in this frame) — a crashing module loader
// is caught by safeScanModule's SEH and skipped, not fatal to the whole scan.
bool scanModuleInner(const std::string& path, std::vector<ScanEntry>& out) {
using namespace VST3::Hosting;
std::string err;
Module::Ptr module = Module::create(path, err);
if (!module) { std::cerr << "[Scan] skip (module load failed): " << path << " — " << err << std::endl; return false; }
const PluginFactory& factory = module->getFactory();
auto infos = factory.classInfos();
int added = 0;
for (const auto& ci : infos) {
if (ci.category() != kVstAudioEffectClass) continue;
const bool isInstr = ci.subCategoriesString().find("Instrument") != std::string::npos;
ScanEntry e;
e.path = path;
e.name = ci.name();
e.isFx = !isInstr;
e.isInstrument = isInstr;
out.push_back(std::move(e));
++added;
}
if (added == 0)
std::cerr << "[Scan] " << path << ": no audio classes" << std::endl;
return added > 0;
}
bool safeScanModule(const std::string& path, std::vector<ScanEntry>& out) {
#ifdef _WIN32
__try { return scanModuleInner(path, out); }
__except (EXCEPTION_EXECUTE_HANDLER) {
std::cerr << "[Scan] skip (crashed module): " << path << std::endl;
return false;
}
#else
return scanModuleInner(path, out);
#endif
}
#endif // HAVE_VST3SDK
} // namespace
int run_scan_dir(const std::string& dirPath) {
#ifdef _WIN32
CoInitializeEx(nullptr, COINIT_MULTITHREADED);
struct ComGuard { ~ComGuard() { CoUninitialize(); } } comGuard;
#endif
std::vector<std::string> vst3Paths; // folders or files ending .vst3
std::vector<std::string> vst2Paths; // .dll / .so (cannot introspect)
namespace fs = std::filesystem;
auto isExt = [](const std::string& p, const char* ext) {
const size_t n = std::strlen(ext);
return p.size() >= n && p.compare(p.size() - n, n, ext) == 0;
};
try {
fs::recursive_directory_iterator it(dirPath), end;
for (; it != end; ++it) {
const std::string p = it->path().string();
std::string low = p;
for (auto& c : low) c = (char)std::tolower((unsigned char)c);
if (it->is_directory()) {
if (isExt(low, ".vst3")) {
vst3Paths.push_back(p);
it.disable_recursion_pending(); // bundle: don't walk Contents/
}
} else if (it->is_regular_file()) {
if (isExt(low, ".vst3")) vst3Paths.push_back(p);
else if (isExt(low, ".dll") || isExt(low, ".so")) vst2Paths.push_back(p);
}
}
} catch (...) { std::cerr << "[Scan] walk failed: " << dirPath << std::endl; }
std::cout << "{\"plugins\":[";
bool first = true;
auto emit = [&](const std::string& path, const std::string& name,
bool isFx, bool isInstr, const char* type) {
if (!first) std::cout << ",";
first = false;
std::cout << "{\"path\":\"" << jsonEscape(path)
<< "\",\"name\":\"" << jsonEscape(name)
<< "\",\"is_fx\":" << (isFx ? "true" : "false")
<< ",\"is_instrument\":" << (isInstr ? "true" : "false")
<< ",\"type\":\"" << type << "\"}";
};
#ifdef HAVE_VST3SDK
for (const auto& p : vst3Paths) {
std::vector<ScanEntry> entries;
if (!safeScanModule(p, entries)) {
// Module unreadable (or crashed): report as opaque instrument-like
// entry so the UI still lists it (VST2 default behavior).
emit(p, p.substr(p.find_last_of("/\\") + 1), false, true, "VST3");
continue;
}
for (const auto& e : entries) emit(e.path, e.name, e.isFx, e.isInstrument, "VST3");
}
#else
for (const auto& p : vst3Paths) {
std::string name = p.substr(p.find_last_of("/\\") + 1);
emit(p, name, false, true, "VST3");
}
#endif
for (const auto& p : vst2Paths) {
std::string name = p.substr(p.find_last_of("/\\") + 1);
emit(p, name, false, true, "VST2");
}
std::cout << "]}" << std::endl;
return 0;
}
+24
View File
@@ -6,6 +6,7 @@
#include "NativeInstrumentEngine.h"
#include "StateStore.h"
#include "RenderJob.h"
#include "RenderFxJob.h"
#ifdef _WIN32
#include <windows.h>
@@ -614,6 +615,29 @@ int main(int argc, char* argv[]) {
return run_render_job(renderJob, renderOut);
}
// --render-fx <job.json> --in <input.wav> --out <output.wav>: one-shot
// offline FX render (VST3 effects + builtins), no SHM/loop.
std::string renderFxJob, renderFxIn, renderFxOut;
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--render-fx") == 0 && i + 1 < argc) renderFxJob = argv[i + 1];
else if (std::strcmp(argv[i], "--in") == 0 && i + 1 < argc) renderFxIn = argv[i + 1];
else if (std::strcmp(argv[i], "--out") == 0 && i + 1 < argc) renderFxOut = argv[i + 1];
}
if (!renderFxJob.empty()) {
if (renderFxIn.empty() || renderFxOut.empty()) {
std::cerr << "[RenderFxJob] --render-fx requires --in <input.wav> --out <output.wav>" << std::endl;
return 1;
}
return run_render_fx_job(renderFxJob, renderFxIn, renderFxOut);
}
// --scan <dir>: one-shot plugin scan, no SHM/loop.
std::string scanDir;
for (int i = 1; i + 1 < argc; ++i) {
if (std::strcmp(argv[i], "--scan") == 0) scanDir = argv[i + 1];
}
if (!scanDir.empty()) return run_scan_dir(scanDir);
// 1. Shared memory name: argv --shm <name> | env SF_SHM_NAME | default
std::string shmName = "SonicForge_DAW_IPC";
for (int i = 1; i + 1 < argc; ++i) {