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:
2026-08-09 13:05:53 +00:00
parent 5ae4fd6149
commit cc8b286f6c
7 changed files with 251 additions and 36 deletions
+43 -12
View File
@@ -1,5 +1,6 @@
# SonicForge Studio VST / VSTi Engine Service
import os
import json
import numpy as np
import functools
from ctypes import c_char_p
@@ -101,19 +102,38 @@ _PLUGIN_MANAGER_INSTANCE = None
_PLUGIN_MANAGER_ARGS = None
_SF_INSTRUMENTS_CACHE = {} # sf_id → list[presets]
def _load_user_plugin_dirs() -> list:
"""Đọc plugin_dirs.json (Plugin Manager user chọn) — cùng file với
plugins.py (STORAGE_DIR/plugin_dirs.json). Không import plugins.py để
tránh vòng import (plugins.py import vst_engine)."""
try:
from app.config import settings as _st
path = os.path.join(_st.STORAGE_DIR, "plugin_dirs.json")
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
return [d for d in (data.get("plugin_dirs") or []) if d]
except Exception:
pass
return []
def get_plugin_manager(vst_dir=None, sf_dir=None, upload_sf_dir=None) -> "PluginManager":
"""Singleton: reuse PluginManager when args match, else create new.
Default dirs từ settings (env/.env/docker-compose hoặc user override)."""
Default dirs từ settings (env/.env/docker-compose hoặc user override).
VST scan gộp thêm plugin_dirs user (Plugin Manager) — nút Synth phải liệt
kê được VSTi đã scan và load_vst phải tìm thấy chúng khi render."""
from app.config import settings as _st
vst_dir = vst_dir or _st.VST_DIR
sf_dir = sf_dir or _st.SOUNDFONT_DIR
upload_sf_dir = upload_sf_dir or _st.STORAGE_DIR + "/soundfonts"
extra = _load_user_plugin_dirs()
global _PLUGIN_MANAGER_INSTANCE, _PLUGIN_MANAGER_ARGS
args = (vst_dir, sf_dir, upload_sf_dir)
args = (vst_dir, sf_dir, upload_sf_dir, tuple(extra))
if _PLUGIN_MANAGER_INSTANCE is not None and _PLUGIN_MANAGER_ARGS == args:
return _PLUGIN_MANAGER_INSTANCE
_PLUGIN_MANAGER_ARGS = args
_PLUGIN_MANAGER_INSTANCE = PluginManager(vst_dir, sf_dir, upload_sf_dir)
_PLUGIN_MANAGER_INSTANCE = PluginManager(vst_dir, sf_dir, upload_sf_dir, extra_vst_dirs=extra)
return _PLUGIN_MANAGER_INSTANCE
def load_soundfont_cached(path: str):
@@ -162,23 +182,34 @@ def release_soundfont(path: str):
_FLUID_CACHE[path] = (fl, ref - 1)
class PluginManager:
def __init__(self, vst_dir=None, sf_dir=None, upload_sf_dir=None):
def __init__(self, vst_dir=None, sf_dir=None, upload_sf_dir=None, extra_vst_dirs=None):
from app.config import settings as _st
self.vst_dir = vst_dir or _st.VST_DIR
self.sf_dir = sf_dir or _st.SOUNDFONT_DIR
self.upload_sf_dir = upload_sf_dir
# Thư mục VST thêm (plugin_dirs user scan trong Plugin Manager) —
# list_available()/load_vst phải thấy VSTi user đã scan (bug: nút
# Synth chỉ quét vst_dir env mặc định /opt/daw_engine/vst3).
self.extra_vst_dirs = [d for d in (extra_vst_dirs or []) if d]
self._sf_scan_cache = None # cache for _scan_soundfonts()
def _scan_plugins(self) -> dict:
plugins = {}
if not os.path.isdir(self.vst_dir):
return plugins
for root, dirs, files in os.walk(self.vst_dir):
for file in files:
if file.endswith(".vst3") or file.endswith(".so"):
plugin_path = os.path.join(root, file)
plugin_name = os.path.splitext(file)[0]
plugins[plugin_name] = plugin_path
for scan_dir in [self.vst_dir] + self.extra_vst_dirs:
if not scan_dir or not os.path.isdir(scan_dir):
continue
for root, dirs, files in os.walk(scan_dir):
# Windows: VST3 là FOLDER tên X.vst3 (chứa X.vst3.dll bên trong)
for d in list(dirs):
if d.lower().endswith(".vst3"):
plugins[os.path.splitext(d)[0]] = os.path.join(root, d)
for file in files:
low = file.lower()
if low.endswith(".vst3") or low.endswith(".so") or low.endswith(".dll"):
plugin_path = os.path.join(root, file)
plugin_name = os.path.splitext(file)[0]
if plugin_name not in plugins:
plugins[plugin_name] = plugin_path
return plugins
def _scan_soundfonts(self) -> list: