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:
+118
-1
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user