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:
+97
-4
@@ -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):
|
||||
|
||||
+87
-3
@@ -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,9 +619,13 @@ 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
|
||||
else:
|
||||
self._restarts += 1
|
||||
_log.warning("[fxrt] bridge chết rc=%s — auto restart #%d", rc, self._restarts)
|
||||
try:
|
||||
|
||||
+644
-83
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -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' }),
|
||||
|
||||
@@ -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--;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -20,21 +20,29 @@
|
||||
#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:
|
||||
@@ -50,16 +58,92 @@ public:
|
||||
// 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
|
||||
@@ -73,10 +157,163 @@ struct JuceFxGuiHost::Impl {
|
||||
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_(std::make_unique<Impl>()), engine_(engine) {
|
||||
impl_->anchors_.assign(kAnchorSlots, Impl::Anchor{});
|
||||
}
|
||||
|
||||
JuceFxGuiHost::~JuceFxGuiHost() { stop(); }
|
||||
|
||||
@@ -138,6 +375,31 @@ 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)
|
||||
@@ -149,6 +411,64 @@ void JuceFxGuiHost::tick() {
|
||||
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.
|
||||
@@ -174,16 +494,68 @@ void JuceFxGuiHost::reconcile() {
|
||||
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();
|
||||
// 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
|
||||
<< " hwnd=0x" << std::hex << hwnd << std::dec
|
||||
<< " 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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user