feat: native host bridge integration — C++ bridge, Rust SHM, JS routing, build scripts, docs

- native_bridge/: InstrumentEngineManager multi-channel, sample-accurate, CC/program/pitchbend, transport, Vst3Instrument stub (HAVE_VST3SDK)
- src-tauri: shm.rs, bridge spawn + audio pump + health monitor, open_vst_gui, externalBin, commands
- app: UnifiedMidiRouter, NativeBridgeService, bridgeAudioNode, audioRoutingEngine, Plugin Manager UI, Bridge/WASM indicator, set_position sync
- build: 3 ps1 (force-added, build/ ignored), verify_bundle --check-bridge, CI workflow
- docs: TASKS.md, TEST_NOTES.md (Windows verify checklist), install/report updates
This commit is contained in:
locpham
2026-08-11 23:37:29 +07:00
parent aae0b05473
commit c3368d0a91
37 changed files with 2970 additions and 80 deletions
+118 -1
View File
@@ -1,7 +1,7 @@
import os, sys, uuid, json, tempfile, subprocess, time as _time, threading
import numpy as np
import soundfile as sf
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Header
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Header, Query
from fastapi.responses import FileResponse
from pydantic import BaseModel
from typing import Optional, Any
@@ -1178,3 +1178,120 @@ async def render_project(
return {"url": f"/static/audio/processed/{os.path.basename(result_path)}", "path": result_path}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Render failed: {str(e)}")
# ── E1/E2: NATIVE HOST BRIDGE (daw_vst_bridge) ──────────────────────────────
# IPC dir giống _pick_dir_via_tauri_bridge: %APPDATA%/SonicForgeDAW/ipc — Rust
# (src-tauri) tạo, engine chỉ đọc/ghi file request/response.
BRIDGE_IPC_DIR = os.path.join(os.environ.get("APPDATA") or os.path.expanduser("~"), "SonicForgeDAW", "ipc")
def _bridge_ipc_dir() -> Optional[str]:
if os.path.isdir(BRIDGE_IPC_DIR):
return BRIDGE_IPC_DIR
return None
@router.get("/bridge/status")
async def bridge_status(current_user: dict = Depends(get_current_user)):
"""E1: trạng thái native bridge. Rust ghi bridge_status (JSON) khi spawn/
health-check; nếu chưa có → probe tail bridge.log (dòng started/exists)."""
ipc = _bridge_ipc_dir()
if not ipc:
return {"connected": False, "reason": "no_ipc_dir"}
st = os.path.join(ipc, "bridge_status")
if os.path.exists(st):
try:
with open(st, "r", encoding="utf-8") as fh:
raw = fh.read() or "{}"
try:
data = json.loads(raw)
except Exception:
data = {}
data.setdefault("connected", True)
return data
except Exception:
pass
log = os.path.normpath(os.path.join(ipc, "..", "logs", "bridge.log"))
if os.path.exists(log):
try:
with open(log, "r", encoding="utf-8", errors="ignore") as fh:
tail = fh.read().splitlines()[-5:]
joined = "\n".join(tail).lower()
return {"connected": ("started" in joined or "exists=true" in joined), "log_tail": tail}
except Exception:
pass
return {"connected": False, "reason": "no_status_no_log"}
class BridgeLoadRequest(BaseModel):
name: str # tên asset hiển thị (sf_xxx / Vital.vst3 / ...)
path: Optional[str] = None # path tuyệt đối nếu đã biết
instrumentType: str = "SF2" # 'VST3'|'VST2'|'SF2'|'SF3'|'SFZ'
channel: Optional[int] = None
def _resolve_bridge_asset(name: str, instrument_type: str, path: Optional[str]) -> Optional[str]:
if path and os.path.exists(path):
return path
base = os.path.basename((path or name).replace("\\", "/"))
base_noext = os.path.splitext(base)[0].lower()
# sf2/sf3/sfz: storage/soundfonts + system + user plugin_dirs
for base_dir in [UPLOAD_SF_DIR, SYSTEM_SF_DIR]:
if not os.path.isdir(base_dir):
continue
for fname in os.listdir(base_dir):
cand = os.path.join(base_dir, fname)
if fname.lower().endswith((".sf2", ".sf3", ".sfz")) and \
(fname.lower() == base.lower() or os.path.splitext(fname)[0].lower() == base_noext):
return cand
ext = (".vst3" if instrument_type.upper() == "VST3" else
".vst2" if instrument_type.upper() == "VST2" else
".sfz" if instrument_type.upper() == "SFZ" else None)
if ext:
try:
from app.core.vst_engine import _load_user_plugin_dirs
dirs = _load_user_plugin_dirs() or []
except Exception:
dirs = []
if not dirs:
dirs = [settings.VST_DIR, settings.SOUNDFONT_DIR]
for base_dir in dirs:
if not os.path.isdir(base_dir):
continue
for root, _, files in os.walk(base_dir):
for fn in files:
if fn.lower().endswith(ext) and \
(fn.lower() == base.lower() or os.path.splitext(fn)[0].lower() == base_noext):
return os.path.join(root, fn)
return None
@router.post("/bridge/load")
async def bridge_load(req: BridgeLoadRequest, current_user: dict = Depends(get_current_user)):
"""E2: resolve path thật của asset rồi (a) ghi bridge_load.request cho Rust
watcher (nếu có) và (b) trả path để JS gọi invoke load_native_instrument
trực tiếp — pipeline hoạt động ngay, không chờ watcher."""
resolved = _resolve_bridge_asset(req.name, req.instrumentType, req.path)
if not resolved:
raise HTTPException(status_code=404, detail=f"Không tìm thấy asset: {req.name}")
wrote_request = False
ipc = _bridge_ipc_dir()
if ipc:
try:
req_file = os.path.join(ipc, "bridge_load.request")
if os.path.exists(req_file):
os.remove(req_file)
with open(req_file, "w", encoding="utf-8") as fh:
json.dump({"path": resolved, "type": req.instrumentType, "channel": req.channel}, fh)
wrote_request = True
except Exception:
pass
return {"success": True, "path": resolved, "type": req.instrumentType, "ipc_request_written": wrote_request}
@router.get("/bridge/log")
async def bridge_log(lines: int = Query(100, ge=1, le=2000), current_user: dict = Depends(get_current_user)):
"""E5: tail bridge.log cho UI debug. Log Rust ghi tại %APPDATA%/SonicForgeDAW/logs/bridge.log."""
log = os.path.normpath(os.path.join(BRIDGE_IPC_DIR, "..", "logs", "bridge.log"))
if not os.path.exists(log):
return {"lines": [], "path": log, "exists": False}
try:
with open(log, "r", encoding="utf-8", errors="ignore") as fh:
all_lines = fh.read().splitlines()
return {"lines": all_lines[-lines:], "path": log, "exists": True}
except Exception as e:
return {"lines": [], "path": log, "exists": False, "error": str(e)}