25 Commits

Author SHA1 Message Date
3dtours c32c77e4f4 fix(ui): master chain panel - xoa toolbar Delta Listen, GUI VST mo mac dinh spawn fresh moi lan mo panel, busy spinner + banner loi + nut Thu lai khi spawn GUI cham/fail 2026-09-02 20:32:20 +07:00
3dtours 8e4fed5f05 fix(api): /fx-gui/close dong TAT CA bridge trung plugin (khong chi proc dau) 2026-09-02 12:41:16 +07:00
3dtours 33c5cc290f fix(api,ui): chan double-spawn GUI embed master chain - register bridge ngay sau Popen + inflight guard + frontend busy ref (crash 0xc0000005 2 instance Ozone) 2026-09-02 12:34:46 +07:00
3dtours f793ebce9b fix(bridge): first-paint kick real click — iZotope GUI embed hien thi that 2026-09-02 11:44:16 +07:00
3dtours d3094a3ba5 fix(ui): add VST FX vao master chain tu dong mo GUI embed
MasteringModal co 2 duong add: addMasterVst (goi openMasterVstGui, ham
chet khong ai dung) va addVstToChain (picker dung, chi them vao chain
khong mo GUI) -> user phai bam vao slot moi thay GUI. Gop: addVstToChain
goi openMasterVstGui ngay sau khi them slot.

Verified: engine-from-source serve bundle co fix; app installed (installer
rebuilt -Frontend -Tauri): Ozone GUI OPEN 11.3s, frames 10/10 jpeg, spawn.log
sach 0x7E/Fatal.
2026-09-02 10:01:48 +07:00
3dtours d068927694 fix(bridge): Ozone FX GUI embed crash license check — sleep truoc attach + retry 5 spawn
Root cause (minidump x3, cung offset iZOzone11Core.dll+0x18f6b92): crash
deterministic 1 instruction - memcmp std::string [rdi+0x100] vs "iLok"
trong license check, doc heap ptr garbage (use-after-free/corrupt trong
iZotope Product Engine license path, flaky theo timing).

Fixes:
- RenderFxJob.cpp: fxGuiLoadAttach giu retry 4 attempt moi instance moi
  (SEH-guarded dispose); them Sleep(2s) giua load va attach cho plugin
  iZotope/Ozone - Product Engine service het race, attempt 1 open ngay
  (E2E: 60-70s -> 10-22s, 8/8 run OK).
- plugins.py: _spawn_fx_gui_embed process-level respawn 3->5 lan + delay
  2s giua respawn (service can thoi gian hoi phuc sau crash); di chuyen
  BELOW_NORMAL/CREATE_NO_WINDOW sang _fx_gui_creationflags(); gui stdout
  bridge qua thread de khong block event loop (asyncio.to_thread).
- main_fx.cpp: FxCrashDumpHandler log MOI exception (khong chi lan dau),
  minidump van 1 lan/process.

Verified: E2E engine-from-source 8/8 run Ozone embed OK; app installed
(thu cai moi installer NSIS): OPEN 49.5s, frames 10/10 jpeg, spawn.log
khong con 0x7E/Fatal Error.
2026-09-02 09:40:17 +07:00
3dtours 5b90bd43b5 bump sf-fx-realtime cache-bust version 2026-09-01 22:49:30 +07:00
3dtours 4da46d87c8 fix(bridge): skip toàn bộ iZotope trong scan + VST2 load — wrapper 0x7E/FastFail hết dialog 2026-09-01 22:26:40 +07:00
3dtours aaf58db56f fix(bridge): VST2 iZotope wrapper 0x7E — add Cores dirs to PATH; skip Ozone Pro fastfail 2026-09-01 22:07:10 +07:00
3dtours be5ecf0a48 fix(ui): Plugin Manager Scan circular JSON — onClick pas synthetic event lam dirsOverride (JSON.stringify event circular) 2026-09-01 21:47:52 +07:00
3dtours 31f19fa537 bridge: skip Ozone 11 modules (license fastfail c0000409 kills scan/render)
Ozone 11 (iZotope) tu __fastfail FATAL_APP_EXIT khi load — khong bi SEH chan,
giet ca bridge process. Scan -> mat ca dir -> FX list rong; render/realtime ->
crash. Khong load: scan emit UNKNOWN, load tra loi sach. Ozone Pro van OK.
ponytail: SandboxVst3Host (1 module 1 process) khi can Ozone 11.
2026-09-01 20:37:59 +07:00
3dtours de8abd12fa fix(bridge): NOMINMAX trong VstDllSearch.h — windows.h macro min/max pha std::min (C2589) 2026-09-01 18:10:08 +07:00
3dtours 1acb6f23f1 fix(bridge): 0x7E iZotope — append core DLL dirs (Program Files/iZotope/*/Cores) vao PATH truoc Module::create
iZotope VST3 single-file (Ozone 11.vst3) reference core DLL
(iZOzone11Core.dll) dat rieng o C:\Program Files\iZotope\<Product>\Cores,
khong canh .vst3. SetDllDirectoryW chi phu dir canh plugin -> LoadLibraryW
fail ERROR_MOD_NOT_FOUND (0x7E) -> Windows dialog khi app load plugin.
VstDllSearch.h quet cac dir Cores co dinh + append vao PATH (process-global,
bridge load 1 plugin 1 luc). Ap dung ca RenderFxJob (FX chain + scan) va
Vst3Instrument (VSTi).
2026-09-01 18:08:10 +07:00
3dtours a8a3038dd2 fix(engine): bundle bridge tu bridge-win/Release (MSBuild) khong phai build/Release (CMake cu)
engine.spec chi nhin native_bridge/build/Release -> bundle chua
fx_vst_bridge.exe cu/hong 282KB (--scan crash c0000409 ngay ca voi dir
khong ton tai) -> Plugin Manager scan chet. Fix: exe tu bridge-win/Release,
DLL tu build/Release, fallback ve build/Release cho checkout cu.
2026-09-01 17:31:41 +07:00
3dtours 67b8658015 V45.39b: engine_port.txt ghi port+engine_exe_path, get_engine_port doi chieu path engine cua chinh app (2 instance khong doc lan port cua nhau) 2026-09-01 17:02:23 +07:00
3dtours 2c4d6c358b V45.39: fix SERVER OFFLINE lau + UI load cham - engine ghi port file (engine_port.txt), UI bootstrap doc port engine cua chinh no (get_engine_port) + timeout/retry 60s, health poll self-heal reload, khong taskkill /IM daw_engine (giet engine instance khac), /api/v1/plugins/available chay scan trong threadpool (khong chan event loop -> /health het timeout) 2026-09-01 16:45:06 +07:00
3dtours 1e0314249d V45.38: bundle VC++ runtime app-local (MSVCP140/VCRUNTIME140) vao installer+portable - bridge C++ link /MD can runtime canh exe, khong phu thuoc vc_redist he thong (fix 0x7E ERROR_MOD_NOT_FOUND tren may khong cai redist) 2026-09-01 16:19:33 +07:00
3dtours c1b73db971 Merge main (V45.1-32 fixes: ReferenceError guard, crackle/solo/seek fixes) vao standalone-shm-bridge (giu FILL 96) 2026-09-01 16:03:28 +07:00
3dtours 3aed7e93ea V45.37: build_windows.ps1 full-build - dung path CMake build/bridge-win (vcxproj+Release), Continue+LASTEXITCODE (native stderr), kill engine theo path khong dong production 2026-09-01 12:25:43 +07:00
3dtours aad25f4049 V45.36: build_install.ps1 robust voi native stderr (Continue+LASTEXITCODE), kill theo path install/dist khong dong production 2026-09-01 12:12:14 +07:00
3dtours e691c43e62 V45.35: fix build_install/build_windows copy layout PHANG - Copy-Item wildcard tran noi dung _internal ra root (exe treo luc chay); dung robocopy /MIR giu dung cau truc onedir exe+_internal 2026-09-01 11:44:04 +07:00
3dtours 4cd8fa780d V45.34: ban cai phai SACH - engine.spec loai dev config (DB/.secret_key/plugin_dirs.json/sf_scan_state.json) khoi bundle; build_install.ps1 + build_windows.ps1 fail-fast neu dist dinh config dev (reset ve mac dinh cho nguoi dung moi) 2026-09-01 11:31:10 +07:00
locpham a8082b27c4 V45.33: fix Plugin Manager VSTi liệt kê nhầm VST FX (Ozone Pro/Tonal Balance Control Pro) làm instrument - /available chặn entry UNKNOWN theo tên (bridge timeout introspect VST3) + _norm_path_stem 2026-09-01 10:37:19 +07:00
admin 2b334c0812 V46: FILL 48->96 (cushion ~512ms) chong stall ~500ms (u2 deficit 723ms) + instrument 3 lop (wl_gap/main_in_gap/pump_hb) 2026-08-29 09:01:35 +07:00
admin 2a3e5bf08f V45: tang FILL 24->48 (cushion 256ms) chong stall OS ~200ms (PDC 14080) 2026-08-29 08:00:22 +07:00
49 changed files with 1254 additions and 4973 deletions
+150 -302
View File
@@ -1,10 +1,10 @@
import os, sys, uuid, json, tempfile, subprocess, time as _time, threading, asyncio, logging, queue
import os, sys, uuid, json, tempfile, subprocess, time as _time, threading, asyncio, logging
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Query, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse
from pydantic import BaseModel
from typing import Optional, Any
from app.config import settings
from app.core.vst_engine import PluginManager, HAS_PYFLUIDSYNTH, render_soundfont_midi_to_audio, RENDER_LOCK, _render_locked, _is_vst2_dll
from app.core.vst_engine import PluginManager, HAS_PYFLUIDSYNTH, render_soundfont_midi_to_audio, RENDER_LOCK, _render_locked
from app.core.soundfont_inspector import SoundFontInspector
from app.core.soundfont_converter import SoundFontConverter
from app.core.soundfont_scanner import SoundFontAutoScanner
@@ -194,6 +194,14 @@ def get_scanner():
@router.get("/available")
async def list_plugins(current_user: dict = Depends(get_current_user)):
# Scan VST/SF chạy subprocess đồng bộ (fx_vst_bridge --scan, timeout 300s/
# dir; Ozone treo tới 30s/module) — chạy trực tiếp trên event loop sẽ chặn
# /health và mọi request khác → UI báo SERVER: OFFLINE suốt thời gian scan.
# Chạy trong threadpool để event loop luôn phản hồi.
return await asyncio.to_thread(_list_plugins_impl)
def _list_plugins_impl():
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"], user_dirs_override=d.get("plugin_dirs_user_set", False))
avail = pm.list_available()
@@ -371,8 +379,7 @@ def _scan_vst_in_dirs(dirs: list) -> list:
"id": name, "name": name, "type": "VST3",
"path": os.path.join(root, f),
}
elif (low.endswith(".dll") and not has_vst3_file
and _is_vst2_dll(os.path.join(root, f))):
elif low.endswith(".dll") and not has_vst3_file:
name = os.path.splitext(f)[0]
if name not in found:
found[name] = {
@@ -668,10 +675,7 @@ class FxGuiRequest(BaseModel):
# đã chỉnh (trước đây GUI mở lại load default → mất settings khi
# hide/show).
preset_b64: str = ""
# C: master=True — GUI master chỉ mở qua session realtime (native editor
# của instance DSP, shm). JS warm-ensure session trước; master mà không có
# session/slot -> 409 (fallback embed đã ngưng cho master).
master: bool = False
class FxGuiCloseRequest(BaseModel):
"""Đóng bridge fx-gui đã mở cho plugin (embed mode)."""
@@ -693,72 +697,6 @@ class PreviewRequest(BaseModel):
preset_data: Optional[str] = None # base64 bytes .vstpreset (project nhúng)
# M24: thao tac GUI native (hide/open/close editor in-process fxrt) co work
# blocking (capture_editor_preset poll file time.sleep 0.05 x3s,
# wait_chain_ready sleep-poll, terminate proc.wait(5)). Truoc M24 cac sleep nay
# chay DONG BO trong async endpoint -> block event loop uvicorn -> pump audio
# `_pump_output` (cung loop) stall 60-200ms -> worklet queue can -> underrun ->
# am lag roi moi play lai moi lan switch GUI luc dang play (log fxrt_pump.log:
# pump_gap ngay sau GUI op). Fix: chay blocking work trong thread
# (asyncio.to_thread) duoi lock per-session de GIU thu tu hide->open nhu cu.
_FXGUI_OP_LOCKS = {} # session name -> asyncio.Lock
_FXGUI_OP_LOCKS_GUARD = threading.Lock()
def _fxgui_op_lock(session_name):
with _FXGUI_OP_LOCKS_GUARD:
lock = _FXGUI_OP_LOCKS.get(session_name)
if lock is None:
lock = asyncio.Lock()
_FXGUI_OP_LOCKS[session_name] = lock
return lock
def _fxgui_open_native_blocking(sess, plugin_path):
"""M24 worker thread (asyncio.to_thread) cho nhanh mo GUI native: cho chain
ready tren bridge, terminate fallback GUI cu, gui ed.open + retry 700ms.
Tra dict ket qua native; None neu plugin khong phai slot chain (roi xuong
fallback); raise HTTPException 409 khi session chet giua chung."""
_slot = sess.chain_slot_for_path(plugin_path)
if _slot is None:
return None
_ready = sess.wait_chain_ready(timeout=15)
logger.info("[fxgui] open native path=%s shm=%s slot=%s ready=%s",
plugin_path, sess.name, _slot, _ready)
if not _ready:
# M13: chua READY (proc respawn / chain reload cham) — cho them
# 5s; van chet thi ed.open chi vao ctrl ring (C++ KHONG reset
# ring khi respawn → proc moi doc duoc) nen van gui duoc.
sess.wait_chain_ready(timeout=5)
if not sess.alive():
logger.warning("[fxgui] open native bo — session chet path=%s", plugin_path)
raise HTTPException(status_code=409,
detail="Session FX realtime đã đóng — bật lại FX PWR rồi thử mở GUI.")
_killed = _terminate_fx_gui_for_path(plugin_path)
_already = fx_realtime.editor_find(plugin_path)
_same = _already is not None and _already[0] == sess.name
_sent = sess.send_editor_cmd("open", slot=_slot)
logger.info("[fxgui] open sent=%s killed_fallback=%s already=%s same=%s proc_pid=%s state=%s",
_sent, _killed, bool(_already), _same,
sess.proc.pid if getattr(sess, "proc", None) else None,
getattr(getattr(sess, "h", None), "state", "?"))
fx_realtime.editor_register(plugin_path, sess.name, _slot)
# M13 workaround click-1: gui doi khi khong hien window ngay (owner
# gan sau show → Win32 an window). Gửi lai ed.open sau 700ms = ban
# sao click-2 — C++ log "ALREADY open — re-show" thi re-show chinh
# la dieu can. ponytail: bo block nay khi log A (prefix p/t) xac
# nhan root cause that (show truoc owner).
def _retry_open(delay):
_time.sleep(delay)
try:
if sess.alive():
sess.send_editor_cmd("open", slot=_slot)
logger.info("[fxgui] retry ed.open path=%s slot=%s", plugin_path, _slot)
except Exception:
pass
threading.Thread(target=_retry_open, args=(0.7,), daemon=True).start()
return {"success": True, "started": not _same,
"already_running": bool(_same), "native": True,
"embed_url": None, "session_id": sess.name, "slot": _slot}
@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.
@@ -780,56 +718,18 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
plugin_path = (req.path or "").strip()
if not plugin_path:
raise HTTPException(status_code=400, detail="Thiếu đường dẫn VST plugin.")
# ── Phase 2 (M4): plugin là slot của session FX realtime (mastering) → mở
# editor của CHÍNH instance DSP ngay trong fxrt_bridge (juce_fx_bridge) qua
# SHM ctrl ring ed.open — KHÔNG spawn fx_vst_bridge instance 2 + feeder
# (nguồn CPU 100%). GUI = cửa sổ native "FX slot N" trên desktop (không
# HTTP embed frame → embed_url=None, native=true — frontend M6 dùng mode
# này). Vẫn spawn fx_vst_bridge khi: không shm / session không tồn tại /
# plugin không phải slot chain (instrument, offline, fallback an toàn).
_sess = fx_realtime.get_session((req.shm or "").strip()) if (req.shm or "").strip() else None
# C: master + session sống nhưng plugin chưa phải slot chain (module vừa add —
# JS set_chain async / watchdog respawn) -> chờ tối đa 15s cho slot xuất hiện;
# session chết giữa chừng -> bỏ (fallback embed bị chặn bên dưới cho master).
if req.master and _sess is not None and _sess.alive() and _sess.chain_slot_for_path(plugin_path) is None:
# M24: poll async (khong time.sleep dong bo — block event loop lam pump
# audio stall). Y nghia giu nguyen: cho toi da 15s slot xuat hien.
for _ in range(150):
if not _sess.alive():
break
if _sess.chain_slot_for_path(plugin_path) is not None:
break
await asyncio.sleep(0.1)
if _sess is not None and _sess.alive():
# M24: toan bo nhanh native (wait_chain_ready sleep-poll, terminate
# fallback proc.wait, ed.open) chay trong worker thread duoi lock per
# session — event loop khong block -> pump audio khong stall khi switch
# GUI luc dang play. None khi plugin chua phai slot -> roi xuong
# fallback ben duoi nhu cu.
async with _fxgui_op_lock(_sess.name):
_native = await asyncio.to_thread(
_fxgui_open_native_blocking, _sess, plugin_path)
if _native is not None:
return _native
# Tránh mở 2 GUI cùng plugin: editor NATIVE (instance DSP trong session
# fxrt) của plugin này đang mở ở session SỐNG → KHÔNG spawn instance 2 /
# bridge fx-gui nữa (nguồn CPU 100% + 2 GUI); trả native để frontend hiện
# đúng trạng thái cửa sổ ngoài (M6).
_nsess, _nslot = _fxrt_native_editor(plugin_path)
if _nsess is not None:
return {"success": True, "started": False, "already_running": True,
"native": True, "embed_url": None,
"session_id": _nsess.name, "slot": _nslot}
# 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ũ).
# đang chạy (đã register sau Popen, kể cả khi còn đang chờ attach verdict)
# → 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": [], "embed_url": getattr(proc, "_sf_embed_url", None) or None}
# C: tới đây mà master vẫn không có session/slot (native không mở được) ->
# từ chối spawn fallback embed (instance VST thứ 2, preset lệch, GUI không
# anchor preset master). Track GUI (không master) giữ fallback embed cũ.
if req.master:
raise HTTPException(status_code=409, detail="Mastering GUI chỉ mở qua session realtime (bật FX PWR hoặc để session warm) — fallback embed đã ngưng cho master.")
# Request thứ 2 cho cùng plugin trong lúc spawn đầu ĐANG CHỜ verdict (có thể
# 20-70s cho Ozone-class) chưa thấy process nào register → phải chặn bằng
# inflight set, nếu không sẽ spawn bridge thứ 2 → 2 GUI instance Ozone cùng
# lúc → crash (0xc0000005 iZOzone11Core.dll) như bug đã gặp.
if req.embed and plugin_path in _FX_GUI_INFLIGHT:
return {"success": True, "started": False, "already_running": True, "cmd": [], "embed_url": None}
job = {"path": plugin_path, "name": req.name or os.path.basename(plugin_path)}
if req.shm:
job["shm"] = req.shm
@@ -844,63 +744,34 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
raise HTTPException(status_code=500, detail="Không ghi được job file cho bridge.")
cmd = [exe, "--fx-gui" if req.embed else "--open-fx-gui", job_path]
try:
# fx-gui spawn o BELOW_NORMAL: process nay load instance VST THU HAI
# (CPU spike vai giay) - neu o NORMAL se preempt realtime audio loop
# (fx_vst_bridge --realtime-fx, thread NORMAL) -> underrun -> am "bop
# nghet" ngay luc mo GUI roi moi hoi phuc. BELOW_NORMAL cho load GUI
# khong canh tranh thread audio.
_gui_creationflags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
if os.name == "nt":
_gui_creationflags |= getattr(subprocess, "BELOW_NORMAL_PRIORITY_CLASS", 0)
if req.embed:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True,
close_fds=os.name != "nt",
env=dict(os.environ, SF_PARENT_PID=str(os.getpid()),
SF_FXGUI_FLOAT="1"),
creationflags=_gui_creationflags)
# Drain stdout NGAY TU DAU (daemon thread) và log TẤT CẢ các dòng
# bridge in — gồm cả các dòng in TRƯỚC SF_FXGUI_PORT= ("[FxGui]
# env ..." của RenderFxJob.cpp) mà cách đọc-tới-port cũ nuốt mất,
# làm log engine không chứng minh được binary/env nào đang chạy.
# Port được chuyển về qua queue (thread này không đọc stdout).
port_q = queue.Queue(maxsize=1)
def _drain_fx_gui_stdout(pp):
try:
for ln in pp.stdout:
ln = (ln or "").strip()
if ln:
logger.info("[fx-gui] %s", ln)
if ln.startswith("SF_FXGUI_PORT="):
try:
port_q.put_nowait(int(ln.split("=", 1)[1]))
except Exception:
pass
except Exception:
pass
threading.Thread(target=_drain_fx_gui_stdout, args=(proc,), daemon=True).start()
embed_url = None
# Spawn the fx-gui bridge and WAIT for the editor attach verdict;
# respawn a fresh process on failure (see _spawn_fx_gui_embed — an
# in-process retry cannot recover from a crashed core DLL state).
# Runs in a worker thread so the API event loop is not blocked for
# the (long) load+attach+retry window.
_FX_GUI_INFLIGHT.add(plugin_path)
try:
port = port_q.get(timeout=60)
embed_url = f"http://127.0.0.1:{port}"
except Exception:
pass
if not embed_url:
proc.terminate()
try:
proc.wait(timeout=5)
except Exception:
proc.kill()
raise HTTPException(status_code=500, detail="Bridge không báo port fx-gui (mở GUI thất bại).")
proc._sf_plugin_path = plugin_path
proc._sf_embed_url = embed_url
_register_fx_gui_process(proc)
return {"success": True, "started": True, "already_running": False,
"embed_url": embed_url, "cmd": cmd}
proc, embed_url = await asyncio.to_thread(_spawn_fx_gui_embed, cmd, plugin_path)
if not embed_url:
_kill_fx_gui_proc(proc)
try:
os.remove(job_path)
except Exception:
pass
raise HTTPException(
status_code=500,
detail="Không mở được GUI embed cho plugin — bridge thất bại "
"nhiều lần khi tải/mở editor (chi tiết trong log [fx-gui]).",
)
proc._sf_embed_url = embed_url
return {"success": True, "started": True, "already_running": False,
"embed_url": embed_url, "cmd": cmd}
finally:
_FX_GUI_INFLIGHT.discard(plugin_path)
proc = subprocess.Popen(cmd, close_fds=os.name != "nt",
env=dict(os.environ, SF_PARENT_PID=str(os.getpid())),
creationflags=_gui_creationflags)
creationflags=_fx_gui_creationflags())
proc._sf_plugin_path = plugin_path
_register_fx_gui_process(proc)
return {"success": True, "started": True, "already_running": False, "cmd": cmd}
@@ -922,150 +793,118 @@ async def close_fx_gui(req: FxGuiCloseRequest, current_user: dict = Depends(get_
plugin_path = (req.path or "").strip()
if not plugin_path:
raise HTTPException(status_code=400, detail="Thiếu đường dẫn VST plugin.")
# M4: plugin đang mở native (editor instance DSP trong session fxrt) →
# gửi ed.close (đóng editor, GIỮ instance + state — mở lại nhanh).
_entry = fx_realtime.editor_find(plugin_path)
if _entry:
_sid, _slot = _entry
fx_realtime.editor_unregister(plugin_path)
_sess = fx_realtime.get_session(_sid)
if _sess is not None and _sess.alive():
# M5: capture state editor TRƯỚC khi đóng (preset user chỉnh trong
# GUI → lưu về project như WM_FXGUI_CAPTURE cũ; preset_b64 dùng để
# mở lại GUI / nạp chain với đúng cài đặt). Không lỗi nếu rỗng.
# M24: capture (poll file sleep toi 3s) trong thread duoi lock —
# khong block event loop/pump audio.
async with _fxgui_op_lock(_sess.name):
_preset = await asyncio.to_thread(
_sess.capture_editor_preset, _slot)
_sess.send_editor_cmd("close", slot=_slot)
return {"success": True, "closed": True, "native": True,
"preset_b64": _preset}
# session đã chết — rơi xuống spawn fallback (đóng bridge fx-gui cũ nếu có)
if _terminate_fx_gui_for_path(plugin_path):
return {"success": True, "closed": True}
return {"success": True, "closed": False}
class FxGuiNativeRequest(BaseModel):
"""Hide/Show GUI native (editor instance DSP trong session fxrt)."""
path: str
class FxGuiRectBody(BaseModel):
"""Rect màn hình (px vật lý, gốc trên-trái; x/y âm = màn hình trái)."""
x: int = 0
y: int = 0
w: int = 0
h: int = 0
class FxGuiRectRequest(BaseModel):
"""M7: anchor rect cho GUI native — rect panel nơi GUI embed xuất hiện."""
path: str
rect: FxGuiRectBody = FxGuiRectBody()
def _fxrt_native_editor(plugin_path):
"""(session, slot) nếu plugin đang mở native trong session fxrt sống."""
entry = fx_realtime.editor_find(plugin_path)
if not entry:
return None, None
sess = fx_realtime.get_session(entry[0])
if sess is None or not sess.alive():
fx_realtime.editor_unregister(plugin_path)
return None, None
return sess, entry[1]
@router.post("/fx-gui/hide")
async def hide_fx_gui(req: FxGuiNativeRequest,
current_user: dict = Depends(get_current_user)):
"""Ẩn GUI native (ed.hide — giữ editor + instance, mở lại nhanh). Chỉ
áp dụng editor in-process fxrt; GUI spawn mode (HTTP bridge) tự có /hide
riêng — trả applied=False không lỗi."""
enforce_password_changed(current_user)
sess, slot = _fxrt_native_editor((req.path or "").strip())
if sess is None:
return {"success": True, "applied": False}
# M13: capture preset TRUOC khi an — dong mastering panel / save project
# giua luc chinh GUI native khong mat preset moi (truoc day chi capture o
# /fx-gui/close; hide chi an -> chain luu preset cu/rong sau khi reload).
# M24: capture (poll file sleep toi 3s) trong thread duoi lock — day la
# blocker chinh moi lan switch GUI (JS fire-and-forget hide truoc open);
# truoc day sleep dong bo trong async endpoint -> pump audio stall -> lag.
async with _fxgui_op_lock(sess.name):
_preset = await asyncio.to_thread(sess.capture_editor_preset, slot)
sess.send_editor_cmd("hide", slot=slot)
return {"success": True, "applied": True, "native": True,
"preset_b64": _preset}
@router.post("/fx-gui/show")
async def show_fx_gui(req: FxGuiNativeRequest,
current_user: dict = Depends(get_current_user)):
"""Hiện lại GUI native (ed.show)."""
enforce_password_changed(current_user)
sess, slot = _fxrt_native_editor((req.path or "").strip())
if sess is None:
return {"success": True, "applied": False}
sess.send_editor_cmd("show", slot=slot)
return {"success": True, "applied": True, "native": True}
@router.post("/fx-gui/rect")
async def fx_gui_rect(req: FxGuiRectRequest,
current_user: dict = Depends(get_current_user)):
"""M7: anchor PluginWindow native vào rect panel embed (SetWindowPos phía
C++ khi rect đổi — mirror FxGuiServer /show của float/embed). Frontend đo
rect panel mỗi 250ms và gửi; chỉ áp dụng editor in-process fxrt — GUI spawn
mode (HTTP bridge) tự có /show riêng, trả applied=False không lỗi."""
enforce_password_changed(current_user)
sess, slot = _fxrt_native_editor((req.path or "").strip())
r = req.rect
if sess is None:
logger.info("[fxgui] rect path=%s editor=NONE rect=(%s,%s,%s,%s) — "
"chua mo native / registry mat", (req.path or "").strip(),
r.x, r.y, r.w, r.h)
return {"success": True, "applied": False}
_sent = sess.send_editor_rect(slot, r.x, r.y, r.w, r.h)
logger.info("[fxgui] rect path=%s editor=%s slot=%s rect=(%s,%s,%s,%s) sent=%s",
(req.path or "").strip(), sess.name, slot,
r.x, r.y, r.w, r.h, _sent)
return {"success": True, "applied": True, "native": True}
found = False
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:
proc.terminate()
try:
proc.wait(timeout=5)
except Exception:
proc.kill()
found = True
_prune_fx_gui_processes()
return {"success": True, "closed": found}
_FX_GUI_PROCESSES = [] # list[subprocess.Popen] — các bridge fx-gui do app spawn
_FX_GUI_INFLIGHT = set() # set[str] plugin_path — spawn embed đang chờ verdict
def _register_fx_gui_process(proc):
global _FX_GUI_PROCESSES
_prune_fx_gui_processes()
_FX_GUI_PROCESSES.append(proc)
if proc not in _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]
def _terminate_fx_gui_for_path(plugin_path):
"""Terminate MỌI bridge fx-gui (embed/float fallback) còn sống cho plugin.
Dùng khi mở native editor thay thế fallback — tránh 2 instance VST + 2 GUI
cùng 1 plugin (Bug 2-GUI). Trả True nếu có proc đã bị chấm dứt."""
_prune_fx_gui_processes()
killed = False
for proc in list(_FX_GUI_PROCESSES):
if proc.poll() is None and getattr(proc, "_sf_plugin_path", None) == plugin_path:
def _fx_gui_creationflags():
# fx-gui spawn o BELOW_NORMAL: process nay load instance VST THU HAI
# (CPU spike vai giay) - neu o NORMAL se preempt realtime audio loop
# (fx_vst_bridge --realtime-fx, thread NORMAL) -> underrun -> am "bop
# nghet" ngay luc mo GUI roi moi hoi phuc. BELOW_NORMAL cho load GUI
# khong canh tranh thread audio.
flags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
if os.name == "nt":
flags |= getattr(subprocess, "BELOW_NORMAL_PRIORITY_CLASS", 0)
return flags
def _kill_fx_gui_proc(proc):
"""Terminate a bridge process best-effort (already-exited = no-op)."""
if proc is None:
return
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except Exception:
proc.kill()
def _spawn_fx_gui_embed(cmd, plugin_path=None):
"""Spawn the fx-gui bridge and wait for the editor attach verdict.
Process-level retry: an Ozone-class plugin can crash its GUI thread
mid-attach; a fresh instance in the SAME process still shares the crashed
core DLL state, so respawn the whole bridge (fresh DLL state per spawn).
Each spawn's in-process retry (4 attempts, bridge side) covers the flaky
first attach. Returns (proc, embed_url) — embed_url is None on total
failure (all spawns failed; proc is the last, already-dead/killed, one).
BLOCKS: call via asyncio.to_thread from the async endpoint.
"""
max_spawns = 5
respawn_delay = 2.0 # Ozone license check: Product Engine service needs
# time between respawns — back-to-back spawns all fail while it is busy.
proc = None
for spawn_i in range(1, max_spawns + 1):
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
close_fds=os.name != "nt",
env=dict(os.environ, SF_PARENT_PID=str(os.getpid())),
creationflags=_fx_gui_creationflags(),
)
# Register NGAY sau Popen (trước vòng chờ verdict 20-70s): request thứ 2
# cho cùng plugin thấy process đang chạy → không spawn thêm instance GUI.
if plugin_path is not None:
proc._sf_plugin_path = plugin_path
_register_fx_gui_process(proc)
holder = {"port": None, "opened": False}
def _watch(pp, h):
try:
proc.terminate()
try:
proc.wait(timeout=5)
except Exception:
proc.kill()
killed = True
for ln in pp.stdout:
ln = (ln or "").strip()
if not ln:
continue
logger.info("[fx-gui] %s", ln)
if ln.startswith("SF_FXGUI_PORT="):
h["port"] = int(ln.split("=", 1)[1])
elif "editor open" in ln:
h["opened"] = True
except Exception:
pass
if killed:
_prune_fx_gui_processes()
return killed
threading.Thread(target=_watch, args=(proc, holder), daemon=True).start()
# Fast path: port prints as soon as the HTTP server binds.
deadline = _time.time() + 20
while holder["port"] is None and proc.poll() is None and _time.time() < deadline:
_time.sleep(0.05)
if holder["port"] is None:
logger.warning("[fx-gui] spawn %d/%d: no port, respawn", spawn_i, max_spawns)
_kill_fx_gui_proc(proc)
_time.sleep(respawn_delay)
continue
# Verdict: "editor open" (success) or process exit (attach all-fail).
deadline = _time.time() + 70
while not holder["opened"] and proc.poll() is None and _time.time() < deadline:
_time.sleep(0.05)
if holder["opened"]:
logger.info("[fx-gui] spawn %d/%d: editor open", spawn_i, max_spawns)
return proc, "http://127.0.0.1:%d" % holder["port"]
logger.warning("[fx-gui] spawn %d/%d: editor failed, respawn", spawn_i, max_spawns)
_kill_fx_gui_proc(proc)
_time.sleep(respawn_delay)
return proc, None
@@ -1399,9 +1238,18 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str):
_pf.write("%d pump_start\n" % int(_time.time() * 1000))
_last_lat = None
_last_proc = None
_hb_last = _time.time()
try:
while True:
try:
# V46: heartbeat — mỗi ~100ms; gap giữa 2 pump_hb liên tiếp
# > 300ms = event loop engine bị freeze (cause toàn cục stall
# cả 2 session — bridge idle chờ input). Phân biệt với client
# stall (wl_gap/main_in_gap) và bridge stall (pump_gap).
_hb_now = _time.time()
if _pf is not None and (_hb_now - _hb_last) >= 0.1:
_hb_last = _hb_now
_pf.write("%d pump_hb" % int(_hb_now * 1000) + chr(10))
sess.flush_input_stale() # input lẻ kẹt trong pending → publish
_lats = sess.get_latencies()
_total = sum(_lats.values()) if _lats else 0
-48
View File
@@ -295,54 +295,6 @@ async def save_project_to_disk(req: SaveProjectRequest, authorization: Optional[
}
@router.get("/disk")
async def list_disk_projects(authorization: Optional[str] = Header(None)):
"""Danh sách file Ctrl-S desktop (Documents/SonicForgeDAW/Projects),
mới nhất trước. Auth optional."""
try:
if authorization and authorization.startswith("Bearer "):
decode_token(authorization.split(" ")[1])
except Exception:
pass
out_dir = _os_projects_dir()
items = []
try:
names = os.listdir(out_dir)
except Exception:
names = []
for fn in names:
if not fn.endswith(".sonicforge.json"):
continue
pth = os.path.join(out_dir, fn)
try:
mtime = os.path.getmtime(pth)
size = os.path.getsize(pth)
except Exception:
continue
items.append({"filename": fn, "name": fn[: -len(".sonicforge.json")], "mtime": mtime, "size_bytes": size})
items.sort(key=lambda x: x["mtime"], reverse=True)
return items
@router.get("/disk/{filename}")
async def get_disk_project(filename: str, authorization: Optional[str] = Header(None)):
"""Đọc nội dung 1 file dự án trên đĩa (mở lại sau khi restart)."""
try:
if authorization and authorization.startswith("Bearer "):
decode_token(authorization.split(" ")[1])
except Exception:
pass
base = os.path.basename(filename)
if not base.endswith(".sonicforge.json"):
raise HTTPException(status_code=400, detail="Invalid project file")
pth = os.path.join(_os_projects_dir(), base)
if not os.path.isfile(pth):
raise HTTPException(status_code=404, detail="Project not found on disk")
with open(pth, "r", encoding="utf-8") as f:
data_json = f.read()
return {"filename": base, "name": base[: -len(".sonicforge.json")], "data_json": data_json}
@router.post("/cloud")
async def save_cloud_project(req: SaveProjectRequest, current_user: dict = Depends(get_current_user)):
validated_data_json = validate_project_data(req.data_json)
+2 -263
View File
@@ -16,7 +16,6 @@ Luồng dữ liệu: browser → WS → write_input() → [bridge xử lý] →
"""
import ctypes
import json
import logging
import os
import subprocess
import tempfile
@@ -35,11 +34,6 @@ FXRT_LAT_SLOTS = 8
FXRT_STATE_STARTING = 0
FXRT_STATE_READY = 1
FXRT_STATE_ERROR = 2
# M4.5 watchdog: số lần AUTO restart tối đa sau crash (editor in-process chết
# không được giết session mãi — Reaper chấp nhận gap audio khi plugin crash).
FXRT_MAX_RESTARTS = 3
_log = logging.getLogger("fx_realtime")
class FxRTHeader(ctypes.Structure):
@@ -137,18 +131,7 @@ class FxRealtimeSession:
self.stats = {'publish': 0, 'drop_batch': 0, 'drop_blocks': 0,
'stale_flush': 0, 'read_blocks': 0, 'read_empty': 0,
'flush': 0}
# M4.5 watchdog (chính sách FXRealtime đã duyệt): proc chết ngoài ý
# muốn (crash view/plugin/loop) → AUTO restart; user kill / close() có
# chủ đích → KHÔNG restart (cờ _closing / running=0 — user gọi lại).
self._closing = False
self._restarts = 0
# M8: bridge JUCE doc job 1 lan luc spawn (khong poll seq) -> set_chain
# phai reload (terminate -> watchdog respawn). Co chong dem crash.
self._juce_bridge = False
self._reload_planned = False
self._watch = threading.Thread(target=self._watch_loop, daemon=True)
self._spawn()
self._watch.start()
# ── process ─────────────────────────────────────────────────────────────
def _chain_json_for_bridge(self):
@@ -191,19 +174,10 @@ class FxRealtimeSession:
"seq": self._chain_seq,
"fx_chain": self._chain_json_for_bridge(),
}
# M4.5 respawn: job cũ không ai đọc (proc cũ đã chết) — dọn rác.
_old_job = self._job_path
if _old_job and os.path.exists(_old_job):
try:
os.remove(_old_job)
except Exception:
pass
fd, self._job_path = tempfile.mkstemp(suffix=".json", prefix="fxrt_")
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(job, f)
self._juce_bridge = os.path.basename(exe).lower().startswith("juce_fx_bridge")
_log.info("[fxrt] spawn exe=%s juce=%s seq=%d shm=%s", os.path.basename(exe), self._juce_bridge, self._chain_seq, self.name)
if self._juce_bridge:
if os.path.basename(exe).lower().startswith("juce_fx_bridge"):
cmd = [exe, "--juce-fx", self._job_path, "--shm", self.name,
"--parent", str(os.getpid())]
else:
@@ -215,22 +189,14 @@ class FxRealtimeSession:
# stall (procMax=bridge/VST cham, outDepth=Python pump cham).
_blog = os.environ.get("FXRT_BRIDGE_LOG")
if not _blog:
# M13: per-session log (fxrt_bridge_<shm-suffix>.log) — trước đây 1
# file chung cho MỌI session → log 2 process lẫn nhau. Env override
# giữ nguyên để ép 1 file khi cần.
_ad = os.environ.get("APPDATA")
if _ad:
_blog = os.path.join(_ad, "SonicForgeDAW", "logs",
"fxrt_bridge_" + self.name + ".log")
else:
_blog = ""
_blog = os.path.join(_ad, "SonicForgeDAW", "logs", "fxrt_bridge.log") if _ad else ""
self.proc = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=open(_blog, "ab", buffering=0) if _blog else subprocess.DEVNULL,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
_log.info("[fxrt] spawned pid=%d log=%s", self.proc.pid, _blog)
def set_chain(self, fx_chain):
"""Cập nhật chain trên session ĐANG CHẠY (không restart session):
@@ -252,56 +218,8 @@ class FxRealtimeSession:
}, f)
except Exception:
return False
# M8: juce_fx_bridge doc job CHI 1 LAN luc spawn (JuceFxLoop.cpp khong
# poll seq nhu RealtimeFxLoop cu) -> chi ghi job KHONG bao gio ap len
# bridge: chain bridge giu nguyen trong khi engine chain da doi -> GUI
# mo slot moi C++ bao "slot ngoai chain (M) - bo qua". Reload chu dong:
# terminate -> watchdog respawn doc chain hien tai (khong tinh crash,
# khong dung gioi han FXRT_MAX_RESTARTS). Legacy fx_vst_bridge poll seq
# giu nguyen (async swap muot - khong reload).
_log.info("[fxrt] set_chain seq=%d len=%d juce=%s proc_alive=%s reload_planned=%s",
self._chain_seq, len(self.chain), self._juce_bridge,
self.proc is not None and self.proc.poll() is None,
self._reload_planned)
if self._juce_bridge:
self._request_chain_reload()
_log.info("[fxrt] set_chain requested reload proc_pid=%s",
self.proc.pid if self.proc else None)
return True
def _request_chain_reload(self):
"""M8: yeu cau watchdog respawn sach voi chain hien tai (chi juce
bridge). Khong block: terminate proc -> vong watchdog (200ms) thay chet
+ co _reload_planned -> respawn doc job moi, khong tinh crash."""
with self._lock:
if self._closing or self.proc is None or self.proc.poll() is not None:
return
self._reload_planned = True
try:
self.proc.terminate()
except Exception:
self._reload_planned = False
def wait_chain_ready(self, timeout=20.0):
"""M8: cho chain moi thuc su ap tren bridge sau set_chain (juce reload
async): het co reload + proc respawn READY. Khong co reload -> tra ngay
True. False khi session dong/chet/qua han."""
deadline = time.time() + timeout
while time.time() < deadline:
with self._lock:
if self._closing:
return False
p = self.proc
reloading = self._reload_planned
alive = p is not None and p.poll() is None
ready = alive and self.h.state == FXRT_STATE_READY
if not reloading and ready:
return True
if not alive and not reloading:
return False
time.sleep(0.1)
return False
def wait_ready(self, timeout=30.0):
deadline = time.time() + timeout
while time.time() < deadline:
@@ -317,7 +235,6 @@ class FxRealtimeSession:
return self.proc is not None and self.proc.poll() is None
def close(self):
self._closing = True # M4.5: watchdog không auto-restart khi close chủ đích
try:
self.h.running = 0
except Exception:
@@ -509,188 +426,11 @@ class FxRealtimeSession:
except Exception:
pass
# ── editor control (Phase 2 — Reaper-style) ─────────────────────────────
# M4: engine điều khiển editor của CHÍNH instance DSP trong juce_fx_bridge
# qua SHM ctrl ring (key ed.*). Không spawn instance 2 / feeder.
def send_editor_cmd(self, action, slot, w=0, h=0):
"""Gửi lệnh editor (ed.open/close/show/hide/resize/capture) cho slot
chain. w/h encode w*4096+h (chính xác tuyệt đối w,h<=4095 — float
24-bit mantissa; xem M3); w,h<=0 → editor preferred size (bridge chỉ
setSize khi w,h>0). Trả False nếu session không có SHM (fake object)."""
key = "ed.%s" % (action or "").strip().lower()
value = 0.0
try:
if int(w) > 0 and int(h) > 0:
value = float(int(w) * 4096 + int(h))
except Exception:
value = 0.0
with self._lock:
if getattr(self, "shm", None) is None:
return False
try:
self._ctrl_enqueue_locked(int(slot), key, value)
return True
except Exception:
return False
def send_editor_rect(self, slot, x, y, w, h):
"""M7: anchor rect cho GUI native (window nằm ĐÚNG rect panel embed).
Gửi 4 key SHM ctrl ed.rx/ry/rw/rh — MỖI key 1 số nguyên float (chính
xác tuyệt đối |v| <= 2^24; x/y âm khi panel nằm màn hình trái OK vì C++
parse int trực tiếp — KHÔNG encode w*4096+h như ed.resize, giới hạn
4095). JuceFxGuiHost lưu anchor (thread-safe) + reconcile GUI thread
30ms SetWindowPos khi rect ĐỔI. Trả False nếu session không SHM."""
with self._lock:
if getattr(self, "shm", None) is None:
return False
try:
for f, v in (("x", int(x)), ("y", int(y)),
("w", int(w)), ("h", int(h))):
self._ctrl_enqueue_locked(int(slot), "ed.r" + f, float(v))
return True
except Exception:
return False
def chain_slot_for_path(self, plugin_path):
"""Slot index (theo chain bridge ĐANG CHẠY — đã lọc active=false, giữ
thứ tự) của plugin path; None nếu plugin không trong chain. Index khớp
editors[] của JuceFxEngine (C++) — ed.* gửi theo index này."""
norm = _norm_plugin_path(plugin_path or "")
if not norm:
return None
for i, s in enumerate(self._chain_json_for_bridge()):
p = (s.get("path") or "")
if p and _norm_plugin_path(p) == norm:
return i
return None
def capture_editor_preset(self, slot, timeout=3.0):
"""M5: capture state editor của CHÍNH instance DSP (ed.capture) → C++
ghi file JSON cạnh job: <jobPath>.preset.json {slot, preset_b64} (xem
JuceFxLoop.cpp) → đọc + xoá file → trả preset_b64 ("" nếu plugin không
có state / bridge chết / timeout). Không cần editor đang mở — state
đọc từ processor sống trong graph; gọi TRƯỚC ed.close khi đóng GUI."""
cap_path = (self._job_path or "") + ".preset.json"
if cap_path and os.path.exists(cap_path):
try:
os.remove(cap_path)
except Exception:
pass
if not self.send_editor_cmd("capture", slot=slot):
return ""
deadline = time.time() + timeout
while time.time() < deadline:
if not self.alive():
return ""
if cap_path and os.path.exists(cap_path):
try:
with open(cap_path, "r", encoding="utf-8") as f:
data = json.load(f)
os.remove(cap_path)
return data.get("preset_b64", "")
except Exception:
try:
os.remove(cap_path)
except Exception:
pass
return ""
time.sleep(0.05)
return ""
def _watch_loop(self):
"""M4.5 watchdog — thread riêng (ngoài audio). Proc chết ngoài ý muốn
(crash view/plugin/loop) → AUTO restart tối đa FXRT_MAX_RESTARTS lần,
log rc để đối chiếu CPU 100%. Không restart khi: close() chủ đích
(_closing) / running=0 (user kill — user gọi lại). Respawn đúng cmd
line (--juce-fx <job> --shm <name> --parent <pid>) qua _spawn()."""
while True:
time.sleep(0.2)
with self._lock:
if self._closing:
return
proc = self.proc
if proc is None:
return
rc = proc.poll()
if rc is None:
continue
if self._closing:
return
if getattr(self.h, "running", 1) == 0:
_log.warning("[fxrt] bridge thoát rc=%s sau running=0 (user kill — không restart)", rc)
return
if self._reload_planned:
self._reload_planned = False
_log.info("[fxrt] bridge rc=%s — reload chain chủ động (set_chain juce) → respawn đọc chain mới", rc)
elif self._restarts >= FXRT_MAX_RESTARTS:
_log.error("[fxrt] bridge crash rc=%s — restart tối đa %d lần đã đạt, bỏ", rc, FXRT_MAX_RESTARTS)
return
else:
self._restarts += 1
_log.warning("[fxrt] bridge chết rc=%s — auto restart #%d", rc, self._restarts)
try:
self._spawn()
except Exception as e:
_log.error("[fxrt] respawn lỗi: %s", e)
return
# Chờ READY (thread riêng — không block API); crash lần nữa → vòng
# ngoài restart tiếp (đếm _restarts đã tăng → dừng đúng giới hạn).
deadline = time.time() + 8.0
while time.time() < deadline:
time.sleep(0.1)
with self._lock:
if self._closing or self.proc is None:
return
if self.proc.poll() is not None:
break
if self.h.state == FXRT_STATE_READY:
break
# Registry toàn tiến trình (desktop app 1 user).
_SESSIONS = {}
_SESSIONS_LOCK = threading.Lock()
def _norm_plugin_path(p):
"""Chuẩn hóa path plugin để so khớp (Windows case-insensitive)."""
p = (p or "").strip()
if not p:
return ""
return os.path.normcase(os.path.normpath(p))
# M4: mapping plugin path -> (session_id, slot) — GUI native đang mở (editor
# instance DSP trong session fxrt). /fx-gui/{close,hide,show} chỉ nhận path →
# tìm session qua registry này. Session chết → entry dọn khi dùng/stop.
_FXRT_EDITORS = {}
_FXRT_EDITORS_LOCK = threading.Lock()
def editor_register(plugin_path, session_id, slot):
key = _norm_plugin_path(plugin_path)
if not key:
return
with _FXRT_EDITORS_LOCK:
_FXRT_EDITORS[key] = (session_id, int(slot))
def editor_find(plugin_path):
key = _norm_plugin_path(plugin_path)
with _FXRT_EDITORS_LOCK:
return _FXRT_EDITORS.get(key)
def editor_unregister(plugin_path):
key = _norm_plugin_path(plugin_path)
with _FXRT_EDITORS_LOCK:
_FXRT_EDITORS.pop(key, None)
def editor_unregister_session(session_id):
with _FXRT_EDITORS_LOCK:
for k in [k for k, v in _FXRT_EDITORS.items() if v[0] == session_id]:
_FXRT_EDITORS.pop(k, None)
def start_session(fx_chain, sample_rate, block_size=FXRT_BLOCK):
sess = FxRealtimeSession(fx_chain, sample_rate, block_size)
try:
@@ -713,7 +453,6 @@ def stop_session(session_id):
sess = _SESSIONS.pop(session_id, None)
if sess:
sess.close()
editor_unregister_session(session_id) # M4: dọn mapping GUI native
return sess is not None
+2 -135
View File
@@ -7,7 +7,6 @@ import time as _time
import functools
import threading
from ctypes import c_char_p
import struct
import subprocess
@@ -39,121 +38,6 @@ def _ensure_fluidsynth_dll_on_path():
_ensure_fluidsynth_dll_on_path()
def _is_vst2_dll(path: str) -> bool:
"""True nếu .dll export entry VST2 (VSTPluginMain hoặc main) — plugin
VST2 thật (Kong Audio Qin_RV_x64.dll...). Các thư viện runtime đặt cạnh
plugin (vd iZotope Cores/IZOzone11Core.dll — core của Ozone, KHÔNG phải
plugin) không export entry VST2 → loại khỏi danh sách plugin: load chúng
như VST2 gây lỗi dependency 0x7E. Đọc export table PE thuần Python, không
load DLL vào process."""
if not path.lower().endswith(".dll"):
return False
try:
f = open(path, "rb")
except OSError:
return False
try:
head = f.read(0x1000)
if len(head) < 0x40 or head[:2] != b"MZ":
return False
e_lfanew = struct.unpack_from("<I", head, 0x3C)[0]
if e_lfanew + 24 + 120 + 8 > len(head):
return False
if head[e_lfanew:e_lfanew + 4] != b"PE" + bytes(2):
return False
num_sections = struct.unpack_from("<H", head, e_lfanew + 6)[0]
opt_size = struct.unpack_from("<H", head, e_lfanew + 20)[0]
sec_start = e_lfanew + 24 + opt_size
if sec_start + num_sections * 40 > len(head):
return False
# Data directory 0 = export table (RVA, size): PE32 -> opt+96,
# PE32+ (x64) -> opt+112 (magic 0x10B / 0x20B).
magic = struct.unpack_from("<H", head, e_lfanew + 24)[0]
dd = 96 if magic == 0x10B else (112 if magic == 0x20B else 0)
if dd == 0:
return False
exp_rva = struct.unpack_from("<I", head, e_lfanew + 24 + dd)[0]
exp_size = struct.unpack_from("<I", head, e_lfanew + 24 + dd + 4)[0]
if exp_rva == 0 or exp_size == 0:
return False
secs = []
for i in range(num_sections):
off = sec_start + i * 40
va = struct.unpack_from("<I", head, off + 12)[0]
raw_size = struct.unpack_from("<I", head, off + 16)[0]
raw_ptr = struct.unpack_from("<I", head, off + 20)[0]
secs.append((va, raw_size, raw_ptr))
def rva_to_off(rva):
for va, raw_size, raw_ptr in secs:
if va <= rva < va + raw_size:
return raw_ptr + (rva - va)
return None
exp_off = rva_to_off(exp_rva)
if exp_off is None:
return False
f.seek(exp_off)
exp = f.read(40)
if len(exp) < 40:
return False
num_names = struct.unpack_from("<I", exp, 24)[0]
if num_names == 0 or num_names > 65536:
return False
names_off = rva_to_off(struct.unpack_from("<I", exp, 32)[0])
if names_off is None:
return False
f.seek(names_off)
name_ptrs = f.read(num_names * 4)
if len(name_ptrs) < num_names * 4:
return False
for i in range(num_names):
noff = rva_to_off(struct.unpack_from("<I", name_ptrs, i * 4)[0])
if noff is None:
continue
f.seek(noff)
raw = f.read(64)
nm = raw.split(bytes(1), 1)[0].decode("ascii", "replace")
if nm in ("VSTPluginMain", "main"):
return True
return False
except Exception:
return False
finally:
f.close()
def _hidden_desktop_startupinfo():
"""STARTUPINFO cho process con chạy trên DESKTOP ẨN mới tạo: dialog của
plugin bên thứ ba (vd iZotope MessageBox [0x7E] khi core load fail trong
--scan) hiện trên desktop vô hình → user không bị chặn, scan bỏ module sau
timeout 4s như cũ. Trả (startupinfo, desktop_handle) — handle phải đóng
sau khi process con kết thúc; (None, None) khi không tạo được (fallback
desktop thường)."""
if os.name != "nt":
return None, None
try:
import ctypes
from ctypes import wintypes
u32 = ctypes.windll.user32 # CreateDesktopW/CloseDesktop thuộc USER32
u32.CreateDesktopW.restype = ctypes.c_void_p # HDESK 64-bit, không cắt
u32.CreateDesktopW.argtypes = [wintypes.LPCWSTR, wintypes.LPCWSTR,
wintypes.LPVOID, wintypes.DWORD,
wintypes.DWORD, wintypes.LPVOID]
u32.CloseDesktop.restype = wintypes.BOOL
u32.CloseDesktop.argtypes = [wintypes.HANDLE]
name = "SF_Scan_%d_%d" % (os.getpid(), int(_time.time() * 1000) % 1000000)
# DESKTOP_ALL_ACCESS (0x000F01FF) — handle chỉ để giữ desktop sống.
h = u32.CreateDesktopW(name, None, None, 0, 0x000F01FF, None)
if not h:
return None, None
si = subprocess.STARTUPINFO()
si.lpDesktop = name
return si, h
except Exception:
return None, None
def midi_note_to_freq(note_number: int) -> float:
return 440.0 * (2.0 ** ((note_number - 69) / 12.0))
@@ -494,8 +378,7 @@ class PluginManager:
plugin_name = os.path.splitext(file)[0]
if plugin_name not in plugins:
plugins[plugin_name] = plugin_path
elif (low.endswith(".dll") and not has_vst3_file
and _is_vst2_dll(os.path.join(root, file))):
elif low.endswith(".dll") and not has_vst3_file:
plugin_path = os.path.join(root, file)
plugin_name = os.path.splitext(file)[0]
if plugin_name not in plugins:
@@ -699,20 +582,12 @@ class PluginManager:
if exe:
dirs = [d for d in self._vst_scan_dirs() if d and os.path.isdir(d)]
for d in dirs:
si, hdesk = _hidden_desktop_startupinfo()
try:
r = subprocess.run([exe, "--scan", d], capture_output=True,
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0,
text=True, timeout=300, startupinfo=si)
text=True, timeout=300)
except Exception:
continue
finally:
if hdesk:
try:
import ctypes
ctypes.windll.user32.CloseDesktop(hdesk)
except Exception:
pass
if r.returncode != 0:
continue
try:
@@ -745,14 +620,6 @@ class PluginManager:
continue
seen.add(e["path"])
uniq.append(e)
# Loại .dll KHÔNG phải VST2 thật (không export VSTPluginMain/main —
# vd iZotope Cores/IZOzone11Core.dll là runtime lib, không phải
# plugin): bridge scan emit mọi .dll thành VST2 instrument → load như
# plugin gây 0x7E. Kiểm export table thuần Python, không load DLL.
uniq = [e for e in uniq
if not (e.get("type") == "VST2"
and (e.get("path") or "").lower().endswith(".dll")
and not _is_vst2_dll(e["path"]))]
# Chỉ cache khi bridge tồn tại: kết quả (kể cả rỗng — dir không có
# FX) là đáng tin; khi thiếu bridge thì trả [] nhưng KHÔNG cache.
if exe:
+374 -1340
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-85
View File
@@ -1,85 +0,0 @@
// fx-rt-worker.js — M25: WebSocket FX realtime chạy trên Web Worker.
// Động lực: main thread bị stall (rAF/React/render, longtask 375ms lúc cold
// start) làm trễ cả WS receive lẫn postMessage → worklet queue cạn → underrun
// nghe được (u1/u10). Chuyển WS + deinterleave sang worker; binary output đi
// THẲNG worker→worklet qua MessageChannel (directPort) — delivery không phụ
// thuộc event loop main → stall main không còn tạo hole âm thanh. Text
// (latency/flush_done) vẫn forward main (PDC/UI). Input: worklet gửi thẳng
// qua directPort ngược chiều (cùng channel, 2 chiều).
//
// Protocol main→worker:
// {kind:'init', wsUrl, port} port = MessagePort tới worklet (transfer)
// {kind:'send', text|buf} send 1 frame (text cmd hoặc binary input)
// {kind:'close'}
// Protocol worker→main:
// {kind:'open'} | {kind:'close'} | {kind:'text', text} (chỉ text; binary
// không bao giờ chạm main)
let ws = null;
let directPort = null;
let flushing = false; // từ lúc gửi cmd 'flush' tới flush_done — drop output stale (V24, cùng logic fxRt.flushing phía main cũ)
self.onmessage = (ev) => {
const d = ev.data;
if (!d) return;
if (d.kind === 'init') {
directPort = d.port || null;
if (directPort) {
// Input burst 2048 floats (4 block) — tách 4 WS frame 512 floats như
// fxRtSendFrames cũ (frame = 1 block 256 stereo; slice() copy đúng
// 2048B, KHÔNG subarray().buffer = gửi cả backing 8192B).
directPort.onmessage = (e2) => {
const d2 = e2.data;
if (!d2 || d2.type !== 'in' || !(d2.data instanceof ArrayBuffer)) return;
if (!ws || ws.readyState !== 1) return;
try {
const f = new Float32Array(d2.data);
for (let i = 0; i < f.length; i += 512) {
const c = new Float32Array(512);
c.set(f.subarray(i, i + 512));
ws.send(c.buffer);
}
} catch (e) {}
};
}
try { ws = new WebSocket(d.wsUrl); } catch (e) { self.postMessage({ kind: 'close' }); return; }
ws.binaryType = 'arraybuffer';
ws.onopen = () => self.postMessage({ kind: 'open' });
ws.onmessage = (mev) => {
if (typeof mev.data === 'string') {
try {
const c = JSON.parse(mev.data);
if (c && c.cmd === 'flush_done') {
flushing = false;
// Xóa cờ worklet NGAY trên cùng channel (trước mọi 'out' kế — FIFO)
// — không chờ main forward (main có thể bận/stall).
if (directPort) directPort.postMessage({ type: 'flush_done' });
}
} catch (e) {}
self.postMessage({ kind: 'text', text: mev.data });
return;
}
if (!(mev.data instanceof ArrayBuffer) || flushing || !directPort) return;
// Deinterleave stereo (giống fxRtWsOnMessage cũ) → gửi thẳng worklet.
const f = new Float32Array(mev.data);
const half = f.length / 2;
const l = new Float32Array(half);
const r = new Float32Array(half);
for (let i = 0, j = 0; i < half; i++) { l[i] = f[j++]; r[i] = f[j++]; }
directPort.postMessage({ type: 'out', l, r });
};
ws.onclose = () => self.postMessage({ kind: 'close' });
ws.onerror = () => { try { ws.close(); } catch (e) {} };
return;
}
if (d.kind === 'send') {
if (!ws || ws.readyState !== 1) return;
if (d.text !== undefined) {
try { const c = JSON.parse(d.text); if (c && c.cmd === 'flush') flushing = true; } catch (e) {}
try { ws.send(d.text); } catch (e) {}
} else if (d.buf !== undefined) {
try { ws.send(d.buf); } catch (e) {}
}
return;
}
if (d.kind === 'close') { try { if (ws) ws.close(); } catch (e) {} }
};
-10
View File
@@ -90,14 +90,6 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
fxRealtimeSetChain: (payload) => apiRequest('/api/v1/plugins/fx-realtime/set-chain', { 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) }),
// M6: đóng/ẩn/hiện GUI NATIVE (editor instance DSP trong session fxrt).
// Engine capture preset khi close → trả preset_b64. Embed cũ đóng thẳng
// qua HTTP của bridge (g.url + '/close') — chỉ dùng cho native.
closeFxGui: (payload) => apiRequest('/api/v1/plugins/fx-gui/close', { method: 'POST', body: JSON.stringify(payload) }),
hideFxGui: (payload) => apiRequest('/api/v1/plugins/fx-gui/hide', { method: 'POST', body: JSON.stringify(payload) }),
showFxGui: (payload) => apiRequest('/api/v1/plugins/fx-gui/show', { method: 'POST', body: JSON.stringify(payload) }),
// M7: anchor GUI native vào rect panel (px màn hình vật lý) — C++ SetWindowPos khi rect đổi
rectFxGui: (payload) => apiRequest('/api/v1/plugins/fx-gui/rect', { method: 'POST', body: JSON.stringify(payload) }),
soundfontRender: (payload) => apiRequest('/api/v1/plugins/soundfont-render', { method: 'POST', body: JSON.stringify(payload) }),
// Thư viện preset VST3 (.vstpreset) — cầu nối preset → pedalboard
listPresets: () => apiRequest('/api/v1/presets', { method: 'GET' }),
@@ -138,8 +130,6 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
// Ctrl-S desktop: lưu project ra THƯ MỤC CỦA HỆ ĐIỀU HÀNH
// (Documents/SonicForgeDAW/Projects — Windows; ~/SonicForgeDAW/Projects — Linux)
saveProjectToDisk: (name, dataJson) => apiRequest('/api/v1/projects/save-to-disk', { method: 'POST', body: JSON.stringify({ name, data_json: dataJson }) }),
listDiskProjects: () => apiRequest('/api/v1/projects/disk', { method: 'GET' }),
getDiskProject: (filename) => apiRequest('/api/v1/projects/disk/' + encodeURIComponent(filename), { method: 'GET' }),
deleteSoundFont: (sfId) => apiRequest(`/api/v1/plugins/soundfont/${sfId}`, { method: 'DELETE' }),
uploadSoundFont: async (file) => {
const formData = new FormData();
+45 -90
View File
@@ -12,7 +12,7 @@
// Latency thêm ~4 block (~21ms) — worklet outQueue 32 block hấp thụ.
//
// FILL (hết crackle do jitter): không phát ngay khi có block đầu — chờ queue
// ≥ FILL (40 block ~213ms @48k) rồi mới phát. Engine gửi ASAP (không pace
// ≥ FILL (8 block ~43ms @48k) rồi mới phát. Engine gửi ASAP (không pace
// realtime) → client tích lũy buffer an toàn; mọi jitter (bridge process VST
// biến thiên, GC, WS, main thread rAF) bị hấp thụ bởi FILL thay vì queue ≈1
// → cạn → ngắt quãng.
@@ -29,10 +29,10 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
super();
this.BLOCK = 256; // FXRT_BLOCK
this.BURST = 4; // số block gom 1 frame (bridge batch)
this.FILL = 40; // V48: 40 block ~213ms @48k — M18: deficit engine ~3.3% (session Ozone loopAvg 22.06ms > 21.33) can dan cushion V44 24 (~90ms) — stall 68-186ms > 90ms -> underrun dinh ky; len 40 -> cushion ~170ms che stall <170ms, keo dai thoi gian toi underrun. PDC 12032 = (7+40)*256 (fxRtApplyPdc).
this.FILL = 96; // V46: 96 block ~512ms @48k: hấp thụ stall ~500ms (u2 08:16:44.8: pump deficit 723ms, event gap 507ms → V45 cushion 256ms cạn → silence ~500ms → wobble). PDC 26368 = (7+96)*256 (fxRtApplyPdc). V45 48 -> cushion 256ms < deficit 723ms -> underrun. V46 96 -> cushion ~512ms (margin ~200ms vs event 507ms; deficit cluster vẫn có thể underrun ngắn — chấp nhận, content jump giữ policy V24).
this.RESUME_FILL = 24; // V26: re-buffer khi resume SAU STOP (flush_for_stop) — 16 block ~85ms (V43: = FILL) → play kế tiếp bắt đầu nhanh; underrun-stall vẫn dùng FILL (crackle/wobble).
this.resumeFill = this.FILL; // mức re-buffer khi resume sau flush — stop: RESUME_FILL, underrun-stall: FILL
this.CAP = 64; // V48: 48->64 (~341ms) — M18: FILL len 40, CAP phai > FILL + in-flight/burst (~8 block) de khong cap-drop ngay sau resume; latency khoi dau/PDC van do FILL quyet dinh.
this.CAP = 104; // V46: = FILL+8: cap queue chặn overflow (~550ms)
this.HOLD_FADE = 4; // 4 block ~21ms: fade 1→0 sau HOLD hết (hết click cắt cứng)
this.holdFadeLeft = 0; // mẫu còn lại của fade-out khi underrun
this.silenced = false; // đã fade về 0 → im lặng; resume phải fade-in, không hold lại
@@ -45,7 +45,6 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.playing = false; // false → im lặng (đang chờ đủ FILL)
this.hasStarted = false; // đã phát block đầu chưa — FILL chỉ chờ ở lần khởi đầu
this.flushPending = false; // V24: đang chờ engine flush (resume sau underrun) — drop block stale
this.stopped = false; // Bug1 echo: sau flush_for_stop dung het output — chi resume khi main gui clear_queue moi
this.resumeFadeIn = false; // V24: interruption đã xử lí (clear+flush) — resume sẽ fade-in từ 0
this.FADE = 512; // fade-in 512 mẫu (~11ms @48k) khi bắt đầu phát/resume — hết click
this.outGain = 0; // gain phát hiện tại (0→1 trong fade)
@@ -59,37 +58,14 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.wrapFadeBlocks = -1; // V42: số block tail pass cũ còn lại sau clear_queue('loop') — đếm ngược để crossfade đúng seam tail→head
this.latencySamples = 0; // G3: latency chain thật (engine qua WS) — align wrap fade theo processed tail
this.wrapMode = false; // V45.3: dang trong wrap (clear_queue loop) — resume underrun bo flush + re-buffer nho
this.WRAP_FADE = 2048; // V46.2: 256->2048 (~43ms @48k) — che hole engine restart (~10-16ms) + jump noi dung end→start tai seam (256 qua ngan = giät nhẹ)
this.WRAP_FADE = 256; // V42: crossfade 256 mẫu (~5.3ms @48k) tại seam wrap — che click jump nội dung end→start (FX path)
this.fadeFromL = 0.0; // mẫu cố định khi fade-out underrun (fade tuyến tính tới 0)
this.fadeFromR = 0.0;
this.port.onmessage = (e) => {
const d = e.data;
// M25: nhận MessageChannel port từ main (worker FX realtime) — output
// đi THẲNG worker→worklet (bỏ hop main thread, hết underrun lớp u1/u10);
// input burst 'in' cũng gửi thẳng ngược chiều trên cùng port. Track FX
// path (không có directPort) vẫn dùng node.port như cũ — không đụng.
if (d && d.type === '__direct') {
const _dp = (e.ports && e.ports[0]) || (d.port) || null;
if (_dp) {
this.directPort = _dp;
this.directPort.onmessage = (e2) => {
const d2 = e2.data;
if (!d2) return;
if (d2.type === 'out' && d2.l && d2.r) this._enqueueOut(d2.l, d2.r);
else if (d2.type === 'flush_done') this.flushPending = false;
};
}
return;
}
if (d && d.type === 'set_param') { this.port.postMessage(d); return; }
if (d && d.type === 'flush_done') { this.flushPending = false; return; }
if (d && d.type === 'latency') { this.latencySamples = (d.samples || 0); return; }
if (d && d.type === 'dbg_state') {
// M26 diag: main thread hoi state worklet quanh PWR OFF mid-play —
// do wet tail (queue/playing) + state sau khi cat 2 canh.
this.port.postMessage({ type: 'dbg_state_r', k: (d.k !== undefined ? d.k : -1), t: Math.round(currentTime * 1000), q: this.outQueue.length, playing: this.playing ? 1 : 0, hasStarted: this.hasStarted ? 1 : 0, silenced: this.silenced ? 1 : 0, flushPending: this.flushPending ? 1 : 0, stopped: this.stopped ? 1 : 0, wrapMode: this.wrapMode ? 1 : 0, wrapFadeBlocks: this.wrapFadeBlocks, latencySamples: this.latencySamples || 0, resumeFill: this.resumeFill, inCount: this.inCount, outPos: this.outPos, fadeLeft: this.fadeLeft, holdFadeLeft: this.holdFadeLeft });
return;
}
if (d && d.type === 'flush_for_stop') {
// V26: user stop — main thread báo xóa audio tail trong queue (audio đã
// xử lí còn phát ~0.3-0.5s sau stop = tiếng "ding"). Fade-out 21ms từ
@@ -110,13 +86,11 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.fadeFromL = this.lastOutL;
this.fadeFromR = this.lastOutR;
this.holdFadeLeft = this.HOLD_FADE * this.BLOCK;
this.stopped = true; // Bug1: drop moi 'out' sau stop — engine van render input dang fade + plugin tail; neu khong drop, queue day RESUME_FILL -> worklet tu resume phat tail = echo sau stop.
this.port.postMessage({ type: 'flush' });
return;
}
if (d && d.type === 'clear_queue') {
this.clearMode = d.mode; // V45.19: nho mode de tag fx_seek_first tai dbgPlayFirst
this.stopped = false; // Bug1: play/seek/wrap moi — cho phep nhan out tro lai
// V34: backlog guard o DAU handler (V33 dat sau mutation -> outQueue.length=0
// chay truoc -> guard luon return, clear van drop queue + fade + re-buffer moi
// lan = V31). Queue nho (< FILL 8 block) -> bo qua hoan toan (khong mutation,
@@ -151,9 +125,8 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
// dai hon roi vao nhanh resume wrap (bo flush + resumeFill 8).
const KEEP = 16;
this.wrapMode = true;
const _preQ = this.outQueue.length; // V46.1 instrument seam
if (_preQ > KEEP) {
const head = _preQ - KEEP;
if (this.outQueue.length > KEEP) {
const head = this.outQueue.length - KEEP;
this.outQueue.splice(0, head);
this.outPos = 0;
if (this.playing) {
@@ -163,13 +136,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.skipFromR = this.lastOutR;
}
}
// V46.2: countdown = DUNG so block tail (sau splice) — truoc day cong
// ceil(latency/BLOCK)~137 (estimate 365ms) => crossfade ban tre ~380ms
// (giua bar1) = vo hieu, seam that di qua khong crossfade. Pipeline lag
// THUC ~25ms (fx_out_first +8ms sau clear) chu khong phai VST latency.
this.wrapFadeBlocks = this.outQueue.length;
// V46.1 instrument seam: pre=queue truoc splice, keep=sau splice, lat=latency est, wfb=countdown toi seam
this.port.postMessage({ type: 'dbg', tag: 'fx_loop_clear', t: Math.round(currentTime * 1000), pre: _preQ, keep: this.outQueue.length, lat: this.latencySamples || 0, wfb: this.wrapFadeBlocks, playing: this.playing ? 1 : 0 });
this.wrapFadeBlocks = this.outQueue.length + Math.ceil((this.latencySamples || 0) / this.BLOCK);
return;
}
if (d.mode === 'seek') {
@@ -207,12 +174,6 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
// engine ramp/jitter dau play); ARM live note giu RESUME_FILL 8 (~43ms) — preview
// nhanh, khong cho FILL 8.
this.resumeFill = (d.mode === 'play') ? this.FILL : this.RESUME_FILL;
// M19 Bug A: wake PWR ON sau warm-bypass — worklet rời graph không process
// nên hasStarted còn true từ lần play trước → không bao giờ gửi 'started'
// lại → main không crossfade đúng lúc. Reset ở clear mode 'play' → resume
// sau FILL gửi 'started' lần nữa (giống cold start). Clear 'play' khác khi
// đã active: main fadedIn=true rồi → 'started' thêm bị bỏ qua, vô hại.
if (d.mode === 'play') this.hasStarted = false;
this.silenced = false;
this.fadeFromL = this.lastOutL;
this.fadeFromR = this.lastOutR;
@@ -220,7 +181,29 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
return;
}
if (!d || d.type !== 'out' || !d.l || !d.r) return;
this._enqueueOut(d.l, d.r);
// V24: đang chờ flush (resume sau underrun/stall) → drop block stale.
// Backlog engine gửi khi worklet im là audio CŨ (chơi lại → lag cộng dồn
// ~1s) — chỉ nhận block MỚI từ sau flush_done.
if (this.flushPending) return;
this.outQueue.push({ l: d.l, r: d.r });
if (this.dbgOutFirst) {
this.dbgOutFirst = false;
this.port.postMessage({ type: 'dbg', tag: 'fx_out_first', t: Math.round(currentTime * 1000), q: this.outQueue.length });
}
while (this.outQueue.length > this.CAP) {
this.outQueue.shift();
this.statCapDrop++;
// V23: drop cứng = jump mẫu → click. Crossfade 64 mẫu từ mẫu output
// cuối sang block mới; chỉ đặt 1 lần cho đợt drop liên tiếp. outPos=0:
// block mới (front) phải phát từ đầu, không tiếp tục vị trí block cũ.
if (this.skipFade <= 0) {
this.skipFade = 64;
this.skipFadeTotal = 64;
this.skipFromL = this.lastOutL;
this.skipFromR = this.lastOutR;
}
this.outPos = 0;
}
};
// Telemetry (chẩn đoán crackle trên session thật): đếm sự kiện + báo main
// thread mỗi ~2s. Nếu user bật master FX nghe crackle mà stats cho thấy
@@ -234,30 +217,8 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.dbgInFirst = false;
this.dbgOutFirst = false;
this.dbgPlayFirst = false;
}
// M25: enqueue 1 block output — dùng chung cho node.port (track FX) và
// directPort (master FX worker→worklet thẳng). Logic giữ nguyên V24/V23.
_enqueueOut(l, r) {
if (this.stopped || this.flushPending) return;
this.outQueue.push({ l: l, r: r });
if (this.dbgOutFirst) {
this.dbgOutFirst = false;
this.port.postMessage({ type: 'dbg', tag: 'fx_out_first', t: Math.round(currentTime * 1000), q: this.outQueue.length });
}
while (this.outQueue.length > this.CAP) {
this.outQueue.shift();
this.statCapDrop++;
// V23: drop cứng = jump mẫu → click. Crossfade 64 mẫu từ mẫu output
// cuối sang block mới; chỉ đặt 1 lần cho đợt drop liên tiếp. outPos=0:
// block mới (front) phải phát từ đầu, không tiếp tục vị trí block cũ.
if (this.skipFade <= 0) {
this.skipFade = 64;
this.skipFadeTotal = 64;
this.skipFromL = this.lastOutL;
this.skipFromR = this.lastOutR;
}
this.outPos = 0;
}
this._wlLast = 0; // V46: thời điểm process() lần trước — đo gián đoạn audio thread
this._wlGapAt = 0; // V46: rate-limit wl_gap 1/s
}
process(inputs, outputs) {
const input = inputs[0];
@@ -266,6 +227,17 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
const outL = output[0];
const outR = output[1];
// V46: wl_gap — audio thread bị gián đoạn (process() cách >30ms, thường do
// main thread block chia sẻ buffer/GC) → dbg log. Rate-limit 1/s.
if (this._wlLast > 0) {
const _wlGap = currentTime - this._wlLast;
if (_wlGap > 0.030 && (currentTime - this._wlGapAt) > 1.0) {
this._wlGapAt = currentTime;
this.port.postMessage({ type: 'dbg', tag: 'wl_gap', t: Math.round(currentTime * 1000), g: Math.round(_wlGap * 1000) });
}
}
this._wlLast = currentTime;
// Góp input thành burst 4 block (1024 mẫu) → gửi main thread
// (transferable buffer). Main thread tách 4 WS frame → bridge batch 4.
if (input && input[0]) {
@@ -283,15 +255,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
inter[j * 2] = this.inL[j];
inter[j * 2 + 1] = this.inR[j];
}
if (this.directPort) {
// M25: master FX — input burst đi THẲNG worklet→worker (bỏ hop
// main; main stall không làm gián đoạn nguồn input → engine không
// ngừng sản xuất output).
this.directPort.postMessage({ type: 'in', data: inter.buffer }, [inter.buffer]);
} else {
// Track FX / chưa có directPort — legacy qua main thread.
this.port.postMessage({ type: 'in', data: inter.buffer }, [inter.buffer]);
}
this.port.postMessage({ type: 'in', data: inter.buffer }, [inter.buffer]);
this.inCount = 0;
if (this.dbgInFirst) {
this.dbgInFirst = false;
@@ -388,20 +352,11 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
// skip-flush o stopAllPlayback -> gap giua seam). Bo flush (round-trip)
// + re-buffer nho hon FILL 24 (~128ms) -> resume sau stall som hon
// (~43ms) -> gap wrap giam tu ~110-235ms xuong con vai chuc ms.
// V46: non-wrap underrun-stall (producer stall giua play, transport
// khong doi) — sau flush da bo backlog stale, cushion 8 block (~43ms,
// proven V45.4 seek/wrap) la du; FILL 24 (~128ms) lam moi underrun
// them ~60-130ms silence (recovery ~150-400ms -> ~90-130ms).
const _wrap = this.wrapMode;
this.outQueue.length = 0;
this.outPos = 0;
this.resumeFadeIn = true;
// V46.3: wrap-context underrun — resume chi can 4 block (~10.7ms) khong
// phai 8 (~21ms): queue pass moi la audio LIEN TUC (bar1 da bat dau),
// khong con backlog stale de phong — cho 8 block them ~10ms gap seam
// (log 08:49 wrap2: q=4 tai wrap -> tail 10.7ms can, bar1 toi +26ms,
// resume 8-block -> gap ~35ms = giat lag). Non-wrap giu 8 (V46).
this.resumeFill = _wrap ? 4 : 8;
this.resumeFill = _wrap ? 8 : this.FILL;
this.flushPending = _wrap ? false : true;
if (!_wrap) this.port.postMessage({ type: 'flush' });
i--;
+2 -2
View File
@@ -34,7 +34,7 @@
<script src="/static/vendor/react-dom.production.min.js"></script>
<script src="/static/js/services/fluidsynthLoader.js?v=202608290945201545"></script>
<script src="/static/js/services/runtime.js?v=202608290945"></script>
<script src="/static/js/services/api.js?v=202609031108"></script>
<script src="/static/js/services/api.js?v=202608290945"></script>
<script src="/static/js/services/audioEngine.js?v=202608290945"></script>
<script src="/static/js/services/storage.js?v=202608290945"></script>
<script src="/static/js/services/soundfontStorage.js?v=202608290945"></script>
@@ -51,7 +51,7 @@
<script src="/static/js/services/promptTemplateManager.js?v=202608290945"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202608290945"></script>
<script src="/static/js/services/chordTheory.js?v=202608290945"></script>
<script src="/static/js/app.precompiled.js?v=202609070903202609070853202609070713202609071004202609071038202609071125" defer></script>
<script src="/static/js/app.precompiled.js?v=202609011700" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202608290945">
<style>
:root {
+1 -2
View File
@@ -12,7 +12,6 @@ const transformed = Babel.transform(src, {
filename: 'app.jsx',
sourceType: 'script',
}).code;
// M25: gom ca fx-rt-worker.js vao cache-buster (worker URL trong app.jsx).
const out = transformed.replace(/((?:sf-fx-realtime|fx-rt-worker)\.js\?v=)\d+/g, '$1' + ver);
const out = transformed.replace(/(sf-fx-realtime\.js\?v=)\d+/g, '$1' + ver);
writeFileSync('app/static/js/app.precompiled.js', out.replace(/\r?\n/g, '\r\n')); // repo CRLF
console.log('BUILD OK', 'v=' + ver, out.length, 'bytes');
+7
View File
@@ -69,6 +69,13 @@ if ($LASTEXITCODE -ge 8) { Write-Host "ERROR: robocopy install that bai (exit $L
if (-not (Test-Path "install\daw_engine\_internal")) { Write-Host "ERROR: install\daw_engine\_internal thieu - onedir layout sai" -ForegroundColor Red; exit 1 }
# 3 binary (bridge/lib/app) da commit san trong install/ - khong ghi de,
# neu user da build lai bridge/app thi tu copy vao day.
# VC++ runtime app-local: bridge C++ link /MD can MSVCP140/VCRUNTIME140 canh
# exe (khong phu thuoc vc_redist he thong). Dong bo tu src-tauri/binaries.
foreach ($dll in @("MSVCP140.dll", "MSVCP140_2.dll", "VCRUNTIME140.dll", "VCRUNTIME140_1.dll")) {
$src = "src-tauri\binaries\$dll"
if (Test-Path $src) { Copy-Item $src "install\$dll" -Force }
else { Write-Host "WARN: thieu $dll trong src-tauri\binaries - chay build_windows.ps1 -Bridge truoc" -ForegroundColor Yellow }
}
Write-Host ""
Write-Host "== DONE =="
+8
View File
@@ -125,6 +125,14 @@ if ($doBridge) {
Copy-Item "native_bridge\build\Release\$dll" "src-tauri\binaries\$dll" -Force
}
}
# VC++ runtime app-local (MSVCP140/VCRUNTIME140) — bridge C++ link /MD nen
# can cac dll nay CANH exe, khong phu thuoc vc_redist he thong. Lay tu
# System32 (ban redist da cai) de bundle vao installer/portable.
foreach ($dll in @("MSVCP140.dll", "MSVCP140_2.dll", "VCRUNTIME140.dll", "VCRUNTIME140_1.dll")) {
$sys = Join-Path $env:WINDIR "System32\$dll"
if (Test-Path $sys) { Copy-Item $sys "src-tauri\binaries\$dll" -Force }
else { Write-Host "WARN: thieu $dll trong System32 (VC++ redist chua cai?)" -ForegroundColor Yellow }
}
Write-Host "Bridge OK -> src-tauri/binaries"
}
+13
View File
@@ -116,6 +116,19 @@ def main():
port = _pick_port()
os.environ["SF_PORT"] = str(port)
# Announce port to Tauri shell: UI bootstrap đọc %APPDATA%/SonicForgeDAW/
# engine_port.txt (qua lệnh get_engine_port) để kết nối ĐÚNG engine của
# chính nó thay vì quét port — quét 8000-8010 có thể dính engine của
# instance khác (2 app chạy cùng lúc) → UI mất server khi instance đó tắt.
# Format "port\nengine_exe_path": Tauri đối chiếu path engine với candidate
# của CHÍNH nó → 2 app chạy song song mỗi cái lấy đúng port của mình.
try:
port_file = os.path.join(os.path.dirname(log_dir), "engine_port.txt")
with open(port_file, "w", encoding="utf-8") as f:
f.write("%d\n%s" % (port, sys.executable))
except OSError:
pass
# Log ra file — khi dong goi console=False, stdout khong nhin thay duoc.
try:
logging.basicConfig(
+16 -3
View File
@@ -16,9 +16,22 @@ _SPEC_ROOT = os.path.abspath(SPECPATH)
# root (dest '.') de find_bridge_exe (frozen: exe_dir = thu muc chua
# sys.executable = dist/daw_engine) tu dong tim thay. Chi build tren Windows.
if os.name == 'nt':
_bridge_dir = os.path.join(_SPEC_ROOT, 'native_bridge', 'build', 'Release')
for _b in ('daw_vst_bridge.exe', 'fx_vst_bridge.exe', 'jpeg62.dll', 'libfluidsynth-3.dll', 'plugin_host.exe'):
_bp = os.path.join(_bridge_dir, _b)
# 2 nguon build LECH NHAU (build_windows.ps1 [2]): exe tu MSBuild
# bridge-win\Release, DLL tu CMake build\Release. Truoc day engine.spec chi
# nhin build\Release -> bundle chua fx_vst_bridge.exe CU/HONG (282KB,
# --scan crash c0000409 ngay ca voi dir khong ton tai). Lay tung loai tu
# dung nguon, co fallback ve build\Release cho checkout cu.
_exe_dir = os.path.join(_SPEC_ROOT, 'native_bridge', 'build', 'bridge-win', 'Release')
_dll_dir = os.path.join(_SPEC_ROOT, 'native_bridge', 'build', 'Release')
if not os.path.isdir(_exe_dir):
print("WARN: khong thay bridge-win/Release - fallback ve build/Release (bridge co the cu)")
_exe_dir = _dll_dir
for _b in ('daw_vst_bridge.exe', 'fx_vst_bridge.exe', 'plugin_host.exe'):
_bp = os.path.join(_exe_dir, _b)
if os.path.exists(_bp):
binaries.append((_bp, '.'))
for _b in ('jpeg62.dll', 'libfluidsynth-3.dll'):
_bp = os.path.join(_dll_dir, _b)
if os.path.exists(_bp):
binaries.append((_bp, '.'))
# G3: JUCE FX bridge (graph latency + processed samples) - bundle rieng vi
+74
View File
@@ -0,0 +1,74 @@
// native_bridge/include/VstDllSearch.h
// iZotope VST3 (Ozone 11, Nectar, Neutron...) là single-file .vst3 — core
// DLL (vd iZOzone11Core.dll) nằm RIÊNG ở
// C:\Program Files\iZotope\<Product>\Cores
// KHÔNG cạnh plugin → LoadLibraryW của VST3 SDK fail ERROR_MOD_NOT_FOUND
// (0x7E) dù setPluginSearchPath đã trỏ dir cạnh plugin. Helper này quét các
// dir Cores cố định và thêm vào PATH (process-global — bridge load 1 plugin
// 1 lúc, giống SetDllDirectoryW hiện có).
#ifndef VST_DLL_SEARCH_H
#define VST_DLL_SEARCH_H
#include <string>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
#ifdef _WIN32
inline std::string vstDllSearchExtras() {
std::string extras;
const char* bases[] = {
"C:\\Program Files\\iZotope",
"C:\\Program Files\\Common Files\\iZotope",
};
for (const char* base : bases) {
std::string pat = std::string(base) + "\\*";
WIN32_FIND_DATAA fd;
HANDLE h = FindFirstFileA(pat.c_str(), &fd);
if (h == INVALID_HANDLE_VALUE) continue;
do {
if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) continue;
if (fd.cFileName[0] == '.') continue;
std::string cores = std::string(base) + "\\" + fd.cFileName + "\\Cores";
if (GetFileAttributesA(cores.c_str()) != INVALID_FILE_ATTRIBUTES) {
if (!extras.empty()) extras += ";";
extras += cores;
}
} while (FindNextFileA(h, &fd));
FindClose(h);
}
return extras;
}
// Append extras vào PATH (bỏ dir đã có). Gọi TRƯỚC Module::create.
inline void vstAddDllSearchPath(const std::string& extras) {
if (extras.empty()) return;
char buf[32768];
DWORD n = GetEnvironmentVariableA("PATH", buf, sizeof(buf));
std::string path = n ? std::string(buf) : "";
std::string toAdd;
size_t start = 0;
while (start <= extras.size()) {
size_t end = extras.find(';', start);
std::string dir = extras.substr(start, end == std::string::npos ? std::string::npos : end - start);
if (!dir.empty() && path.find(dir) == std::string::npos) {
if (!toAdd.empty()) toAdd += ";";
toAdd += dir;
}
if (end == std::string::npos) break;
start = end + 1;
}
if (toAdd.empty()) return;
if (!path.empty()) path += ";";
path += toAdd;
SetEnvironmentVariableA("PATH", path.c_str());
}
#else
inline std::string vstDllSearchExtras() { return std::string(); }
inline void vstAddDllSearchPath(const std::string&) {}
#endif
#endif // VST_DLL_SEARCH_H
+2 -52
View File
@@ -20,7 +20,6 @@ add_executable(juce_fx_bridge
main_juce_fx.cpp
JuceFxLoop.cpp
JuceFxEngine.cpp
JuceFxGuiHost.cpp
)
target_include_directories(juce_fx_bridge PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../include
@@ -29,61 +28,12 @@ target_link_libraries(juce_fx_bridge PRIVATE
juce::juce_core
juce::juce_audio_basics
juce::juce_audio_processors
# M1 editor host: AudioProcessorEditor cần gui_basics (Component/MessageManager)
juce::juce_gui_basics
)
# G2: VST3 hosting — JUCE mặc định JUCE_PLUGINHOST_VST3=0, bật để
# VST3PluginFormat (findAllTypesForFile/createInstanceFromDescription/...)
# được compile (juce_audio_processors tự bundle VST3_SDK).
target_compile_definitions(juce_fx_bridge PRIVATE JUCE_PLUGINHOST_VST3=1)
# M2 GUI thread: runDispatchLoopUntil cần modal loops được phép
target_compile_definitions(juce_fx_bridge PRIVATE JUCE_MODAL_LOOPS_PERMITTED=1)
if(WIN32)
# timeBeginPeriod (winmm) + MMCSS AvSetMmThreadCharacteristics (avrt)
target_link_libraries(juce_fx_bridge PRIVATE winmm avrt)
endif()
# ── M1 selftest (editor API — KHÔNG ship): verify editor host trên instance
# DSP thật trước khi M2 attach GUI thread. Chạy thủ công:
# build_juce/Release/juce_fx_selftest.exe "<path.vst3>"
add_executable(juce_fx_selftest
editor_selftest.cpp
JuceFxEngine.cpp
)
target_include_directories(juce_fx_selftest PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../include
)
target_link_libraries(juce_fx_selftest PRIVATE
juce::juce_core
juce::juce_audio_basics
juce::juce_audio_processors
juce::juce_gui_basics
)
target_compile_definitions(juce_fx_selftest PRIVATE JUCE_PLUGINHOST_VST3=1)
target_compile_definitions(juce_fx_selftest PRIVATE JUCE_MODAL_LOOPS_PERMITTED=1)
if(WIN32)
target_link_libraries(juce_fx_selftest PRIVATE winmm)
endif()
# ── M2 gui selftest (GUI thread + host window — KHÔNG ship): verify host
# window trên instance DSP thật, không SHM:
# build_juce/Release/juce_fx_gui_selftest.exe "<path.vst3>"
add_executable(juce_fx_gui_selftest
gui_host_selftest.cpp
JuceFxEngine.cpp
JuceFxGuiHost.cpp
)
target_include_directories(juce_fx_gui_selftest PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../include
)
target_link_libraries(juce_fx_gui_selftest PRIVATE
juce::juce_core
juce::juce_audio_basics
juce::juce_audio_processors
juce::juce_gui_basics
)
target_compile_definitions(juce_fx_gui_selftest PRIVATE JUCE_PLUGINHOST_VST3=1)
target_compile_definitions(juce_fx_gui_selftest PRIVATE JUCE_MODAL_LOOPS_PERMITTED=1)
if(WIN32)
target_link_libraries(juce_fx_gui_selftest PRIVATE winmm)
# timeBeginPeriod (winmm) — như fx_vst_bridge
target_link_libraries(juce_fx_bridge PRIVATE winmm)
endif()
+3 -284
View File
@@ -9,12 +9,10 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <mutex>
#include <string>
#include <vector>
@@ -191,271 +189,13 @@ struct JuceFxEngine::Impl {
std::vector<juce::AudioProcessorGraph::Node::Ptr> pluginNodes; // node theo slot (null = bỏ qua)
std::vector<uint32_t> slotLatencies; // latency thật từng slot (0 = skip/bypass/fail)
uint32_t totalLatency = 0;
// ── Editor host (M1): slot -> processor + editor (chỉ GUI thread đụng).
struct SlotEditor {
juce::AudioProcessor* proc = nullptr; // processor slot (không sở hữu)
std::unique_ptr<juce::AudioProcessorEditor> editor; // sở hữu editor (GUI thread)
bool open = false;
bool visible = true; // M2: trạng thái Show/Hide (GuiHost sync window)
std::string captured; // state capture gần nhất (raw)
};
struct EditorCmd {
uint32_t slot = 0;
EditorAction action = EditorAction::Open;
int w = 0, h = 0;
};
std::vector<SlotEditor> editors; // theo slot (dựng lại mỗi prepare)
std::vector<EditorCmd> editorQueue; // lệnh chờ (requestEditor)
mutable std::mutex editorMutex; // editors + captured
std::mutex queueMutex; // editorQueue
// M14: retry Open khi editor chưa mở (lần Open trước fail/trễ).
struct RetryOpen {
uint32_t slot = 0;
int w = 0, h = 0;
uint64_t dueMs = 0;
int tries = 0;
};
std::vector<RetryOpen> retryOpens; // M14 GUI-thread-only
};
JuceFxEngine::JuceFxEngine() : impl_(std::make_unique<Impl>()) {}
JuceFxEngine::~JuceFxEngine() { shutdown(); }
void JuceFxEngine::requestEditor(uint32_t slot, EditorAction action, int w, int h) {
// Chỉ xếp lệnh (thread-safe); validation + thực thi ở pumpEditorQueue()
// trên GUI thread — không log ở đây (có thể gọi từ thread khác).
std::lock_guard<std::mutex> lk(impl_->queueMutex);
impl_->editorQueue.push_back({slot, action, w, h});
}
bool JuceFxEngine::pumpEditorQueue() {
std::vector<Impl::EditorCmd> cmds;
{
// M14: retry drain cần chạy kể cả khi queue rỗng (retry pending)
// — bỏ early-return, chỉ đổi queue nếu có lệnh.
std::lock_guard<std::mutex> lk(impl_->queueMutex);
if (!impl_->editorQueue.empty()) cmds.swap(impl_->editorQueue);
}
bool any = false;
// M14: đồng hồ + retry-open helpers (GUI-thread-only).
const auto nowMs = []() -> uint64_t {
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
};
const auto retryClearSlot = [this](uint32_t slot) {
auto& v = impl_->retryOpens;
for (auto it = v.begin(); it != v.end(); ++it) {
if (it->slot == slot) { v.erase(it); return; }
}
};
const auto retrySchedule = [this, &nowMs](uint32_t slot, int w, int h) {
for (const auto& r : impl_->retryOpens)
if (r.slot == slot) return; // không xếp chồng
impl_->retryOpens.push_back({slot, w, h, nowMs() + 1000, 0});
};
for (const auto& c : cmds) {
std::lock_guard<std::mutex> lk(impl_->editorMutex);
if (c.slot >= impl_->editors.size()) {
std::cerr << "[JuceFxEngine] editor cmd slot " << c.slot
<< " ngoài chain (" << impl_->editors.size()
<< ") — bỏ qua" << std::endl;
continue;
}
auto& se = impl_->editors[c.slot];
if (!se.proc) {
if (c.action == EditorAction::Open)
std::cerr << "[JuceFxEngine] slot " << c.slot
<< " không có processor (bypass/fail) — không mở editor"
<< std::endl;
continue;
}
switch (c.action) {
case EditorAction::Open: {
if (!se.editor) {
juce::AudioProcessorEditor* ed = se.proc->createEditorIfNeeded();
if (!ed) {
std::cerr << "[JuceFxEngine] slot " << c.slot
<< " createEditorIfNeeded null (plugin không có UI)"
<< std::endl;
retrySchedule(c.slot, c.w, c.h); // M14: thử lại sau
break;
}
se.editor.reset(ed); // engine sở hữu editor (M1)
se.open = true;
se.visible = true; // M2: editor mở = hiển thị (GuiHost mở window)
if (c.w > 0 && c.h > 0) se.editor->setSize(c.w, c.h);
std::cerr << "[JuceFxEngine] slot " << c.slot << " editor open "
<< se.editor->getWidth() << "x"
<< se.editor->getHeight() << std::endl;
retryClearSlot(c.slot); // M14: mở thành công = hết retry
} else {
// Đã mở — Show lại (Reaper: editor còn sống, mở window lại).
se.visible = true;
se.editor->setVisible(true);
if (c.w > 0 && c.h > 0) se.editor->setSize(c.w, c.h);
retryClearSlot(c.slot); // M14
std::cerr << "[JuceFxEngine] slot " << c.slot
<< " editor ALREADY open — re-show" << std::endl;
}
any = true;
break;
}
case EditorAction::Close: {
if (se.editor) {
se.editor.reset(); // ~editor -> processor->editorBeingDeleted
se.open = false;
std::cerr << "[JuceFxEngine] slot " << c.slot
<< " editor closed" << std::endl;
any = true;
}
retryClearSlot(c.slot); // M14: đóng = huỷ retry mở dang dở
break;
}
case EditorAction::Show:
case EditorAction::Hide: {
// M2: GuiHost đồng bộ window theo flag visible (editor setVisible
// cho nội dung). Show = hiện window, Hide = ẩn (giữ editor+instance).
const bool vis = (c.action == EditorAction::Show);
se.visible = vis;
if (se.editor) se.editor->setVisible(vis);
any = true;
break;
}
case EditorAction::Resize: {
if (se.editor && c.w > 0 && c.h > 0) {
se.editor->setSize(c.w, c.h);
any = true;
}
break;
}
case EditorAction::Capture: {
juce::MemoryBlock mb;
se.proc->getStateInformation(mb);
se.captured.assign(static_cast<const char*>(mb.getData()), mb.getSize());
std::cerr << "[JuceFxEngine] slot " << c.slot << " captured "
<< se.captured.size() << " bytes" << std::endl;
any = true;
break;
}
}
}
// M14: Drain retry-open — editor chưa mở do lần Open trước fail/trễ.
{
auto& rv = impl_->retryOpens;
for (auto it = rv.begin(); it != rv.end();) {
bool gone;
{
std::lock_guard<std::mutex> lk(impl_->editorMutex);
gone = (it->slot >= impl_->editors.size()) ||
!impl_->editors[it->slot].proc ||
impl_->editors[it->slot].open;
}
if (gone || it->tries >= 2) {
it = rv.erase(it);
continue;
}
if (nowMs() >= it->dueMs) {
std::cerr << "[JuceFxEngine] M14 retry open slot " << it->slot
<< " lần " << (it->tries + 1) << std::endl;
{
std::lock_guard<std::mutex> lk(impl_->queueMutex);
impl_->editorQueue.push_back({it->slot, EditorAction::Open, it->w, it->h});
}
it->tries++;
it->dueMs = nowMs() + 1000;
}
++it;
}
}
return any;
}
bool JuceFxEngine::isEditorOpen(uint32_t slot) const {
std::lock_guard<std::mutex> lk(impl_->editorMutex);
if (slot >= impl_->editors.size()) return false;
return impl_->editors[slot].open;
}
bool JuceFxEngine::guiBusy() const {
{
std::lock_guard<std::mutex> lk(impl_->queueMutex);
if (!impl_->editorQueue.empty()) return true;
}
{
std::lock_guard<std::mutex> lk(impl_->editorMutex);
for (const auto& se : impl_->editors)
if (se.open) return true;
}
return !impl_->retryOpens.empty(); // GUI-thread-only — chỉ GUI thread gọi
}
std::string JuceFxEngine::takeCapturedState(uint32_t slot) {
std::lock_guard<std::mutex> lk(impl_->editorMutex);
if (slot >= impl_->editors.size()) return {};
std::string s = std::move(impl_->editors[slot].captured);
impl_->editors[slot].captured.clear();
return s;
}
void* JuceFxEngine::editorHandle(uint32_t slot) {
std::lock_guard<std::mutex> lk(impl_->editorMutex);
if (slot >= impl_->editors.size()) return nullptr;
return impl_->editors[slot].editor.get();
}
uint32_t JuceFxEngine::slotCount() const {
std::lock_guard<std::mutex> lk(impl_->editorMutex);
return (uint32_t)impl_->editors.size();
}
bool JuceFxEngine::hasOpenEditor() const {
std::lock_guard<std::mutex> lk(impl_->editorMutex);
for (const auto& se : impl_->editors)
if (se.open) return true;
return false;
}
void JuceFxEngine::closeAllEditors() {
// Gom slot đang mở (editorMutex) rồi enqueue Close (queueMutex) — gọi từ
// main thread trước khi prepare()/shutdown() khi GUI thread đang chạy.
std::vector<uint32_t> open;
{
std::lock_guard<std::mutex> lk(impl_->editorMutex);
for (size_t i = 0; i < impl_->editors.size(); ++i)
if (impl_->editors[i].open) open.push_back((uint32_t)i);
}
{
std::lock_guard<std::mutex> lk(impl_->queueMutex);
for (uint32_t s : open)
impl_->editorQueue.push_back({s, EditorAction::Close, 0, 0});
}
}
bool JuceFxEngine::isEditorVisible(uint32_t slot) const {
std::lock_guard<std::mutex> lk(impl_->editorMutex);
if (slot >= impl_->editors.size()) return false;
return impl_->editors[slot].open && impl_->editors[slot].visible;
}
void JuceFxEngine::shutdown() {
// Đóng mọi editor TRƯỚC khi clear graph — JUCE assert editor phải xoá
// trước processor. Editor ops ở đây chạy trên thread gọi shutdown; M1
// chưa có GUI thread đồng thời nên an toàn. Khi M2 có GUI thread: chain
// đổi phải gửi ed.close trước khi re-prepare (M4/M5).
{
std::lock_guard<std::mutex> lk(impl_->editorMutex);
for (auto& se : impl_->editors) {
if (se.editor) se.editor.reset(); // ~editor -> processor->editorBeingDeleted
se.proc = nullptr;
se.open = false;
se.visible = false;
se.captured.clear();
}
impl_->editors.clear();
}
impl_->retryOpens.clear(); // M14
if (impl_->prepared) {
impl_->graph.releaseResources();
impl_->graph.clear();
@@ -569,16 +309,9 @@ void JuceFxEngine::prepare(uint32_t sampleRate, uint32_t blockSize) {
if (base64Decode(s.presetB64, bytes) && !bytes.empty()) {
juce::MemoryBlock mb(bytes.data(), bytes.size());
if (juce::VST3PluginFormat::setStateFromVSTPresetFile(inst.get(), mb))
std::cerr << "[JuceFxEngine] slot " << i << " preset applied (vstpreset)" << std::endl;
else {
// M13: preset_b64 tu editor capture (getStateInformation) la
// JUCE VST3 state raw (4B len + "VstW" + payload), KHONG
// phai file .vstpreset -> setStateFromVSTPresetFile luon FAIL
// -> plugin mo default moi lan spawn (preset da luu mat).
// Ap thang setStateInformation (doi xung capture - cung wrapper).
inst->setStateInformation(mb.getData(), (int)mb.getSize());
std::cerr << "[JuceFxEngine] slot " << i << " preset applied (state)" << std::endl;
}
std::cerr << "[JuceFxEngine] slot " << i << " preset applied" << std::endl;
else
std::cerr << "[JuceFxEngine] slot " << i << " preset FAILED" << std::endl;
}
}
auto node = g.addNode(std::unique_ptr<juce::AudioProcessor>(std::move(inst)));
@@ -591,20 +324,6 @@ void JuceFxEngine::prepare(uint32_t sampleRate, uint32_t blockSize) {
prev = node;
}
if (cacheDirty) saveDescCache(descCache);
// Editor mapping: slot -> processor (Reaper-style — editor dùng ĐÚNG
// instance trong graph). Editors cũ đã đóng ở shutdown() khi graph đổi;
// chain không đổi (early-return trên) thì editors giữ nguyên + proc sống.
// Lock editorMutex: GUI thread (GuiHost tick reconcile) có thể đang đọc.
{
std::lock_guard<std::mutex> lk(impl_->editorMutex);
impl_->editors.clear();
impl_->editors.resize(impl_->slots.size());
for (size_t i = 0; i < impl_->pluginNodes.size(); ++i)
if (impl_->pluginNodes[i])
impl_->editors[i].proc = impl_->pluginNodes[i]->getProcessor();
}
for (int ch = 0; ch < 2; ++ch) {
NCh src; src.nodeID = prev->nodeID; src.channelIndex = ch;
NCh dst; dst.nodeID = outNode->nodeID; dst.channelIndex = ch;
-54
View File
@@ -28,60 +28,6 @@ public:
uint32_t latencySamples() const;
// Latency thật từng slot theo thứ tự chain (0 = skip/bypass/fail).
std::vector<uint32_t> entryLatencies() const;
// ── Editor host (Phase 2 — Reaper-style): editor của CHÍNH instance DSP
// trong graph, không instance thứ 2 / không feeder. Audio thread
// process() KHÔNG bao giờ chạm editor; mọi thao tác đi qua queue và chỉ
// thực thi trong pumpEditorQueue() trên GUI thread (JUCE message thread).
enum class EditorAction {
Open, // tạo editor (createEditorIfNeeded) cho slot — giữ instance DSP
Close, // huỷ editor, GIỮ instance DSP + state (mở lại nhanh)
Show, // hiện editor + host window (M2)
Hide, // ẩn editor + host window, GIỮ editor+instance (mở lại nhanh)
Resize, // đổi kích thước editor (M2: host window)
Capture // đọc state processor (getStateInformation) -> captured[slot]
};
// Thread-safe (gọi từ bất kỳ thread nào, kể cả ctrl-reader): xếp lệnh.
// w/h dùng cho Open/Resize (kích thước editor mong muốn).
void requestEditor(uint32_t slot, EditorAction action, int w = 0, int h = 0);
// GUI thread (JUCE message thread): drain queue + thực thi lệnh editor.
// Trả true nếu xử lý >= 1 lệnh. Không gọi từ audio thread.
bool pumpEditorQueue();
// Thread-safe đọc: slot có editor đang mở không.
bool isEditorOpen(uint32_t slot) const;
// Thread-safe: lấy + xoá state capture gần nhất của slot (rỗng nếu chưa).
// Raw bytes (chưa base64) — M3 encode khi đẩy về engine Python.
std::string takeCapturedState(uint32_t slot);
// ── GUI host support (M2): GuiHost chạy trên GUI thread dùng các API này
// để attach editor vào host window + teardown an toàn. Header giữ JUCE-free
// (editorHandle trả void* = juce::Component*).
// GUI thread: con trỏ editor Component của slot (null nếu chưa mở). Chỉ
// gọi trên message thread, cùng lúc pumpEditorQueue().
void* editorHandle(uint32_t slot);
// Thread-safe: số slot chain hiện tại (sau prepare; trước = 0).
uint32_t slotCount() const;
// Thread-safe: có editor nào đang mở không (teardown đồng bộ).
bool hasOpenEditor() const;
// Thread-safe: enqueue Close cho MỌI slot đang mở — main thread gọi trước
// engine.prepare()/shutdown() khi GUI thread đang chạy (editor là JUCE
// Component, phải đóng trên message thread; GuiHost chờ + pump).
void closeAllEditors();
// Thread-safe đọc: editor slot đang hiển thị (Show/Hide) không.
bool isEditorVisible(uint32_t slot) const;
// GUI host: có việc editor cần tick nhanh không (queue lệnh chờ / editor
// đang mở / retry Open chờ). Gọi từ GUI thread (retryOpens GUI-thread-only).
bool guiBusy() const;
void shutdown();
private:
-622
View File
@@ -1,622 +0,0 @@
// native_bridge/juce_fx/JuceFxGuiHost.cpp
// M2 implementation: GUI thread + PluginWindow host. Lịch sử fix:
// - tick 30ms = pumpEditorQueue + reconcile (editor Component chỉ đụng trên
// message thread — mọi JUCE GUI op chạy trong GUI thread này).
// - KHÔNG dùng juce::Timer cho cadence: timer không fire trên GUI thread
// riêng (0 tick trong selftest) — guiMain dùng inline steady_clock 30ms
// quanh runDispatchLoopUntil(10), tick() chạy trên message thread.
// - stop(): waitEditorsClosed(1s) → quit → join 2s → detach nếu kẹt
// (plugin editor block message thread); teardown còn editor thì close
// TRƯỚC shutdownJuce_GUI (JUCE assert editor phải xoá trước MM).
// - reconcile erase window khi !isEditorOpen (sau re-prepare editors clear);
// hide = setVisible(false) GIỮ window object (editor + instance sống).
#include "JuceFxGuiHost.h"
#include "JuceFxEngine.h"
#include <juce_gui_basics/juce_gui_basics.h>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <tlhelp32.h>
#endif
#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <cstdint>
#include <functional>
#include <iostream>
#include <mutex>
#include <thread>
#include <string>
#include <tuple>
#include <cwchar>
#include <utility>
#include <vector>
namespace {
// Số slot tối đa — khớp FXRT_CTRL_SLOTS (FxRealtimeIPC.h); anchor per slot.
constexpr uint32_t kAnchorSlots = 8;
// Window host cho editor của slot (engine sở hữu editor — window không xoá).
class PluginWindow final : public juce::DocumentWindow {
public:
PluginWindow(uint32_t slot, juce::Component* editor, JuceFxGuiHost& host)
: juce::DocumentWindow("FX slot " + juce::String((int)slot),
juce::Colours::darkgrey,
juce::DocumentWindow::allButtons,
true), // addToDesktop — native window thật
slot(slot),
host_(host) {
setUsingNativeTitleBar(true);
setResizable(true, false); // user resize được; resizeToFit co theo editor
// resizeToFitWhenContentChangesSize=true: window duy trì kích thước ôm
// editor (plugin đổi size → window theo). Engine vẫn sở hữu editor.
setContentNonOwned(editor, true);
editor_ = editor;
// M9b: WS_EX_TOOLWINDOW dặt NGAY khi native window tồn tại (DocumentWindow
// ctor đã addToDesktop) — cửa sổ GUI plugin không tạo taskbar icon phụ /
// không lọt Alt-Tab kể cả khi owner DAW chưa gắn được (unowned tạm thời).
// Đặt ở preShow (cũ) vẫn có thể show sớm trước khi style áp.
if (auto* peer = getPeer()) {
if (HWND hwnd = (HWND)peer->getNativeHandle()) {
// M15: JUCE addToDesktop set WS_EX_APPWINDOW (0x40000) — taskbar
// icon riêng THẮNG WS_EX_TOOLWINDOW. Clear APPWINDOW + set
// TOOLWINDOW trước show: window plugin không bao giờ vào taskbar.
LONG_PTR ex = GetWindowLongPtrA(hwnd, GWL_EXSTYLE);
if ((ex & WS_EX_APPWINDOW) || !(ex & WS_EX_TOOLWINDOW))
SetWindowLongPtrA(hwnd, GWL_EXSTYLE,
(ex & ~WS_EX_APPWINDOW) | WS_EX_TOOLWINDOW);
}
}
}
// X button → Close editor (giữ instance DSP + state — mở lại nhanh như
// Reaper). Tick sau: pump Close → reconcile xoá window này.
void closeButtonPressed() override { host_.onWindowClose(slot); }
// M7: SetWindowPos window vào rect anchor (outer bounds == rect; caption
// native nằm trong rect như cửa sổ embedded). HWND_TOP (KHÔNG TOPMOST —
// fix "floating over other apps") + SWP_NOACTIVATE — mirror FxGuiServer
// /show. Re-anchor chỉ khi rect ĐỔI so với lần áp cuối (dedupe — tránh
// flicker/giật khi user thao tác DAW mà panel yên). Chạy trên GUI thread
// (reconcile tick). Window mới chưa áp lần nào → luôn áp.
// M13c (Bug 1): kich thuoc window = DUNG editor tu nhien + khung cua so —
// KHONG ep theo rect panel (M7/M12 lam window to hon editor cua plugin
// kich thuoc co dinh nhu Ozone 11 -> vung DocumentWindow trong quanh GUI,
// "GUI lot thom giua khung"). x,y van la goc tren-trai panel (neo dung o
// anchor trong DAW); w,h tu JS (rect panel) chi la doi chieu vi tri.
// Nhung doi GUI -> window doi theo qua resizeToFit content (JUCE).
void applyAnchor(int x, int y, int /*w*/, int /*h*/) {
auto* peer = getPeer();
if (!peer) return;
const HWND hwnd = (HWND)peer->getNativeHandle();
if (!hwnd) return;
const int ew = editor_ ? editor_->getWidth() : 0;
const int eh = editor_ ? editor_->getHeight() : 0;
if (ew < 32 || eh < 32) return; // editor chua co size — tick sau
RECT rc = { 0, 0, ew, eh };
AdjustWindowRectEx(&rc,
(DWORD)GetWindowLongPtrA(hwnd, GWL_STYLE),
FALSE,
(DWORD)GetWindowLongPtrA(hwnd, GWL_EXSTYLE));
const int cw = rc.right - rc.left;
const int chh = rc.bottom - rc.top;
if (cw <= 0 || chh <= 0) return;
if (applied_ && ax_ == x && ay_ == y && aw_ == cw && ah_ == chh) {
// M12 stick-to-anchor: rect khong doi nhung window bi keo di (native
// title bar cho drag) -> re-snap ve vi tri panel moi tick <=30ms (cam
// giac "dinh" nhu Reaper docked, khong floating). Bo qua khi an/thu
// nho — khong pha trang thai user. GetWindowRect re re — khong chi
// phi khi that su lech.
if (!IsWindowVisible(hwnd) || IsIconic(hwnd)) return;
RECT r;
if (!GetWindowRect(hwnd, &r)) return;
if (r.left == x && r.top == y && (r.right - r.left) == cw &&
(r.bottom - r.top) == chh)
return;
SetWindowPos(hwnd, HWND_TOP, x, y, cw, chh,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
std::cerr << "[JuceFxGuiHost] re-snap slot " << (int)slot
<< " -> " << x << "," << y << " " << cw << "x" << chh
<< " (editor " << ew << "x" << eh << ") hwnd=0x"
<< std::hex << (uintptr_t)hwnd << std::dec << std::endl;
return;
}
SetWindowPos(hwnd, HWND_TOP, x, y, cw, chh,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
std::cerr << "[JuceFxGuiHost] anchor slot " << (int)slot
<< " -> " << x << "," << y << " " << cw << "x" << chh
<< " (editor " << ew << "x" << eh << ") hwnd=0x"
<< std::hex << (uintptr_t)hwnd << std::dec << std::endl;
ax_ = x; ay_ = y; aw_ = cw; ah_ = chh; applied_ = true;
}
const uint32_t slot;
private:
JuceFxGuiHost& host_;
juce::Component* editor_ = nullptr; // content non-owned (engine so huu)
int ax_ = 0, ay_ = 0, aw_ = 0, ah_ = 0;
bool applied_ = false;
};
} // namespace
struct JuceFxGuiHost::Impl {
// Chỉ GUI thread chạm; stop() join GUI thread trước khi ~GuiHost.
std::vector<std::unique_ptr<PluginWindow>> windows_;
// runOnGui task queue: main thread xếp (engine.prepare/shutdown phải chạy
// trên GUI thread — thread tạo plugin instance); GUI thread drain mỗi vòng.
std::mutex guiMutex;
std::condition_variable guiCv;
std::vector<std::function<void()>> guiTasks;
uint64_t guiTaskDone = 0; // số task đã hoàn thành (runOnGui chờ tăng)
// M7: anchor rect per slot — ctrl thread (JuceFxLoop) ghi từng field
// (ed.rx/ry/rw/rh), GUI thread reconcile đọc dưới mutex + áp SetWindowPos.
struct Anchor {
bool xSet = false, ySet = false, wSet = false, hSet = false;
bool valid = false; // đủ 4 field (frontend gửi trọn bộ mỗi 250ms)
int x = 0, y = 0, w = 0, h = 0;
};
std::mutex anchorMutex;
std::vector<Anchor> anchors_; // size = kAnchorSlots (ctor khởi tạo)
// M9 owner window: main window DAW lam OWNER cho PluginWindow (owned
// luon TREN owner trong z-order -> khong bi app che khi DAW foreground,
// an theo owner khi minimize, khong taskbar/Alt-Tab rieng). GUI thread
// duy nhat ghi/doc (reconcile). 0 = chua tim thay (headless/test).
uintptr_t ownerHwnd = 0;
uint64_t ownerFindLastMs = 0;
};
// ── M9 helpers (Win32): tim main window DAW de lam owner. Bridge spawn boi
// engine, engine la sidecar cua DAW (Tauri) -> parent chain
// bridge -> engine -> sonicforge-daw. Fallback: scan process ten DAW.
#ifdef _WIN32
namespace {
uint64_t m9NowMs() {
using namespace std::chrono;
return (uint64_t)duration_cast<milliseconds>(
steady_clock::now().time_since_epoch())
.count();
}
std::wstring m9ExeName(DWORD pid) {
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) return L"";
std::wstring name;
PROCESSENTRY32W pe;
pe.dwSize = sizeof(pe);
if (Process32FirstW(snap, &pe)) {
do {
if (pe.th32ProcessID == pid) {
name = pe.szExeFile;
break;
}
} while (Process32NextW(snap, &pe));
}
CloseHandle(snap);
return name;
}
bool m9ExeIsDaw(const std::wstring& exe) {
std::wstring n;
for (wchar_t c : exe) n += (c >= L'A' && c <= L'Z') ? (wchar_t)(c + 32) : c;
if (n.size() > 4 && n.compare(n.size() - 4, 4, L".exe") == 0)
n = n.substr(0, n.size() - 4);
return n == L"sonicforge-daw";
}
DWORD m9DawPid() {
static DWORD cached = 0;
if (cached) return cached;
DWORD cur = GetCurrentProcessId();
for (int hop = 0; hop < 3; ++hop) { // bridge -> engine -> daw
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) break;
DWORD parent = 0;
PROCESSENTRY32W pe;
pe.dwSize = sizeof(pe);
if (Process32FirstW(snap, &pe)) {
do {
if (pe.th32ProcessID == cur) {
parent = pe.th32ParentProcessID;
break;
}
} while (Process32NextW(snap, &pe));
}
CloseHandle(snap);
if (!parent) break;
if (m9ExeIsDaw(m9ExeName(parent))) {
cached = parent;
return cached;
}
cur = parent;
}
// Fallback: process dau tien ten sonicforge-daw.
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap != INVALID_HANDLE_VALUE) {
PROCESSENTRY32W pe;
pe.dwSize = sizeof(pe);
if (Process32FirstW(snap, &pe)) {
do {
if (m9ExeIsDaw(pe.szExeFile)) {
cached = pe.th32ProcessID;
break;
}
} while (Process32NextW(snap, &pe));
}
CloseHandle(snap);
}
return cached;
}
struct M9EnumCtx {
DWORD pid;
std::vector<HWND> wins;
};
BOOL CALLBACK m9CollectWnd(HWND h, LPARAM lp) {
auto* ctx = reinterpret_cast<M9EnumCtx*>(lp);
DWORD pid = 0;
GetWindowThreadProcessId(h, &pid);
if (pid == ctx->pid) ctx->wins.push_back(h);
return TRUE;
}
HWND m9FindDawMainWindow() {
const DWORD pid = m9DawPid();
if (!pid) return nullptr;
M9EnumCtx ctx;
ctx.pid = pid;
EnumWindows(m9CollectWnd, reinterpret_cast<LPARAM>(&ctx));
HWND best = nullptr;
std::tuple<int, int, int> bestKey; // (iconic, !tauri, -area)
bool haveBest = false;
for (HWND h : ctx.wins) {
// M9b (Bug 2): chỉ top-level THẬT của DAW pid — loại window ẩn / child /
// owned (WebView2 host, tooltip, helper owned): chọn nhầm → owner sai →
// PluginWindow không owned được → taskbar icon riêng (err=5 symptom).
if (!IsWindowVisible(h)) continue;
if (GetWindowLongPtrA(h, GWL_STYLE) & WS_CHILD) continue;
if (GetParent(h) != nullptr) continue; // owned top-level: GetParent trả owner
RECT r;
if (!GetWindowRect(h, &r)) continue;
const int w = r.right - r.left;
const int hgt = r.bottom - r.top;
if (w <= 0 || hgt <= 0) continue; // hidden/minimized rect 0
const int area = w * hgt;
wchar_t cls[64] = {0};
GetClassNameW(h, cls, 64);
const bool tauri = (wcscmp(cls, L"Tauri Window") == 0);
const bool iconic = IsIconic(h) != 0;
const auto key = std::make_tuple(iconic ? 1 : 0, tauri ? 0 : 1, -area);
if (!haveBest || key < bestKey) {
haveBest = true;
best = h;
bestKey = key;
}
}
return best;
}
} // namespace
#endif
JuceFxGuiHost::JuceFxGuiHost(JuceFxEngine& engine)
: impl_(std::make_unique<Impl>()), engine_(engine) {
impl_->anchors_.assign(kAnchorSlots, Impl::Anchor{});
}
JuceFxGuiHost::~JuceFxGuiHost() { stop(); }
void JuceFxGuiHost::start() {
if (started_.exchange(true)) return;
quit_ = false;
guiExited_ = false;
thread_ = std::thread([this] { guiMain(); });
}
void JuceFxGuiHost::stop() {
if (!started_.load()) return;
std::cerr << "[JuceFxGuiHost] stop begin" << std::endl;
waitEditorsClosed(1000); // started_ vẫn true → có pump từ GUI thread
started_.store(false);
quit_ = true;
// Chờ guiMain thoát tối đa 2s (runDispatchLoopUntil trả sau <=50ms; kẹt
// khi plugin editor block message thread). Kẹt → detach, không treo audio
// loop; teardown lệch có thể xảy ra nhưng editor block là pathological.
for (int i = 0; i < 200 && !guiExited_.load(); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
if (guiExited_.load() && thread_.joinable()) {
thread_.join();
std::cerr << "[JuceFxGuiHost] stop joined" << std::endl;
} else if (thread_.joinable()) {
std::cerr << "[JuceFxGuiHost] GUI thread không thoát trong 2s — detach"
<< std::endl;
thread_.detach();
}
}
void JuceFxGuiHost::enqueueGuiTask(std::function<void()> task) {
// GUI không chạy / đang thoát → chạy inline (không có message thread).
if (!started_.load() || quit_.load() || guiExited_.load()) {
task();
return;
}
std::unique_lock<std::mutex> lk(impl_->guiMutex);
const uint64_t before = impl_->guiTaskDone;
impl_->guiTasks.push_back(std::move(task));
impl_->guiCv.wait(lk, [&] { return impl_->guiTaskDone > before; });
}
void JuceFxGuiHost::waitEditorsClosed(uint32_t timeoutMs) {
if (!started_.load()) return;
engine_.closeAllEditors(); // enqueue Close mọi editor mở
const auto t0 = std::chrono::steady_clock::now();
while (engine_.hasOpenEditor()) {
const auto el =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0)
.count();
if ((uint32_t)el >= timeoutMs) break;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
}
void JuceFxGuiHost::onWindowClose(uint32_t slot) {
engine_.requestEditor(slot, JuceFxEngine::EditorAction::Close);
}
void JuceFxGuiHost::setAnchorField(uint32_t slot, char field, int value) {
std::lock_guard<std::mutex> lk(impl_->anchorMutex);
if (slot >= impl_->anchors_.size()) return;
Impl::Anchor& a = impl_->anchors_[slot];
switch (field) {
case 'x': a.x = value; a.xSet = true; break;
case 'y': a.y = value; a.ySet = true; break;
case 'w': a.w = value; a.wSet = true; break;
case 'h': a.h = value; a.hSet = true; break;
default: return;
}
a.valid = a.xSet && a.ySet && a.wSet && a.hSet;
if (a.valid) {
std::cerr << "[JuceFxGuiHost] anchor fields slot " << slot << " rect "
<< a.x << "," << a.y << " " << a.w << "x" << a.h
<< std::endl;
}
}
void JuceFxGuiHost::clearAnchor(uint32_t slot) {
std::lock_guard<std::mutex> lk(impl_->anchorMutex);
if (slot < impl_->anchors_.size())
impl_->anchors_[slot] = Impl::Anchor{};
}
void JuceFxGuiHost::tick() {
static int n = 0;
if (++n <= 3 || n % 100 == 0)
std::cerr << "[JuceFxGuiHost] tick " << n << std::endl;
engine_.pumpEditorQueue();
reconcile();
}
void JuceFxGuiHost::reconcile() {
const uint32_t n = engine_.slotCount();
auto& windows = impl_->windows_;
// M9: gan owner (main DAW) cho window plugin — OWNED window luon tren
// owner: fix bug "click VST FX khong thay GUI" (bridge sinh window duoi
// main DAW foreground-lock). Tim DAW toi da 1 lan/s (ownerHwnd=0 -> DAW
// chua xac dinh / headless). Set truoc khi show; retry moi reconcile.
// M9b: log định danh window (class/title/pid/vis) — chẩn đoán err=5
// (owner target sai / integrity / target chết).
auto logWnd = [](HWND hwnd, const char* tag) {
wchar_t cls[128] = {0}, title[256] = {0};
GetClassNameW(hwnd, cls, 128);
GetWindowTextW(hwnd, title, 256);
char clsA[256], titleA[512];
snprintf(clsA, sizeof(clsA), "%ls", cls);
snprintf(titleA, sizeof(titleA), "%ls", title);
DWORD pid = 0;
GetWindowThreadProcessId(hwnd, &pid);
std::cerr << "[JuceFxGuiHost] " << tag << " hwnd=0x" << std::hex
<< (uintptr_t)hwnd << std::dec << " pid=" << pid
<< " class=\"" << clsA << "\" title=\"" << titleA << "\""
<< " vis=" << (IsWindowVisible(hwnd) ? 1 : 0)
<< std::endl;
};
auto applyOwner = [&](HWND wnd) {
if (!impl_->ownerHwnd) {
const uint64_t now = m9NowMs();
if (now - impl_->ownerFindLastMs >= 1000) {
impl_->ownerFindLastMs = now;
const HWND daw = m9FindDawMainWindow();
if (daw) {
impl_->ownerHwnd = (uintptr_t)daw;
logWnd(daw, "M9 owner DAW main");
}
}
}
if (!impl_->ownerHwnd) return;
const HWND target = (HWND)impl_->ownerHwnd;
if ((HWND)GetWindowLongPtrA(wnd, GWLP_HWNDPARENT) == target) return;
// M9b bug cũ: SetWindowLongPtrA trả PREVIOUS owner — previous NULL → trả
// 0 → code cũ log "fail err=0" dù THÀNH CÔNG (owned OK). SetLastError(0)
// trước call: fail luôn set error → err != 0 mới là fail thật.
SetLastError(0);
const LONG_PTR prev = SetWindowLongPtrA(wnd, GWLP_HWNDPARENT,
(LONG_PTR)target);
const DWORD err = GetLastError();
if (err == 0) {
std::cerr << "[JuceFxGuiHost] owned hwnd=0x" << std::hex
<< (uintptr_t)wnd << std::dec << " prevOwner=0x"
<< std::hex << (uintptr_t)prev << std::dec
<< " owner=0x" << std::hex << (uintptr_t)target
<< std::dec << std::endl;
} else {
std::cerr << "[JuceFxGuiHost] owner SetWindowLongPtr fail err="
<< err << std::hex << " hwnd=0x" << (uintptr_t)wnd
<< std::dec << std::endl;
logWnd(target, " target");
logWnd(wnd, " wnd");
}
};
// Xoá window: slot hết chain hoặc editor đã đóng (Close pump / re-prepare
// clear editors). ~DocumentWindow trên GUI thread; content editor có thể
// đã bị engine xoá — ResizableWindow giữ SafePointer nên an toàn.
for (auto it = windows.begin(); it != windows.end();) {
if ((*it)->slot >= n || !engine_.isEditorOpen((*it)->slot)) {
it = windows.erase(it);
} else {
++it;
}
}
// Tạo window cho slot đang mở; ẩn/hiện theo flag visible (Hide giữ window).
for (uint32_t s = 0; s < n; ++s) {
if (!engine_.isEditorOpen(s)) continue;
auto it = std::find_if(windows.begin(), windows.end(),
[s](const std::unique_ptr<PluginWindow>& w) {
return w->slot == s;
});
if (it != windows.end()) {
const bool vis = engine_.isEditorVisible(s);
if ((*it)->isVisible() != vis) (*it)->setVisible(vis);
continue;
}
auto* ed = static_cast<juce::Component*>(engine_.editorHandle(s));
if (!ed) continue; // open nhưng handle chưa sẵn — tick sau
auto w = std::make_unique<PluginWindow>(s, ed, *this);
if (auto* peer = w->getPeer()) {
// M9b: WS_EX_TOOLWINDOW đã đặt trong PluginWindow ctor (peer có ngay
// sau DocumentWindow ctor) — trước mọi show.
const HWND hwnd = (HWND)peer->getNativeHandle();
if (hwnd) {
std::cerr << "[JuceFxGuiHost] window slot " << (int)s
<< " preShow hwnd=0x" << std::hex
<< (uintptr_t)hwnd << std::dec
<< " exstyle=0x" << std::hex
<< GetWindowLongPtrA(hwnd, GWL_EXSTYLE)
<< std::dec << std::endl;
applyOwner(hwnd);
}
// M14: anchor NGAY TRUOC show — JS gui rect ngay sau openFxGui,
// thuong toi TRUOC reconcile tao window -> dat vi tri dung tick
// dau, khong flash vi tri mac dinh giua man hinh nhu window noi.
{
std::lock_guard<std::mutex> lk(impl_->anchorMutex);
if (s < impl_->anchors_.size()) {
const Impl::Anchor& a = impl_->anchors_[s];
if (a.valid) w->applyAnchor(a.x, a.y, a.w, a.h);
}
}
std::cerr << "[JuceFxGuiHost] window slot " << (int)s
<< " showing visible="
<< (engine_.isEditorVisible(s) ? 1 : 0) << std::endl;
w->setVisible(engine_.isEditorVisible(s));
std::cerr << "[JuceFxGuiHost] window slot " << (int)s
<< " hwnd=0x" << std::hex << (uintptr_t)hwnd << std::dec
<< " editor=" << ed->getWidth() << "x" << ed->getHeight()
<< std::endl;
} else {
w->setVisible(engine_.isEditorVisible(s));
}
windows.push_back(std::move(w));
}
// M7: áp anchor (rect panel embed) cho mọi window đang mở — snapshot dưới
// mutex, SetWindowPos trên GUI thread; dedupe trong applyAnchor. Window
// mới (chưa applied_) luôn áp; khi panel di chuyển rect đổi → re-anchor.
{
std::lock_guard<std::mutex> lk(impl_->anchorMutex);
for (const auto& wp : windows) {
if (wp->slot >= impl_->anchors_.size()) continue;
const Impl::Anchor& a = impl_->anchors_[wp->slot];
if (a.valid) wp->applyAnchor(a.x, a.y, a.w, a.h);
}
}
// M9 retry: window da mo truoc khi tim duoc DAW -> owned lai khi co.
for (const auto& wp : windows) {
if (auto* peer = wp->getPeer()) {
if (HWND hwnd = (HWND)peer->getNativeHandle()) {
// M9d: bo retry khi an (panel dong / DAW an) — owner khong set
// duoc luc ca hai an, spam err=5 moi tick vo nghia. Re-show ->
// ca hai visible -> retry tu nhien. Bo qua o retry LOOP nay
// thoi; applyOwner preShow (truoc show) van giu nguyen.
if (!IsWindowVisible(hwnd)) continue;
const HWND tgt = (HWND)impl_->ownerHwnd;
if (tgt && !IsWindowVisible(tgt)) continue;
applyOwner(hwnd);
}
}
}
winCount_.store((uint32_t)windows.size());
}
void JuceFxGuiHost::guiMain() {
#ifdef _WIN32
// BELOW_NORMAL: GUI spike không preempt audio thread HIGHEST.
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
#endif
// Thread này trở thành JUCE message thread.
std::cerr << "[JuceFxGuiHost] guiMain start" << std::endl;
juce::initialiseJuce_GUI();
std::cerr << "[JuceFxGuiHost] initialiseJuce_GUI ok" << std::endl;
// Cadence inline (30ms busy / 200ms idle) — KHÔNG dùng juce::Timer: trên GUI thread riêng,
// timer callback không bao giờ fire dù runDispatchLoopUntil pump liên tục
// (test cho thấy 0 tick). tick() ở đây chạy trên chính message thread nên
// pump + tạo/xoá Component an toàn.
{
using clock = std::chrono::steady_clock;
const auto fastMs = std::chrono::milliseconds(30);
const auto slowMs = std::chrono::milliseconds(200);
auto lastTick = clock::now();
while (!quit_.load()) {
// Drain runOnGui tasks trước mỗi pump: engine.prepare()/shutdown()
// phải chạy trên GUI thread (= thread tạo plugin instance — M2b).
for (;;) {
std::function<void()> t;
{
std::lock_guard<std::mutex> lk(impl_->guiMutex);
if (impl_->guiTasks.empty()) break;
t = std::move(impl_->guiTasks.front());
impl_->guiTasks.erase(impl_->guiTasks.begin());
}
if (t) t();
{
std::lock_guard<std::mutex> lk(impl_->guiMutex);
++impl_->guiTaskDone;
}
impl_->guiCv.notify_all();
}
juce::MessageManager::getInstance()->runDispatchLoopUntil(10);
const auto now = clock::now();
// Cadence động: có việc editor (queue/open/retry) → 30ms giữ
// anchor/reconcile phản hồi nhanh; idle (không editor) → 200ms —
// giảm wakeup + churn GUI thread BELOW_NORMAL khi chỉ chạy audio.
const auto curMs = engine_.guiBusy() ? fastMs : slowMs;
if (now - lastTick >= curMs) {
tick();
lastTick = now;
}
}
std::cerr << "[JuceFxGuiHost] loop exit" << std::endl;
}
// Teardown order: editor là JUCE Component — xoá TRƯỚC shutdownJuce_GUI.
std::cerr << "[JuceFxGuiHost] teardown begin" << std::endl;
engine_.closeAllEditors();
std::cerr << "[JuceFxGuiHost] teardown closeAllEditors ok" << std::endl;
engine_.pumpEditorQueue(); // xử lý Close đồng bộ (message thread)
std::cerr << "[JuceFxGuiHost] teardown pump ok" << std::endl;
impl_->windows_.clear(); // ~PluginWindow trên message thread
std::cerr << "[JuceFxGuiHost] teardown windows clear ok" << std::endl;
juce::shutdownJuce_GUI();
std::cerr << "[JuceFxGuiHost] teardown done" << std::endl;
guiExited_ = true;
}
-80
View File
@@ -1,80 +0,0 @@
// native_bridge/juce_fx/JuceFxGuiHost.h
// M2: GUI thread + window host (Reaper-style) — host editor của ĐÚNG instance
// DSP trong graph ngay trong fxrt_bridge (juce_fx_bridge.exe), không instance
// 2 / feeder (nguyên nhân CPU 100% .24).
//
// GUI thread riêng (BELOW_NORMAL) chạy juce::MessageManager + tick 30ms:
// engine.pumpEditorQueue() → reconcile window theo editor state.
// PluginWindow (DocumentWindow native) bọc juce::AudioProcessorEditor mà
// ENGINE sở hữu — window KHÔNG sở hữu content (setContentNonOwned).
// Idle (không Open, không play) ≈ 0 CPU như Reaper.
#pragma once
#include <atomic>
#include <cstdint>
#include <functional>
#include <memory>
#include <thread>
class JuceFxEngine;
class JuceFxGuiHost {
public:
explicit JuceFxGuiHost(JuceFxEngine& engine);
~JuceFxGuiHost(); // .cpp (Impl incomplete ở header)
JuceFxGuiHost(const JuceFxGuiHost&) = delete;
JuceFxGuiHost& operator=(const JuceFxGuiHost&) = delete;
// Start GUI thread (thread này thành JUCE message thread). Gọi TRƯỚC mọi
// engine.prepare() — plugin instance phải tạo trên GUI thread (plugin JUCE
// MM gắn thread tạo instance; editor tạo thread khác → treo — M1 chỉ chạy
// vì main thread vừa tạo instance vừa là message thread).
void start();
// Chạy task đồng bộ trên GUI thread (chờ xong). Main/audio thread dùng để
// bọc engine.prepare()/shutdown() — instance + editor sống trên cùng thread
// như plugin yêu cầu. KHÔNG gọi từ GUI thread; không started → chạy inline.
template <typename F>
void runOnGui(F&& f) {
enqueueGuiTask([f = std::forward<F>(f)]() mutable { f(); });
}
// Đóng editor + dừng GUI thread + join (tối đa 2s, detach nếu GUI thread
// kẹt trong plugin editor). Gọi TRƯỚC engine.shutdown().
void stop();
// Main thread: enqueue Close cho mọi editor đang mở + chờ GUI thread pump
// xong (poll hasOpenEditor). Bắt buộc trước engine.prepare()/shutdown()
// khi GUI thread đang chạy (editor là JUCE Component — xoá trên message
// thread). Không started → return ngay.
void waitEditorsClosed(uint32_t timeoutMs);
// Thread-safe: số host window đang tồn tại (selftest).
uint32_t windowCount() const { return winCount_.load(); }
// GUI thread: X button của PluginWindow slot → engine Close (queue) — tick
// sau pump + reconcile xoá window.
void onWindowClose(uint32_t slot);
// M7: anchor rect (embed panel — frontend đo + gửi ed.rx/ry/rw/rh qua SHM
// ctrl ring). Thread-safe — ctrl thread của JuceFxLoop gọi setAnchorField
// từng field (KHÔNG qua engine queue — engine không sở hữu window); GUI
// thread tick reconcile áp dụng bằng SetWindowPos khi rect ĐỔI (dedupe,
// mirror FxGuiServer g_embedRect) → window native nằm ĐÚNG rect panel.
// field = 'x'|'y'|'w'|'h'. clearAnchor: quên rect cũ (gọi lúc ed.open/
// ed.close — mở mới chỉ anchor sau khi có rect fresh từ frontend).
void setAnchorField(uint32_t slot, char field, int value);
void clearAnchor(uint32_t slot);
// GUI thread tick 30ms (GuiTickTimer trong .cpp gọi): pump + reconcile.
void tick();
private:
// runOnGui: xếp task cho GUI thread + chờ hoàn thành (count-based).
void enqueueGuiTask(std::function<void()> task);
// GUI thread: tạo/xoá/ẩn-hiện PluginWindow theo engine editor state.
void reconcile();
// GUI thread entry.
void guiMain();
struct Impl;
std::unique_ptr<Impl> impl_; // windows_ (PluginWindow chỉ thấy trong .cpp)
JuceFxEngine& engine_;
std::thread thread_;
std::atomic<bool> started_{false};
std::atomic<bool> quit_{false};
std::atomic<bool> guiExited_{false};
std::atomic<uint32_t> winCount_{0};
};
+3 -245
View File
@@ -6,7 +6,6 @@
// Validate header mỗi iteration: sampleRate/blockSize đổi giữa chừng →
// teardown + prepareToPlay lại + report latency mới qua FxLatReport.
#include "JuceFxEngine.h"
#include "JuceFxGuiHost.h"
#include "FxRealtimeIPC.h"
#include "FxShm.h"
@@ -16,17 +15,14 @@
#endif
#include <windows.h>
#include <mmsystem.h>
#include <avrt.h>
#endif
#include <algorithm>
#include <chrono>
#include <cstring>
#include <cstdio>
#include <fstream>
#include <iostream>
#include <iterator>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
@@ -82,114 +78,10 @@ std::string extractChainJson(const std::string& s) {
return s.substr(p, i - p + 1);
}
// M5: base64 encode (RFC 4648) cho editor state capture -> file preset.
static std::string base64Encode(const std::string& in) {
static const char* tbl =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string out;
out.reserve(((in.size() + 2) / 3) * 4);
for (size_t i = 0; i < in.size(); i += 3) {
unsigned a = (unsigned char)in[i];
unsigned b = i + 1 < in.size() ? (unsigned char)in[i + 1] : 0;
unsigned c = i + 2 < in.size() ? (unsigned char)in[i + 2] : 0;
out += tbl[a >> 2];
out += tbl[((a & 3) << 4) | (b >> 4)];
out += i + 1 < in.size() ? tbl[((b & 15) << 2) | (c >> 6)] : '=';
out += i + 2 < in.size() ? tbl[c & 63] : '=';
}
return out;
}
// M13: prefix moi dong std::cerr bang [p=<pid> t=<ms-tu-start>] - log cua
// nhieu bridge process tron 1 file (FXRT_BRIDGE_LOG default) nen khong tach
// duoc event theo process/thoi gian. Wrapper stream buffer: phat prefix o dau
// dong, khong buffer ca dong (cerr unitbuf flush tung token - emit o sync()
// se tao prefix gia); mutex bao an toan thread (main/hb/ctrl/gui thread cung
// cerr). Cai dau run_juce_fx_loop truoc khi thread khac chay.
class PfxBuf : public std::streambuf {
public:
explicit PfxBuf(std::streambuf* sink)
: sink_(sink), lineStart_(true), t0_(std::chrono::steady_clock::now()) {
pid_ = (unsigned long)::GetCurrentProcessId();
}
~PfxBuf() override {
if (sink_) std::cerr.rdbuf(sink_);
}
protected:
int_type overflow(int_type c) override {
if (traits_type::eq_int_type(c, traits_type::eof()))
return traits_type::not_eof(c);
char ch = traits_type::to_char_type(c);
std::lock_guard<std::mutex> lk(mu_);
if (!writeOne(ch)) return traits_type::eof();
return c;
}
std::streamsize xsputn(const char* s, std::streamsize n) override {
std::lock_guard<std::mutex> lk(mu_);
std::streamsize done = 0;
while (done < n) {
const char c = s[done];
if (c == '\n') {
if (sink_->sputc('\n') == traits_type::eof()) break;
++done;
lineStart_ = true;
continue;
}
if (!emitPrefix()) break;
std::streamsize j = done;
while (j < n && s[j] != '\n') ++j;
const std::streamsize chunk = j - done;
if (chunk > 0) {
const std::streamsize w = sink_->sputn(s + done, chunk);
if (w != chunk) break;
done += chunk;
}
}
return done;
}
int sync() override {
std::lock_guard<std::mutex> lk(mu_);
return sink_->pubsync() == 0 ? 0 : -1;
}
private:
bool emitPrefix() {
if (!lineStart_) return true;
lineStart_ = false;
const long long ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0_).count();
char buf[64];
const int len = std::snprintf(buf, sizeof(buf), "[p=%lu t=%lld] ",
pid_, ms);
if (len <= 0) return true;
return sink_->sputn(buf, len) == (std::streamsize)len;
}
bool writeOne(char c) {
if (c == '\n') {
if (sink_->sputc('\n') == traits_type::eof()) return false;
lineStart_ = true;
return true;
}
if (!emitPrefix()) return false;
return sink_->sputc(c) != traits_type::eof();
}
std::streambuf* sink_;
std::mutex mu_;
bool lineStart_;
unsigned long pid_;
std::chrono::steady_clock::time_point t0_;
};
} // namespace
int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName,
uint32_t parentPid) {
PfxBuf pfx(std::cerr.rdbuf());
std::cerr.rdbuf(&pfx);
std::cerr << "[JuceFxLoop] start job=" << jobPath << " shm=" << shmName
<< " parent=" << parentPid << std::endl;
const std::string job = readFile(jobPath);
@@ -230,18 +122,6 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName,
#ifdef _WIN32
timeBeginPeriod(1);
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
// MMCSS "Pro Audio": Windows đảm bảo quantum + ưu tiên cho thread này khi
// CPU cạnh tranh — chống spike deschedule (loopMax 430ms / 5756ms ở log).
// AvSetMmThreadCharacteristicsW (Avrt.lib). Process HIGH_PRIORITY_CLASS —
// không cần admin (REALTIME_PRIORITY_CLASS cần SeIncreaseBasePriorityPrivilege).
DWORD mmTaskIndex = 0;
HANDLE hMm = AvSetMmThreadCharacteristicsW(L"Pro Audio", &mmTaskIndex);
if (hMm) {
SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
std::cerr << "[JuceFxLoop] MMCSS Pro Audio ok" << std::endl;
} else {
std::cerr << "[JuceFxLoop] MMCSS unavailable err=" << GetLastError() << std::endl;
}
#endif
// Engine: prepare graph (chain theo fx_chain). sr/block từ job; header là
@@ -252,16 +132,7 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName,
engine.setChain(chainJson);
if (ipc->h.sampleRate) curSr = ipc->h.sampleRate;
if (ipc->h.blockSize) curBlock = ipc->h.blockSize;
// M2: GUI thread + host window (Reaper-style — editor của CHÍNH instance
// DSP trong graph, không instance 2 / feeder). GUI thread cũng là thread
// tạo plugin instance: JUCE plugin gắn MM vào thread tạo instance — editor
// tạo trên thread khác sẽ treo (selftest: createEditorIfNeeded hang khi
// instance tạo trên main). Vì vậy start() TRƯỚC prepare, prepare chạy qua
// runOnGui; main thread sau này chỉ engine.process (JUCE cho phép
// cross-thread process như host chuẩn).
JuceFxGuiHost guiHost(engine);
guiHost.start();
guiHost.runOnGui([&] { engine.prepare(curSr, curBlock); });
engine.prepare(curSr, curBlock);
std::cerr << "[JuceFxLoop] prepared sr=" << curSr << " block=" << curBlock
<< " latency=" << engine.latencySamples() << std::endl;
@@ -289,8 +160,6 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName,
ipc->h.state = FXRT_STATE_READY;
std::cerr << "[JuceFxLoop] ready" << std::endl;
// (guiHost đã start TRƯỚC engine.prepare ở trên — instance phải tạo trên
// GUI thread.) Editor chỉ mở khi có lệnh (M3 ctrl ring); idle ≈ 0 CPU.
std::thread hb([&]() {
while (ipc->h.running && ipc->h.state == FXRT_STATE_READY) {
ipc->h.heartbeat++;
@@ -298,110 +167,6 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName,
}
});
// M3: ctrl ring reader (engine → bridge, poll 2ms). Key "ed.*" = editor
// command (Reaper-style: engine điều khiển editor của CHÍNH instance DSP
// trong graph — không instance 2). Dispatch qua engine.requestEditor
// (thread-safe queue); GUI tick 30ms pumpEditorQueue thực thi. Key khác
// (SET_PARAM cũ: param name) — JuceFxEngine chưa có param API (params đi
// trong chain JSON) → consume + bỏ qua an toàn (ring không đầy, không chặn
// producer). w/h mã hoá trong value: w*4096 + h — chính xác tuyệt đối với
// w,h <= 4095 (float 24-bit mantissa); value <= 0 → editor preferred size
// (engine chỉ setSize khi w,h > 0).
std::thread ctrl([&]() {
while (ipc->h.running && ipc->h.state == FXRT_STATE_READY) {
while (ipc->h.ctrlRead < ipc->h.ctrlWrite) {
const uint32_t cs = ipc->h.ctrlRead & (FXRT_CTRL_SLOTS - 1);
const FxCtrlCmd& cmd = ipc->ctrl[cs];
const uint32_t slot = cmd.slot;
char key[17];
std::memcpy(key, cmd.key, 16);
key[16] = '\0';
const float value = cmd.value;
MemoryBarrier();
ipc->h.ctrlRead++; // consume trước dispatch — không block ring
if (std::strncmp(key, "ed.", 3) != 0)
continue; // SET_PARAM cũ — no-op (xem comment trên)
// M7: ed.rx/ry/rw/rh = anchor rect (frontend đo panel embed,
// gửi 4 key mỗi 250ms) → guiHost.setAnchorField (thread-safe,
// không qua engine queue); reconcile GUI thread SetWindowPos
// khi rect đổi → window native nằm đúng rect panel.
if (key[3] == 'r' && (key[4] == 'x' || key[4] == 'y' ||
key[4] == 'w' || key[4] == 'h') &&
key[5] == '\0') {
guiHost.setAnchorField(slot, key[4], (int)value);
continue;
}
JuceFxEngine::EditorAction act;
bool known = true;
if (std::strcmp(key, "ed.open") == 0)
act = JuceFxEngine::EditorAction::Open;
else if (std::strcmp(key, "ed.close") == 0)
act = JuceFxEngine::EditorAction::Close;
else if (std::strcmp(key, "ed.show") == 0)
act = JuceFxEngine::EditorAction::Show;
else if (std::strcmp(key, "ed.hide") == 0)
act = JuceFxEngine::EditorAction::Hide;
else if (std::strcmp(key, "ed.resize") == 0)
act = JuceFxEngine::EditorAction::Resize;
else if (std::strcmp(key, "ed.capture") == 0)
act = JuceFxEngine::EditorAction::Capture;
else
known = false;
if (!known) {
std::cerr << "[JuceFxLoop] ctrl ed.* lạ: " << key
<< " slot=" << slot << std::endl;
continue;
}
int w = 0, h = 0;
if (value > 0.0f) {
const int v = (int)(value + 0.5f);
w = v / 4096;
h = v % 4096;
}
// M16: KHÔNG clearAnchor khi Open — backend retry ed.open sau
// ~700ms (plugins.py) clear rect fresh JS vừa gửi → window mới
// tạo không anchor được (lần đầu mở panel). Rect cũ vẫn đúng khi
// panel yên (M7 dedupe); panel dời → JS gửi rect mới ≤250ms sửa.
// Chỉ clear khi Close (window xoá — rect cũ vô dụng).
engine.requestEditor(slot, act, w, h);
std::cerr << "[JuceFxLoop] ctrl " << key << " slot=" << slot
<< " w=" << w << " h=" << h << std::endl;
// M5: capture state editor (GUI đóng → preset về project như
// WM_FXGUI_CAPTURE cũ). PumpEditorQueue (GUI tick 30ms) thực thi
// Capture → captured[slot] (raw). Ctrl thread đợi tối đa 3s rồi
// base64 + ghi file JSON cạnh job: <jobPath>.preset.json
// {slot, preset_b64}. Python /fx-gui/close poll file này (xoá file
// cũ trước — chỉ đọc file MỚI cho capture lần này).
if (act == JuceFxEngine::EditorAction::Capture) {
const std::string capFile = jobPath + ".preset.json";
std::remove(capFile.c_str());
std::string raw;
for (int i = 0; i < 300 && raw.empty(); ++i) {
sleepMs(10);
if (!ipc->h.running || ipc->h.state != FXRT_STATE_READY)
break;
raw = engine.takeCapturedState(slot);
}
if (!raw.empty()) {
const std::string b64 = base64Encode(raw);
std::ofstream f(capFile, std::ios::binary);
f << "{\"slot\":" << slot << ",\"preset_b64\":\""
<< b64 << "\"}" << std::endl;
std::cerr << "[JuceFxLoop] capture slot=" << slot
<< " -> " << capFile << " (" << raw.size()
<< " bytes, " << b64.size() << " b64)"
<< std::endl;
} else {
std::cerr << "[JuceFxLoop] capture slot=" << slot
<< " rỗng — plugin không có state" << std::endl;
}
}
}
sleepMs(2);
}
});
const uint32_t n = curBlock;
const uint32_t inMask = ipc->h.inSlots - 1;
const uint32_t outMask = ipc->h.outSlots - 1;
@@ -427,11 +192,7 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName,
ipc->h.blockSize != curBlock)) {
curSr = ipc->h.sampleRate;
curBlock = ipc->h.blockSize;
// M2: editor là JUCE Component — đóng trên GUI thread TRƯỚC khi
// re-prepare; prepare() chạy trên GUI thread (thread tạo instance —
// xem startup, M2b).
guiHost.waitEditorsClosed(1500);
guiHost.runOnGui([&] { engine.prepare(curSr, curBlock); });
engine.prepare(curSr, curBlock);
reportLatencies();
std::cerr << "[JuceFxLoop] re-prepared sr=" << curSr
<< " block=" << curBlock
@@ -509,12 +270,9 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName,
}
ipc->h.state = FXRT_STATE_STARTING;
hb.join();
ctrl.join(); // dừng trước shutdown — không còn lệnh editor mới
guiHost.runOnGui([&] { engine.shutdown(); }); // graph clear trên GUI thread
guiHost.stop(); // M2: đóng editor (GUI thread) + dừng GUI thread + join
engine.shutdown();
fxshm::closeShm(v);
#ifdef _WIN32
if (hMm) AvRevertMmThreadCharacteristics(hMm);
timeEndPeriod(1);
#endif
std::cerr << "[JuceFxLoop] exit processed=" << processed << std::endl;
-73
View File
@@ -1,73 +0,0 @@
// native_bridge/juce_fx/editor_selftest.cpp
// M1 selftest (KHÔNG ship): verify editor host API trên instance DSP thật.
// juce_fx_selftest.exe "<path.vst3>"
// Main thread = JUCE message thread (initialiseJuce_GUI). Plugin không có
// editor (createEditorIfNeeded null) → [FAIL] editor open — thử plugin khác.
#include "JuceFxEngine.h"
#include <juce_audio_processors/juce_audio_processors.h>
#include <juce_gui_basics/juce_gui_basics.h>
#include <cstdio>
#include <string>
static int failures = 0;
static void check(bool ok, const char* what) {
std::printf("%s %s\n", ok ? "[PASS]" : "[FAIL]", what);
if (!ok) ++failures;
}
int main(int argc, char** argv) {
if (argc < 2) {
std::fprintf(stderr, "usage: juce_fx_selftest <path.vst3>\n");
return 2;
}
// Escape backslash + quote cho JSON path.
std::string path(argv[1]), jpath;
jpath.reserve(path.size() + 8);
for (char c : path) {
if (c == '\\' || c == '"') jpath += '\\';
jpath += c;
}
juce::initialiseJuce_GUI(); // main thread = message thread
int rc = 1;
{
JuceFxEngine engine;
const std::string chain =
"[{\"type\":\"vst3\",\"path\":\"" + jpath + "\",\"bypass\":false}]";
engine.setChain(chain);
engine.prepare(48000, 256);
// Negative: slot ngoài chain — pump xử lý nhưng không có editor nào.
engine.requestEditor(9, JuceFxEngine::EditorAction::Open);
check(!engine.pumpEditorQueue(), "negative: slot 9 ngoài chain -> pump false");
// Open editor 400x300.
engine.requestEditor(0, JuceFxEngine::EditorAction::Open, 400, 300);
bool pumped = engine.pumpEditorQueue();
// Pump message queue ngắn: editor constructor có thể tạo timer/callback.
juce::MessageManager::getInstance()->runDispatchLoopUntil(50);
check(pumped, "open: pumpEditorQueue true");
check(engine.isEditorOpen(0), "open: isEditorOpen(0) true");
// Capture state — phải có dữ liệu nếu plugin đã nạp preset mặc định.
engine.requestEditor(0, JuceFxEngine::EditorAction::Capture);
check(engine.pumpEditorQueue(), "capture: pumpEditorQueue true");
std::string cap = engine.takeCapturedState(0);
check(!cap.empty(), "capture: takeCapturedState non-empty");
std::string cap2 = engine.takeCapturedState(0);
check(cap2.empty(), "capture: lần 2 rỗng (đã xoá)");
// Close — editor đóng, processor + state giữ nguyên.
engine.requestEditor(0, JuceFxEngine::EditorAction::Close);
check(engine.pumpEditorQueue(), "close: pumpEditorQueue true");
check(!engine.isEditorOpen(0), "close: isEditorOpen(0) false");
} // ~engine -> shutdown() (đóng editor trước graph) — trước shutdownJuce_GUI
juce::shutdownJuce_GUI();
rc = (failures == 0) ? 0 : 1;
std::printf(failures == 0 ? "SELFTEST OK\n" : "SELFTEST FAILED\n");
return rc;
}
-105
View File
@@ -1,105 +0,0 @@
// native_bridge/juce_fx/gui_host_selftest.cpp
// M2 selftest (KHÔNG ship): GUI thread + host window trên instance DSP thật.
// juce_fx_gui_selftest.exe "<path.vst3>"
// Main thread = engine + GuiHost điều khiển; GUI thread riêng (BELOW_NORMAL)
// chạy MessageManager + tick 30ms (pump + reconcile). Main thread KHÔNG gọi
// JUCE GUI — mọi thao tác qua requestEditor queue (thread-safe) + poll.
// Plugin không có editor (createEditorIfNeeded null) → [FAIL] open — thử khác.
#include "JuceFxEngine.h"
#include "JuceFxGuiHost.h"
#include <chrono>
#include <cstdio>
#include <string>
#include <thread>
static int failures = 0;
static void check(bool ok, const char* what) {
std::printf("%s %s\n", ok ? "[PASS]" : "[FAIL]", what);
if (!ok) ++failures;
}
static void sleepMs(int ms) {
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}
// Poll predicate tối đa timeoutMs (bước 10ms).
template <typename F>
static bool waitFor(F f, int timeoutMs) {
const auto t0 = std::chrono::steady_clock::now();
while (!f()) {
const auto el = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0)
.count();
if (el > timeoutMs) return false;
sleepMs(10);
}
return true;
}
int main(int argc, char** argv) {
if (argc < 2) {
std::fprintf(stderr, "usage: juce_fx_gui_selftest <path.vst3>\n");
return 2;
}
// Escape backslash + quote cho JSON path.
std::string path(argv[1]), jpath;
jpath.reserve(path.size() + 8);
for (char c : path) {
if (c == '\\' || c == '"') jpath += '\\';
jpath += c;
}
JuceFxEngine engine;
const std::string chain =
"[{\"type\":\"vst3\",\"path\":\"" + jpath + "\",\"bypass\":false}]";
engine.setChain(chain);
JuceFxGuiHost host(engine);
host.start();
// M2b: plugin instance phải tạo trên GUI thread — plugin JUCE MM gắn thread
// tạo instance, editor tạo thread khác → treo (M1 chỉ chạy vì main thread
// vừa tạo instance vừa là message thread). Prepare chạy trên GUI thread;
// audio thread chỉ gọi engine.process (JUCE cho phép cross-thread).
host.runOnGui([&] { engine.prepare(48000, 256); });
int rc = 1;
// Open editor 640x480 — GUI thread tạo editor + PluginWindow (host native).
engine.requestEditor(0, JuceFxEngine::EditorAction::Open, 640, 480);
check(waitFor([&] { return engine.isEditorOpen(0) && host.windowCount() == 1; }, 5000),
"open: isEditorOpen(0) && windowCount==1 (editor + host window)");
sleepMs(3000); // chừa thời gian render — chụp màn hình bên ngoài
// Hide: editor + instance GIỮ (mở lại nhanh), window ẩn.
engine.requestEditor(0, JuceFxEngine::EditorAction::Hide);
check(waitFor([&] { return !engine.isEditorVisible(0) && engine.isEditorOpen(0); }, 3000),
"hide: !isEditorVisible(0) nhưng editor vẫn open");
check(host.windowCount() == 1, "hide: windowCount vẫn 1 (giữ editor+instance)");
// Show lại.
engine.requestEditor(0, JuceFxEngine::EditorAction::Show);
check(waitFor([&] { return engine.isEditorVisible(0); }, 3000),
"show: isEditorVisible(0) true lại");
// Resize 700x500 — setSize editor; window resizeToFit co theo.
engine.requestEditor(0, JuceFxEngine::EditorAction::Resize, 700, 500);
sleepMs(1500);
check(host.windowCount() == 1 && engine.isEditorOpen(0),
"resize: editor vẫn mở sau Resize 700x500");
sleepMs(2000); // chừa render sau resize — chụp màn hình bên ngoài
// Close: window bị reconcile xoá → windowCount 0.
engine.requestEditor(0, JuceFxEngine::EditorAction::Close);
check(waitFor([&] { return !engine.isEditorOpen(0) && host.windowCount() == 0; }, 3000),
"close: !isEditorOpen(0) && windowCount==0 (window xoá)");
host.runOnGui([&] { engine.shutdown(); }); // graph clear trên GUI thread
host.stop(); // đóng editor + dừng GUI thread + join
rc = (failures == 0) ? 0 : 1;
std::printf(failures == 0 ? "GUI SELFTEST OK\n" : "GUI SELFTEST FAILED\n");
return rc;
}
+84 -277
View File
@@ -1,24 +1,16 @@
// native_bridge/src/FxGuiServer.cpp
// VST GUI server (Phase 0 of PLAN_MASTERBUS_VST_GUI_EMBED.md; P2 float mode of
// the gop-process plan): `daw_vst_bridge --fx-gui <job.json>` loads ONE VST3
// effect plugin and attaches its native editor to a host window.
// - Embedded mode (default): window off-screen; JPEG frames + input relay
// over HTTP so the webview can show it in a panel (VNC-style).
// - Float mode (SF_FXGUI_FLOAT=1): the window is a REAL on-screen window the
// user moves/sizes freely; /frame and /input are not used. WM_CLOSE only
// hides it (plugin instance stays loaded — reopen via /show is instant);
// /close and /exit tear the process down for real.
// Windows only.
// Embedded VST GUI server (Phase 0 of PLAN_MASTERBUS_VST_GUI_EMBED.md):
// `daw_vst_bridge --fx-gui <job.json>` loads ONE VST3 effect plugin, attaches
// its native editor to an off-screen host window, and serves the window as
// JPEG frames over a tiny HTTP server so the web frontend can embed it in a
// panel (frame-capture + input relay, VNC-style). Windows only.
//
// Routes:
// GET /ping -> {"ok":true}
// GET /frame -> image/jpeg (BitBlt, fallback PrintWindow) — embed
// GET /frame -> image/jpeg (BitBlt, fallback PrintWindow)
// POST /input -> {type:"mousedown|mousemove|mouseup|wheel|keydown|keyup",
// x,y,button,deltaY,keyCode} — embed
// POST /show -> show (+focus, float) / park at panel rect (embed)
// POST /hide -> hide the window (bridge keeps running)
// x,y,button,deltaY,keyCode} -> PostMessage to HWND
// POST /close -> detach + exit 0
// POST /exit -> alias of /close
// GET /preset -> {"preset_b64":"..."} current plugin state
//
// The HTTP server runs on a worker thread; the calling (STA) thread runs the
@@ -36,7 +28,6 @@
#include <atomic>
#include <cstring>
#include <cstdlib>
#include <cstdint>
#include <functional>
#include <iostream>
@@ -58,24 +49,12 @@ extern void fxGuiRequestCapture(void* hwnd, std::string& outB64, int timeoutMs);
// Tell RenderFxJob's SHM audio feeder which realtime session is live (the
// frontend pushes the session name; "" = session stopped/restarted).
extern void fxGuiSetShm(const std::string& name);
// Set by RenderFxJob once the VST editor attached to the host window; /frame
// refuses (503) until then so the webview never shows the empty host window.
extern std::atomic<bool> g_fxGuiEditorReady;
// Set by float /show when the editor is still loading/attaching; consumed by
// fxGuiLoadAttach after attach so a pre-ready /show is not lost (Bug 2).
extern std::atomic<bool> g_fxGuiShowPending;
// Float mode flag (RenderFxJob.cpp global, set from SF_FXGUI_FLOAT).
extern bool g_fxGuiFloatMode;
// Ask the pump thread to destroy the window (full exit in float mode).
extern void fxGuiRequestExit(HWND hwnd);
// True once the VST editor view is attached (RenderFxJob.cpp) — /show must
// not run its first-paint kick before the editor exists.
extern bool fxGuiEditorAttached();
namespace {
// Last good captured JPEG: /frame falls back to it while the editor window is
// blank/repainting (avoids flashing the panel white/black). Only touched by
// the single HTTP worker thread — no lock needed.
std::vector<unsigned char> g_lastGood;
// --- tiny JSON helpers (same API as RenderFxJob.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)
@@ -187,7 +166,7 @@ void sendOptions(SOCKET c) {
// frame, while the GDI BitBlt view is a stale first paint for GPU-rendered
// editors (JUCE/FAST Compressor) and never updates. BitBlt stays as fallback.
// Returns JPEG.
// Mean of the raw BGRA frame (0..255) — black-frame guard below.
// Mean luma (0..255) of the raw BGRA frame — black-frame guard below.
static double frameMean(const unsigned char* px, int w, int h) {
const size_t n = (size_t)w * h;
unsigned long long s = 0;
@@ -195,9 +174,7 @@ static double frameMean(const unsigned char* px, int w, int h) {
s += px[i * 4 + 2] + px[i * 4 + 1] + px[i * 4 + 0];
return (double)s / (3.0 * (double)n);
}
bool captureHwndToJpeg(HWND hwnd, std::vector<unsigned char>& out, int quality,
float* outAvg = nullptr, bool* outOkB = nullptr,
bool* outOkP = nullptr) {
bool captureHwndToJpeg(HWND hwnd, std::vector<unsigned char>& out, int quality) {
RECT rc;
if (!GetClientRect(hwnd, &rc)) return false;
const int w = rc.right - rc.left;
@@ -229,42 +206,32 @@ bool captureHwndToJpeg(HWND hwnd, std::vector<unsigned char>& out, int quality,
// window (child + GPU surfaces). The GDI BitBlt view is a stale first paint
// for GPU-rendered editors (JUCE/FAST Compressor) and never updates, which
// made the panel look frozen. BitBlt stays only as a fallback.
// Black-frame guard: PrintWindow returns TRUE with an ALL-BLACK frame for
// editors that have not painted yet (iZotope-class, e.g. Nectar — the
// /show first-paint kick fixes the window, but until then BitBlt is the
// only source of real pixels). When PrintWindow is pure black and the GDI
// view has content, send the GDI view instead of a black panel.
unsigned char* px = nullptr;
if (okP && okB) {
// Black-frame guard: PrintWindow can return TRUE with an ALL-BLACK
// frame while the editor has not painted yet (iZotope-class, e.g.
// Nectar — those editors paint only after their window was activated;
// the /show first-paint kick fixes that, but until it fires BitBlt is
// the only source of real pixels). If PrintWindow is pure black and
// the GDI view has content, send the GDI view instead of a black panel.
const double mP = frameMean(pxP.data(), w, h);
const double mB = frameMean(pxB.data(), w, h);
if (mP < 6.0 && mB > 6.0) px = pxB.data();
else px = pxP.data();
} else if (okP) px = pxP.data();
else if (okB) px = pxB.data();
if (outOkB) *outOkB = okB;
if (outOkP) *outOkP = okP;
SelectObject(mem, oldBmp);
DeleteObject(bmp);
DeleteDC(mem);
ReleaseDC(hwnd, win);
if (!px) return false;
// BGRA (GDI) -> RGB, then JPEG. Accumulate mean luma (0..1) so the
// caller can reject blank frames (host window not painted yet).
// BGRA (GDI) -> RGB, then JPEG.
std::vector<unsigned char> rgb((size_t)w * h * 3);
double lumaSum = 0;
for (int i = 0; i < w * h; ++i) {
const unsigned char b = px[(size_t)i * 4 + 0];
const unsigned char g = px[(size_t)i * 4 + 1];
const unsigned char r = px[(size_t)i * 4 + 2];
rgb[(size_t)i * 3 + 0] = r;
rgb[(size_t)i * 3 + 1] = g;
rgb[(size_t)i * 3 + 2] = b;
lumaSum += (double)r + g + b;
rgb[(size_t)i * 3 + 0] = px[(size_t)i * 4 + 2];
rgb[(size_t)i * 3 + 1] = px[(size_t)i * 4 + 1];
rgb[(size_t)i * 3 + 2] = px[(size_t)i * 4 + 0];
}
if (outAvg) *outAvg = (float)(lumaSum / ((double)w * h * 3 * 255.0));
jpeg_compress_struct cinfo = {};
jpeg_error_mgr jerr;
@@ -305,34 +272,17 @@ std::atomic<bool> g_realInput{true}; // always real hardware input
std::atomic<bool> g_dance{false}; // window temporarily on-screen/topmost
std::atomic<DWORD> g_parkUntil{0}; // safety restore deadline (GetTickCount)
POINT g_savedCursor = {0, 0}; // cursor to restore after the dance
// iZotope-class editors only paint their surface once the window has been
// activated (SetForegroundWindow) at least once; parked off-screen they stay
// black forever. /show performs a one-time kick (on-screen + foreground +
// invalidate), then demotes the window under the DAW. Reset on /hide.
std::atomic<bool> g_paintKicked{false};
// Anchored mode: the frontend sends the embed panel's screen rect (physical
// px) with every /input, so the real-input dance places the plugin window
// exactly over the panel -- cursor never moves, no park/restore churn. Zero
// rect = legacy cursor-relative mode.
RECT g_embedRect = {0, 0, 0, 0};
bool g_anchored = false; // window anchored over panel (stays until /close)
// iZotope-class editors (Nectar/Ozone) paint their surface ONLY after the
// window was actually visible AND activated at least once; parked HWND_BOTTOM
// under the DAW they never paint -> black forever. /show performs a one-time
// kick (raise above the DAW + invalidate + real click), then demotes the
// window back under the DAW — the painted surface survives the demote.
// Reset on /hide so each reopen re-kicks.
std::atomic<bool> g_paintKicked{false};
// On-screen embed parking (DEFAULT since V45.45): park the host window ON a
// monitor at the embed panel rect (HWND_BOTTOM, under the DAW -> user never
// sees it) instead of off-screen at -20000. DWM does not redirect windows
// parked fully off the desktop on some drivers (Intel UHD 630 + iZotope GL
// editor) -> capture sees only black. On a monitor (even occluded by the DAW
// above) DWM keeps a redirection surface -> /frame gets real pixels. Real-input
// dance still lifts the window to HWND_TOP for actual clicks (g_anchored
// guard). SF_FXGUI_EMBED_ONSCREEN=0 disables (env kept for debugging).
static const bool g_embedOnscreen = [] {
const char* v = std::getenv("SF_FXGUI_EMBED_ONSCREEN");
return v == nullptr || v[0] != '0';
}();
// SF_FXGUI_DEBUG=1: rate-limited /frame capture ground truth ([FxGuiDbg] lines).
static const bool g_fxGuiDbg = std::getenv("SF_FXGUI_DEBUG") != nullptr;
// Lift the window above the foreground app without making it TOPMOST (the
@@ -418,9 +368,6 @@ void realClick(int sx, int sy, bool down, int button) {
// --- input relay -------------------------------------------------------------
void relayInput(HWND hwnd, const json_object_s* o) {
// Float mode: the user interacts with the real window directly — the
// relay/dance machinery is embed-only.
if (g_fxGuiFloatMode) return;
// The plugin editor is a CHILD window created by IPlugView::attached().
// DefWindowProc on the top-level host does NOT forward mouse/keyboard to
// it, so post directly to the view (fallback to the host window).
@@ -520,42 +467,6 @@ void relayInput(HWND hwnd, const json_object_s* o) {
}
}
// One-time first-paint kick (shared by /show and /frame). iZotope-class
// editors (Nectar/Ozone) paint ONLY when their window is actually visible
// AND activated: parked HWND_BOTTOM under the DAW they never paint -> black
// forever, and SetForegroundWindow from this background process is blocked
// by the Windows foreground lock. So: raise the window ABOVE the DAW at the
// panel rect, paint it there, then demote back under the DAW — the painted
// surface survives the demote (capture stays non-black at HWND_BOTTOM).
static void kickFirstPaint(HWND hwnd, const char* via) {
if (g_paintKicked.load()) return;
g_paintKicked = true;
const int cw = g_embedRect.right - g_embedRect.left;
const int ch = g_embedRect.bottom - g_embedRect.top;
SetWindowPos(hwnd, HWND_TOP, g_embedRect.left, g_embedRect.top,
cw, ch, SWP_SHOWWINDOW | SWP_NOACTIVATE);
ShowWindow(hwnd, SW_SHOW);
InvalidateRect(hwnd, nullptr, TRUE);
RedrawWindow(hwnd, nullptr, nullptr,
RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN);
// Foreground-lock bypass: a real synthesized click (system input, not
// subject to the SetForegroundWindow lock) activates the window the same
// way a user click would.
Sleep(200);
RECT wr = {};
GetWindowRect(hwnd, &wr);
const int cx = wr.left + (wr.right - wr.left) / 2;
const int cy = wr.top + 8; // title-bar strip, avoid knobs
SetCursorPos(cx, cy);
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
Sleep(500); // give the editor a beat to paint while on top
SetWindowPos(hwnd, HWND_BOTTOM, g_embedRect.left, g_embedRect.top,
cw, ch, SWP_NOACTIVATE);
std::cerr << "[FxGui] first-paint kick fg=" << via << " @" << cx
<< "," << cy << std::endl;
}
// --- HTTP server loop (worker thread) ---------------------------------------
void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
WSADATA wsa;
@@ -600,82 +511,11 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
if (g_dance && g_parkUntil &&
(int32_t)(GetTickCount() - g_parkUntil) >= 0)
danceEnd(hwnd);
if (!g_fxGuiEditorReady.load()) {
// VST still loading / editor attaching (pump thread): refuse
// rather than stream the empty host window — the webview used
// to show that blank frame as a white flash on open.
sendResponse(c, 503, "application/json", "{\"error\":\"loading\"}", 19);
} else {
// On-screen embed parking: keep the host window ON a monitor
// at the embed panel rect (HWND_BOTTOM, under the DAW) so DWM
// redirects the editor's GPU surface and capture sees real
// pixels — the one-shot rect /show may have run pre-attach
// (deduped), so reconcile here on every poll until it fits.
if (!g_fxGuiFloatMode && g_embedOnscreen && !g_anchored &&
!g_dance && g_embedRect.right > g_embedRect.left &&
g_embedRect.bottom > g_embedRect.top) {
const int cw = g_embedRect.right - g_embedRect.left;
const int ch = g_embedRect.bottom - g_embedRect.top;
RECT cur = {};
GetWindowRect(hwnd, &cur);
if (cur.left != g_embedRect.left || cur.top != g_embedRect.top ||
cur.right - cur.left != cw || cur.bottom - cur.top != ch)
SetWindowPos(hwnd, HWND_BOTTOM, g_embedRect.left,
g_embedRect.top, cw, ch,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
// First-paint kick via /frame poll: the one-shot /show
// may have landed while the editor was still loading
// (ready=false) and is deduped, so no later /show fires;
// iZotope-class editors paint only when visible+activated
// -> black panel until a user click. Kick here once the
// editor is ready and the window is parked at the rect.
kickFirstPaint(hwnd, "frame");
}
std::vector<unsigned char> jpg;
float avg = -1.0f;
bool capOkB = false, capOkP = false;
const bool capOk = captureHwndToJpeg(hwnd, jpg, 70, &avg, &capOkB, &capOkP);
const char* served = nullptr;
if (capOk && avg >= 0.02f && avg <= 0.98f) {
g_lastGood = jpg; // single HTTP worker thread — no lock
served = "frame";
sendResponse(c, 200, "image/jpeg", jpg.data(), jpg.size());
} else if (!g_lastGood.empty()) {
// Blank/transient frame (window still repainting) — keep
// the last good frame so the panel never flashes blank.
served = "cached";
sendResponse(c, 200, "image/jpeg", g_lastGood.data(), g_lastGood.size());
} else {
served = "503";
sendResponse(c, 503, "application/json", "{\"error\":\"loading\"}", 19);
}
// SF_FXGUI_DEBUG=1: rate-limited capture ground truth.
if (g_fxGuiDbg) {
static DWORD64 s_lastDbg = 0; // HTTP worker thread — no lock
const DWORD64 nowMs = GetTickCount64();
if (nowMs - s_lastDbg >= 2000) {
s_lastDbg = nowMs;
RECT cr = {}, wr = {};
GetClientRect(hwnd, &cr);
GetWindowRect(hwnd, &wr);
std::cout << "[FxGuiDbg] served=" << served
<< " capOk=" << (capOk ? 1 : 0)
<< " okB=" << (capOkB ? 1 : 0)
<< " okP=" << (capOkP ? 1 : 0)
<< " avg=" << avg
<< " vis=" << (IsWindowVisible(hwnd) ? 1 : 0)
<< " client=" << (cr.right - cr.left) << "x"
<< (cr.bottom - cr.top)
<< " win=(" << wr.left << "," << wr.top << " "
<< (wr.right - wr.left) << "x"
<< (wr.bottom - wr.top) << ")"
<< " lastGood=" << g_lastGood.size()
<< " anchored=" << (g_anchored ? 1 : 0)
<< " dance=" << (g_dance ? 1 : 0)
<< std::endl;
}
}
}
std::vector<unsigned char> jpg;
if (captureHwndToJpeg(hwnd, jpg, 70))
sendResponse(c, 200, "image/jpeg", jpg.data(), jpg.size());
else
sendResponse(c, 500, "application/json", "{\"error\":\"capture failed\"}", 25);
} else if (req.path == "/input") {
json_parse_result_s pres = {};
json_value_s* root = json_parse_ex(req.body.data(), req.body.size(),
@@ -694,62 +534,9 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
ShowWindow(hwnd, SW_HIDE);
g_fxGuiShowPending.store(false); // panel closed while attaching: don't auto-show later
// Reopen re-anchors from scratch (/show + rect) — clear the
// real-input state or the reopened panel would never re-place
// the window at the panel rect (stuck anchored/offscreen).
g_anchored = false;
g_dance = false;
g_parkUntil = 0;
g_paintKicked = false; // next /show re-kicks the first paint
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
} else if (req.path == "/show") {
if (g_fxGuiFloatMode) {
// P2 float mode: reopen the hidden window (WM_CLOSE only hides
// in float mode — the plugin instance stays loaded, so a show
// is instant). Optional {rect:{x,y,w,h}} positions/sizes the
// window explicitly; without one, show at the previous spot and
// try to focus (SetForegroundWindow is best-effort — the
// Windows foreground lock may swallow it; the user's first
// click reactivates the window anyway). Guarded by
// g_fxGuiEditorReady: while the pump thread is still loading +
// attaching, a SetWindowPos from this worker would block.
if (g_fxGuiEditorReady.load()) {
json_parse_result_s spres = {};
json_value_s* sroot = json_parse_ex(req.body.data(), req.body.size(),
json_parse_flags_default, nullptr, nullptr, &spres);
bool rectOk = false;
if (sroot && sroot->type == json_type_object) {
const json_object_s* so = static_cast<const json_object_s*>(sroot->payload);
const json_value_s* srv = memberValue(so, "rect");
if (srv && srv->type == json_type_object) {
const json_object_s* sro = static_cast<const json_object_s*>(srv->payload);
const LONG srw = (LONG)memberInt(sro, "w", 0);
const LONG srh = (LONG)memberInt(sro, "h", 0);
if (srw > 0 && srh > 0) {
SetWindowPos(hwnd, nullptr,
(int)memberInt(sro, "x", 0),
(int)memberInt(sro, "y", 0),
(int)srw, (int)srh,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW);
rectOk = true;
}
}
}
if (sroot) std::free(sroot);
if (!rectOk) {
ShowWindow(hwnd, SW_SHOW);
bringToTop(hwnd);
}
} else {
// Editor still loading/attaching (slow VST): cannot show
// yet. Remember the request — fxGuiLoadAttach shows the
// window right after attach, else the GUI stays hidden
// forever on a reopen during attach (Bug 2).
g_fxGuiShowPending.store(true);
}
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
} else {
// Optional body {rect:{x,y,w,h}} (screen px) — anchored placement
// over the embed panel; without a rect just show + top (never
// topmost — the GUI must not stay above other apps).
@@ -774,21 +561,16 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
}
}
if (sroot) std::free(sroot);
// Embedded mode: park the host window ON a monitor at the embed
// panel rect (HWND_BOTTOM, under the DAW -> user never sees it)
// instead of off-screen at -20000. iZotope-class editors never
// paint while fully off-screen, and DWM does not redirect windows
// parked off the desktop on some drivers (Intel UHD 630) — on a
// monitor (even occluded by the DAW above) capture sees real
// pixels. The panel rect also sizes the host window + plugin view
// so the capture frame matches the embed panel (layout bug);
// /hide hid the window, so re-show it here.
// Skip while the editor is still attaching (pump thread busy;
// SetWindowPos from this worker would block until load/attach
// finishes) and while anchored/dancing (real-input owns the
// window position during clicks).
if (g_embedOnscreen && rectOk && g_fxGuiEditorReady.load() &&
!g_anchored && !g_dance) {
// Embedded mode: the native window stays parked ON-SCREEN under
// the DAW (HWND_BOTTOM at the embed panel rect — invisible, the
// DAW window covers it). iZotope-class editors (Nectar) NEVER
// paint while off-screen: both PrintWindow and BitBlt stay pure
// black, so the panel showed "can't load GUI". On-screen the
// editor paints once (first-paint kick below) and the capture
// surface keeps content. The panel rect sizes the host window +
// plugin view so the capture frame matches the embed panel
// (layout bug); /hide hid the window, so re-show it.
if (rectOk && !g_anchored) {
const int cw = g_embedRect.right - g_embedRect.left;
const int ch = g_embedRect.bottom - g_embedRect.top;
RECT cur = {};
@@ -796,28 +578,56 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
const bool samePlace =
cur.left == g_embedRect.left && cur.top == g_embedRect.top &&
cur.right - cur.left == cw && cur.bottom - cur.top == ch;
// Skip when already parked right: frontend re-POSTs /show
// every ~500ms; re-laying-out the plugin view every tick is
// wasteful (WM_FXGUI_RESIZE -> checkSizeConstraint/onSize).
// Skip when already parked right: frontend re-POSTs /show every
// ~500ms; re-laying-out the plugin view every tick is wasteful
// (WM_FXGUI_RESIZE -> checkSizeConstraint/onSize).
if (!samePlace || !IsWindowVisible(hwnd))
SetWindowPos(hwnd, HWND_BOTTOM, g_embedRect.left,
g_embedRect.top, cw, ch,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
SetWindowPos(hwnd, HWND_BOTTOM, g_embedRect.left, g_embedRect.top,
cw, ch, SWP_NOACTIVATE | SWP_SHOWWINDOW);
if (cur.right - cur.left != cw || cur.bottom - cur.top != ch)
fxGuiRequestResize(hwnd, cw, ch);
// First-paint kick — one-time, runs from the shared helper;
// /frame also kicks (the deduped /show may land pre-ready).
kickFirstPaint(hwnd, "click");
} else if (!IsWindowVisible(hwnd)) {
// No usable rect yet (pre-layout / still attaching): keep the
// window somewhere harmless until the frontend supplies it.
if (!g_paintKicked && fxGuiEditorAttached()) {
// First-paint kick (one-time per /show cycle). iZotope-class
// editors (Nectar) paint ONLY when their window is actually
// visible (not covered) AND activated: parked HWND_BOTTOM
// under the DAW the editor never paints -> black forever,
// and SetForegroundWindow from this background process is
// blocked by the Windows foreground lock (returns FALSE).
// So: raise the window ABOVE the DAW (invisible blink — the
// DAW covers the panel rect anyway), paint it there, then
// demote back under the DAW; the painted surface survives
// the demote (verified: capture stays non-black at HWND_BOTTOM).
g_paintKicked = true;
SetWindowPos(hwnd, HWND_TOP, g_embedRect.left, g_embedRect.top,
cw, ch, SWP_SHOWWINDOW | SWP_NOACTIVATE);
ShowWindow(hwnd, SW_SHOW);
InvalidateRect(hwnd, nullptr, TRUE);
RedrawWindow(hwnd, nullptr, nullptr,
RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN);
// Foreground-lock bypass: a real synthesized click (system
// input, not subject to the SetForegroundWindow lock)
// activates the window the same way a user click would.
// iZotope-class editors paint only after such an activation.
Sleep(200);
RECT wr = {};
GetWindowRect(hwnd, &wr);
const int cx = wr.left + (wr.right - wr.left) / 2;
const int cy = wr.top + 8; // title-bar strip, avoid knobs
SetCursorPos(cx, cy);
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
Sleep(500); // give the editor a beat to paint while on top
SetWindowPos(hwnd, HWND_BOTTOM, g_embedRect.left, g_embedRect.top,
cw, ch, SWP_NOACTIVATE);
std::cerr << "[FxGui] first-paint kick fg=click @" << cx << "," << cy << std::endl;
}
} else if (!rectOk && !IsWindowVisible(hwnd)) {
SetWindowPos(hwnd, HWND_NOTOPMOST, -20000, -20000, 0, 0,
SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
}
// Dance/anchor state is owned by the input relay — /show must not
// yank the window off-screen (real input is always on).
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
} // end embed-mode /show body (else of g_fxGuiFloatMode)
} else if (req.path == "/preset") {
std::string b64;
fxGuiRequestCapture(hwnd, b64, 500); // 1500 blocked the HTTP worker, stalling /input (clicks)
@@ -836,13 +646,10 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
if (sroot) std::free(sroot);
sendResponse(c, 400, "application/json", "{\"error\":\"bad json\"}", 20);
}
} else if (req.path == "/close" || req.path == "/exit") {
} else if (req.path == "/close") {
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
stop.store(true);
// Float mode: WM_CLOSE only hides — WM_FXGUI_EXIT destroys the
// window so the pump exits and the process returns. Embedded mode
// is unchanged (there WM_CLOSE already destroyed).
fxGuiRequestExit(hwnd); // pump exits -> process returns
PostMessageA(hwnd, WM_CLOSE, 0, 0); // pump exits -> process returns
} else {
sendResponse(c, 404, "application/json", "{\"error\":\"not found\"}", 22);
}
+10 -18
View File
@@ -69,27 +69,16 @@ bool FluidSynthInstrument::init(double sampleRate, uint32_t maxBlockSize) {
return synth != nullptr;
}
// SF2 fallback: nhieu soundfont khong co bank 128 (GM drum) hoac thieu preset
// GM (vd Cajon.sf2 chi co it preset, khong co violin 40 / contrabass 44) —
// neu select fail thi lui dan (bank,program) -> (0,program) -> (0,0) de khong
// cam (fluidsynth giu preset cu -> sai am) hay silent (log error preset 40/44).
static void selectPresetFallback(fluid_synth_t* synth, int sfontId, uint32_t channel,
uint32_t& bank, uint32_t& program) {
if (fluid_synth_program_select(synth, channel, sfontId, bank, program) == FLUID_OK) return;
if (bank != 0 && fluid_synth_program_select(synth, channel, sfontId, 0, program) == FLUID_OK) {
bank = 0;
return;
}
bank = 0;
program = 0;
fluid_synth_program_select(synth, channel, sfontId, 0, 0);
}
void FluidSynthInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) {
if (!synth || channel >= 16) return;
bank_[channel] = bank;
program_[channel] = program;
selectPresetFallback(FS_SYNTH, sfontId, channel, bank_[channel], program_[channel]);
// SF2 fallback: nhieu soundfont khong co bank 128 (GM drum) — neu select
// fail, thu lai bank 0 de khong cam (fluidsynth giu preset cu -> sai am).
if (fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank, program) != FLUID_OK && bank != 0) {
bank_[channel] = 0;
fluid_synth_program_select(FS_SYNTH, channel, sfontId, 0, program);
}
}
void FluidSynthInstrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
@@ -116,7 +105,10 @@ void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) {
// Dung bank da nhan tu CC0/CC32 — bank hardcode 0 lam preset o bank != 0
// khong duoc chon (fluid giu preset cu -> ra piano sai).
program_[channel] = program;
selectPresetFallback(FS_SYNTH, sfontId, channel, bank_[channel], program_[channel]);
if (fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank_[channel], program) != FLUID_OK && bank_[channel] != 0) {
bank_[channel] = 0;
fluid_synth_program_select(FS_SYNTH, channel, sfontId, 0, program);
}
}
void FluidSynthInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
+169 -415
View File
@@ -10,6 +10,7 @@
// MIDI/GUI — component + controller, audio buses, processData with input
// buffers copied directly (setChannelBuffers is unusable: prepare() owns them).
#include "RenderFxJob.h"
#include "VstDllSearch.h"
// sheredom/json.h (public domain) — vendored copy; SDK copy as fallback.
#if __has_include("sheredom_json.h")
@@ -25,7 +26,6 @@
#include "public.sdk/source/vst/vstpresetfile.h"
#include "public.sdk/source/common/memorystream.h"
#include "pluginterfaces/vst/ivstaudioprocessor.h"
#include "pluginterfaces/vst/vstspeaker.h"
#include "pluginterfaces/vst/ivstcomponent.h"
#include "pluginterfaces/vst/ivsteditcontroller.h"
#include "pluginterfaces/vst/ivstprocesscontext.h"
@@ -63,19 +63,6 @@
#include <thread>
#include <vector>
// Editor-ready flag (fx-gui server mode). Set true once the VST editor
// attached to the host window (fxGuiLoadAttach, pump thread). FxGuiServer.cpp
// refuses to serve /frame while false: before attach the host window is empty
// and capturing it would stream a blank frame -> white flash in the webview.
std::atomic<bool> g_fxGuiEditorReady{false};
// /show that arrived while the editor was still loading/attaching (slow VST,
// e.g. iZotope 20-70s cold attach) must not be silently dropped: FxGuiServer's
// float /show sets this when it cannot show yet; fxGuiLoadAttach consumes it
// right after a successful attach and shows the window. Without it, closing
// the Mastering panel while the GUI is attaching and reopening it later leaves
// the GUI permanently hidden (Bug 2).
std::atomic<bool> g_fxGuiShowPending{false};
namespace {
// --- sheredom/json.h helpers (same as RenderJob.cpp) ------------------------
@@ -279,7 +266,23 @@ static std::string base64Encode(const uint8_t* data, size_t len) {
return out;
}
// Append-only variant (no return temporary) so it can run inside an SEH
// __try frame (C2712: __try functions cannot have destructor temporaries).
static void base64EncodeTo(const uint8_t* data, size_t len, std::string& out) {
static const char* tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t cap = ((len + 2) / 3) * 4;
out.clear();
out.reserve(cap);
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 += tbl[(n >> 18) & 63];
out += tbl[(n >> 12) & 63];
out += (i + 1 < len) ? tbl[(n >> 6) & 63] : '=';
out += (i + 2 < len) ? tbl[n & 63] : '=';
}
}
static int b64val(unsigned char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
@@ -340,18 +343,7 @@ using Steinberg::Vst::kSample32;
class HostComponentHandlerFx : public IComponentHandler {
public:
tresult queryInterface(const char* iid, void** v) override {
// FUnknown contract: answer for our own iid (+FUnknown::iid) with
// addRef + this; kNoInterface otherwise (SDK QUERY_INTERFACE pattern).
if (Steinberg::FUnknownPrivate::iidEqual(iid, Steinberg::FUnknown::iid) ||
Steinberg::FUnknownPrivate::iidEqual(iid, IComponentHandler::iid)) {
addRef();
*v = this;
return kResultOk;
}
*v = nullptr;
return kNoInterface;
}
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; }
@@ -373,15 +365,7 @@ using Steinberg::kPlatformTypeHWND;
// Minimal IPlugFrame so the plugin can resize its editor view.
class HostPlugFrameFx : public Steinberg::IPlugFrame {
public:
tresult queryInterface(const char* iid, void** v) override {
// FUnknown contract: answer for our own iid (+FUnknown::iid) with
// addRef + this; kNoInterface otherwise (SDK QUERY_INTERFACE pattern).
if (Steinberg::FUnknownPrivate::iidEqual(iid, Steinberg::FUnknown::iid) ||
Steinberg::FUnknownPrivate::iidEqual(iid, Steinberg::IPlugFrame::iid)) {
addRef();
*v = this;
return kResultOk;
}
tresult queryInterface(const char*, void** v) override {
*v = nullptr;
return kNoInterface;
}
@@ -460,6 +444,10 @@ static void setPluginSearchPath(const std::string& path) {
// at a time (render slots serial; realtime chain loads on its own thread).
std::wstring wdir(dir.begin(), dir.end());
SetDllDirectoryW(wdir.c_str());
// iZotope core DLLs sống ở C:\Program Files\iZotope\<Product>\Cores (KHÔNG
// cạnh .vst3) — SetDllDirectoryW chỉ phủ dir cạnh plugin, core vẫn 0x7E.
// Append Cores dirs vào PATH (LoadLibraryW search PATH sau app/system dirs).
vstAddDllSearchPath(vstDllSearchExtras());
std::cerr << "[RenderFx] dll search dir: " << dir << std::endl;
}
#endif
@@ -519,38 +507,8 @@ bool vst3FxLoadInner(Vst3FxState* s, const std::string& path, double sampleRate,
std::cerr << "[RenderFx] STEP connection points OK" << std::endl;
int32 numAudioInputs = component->getBusCount(kAudio, kInput);
int32 numAudioOutputs = component->getBusCount(kAudio, kOutput);
// Reaper-style negotiation BEFORE activation: offer mono/stereo for the
// <=2ch audio busses so plugins (Ozone 11 family) never see an empty or
// unset arrangement on their first GUI attach. Wider busses keep their
// current arrangement. Failure to negotiate is not fatal (log only).
{
FUnknownPtr<IAudioProcessor> proc(component);
if (proc) {
std::vector<Steinberg::Vst::SpeakerArrangement> inArr, outArr;
for (int32 i = 0; i < numAudioInputs; ++i) {
Steinberg::Vst::SpeakerArrangement arr = 0;
proc->getBusArrangement(kInput, i, arr);
int32 ch = Steinberg::Vst::SpeakerArr::getChannelCount(arr);
inArr.push_back(ch == 1 ? Steinberg::Vst::SpeakerArr::kMono
: (ch == 2 ? Steinberg::Vst::SpeakerArr::kStereo : arr));
}
for (int32 i = 0; i < numAudioOutputs; ++i) {
Steinberg::Vst::SpeakerArrangement arr = 0;
proc->getBusArrangement(kOutput, i, arr);
int32 ch = Steinberg::Vst::SpeakerArr::getChannelCount(arr);
outArr.push_back(ch == 1 ? Steinberg::Vst::SpeakerArr::kMono
: (ch == 2 ? Steinberg::Vst::SpeakerArr::kStereo : arr));
}
tresult res = proc->setBusArrangements(inArr.data(), (int32)inArr.size(),
outArr.data(), (int32)outArr.size());
std::cerr << "[RenderFx] STEP bus arrangements "
<< (res == kResultOk || res == kResultTrue ? "OK" : "skip")
<< " (ain=" << numAudioInputs << " aout=" << numAudioOutputs << ")"
<< std::endl;
}
}
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);
@@ -621,115 +579,19 @@ bool vst3FxLoadInner(Vst3FxState* s, const std::string& path, double sampleRate,
}
#endif // HAVE_VST3SDK
// Canonical preset wrapper (PLAN A) — JUCE XML, đúng bytes
// AudioProcessor::copyXmlToBinary(<VST3PluginState>): "VC2!" + LE u32 xmlLen
// + single-line XML text + NUL. XML = <VST3PluginState><IComponent>len-dot
// </IComponent><IEditController>len-dot</IEditController></VST3PluginState>
// (controller element optional). Cả JuceFxEngine::getStateInformation lẫn
// Vst3Fx::captureState xuất wrapper này → 1 format dùng chung mọi đường
// (GUI fallback, offline --render-fx, --realtime-fx, fxrt juce). Parse ngược
// (setState comp/ctrl) trong applyPreset.
//
// Element text = MemoryBlock::toBase64Encoding (JUCE) — KHÔNG phải RFC 4648
// base64! Format: "<số byte>." rồi mỗi ký tự mã 6 bit của state qua bảng
// ".ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+" ('.' =
// 6 bit 0; không '/', không '=' padding), đọc LSB-first từng byte như
// getBitRange/setBitRange. Decode ngược fromBase64Encoding: lấy size trước
// '.' đầu tiên, mỗi char c-43 (0..79) → bảng decode rồi ghi 6 bit; char
// ngoài 0..79 bỏ qua (không tốn bit). (juce_MemoryBlock.cpp.)
static const char kJuceB64EncTbl[] =
".ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+";
static const int kJuceB64DecTbl[80] = {
63, 0, 0, 0, 0, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 0, 0, 0, 0, 0, 0, 0,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24, 25, 26,
0, 0, 0, 0, 0, 0, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52
};
// JUCE bit access — LSB-first per byte, giống getBitRange/setBitRange.
static int juceGetBits(const uint8_t* data, size_t size, size_t bitStart, size_t numBits) {
int res = 0;
for (size_t k = 0; k < numBits; ++k) {
size_t bp = bitStart + k;
if ((bp >> 3) >= size) break;
if (data[bp >> 3] & (uint8_t)(1u << (bp & 7))) res |= (int)(1u << k);
}
return res;
}
static void juceSetBits(uint8_t* data, size_t size, size_t bitStart, size_t numBits, int bitsToSet) {
for (size_t k = 0; k < numBits; ++k) {
size_t bp = bitStart + k;
if ((bp >> 3) >= size) break;
if (bitsToSet & (int)(1u << k))
data[bp >> 3] |= (uint8_t)(1u << (bp & 7));
else
data[bp >> 3] &= (uint8_t)~(1u << (bp & 7));
}
}
// Encode state bytes như MemoryBlock::toBase64Encoding (len-dot, LSB-first).
static void juceStateEncode(const std::vector<uint8_t>& data, std::string& out) {
out += std::to_string(data.size());
out += '.';
size_t numChars = ((data.size() << 3) + 5) / 6; // ceil(size*8/6)
out.reserve(out.size() + numChars);
for (size_t i = 0; i < numChars; ++i)
out += kJuceB64EncTbl[juceGetBits(data.data(), data.size(), i * 6, 6)];
}
// Decode text len-dot JUCE; false nếu không phải (vd RFC 4648) — caller
// fallback. Giống fromBase64Encoding: size trước '.', char ngoài bảng skip.
static bool juceStateDecode(const std::string& text, std::vector<uint8_t>& out) {
size_t dot = text.find('.');
if (dot == std::string::npos) return false;
size_t n = 0;
for (size_t i = 0; i < dot; ++i) {
if (text[i] < '0' || text[i] > '9') return false;
n = n * 10 + (size_t)(text[i] - '0');
if (n > (1u << 26)) return false; // 64MB cap
}
out.assign(n, 0); // setSize(n, true): zero-init
size_t pos = 0; // bit offset
for (size_t i = dot + 1; i < text.size(); ++i) {
int c = (int)(unsigned char)text[i] - 43;
if (c >= 0 && c < 80) {
juceSetBits(out.data(), out.size(), pos, 6, kJuceB64DecTbl[c]);
pos += 6;
}
}
return true;
}
static bool juceXmlWrapperToStates(const std::vector<uint8_t>& bytes,
std::vector<uint8_t>& comp,
std::vector<uint8_t>& ctrl) {
if (bytes.size() < 9 || std::memcmp(bytes.data(), "VC2!", 4) != 0) return false;
uint32_t len = ((uint32_t)bytes[4]) | ((uint32_t)bytes[5] << 8) |
((uint32_t)bytes[6] << 16) | ((uint32_t)bytes[7] << 24);
if (len == 0 || (size_t)len > bytes.size() - 8) return false;
std::string xml((const char*)bytes.data() + 8, (size_t)len);
auto pick = [&xml](const char* tag, std::vector<uint8_t>& out) -> int {
std::string open = std::string("<") + tag + ">";
std::string close = std::string("</") + tag + ">";
size_t a = xml.find(open);
if (a == std::string::npos) return 0; // element absent
a += open.size();
size_t b = xml.find(close, a);
if (b == std::string::npos) return -1; // malformed
std::string text = xml.substr(a, b - a);
if (juceStateDecode(text, out)) return 1; // JUCE len-dot (canonical)
return base64Decode(text, out) ? 1 : -1; // fallback RFC 4648
};
comp.clear();
ctrl.clear();
int rc = pick("IComponent", comp);
if (rc < 0) return false;
rc = pick("IEditController", ctrl);
if (rc < 0) return false;
return true;
}
class Vst3Fx {
public:
Vst3Fx() = default;
~Vst3Fx() {
#ifdef HAVE_VST3SDK
~Vst3Fx() { dispose(); }
// Full teardown of a loaded plugin (component terminate + controller
// terminate + process data). Idempotent. Callers on the retry path wrap
// this in SEH (fxGuiDisposeSafe) — a plugin whose GUI attach crashed may
// be too broken to terminate cleanly; worst case it leaks instead of
// re-crashing the process.
void dispose() {
#ifndef HAVE_VST3SDK
return;
#else
if (!state_) return;
auto* s = static_cast<Vst3FxState*>(state_);
if (s->component) {
@@ -759,31 +621,6 @@ public:
}
state_ = s;
return true;
#endif
}
// Unload the current instance so a retry can load a fresh one. On the
// retry path the editor never attached (view is null) and closeEditor()
// only frees the plugFrame leaked by the crashed attach. state_ is
// cleared BEFORE the plugin teardown calls: a crash in one of them
// (SEH-caught by fxGuiUnloadSafe) leaves this object usable again — the
// old Vst3FxState leaks (accepted: the retry loop gives up anyway).
void unload() {
#ifndef HAVE_VST3SDK
return;
#else
auto* s = static_cast<Vst3FxState*>(state_);
if (!s) return;
closeEditor();
state_ = nullptr;
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;
#endif
}
// Audio in → audio out, one block. Must be SEH-wrapped by the caller.
@@ -852,9 +689,7 @@ public:
#endif
}
// Apply preset: base64 of a raw .vstpreset FILE (VST3 magic + chunk list)
// OR of the JUCE XML wrapper (VC2! + len + <VST3PluginState>... — canonical
// PLAN A format) OR of the legacy bridge state blob [4B BE compLen][comp]
// [4B BE ctrlLen][ctrl].
// 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;
@@ -885,10 +720,8 @@ public:
if (!readChunk(Steinberg::Vst::kComponentState, comp) ||
!readChunk(Steinberg::Vst::kControllerState, ctrl))
return false;
} else if (juceXmlWrapperToStates(bytes, comp, ctrl)) {
// JUCE XML wrapper — canonical preset format (PLAN A).
} else {
// Legacy bridge state blob (backward-compatible).
// 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];
@@ -924,11 +757,9 @@ public:
#endif
}
// Capture the plugin's CURRENT state (component + edit controller) as the
// canonical JUCE XML wrapper (VC2! + len + <VST3PluginState>, PLAN A) —
// cùng format JuceFxEngine::getStateInformation; applyPreset + JUCE
// setStateInformation đều đọc được. Lets the GUI bridge report knob
// tweaks so a saved project can restore them. Must run on the thread
// owning the plugin.
// bridge blob [4B BE compLen][comp][4B BE ctrlLen][ctrl] base64 — same
// format applyPreset accepts. Lets the GUI bridge report knob tweaks so a
// saved project can restore them. Must run on the thread owning the plugin.
void captureState(std::string& out) {
#ifndef HAVE_VST3SDK
(void)out;
@@ -948,28 +779,18 @@ public:
ctrl.assign(stream.getData(), stream.getData() + stream.getSize());
}
if (comp.empty() && ctrl.empty()) return;
std::string xml = "<VST3PluginState>";
if (!comp.empty()) {
std::string b;
juceStateEncode(comp, b);
xml += "<IComponent>" + b + "</IComponent>";
}
if (!ctrl.empty()) {
std::string b;
juceStateEncode(ctrl, b);
xml += "<IEditController>" + b + "</IEditController>";
}
xml += "</VST3PluginState>";
out.clear();
out.reserve(8 + xml.size() + 1);
out += "VC2!";
uint32_t xlen = (uint32_t)xml.size();
out.push_back((char)(xlen & 0xFF));
out.push_back((char)((xlen >> 8) & 0xFF));
out.push_back((char)((xlen >> 16) & 0xFF));
out.push_back((char)((xlen >> 24) & 0xFF));
out += xml;
out += '\0';
std::vector<uint8_t> blob;
auto append32 = [&blob](uint32_t v) {
blob.push_back((uint8_t)(v >> 24));
blob.push_back((uint8_t)(v >> 16));
blob.push_back((uint8_t)(v >> 8));
blob.push_back((uint8_t)v);
};
append32((uint32_t)comp.size());
blob.insert(blob.end(), comp.begin(), comp.end());
append32((uint32_t)ctrl.size());
blob.insert(blob.end(), ctrl.begin(), ctrl.end());
base64EncodeTo(blob.data(), blob.size(), out);
#endif
}
// Live param change (realtime chain, NO restart): address by lowercase
@@ -1134,6 +955,14 @@ public:
return state_ ? static_cast<Vst3FxState*>(state_)->latencySamples : 0;
#endif
}
// Retry path (fxGuiLoadAttach): take ownership of another instance's
// loaded plugin state. other->state_ nulled so its destructor is a no-op;
// the caller may then delete other safely. Only use on instances that
// NEVER attached successfully (their state is clean or leaked anyway).
void stealState(Vst3Fx* other) {
state_ = other->state_;
other->state_ = nullptr;
}
private:
void* state_ = nullptr;
@@ -1686,17 +1515,15 @@ void RealtimeFxChain::Impl::workerLoop() {
static void fxGuiResizeSafe(void* fx, HWND hwnd, int w, int h);
static void fxGuiCaptureSafe(void* fx, std::string& out);
// Float mode (P2 gop-process plan): SF_FXGUI_FLOAT=1 makes --fx-gui a REAL
// on-screen window instead of the embedded frame-capture server. WM_CLOSE only
// hides it (plugin instance stays loaded — reopen is instant); /exit, /close
// or a dead parent tear the process down for real. Set in run_fx_gui_server
// from the env, before the window is created.
bool g_fxGuiFloatMode = false;
namespace {
const char* kFxGuiWndClass = "SonicForge_FX_GUI_Class";
// Set once the editor view is attached (fxGuiLoadAttach success) — the
// FxGuiServer /show handler skips its one-time first-paint kick until the
// editor exists, so the kick cannot fire on a bare host window.
std::atomic<bool> g_fxGuiEditorAttached{false};
struct FxGuiCtx {
bool closing = false;
};
@@ -1714,42 +1541,13 @@ static bool fxGuiParentAlive(uint32_t pid) {
CloseHandle(h);
return true;
}
// Attach attempts per process (VẤN ĐỀ #2 auto-retry): Ozone 11's editor
// crashes (0xC0000005) on a COLD first attach (createInstance ~74s) but a
// warm reload (~20-30s) usually attaches fine. Retrying in-process keeps the
// SAME hwnd, HTTP port and process so the frontend embed never sees the
// bridge die. Env SF_FXGUI_ATTACH_RETRIES overrides the count (1 = no retry,
// the pre-V45.46 behavior; cap 6).
static int fxGuiAttachRetries() {
const char* e = std::getenv("SF_FXGUI_ATTACH_RETRIES");
int n = e ? std::atoi(e) : 3;
if (n < 1) n = 1;
if (n > 6) n = 6;
return n;
}
// Leftover plugin child windows (GL surfaces etc.) from a crashed attach must
// go before the next attempt — the fresh instance creates its own children.
// SEH-wrapped: a bad child wndproc after a crash must not kill the retry.
static BOOL CALLBACK fxGuiDestroyChildProc(HWND child, LPARAM) {
__try { if (IsWindow(child)) DestroyWindow(child); }
__except (EXCEPTION_EXECUTE_HANDLER) {}
return TRUE; // keep enumerating
}
static void fxGuiDestroyChildWindows(HWND hwnd) {
if (IsWindow(hwnd)) EnumChildWindows(hwnd, fxGuiDestroyChildProc, 0);
}
// Float-mode exit message: destroy the window -> pump loop ends -> process
// exits for real (WM_CLOSE in float mode only hides).
#define WM_FXGUI_EXIT (WM_APP + 0x53)
static void fxGuiStartWatchdog(HWND hwnd, uint32_t parentPid) {
if (parentPid == 0) return;
std::thread([hwnd, parentPid]() {
while (IsWindow(hwnd)) {
Sleep(2000);
if (!fxGuiParentAlive(parentPid)) {
PostMessageA(hwnd, g_fxGuiFloatMode ? WM_FXGUI_EXIT : WM_CLOSE,
0, 0); // pump exits -> process returns
PostMessageA(hwnd, WM_CLOSE, 0, 0); // pump exits -> process returns
return;
}
}
@@ -1774,39 +1572,11 @@ LRESULT CALLBACK FxGuiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
FxGuiCtx* ctx = (FxGuiCtx*)GetWindowLongPtrA(hwnd, GWLP_USERDATA);
switch (msg) {
case WM_CLOSE:
// Float mode: hide, keep the window + plugin instance alive so a
// reopen (/show) is instant. Only embedded mode (or WM_FXGUI_EXIT)
// destroys the window for real.
if (g_fxGuiFloatMode) {
ShowWindow(hwnd, SW_HIDE);
return 0;
}
DestroyWindow(hwnd);
return 0;
case WM_FXGUI_EXIT:
DestroyWindow(hwnd);
return 0;
case WM_DESTROY:
if (ctx) ctx->closing = true;
return 0;
case WM_EXITSIZEMOVE:
// Float mode: user finished dragging/moving the frame. The plugin
// child window does NOT follow host resizes by itself, so the
// newly exposed/stale client area keeps old pixels (ảnh ma, Bug 1).
// Resize the editor view to the new client rect (plugin clamps via
// checkSizeConstraint; resizeEditor re-sizes the host to match) and
// force a full repaint of window + children. Runs on the pump
// thread (owner of the editor), so the direct call is safe.
if (g_fxGuiFloatMode && !(ctx && ctx->closing)) {
RECT rc;
if (GetClientRect(hwnd, &rc) && rc.right > 0 && rc.bottom > 0) {
fxGuiResizeSafe((void*)GetPropA(hwnd, "FXGUI_FX"), hwnd,
(int)rc.right, (int)rc.bottom);
}
RedrawWindow(hwnd, nullptr, nullptr,
RDW_INVALIDATE | RDW_ALLCHILDREN);
}
return 0;
case WM_FXGUI_RESIZE:
fxGuiResizeSafe((void*)GetPropA(hwnd, "FXGUI_FX"), hwnd, (int)wp, (int)lp);
return 0;
@@ -1838,6 +1608,32 @@ static bool fxGuiAttachSafe(void* fx, HWND hwnd) {
return false;
#endif
}
// SEH-wrapped plugin load: a load crash (rare) becomes a clean failure instead
// of killing the bridge. locals in load() are leaked on crash — acceptable on
// the retry path (process exits after the session anyway).
static bool fxGuiLoadSafe(Vst3Fx* fx, const std::string& path) {
#ifdef HAVE_VST3SDK
__try { return fx->load(path, 44100.0, 512); }
__except (EXCEPTION_EXECUTE_HANDLER) { return false; }
#else
(void)fx; (void)path;
return false;
#endif
}
// SEH-guarded delete of a failed-attempt instance: dispose() (destructor) runs
// guarded so a plugin too broken to terminate cleanly leaks instead of
// re-crashing the process; the main goal is stopping the failed instance's
// component/worker threads so they cannot crash the process asynchronously
// later (a leaked crashed instance's workers kept killing the GUI session
// seconds after a successful attach).
static void fxGuiDisposeSafe(Vst3Fx* fx) {
#ifdef HAVE_VST3SDK
__try { delete fx; }
__except (EXCEPTION_EXECUTE_HANDLER) {}
#else
(void)fx;
#endif
}
static void fxGuiDetachSafe(void* fx) {
#ifdef HAVE_VST3SDK
__try { static_cast<Vst3Fx*>(fx)->closeEditor(); }
@@ -1846,18 +1642,6 @@ static void fxGuiDetachSafe(void* fx) {
(void)fx;
#endif
}
// Unload a GUI instance under SEH — the plugin's terminate may crash (same
// rule as fxGuiAttachSafe: raw-pointer frame, C2712-safe). False = exception
// mid-teardown → instance state unknown → stop retrying.
static bool fxGuiUnloadSafe(Vst3Fx* fx) {
#ifdef HAVE_VST3SDK
__try { if (fx) fx->unload(); return true; }
__except (EXCEPTION_EXECUTE_HANDLER) { return false; }
#else
(void)fx;
return true;
#endif
}
static void fxGuiResizeSafe(void* fx, HWND hwnd, int w, int h) {
#ifdef HAVE_VST3SDK
__try { static_cast<Vst3Fx*>(fx)->resizeEditor(hwnd, w, h); }
@@ -1879,6 +1663,9 @@ void fxGuiRequestResize(HWND hwnd, int w, int h) {
PostMessageA(hwnd, WM_FXGUI_RESIZE, (WPARAM)w, (LPARAM)h);
}
// Editor-attach state for the /show first-paint kick (FxGuiServer.cpp).
bool fxGuiEditorAttached() { return g_fxGuiEditorAttached.load(); }
// Capture the GUI plugin state on the pump thread; blocks up to timeoutMs.
void fxGuiRequestCapture(void* hwndV, std::string& outB64, int timeoutMs) {
HWND hwnd = (HWND)hwndV;
@@ -1896,13 +1683,6 @@ void fxGuiRequestCapture(void* hwndV, std::string& outB64, int timeoutMs) {
}
}
// Float mode close: post WM_FXGUI_EXIT so the pump thread destroys the window
// (full process exit — WM_CLOSE in float mode only hides). Used by the
// FxGuiServer /close and /exit routes; embedded mode is unaffected.
void fxGuiRequestExit(HWND hwnd) {
PostMessageA(hwnd, WM_FXGUI_EXIT, 0, 0);
}
// --- SHM audio feeder (GUI meters) -----------------------------------------
// The GUI process PEEKS the SAME input ring the realtime session uses (never
@@ -2064,9 +1844,7 @@ static int fxGuiCreateWindow(const std::string& name, bool offscreen,
RegisterClassA(&wc);
const int x = offscreen ? -20000 : CW_USEDEFAULT;
const int y = offscreen ? -20000 : CW_USEDEFAULT;
// WS_EX_TOOLWINDOW cả khi onscreen (float/--open-fx-gui): GUI plugin là cửa
// sổ phụ của DAW — không được tạo icon taskbar riêng (user chỉ thấy app).
const DWORD exStyle = (WS_EX_TOOLWINDOW) | (offscreen ? WS_EX_NOACTIVATE : 0);
const DWORD exStyle = offscreen ? (WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE) : 0;
// Borderless (WS_POPUP): GetWindowRect == client rect, so anchored
// placement over the embed panel is exact. A captioned/bordered window
// (WS_OVERLAPPEDWINDOW) put the client area ~31px lower/right of the
@@ -2086,43 +1864,61 @@ static int fxGuiCreateWindow(const std::string& name, bool offscreen,
// 3. Load the plugin + attach the editor (SEH-guarded — plugin GUI code may
// crash). Slow — runs on the pump thread in server mode, after HTTP is up.
// Returns 0 ok, 2 plugin load or editor attach failure.
// Ozone-class plugins crash intermittently INSIDE view->attached()
// (iZOzone11Core.dll, null-vtable read — GUI-worker race), so each attempt
// loads a FRESH instance; failed ones are torn down best-effort (guarded —
// see fxGuiDisposeSafe). Returns 0 ok, 2 plugin load or editor attach
// failure.
static int fxGuiLoadAttach(Vst3Fx* fx, const std::string& path, const std::string& name, HWND hwnd, const std::string& presetB64) {
// Fixed 44.1k/512 — GUI only, no audio processed here.
if (!fx->load(path, 44100.0, 512)) { std::cerr << "[FxGui] plugin load failed: " << path << std::endl; return 2; }
// Khôi phục preset GUI capture cuối (hide/show mất settings bug): áp
// preset NGAY SAU load — trước attach editor để GUI hiện đúng cài đặt.
if (!presetB64.empty() && !fx->applyPreset(presetB64))
std::cerr << "[FxGui] preset apply FAILED (default kept): " << path << std::endl;
else if (!presetB64.empty())
std::cerr << "[FxGui] preset applied: " << path << std::endl;
g_fxGuiEditorReady.store(false);
if (!fxGuiAttachSafe(fx, hwnd)) {
std::cerr << "[FxGui] editor attach failed for " << path << std::endl;
// Do NOT destroy hwnd here — the retry loop reuses the same window;
// after the last failed attempt the caller posts WM_CLOSE (teardown).
return 2;
const int kMaxAttempts = 4;
for (int attempt = 1; attempt <= kMaxAttempts; ++attempt) {
Vst3Fx* fresh = new Vst3Fx();
std::cerr << "[FxGui] attempt " << attempt << "/" << kMaxAttempts
<< ": load " << path << std::endl;
// Fixed 44.1k/512 — GUI only, no audio processed here.
if (!fxGuiLoadSafe(fresh, path)) {
std::cerr << "[FxGui] plugin load failed: " << path << std::endl;
delete fresh; // clean failure — state_ never set, destructor no-op
DestroyWindow(hwnd);
return 2;
}
// Khôi phục preset GUI capture cuối (hide/show mất settings bug): áp
// preset NGAY SAU load — trước attach editor để GUI hiện đúng cài đặt.
if (!presetB64.empty() && !fresh->applyPreset(presetB64))
std::cerr << "[FxGui] preset apply FAILED (default kept): " << path << std::endl;
else if (!presetB64.empty())
std::cerr << "[FxGui] preset applied: " << path << std::endl;
// iZotope (Ozone-class): license check races the editor attach and
// intermittently crashes inside iZOzone11Core.dll — give the Product
// Engine license service a beat after load before attaching.
if (path.find("Ozone") != std::string::npos ||
path.find("iZotope") != std::string::npos)
Sleep(2000);
if (fxGuiAttachSafe(fresh, hwnd)) {
fx->stealState(fresh);
delete fresh;
g_fxGuiEditorAttached = true; // /show first-paint kick only after editor exists
std::cout << "[FxGui] editor open: " << (name.empty() ? path : name) << std::endl;
return 0;
}
std::cerr << "[FxGui] editor attach FAILED (attempt " << attempt << "/"
<< kMaxAttempts << ") — retrying fresh instance" << std::endl;
// Best-effort teardown of the failed instance (guarded): stop its
// component/worker threads so they cannot crash the process later.
// Worst case the instance leaks (unchanged from before).
fxGuiDisposeSafe(fresh);
}
// Attach OK: allow /frame. First captures may still be blank (plugin paints
// on its first WM_PAINT) — FxGuiServer's luma check rejects those and
// serves the last good frame instead.
g_fxGuiEditorReady.store(true);
if (g_fxGuiShowPending.exchange(false)) {
// A /show was waiting while this editor attached (Bug 2): show now
// that the plugin view exists. Runs on the pump thread (owner).
ShowWindow(hwnd, SW_SHOW);
}
std::cout << "[FxGui] editor open: " << (name.empty() ? path : name) << std::endl;
return 0;
DestroyWindow(hwnd);
return 2;
}
// Combined setup (legacy open mode): parse + load + window + attach, in order.
// Combined setup (legacy open mode): parse + window + attach (load happens
// inside fxGuiLoadAttach's retry loop), in order.
static int fxGuiSetup(const std::string& jobPath, Vst3Fx* fx, HWND* hwndOut,
bool offscreen, LONG_PTR userData, std::string& shmOut) {
std::string path, name, presetB64;
int rc = fxGuiParseJob(jobPath, path, name, shmOut, presetB64);
if (rc) return rc;
if (!fx->load(path, 44100.0, 512)) { std::cerr << "[FxGui] plugin load failed: " << path << std::endl; return 2; }
rc = fxGuiCreateWindow(name, offscreen, userData, hwndOut);
if (rc) return rc;
return fxGuiLoadAttach(fx, path, name, *hwndOut, presetB64);
@@ -2167,15 +1963,6 @@ int run_fx_gui_server(const std::string& jobPath) {
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
struct ComGuard { ~ComGuard() { CoUninitialize(); } } comGuard;
// P2 float mode: SF_FXGUI_FLOAT=1 (engine sets it for --fx-gui) turns the
// embed server into a REAL on-screen window. Must be read before
// fxGuiCreateWindow + watchdog (window style + WM_CLOSE handling differ).
const char* floatEnv = std::getenv("SF_FXGUI_FLOAT");
g_fxGuiFloatMode = floatEnv != nullptr && floatEnv[0] != '0';
if (g_fxGuiFloatMode)
std::cout << "[FxGui] float mode (real on-screen window; WM_CLOSE=hide)"
<< std::endl;
Vst3Fx fx;
HWND hwnd = nullptr;
// Fast path: parse job + create the host window, then start the HTTP
@@ -2185,73 +1972,17 @@ int run_fx_gui_server(const std::string& jobPath) {
std::string path, name, shm, presetB64;
int rc = fxGuiParseJob(jobPath, path, name, shm, presetB64);
if (rc) return rc;
rc = fxGuiCreateWindow(name, /*offscreen=*/!g_fxGuiFloatMode, 0, &hwnd);
rc = fxGuiCreateWindow(name, /*offscreen=*/true, 0, &hwnd);
if (rc) return rc;
fxGuiStartWatchdog(hwnd, fxGuiParentPid());
SetPropA(hwnd, "FXGUI_FX", &fx); // for WM_FXGUI_RESIZE on the pump thread
fxGuiShmSetName(shm); // EARLY — frontend may /shm push before load finishes
int loadRc = 0;
// EMBED_ONSCREEN implies ATTACH_ONSCREEN: the Intel UHD 630 attach
// crash fix (attach on a real monitor) and the black-capture fix (stay
// on a monitor afterwards, parked under the DAW at the panel rect) go
// together — setting only EMBED_ONSCREEN must not regress the crash.
// V45.45: on-screen embed parking la MAC DINH (DWM Intel UHD 630 khong
// redirect window parked -20000). Env chi con de TAT: SF_FXGUI_EMBED_ONSCREEN=0.
const char* eosEnv = std::getenv("SF_FXGUI_EMBED_ONSCREEN");
const bool embedOnscreen = eosEnv == nullptr || eosEnv[0] != '0';
const bool attachOnscreen = embedOnscreen || std::getenv("SF_FXGUI_ATTACH_ONSCREEN") != nullptr;
std::cout << "[FxGui] env SF_FXGUI_ATTACH_ONSCREEN=" << (attachOnscreen ? 1 : 0)
<< " SF_FXGUI_EMBED_ONSCREEN=" << (embedOnscreen ? 1 : 0)
<< " SF_FXGUI_DEBUG=" << (std::getenv("SF_FXGUI_DEBUG") ? 1 : 0)
<< std::endl;
const int srv = fxGuiServerLoop(hwnd, [&]() {
// Auto-retry attach (V45.46, VẤN ĐỀ #2): iZotope Ozone 11's editor
// crashes (0xC0000005 in iZOzone11Core.dll) on a COLD first attach.
// Each retry unloads the instance and loads a fresh one (warm load
// ~20-30s vs ~74s cold) while keeping the SAME hwnd, HTTP port and
// process — the frontend embed poll never sees the bridge die and
// its spinner just keeps waiting. SF_FXGUI_ATTACH_RETRIES overrides
// the count (1 = old no-retry behavior; cap 6). "attach attempt N/M"
// in the engine log lets the user verify retries happened.
const int maxAttempts = fxGuiAttachRetries();
int attempt = 0;
for (; attempt < maxAttempts; ++attempt) {
// Some plugin GL editors (e.g. iZotope Ozone 11 on Intel UHD 630)
// crash when parked offscreen during attach. Show on-screen first,
// park back once the editor attached. Visible for one frame max
// (feeder is started only after attach succeeds).
if (attachOnscreen)
SetWindowPos(hwnd, nullptr, 100, 100, 0, 0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
std::cout << "[FxGui] attach attempt " << (attempt + 1) << "/"
<< maxAttempts << std::endl;
if (attempt > 0) {
// Let audio/session settle after the crash, drop the crashed
// instance's leftover child windows, then unload it.
Sleep(1500);
fxGuiDestroyChildWindows(hwnd);
if (!fxGuiUnloadSafe(&fx)) break; // instance state unknown — give up
}
loadRc = fxGuiLoadAttach(&fx, path, name, hwnd, presetB64);
if (loadRc == 0) {
fxGuiStartFeederThread(&fx, hwnd); // after load+attach only
// EMBED_ONSCREEN: leave the window where attach put it (on a
// monitor); the /show + /frame reconcile park it at the panel
// rect (HWND_BOTTOM) once the frontend supplies the rect.
// Float mode: leave the window where attach put it (visible,
// user-positionable) — nothing to park off-screen.
if (!g_fxGuiFloatMode && attachOnscreen && !embedOnscreen)
SetWindowPos(hwnd, nullptr, -20000, -20000, 0, 0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
break;
}
std::cerr << "[FxGui] attach attempt " << (attempt + 1) << "/"
<< maxAttempts << " FAILED (rc=" << loadRc << ")" << std::endl;
}
if (loadRc != 0)
PostMessageA(hwnd, g_fxGuiFloatMode ? WM_FXGUI_EXIT : WM_CLOSE,
0, 0); // exit; port already printed
loadRc = fxGuiLoadAttach(&fx, path, name, hwnd, presetB64);
if (loadRc == 0) fxGuiStartFeederThread(&fx, hwnd); // after load+attach only
if (loadRc != 0) PostMessageA(hwnd, WM_CLOSE, 0, 0); // exit; port already printed
});
RemovePropA(hwnd, "FXGUI_FX");
fxGuiDetachSafe(&fx);
@@ -2425,6 +2156,19 @@ int run_scan_dir(const std::string& dirPath) {
#ifdef HAVE_VST3SDK
for (const auto& p : vst3Paths) {
std::vector<ScanEntry> entries;
// iZotope VST3 (Ozone 11/Pro, Neutron, RX, Nectar...) — wrapper cần
// companion core riêng, tự __fastfail (c0000409) hoặc MessageBox
// "Fatal Error 0x7E" khi load → không bị SEH chặn → chết cả process →
// mất TOÀN BỘ dir → FX list rỗng. Không load: emit UNKNOWN (giống skip
// module lỗi). ponytail: SandboxVst3Host (mỗi module 1 process).
{
std::string low = p;
for (auto& c : low) c = (char)std::tolower((unsigned char)c);
if (low.find("izotope") != std::string::npos) {
emit(p, p.substr(p.find_last_of("/\\") + 1), false, false, "VST3");
continue;
}
}
if (!safeScanModule(p, entries)) {
// Module unreadable/crashed/hung (e.g. Ozone 11 Clarity): emit
// entry UNKNOWN (is_fx=false, is_instrument=false) — trước đây
@@ -2443,6 +2187,16 @@ int run_scan_dir(const std::string& dirPath) {
}
#endif
for (const auto& p : vst2Paths) {
// iZotope VST2 wrapper DLL (iZOzonePro.dll, iZTonalBalanceControlPro.dll,
// iZNeutronPro.dll...) LoadLibrary companion core theo cơ chế riêng,
// không theo PATH → iZotope SDK tự hiện MessageBox "Fatal Error 0x7E"
// khi scan/load. Cũng là wrapper trùng VST3 cùng tên → bỏ qua hoàn
// toàn (user dùng bản VST3).
{
std::string low = p;
for (auto& c : low) c = (char)std::tolower((unsigned char)c);
if (low.find("izotope") != std::string::npos) continue;
}
std::string name = p.substr(p.find_last_of("/\\") + 1);
emit(p, name, false, true, "VST2");
}
+16
View File
@@ -9,6 +9,8 @@
#include "vestige/aeffect.h"
#include "VstDllSearch.h"
#include <windows.h>
#include <cstring>
@@ -301,6 +303,20 @@ Vst2Instrument::~Vst2Instrument() {
}
bool Vst2Instrument::loadPlugin(const std::string& path, double sampleRate) {
// iZotope VST2 wrapper DLL (iZOzonePro.dll...) LoadLibrary companion core
// DLL (iZOzone11Core.dll) ở C:\Program Files\iZotope\<Product>\Cores —
// không cạnh wrapper → iZotope SDK tự hiện MessageBox "Fatal Error 0x7E".
// vstAddDllSearchPath chỉ giúp loader search PATH; iZotope wrapper dùng cơ
// chế riêng → vẫn dialog. Bỏ qua hoàn toàn (wrapper trùng VST3 cùng tên).
{
std::string low = path;
for (auto& c : low) c = (char)std::tolower((unsigned char)c);
if (low.find("izotope") != std::string::npos) {
std::cerr << "[Vst2Instrument] skip (iZotope wrapper): " << path << std::endl;
return false;
}
}
vstAddDllSearchPath(vstDllSearchExtras());
HMODULE h = LoadLibraryA(path.c_str());
if (!h) {
std::cerr << "[Vst2Instrument] LoadLibraryA failed: " << path
+6
View File
@@ -8,6 +8,7 @@
// connect them, process MIDI events + audio blocks, and attach the editor
// view to a native HWND (openGUI) for the floating GUI (B9).
#include "Vst3Instrument.h"
#include "VstDllSearch.h"
#ifdef HAVE_VST3SDK
#ifdef _WIN32
@@ -254,6 +255,11 @@ bool Vst3Instrument::loadPlugin(const std::string& path, double sampleRate) {
using namespace VST3::Hosting;
std::string err;
#ifdef _WIN32
// iZotope VST3 core DLLs (vd iZOzone11Core.dll) nằm ở
// C:\Program Files\iZotope\<Product>\Cores — không cạnh .vst3, cần PATH.
vstAddDllSearchPath(vstDllSearchExtras());
#endif
Module::Ptr module = Module::create(path, err);
if (!module) {
std::cerr << "[Vst3Instrument] Module::create failed: " << err << std::endl;
-4
View File
@@ -106,10 +106,6 @@ public:
};
int main(int argc, char* argv[]) {
#ifdef _WIN32
// Suppress Windows loader error dialogs (0x7E) during probe loads.
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX);
#endif
if (argc < 2) {
printf("usage: gui_probe <plugin.vst3> [variant] [secs] [hwnd]\n"
"variants: main_own | worker_own_nopump | worker_own_pump | worker_foreign_nopump | worker_foreign_pump | bridge_like | same_thread | two_instances | two_instances_close | close_reopen | bridge_two | shared_worker | ui_thread | two_workers_close\n");
+1 -7
View File
@@ -627,9 +627,7 @@ static LONG WINAPI CrashDumpHandler(EXCEPTION_POINTERS* ep) {
std::cerr << "[CRASH] faulting module: " << modPath << std::endl;
}
}
char dumpDir[MAX_PATH];
if (!GetTempPathA(MAX_PATH, dumpDir)) return EXCEPTION_CONTINUE_SEARCH;
std::strncat(dumpDir, "sf_dumps", MAX_PATH - std::strlen(dumpDir) - 1);
const char* dumpDir = "C:/Users/locpham/AppData/Local/Temp/sf_dumps";
CreateDirectoryA(dumpDir, nullptr);
char dumpPath[MAX_PATH];
snprintf(dumpPath, sizeof(dumpPath), "%s/bridge_crash_%lu.dmp", dumpDir, GetCurrentProcessId());
@@ -717,10 +715,6 @@ int main(int argc, char* argv[]) {
SetUnhandledExceptionFilter(CrashDumpHandler);
AddVectoredExceptionHandler(1, VectoredCrashHandler);
#ifdef _WIN32
// Suppress Windows loader error dialogs: a plugin whose dependency DLL is
// missing (LoadLibrary -> 0x7E) must fail silently, not pop a MessageBox
// that blocks the user. Child processes inherit this error mode.
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX);
// Background daemon: detach the console window unless SF_KEEP_CONSOLE is
// set. Console apps spawned from the GUI (tauri-shell, Popen) flash a
// console window otherwise. Redirected stdio (pipe to bridge.log / the
+4 -18
View File
@@ -28,25 +28,15 @@
#ifdef _WIN32
// Minidump on crash -> %TEMP%\sf_dumps (same convention as daw_vst_bridge).
// Every qualifying exception is LOGGED (diagnosing the retry path needs to see
// crashes after the first); only the minidump is written once per process.
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;
if (er) {
HMODULE hMod = nullptr;
DWORD_PTR crashAddr = (DWORD_PTR)er->ExceptionAddress;
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCSTR)er->ExceptionAddress, &hMod) && hMod) {
char modPath[MAX_PATH];
if (GetModuleFileNameA(hMod, modPath, MAX_PATH))
std::cerr << "[CRASH] in " << modPath << "+0x" << std::hex
<< (crashAddr - (DWORD_PTR)hMod) << std::dec << std::endl;
}
}
static LONG once = 0;
if (InterlockedCompareExchange(&once, 1, 0)) return EXCEPTION_CONTINUE_SEARCH;
char dumpDir[MAX_PATH];
if (GetTempPathA(MAX_PATH, dumpDir)) {
std::strncat(dumpDir, "sf_dumps", MAX_PATH - std::strlen(dumpDir) - 1);
@@ -75,7 +65,6 @@ static LONG WINAPI FxCrashDumpHandler(EXCEPTION_POINTERS* ep) {
hFile, MiniDumpNormal, &mei, nullptr,
nullptr);
CloseHandle(hFile);
std::cerr << "[CRASH] minidump: " << dumpPath << std::endl;
}
}
}
@@ -119,9 +108,6 @@ int main(int argc, char* argv[]) {
#ifdef _WIN32
SetUnhandledExceptionFilter(FxCrashDumpHandler);
AddVectoredExceptionHandler(1, FxVectoredCrashHandler);
// Suppress Windows loader error dialogs (plugin dependency thiếu -> 0x7E
// phải fail âm thầm, không MessageBox chặn user; child kế thừa error mode).
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX);
win_daemon_init();
#endif
// stderr, not stdout: stdout is reserved for machine-readable output in
-5
View File
@@ -86,11 +86,6 @@ static HWND make_host_window() {
}
int main(int argc, char* argv[]) {
#ifdef _WIN32
// Suppress Windows loader error dialogs — plugin dependency thiếu (0x7E)
// phải fail âm thầm, không MessageBox chặn user.
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX);
#endif
std::string shmName, path, presetFile;
double sr = 44100.0;
uint32_t block = AUDIO_BLOCK_SIZE;
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -8
View File
@@ -1,12 +1,7 @@
; NSIS installer hooks — cai Microsoft Visual C++ Redistributable neu thieu
; (MSVCP140.dll missing). Dat vc_redist.x64.exe CANH tauri.conf.json
; (build_windows.ps1 tu tai ve neu chua co).
!macro NSIS_HOOK_POSTINSTALL
; vc_redist.x64.exe dong goi nhu bundle resource -> $INSTDIR (khong dung File:
; path tuong doi trong macro resolve theo thu muc installer.nsi sinh ra).
ExecWait '"$INSTDIR\vc_redist.x64.exe" /install /quiet /norestart' $0
${If} $0 = 0
${OrIf} $0 = 3010
Delete "$INSTDIR\vc_redist.x64.exe"
${EndIf}
!macro customInstall
File "/oname=$PLUGINSDIR\vc_redist.x64.exe" "vc_redist.x64.exe"
ExecWait '"$PLUGINSDIR\vc_redist.x64.exe" /install /quiet /norestart'
!macroend
+55 -2
View File
@@ -294,7 +294,8 @@ pub fn run() {
transport_control,
set_fx_chain,
restart_bridge_with_sample_rate,
dbg_log
dbg_log,
get_engine_port
])
.setup(|app| {
// V10 (FIX_DRAG_DROP_WINDOWS): disable Tauri's built-in drag-drop
@@ -741,7 +742,12 @@ pub fn run() {
// taskkill chi nhan MOT /IM moi lan goi — goi rieng cho tung
// process, khong gop /IM ... /IM (truoc day chi cai dau chay,
// fx_vst_bridge/plugin_host song sot thanh orphan).
for im in ["daw_engine.exe", "daw_vst_bridge.exe",
// KHONG taskkill /IM daw_engine.exe: image name dung chung moi
// instance — se giet ca engine cua app KHAC dang chay (2 app
// cung luc: dong 1 cai → app kia mat server → SERVER OFFLINE).
// Engine cua chinh minh da duoc kill_process_tree o tren; con
// sot (crash/khong parent) tu thoat nho watchdog SF_PARENT_PID.
for im in ["daw_vst_bridge.exe",
"fx_vst_bridge.exe", "plugin_host.exe"] {
let _ = std::process::Command::new("taskkill")
.args(["/IM", im, "/T", "/F"])
@@ -904,6 +910,53 @@ fn bridge_status(app: AppHandle) -> Result<BridgeStatus, String> {
/// V28: JS timing diagnostics — append `{unix_ms} {tag} {extra}` to
/// %APPDATA%/SonicForgeDAW/logs/dbg.log (create dirs as needed). Fire-and-forget
/// từ app.jsx (_dbgLog) để định vị delay Play/Stop không cần devtools.
/// Port engine đã chọn (desktop_engine.py ghi %APPDATA%/SonicForgeDAW/
/// engine_port.txt "port\nengine_exe_path" ngay sau _pick_port). UI bootstrap
/// ưu tiên dùng port này để kết nối ĐÚNG engine của chính mình; -1 khi chưa
/// có (engine đang khởi động) hoặc engine ghi file không phải của app này
/// (instance khác) → bootstrap retry/fallback quét 8000-8010.
#[tauri::command]
fn get_engine_port(app: AppHandle) -> i32 {
let appdata = std::env::var("APPDATA").unwrap_or_else(|_| ".".into());
let path = std::path::Path::new(&appdata)
.join("SonicForgeDAW")
.join("engine_port.txt");
let raw = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(_) => return -1,
};
let mut lines = raw.lines();
let Some(port_str) = lines.next() else { return -1 };
let port: i32 = match port_str.trim().parse() {
Ok(p) => p,
Err(_) => return -1,
};
// Đối chiếu engine_exe_path với candidate của CHÍNH app này — file có thể
// do instance khác (AppData vs target/release) ghi đè sau khi mình khởi
// động; nếu không khớp thì không dùng (fallback scan sẽ tìm engine mình).
if let Some(exe_line) = lines.next() {
let exe = exe_line.trim().to_string();
let norm = |s: String| s.replace("\\\\?\\", "").replace('/', "\\").to_lowercase();
let norm_exe = norm(exe);
let exe_dir = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf()));
let mut bases: Vec<PathBuf> = Vec::new();
if let Ok(res) = app.path().resource_dir() {
bases.push(res);
}
if let Some(d) = exe_dir {
bases.push(d);
}
let name = if cfg!(windows) { "daw_engine.exe" } else { "daw_engine" };
let matches = bases.iter().any(|base| {
norm(base.join("daw_engine").join(name).to_string_lossy().to_string()) == norm_exe
});
if !matches {
return -1;
}
}
port
}
#[tauri::command]
fn dbg_log(tag: String, extra: String) -> Result<(), String> {
let Some(appdata) = std::env::var("APPDATA").ok() else { return Ok(()) };
+5 -3
View File
@@ -26,13 +26,15 @@
"resources/daw_engine": "daw_engine/",
"binaries/libfluidsynth-3.dll": "libfluidsynth-3.dll",
"binaries/jpeg62.dll": "jpeg62.dll",
"vc_redist.x64.exe": "vc_redist.x64.exe"
"binaries/MSVCP140.dll": "MSVCP140.dll",
"binaries/MSVCP140_2.dll": "MSVCP140_2.dll",
"binaries/VCRUNTIME140.dll": "VCRUNTIME140.dll",
"binaries/VCRUNTIME140_1.dll": "VCRUNTIME140_1.dll"
},
"externalBin": ["binaries/daw_vst_bridge", "binaries/fx_vst_bridge", "binaries/plugin_host"],
"windows": {
"nsis": {
"installerHooks": "hooks.nsh",
"installMode": "perMachine"
"installerHooks": "hooks.nsh"
}
}
}
+34 -5
View File
@@ -20,16 +20,45 @@
// (origin tauri:// giữ nguyên -> __TAURI__ sống), base href + API_BASE_URL
// trỏ engine HTTP (CORS * đã bật).
(async function () {
// Đợi engine của CHÍNH app này (port do desktop_engine.py ghi vào
// engine_port.txt, đọc qua lệnh Tauri get_engine_port) tối đa 60s.
// Fallback: quét 8000-8010 (engine cũ/cài chồng) nhưng CÓ timeout mỗi
// port — trước đây fetch không timeout + không retry: port giữ kết nối
// không phản hồi → UI treo "Đang khởi động" vĩnh viễn; engine khởi
// động chậm (cold start) → "Không tìm thấy" dù sau đó engine sống.
var base = null;
for (var p = 8000; p <= 8010 && !base; p++) {
var deadline = Date.now() + 60000;
function withTimeout(ms) {
var ctl = new AbortController();
var t = setTimeout(function () { ctl.abort(); }, ms);
return { ctl: ctl, t: t };
}
while (!base && Date.now() < deadline) {
try {
var r = await fetch('http://127.0.0.1:' + p + '/health', { cache: 'no-store' });
if (r.ok) base = 'http://127.0.0.1:' + p + '/';
} catch (e) { /* port chưa mở */ }
if (window.__TAURI__ && window.__TAURI__.core && window.__TAURI__.core.invoke) {
var port = await window.__TAURI__.core.invoke('get_engine_port');
if (port && port > 0) {
var h = withTimeout(2000);
try {
var r = await fetch('http://127.0.0.1:' + port + '/health', { signal: h.ctl.signal, cache: 'no-store' });
if (r.ok) base = 'http://127.0.0.1:' + port + '/';
} finally { clearTimeout(h.t); }
}
}
} catch (e) { /* engine chưa ghi port — retry */ }
if (base) break;
for (var p = 8000; p <= 8010 && !base; p++) {
var h2 = withTimeout(1000);
try {
var r2 = await fetch('http://127.0.0.1:' + p + '/health', { signal: h2.ctl.signal, cache: 'no-store' });
if (r2.ok) base = 'http://127.0.0.1:' + p + '/';
} catch (e) { /* port chưa mở */ } finally { clearTimeout(h2.t); }
}
if (!base) await new Promise(function (res) { setTimeout(res, 1500); });
}
if (!base) {
document.getElementById('msg').textContent =
'Không tìm thấy SonicForge Engine (port 8000–8010). Hãy đóng và chạy lại ứng dụng. ' +
'Không tìm thấy SonicForge Engine (port 8000–8010) sau 60s. Hãy đóng và chạy lại ứng dụng. ' +
'Nếu lặp lại, xem log: %APPDATA%\\SonicForgeDAW\\logs\\engine.log';
return;
}