diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 46f5c1f..f67ffed 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -693,6 +693,72 @@ 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. @@ -726,62 +792,25 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren # 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 - _time.sleep(0.1) + await asyncio.sleep(0.1) if _sess is not None and _sess.alive(): - _slot = _sess.chain_slot_for_path(plugin_path) - if _slot is not None: - # Bug 2-GUI: fallback bridge fx-gui (embed/float) còn sống cho cùng - # plugin — instance VST THỨ 2 + window riêng; mở native phải loại nó - # trước, nếu không 1 plugin hiện 2 GUI (1 floating native + 1 embed) - # và floating không tắt được qua UI. Terminate proc cũ → frontend - # embed panel của nó onDead → tự đóng. - # M8: set_chain (juce bridge) reload async (watchdog respawn) — - # cho chain moi ap tren bridge truoc khi ed.open, khong thi C++ - # bao "slot ngoai chain (M) — bo qua". Khong co reload dang chay - # -> tra ngay (khong them latency mo GUI). - _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} + # 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 @@ -904,8 +933,12 @@ async def close_fx_gui(req: FxGuiCloseRequest, current_user: dict = Depends(get_ # 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. - _preset = _sess.capture_editor_preset(_slot) - _sess.send_editor_cmd("close", slot=_slot) + # 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ó) @@ -958,8 +991,12 @@ async def hide_fx_gui(req: FxGuiNativeRequest, # 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). - _preset = sess.capture_editor_preset(slot) - sess.send_editor_cmd("hide", slot=slot) + # 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}