fix(bridge): đồng đều âm lượng VSTi jbridge, GUI native lên top, modal instrument lọc FX

- calibrate: probe sandbox (jbridge) theo real-time pacing — child render qua SHM
  consumer-sync, probe µs-speed chỉ đo được silence/attack → makeup=1.0 mọi channel.
  Thêm virtual probeNeedsRealtime() (SandboxVst2Host → true), calibrate() Sleep
  ~1 block/5.8ms để child kịp nhận MIDI + render steady-state → makeup đo peak thật
  (3 channel Qin ≈ 2.6, trước đây = 1.0 → âm lượng đồng đều giữa các track/VSTi).
- OPEN_GUI: SetWindowPos(HWND_TOPMOST) + SetForegroundWindow + BringWindowToTop
  → GUI VSTi hiển thị ở top window, tương tác ngay.
- /api/v1/plugins/available: filter vst_instruments + extra theo is_fx của
  fx_vst_bridge --scan (trước merge entry) — modal Select Instrument chỉ còn
  soundfont + VSTi, không hiện VST FX (90 → 33).
This commit is contained in:
2026-08-21 10:18:25 +07:00
parent da7ae02509
commit d7756b08ab
6 changed files with 41 additions and 2 deletions
+15 -1
View File
@@ -166,10 +166,24 @@ async def list_plugins(current_user: dict = Depends(get_current_user)):
d = _effective_dirs()
pm = PluginManager(vst_dir=d["vst_dir"], sf_dir=d["soundfont_dir"], upload_sf_dir=UPLOAD_SF_DIR, extra_vst_dirs=d["plugin_dirs"])
avail = pm.list_available()
# Modal chọn instrument (nút Synth TCP) chỉ hiện soundfont + VSTi — loại
# VST FX (Ozone/Sonible/...) khỏi vst_instruments theo is_fx của bridge scan.
# Filter TRƯỚC merge extra: nếu không, entry FX (path .vst3) chiếm id và
# setdefault chặn entry instrument (path .dll) cùng tên → mất cả hai.
fx_paths = set()
try:
from app.core.vst_engine import get_plugin_manager
fx_paths = {f.get("path", "").lower() for f in get_plugin_manager().list_fx()}
except Exception:
pass
if fx_paths:
avail["vst_instruments"] = [v for v in avail["vst_instruments"]
if (v.get("path") or "").lower() not in fx_paths]
# Gộp VST từ plugin_dirs user đã scan — list_available() CHỈ quét vst_dir
# env (mặc định /opt/daw_engine/vst3) → Synth dropdown không thấy VSTi mà
# Plugin Manager đã scan trong thư mục user chọn (bug: nút Synth rỗng).
extra = _scan_vst_in_dirs(d["plugin_dirs"])
extra = [v for v in _scan_vst_in_dirs(d["plugin_dirs"])
if (v.get("path") or "").lower() not in fx_paths]
by_id = {v["id"]: v for v in avail["vst_instruments"]}
for v in extra:
by_id.setdefault(v["id"], v)
+1 -1
View File
@@ -464,7 +464,7 @@ class PluginManager:
plugins = self._scan_plugins()
return {
"vst_instruments": [
{"id": k, "name": k,
{"id": k, "name": k, "path": p,
"type": "VST3" if p.lower().endswith(".vst3") else "VST2",
"has_native_support": True}
for k, p in plugins.items()
@@ -83,6 +83,13 @@ public:
virtual uint32_t bankOf(uint32_t channel) const { return 0; }
virtual uint32_t programOf(uint32_t channel) const { return 0; }
// Engines whose render is paced by an external process (sandbox VST2
// child): the auto-calibration probe must consume at ~real-time speed so
// the child can wake, pick up the MIDI and render each block. Direct
// engines (VST3/VST2-in-proc/SF2/SFZ) render synchronously in
// processAudioBlock and probe at µs speed.
virtual bool probeNeedsRealtime() const { return false; }
// Real-time Audio PCM Float32 rendering loop
virtual void processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) = 0;
};
@@ -556,6 +556,11 @@ float InstrumentEngineManager::calibrate(INativeInstrument* inst, double sampleR
const uint32_t bs = blockSize ? blockSize : 512;
const uint32_t body = (uint32_t)(sampleRate * 0.6);
const uint32_t tail = (uint32_t)(sampleRate * 0.25);
// Sandbox hosts render through a child process at consumer pace: consume
// one block per real-time block (~5.8ms @256/44.1k) so the child wakes
// from its idle Sleep(15), picks up the probe notes and renders steady
// state. Direct engines: no pacing (probe finishes in µs).
const bool realtime = inst->probeNeedsRealtime();
std::vector<float> L(bs + 64, 0.0f), R(bs + 64, 0.0f);
const int pitches[3] = { 60, 64, 67 };
float peak = 0.0f;
@@ -578,6 +583,7 @@ float InstrumentEngineManager::calibrate(INativeInstrument* inst, double sampleR
while (off < body) {
uint32_t n = bs; if (off + n > body) n = body - off;
if (!renderProbe(n)) { inst->noteOff(0, (uint32_t)p, 0); return 1.0f; }
if (realtime) Sleep((DWORD)(n * 1000.0 / sampleRate + 0.5));
off += n;
}
inst->noteOff(0, (uint32_t)p, 0);
@@ -585,6 +591,7 @@ float InstrumentEngineManager::calibrate(INativeInstrument* inst, double sampleR
while (off < tail) {
uint32_t n = bs; if (off + n > tail) n = tail - off;
if (!renderProbe(n)) return 1.0f;
if (realtime) Sleep((DWORD)(n * 1000.0 / sampleRate + 0.5));
off += n;
}
}
+3
View File
@@ -37,6 +37,9 @@ public:
bool openGUI(void* parentWindowHandle) override;
bool canOpenGUI() const override { return true; }
bool ownsGuiWindow() const override { return true; } // child owns its top-level window
// The child renders at consumer pace and sleeps 15ms while idle - the
// calibration probe must run in real-time or it only sees silence/attack.
bool probeNeedsRealtime() const override { return true; }
bool attachView(void* parentWindowHandle) override; // bridge OPEN_GUI job calls this
bool hasAttachedView() const override { return guiOpen_.load(); }
void closeGUI() override;
+8
View File
@@ -255,6 +255,14 @@ int main(int argc, char* argv[]) {
fflush(stdout);
inst.openGUI(g_hostHwnd);
ShowWindow(g_hostHwnd, SW_SHOW);
// Đưa GUI lên top để tương tác ngay: TOPMOST tạm thời
// (rồi hạ ngay) kéo window lên đỉnh z-order + focus.
SetWindowPos(g_hostHwnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
SetWindowPos(g_hostHwnd, HWND_NOTOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE);
SetForegroundWindow(g_hostHwnd);
BringWindowToTop(g_hostHwnd);
}
} else if (c.type == 5) {
inst.closeGUI();