fix(fx-list): MASTERING PANEL khong hien thi VST FX da scan trong Plugin Manager (cache _fx_scan_all ket [] vinh vien)

- vst_engine: list_scan_all cache co TTL 300s (FX_SCAN_TTL) + chi cache khi
  find_fx_bridge_exe() co exe (thieu bridge -> tra [] NHUNG khong cache de
  lan sau thu lai) — truoc day ket [] vi mui gio, MASTERING PANEL rong.
- vst_engine: them invalidate_fx_scan() clear cache scan VST.
- plugins.py: POST /scan (Plugin Manager Scan) goi _invalidate_fx_scan_cache()
  -> MASTERING PANEL lan mo ke tiep quet lai voi dirs moi; response them
  fx_cache_invalidated.
- app.jsx: MasteringModal/FXRackModal load listFx() lai moi lan mo modal
  (deps [isOpen]/[track.id]) + loading state; VstPluginPicker nhan prop
  loading -> hien 'Dang quet VST FX...' thay vi 'Khong co VST FX'.
- precompiled rebuild (LF) + index.html ?v=202608220330; dist sync.
- tests: +4 test cache TTL/invalidate/no-bridge (TestFxScanCache).
This commit is contained in:
2026-08-22 03:34:33 +00:00
parent ac5a171464
commit 34d43f5018
7 changed files with 147 additions and 23 deletions
+18
View File
@@ -41,6 +41,23 @@ def _save_plugin_dirs(dirs: dict):
with open(PLUGIN_DIRS_FILE, "w", encoding="utf-8") as f:
json.dump(dirs, f, indent=2)
def _invalidate_fx_scan_cache() -> bool:
"""Clear cache scan VST FX của PluginManager singleton.
Plugins Manager bấm Scan (POST /scan) quét dirs MỚI nhưng trước đây
không clear `_fx_scan_all` của vst_engine → MASTERING PANEL / FX Rack
vẫn trả cache cũ (thường là [] kẹt từ lúc app khởi động) → VST FX đã
load trong Plugin Manager KHÔNG hiển thị. Gọi invalidate để lần mở panel
kế tiếp quét lại với dirs mới.
"""
try:
from app.core.vst_engine import get_plugin_manager
get_plugin_manager().invalidate_fx_scan()
return True
except Exception:
return False
def _effective_dirs() -> dict:
"""Env/.env (Docker) là base; user dirs (file) override nếu khai báo.
@@ -141,6 +158,7 @@ async def scan_plugin_dirs(background_tasks: BackgroundTasks = None,
"vst_count": len(vst_found),
"soundfonts": sf_found,
"soundfont_count": len(sf_found),
"fx_cache_invalidated": _invalidate_fx_scan_cache(),
}
_inspector = None
+26 -2
View File
@@ -2,6 +2,7 @@
import os
import json
import re
import time as _time
import numpy as np
import functools
import threading
@@ -300,6 +301,17 @@ class PluginManager:
self._sf_scan_cache = None # cache for _scan_soundfonts()
self._fx_scan_cache = None # cache for list_fx()
self._fx_scan_all = None # cache for list_scan_all()
self._fx_scan_all_ts = 0.0 # timestamp cache (TTL — tránh kẹt [] vĩnh viễn)
FX_SCAN_TTL = 300.0 # giây: cache scan VST tự hết hạn → MASTERING PANEL
# không bao giờ kẹt danh sách cũ/rỗng; refresh=true vẫn ép scan ngay.
def invalidate_fx_scan(self):
"""Clear cache scan VST FX — Plugins Manager bấm Scan phải gọi để
MASTERING PANEL / FX Rack thấy plugin mới ngay lần mở kế tiếp."""
self._fx_scan_all = None
self._fx_scan_all_ts = 0.0
self._fx_scan_cache = None
def _scan_plugins(self) -> dict:
plugins = {}
@@ -500,8 +512,16 @@ class PluginManager:
Clarity) — không xếp bừa vào instrument. `/available` dùng entry
unknown để LOẠI plugin đó khỏi nút Synth (không biết loại → không
hiện là instrument), còn list_fx() chỉ lấy is_fx=true.
Cache có TTL (FX_SCAN_TTL): nếu cache cũ hơn TTL → tự scan lại. QUAN
TRỌNG: chỉ cache khi bridge exe tồn tại — nếu chưa tìm thấy bridge
(vd app mới khởi động, sidecar chưa ready) thì KHÔNG lưu kết quả rỗng
vào cache, để lần gọi sau thử lại (trước đây kẹt [] vĩnh viễn →
MASTERING PANEL không hiển thị VST FX đã scan trong Plugin Manager).
"""
if not refresh and self._fx_scan_all is not None:
now = _time.time()
if (not refresh and self._fx_scan_all is not None
and (now - self._fx_scan_all_ts) < self.FX_SCAN_TTL):
return self._fx_scan_all
from app.core.native_render import find_fx_bridge_exe
exe = find_fx_bridge_exe()
@@ -542,7 +562,11 @@ class PluginManager:
continue
seen.add(e["path"])
uniq.append(e)
self._fx_scan_all = uniq
# Chỉ cache khi bridge tồn tại: kết quả (kể cả rỗng — dir không có
# FX) là đáng tin; khi thiếu bridge thì trả [] nhưng KHÔNG cache.
if exe:
self._fx_scan_all = uniq
self._fx_scan_all_ts = now
return uniq
@staticmethod
+13 -8
View File
@@ -12348,7 +12348,7 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
{ id: 'distortion', label: 'Distortion / Saturation' },
{ id: 'utility', label: 'Utility' }
];
const VstPluginPicker = ({ open, onClose, plugins, onPick }) => {
const VstPluginPicker = ({ open, onClose, plugins, onPick, loading }) => {
const [cat, setCat] = React.useState('all');
React.useEffect(() => { if (open) setCat('all'); }, [open]);
if (!open) return null;
@@ -12366,8 +12366,9 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
))}
</div>
<div className="flex-1 overflow-y-auto oz-scrollbar grid grid-cols-2 gap-2">
{list.length === 0 && <div className="col-span-2 text-[11px] text-slate-500 font-mono py-10 text-center">Không có VST FX trong mục này.</div>}
{list.map(f => (
{loading && <div className="col-span-2 text-[11px] text-amber-300 font-mono py-10 text-center animate-pulse">Đang quét VST FX...</div>}
{!loading && list.length === 0 && <div className="col-span-2 text-[11px] text-slate-500 font-mono py-10 text-center">Không có VST FX trong mục này.</div>}
{!loading && list.map(f => (
<button key={f.path} onClick={() => { onPick(f); onClose(); }} className="p-2 bg-slate-950 hover:bg-slate-800 border border-slate-800 rounded-lg text-left space-y-0.5 transition-colors">
<div className="text-[11px] font-bold text-teal-300 flex items-center gap-1.5"><i data-lucide="plug" className="w-3 h-3"></i> {f.name || f.id}</div>
<div className="text-[9px] text-slate-500 font-mono truncate">{f.path}</div>
@@ -12404,9 +12405,11 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
const scopeStateRef = React.useRef({ L: null, R: null, head: 0, len: 0, tmpL: null, tmpR: null, freq: null });
// VST FX (native_bridge --render-fx) — list VST3 effect plugins + preview
const [fxList, setFxList] = React.useState([]);
const [fxLoading, setFxLoading] = React.useState(false);
React.useEffect(() => {
if (window.SonicAPI && window.SonicAPI.listFx) {
window.SonicAPI.listFx().then(d => setFxList((d && d.plugins) || [])).catch(() => setFxList([]));
setFxLoading(true);
window.SonicAPI.listFx().then(d => setFxList((d && d.plugins) || [])).catch(() => setFxList([])).finally(() => setFxLoading(false));
}
}, [track && track.id]);
const previewAudioRef = React.useRef(null);
@@ -12994,7 +12997,7 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
</div>
</div>
)}
<VstPluginPicker open={vstPickerOpen} onClose={() => setVstPickerOpen(false)} plugins={fxList} onPick={(f) => addVst(f.path)} />
<VstPluginPicker open={vstPickerOpen} onClose={() => setVstPickerOpen(false)} plugins={fxList} loading={fxLoading} onPick={(f) => addVst(f.path)} />
</div>
);
};
@@ -13108,11 +13111,13 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
// VST FX masterbus (native_bridge --render-fx) — list VST3 effect plugins + preview
const [masterFxList, setMasterFxList] = React.useState([]);
const [masterFxLoading, setMasterFxLoading] = React.useState(false);
React.useEffect(() => {
if (window.SonicAPI && window.SonicAPI.listFx) {
window.SonicAPI.listFx().then(d => setMasterFxList((d && d.plugins) || [])).catch(() => setMasterFxList([]));
setMasterFxLoading(true);
window.SonicAPI.listFx().then(d => setMasterFxList((d && d.plugins) || [])).catch(() => setMasterFxList([])).finally(() => setMasterFxLoading(false));
}
}, []);
}, [isOpen]);
const previewAudioRef = React.useRef(null);
const [addMasterVstSel, setAddMasterVstSel] = React.useState('');
const [masterVstPickerOpen, setMasterVstPickerOpen] = React.useState(false);
@@ -14364,7 +14369,7 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
</div>
</div>
)}
<VstPluginPicker open={masterVstPickerOpen} onClose={() => setMasterVstPickerOpen(false)} plugins={masterFxList} onPick={(f) => addVstToChain(f.path)} />
<VstPluginPicker open={masterVstPickerOpen} onClose={() => setMasterVstPickerOpen(false)} plugins={masterFxList} loading={masterFxLoading} onPick={(f) => addVstToChain(f.path)} />
</div>
);
};
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -51,7 +51,7 @@
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/services/chordTheory.js?v=202608211300"></script>
<script src="/static/js/app.precompiled.js?v=202608212200" defer></script>
<script src="/static/js/app.precompiled.js?v=202608220330" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+66
View File
@@ -54,6 +54,72 @@ class TestPluginManager:
assert PluginManager.validate_sf2_header(b'RIFF') is False
class TestFxScanCache:
"""list_scan_all cache TTL + invalidate — fix MASTERING PANEL không hiển thị
VST FX đã scan trong Plugin Manager (cache kẹt [] vĩnh viễn)."""
SCAN_JSON = json.dumps({"plugins": [
{"path": "C:\\VST3\\Ozone Pro.vst3", "name": "Ozone Pro",
"type": "VST3", "is_fx": True, "is_instrument": False},
]})
def _pm(self, tmp_path):
d = str(tmp_path)
return PluginManager(vst_dir=d, sf_dir=d, extra_vst_dirs=[])
def test_scan_caches_with_bridge(self, tmp_path):
pm = self._pm(tmp_path)
with patch("app.core.native_render.find_fx_bridge_exe", return_value="fx_bridge.exe"), \
patch("subprocess.run", return_value=MagicMock(
returncode=0, stdout=self.SCAN_JSON, stderr="")) as mrun:
r1 = pm.list_scan_all()
assert len(r1) == 1 and r1[0]["name"] == "Ozone Pro"
assert pm._fx_scan_all is not None and len(pm._fx_scan_all) == 1
# Lần 2 trong TTL → KHÔNG chạy lại bridge
mrun.reset_mock()
r2 = pm.list_scan_all()
assert r2 == r1
mrun.assert_not_called()
def test_scan_no_bridge_does_not_cache_empty(self, tmp_path):
"""Bridge chưa tồn tại → trả [] nhưng KHÔNG cache → lần sau thử lại
(trước đây cache [] vĩnh viễn → MASTERING PANEL trống)."""
pm = self._pm(tmp_path)
with patch("app.core.native_render.find_fx_bridge_exe", return_value=""):
assert pm.list_scan_all() == []
assert pm._fx_scan_all is None # KHÔNG cache rỗng
# Có bridge → scan thật chạy
with patch("app.core.native_render.find_fx_bridge_exe", return_value="fx_bridge.exe"), \
patch("subprocess.run", return_value=MagicMock(
returncode=0, stdout=self.SCAN_JSON, stderr="")):
r = pm.list_scan_all()
assert len(r) == 1
def test_scan_ttl_expiry_rescans(self, tmp_path):
pm = self._pm(tmp_path)
with patch("app.core.native_render.find_fx_bridge_exe", return_value="fx_bridge.exe"), \
patch("subprocess.run", return_value=MagicMock(
returncode=0, stdout=self.SCAN_JSON, stderr="")) as mrun:
pm.list_scan_all()
mrun.assert_called_once()
# Giả TTL đã hết hạn → gọi lại phải scan
pm._fx_scan_all_ts -= (pm.FX_SCAN_TTL + 1)
pm.list_scan_all()
assert mrun.call_count == 2
def test_invalidate_fx_scan(self, tmp_path):
pm = self._pm(tmp_path)
with patch("app.core.native_render.find_fx_bridge_exe", return_value="fx_bridge.exe"), \
patch("subprocess.run", return_value=MagicMock(
returncode=0, stdout=self.SCAN_JSON, stderr="")):
pm.list_scan_all()
assert pm._fx_scan_all is not None
pm.invalidate_fx_scan()
assert pm._fx_scan_all is None
assert pm._fx_scan_all_ts == 0.0
assert pm._fx_scan_cache is None
class TestMidiNoteToFreq:
def test_a4_440(self):
assert midi_note_to_freq(69) == 440.0
+11
View File
@@ -1,3 +1,14 @@
### [2026-08-22] Task: FIX MASTERING PANEL — VST FX đã scan trong Plugin Manager không hiển thị (cache kẹt [] vĩnh viễn)
- **Báo cáo user:** MASTERING PANEL (Master FX Chain) khi load VST FX plugin không hiển thị các VST FX đã load trong Plugin Manager (window 192.168.1.24).
- **Chẩn đoán (probe thật trên 192.168.1.24):** `GET /api/v1/plugins/fx` → `FX_COUNT=0 time=0s` (cache rỗng); `GET /api/v1/plugins/fx?refresh=true` → 44 VST FX sau 54s (bridge scan chạy TỐT). `/available` → 93 plugin NHƯNG bị xếp nhầm thành instrument (Ozone/Neutron/Insight... là FX) vì `list_scan_all()` trả cache rỗng → block set rỗng → không loại được FX.
- **Root cause:** `PluginManager.list_scan_all()` cache `_fx_scan_all` KHÔNG có TTL và set kết quả rỗng `[]` ngay cả khi bridge chưa tồn tại (app mới khởi động) → mọi lần gọi sau (MASTERING PANEL / FX Rack, không refresh) trả `[]` vĩnh viễn; `POST /plugins/scan` (Plugin Manager Scan) cũng không clear cache này.
- **FIX:**
(1) `app/core/vst_engine.py`: thêm `FX_SCAN_TTL = 300s` + `_fx_scan_all_ts` — cache tự hết hạn; chỉ cache khi `find_fx_bridge_exe()` trả exe (thiếu bridge → trả [] KHÔNG cache để lần sau thử lại); thêm `invalidate_fx_scan()`.
(2) `app/api/v1/plugins.py`: `POST /scan` gọi `_invalidate_fx_scan_cache()` (clear `_fx_scan_all`/`_fx_scan_cache` của singleton) → MASTERING PANEL lần mở kế tiếp quét lại với dirs mới; response thêm `fx_cache_invalidated`.
(3) `app/static/js/app.jsx`: `MasteringModal`/`FXRackModal` load `listFx()` lại mỗi lần mở (deps `[isOpen]` / `[track.id]` — trước `[]` chỉ chạy 1 lần lúc mount) + loading state; `VstPluginPicker` nhận prop `loading` → hiển thị "Đang quét VST FX..." thay vì "Không có VST FX" trong lúc scan ~50s.
- **Các file ảnh hưởng:** `app/core/vst_engine.py`, `app/api/v1/plugins.py`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild babel, LF), `app/templates/index.html` (?v=202608220330), `tests/test_vst_engine.py` (+4 test cache TTL/invalidate/no-bridge), `dist/daw_engine/_internal/app/static/js/app.precompiled.js` + `templates/index.html` (sync).
- **Ghi chú/Test:** pytest: 121 passed, 2 failed (pre-existing — thiếu ffmpeg/fx_vst_bridge trên máy dev Linux, fail cả trước khi sửa). Probe lại trên máy Windows sau deploy: `/plugins/fx` phải trả 44 plugin KHÔNG cần refresh; `/available` phải LOẠI FX khỏi vst_instruments. Package deploy: `/workspace/deploy_fx_fix/` (4 file + deploy.ps1) hoặc `deploy_fx_fix.zip`. ⚠️ Máy 192.168.1.24 chạy SOURCE (uvicorn, ?v=202608212200) → copy 4 file vào `C:\Users\locpham\SonicForgeStudio` + restart engine + hard refresh browser.
### [2026-08-10] Task: Thực thi fix (phiên có tool) — mastering ON mặc định, âm sai instrument, Carla alive check
- **Tóm tắt thay đổi:**
(1) **Mastering FX bật ON mặc định**: `masteringSettings` khởi tạo `masterConnected: true` — MIDI item/preview được xử lí qua mastering FX ngay khi chạy app (không cần bật/tắt power). Kèm `ensureMasteringRouting()` + `refreshCarlaStatus()` gọi ở đầu `startTrackPlayback`/`startLocalTrackPlayback`/`schedulePianoRollMidi`.