M7-M16: VST FX GUI native embed hoàn thiện — fix click-2-lần chuyển slot, anchor lần mở đầu, taskbar icon

- app.jsx: masterGuiOpenRef sync truoc setState — guard chuyen slot doc ref, het race closure (click 1 lan chuyen slot)
- JuceFxGuiHost.cpp: M14 applyAnchor truoc setVisible (het flash giua man hinh); M15 clear WS_EX_APPWINDOW + set TOOLWINDOW (het taskbar icon — JUCE set APPWINDOW 0x40000 thang TOOLWINDOW); M9 owner DAW + M9d skip applyOwner khi an
- JuceFxLoop.cpp: M16 bo clearAnchor khi ed.open (retry ed.open ~700ms xoa rect fresh -> window moi khong anchor duoc lan dau mo panel); chi clear khi Close
- JuceFxEngine/app.jsx/plugins.py/fx_realtime.py: editor session realtime, anchor rect routing, retry ed.open
- Rebuild engine PyInstaller + juce_fx_bridge cmake; binaries dong bo 5 noi (bridge 7464a7ef)
This commit is contained in:
2026-09-06 19:45:14 +07:00
parent ec2c925d09
commit ef521d9981
20 changed files with 2040 additions and 461 deletions
+97 -4
View File
@@ -668,7 +668,10 @@ 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)."""
@@ -719,6 +722,16 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
# 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:
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)
if _sess is not None and _sess.alive():
_slot = _sess.chain_slot_for_path(plugin_path)
if _slot is not None:
@@ -727,11 +740,45 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
# 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.
_terminate_fx_gui_for_path(plugin_path)
# 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
_sess.send_editor_cmd("open", slot=_slot)
_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}
@@ -749,6 +796,11 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
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.")
job = {"path": plugin_path, "name": req.name or os.path.basename(plugin_path)}
if req.shm:
job["shm"] = req.shm
@@ -867,6 +919,20 @@ class FxGuiNativeRequest(BaseModel):
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)
@@ -889,8 +955,13 @@ async def hide_fx_gui(req: FxGuiNativeRequest,
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).
_preset = sess.capture_editor_preset(slot)
sess.send_editor_cmd("hide", slot=slot)
return {"success": True, "applied": True, "native": True}
return {"success": True, "applied": True, "native": True,
"preset_b64": _preset}
@router.post("/fx-gui/show")
@@ -905,6 +976,28 @@ async def show_fx_gui(req: FxGuiNativeRequest,
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}
_FX_GUI_PROCESSES = [] # list[subprocess.Popen] — các bridge fx-gui do app spawn
def _register_fx_gui_process(proc):
+89 -5
View File
@@ -142,6 +142,10 @@ class FxRealtimeSession:
# 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()
@@ -197,7 +201,9 @@ class FxRealtimeSession:
fd, self._job_path = tempfile.mkstemp(suffix=".json", prefix="fxrt_")
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(job, f)
if os.path.basename(exe).lower().startswith("juce_fx_bridge"):
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:
cmd = [exe, "--juce-fx", self._job_path, "--shm", self.name,
"--parent", str(os.getpid())]
else:
@@ -209,14 +215,22 @@ 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")
_blog = os.path.join(_ad, "SonicForgeDAW", "logs", "fxrt_bridge.log") if _ad else ""
if _ad:
_blog = os.path.join(_ad, "SonicForgeDAW", "logs",
"fxrt_bridge_" + self.name + ".log")
else:
_blog = ""
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):
@@ -238,8 +252,56 @@ 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:
@@ -471,6 +533,24 @@ class FxRealtimeSession:
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
@@ -539,11 +619,15 @@ class FxRealtimeSession:
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._restarts >= FXRT_MAX_RESTARTS:
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
self._restarts += 1
_log.warning("[fxrt] bridge chết rc=%s — auto restart #%d", rc, self._restarts)
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:
+658 -97
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+2
View File
@@ -96,6 +96,8 @@ window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\
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' }),
+25 -6
View File
@@ -45,6 +45,7 @@ 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)
@@ -58,7 +59,7 @@ 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 = 256; // V42: crossfade 256 mẫu (~5.3ms @48k) tại seam wrap — che click jump nội dung end→start (FX path)
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.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) => {
@@ -86,11 +87,13 @@ 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,
@@ -125,8 +128,9 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
// dai hon roi vao nhanh resume wrap (bo flush + resumeFill 8).
const KEEP = 16;
this.wrapMode = true;
if (this.outQueue.length > KEEP) {
const head = this.outQueue.length - KEEP;
const _preQ = this.outQueue.length; // V46.1 instrument seam
if (_preQ > KEEP) {
const head = _preQ - KEEP;
this.outQueue.splice(0, head);
this.outPos = 0;
if (this.playing) {
@@ -136,7 +140,13 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
this.skipFromR = this.lastOutR;
}
}
this.wrapFadeBlocks = this.outQueue.length + Math.ceil((this.latencySamples || 0) / this.BLOCK);
// 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 });
return;
}
if (d.mode === 'seek') {
@@ -184,7 +194,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor {
// 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;
if (this.stopped || this.flushPending) return;
this.outQueue.push({ l: d.l, r: d.r });
if (this.dbgOutFirst) {
this.dbgOutFirst = false;
@@ -339,11 +349,20 @@ 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;
this.resumeFill = _wrap ? 8 : this.FILL;
// 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.flushPending = _wrap ? false : true;
if (!_wrap) this.port.postMessage({ type: 'flush' });
i--;
+1 -1
View File
@@ -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=202609032200" defer></script>
<script src="/static/js/app.precompiled.js?v=202609061838" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202608290945">
<style>
:root {
+73 -5
View File
@@ -9,6 +9,7 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <cstring>
@@ -208,6 +209,14 @@ struct JuceFxEngine::Impl {
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>()) {}
@@ -224,11 +233,28 @@ void JuceFxEngine::requestEditor(uint32_t slot, EditorAction action, int w, int
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()) return false;
cmds.swap(impl_->editorQueue);
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()) {
@@ -253,6 +279,7 @@ bool JuceFxEngine::pumpEditorQueue() {
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)
@@ -262,11 +289,15 @@ bool JuceFxEngine::pumpEditorQueue() {
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;
@@ -279,6 +310,7 @@ bool JuceFxEngine::pumpEditorQueue() {
<< " editor closed" << std::endl;
any = true;
}
retryClearSlot(c.slot); // M14: đóng = huỷ retry mở dang dở
break;
}
case EditorAction::Show:
@@ -309,6 +341,34 @@ bool JuceFxEngine::pumpEditorQueue() {
}
}
}
// 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;
}
@@ -382,6 +442,7 @@ void JuceFxEngine::shutdown() {
}
impl_->editors.clear();
}
impl_->retryOpens.clear(); // M14
if (impl_->prepared) {
impl_->graph.releaseResources();
impl_->graph.clear();
@@ -495,9 +556,16 @@ 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" << std::endl;
else
std::cerr << "[JuceFxEngine] slot " << i << " preset FAILED" << std::endl;
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;
}
}
}
auto node = g.addNode(std::unique_ptr<juce::AudioProcessor>(std::move(inst)));
+617 -245
View File
@@ -1,245 +1,617 @@
// 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>
#endif
#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <functional>
#include <iostream>
#include <mutex>
#include <thread>
#include <utility>
#include <vector>
namespace {
// 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);
}
// 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); }
const uint32_t slot;
private:
JuceFxGuiHost& host_;
};
} // 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)
};
JuceFxGuiHost::JuceFxGuiHost(JuceFxEngine& engine)
: impl_(std::make_unique<Impl>()), engine_(engine) {}
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::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_;
// 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);
w->setVisible(engine_.isEditorVisible(s));
if (auto* peer = w->getPeer()) {
const auto hwnd = (uintptr_t)peer->getNativeHandle();
std::cerr << "[JuceFxGuiHost] window slot " << (int)s
<< " hwnd=0x" << std::hex << hwnd << std::dec
<< " editor=" << ed->getWidth() << "x" << ed->getHeight()
<< std::endl;
}
windows.push_back(std::move(w));
}
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 — 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 tickMs = std::chrono::milliseconds(30);
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();
if (now - lastTick >= tickMs) {
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;
}
// 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 — 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 tickMs = std::chrono::milliseconds(30);
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();
if (now - lastTick >= tickMs) {
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;
+9
View File
@@ -49,6 +49,15 @@ public:
// 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();
+103
View File
@@ -25,6 +25,7 @@
#include <fstream>
#include <iostream>
#include <iterator>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
@@ -97,10 +98,97 @@ static std::string base64Encode(const std::string& in) {
}
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);
@@ -220,6 +308,16 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName,
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)
@@ -247,6 +345,11 @@ int run_juce_fx_loop(const std::string& jobPath, const std::string& shmName,
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;
+141 -35
View File
@@ -279,23 +279,7 @@ 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;
@@ -637,6 +621,110 @@ 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;
@@ -764,7 +852,9 @@ public:
#endif
}
// Apply preset: base64 of a raw .vstpreset FILE (VST3 magic + chunk list)
// OR of the bridge state blob [4B BE compLen][comp][4B BE ctrlLen][ctrl].
// 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].
bool applyPreset(const std::string& b64) {
#ifndef HAVE_VST3SDK
(void)b64;
@@ -795,8 +885,10 @@ 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 {
// Bridge state blob.
// Legacy bridge state blob (backward-compatible).
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];
@@ -832,9 +924,11 @@ public:
#endif
}
// Capture the plugin's CURRENT state (component + edit controller) as the
// 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.
// 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.
void captureState(std::string& out) {
#ifndef HAVE_VST3SDK
(void)out;
@@ -854,18 +948,28 @@ public:
ctrl.assign(stream.getData(), stream.getData() + stream.getSize());
}
if (comp.empty() && ctrl.empty()) return;
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);
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';
#endif
}
// Live param change (realtime chain, NO restart): address by lowercase
@@ -1960,7 +2064,9 @@ 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;
const DWORD exStyle = offscreen ? (WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE) : 0;
// 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);
// 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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.