fix(tcp): open VSTi GUI via bridge API on LAN browser; scale vol/pan ruler wheel by deltaY

This commit is contained in:
2026-08-17 20:44:44 +07:00
parent bbde74f1a7
commit 8fbacd1d2b
5 changed files with 190 additions and 16 deletions
+134
View File
@@ -1291,3 +1291,137 @@ async def bridge_log(lines: int = Query(100, ge=1, le=2000), current_user: dict
return {"lines": all_lines[-lines:], "path": log, "exists": True}
except Exception as e:
return {"lines": [], "path": log, "exists": False, "error": str(e)}
# ── E6: LAN/standalone fallback — mở GUI VSTi qua API (không cần Tauri) ─────
# Browser client (LAN) không có window.__TAURI__ → NativeBridgeService chỉ log,
# không invoke được open_vst_gui. Engine đẩy LOAD_INSTRUMENT + OPEN_GUI thẳng
# vào SHM của daw_vst_bridge (layout SharedMemoryIPC.h: controlQueue @2836,
# stride 1040, controlQueueCount @11156 — offsets verified bằng ctypes test).
# GUI hiện trên desktop máy chạy server.
_IT_ENUM = {"VST3": 0, "VST2": 1, "SF2": 2, "SF3": 2, "SFZ": 3}
class BridgeGuiRequest(BaseModel):
name: str # plugin id / tên asset
path: Optional[str] = None # path tuyệt đối nếu đã biết
instrumentType: Optional[str] = None # VST3|VST2|SF2|SF3|SFZ; None → tự suy
channel: int = 0 # MIDI channel bridge assign/open
def _resolve_bridge_asset_any(name: str, path: Optional[str], hint: Optional[str] = None):
"""Thử resolve asset qua từng loại instrument (hint trước, rồi VST3→VST2→
SF…). Client LAN không chắc type — backend tự tìm, tránh 404 nhầm (VST2
.dll mà client đoán VST3). Trả (path, instrument_type suy từ extension)."""
order = []
if hint and hint.upper() in _IT_ENUM:
order.append(hint.upper())
for t in order + [x for x in ("VST3", "VST2", "SF2", "SF3", "SFZ") if x not in order]:
p = _resolve_bridge_asset(name, t, path)
if p:
low = p.lower()
if low.endswith(".dll"):
t = "VST2"
elif low.endswith(".sfz"):
t = "SFZ"
elif low.endswith(".sf2") or low.endswith(".sf3"):
t = "SF2"
else:
t = "VST3"
return p, t
return None, None
def _running_sonicforge_pids() -> list:
pids = []
try:
out = subprocess.run(
["powershell", "-NoProfile", "-Command",
"Get-Process sonicforge-daw,daw_engine -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id"],
capture_output=True, text=True, timeout=10).stdout
for line in out.splitlines():
line = line.strip()
if line.isdigit():
pids.append(int(line))
except Exception:
pass
return pids
def _shm_exists(name: str) -> bool:
import ctypes, ctypes.wintypes as w
k = ctypes.windll.kernel32
k.OpenFileMappingA.argtypes = [w.DWORD, w.BOOL, ctypes.c_char_p]
k.OpenFileMappingA.restype = ctypes.c_void_p
h = k.OpenFileMappingA(0xF001F, False, name.encode())
if h:
k.CloseHandle(h)
return True
return False
def _shm_push_gui(shm_name: str, path: str, enum: int, plugin_id: str, channel: int):
"""Push LOAD_INSTRUMENT (type=2) + OPEN_GUI (type=4) vào SHM bridge.
C++ xử lý async — LOAD xong mới fulfil OPEN_GUI (pending-GUI sweep)."""
import ctypes, ctypes.wintypes as w, struct
k = ctypes.windll.kernel32
k.OpenFileMappingA.argtypes = [w.DWORD, w.BOOL, ctypes.c_char_p]
k.OpenFileMappingA.restype = ctypes.c_void_p
k.MapViewOfFile.argtypes = [ctypes.c_void_p, w.DWORD, w.DWORD, w.DWORD, ctypes.c_size_t]
k.MapViewOfFile.restype = ctypes.c_void_p
k.UnmapViewOfFile.argtypes = [ctypes.c_void_p]
k.CloseHandle.argtypes = [ctypes.c_void_p]
h = k.OpenFileMappingA(0xF001F, False, shm_name.encode())
if not h:
return False, "open shm fail"
ptr = k.MapViewOfFile(h, 0xF001F, 0, 0, 0)
if not ptr:
k.CloseHandle(h)
return False, "map shm fail"
try:
for i in range(8): # zero queue trước — tránh slot cũ lọt vào count mới
ctypes.memmove(ptr + 2836 + i * 1040, b"\x00" * 1040, 1040)
# LOAD_INSTRUMENT: type=2, arg0=InstrumentType, arg1=0 (Rust bỏ qua), channel, arg2=path
b = path.encode("utf-8")[:1023] + b"\x00"
ctypes.memmove(ptr + 2836, struct.pack("IIII", 2, enum, 0, channel), 16)
ctypes.memmove(ptr + 2836 + 16, b, len(b))
# OPEN_GUI: type=4, arg0=0, arg1=0 (hwnd=0 → bridge tự tạo native window), channel, arg2=plugin id
pb = plugin_id.encode("utf-8")[:1023] + b"\x00"
off = 2836 + 1040
ctypes.memmove(ptr + off, struct.pack("IIII", 4, 0, 0, channel), 16)
ctypes.memmove(ptr + off + 16, pb, len(pb))
ctypes.memmove(ptr + 11156, struct.pack("I", 2), 4) # controlQueueCount = 2
return True, None
finally:
k.UnmapViewOfFile(ptr)
k.CloseHandle(h)
@router.post("/bridge/gui")
async def bridge_gui(req: BridgeGuiRequest, current_user: dict = Depends(get_current_user)):
"""E6: mở native GUI VSTi trên máy chạy server — fallback cho browser LAN
không có Tauri. Pipeline giống invoke open_vst_gui: LOAD_INSTRUMENT rồi
OPEN_GUI qua SHM daw_vst_bridge."""
resolved, itype = _resolve_bridge_asset_any(req.name, req.path, req.instrumentType)
if not resolved:
raise HTTPException(status_code=404, detail=f"Không tìm thấy asset: {req.name}")
enum = _IT_ENUM.get(itype.upper(), 0)
shm_name = None
ipc = _bridge_ipc_dir()
if ipc:
st = os.path.join(ipc, "bridge_status")
if os.path.exists(st):
try:
with open(st, "r", encoding="utf-8") as fh:
data = json.loads(fh.read() or "{}")
shm_name = data.get("shm_name")
except Exception:
pass
if not shm_name:
for pid in _running_sonicforge_pids():
cand = f"SonicForge_DAW_IPC_{pid}"
if _shm_exists(cand):
shm_name = cand
break
if not shm_name:
raise HTTPException(status_code=503,
detail="Bridge chưa chạy (không tìm thấy SHM) — hãy mở app desktop trên máy chạy server trước")
ch = req.channel & 0xF
ok, err = _shm_push_gui(shm_name, resolved, enum, req.name, ch)
if not ok:
raise HTTPException(status_code=503, detail=f"Không ghi được SHM {shm_name}: {err}")
return {"success": True, "path": resolved, "type": itype, "enum": enum,
"channel": ch, "shm": shm_name}