feat: open native VST FX GUI from mastering panel + FX rack (bridge --open-fx-gui)
This commit is contained in:
@@ -499,6 +499,12 @@ class OpenInCarlaRequest(BaseModel):
|
||||
plugin_path: Optional[str] = None
|
||||
|
||||
|
||||
class FxGuiRequest(BaseModel):
|
||||
"""Mở native GUI của VST FX qua bridge (Windows desktop)."""
|
||||
path: str
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
class PreviewRequest(BaseModel):
|
||||
"""Quick-render preview: pedalboard render clip ngắn bằng ĐÚNG plugin +
|
||||
preset (cùng code path với export) → trả wav để browser phát.
|
||||
@@ -677,6 +683,66 @@ async def open_in_carla(req: OpenInCarlaRequest, current_user: dict = Depends(ge
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Không mở được Carla: {e}")
|
||||
|
||||
@router.post("/fx-gui")
|
||||
async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_current_user)):
|
||||
"""Mở native GUI của VST FX (mastering chain / FX rack) qua bridge.
|
||||
|
||||
Bridge có mode `--open-fx-gui <job.json>` (Windows): spawn tiến trình riêng
|
||||
giữ vòng lặp message pump, tạo window 800x600 + attach IPlugView của plugin.
|
||||
Đây là GUI tách rời — user chỉnh thông số, đóng window là bridge thoát
|
||||
(exit 0). Không spawn 2 lần cho cùng plugin đang mở (tránh trùng GUI).
|
||||
"""
|
||||
enforce_password_changed(current_user)
|
||||
from app.core.native_render import find_bridge_exe
|
||||
exe = find_bridge_exe()
|
||||
if not exe:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Không tìm thấy bridge native (daw_vst_bridge.exe). "
|
||||
"Hãy chạy bản desktop đã build đầy đủ.",
|
||||
)
|
||||
plugin_path = (req.path or "").strip()
|
||||
if not plugin_path:
|
||||
raise HTTPException(status_code=400, detail="Thiếu đường dẫn VST plugin.")
|
||||
# Tránh mở 2 GUI cùng plugin: nếu tiến trình bridge fx-gui cho plugin này
|
||||
# đang chạy → trả started=False (frontend có thể focus window cũ).
|
||||
for proc in _FX_GUI_PROCESSES:
|
||||
if proc is not None and proc.poll() is None and getattr(proc, "_sf_plugin_path", None) == plugin_path:
|
||||
return {"success": True, "started": False, "already_running": True, "cmd": []}
|
||||
job = {"path": plugin_path, "name": req.name or os.path.basename(plugin_path)}
|
||||
fd, job_path = tempfile.mkstemp(prefix="sf_fx_gui_", suffix=".json", dir=tempfile.gettempdir())
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump(job, f)
|
||||
except Exception:
|
||||
os.close(fd)
|
||||
raise HTTPException(status_code=500, detail="Không ghi được job file cho bridge.")
|
||||
cmd = [exe, "--open-fx-gui", job_path]
|
||||
try:
|
||||
proc = subprocess.Popen(cmd, close_fds=os.name != "nt")
|
||||
proc._sf_plugin_path = plugin_path
|
||||
_register_fx_gui_process(proc)
|
||||
return {"success": True, "started": True, "already_running": False, "cmd": cmd}
|
||||
except Exception as e:
|
||||
try:
|
||||
os.remove(job_path)
|
||||
except Exception:
|
||||
pass
|
||||
raise HTTPException(status_code=500, detail=f"Không mở được GUI của VST: {e}")
|
||||
|
||||
|
||||
_FX_GUI_PROCESSES = [] # list[subprocess.Popen] — các bridge fx-gui do app spawn
|
||||
|
||||
def _register_fx_gui_process(proc):
|
||||
global _FX_GUI_PROCESSES
|
||||
_prune_fx_gui_processes()
|
||||
_FX_GUI_PROCESSES.append(proc)
|
||||
|
||||
def _prune_fx_gui_processes():
|
||||
global _FX_GUI_PROCESSES
|
||||
_FX_GUI_PROCESSES = [p for p in _FX_GUI_PROCESSES if p is not None and p.poll() is None]
|
||||
|
||||
|
||||
|
||||
# ── Carla bridge lifecycle ─────────────────────────────────────────────────
|
||||
# App spawn Carla (open_in_carla) và PHẢI có khả năng dừng nó khi track chuyển
|
||||
|
||||
+10
-5
@@ -11754,7 +11754,7 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
||||
);
|
||||
};
|
||||
|
||||
const vstRow = (s, idx, toggleV, changeV, uploadV, moveV, removeV, fxl, chainLen) => (
|
||||
const vstRow = (s, idx, toggleV, changeV, uploadV, moveV, removeV, fxl, chainLen, openGui) => (
|
||||
<div key={idx} className="flex items-center gap-2 bg-slate-900/60 border border-slate-800 rounded-lg px-2 py-1.5 flex-wrap">
|
||||
<button onClick={() => toggleV(idx)} className={`w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0 ${!s.bypass ? 'bg-teal-500 text-slate-950' : 'bg-slate-700 text-slate-300'}`} title={s.bypass ? 'Bypass' : 'Active'}>
|
||||
<i data-lucide="power" className="w-2.5 h-2.5"></i>
|
||||
@@ -11766,6 +11766,7 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
||||
</select>
|
||||
<span className={`text-[9px] font-mono ${s.preset_b64 ? 'text-emerald-400' : 'text-slate-600'}`}>{s.preset_b64 ? '✓ preset' : 'no preset'}</span>
|
||||
<button onClick={() => uploadV(idx)} className="px-2 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 text-[10px] font-semibold rounded transition" title="Upload .vstpreset xuất từ Carla (Save preset)">Preset</button>
|
||||
{openGui && <button onClick={() => openGui(s)} disabled={!s.path} className="px-2 py-1 bg-violet-800 hover:bg-violet-700 disabled:opacity-30 text-violet-100 text-[10px] font-semibold rounded transition" title="Mở GUI của VST (chỉnh thông số trực tiếp)"><i data-lucide="monitor" className="w-3 h-3"></i> GUI</button>}
|
||||
<button onClick={() => { if (!s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } window.SonicAPI.openInCarla(s.name || s.path, s.path).then(r => { if (r && r.success) window.showToast && window.showToast('Đã mở Carla với ' + (s.name || s.path) + ' — chỉnh âm trong Carla rồi Save preset', 'success'); else window.showToast && window.showToast('Không mở được Carla', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở Carla: ' + (err.message || err), 'error')); }} className="px-2 py-1 bg-teal-800 hover:bg-teal-700 text-teal-100 text-[10px] font-semibold rounded transition" title="Mở native GUI trong Carla">Carla</button>
|
||||
<button onClick={() => moveV(idx, -1)} disabled={idx === 0} className="w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30" title="Lên"><i data-lucide="chevron-up" className="w-3 h-3"></i></button>
|
||||
<button onClick={() => moveV(idx, 1)} disabled={idx === chainLen - 1} className="w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30" title="Xuống"><i data-lucide="chevron-down" className="w-3 h-3"></i></button>
|
||||
@@ -11817,7 +11818,9 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
|
||||
const toggleVst = (idx) => setVstChain(vstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
|
||||
const removeVst = (idx) => setVstChain(vstChain.filter((_, i) => i !== idx));
|
||||
const moveVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= vstChain.length) return; const next = [...vstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setVstChain(next); };
|
||||
const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path); if (!fx) return; setVstChain([...vstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]); };
|
||||
const openVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path }).then(r => { if (r && r.success) window.showToast && window.showToast('Đã mở GUI của ' + (s.name || s.path) + ' — chỉnh xong đóng cửa sổ plugin', 'success'); else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn', 'info'); else window.showToast && window.showToast('Không mở được GUI', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
|
||||
const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path); if (!fx) return; setVstChain([...vstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]);
|
||||
openVstGui({ path: fx.path, name: fx.name || fx.id }); };
|
||||
const changeVstPlugin = (idx, path) => { const fx = (fxList || []).find(f => f.path === path); setVstChain(vstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
|
||||
const uploadVstPreset = (idx) => {
|
||||
const inp = document.createElement('input');
|
||||
@@ -12138,7 +12141,7 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
|
||||
</div>
|
||||
</div>
|
||||
{vstChain.length === 0 && <div className="text-[10px] text-slate-500 font-mono">Chưa có VST FX — chọn plugin bên dưới để thêm vào insert chain (WebAudio chain → VST FX → Fader).</div>}
|
||||
{vstChain.map((s, idx) => vstRow(s, idx, toggleVst, changeVstPlugin, uploadVstPreset, moveVst, removeVst, fxList, vstChain.length))}
|
||||
{vstChain.map((s, idx) => vstRow(s, idx, toggleVst, changeVstPlugin, uploadVstPreset, moveVst, removeVst, fxList, vstChain.length, openVstGui))}
|
||||
<select value={addVstSel} onChange={e => { setAddVstSel(e.target.value); if (e.target.value) { addVst(e.target.value); setAddVstSel(''); } }} className="w-full bg-slate-950 border border-dashed border-slate-700 hover:border-teal-600 text-teal-300 rounded px-2 py-1.5 text-[11px] outline-none" title="Thêm VST FX">
|
||||
<option value="">+ Thêm VST FX...</option>
|
||||
{(fxList || []).map(f => <option key={f.path} value={f.path}>{f.name || f.id}</option>)}
|
||||
@@ -12375,7 +12378,9 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
||||
const toggleMasterVst = (idx) => setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
|
||||
const removeMasterVst = (idx) => setMasterVstChain(masterVstChain.filter((_, i) => i !== idx));
|
||||
const moveMasterVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= masterVstChain.length) return; const next = [...masterVstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setMasterVstChain(next); };
|
||||
const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.path === path); if (!fx) return; setMasterVstChain([...masterVstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]); };
|
||||
const openMasterVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path }).then(r => { if (r && r.success) window.showToast && window.showToast('Đã mở GUI của ' + (s.name || s.path) + ' — chỉnh xong đóng cửa sổ plugin', 'success'); else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn', 'info'); else window.showToast && window.showToast('Không mở được GUI', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
|
||||
const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.path === path); if (!fx) return; setMasterVstChain([...masterVstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]);
|
||||
openMasterVstGui({ path: fx.path, name: fx.name || fx.id }); };
|
||||
const changeMasterVstPlugin = (idx, path) => { const fx = (masterFxList || []).find(f => f.path === path); setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
|
||||
const uploadMasterVstPreset = (idx) => {
|
||||
const inp = document.createElement('input');
|
||||
@@ -13143,7 +13148,7 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
||||
</div>
|
||||
</div>
|
||||
{masterVstChain.length === 0 && <div className="text-[10px] text-slate-500 oz-font-mono">Chưa có VST FX masterbus — chọn plugin bên dưới (Ozone...) để chạy chain sau khi mix WebAudio.</div>}
|
||||
{masterVstChain.map((s, idx) => vstRow(s, idx, toggleMasterVst, changeMasterVstPlugin, uploadMasterVstPreset, moveMasterVst, removeMasterVst, masterFxList, masterVstChain.length))}
|
||||
{masterVstChain.map((s, idx) => vstRow(s, idx, toggleMasterVst, changeMasterVstPlugin, uploadMasterVstPreset, moveMasterVst, removeMasterVst, masterFxList, masterVstChain.length, openMasterVstGui))}
|
||||
<select value={addMasterVstSel} onChange={e => { setAddMasterVstSel(e.target.value); if (e.target.value) { addMasterVst(e.target.value); setAddMasterVstSel(''); } }} className="w-full bg-slate-950 border border-dashed border-slate-700 hover:border-teal-600 text-teal-300 rounded px-2 py-1.5 text-[11px] outline-none" title="Thêm VST FX">
|
||||
<option value="">+ Thêm VST FX...</option>
|
||||
{(masterFxList || []).map(f => <option key={f.path} value={f.path}>{f.name || f.id}</option>)}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -92,6 +92,8 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
|
||||
midiRender: (payload) => apiRequest('/api/v1/plugins/midi-render', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
listFx: () => apiRequest('/api/v1/plugins/fx', { method: 'GET' }),
|
||||
fxRender: (payload) => apiRequest('/api/v1/plugins/fx-render', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Mở native GUI của VST FX (mastering chain / FX rack) qua bridge
|
||||
openFxGui: (payload) => apiRequest('/api/v1/plugins/fx-gui', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
soundfontRender: (payload) => apiRequest('/api/v1/plugins/soundfont-render', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Phát dãy MIDI notes realtime qua Carla bridge (OSC) — preview khi
|
||||
// pedalboard không render được plugin (VD VST2)
|
||||
|
||||
@@ -36,4 +36,12 @@ int run_render_fx_job(const std::string& jobPath, const std::string& inPath,
|
||||
// crashes the module loader is skipped, not fatal).
|
||||
int run_scan_dir(const std::string& dirPath);
|
||||
|
||||
// GUI mode (Mastering/FX Rack → "mở VST GUI"): `daw_vst_bridge --open-fx-gui
|
||||
// <job.json>` loads ONE VST3 effect plugin, opens its native editor window and
|
||||
// runs a message loop until the user closes the window (WM_DESTROY), then
|
||||
// detaches the view and exits 0. job.json: { "path": "...", "name": "..." }.
|
||||
// Windows only. Exit codes: 0 ok, 1 job/argument error, 2 plugin load or
|
||||
// editor attach failure.
|
||||
int run_open_fx_gui(const std::string& jobPath);
|
||||
|
||||
#endif // RENDER_FX_JOB_H
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "pluginterfaces/vst/ivsteditcontroller.h"
|
||||
#include "pluginterfaces/vst/ivstprocesscontext.h"
|
||||
#include "pluginterfaces/vst/ivstmessage.h"
|
||||
#include "pluginterfaces/gui/iplugview.h"
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -318,6 +319,24 @@ public:
|
||||
tresult restartComponent(Steinberg::int32) override { return kResultOk; }
|
||||
};
|
||||
|
||||
using Steinberg::IPlugView;
|
||||
using Steinberg::kPlatformTypeHWND;
|
||||
|
||||
// Minimal IPlugFrame so the plugin can resize its editor view.
|
||||
class HostPlugFrameFx : public Steinberg::IPlugFrame {
|
||||
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 resizeView(Steinberg::IPlugView* view, Steinberg::ViewRect* newSize) override {
|
||||
if (view && newSize) view->onSize(newSize);
|
||||
return kResultOk;
|
||||
}
|
||||
};
|
||||
|
||||
struct Vst3FxState {
|
||||
VST3::Hosting::Module::Ptr module; // destroyed LAST (owns the factory)
|
||||
IPtr<IComponent> component;
|
||||
@@ -329,6 +348,8 @@ struct Vst3FxState {
|
||||
int32 inputChannels = 2;
|
||||
int32 outputChannels = 2;
|
||||
bool controllerIsComponent = false;
|
||||
Steinberg::IPlugFrame* plugFrame = nullptr; // owned; freed in closeEditor
|
||||
IPtr<IPlugView> view; // editor view while GUI open
|
||||
};
|
||||
|
||||
// Class selection for FX: any kVstAudioEffectClass whose subcategory does NOT
|
||||
@@ -590,6 +611,80 @@ public:
|
||||
std::cout << "[RenderFx] preset applied component=" << comp.size()
|
||||
<< " controller=" << ctrl.size() << std::endl;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
// Native editor: attach the plugin's IPlugView to a host window (must run
|
||||
// on the thread that will pump its messages — same rule as Vst3Instrument).
|
||||
bool openEditor(void* parentWindowHandle) {
|
||||
#ifndef HAVE_VST3SDK
|
||||
(void)parentWindowHandle;
|
||||
return false;
|
||||
#else
|
||||
auto* s = static_cast<Vst3FxState*>(state_);
|
||||
if (!s || !s->controller || !parentWindowHandle) return false;
|
||||
if (s->plugFrame) return true; // already attached
|
||||
s->plugFrame = new HostPlugFrameFx();
|
||||
IPlugView* rawView = nullptr;
|
||||
s->controller->queryInterface(IPlugView::iid, (void**)&rawView);
|
||||
FUnknownPtr<IPlugView> view(rawView);
|
||||
if (!view) {
|
||||
// Some plugins expose IPlugView on the component instead.
|
||||
IPlugView* rawViewC = nullptr;
|
||||
s->component->queryInterface(IPlugView::iid, (void**)&rawViewC);
|
||||
view = FUnknownPtr<IPlugView>(rawViewC);
|
||||
}
|
||||
if (!view) {
|
||||
// Official editorhost.cpp pattern: IEditController::createView.
|
||||
// JUCE-based plugins (Scaler2) expose the editor only this way.
|
||||
view = owned(s->controller->createView(Steinberg::Vst::ViewType::kEditor));
|
||||
}
|
||||
if (!view) {
|
||||
delete s->plugFrame;
|
||||
s->plugFrame = nullptr;
|
||||
return false;
|
||||
}
|
||||
view->setFrame(s->plugFrame);
|
||||
view->isPlatformTypeSupported(kPlatformTypeHWND);
|
||||
if (view->attached(parentWindowHandle, kPlatformTypeHWND) != kResultOk) {
|
||||
view->setFrame(nullptr);
|
||||
delete s->plugFrame;
|
||||
s->plugFrame = nullptr;
|
||||
return false;
|
||||
}
|
||||
s->view = view;
|
||||
#ifdef _WIN32
|
||||
HWND hwnd = (HWND)parentWindowHandle;
|
||||
Steinberg::ViewRect rect;
|
||||
if (view->getSize(&rect) == kResultOk) {
|
||||
int w = rect.right - rect.left;
|
||||
int h = rect.bottom - rect.top;
|
||||
RECT r = { 0, 0, w, h };
|
||||
AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE);
|
||||
SetWindowPos(hwnd, nullptr, 0, 0, r.right - r.left, r.bottom - r.top,
|
||||
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
view->onSize(&rect); // some Skia/OpenGL editors skip first paint
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
// Detach editor. Must run on the SAME thread that attached (the GUI message
|
||||
// loop has exited). removed() is intentionally NOT called — Vst3Instrument
|
||||
// documents a deadlock when the pump is not running.
|
||||
void closeEditor() {
|
||||
#ifndef HAVE_VST3SDK
|
||||
return;
|
||||
#else
|
||||
auto* s = static_cast<Vst3FxState*>(state_);
|
||||
if (!s) return;
|
||||
if (s->view) {
|
||||
s->view->setFrame(nullptr);
|
||||
s->view = nullptr; // IPtr releases the view
|
||||
}
|
||||
if (s->plugFrame) {
|
||||
delete s->plugFrame;
|
||||
s->plugFrame = nullptr;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
bool loaded() const { return state_ != nullptr; }
|
||||
@@ -791,6 +886,125 @@ int run_render_fx_job(const std::string& jobPath, const std::string& inPath,
|
||||
return rc;
|
||||
}
|
||||
|
||||
// --- --open-fx-gui <job.json>: native editor window for one VST3 FX ----------
|
||||
#ifdef _WIN32
|
||||
namespace {
|
||||
|
||||
const char* kFxGuiWndClass = "SonicForge_FX_GUI_Class";
|
||||
|
||||
struct FxGuiCtx {
|
||||
bool closing = false;
|
||||
};
|
||||
|
||||
LRESULT CALLBACK FxGuiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
FxGuiCtx* ctx = (FxGuiCtx*)GetWindowLongPtrA(hwnd, GWLP_USERDATA);
|
||||
switch (msg) {
|
||||
case WM_CLOSE:
|
||||
DestroyWindow(hwnd);
|
||||
return 0;
|
||||
case WM_DESTROY:
|
||||
if (ctx) ctx->closing = true;
|
||||
return 0;
|
||||
default:
|
||||
return DefWindowProcA(hwnd, msg, wp, lp);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// SEH frames hold only raw pointers / trivials (C2712 — no unwinding locals).
|
||||
static bool fxGuiAttachSafe(void* fx, HWND hwnd) {
|
||||
#ifdef HAVE_VST3SDK
|
||||
__try { return static_cast<Vst3Fx*>(fx)->openEditor(hwnd); }
|
||||
__except (EXCEPTION_EXECUTE_HANDLER) { return false; }
|
||||
#else
|
||||
(void)fx; (void)hwnd;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
static void fxGuiDetachSafe(void* fx) {
|
||||
#ifdef HAVE_VST3SDK
|
||||
__try { static_cast<Vst3Fx*>(fx)->closeEditor(); }
|
||||
__except (EXCEPTION_EXECUTE_HANDLER) {}
|
||||
#else
|
||||
(void)fx;
|
||||
#endif
|
||||
}
|
||||
|
||||
int run_open_fx_gui(const std::string& jobPath) {
|
||||
// The editor window and its message pump live on this (STA) thread for the
|
||||
// whole GUI session — VST3 editors need the COM apartment + pump together.
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
struct ComGuard { ~ComGuard() { CoUninitialize(); } } comGuard;
|
||||
|
||||
// 1. Parse job { path, name }.
|
||||
std::ifstream jf(jobPath, std::ios::binary);
|
||||
if (!jf) { std::cerr << "[FxGui] cannot read job file: " << jobPath << std::endl; return 1; }
|
||||
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) { std::cerr << "[FxGui] job JSON parse error (code " << std::to_string(pres.error) << ")" << std::endl; return 1; }
|
||||
struct RootHolder { json_value_s* p = nullptr; ~RootHolder() { if (p) std::free(p); } } rootH;
|
||||
rootH.p = root;
|
||||
if (root->type != json_type_object) { std::cerr << "[FxGui] job root must be a JSON object" << std::endl; return 1; }
|
||||
const json_object_s* job = static_cast<const json_object_s*>(root->payload);
|
||||
const std::string path = memberString(job, "path", "");
|
||||
const std::string name = memberString(job, "name", "");
|
||||
if (path.empty()) { std::cerr << "[FxGui] job missing path" << std::endl; return 1; }
|
||||
|
||||
// 2. Load the plugin (fixed 44.1k/512 — GUI only, no audio processed here).
|
||||
Vst3Fx fx;
|
||||
if (!fx.load(path, 44100.0, 512)) { std::cerr << "[FxGui] plugin load failed: " << path << std::endl; return 2; }
|
||||
|
||||
// 3. Host window.
|
||||
WNDCLASSA wc = {};
|
||||
wc.lpfnWndProc = FxGuiWndProc;
|
||||
wc.hInstance = GetModuleHandleA(nullptr);
|
||||
wc.lpszClassName = kFxGuiWndClass;
|
||||
RegisterClassA(&wc);
|
||||
FxGuiCtx ctx;
|
||||
HWND hwnd = CreateWindowExA(0, kFxGuiWndClass,
|
||||
(name.empty() ? "VST FX" : name).c_str(),
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
|
||||
nullptr, nullptr, GetModuleHandleA(nullptr), nullptr);
|
||||
if (!hwnd) { std::cerr << "[FxGui] create window failed" << std::endl; return 1; }
|
||||
SetWindowLongPtrA(hwnd, GWLP_USERDATA, (LONG_PTR)&ctx);
|
||||
|
||||
// 4. Attach the plugin editor (SEH-guarded — plugin GUI code may crash).
|
||||
if (!fxGuiAttachSafe(&fx, hwnd)) {
|
||||
std::cerr << "[FxGui] editor attach failed for " << path << std::endl;
|
||||
DestroyWindow(hwnd);
|
||||
return 2;
|
||||
}
|
||||
std::cout << "[FxGui] editor open: " << (name.empty() ? path : name) << std::endl;
|
||||
|
||||
// 5. Pump messages until the window is destroyed.
|
||||
MSG msg;
|
||||
while (!ctx.closing) {
|
||||
BOOL r = GetMessageA(&msg, nullptr, 0, 0);
|
||||
if (r <= 0) break;
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageA(&msg);
|
||||
}
|
||||
|
||||
// 6. Detach + exit.
|
||||
fxGuiDetachSafe(&fx);
|
||||
std::cout << "[FxGui] editor closed" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
#else // !_WIN32
|
||||
|
||||
int run_open_fx_gui(const std::string& jobPath) {
|
||||
(void)jobPath;
|
||||
std::cerr << "[FxGui] GUI mode chỉ hỗ trợ Windows" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endif // _WIN32
|
||||
|
||||
// --- --scan: classify VST3 modules as instrument/effect ----------------------
|
||||
namespace {
|
||||
|
||||
|
||||
@@ -638,6 +638,14 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
if (!scanDir.empty()) return run_scan_dir(scanDir);
|
||||
|
||||
// --open-fx-gui <job.json>: one-shot native editor window for a VST3 FX
|
||||
// (Mastering/FX Rack → "mở VST GUI"); runs until the user closes it.
|
||||
std::string fxGuiJob;
|
||||
for (int i = 1; i + 1 < argc; ++i) {
|
||||
if (std::strcmp(argv[i], "--open-fx-gui") == 0) fxGuiJob = argv[i + 1];
|
||||
}
|
||||
if (!fxGuiJob.empty()) return run_open_fx_gui(fxGuiJob);
|
||||
|
||||
// 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) {
|
||||
|
||||
Reference in New Issue
Block a user