refactor(bridge): split fx bridge, stabilize VST3 host chain, drop old install engines

- remove diagnostic canaries from audio hot path (crackle fix) and
  stuck C5 note raw-MIDI test (Vst2Instrument::init noteOff)
- split native bridge into daw_vst_bridge (VSTi) + fx_vst_bridge (VST FX)
- VST3: processData prepare before setActive, zero input buses, accept
  both .vstpreset and bridge-state blob in RenderJob presets
- native_render/vst_engine: bridge-exe discovery + render_fx_chain + /fx-gui
- delete old install/ engine binaries (install via sidecar+resources now)
- add BUILD_LAYOUT.md, build_selfcheck, tools/ diagnostics
This commit is contained in:
2026-08-20 09:42:42 +07:00
parent 5df96aef1e
commit 7c22ae1f02
42 changed files with 2363 additions and 210 deletions
+2 -37
View File
@@ -24,7 +24,6 @@
#include <thread>
#include <chrono>
#ifdef _WIN32
#include <malloc.h> // _heapchk (TEMP diag)
#endif
// -----------------------------------------------------------------
@@ -438,9 +437,8 @@ void InstrumentEngineManager::allNotesOff() {
// audio buffers; plain std::vector is only 16B-aligned. Over-allocate and
// align the pointer handed to plugins to 64B.
static float* alignF(std::vector<float>& v, size_t n) {
// 64B-aligned pointer can sit up to 63B into the allocation; TEMP diag:
// huge tail so we can measure how far DUNE writes past n.
const size_t kTail = 2097152 + 64;
// 64B-aligned pointer can sit up to 63B into the allocation.
const size_t kTail = 64;
if (v.size() < n + kTail) v.assign(n + kTail, 0.0f);
uintptr_t a = reinterpret_cast<uintptr_t>(v.data());
return reinterpret_cast<float*>((a + 63) & ~(uintptr_t)63);
@@ -562,17 +560,6 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
if (channelQuiet(ch)) continue; // editor open / crashed: do not process
float* spL = alignF(scratchL_, numSamples);
float* spR = alignF(scratchR_, numSamples);
std::cerr << "[NativeBridge][diag] renderAll ch=" << ch << " n=" << numSamples
<< " spL=0x" << std::hex << (uintptr_t)spL
<< " spR=0x" << (uintptr_t)spR
<< " capL=" << std::dec << scratchL_.size()
<< " capR=" << scratchR_.size() << std::endl;
// TEMP diag: fill canary with a NaN pattern so writes of 0.0 (silence)
// are still detected.
for (uint32_t i = 0; i < numSamples + 2097152; ++i) {
spL[i] = 0x7FC00000u; // quiet NaN
spR[i] = 0x7FC00000u;
}
#ifdef _WIN32
DWORD sehCode = 0;
#else
@@ -617,28 +604,6 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
}
continue;
}
// TEMP diag: plugin output-overrun canary + heap health.
{
static uint64_t diagBlock = 0; ++diagBlock;
int farL = -1, farR = -1;
for (uint32_t i = 0; i < 2097152; ++i) {
uint32_t bL, bR;
std::memcpy(&bL, &spL[numSamples + i], 4);
std::memcpy(&bR, &spR[numSamples + i], 4);
if (bL != 0x7FC00000u) farL = (int)i;
if (bR != 0x7FC00000u) farR = (int)i;
}
if (farL >= 0 || farR >= 0)
std::cerr << "[NativeBridge][diag] block " << diagBlock
<< " OUTPUT OVERRUN past n: L=" << farL
<< " R=" << farR << " (ch=" << ch << ")" << std::endl;
#ifdef _WIN32
int h = _heapchk();
if (h != _HEAPOK)
std::cerr << "[NativeBridge][diag] block " << diagBlock
<< " heap NOT OK code=" << h << std::endl;
#endif
}
// Auto-normalize: makeup calibrated (constant per-channel gain, no steps).
auto mit = makeup_.find(ch);
const float g = (mit != makeup_.end()) ? mit->second : 1.0f;
+17 -4
View File
@@ -351,11 +351,24 @@ int run_render_job(const std::string& jobPath, const std::string& outPath) {
std::cerr << "[RenderJob] preset ignored for non-VST3 type " << (int)type
<< " (SF2/SFZ use soundfont_bank/soundfont_program)" << std::endl;
#ifdef HAVE_VST3SDK
} else if (!applyVst3Preset(mgr.get(0), preset)) {
fail("preset import FAILED for " + pluginPath);
return rc; // rc stays 1: job/argument error
} else {
std::cout << "[RenderJob] preset imported (VST3 .vstpreset) type=" << (int)type << std::endl;
// Accept BOTH .vstpreset ('VST3' magic) and the bridge state
// blob ([4B BE compLen][comp][4B BE ctrlLen][ctrl]) -- realtime
// LOAD restores with loadSerializedState, so offline --render
// must too (plugins whose default preset is silent -- Ample,
// Toontrack, Kong Audio -- otherwise render digital silence).
std::vector<uint8_t> head;
bool isVstPreset = base64Decode(preset, head) && head.size() >= 4 &&
std::memcmp(head.data(), "VST3", 4) == 0;
if (!isVstPreset) {
mgr.get(0)->loadSerializedState(preset);
std::cout << "[RenderJob] preset restored (bridge state blob) type=" << (int)type << std::endl;
} else if (!applyVst3Preset(mgr.get(0), preset)) {
fail("preset import FAILED for " + pluginPath);
return rc; // rc stays 1: job/argument error
} else {
std::cout << "[RenderJob] preset imported (VST3 .vstpreset) type=" << (int)type << std::endl;
}
}
#else
} else {
+4 -113
View File
@@ -25,8 +25,6 @@ std::unordered_map<const void*, Vst2Instrument*> g_v2Registry;
// the callback may be invoked with a null effect (some plugins query the
// sample rate during construction).
thread_local Vst2Instrument* t_loading = nullptr;
// TEMP probe: log every audioMaster call while effEditOpen is running.
thread_local bool t_editingProbe = false;
// Resize hook registered by the bridge (main.cpp): plugin audioMaster
// (SizeWindow) -> move the host window on the UI worker thread.
static void (*g_vst2ResizeHook)(void*, int, int) = nullptr;
@@ -98,9 +96,6 @@ struct Vst2HostState {
intptr_t audioMaster(AEffect* effect, int32_t opcode, int32_t index,
intptr_t value, void* ptr, float opt) {
(void)index; (void)value; (void)ptr; (void)opt;
if (t_editingProbe || true)
std::cerr << "[Vst2Instrument][editprobe] audioMaster op=" << opcode
<< " idx=" << index << " val=" << value << " ptr=" << ptr << std::endl;
Vst2Instrument* self = nullptr;
if (effect) {
std::lock_guard<std::mutex> lock(g_v2Mutex);
@@ -155,7 +150,6 @@ intptr_t audioMaster(AEffect* effect, int32_t opcode, int32_t index,
ti.ppqPos = pos * 120.0 / 60.0 / 44100.0;
ti.flags = kVstTempoValid | kVstTransportPlaying; // TEMP diag: DUNE silent - try forcing transport playing
pos += (self ? self->maxBlockSize() : 512);
std::cerr << "[Vst2Instrument][diag] getTime flags=" << ti.flags << " pos=" << pos << std::endl;
return (intptr_t)&ti;
}
case kAudioMasterGetCurrentProcessLevel: return 1; // audio thread
@@ -268,9 +262,7 @@ static intptr_t v2_dispatch(AEffect* fx, int32_t op, int32_t idx, intptr_t val,
static void* g_sehAddr = nullptr;
static CONTEXT g_ctxCopy;
__try {
if (op == kEffectEditOpen) t_editingProbe = true;
intptr_t r = fx->dispatcher(fx, op, idx, val, ptr, opt);
if (op == kEffectEditOpen) t_editingProbe = false;
return r;
} __except (g_sehCode = GetExceptionCode(),
g_sehAddr = GetExceptionInformation()->ExceptionRecord->ExceptionAddress,
@@ -388,73 +380,11 @@ bool Vst2Instrument::init(double sampleRate, uint32_t maxBlockSize) {
maxBlockSize_ = maxBlockSize;
AEffect* fx = s->effect;
try {
// TEMP diag variant B: sampleRate/blockSize BEFORE effOpen, block size
// matches the actual render size (256), no StartProcess.
fx->dispatcher(fx, kEffectSetSampleRate, 0, 0, nullptr, (float)sampleRate);
fx->dispatcher(fx, kEffectSetBlockSize, 0, (intptr_t)maxBlockSize, nullptr, 0);
fx->dispatcher(fx, kEffectOpen, 0, 0, nullptr, 0);
fx->dispatcher(fx, kEffectSetProgram, 0, 0, nullptr, 0);
fx->dispatcher(fx, kEffectMainsChanged, 0, 1, nullptr, 0);
// TEMP diag: dump first params to see if the default patch is audible
{
const int pis[] = {0, 1, 10, 100, 500, 1000, 1160, 1168};
for (int pi : pis) {
if (pi >= fx->numParams) continue;
char nm[128] = {0}, disp[128] = {0}, lbl[128] = {0};
fx->dispatcher(fx, kEffectGetParamName, 0, pi, nm, 0);
fx->dispatcher(fx, kEffectGetParamDisplay, 0, pi, disp, 0);
fx->dispatcher(fx, kEffectGetParamLabel, 0, pi, lbl, 0);
std::cerr << "[Vst2Instrument][diag] param " << pi << " name=" << nm << " disp=" << disp << " label=" << lbl << std::endl;
}
}
// TEMP diag: does the plugin itself claim to receive MIDI?
{
const char* probes[] = {"receiveVstEvents", "receiveVstMidiEvent", "midiProgramNames", "bypass"};
for (auto pr : probes) {
intptr_t r = fx->dispatcher(fx, kEffectCanDo, 0, 0, (void*)pr, 0);
std::cerr << "[Vst2Instrument][diag] plugin canDo " << pr << " -> " << r << std::endl;
}
}
// TEMP diag: send raw MIDI directly (bypass SHM path). CC7 vol max + noteOn
// with kVstMidiEventIsRealtime + retriggers.
{
auto sendMidi = [&](int b0, int b1, int b2) {
VstMidiEvent e;
std::memset(&e, 0, sizeof(e));
e.type = kVstMidiType;
e.byteSize = (int32_t)sizeof(e);
e.flags = kVstMidiEventIsRealtime;
e.midiData[0] = (char)b0; e.midiData[1] = (char)b1; e.midiData[2] = (char)b2;
VstEvent* evs[1] = { reinterpret_cast<VstEvent*>(&e) };
VstEvents events;
std::memset(&events, 0, sizeof(events));
events.numEvents = 1;
events.events[0] = evs[0];
fx->dispatcher(fx, kEffectProcessEvents, 0, 0, &events, 0);
};
std::cerr << "[Vst2Instrument][diag] raw CC7 127 + noteOn 60/100 x8" << std::endl;
sendMidi(0xB0, 7, 127);
for (int r = 0; r < 8; ++r) sendMidi(0x90, 60, 100);
}
// TEMP diag: does DUNE hold a preset chunk? dump its head as hex+ascii
{
void* chunkPtr = nullptr;
intptr_t sz = fx->dispatcher(fx, kEffectGetChunk, 0, 1, &chunkPtr, 0);
std::cerr << "[Vst2Instrument][diag] getChunk size=" << sz << " ptr=" << chunkPtr << std::endl;
if (sz > 0 && chunkPtr) {
const unsigned char* b = (const unsigned char*)chunkPtr;
std::string ascii;
for (intptr_t i = 0; i < sz && i < 512; ++i) {
ascii += (b[i] >= 32 && b[i] < 127) ? (char)b[i] : '.';
}
std::cerr << "[Vst2Instrument][diag] chunkhead: " << ascii << std::endl;
std::cerr << "[Vst2Instrument][diag] chunkbytes: ";
for (intptr_t i = 0; i < sz && i < 64; ++i) {
char hx[8]; snprintf(hx, sizeof(hx), "%02x ", b[i]); std::cerr << hx;
}
std::cerr << std::endl;
}
}
} catch (...) {
std::cerr << "[Vst2Instrument] init dispatcher EXCEPTION ch=" << channel_ << std::endl;
return false;
@@ -683,29 +613,25 @@ void Vst2Instrument::loadSerializedState(const std::string& base64) {
}
void Vst2Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
std::cerr << "[Vst2Instrument] pab START n=" << numSamples << std::endl;
auto* s = static_cast<Vst2HostState*>(state_);
std::cerr << "[Vst2Instrument] pab state ok=" << (s && s->effect) << std::endl;
if (!s || !s->effect) {
std::memset(outputL, 0, numSamples * sizeof(float));
std::memset(outputR, 0, numSamples * sizeof(float));
return;
}
std::cerr << "[Vst2Instrument] pab before alignF" << std::endl;
AEffect* fx = s->effect;
// 64B-align inputs: DUNE 3 etc. use aligned SIMD on audio buffers.
auto alignF = [](std::vector<float>& v, size_t n) -> float* {
// TEMP diag (heap-corrupt hunt): huge tail + input canary to measure
// how far DUNE writes past n.
const size_t kTail = 2097152 + 64;
// 64B-aligned pointer can sit up to 63B into the allocation.
const size_t kTail = 64;
if (v.size() < n + kTail) v.assign(n + kTail, 0.0f);
uintptr_t a = reinterpret_cast<uintptr_t>(v.data());
return reinterpret_cast<float*>((a + 63) & ~(uintptr_t)63);
};
float* inL = alignF(inL_, numSamples);
float* inR = alignF(inR_, numSamples);
for (uint32_t i = 0; i < numSamples + 2097152; ++i) {
inL[i] = 0.0f; // TEMP: clean input (NaN poisons effect plugins)
for (uint32_t i = 0; i < numSamples; ++i) {
inL[i] = 0.0f;
inR[i] = 0.0f;
}
// Build pointer arrays covering the plugin's full ins/outs: plugins with
@@ -731,42 +657,7 @@ void Vst2Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t
}
if (nIn == 0)
inBufs[0] = nullptr; // ins=0: pass NULL input array entry
std::cerr << "[Vst2Instrument] processReplacing IN ch=" << channel_ << " n=" << numSamples
<< " ins=" << nIn << " outs=" << nOut
<< " inL=0x" << std::hex << (uintptr_t)inL << " inR=0x" << (uintptr_t)inR
<< " outL=0x" << (uintptr_t)outputL << " outR=0x" << (uintptr_t)outputR
<< std::dec << std::endl;
fx->processReplacing(fx, inBufs.data(), outBufs.data(), (int32_t)numSamples);
std::cerr << "[Vst2Instrument] processReplacing OUT ch=" << channel_ << std::endl;
// TEMP diag: does the plugin write any audio?
{
static int diagBlk = 0;
float peakL = 0.f, peakR = 0.f;
for (uint32_t i = 0; i < numSamples; ++i) {
float a = outputL[i], b = outputR[i];
if (a > peakL) peakL = a; else if (-a > peakL) peakL = -a;
if (b > peakR) peakR = b; else if (-b > peakR) peakR = -b;
}
if (peakL != 0.f || peakR != 0.f || diagBlk < 10) {
std::cerr << "[Vst2Instrument][diag] PEAK blk=" << diagBlk
<< " n=" << numSamples << " L=" << peakL << " R=" << peakR << std::endl;
diagBlk++;
}
}
// TEMP diag: how far past n did DUNE write into the INPUT buffers?
{
int farL = -1, farR = -1;
for (uint32_t i = 0; i < 2097152; ++i) {
uint32_t bL, bR;
std::memcpy(&bL, &inL[numSamples + i], 4);
std::memcpy(&bR, &inR[numSamples + i], 4);
if (bL != 0x7FC00000u) farL = (int)i;
if (bR != 0x7FC00000u) farR = (int)i;
}
if (farL >= 0 || farR >= 0)
std::cerr << "[Vst2Instrument][diag] INPUT OVERRUN past n: L=" << farL
<< " R=" << farR << std::endl;
}
}
const void* Vst2Instrument::effect() const {
+15 -6
View File
@@ -321,12 +321,6 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
std::cerr << "[Vst3Instrument] setupProcessing failed" << std::endl;
return false;
}
if (component->setActive(true) != kResultOk) {
std::cerr << "[Vst3Instrument] setActive failed" << std::endl;
return false;
}
processor->setProcessing(true);
// Build per-bus buffers sized maxBlockSize_ (HostProcessData owns them;
// we must NOT override channelBuffers with external pointers — unprepare
// would delete[] them).
@@ -335,6 +329,13 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
return false;
}
if (component->setActive(true) != kResultOk) {
std::cerr << "[Vst3Instrument] setActive failed" << std::endl;
return false;
}
processor->setProcessing(true);
// Count output channels of bus 0 (mono plugins duplicate ch0 → both outs).
int32 outChannels = 2;
if (component->getBusCount(kAudio, kOutput) > 0) {
@@ -848,6 +849,14 @@ void Vst3Instrument::processAudioBlock(float* outputL, float* outputR, uint32_t
std::memset(ob.channelBuffers32[c], 0, numSamples * sizeof(float));
}
}
if (s->processData.numInputs > 0) {
for (int32 b = 0; b < s->processData.numInputs; ++b) {
const Steinberg::Vst::AudioBusBuffers& ib = s->processData.inputs[b];
for (int32 c = 0; c < ib.numChannels; ++c)
if (ib.channelBuffers32[c])
std::memset(ib.channelBuffers32[c], 0, numSamples * sizeof(float));
}
}
tresult pr = processor->process(s->processData);
float mx = 0.f;
if (pr >= 0 && s->processData.numOutputs > 0) {
+148
View File
@@ -0,0 +1,148 @@
// native_bridge/src/main_fx.cpp
// Entry point of fx_vst_bridge.exe — VST FX host bridge (task: split FX out
// of daw_vst_bridge). Windows only. One-shot modes only:
// --render-fx <job.json> --in <input.wav> --out <output.wav>
// --scan <dir>
// --open-fx-gui <job.json>
// --fx-gui <job.json>
// No SHM realtime loop: the realtime master FX chain (control type 6) stays
// in daw_vst_bridge because it processes the live instrument mix in-process.
#include "RenderFxJob.h"
#ifdef _WIN32
#include <windows.h>
#include <imm.h>
#include <dbghelp.h>
#include <cstdio>
#include <cstdlib>
#else
#include <cstdlib>
#endif
#include <cstring>
#include <iostream>
#include <string>
#ifdef _WIN32
// Minidump on crash -> %TEMP%\sf_dumps (same convention as daw_vst_bridge).
static LONG WINAPI FxCrashDumpHandler(EXCEPTION_POINTERS* ep) {
static LONG once = 0;
if (InterlockedCompareExchange(&once, 1, 0)) return EXCEPTION_CONTINUE_SEARCH;
PEXCEPTION_RECORD er = ep ? ep->ExceptionRecord : nullptr;
std::cerr << "[CRASH] fx_vst_bridge exception code=0x" << std::hex
<< (er ? er->ExceptionCode : 0) << " addr=0x"
<< (er ? (void*)er->ExceptionAddress : nullptr) << std::dec << std::endl;
char dumpDir[MAX_PATH];
if (GetTempPathA(MAX_PATH, dumpDir)) {
std::strncat(dumpDir, "sf_dumps", MAX_PATH - std::strlen(dumpDir) - 1);
CreateDirectoryA(dumpDir, nullptr);
char dumpPath[MAX_PATH];
std::snprintf(dumpPath, sizeof(dumpPath), "%s/fx_bridge_crash_%lu.dmp",
dumpDir, GetCurrentProcessId());
HMODULE hDbg = LoadLibraryA("dbghelp.dll");
if (hDbg) {
typedef BOOL(WINAPI* MiniDumpWriteDumpFn)(
HANDLE, DWORD, HANDLE, MINIDUMP_TYPE,
PMINIDUMP_EXCEPTION_INFORMATION, PMINIDUMP_USER_STREAM_INFORMATION,
PMINIDUMP_CALLBACK_INFORMATION);
auto pMiniDumpWriteDump =
(MiniDumpWriteDumpFn)GetProcAddress(hDbg, "MiniDumpWriteDump");
if (pMiniDumpWriteDump) {
HANDLE hFile = CreateFileA(dumpPath, GENERIC_WRITE, 0, nullptr,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,
nullptr);
if (hFile != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION mei = {};
mei.ThreadId = GetCurrentThreadId();
mei.ExceptionPointers = ep;
mei.ClientPointers = TRUE;
pMiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
hFile, MiniDumpNormal, &mei, nullptr,
nullptr);
CloseHandle(hFile);
}
}
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
static LONG WINAPI FxVectoredCrashHandler(PEXCEPTION_POINTERS ep) {
PEXCEPTION_RECORD er = ep ? ep->ExceptionRecord : nullptr;
DWORD code = er ? er->ExceptionCode : 0;
if (code != 0xC0000374 && code != 0xC0000005 && code != 0xC0000409)
return EXCEPTION_CONTINUE_SEARCH;
return FxCrashDumpHandler(ep);
}
// Same daemon init as daw_vst_bridge main(): no console flash, no power
// throttling, per-monitor DPI (plugin renderers must not be OS-scaled),
// no default IME on the main thread (IME recursion fix). Redirected stdio
// (bridge.log pipe / SF_FXGUI_PORT reader) survives FreeConsole.
static void win_daemon_init() {
if (!std::getenv("SF_KEEP_CONSOLE")) FreeConsole();
#ifndef PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION
#define PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION 0x00000004
#endif
#ifndef PROCESS_POWER_THROTTLING_EXECUTION_SPEED
#define PROCESS_POWER_THROTTLING_EXECUTION_SPEED 0x00000001
#endif
PROCESS_POWER_THROTTLING_STATE powerState = {};
powerState.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
powerState.ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION |
PROCESS_POWER_THROTTLING_EXECUTION_SPEED;
powerState.StateMask = 0;
SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling,
&powerState, sizeof(powerState));
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
ImmAssociateContextEx(nullptr, nullptr, IACE_DEFAULT);
}
#endif // _WIN32
int main(int argc, char* argv[]) {
#ifdef _WIN32
SetUnhandledExceptionFilter(FxCrashDumpHandler);
AddVectoredExceptionHandler(1, FxVectoredCrashHandler);
win_daemon_init();
#endif
// stderr, not stdout: stdout is reserved for machine-readable output in
// one-shot modes (--scan JSON, --fx-gui "SF_FXGUI_PORT=...").
std::cerr << "[FxBridge] Starting DAW FX Host Bridge..." << std::endl;
std::string renderFxJob, renderFxIn, renderFxOut;
std::string scanDir;
std::string openFxGuiJob;
std::string fxGuiServerJob;
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];
else if (std::strcmp(argv[i], "--scan") == 0 && i + 1 < argc)
scanDir = argv[i + 1];
else if (std::strcmp(argv[i], "--open-fx-gui") == 0 && i + 1 < argc)
openFxGuiJob = argv[i + 1];
else if (std::strcmp(argv[i], "--fx-gui") == 0 && i + 1 < argc)
fxGuiServerJob = 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);
}
if (!scanDir.empty()) return run_scan_dir(scanDir);
if (!openFxGuiJob.empty()) return run_open_fx_gui(openFxGuiJob);
if (!fxGuiServerJob.empty()) return run_fx_gui_server(fxGuiServerJob);
std::cerr << "fx_vst_bridge: no mode. Usage:\n"
" --render-fx <job.json> --in <input.wav> --out <output.wav>\n"
" --scan <dir>\n"
" --open-fx-gui <job.json>\n"
" --fx-gui <job.json>" << std::endl;
return 1;
}