phase4: Linux native_bridge build (Docker) — sfizz from source, --render SF2/SFZ

- Dockerfile: build sfizz tu native_bridge/sfizz-src (hoac git clone v1.2.3),
  sed fix sfizz.pc (-llibsfizz -> -lsfizz), cmake bridge voi 3 flag SMTG OFF
  (VSTGUI/plugin/hosting examples) — bridge headless chi can --render.
- CMakeLists: nhanh non-WIN32 build daw_vst_bridge tu main_linux.cpp +
  RenderJob.cpp + NativeInstrumentEngine.cpp (SF2/SF3 + SFZ; VST3/VST2
  hosting Windows-only, guard #ifdef _WIN32). gui_probe/plugin_host/
  seh_channel_test chi build tren Windows.
- RenderJob.cpp: JSON parser (sheredom json.h) khong con phu thuoc
  HAVE_VST3SDK — render SF2/SFZ chay duoc khong can VST3 SDK.
- NativeInstrumentEngine.cpp: guard windows.h/VST3/VST2/Sandbox/SEH.
- main.cpp: g_engine unconditional + guard disable_ime_contexts (_WIN32).
- engine.spec: bo pedalboard collect_dynamic_libs + hidden imports.
- vst_engine/render_engine: DecentSampler -> NotImplementedError + huong
  dan convert .dspreset -> .vstpreset; pianobook fallback synth.
- Verified: WSL Ubuntu 24.04 build OK, bridge Linux --render SF2 ra WAV
  44100 stereo peak 1575; Python tests 115 passed 1 skipped.
- Dockerfile build chua test duoc (may khong co Docker).
This commit is contained in:
2026-08-17 11:46:24 +07:00
parent efe26e3277
commit e79dae5d5c
10 changed files with 135 additions and 71 deletions
+36 -1
View File
@@ -3,15 +3,19 @@
#include <fluidsynth.h>
#include <sfizz.hpp>
#ifdef _WIN32
#include "Vst3Instrument.h"
#include "Vst2Instrument.h"
#include "SandboxVst3Host.h"
#endif
// void* members keep fluid types out of the public header; cast here.
#define FS_SYNTH (static_cast<fluid_synth_t*>(synth))
#define FS_SETTINGS (static_cast<fluid_settings_t*>(settings))
#ifdef _WIN32
#include <windows.h>
#endif
#include <algorithm>
#include <cctype>
@@ -168,6 +172,7 @@ std::unique_ptr<INativeInstrument> InstrumentEngineManager::create_instrument(In
switch (type) {
case InstrumentType::SOUNDFONT_SF2_SF3: return std::make_unique<FluidSynthInstrument>();
case InstrumentType::SFZ: return std::make_unique<SfizzInstrument>();
#ifdef _WIN32
case InstrumentType::VST3:
// G4.2: SF_SANDBOX_VST3=1 -> host the VST3 in a child
// process (plugin_host.exe). Crash kills only the child.
@@ -176,6 +181,13 @@ std::unique_ptr<INativeInstrument> InstrumentEngineManager::create_instrument(In
return std::make_unique<Vst3Instrument>();
case InstrumentType::VST2:
return std::make_unique<Vst2Instrument>();
#else
case InstrumentType::VST3:
case InstrumentType::VST2:
// Linux bridge: SF2/SF3 + SFZ only (Docker render). VST3/VST2
// hosting is Windows-only (COM/HWND/SHM realtime loop).
return nullptr;
#endif
default: return nullptr;
}
}
@@ -191,6 +203,7 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
loaded = static_cast<FluidSynthInstrument*>(inst.get())->loadSoundFontFile(path, sampleRate);
else if (type == InstrumentType::SFZ)
loaded = static_cast<SfizzInstrument*>(inst.get())->loadSfzFile(path, sampleRate);
#ifdef _WIN32
else if (type == InstrumentType::VST3) {
if (auto* v = dynamic_cast<Vst3Instrument*>(inst.get()))
loaded = v->loadPlugin(path, sampleRate);
@@ -207,6 +220,7 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
if (auto* v = dynamic_cast<Vst3Instrument*>(inst.get()))
v->setChannel(channel);
}
#endif
if (!loaded) return false;
// init() AFTER load: FluidSynth creates its synth inside loadSoundFontFile.
if (!inst->init(sampleRate, blockSize)) return false;
@@ -415,8 +429,11 @@ static float* alignF(std::vector<float>& v, size_t n) {
// (0xC0000005) is caught here — the channel is muted instead of killing the
// whole bridge process. /EHa (set in CMakeLists) allows mixing with the
// outer C++ try/catch net in main.cpp.
#ifdef _WIN32
static DWORD g_sehCode = 0;
static void* g_sehAddr = nullptr;
#endif
#ifdef _WIN32
static bool SafeProcessChannel(INativeInstrument* inst, float* outL, float* outR, uint32_t n, DWORD* outCode) {
*outCode = 0;
__try {
@@ -429,6 +446,18 @@ static bool SafeProcessChannel(INativeInstrument* inst, float* outL, float* outR
return false;
}
}
#else
static bool SafeProcessChannel(INativeInstrument* inst, float* outL, float* outR, uint32_t n, uint32_t* outCode) {
*outCode = 0;
try {
inst->processAudioBlock(outL, outR, n);
return true;
} catch (...) {
*outCode = 1;
return false;
}
}
#endif
void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t numSamples) {
if (isBypassed_.load()) {
@@ -446,11 +475,12 @@ 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));
DWORD sehCode = 0;
uint32_t sehCode = 0;
if (!SafeProcessChannel(inst.get(), spL, spR, numSamples, &sehCode)) {
// G3.3: plugin fault — mute this channel only, keep the bridge up.
if (!crashedCh_[ch]) {
crashedCh_[ch] = true;
#ifdef _WIN32
{
HMODULE m = nullptr;
char mod[MAX_PATH] = "?";
@@ -477,6 +507,11 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
}
std::cerr << std::endl;
}
#else
std::cerr << "[NativeBridge] G3.3: channel " << ch
<< " plugin crashed in processAudioBlock — muted until reload"
<< std::endl;
#endif
}
continue;
}
+9 -8
View File
@@ -9,8 +9,10 @@
#include "RenderJob.h"
#include "NativeInstrumentEngine.h"
#ifdef HAVE_VST3SDK
// sheredom/json.h (public domain, vendored with the VST3 SDK) is header-only
// and needed for SF2/SFZ render even without the VST3 SDK — include it directly.
#include "vst3sdk/public.sdk/source/vst/moduleinfo/json.h"
#ifdef HAVE_VST3SDK
#include "public.sdk/source/vst/vstpresetfile.h"
#include "public.sdk/source/common/memorystream.h"
using Steinberg::int32;
@@ -37,7 +39,6 @@ using Steinberg::uint32;
namespace {
#ifdef HAVE_VST3SDK
// --- sheredom/json.h helpers -------------------------------------------------
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)
@@ -73,7 +74,6 @@ 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;
}
#endif // HAVE_VST3SDK
// --- WAV writer (stdlib only, 16-bit PCM stereo, little-endian) --------------
void writeU16(std::ofstream& f, uint16_t v) {
@@ -247,10 +247,6 @@ int run_render_job(const std::string& jobPath, const std::string& outPath) {
std::remove(outPath.c_str()); // never leave a partial clip behind
};
#ifndef HAVE_VST3SDK
fail("built without the VST3 SDK submodule — JSON parser unavailable, --render disabled");
return rc;
#else
struct RootHolder { json_value_s* p = nullptr; ~RootHolder() { if (p) std::free(p); } } rootH;
// 1. Read + parse the job.
@@ -347,12 +343,18 @@ int run_render_job(const std::string& jobPath, const std::string& outPath) {
if (type != InstrumentType::VST3) {
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;
}
#else
} else {
std::cerr << "[RenderJob] preset ignored: built without VST3 SDK" << std::endl;
}
#endif
}
// 6. Offline render loop — same dispatch/render split as the realtime
@@ -404,5 +406,4 @@ int run_render_job(const std::string& jobPath, const std::string& outPath) {
<< " samples, " << (dataBytes / 4) << " frames)" << std::endl;
rc = 0;
return rc;
#endif // HAVE_VST3SDK
}
+5 -1
View File
@@ -110,7 +110,7 @@ class ChannelWorker; // fwd — WM_DESTROY posts closeGUI() to the channel work
static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker
static void post_resize_view(uint32_t ch, int w, int h); // defined after ChannelWorker
static InstrumentEngineManager* g_engine = nullptr;
static InstrumentEngineManager* g_engine = nullptr; // declared unconditionally (Linux --render build needs the symbol)
static std::mutex g_guiMutex;
static std::map<uint32_t, void*> g_guiWindows; // channel -> HWND (keep window alive)
static std::map<HWND, uint32_t> g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup)
@@ -1010,7 +1010,9 @@ int main(int argc, char* argv[]) {
// a JUCE_ window + plugin wndproc (ImmIsUIMessageW ->
// SendMessageW WM_IME_SETCONTEXT same hwnd) = infinite
// recursion (0xC00000FD, observed inside attachView).
#ifdef _WIN32
if (ok) disable_ime_contexts((HWND)hwnd);
#endif
if (ok) ok = inst->attachView(hwnd);
} catch (...) {
std::cerr << "[NativeBridge] GUI attach EXCEPTION ch=" << guiCh
@@ -1033,7 +1035,9 @@ int main(int argc, char* argv[]) {
unmute_if_not_closing(y, "openGUI");
if (ok) {
#ifdef _WIN32
disable_ime_contexts((HWND)hwnd);
#endif
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
<< " plugin=" << arg2 << " ch=" << guiCh
+22
View File
@@ -0,0 +1,22 @@
// native_bridge/src/main_linux.cpp
// Linux entry point of daw_vst_bridge: offline --render only (Docker).
// The Windows realtime SHM loop / VST3 GUI hosting is Windows-only and is
// not built here. See main.cpp for the full Windows entry.
#include "RenderJob.h"
#include <cstring>
#include <iostream>
#include <string>
int main(int argc, char* argv[]) {
std::string renderJob, renderOut;
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--render") == 0 && i + 1 < argc) renderJob = argv[i + 1];
else if (std::strcmp(argv[i], "--out") == 0 && i + 1 < argc) renderOut = argv[i + 1];
}
if (renderJob.empty() || renderOut.empty()) {
std::cerr << "usage: daw_vst_bridge --render <job.json> --out <clip.wav>" << std::endl;
return 1;
}
return run_render_job(renderJob, renderOut);
}