VST FX integration: plugin picker theo category + GUI embed fill viewport (FX Rack & Mastering)
This commit is contained in:
@@ -716,7 +716,7 @@ async def open_fx_gui(req: FxGuiRequest, current_user: dict = Depends(get_curren
|
||||
# đang chạy → trả started=False (frontend có thể focus window cũ).
|
||||
for proc in _FX_GUI_PROCESSES:
|
||||
if proc is not None and proc.poll() is None and getattr(proc, "_sf_plugin_path", None) == plugin_path:
|
||||
return {"success": True, "started": False, "already_running": True, "cmd": []}
|
||||
return {"success": True, "started": False, "already_running": True, "cmd": [], "embed_url": getattr(proc, "_sf_embed_url", None) or None}
|
||||
job = {"path": plugin_path, "name": req.name or os.path.basename(plugin_path)}
|
||||
fd, job_path = tempfile.mkstemp(prefix="sf_fx_gui_", suffix=".json", dir=tempfile.gettempdir())
|
||||
try:
|
||||
|
||||
+18
-1
@@ -1,6 +1,7 @@
|
||||
# SonicForge Studio VST / VSTi Engine Service
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import numpy as np
|
||||
import functools
|
||||
import threading
|
||||
@@ -271,6 +272,21 @@ def release_soundfont(path: str):
|
||||
else:
|
||||
_FLUID_CACHE[path] = (fl, ref - 1)
|
||||
|
||||
def _classify_fx_category(name, path=""):
|
||||
"""Heuristic phân loại VST FX cho Plugin Picker (spec vst_fx_panel_integration).
|
||||
C++ scan có subCategoriesString (VD 'Fx|EQ') nhưng không emit ra JSON; dùng
|
||||
tên plugin để xếp category. Chỉ ảnh hưởng UI grouping, không ảnh hưởng audio."""
|
||||
s = (" " + name + " " + path).lower()
|
||||
if any(k in s for k in ("eq", "equalizer", "filter", "graphic", "parametric")) or re.search(r"(^|[^a-z0-9])q([0-9]|[^a-z]|$)", s):
|
||||
return "equalizer"
|
||||
if any(k in s for k in ("reverb", "delay", "echo", "chorus", "hall", "space")):
|
||||
return "reverb_delay"
|
||||
if any(k in s for k in ("comp", "limiter", "maximizer", "gate", "leveler", "de-ess")):
|
||||
return "comp_limiter"
|
||||
if any(k in s for k in ("distortion", "drive", "satur", "overdrive", "fuzz", "amp")):
|
||||
return "distortion"
|
||||
return "utility"
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self, vst_dir=None, sf_dir=None, upload_sf_dir=None, extra_vst_dirs=None):
|
||||
from app.config import settings as _st
|
||||
@@ -491,7 +507,8 @@ class PluginManager:
|
||||
continue
|
||||
name = p.get("name") or os.path.splitext(os.path.basename(path))[0]
|
||||
results.append({"id": name, "name": name, "path": path,
|
||||
"type": p.get("type", "VST3")})
|
||||
"type": p.get("type", "VST3"),
|
||||
"category": _classify_fx_category(name, path)})
|
||||
seen, uniq = set(), []
|
||||
for fx in results:
|
||||
if fx["path"] in seen:
|
||||
|
||||
+80
-39
@@ -11958,26 +11958,68 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
||||
);
|
||||
};
|
||||
|
||||
const vstRow = (s, idx, toggleV, changeV, uploadV, moveV, removeV, fxl, chainLen, openGui, showCarla = true) => (
|
||||
<div key={idx} className="flex items-center gap-2 bg-slate-900/60 border border-slate-800 rounded-lg px-2 py-1.5 flex-wrap">
|
||||
<button onClick={() => toggleV(idx)} className={`w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0 ${!s.bypass ? 'bg-teal-500 text-slate-950' : 'bg-slate-700 text-slate-300'}`} title={s.bypass ? 'Bypass' : 'Active'}>
|
||||
const vstRow = (s, idx, toggleV, changeV, uploadV, moveV, removeV, fxl, chainLen, openGui, showCarla = true, activeIdx = -1, onSelect = null) => (
|
||||
<div key={idx} onClick={() => onSelect && onSelect(s, idx)} className={`flex items-center gap-2 bg-slate-900/60 border rounded-lg px-2 py-1.5 flex-wrap cursor-pointer transition-all ${activeIdx === idx ? 'border-teal-400 bg-slate-800/80' : 'border-slate-800 hover:border-slate-600'}`}>
|
||||
<button onClick={e => { e.stopPropagation(); toggleV(idx); }} className={`w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0 ${!s.bypass ? 'bg-teal-500 text-slate-950' : 'bg-slate-700 text-slate-300'}`} title={s.bypass ? 'Bypass' : 'Active'}>
|
||||
<i data-lucide="power" className="w-2.5 h-2.5"></i>
|
||||
</button>
|
||||
<span className="text-[10px] font-bold text-slate-300 font-mono w-6 shrink-0">{idx + 1}</span>
|
||||
<select value={s.path || ''} onChange={e => changeV(idx, e.target.value)} className="flex-1 min-w-[200px] bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1 text-[11px] outline-none focus:border-teal-500">
|
||||
<select value={s.path || ''} onClick={e => e.stopPropagation()} onChange={e => changeV(idx, e.target.value)} className="flex-1 min-w-[200px] bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1 text-[11px] outline-none focus:border-teal-500">
|
||||
<option value="">— Chọn VST FX —</option>
|
||||
{(fxl || []).map(f => <option key={f.path} value={f.path}>{f.name || f.id}</option>)}
|
||||
</select>
|
||||
<span className={`text-[9px] font-mono ${s.preset_b64 ? 'text-emerald-400' : 'text-slate-600'}`}>{s.preset_b64 ? '✓ preset' : 'no preset'}</span>
|
||||
<button onClick={() => uploadV(idx)} className="px-2 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 text-[10px] font-semibold rounded transition" title="Upload .vstpreset (Save preset)">Preset</button>
|
||||
{openGui && <button onClick={() => openGui(s)} disabled={!s.path} className="px-2 py-1 bg-violet-800 hover:bg-violet-700 disabled:opacity-30 text-violet-100 text-[10px] font-semibold rounded transition" title="Mở GUI của VST (chỉnh thông số trực tiếp)"><i data-lucide="monitor" className="w-3 h-3"></i> GUI</button>}
|
||||
{showCarla && <button onClick={() => { if (!s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } window.SonicAPI.openInCarla(s.name || s.path, s.path).then(r => { if (r && r.success) window.showToast && window.showToast('Đã mở Carla với ' + (s.name || s.path) + ' — chỉnh âm trong Carla rồi Save preset', 'success'); else window.showToast && window.showToast('Không mở được Carla', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở Carla: ' + (err.message || err), 'error')); }} className="px-2 py-1 bg-teal-800 hover:bg-teal-700 text-teal-100 text-[10px] font-semibold rounded transition" title="Mở native GUI trong Carla">Carla</button>}
|
||||
<button onClick={() => moveV(idx, -1)} disabled={idx === 0} className="w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30" title="Lên"><i data-lucide="chevron-up" className="w-3 h-3"></i></button>
|
||||
<button onClick={() => moveV(idx, 1)} disabled={idx === chainLen - 1} className="w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30" title="Xuống"><i data-lucide="chevron-down" className="w-3 h-3"></i></button>
|
||||
<button onClick={() => removeV(idx)} className="text-slate-600 hover:text-red-400 text-xs px-0.5" title="Xóa slot"><i data-lucide="x" className="w-3 h-3"></i></button>
|
||||
<button onClick={e => { e.stopPropagation(); uploadV(idx); }} className="px-2 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 text-[10px] font-semibold rounded transition" title="Upload .vstpreset (Save preset)">Preset</button>
|
||||
{openGui && <button onClick={e => { e.stopPropagation(); openGui(s, idx); }} disabled={!s.path} className="px-2 py-1 bg-violet-800 hover:bg-violet-700 disabled:opacity-30 text-violet-100 text-[10px] font-semibold rounded transition" title="Mở GUI của VST (chỉnh thông số trực tiếp)"><i data-lucide="monitor" className="w-3 h-3"></i> GUI</button>}
|
||||
{showCarla && <button onClick={e => { e.stopPropagation(); if (!s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } window.SonicAPI.openInCarla(s.name || s.path, s.path).then(r => { if (r && r.success) window.showToast && window.showToast('Đã mở Carla với ' + (s.name || s.path) + ' — chỉnh âm trong Carla rồi Save preset', 'success'); else window.showToast && window.showToast('Không mở được Carla', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở Carla: ' + (err.message || err), 'error')); }} className="px-2 py-1 bg-teal-800 hover:bg-teal-700 text-teal-100 text-[10px] font-semibold rounded transition" title="Mở native GUI trong Carla">Carla</button>}
|
||||
<button onClick={e => { e.stopPropagation(); moveV(idx, -1); }} disabled={idx === 0} className="w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30" title="Lên"><i data-lucide="chevron-up" className="w-3 h-3"></i></button>
|
||||
<button onClick={e => { e.stopPropagation(); moveV(idx, 1); }} disabled={idx === chainLen - 1} className="w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30" title="Xuống"><i data-lucide="chevron-down" className="w-3 h-3"></i></button>
|
||||
<button onClick={e => { e.stopPropagation(); removeV(idx); }} className="text-slate-600 hover:text-red-400 text-xs px-0.5" title="Xóa slot"><i data-lucide="x" className="w-3 h-3"></i></button>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── VST FX Plugin Picker (spec vst_fx_panel_integration.md §II) ──────────
|
||||
// Modal chọn VST FX theo category (heuristic từ backend list_fx.category).
|
||||
// Dùng chung FXRackModal + MasteringModal: [+] → picker → chèn VST card.
|
||||
const VST_FX_CATEGORIES = [
|
||||
{ id: 'all', label: 'Tất cả' },
|
||||
{ id: 'equalizer', label: 'Equalizers / Dynamic EQ' },
|
||||
{ id: 'reverb_delay', label: 'Reverbs & Delays' },
|
||||
{ id: 'comp_limiter', label: 'Compressors & Limiters' },
|
||||
{ id: 'distortion', label: 'Distortion / Saturation' },
|
||||
{ id: 'utility', label: 'Utility' }
|
||||
];
|
||||
const VstPluginPicker = ({ open, onClose, plugins, onPick }) => {
|
||||
const [cat, setCat] = React.useState('all');
|
||||
React.useEffect(() => { if (open) setCat('all'); }, [open]);
|
||||
if (!open) return null;
|
||||
const list = (plugins || []).filter(p => cat === 'all' || (p.category || 'utility') === cat);
|
||||
return (
|
||||
<div className="fixed inset-0 z-[125] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4" onClick={onClose}>
|
||||
<div className="w-full max-w-2xl bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl max-h-[80vh] flex flex-col" onClick={e => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between border-b border-slate-800 pb-3">
|
||||
<h3 className="text-xs font-bold text-white uppercase tracking-wider font-mono">CHỌN VST FX — PLUGIN PICKER</h3>
|
||||
<button onClick={onClose} className="text-slate-400 hover:text-white"><i data-lucide="x" className="w-4 h-4"></i></button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 shrink-0">
|
||||
{VST_FX_CATEGORIES.map(c => (
|
||||
<button key={c.id} onClick={() => setCat(c.id)} className={`px-2.5 py-1 rounded text-[10px] font-semibold transition ${cat === c.id ? 'bg-teal-700 text-white' : 'bg-slate-800 hover:bg-slate-700 text-slate-300'}`}>{c.label}</button>
|
||||
))}
|
||||
</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 => (
|
||||
<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>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
|
||||
const [activeType, setActiveType] = React.useState('eq');
|
||||
const [addModuleOpen, setAddModuleOpen] = React.useState(false);
|
||||
@@ -12002,7 +12044,8 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
|
||||
}, [track && track.id]);
|
||||
const previewAudioRef = React.useRef(null);
|
||||
const [vstBypass, setVstBypass] = React.useState(!!(track && track.vstFxBypass));
|
||||
const [addVstSel, setAddVstSel] = React.useState('');
|
||||
const [activeVstIdx, setActiveVstIdx] = React.useState(-1);
|
||||
const [vstPickerOpen, setVstPickerOpen] = React.useState(false);
|
||||
|
||||
if (!track) return null;
|
||||
|
||||
@@ -12018,11 +12061,10 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
|
||||
};
|
||||
React.useEffect(() => () => { if (fxGuiOpen) { try { fetch(fxGuiOpen.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} } }, []);
|
||||
const toggleVst = (idx) => setVstChain(vstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
|
||||
const removeVst = (idx) => setVstChain(vstChain.filter((_, i) => i !== idx));
|
||||
const removeVst = (idx) => { const rem = vstChain[idx]; if (rem && fxGuiOpen && fxGuiOpen.path === rem.path) closeFxGui(); if (idx === activeVstIdx) setActiveVstIdx(-1); setVstChain(vstChain.filter((_, i) => i !== idx)); };
|
||||
const moveVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= vstChain.length) return; const next = [...vstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setVstChain(next); };
|
||||
const openVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (fxGuiOpen) closeFxGui(); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.success && r.embed_url) setFxGuiOpen({ path: s.path, url: r.embed_url }); else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
|
||||
const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path); if (!fx) return; setVstChain([...vstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]);
|
||||
openVstGui({ path: fx.path, name: fx.name || fx.id }); };
|
||||
const openVstGui = (s, idx) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (fxGuiOpen) closeFxGui(); if (idx !== undefined) setActiveVstIdx(idx); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.embed_url) setFxGuiOpen({ path: s.path, url: r.embed_url }); else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
|
||||
const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path); if (!fx) return; const next = [...vstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]; setVstChain(next); setActiveVstIdx(next.length - 1); };
|
||||
const changeVstPlugin = (idx, path) => { const fx = (fxList || []).find(f => f.path === path); setVstChain(vstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
|
||||
const uploadVstPreset = (idx) => {
|
||||
const inp = document.createElement('input');
|
||||
@@ -12304,7 +12346,7 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
|
||||
onDragStart={e => { dragChainIndexRef.current = idx; e.dataTransfer.effectAllowed = 'move'; }}
|
||||
onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }}
|
||||
onDrop={e => { e.preventDefault(); const from = dragChainIndexRef.current; if (from !== null && from !== idx) { const next = [...chain]; const mv = next.splice(from, 1)[0]; next.splice(idx, 0, mv); setChain(next); } dragChainIndexRef.current = null; }}
|
||||
onClick={() => setActiveType(m.type)}
|
||||
onClick={() => { closeFxGui(); setActiveType(m.type); }}
|
||||
className={`w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all shrink-0 ${activeType === m.type ? 'border-2 border-cyan-400 bg-slate-800' : 'bg-slate-900 border border-slate-800 hover:border-slate-600'}`}>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<button onClick={e => { e.stopPropagation(); toggleMod(idx); }} className="w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0" style={{ backgroundColor: on ? '#38bdf8' : '#334155', color: on ? '#0f172a' : '#94a3b8' }}>
|
||||
@@ -12343,16 +12385,15 @@ const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path);
|
||||
</div>
|
||||
</div>
|
||||
{vstChain.length === 0 && <div className="text-[10px] text-slate-500 font-mono">Chưa có VST FX — chọn plugin bên dưới để thêm vào insert chain (WebAudio chain → VST FX → Fader).</div>}
|
||||
{vstChain.map((s, idx) => vstRow(s, idx, toggleVst, changeVstPlugin, uploadVstPreset, moveVst, removeVst, fxList, vstChain.length, openVstGui, false))}
|
||||
<select value={addVstSel} onChange={e => { setAddVstSel(e.target.value); if (e.target.value) { addVst(e.target.value); setAddVstSel(''); } }} className="w-full bg-slate-950 border border-dashed border-slate-700 hover:border-teal-600 text-teal-300 rounded px-2 py-1.5 text-[11px] outline-none" title="Thêm VST FX">
|
||||
<option value="">+ Thêm VST FX...</option>
|
||||
{(fxList || []).map(f => <option key={f.path} value={f.path}>{f.name || f.id}</option>)}
|
||||
</select>
|
||||
{vstChain.map((s, idx) => vstRow(s, idx, toggleVst, changeVstPlugin, uploadVstPreset, moveVst, removeVst, fxList, vstChain.length, openVstGui, false, activeVstIdx, (s2, i2) => openVstGui(s2, i2)))}
|
||||
<button onClick={() => setVstPickerOpen(true)} className="w-full bg-slate-950 border border-dashed border-slate-700 hover:border-teal-600 text-teal-300 rounded px-2 py-1.5 text-[11px] outline-none text-left" title="Thêm VST FX (Plugin Picker theo category)">
|
||||
<i data-lucide="plus" className="w-3 h-3 inline-block mr-1"></i> Thêm VST FX... (Picker: EQ / Reverb / Comp / Utility)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* WORKSPACE CONTROLS */}
|
||||
{fxGuiOpen ? (
|
||||
<div className="p-4">
|
||||
<div className="flex-1 min-h-0">
|
||||
<VstGuiEmbed url={fxGuiOpen.url} onClose={closeFxGui}
|
||||
onDead={() => { window.showToast && window.showToast('Bridge VST GUI đã mất kết nối — trả về graph', 'error'); closeFxGui(); }} />
|
||||
</div>
|
||||
@@ -12526,6 +12567,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)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12549,15 +12591,17 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
||||
const masterVstChain = (ozState.vstFxChain || []);
|
||||
// Embedded VST GUI state (PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 2)
|
||||
const [masterGuiOpen, setMasterGuiOpen] = React.useState(null); // { path, url } | null
|
||||
const [activeMasterVstPath, setActiveMasterVstPath] = React.useState(null);
|
||||
const closeMasterGui = () => {
|
||||
if (masterGuiOpen) { try { fetch(masterGuiOpen.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} setMasterGuiOpen(null); }
|
||||
setActiveMasterVstPath(null);
|
||||
};
|
||||
React.useEffect(() => () => { if (masterGuiOpen) { try { fetch(masterGuiOpen.url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} } }, []);
|
||||
const setMasterVstChain = (next) => setOzState(prev => ({ ...prev, vstFxChain: next }));
|
||||
const toggleMasterVst = (idx) => setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
|
||||
const removeMasterVst = (idx) => setMasterVstChain(masterVstChain.filter((_, i) => i !== idx));
|
||||
const moveMasterVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= masterVstChain.length) return; const next = [...masterVstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setMasterVstChain(next); };
|
||||
const openMasterVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (masterGuiOpen) closeMasterGui(); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.success && r.embed_url) setMasterGuiOpen({ path: s.path, url: r.embed_url }); else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
|
||||
const openMasterVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (masterGuiOpen) closeMasterGui(); if (s.path) setActiveMasterVstPath(s.path); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.embed_url) setMasterGuiOpen({ path: s.path, url: r.embed_url }); else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
|
||||
const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.path === path); if (!fx) return; setMasterVstChain([...masterVstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]);
|
||||
openMasterVstGui({ path: fx.path, name: fx.name || fx.id }); };
|
||||
const changeMasterVstPlugin = (idx, path) => { const fx = (masterFxList || []).find(f => f.path === path); setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
|
||||
@@ -12609,6 +12653,7 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
|
||||
}, []);
|
||||
const previewAudioRef = React.useRef(null);
|
||||
const [addMasterVstSel, setAddMasterVstSel] = React.useState('');
|
||||
const [masterVstPickerOpen, setMasterVstPickerOpen] = React.useState(false);
|
||||
|
||||
// Wave Observer Refs & States
|
||||
const woCanvasRef = React.useRef(null);
|
||||
@@ -13114,6 +13159,8 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
|
||||
});
|
||||
};
|
||||
const removeChainModule = (modId) => {
|
||||
const removedMod = (ozState.chain || []).find(m => m.id === modId);
|
||||
if (removedMod && removedMod.type === 'vst3' && masterGuiOpen && masterGuiOpen.path === removedMod.path) closeMasterGui();
|
||||
setOzState(prev => {
|
||||
const removed = prev.chain.find(m => m.id === modId);
|
||||
let chain = prev.chain.filter(m => m.id !== modId);
|
||||
@@ -13302,7 +13349,7 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
|
||||
{(ozState.chain || []).map((mod, idx) => {
|
||||
const isVst = mod.type === 'vst3';
|
||||
const meta = MODULE_META[mod.type] || { name: mod.type, sub: '', icon: 'circle', color: '#94a3b8' };
|
||||
const isEditing = !isVst && ozState.activeModule === mod.type;
|
||||
const isEditing = (!isVst && ozState.activeModule === mod.type) || (isVst && activeMasterVstPath === mod.path);
|
||||
const isOn = isVst ? mod.active !== false : chainActive(mod.type);
|
||||
return (
|
||||
<div key={mod.id}
|
||||
@@ -13310,7 +13357,7 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
|
||||
onDragStart={e => { dragChainIndexRef.current = idx; e.dataTransfer.effectAllowed = 'move'; }}
|
||||
onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }}
|
||||
onDrop={e => { e.preventDefault(); const from = dragChainIndexRef.current; if (from !== null && from !== idx) reorderChain(from, idx); dragChainIndexRef.current = null; }}
|
||||
onClick={() => { if (mod.type === 'vst3') { openMasterVstGui({ path: mod.path, name: mod.name || mod.path }); } else switchModule(mod.type); }}
|
||||
onClick={() => { if (mod.type === 'vst3') { openMasterVstGui({ path: mod.path, name: mod.name || mod.path }); } else { closeMasterGui(); switchModule(mod.type); } }}
|
||||
className={`w-40 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all shrink-0 ${isEditing ? 'oz-card-active' : 'oz-card'}`}>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<button onClick={e => { e.stopPropagation(); toggleChainModule(mod.id); }} className="w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0" style={{backgroundColor: isOn ? '#38bdf8' : '#334155', color: isOn ? '#0f172a' : '#94a3b8'}}>
|
||||
@@ -13341,7 +13388,7 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
|
||||
|
||||
{/* LEFT: MODULE VIEWS */}
|
||||
{masterGuiOpen ? (
|
||||
<div className="flex-1 flex flex-col bg-slate-950 relative overflow-y-auto oz-scrollbar p-4">
|
||||
<div className="flex-1 flex flex-col bg-slate-950 relative min-h-0">
|
||||
<VstGuiEmbed url={masterGuiOpen.url} onClose={closeMasterGui}
|
||||
onDead={() => { window.showToast && window.showToast('Bridge VST GUI đã mất kết nối — trả về graph', 'error'); closeMasterGui(); }} />
|
||||
</div>
|
||||
@@ -13843,22 +13890,16 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{(masterFxList || []).length > 0 && (
|
||||
<div className="border-t border-slate-800 pt-3">
|
||||
<div className="text-[10px] font-bold text-teal-400 uppercase tracking-wider oz-font-mono mb-2">VST FX (Ozone...)</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{masterFxList.map(f => (
|
||||
<button key={f.path} onClick={() => addVstToChain(f.path)} 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>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-slate-800 pt-3">
|
||||
<div className="text-[10px] font-bold text-teal-400 uppercase tracking-wider oz-font-mono mb-2">VST FX (Ozone...)</div>
|
||||
<button onClick={() => setMasterVstPickerOpen(true)} className="w-full bg-slate-950 hover:bg-slate-800 border border-dashed border-slate-700 hover:border-teal-600 text-teal-300 rounded px-2 py-2 text-[11px] text-left transition-colors" title="Plugin Picker theo category (EQ / Reverb / Comp / Utility)">
|
||||
<i data-lucide="plug" className="w-3 h-3 inline-block mr-1"></i> Chọn VST FX từ thư viện — Picker: EQ / Reverb / Comp / Utility
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<VstPluginPicker open={masterVstPickerOpen} onClose={() => setMasterVstPickerOpen(false)} plugins={masterFxList} onPick={(f) => addVstToChain(f.path)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -50,7 +50,7 @@
|
||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608182330" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608182350" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user