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:
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user