feat: native folder picker (Explorer) + Synth liet ke VSTi da scan + fix VU leak (ban 1.1.4)
1) Folder picker dung WINDOW EXPLORER (khong prompt nhap tay):
- src-tauri/src/lib.rs: IPC bridge — thread watcher ipc/pick_dir.request
-> run_on_main_thread -> dialog().file().blocking_pick_folder()
(tauri-plugin-dialog = IFileDialog/Explorer) -> ghi pick_dir.response;
ghi marker tauri_bridge_ready luc setup.
- app/api/v1/plugins.py: POST /pick-dir (def sync -> threadpool, khong
auth) — uu tien Tauri bridge, fallback PowerShell FolderBrowserDialog
(Win) / osascript (macOS) / zenity-kdialog (Linux).
- app.jsx pickPluginFolder: 1) pickPluginDir (native) -> 2) __TAURI__
invoke -> 3) in-app browser -> 4) prompt (cuoi cung).
2) Nut Synth liet ke VSTi da scan (truoc day rong):
- Root: list_available() chi quet settings.VST_DIR (mac dinh
/opt/daw_engine/vst3) trong khi Plugin Manager scan plugin_dirs user.
- plugins.py list_plugins: gop _scan_vst_in_dirs(plugin_dirs) (file
.vst3/.dll/.so + folder X.vst3 Windows).
- vst_engine.py: PluginManager.extra_vst_dirs + _scan_plugins quet them
(ca folder .vst3) + get_plugin_manager doc plugin_dirs.json -> load_vst
tim thay plugin user scan khi render.
3) VU meter leak: section-tab play -> sang MAIN SESSION -> track MAIN van
animate theo am section.
- Root: VU tick fallback _sub_ (sub-node section co analyser) chay cho
ca canvas MAIN; startSubTabPlayback ghi node o key track MAIN.
- Fix: fallback _sub_ chi ap dung cho canvas SECTION (isSessVu); 2 trigger
piano-roll them prefix _sess_ theo st.parent_tab_id.
Verify: 86 tests pass; engine frozen STARTUP 1.35s, 1 engine, 0 spawn '-c';
pick-dir IPC mock tra dung path; scan VST user dirs OK.
This commit is contained in:
+124
-2
@@ -1,4 +1,4 @@
|
||||
import os, uuid, json, tempfile
|
||||
import os, sys, uuid, json, tempfile, subprocess, time as _time
|
||||
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
@@ -156,7 +156,129 @@ def get_scanner():
|
||||
async def list_plugins(current_user: dict = Depends(get_current_user)):
|
||||
d = _effective_dirs()
|
||||
pm = PluginManager(vst_dir=d["vst_dir"], sf_dir=d["soundfont_dir"], upload_sf_dir=UPLOAD_SF_DIR)
|
||||
return pm.list_available()
|
||||
avail = pm.list_available()
|
||||
# Gộp VST từ plugin_dirs user đã scan — list_available() CHỈ quét vst_dir
|
||||
# env (mặc định /opt/daw_engine/vst3) → Synth dropdown không thấy VSTi mà
|
||||
# Plugin Manager đã scan trong thư mục user chọn (bug: nút Synth rỗng).
|
||||
extra = _scan_vst_in_dirs(d["plugin_dirs"])
|
||||
by_id = {v["id"]: v for v in avail["vst_instruments"]}
|
||||
for v in extra:
|
||||
by_id.setdefault(v["id"], v)
|
||||
avail["vst_instruments"] = list(by_id.values())
|
||||
return avail
|
||||
|
||||
|
||||
def _scan_vst_in_dirs(dirs: list) -> list:
|
||||
"""Walk các thư mục (plugin_dirs user) → danh sách VST giống /scan:
|
||||
file .vst3/.dll/.so (VST3 folder Windows = .vst3.dll bên trong)."""
|
||||
found = {}
|
||||
for d in dirs:
|
||||
if not d or not os.path.isdir(d):
|
||||
continue
|
||||
for root, _dirs, files in os.walk(d):
|
||||
for f in files:
|
||||
low = f.lower()
|
||||
if low.endswith(".vst3") or low.endswith(".dll") or low.endswith(".so"):
|
||||
name = os.path.splitext(f)[0]
|
||||
if name not in found:
|
||||
found[name] = {
|
||||
"id": name, "name": name,
|
||||
"type": "VST3" if low.endswith(".vst3") else "VST2",
|
||||
"path": os.path.join(root, f),
|
||||
}
|
||||
return list(found.values())
|
||||
|
||||
|
||||
# ── Native folder picker (user yêu cầu: dùng Windows Explorer, không phải
|
||||
# nhập tay) ─────────────────────────────────────────────────────────────
|
||||
PICK_DIR_TIMEOUT = 120 # user có thể mở dialog lâu
|
||||
|
||||
|
||||
def _pick_dir_via_tauri_bridge() -> Optional[str]:
|
||||
"""Tauri shell (Rust watcher trong lib.rs) mở NATIVE dialog (IFileDialog /
|
||||
Explorer) qua file IPC: engine ghi pick_dir.request → Rust mở dialog →
|
||||
ghi pick_dir.response. Trả None nếu bridge không tồn tại (chạy standalone)."""
|
||||
root = os.environ.get("APPDATA") or os.path.expanduser("~")
|
||||
ipc = os.path.join(root, "SonicForgeDAW", "ipc")
|
||||
if not os.path.isdir(ipc):
|
||||
return None
|
||||
# Marker do Rust viết lúc setup — bridge chỉ có trong app Tauri desktop
|
||||
if not os.path.exists(os.path.join(ipc, "tauri_bridge_ready")):
|
||||
return None
|
||||
req = os.path.join(ipc, "pick_dir.request")
|
||||
resp = os.path.join(ipc, "pick_dir.response")
|
||||
try:
|
||||
for f in (req, resp):
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
with open(req, "w", encoding="utf-8") as fh:
|
||||
fh.write("1")
|
||||
deadline = _time.time() + PICK_DIR_TIMEOUT
|
||||
while _time.time() < deadline:
|
||||
if os.path.exists(resp):
|
||||
try:
|
||||
with open(resp, "r", encoding="utf-8") as fh:
|
||||
val = fh.read().strip()
|
||||
finally:
|
||||
os.remove(resp)
|
||||
return val or None
|
||||
_time.sleep(0.1)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _pick_dir_native_engine() -> Optional[str]:
|
||||
"""Fallback khi không có Tauri bridge: PowerShell FolderBrowserDialog
|
||||
(Windows), osascript (macOS), zenity/kdialog (Linux)."""
|
||||
if os.name == "nt":
|
||||
ps = (
|
||||
"Add-Type -AssemblyName System.Windows.Forms; "
|
||||
"$f = New-Object System.Windows.Forms.FolderBrowserDialog; "
|
||||
"$f.Description = 'Chọn thư mục chứa VST / SoundFont'; "
|
||||
"if ($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { Write-Output $f.SelectedPath }"
|
||||
)
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["powershell", "-NoProfile", "-STA", "-Command", ps],
|
||||
capture_output=True, text=True, timeout=PICK_DIR_TIMEOUT,
|
||||
)
|
||||
return r.stdout.strip() or None
|
||||
except Exception:
|
||||
return None
|
||||
if sys.platform == "darwin":
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["osascript", "-e",
|
||||
'POSIX path of (choose folder with prompt "Chọn thư mục plugin")'],
|
||||
capture_output=True, text=True, timeout=PICK_DIR_TIMEOUT,
|
||||
)
|
||||
return r.stdout.strip() or None
|
||||
except Exception:
|
||||
return None
|
||||
for cmd in (["zenity", "--file-selection", "--directory"],
|
||||
["kdialog", "--getexistingdirectory", os.path.expanduser("~")]):
|
||||
try:
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=PICK_DIR_TIMEOUT)
|
||||
if r.returncode == 0:
|
||||
p = r.stdout.strip()
|
||||
if p:
|
||||
return p
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
@router.post("/pick-dir")
|
||||
def pick_plugin_directory():
|
||||
"""Mở NATIVE folder picker. Không cần auth (desktop local, chỉ mở dialog).
|
||||
Trả {"path": "<thư mục>"} hoặc {"path": None} (hủy/không có dialog).
|
||||
Định nghĩa def (sync) → FastAPI chạy trong threadpool — không block loop
|
||||
trong lúc user chọn thư mục (có thể mất phút)."""
|
||||
path = _pick_dir_via_tauri_bridge()
|
||||
if path is None:
|
||||
path = _pick_dir_native_engine()
|
||||
return {"path": path}
|
||||
|
||||
|
||||
@router.get("/default-soundfonts")
|
||||
|
||||
Reference in New Issue
Block a user