connect solution into app: VSTi live native (vst3 attach/vst2 bridge) with WASM autosample fallback, VST GUI window (Bug 1), native-first track instrument
This commit is contained in:
+68
-2
@@ -8,6 +8,7 @@ import os
|
|||||||
|
|
||||||
from app.core.native_audio_service import get_service
|
from app.core.native_audio_service import get_service
|
||||||
from app.core.render_engine import _find_sf2_path
|
from app.core.render_engine import _find_sf2_path
|
||||||
|
from app.core.vst_engine import get_plugin_manager
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -47,7 +48,8 @@ class SfNoteOffRequest(BaseModel):
|
|||||||
|
|
||||||
class Vst2EnsureRequest(BaseModel):
|
class Vst2EnsureRequest(BaseModel):
|
||||||
track_id: str
|
track_id: str
|
||||||
plugin_path: str
|
plugin_path: Optional[str] = None
|
||||||
|
plugin_id: Optional[str] = None # resolve path server-side
|
||||||
live: bool = True
|
live: bool = True
|
||||||
|
|
||||||
|
|
||||||
@@ -63,6 +65,23 @@ class Vst2NoteOffRequest(BaseModel):
|
|||||||
channel: int = 0
|
channel: int = 0
|
||||||
pitch: int
|
pitch: int
|
||||||
|
|
||||||
|
class Vst3EnsureRequest(BaseModel):
|
||||||
|
track_id: str
|
||||||
|
plugin_path: Optional[str] = None
|
||||||
|
plugin_id: Optional[str] = None # resolve path server-side
|
||||||
|
live: bool = True
|
||||||
|
|
||||||
|
class Vst3NoteRequest(BaseModel):
|
||||||
|
track_id: str
|
||||||
|
channel: int = 0
|
||||||
|
pitch: int
|
||||||
|
velocity: int = 100
|
||||||
|
|
||||||
|
class Vst3NoteOffRequest(BaseModel):
|
||||||
|
track_id: str
|
||||||
|
channel: int = 0
|
||||||
|
pitch: int
|
||||||
|
|
||||||
|
|
||||||
class RenderNote(BaseModel):
|
class RenderNote(BaseModel):
|
||||||
note: int = 60
|
note: int = 60
|
||||||
@@ -89,6 +108,24 @@ def _svc():
|
|||||||
return get_service()
|
return get_service()
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_plugin_path(plugin_id: str = None, plugin_path: str = None) -> str:
|
||||||
|
"""Resolve plugin path server-side: uu tien plugin_path truc tiep, con
|
||||||
|
khong thi plugin_id = ten plugin trong registry scan (JS chi co plugin_id)."""
|
||||||
|
path = (plugin_path or "").strip()
|
||||||
|
if path:
|
||||||
|
return path
|
||||||
|
pid = (plugin_id or "").strip()
|
||||||
|
if pid:
|
||||||
|
try:
|
||||||
|
plugins = get_plugin_manager()._scan_plugins()
|
||||||
|
if pid in plugins:
|
||||||
|
return plugins[pid]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/status")
|
@router.get("/status")
|
||||||
async def status():
|
async def status():
|
||||||
return _svc().status()
|
return _svc().status()
|
||||||
@@ -142,8 +179,12 @@ async def sf_audio_stop(req: SfNoteOffRequest):
|
|||||||
|
|
||||||
@router.post("/vst2/ensure")
|
@router.post("/vst2/ensure")
|
||||||
async def vst2_ensure(req: Vst2EnsureRequest):
|
async def vst2_ensure(req: Vst2EnsureRequest):
|
||||||
|
path = _resolve_plugin_path(req.plugin_id, req.plugin_path)
|
||||||
|
if not path or not os.path.exists(path):
|
||||||
|
raise HTTPException(status_code=400, detail=f"khong tim thay plugin "
|
||||||
|
f"(plugin_id={req.plugin_id!r} plugin_path={req.plugin_path!r})")
|
||||||
try:
|
try:
|
||||||
return _svc().ensure_vst2(req.track_id, req.plugin_path, req.live)
|
return _svc().ensure_vst2(req.track_id, path, req.live)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
@@ -163,6 +204,31 @@ async def vst2_note_off(req: Vst2NoteOffRequest):
|
|||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
@router.post("/vst3/ensure")
|
||||||
|
async def vst3_ensure(req: Vst3EnsureRequest):
|
||||||
|
path = _resolve_plugin_path(req.plugin_id, req.plugin_path)
|
||||||
|
if not path or not os.path.exists(path):
|
||||||
|
raise HTTPException(status_code=400, detail=f"khong tim thay plugin "
|
||||||
|
f"(plugin_id={req.plugin_id!r} plugin_path={req.plugin_path!r})")
|
||||||
|
try:
|
||||||
|
return _svc().ensure_vst3(req.track_id, path, req.live)
|
||||||
|
except RuntimeError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
@router.post("/vst3/note_on")
|
||||||
|
async def vst3_note_on(req: Vst3NoteRequest):
|
||||||
|
try:
|
||||||
|
return _svc().vst3_note_on(req.track_id, req.channel, req.pitch, req.velocity)
|
||||||
|
except RuntimeError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
@router.post("/vst3/note_off")
|
||||||
|
async def vst3_note_off(req: Vst3NoteOffRequest):
|
||||||
|
try:
|
||||||
|
return _svc().vst3_note_off(req.track_id, req.channel, req.pitch)
|
||||||
|
except RuntimeError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/render")
|
@router.post("/render")
|
||||||
async def render(req: RenderRequest):
|
async def render(req: RenderRequest):
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ _NATIVE_DIR = (
|
|||||||
or os.path.join(settings.BASE_DIR, "native_host", "build", "Release")
|
or os.path.join(settings.BASE_DIR, "native_host", "build", "Release")
|
||||||
)
|
)
|
||||||
|
|
||||||
_ERR = None
|
_ERR = None # khoi tao buffer sau khi dinh nghia _errbuf()
|
||||||
_LOCK = threading.Lock()
|
_LOCK = threading.RLock() # reentrant: _hidden_hwnd() goi trong ensure_*
|
||||||
|
|
||||||
|
|
||||||
def native_dir() -> str:
|
def native_dir() -> str:
|
||||||
@@ -49,6 +49,39 @@ def _errbuf():
|
|||||||
def _errval(buf) -> str:
|
def _errval(buf) -> str:
|
||||||
return buf.value.decode(errors="replace") if buf and buf.value else ""
|
return buf.value.decode(errors="replace") if buf and buf.value else ""
|
||||||
|
|
||||||
|
_ERR = _errbuf() # module-level err buffer (dung chung, 256 bytes)
|
||||||
|
|
||||||
|
|
||||||
|
def _hidden_hwnd():
|
||||||
|
"""Tao 1 hidden window dung lam parent cho VST3 editor headless (live).
|
||||||
|
SF_VST3_Attach yeu cau parent_hwnd != NULL; window 0-size khong hien thi."""
|
||||||
|
import ctypes as _ct
|
||||||
|
from ctypes import wintypes as _wt
|
||||||
|
if getattr(_hidden_hwnd, "_hwnd", 0):
|
||||||
|
return _hidden_hwnd._hwnd
|
||||||
|
with _LOCK:
|
||||||
|
if getattr(_hidden_hwnd, "_hwnd", 0):
|
||||||
|
return _hidden_hwnd._hwnd
|
||||||
|
_u = _ct.windll.user32
|
||||||
|
_k = _ct.windll.kernel32
|
||||||
|
_u.DefWindowProcW.argtypes = [_wt.HWND, _wt.UINT, _ct.c_void_p, _ct.c_void_p]
|
||||||
|
_u.DefWindowProcW.restype = _ct.c_long
|
||||||
|
_WNDPROC = _ct.WINFUNCTYPE(_ct.c_long, _wt.HWND, _wt.UINT, _ct.c_void_p, _ct.c_void_p) # WPARAM/LPARAM 64-bit
|
||||||
|
class _WC(_ct.Structure):
|
||||||
|
_fields_ = [("style", _ct.c_uint), ("lpfnWndProc", _WNDPROC), ("cbClsExtra", _ct.c_int),
|
||||||
|
("cbWndExtra", _ct.c_int), ("hInstance", _wt.HINSTANCE), ("hIcon", _wt.HICON),
|
||||||
|
("hCursor", _wt.HANDLE), ("hbrBackground", _wt.HBRUSH), ("lpszMenuName", _wt.LPCWSTR),
|
||||||
|
("lpszClassName", _wt.LPCWSTR)]
|
||||||
|
_wc = _WC()
|
||||||
|
_wc.lpfnWndProc = _WNDPROC(_u.DefWindowProcW)
|
||||||
|
_wc.lpszClassName = "SonicForgeNativeHidden"
|
||||||
|
_wc.hInstance = _k.GetModuleHandleW(None)
|
||||||
|
if _u.RegisterClassW(_ct.byref(_wc)) or _k.GetLastError() == 1410: # 1410 = class ton tai
|
||||||
|
_h = _u.CreateWindowExW(0, "SonicForgeNativeHidden", "sf", 0, 0, 0, 0, 0,
|
||||||
|
None, None, _wc.hInstance, None)
|
||||||
|
_hidden_hwnd._hwnd = _h or 0
|
||||||
|
return _hidden_hwnd._hwnd
|
||||||
|
|
||||||
|
|
||||||
def is_available() -> bool:
|
def is_available() -> bool:
|
||||||
return os.path.isfile(_dll("sf_host_bridge.dll"))
|
return os.path.isfile(_dll("sf_host_bridge.dll"))
|
||||||
@@ -133,11 +166,21 @@ class _Vst3Ctx:
|
|||||||
self.handle = 0
|
self.handle = 0
|
||||||
|
|
||||||
def create(self, plugin_path):
|
def create(self, plugin_path):
|
||||||
|
# Attach can plugin_name = ten class VST3 (ClassInfo::name) + parent
|
||||||
|
# hwnd. Ten class thuong = ten file/folder .vst3; hidden window cho
|
||||||
|
# live headless. Neu ten class khong khop -> rc 0 -> caller fallback.
|
||||||
|
base = os.path.basename(plugin_path.rstrip("\\/"))
|
||||||
|
if base.lower().endswith(".vst3"):
|
||||||
|
plugin_name = base[:-5]
|
||||||
|
else:
|
||||||
|
plugin_name = os.path.splitext(base)[0]
|
||||||
w = ctypes.c_int32(0)
|
w = ctypes.c_int32(0)
|
||||||
h = ctypes.c_int32(0)
|
h = ctypes.c_int32(0)
|
||||||
self.handle = self.dll.SF_VST3_Load(plugin_path.encode("utf-8"), 0,
|
self.handle = self.dll.SF_VST3_Attach(plugin_path.encode("utf-8"),
|
||||||
ctypes.byref(w), ctypes.byref(h),
|
plugin_name.encode("utf-8"),
|
||||||
_ERR, 256)
|
_hidden_hwnd(),
|
||||||
|
ctypes.byref(w), ctypes.byref(h),
|
||||||
|
_ERR, 256)
|
||||||
return self.handle
|
return self.handle
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
@@ -509,10 +552,10 @@ class NativeAudioService:
|
|||||||
raise RuntimeError(f"thiếu {_dll('vst3_host_bridge.dll')}")
|
raise RuntimeError(f"thiếu {_dll('vst3_host_bridge.dll')}")
|
||||||
dll = ctypes.WinDLL(_dll("vst3_host_bridge.dll"))
|
dll = ctypes.WinDLL(_dll("vst3_host_bridge.dll"))
|
||||||
i32 = ctypes.c_int32
|
i32 = ctypes.c_int32
|
||||||
dll.SF_VST3_Load.argtypes = [ctypes.c_char_p, ctypes.c_void_p,
|
dll.SF_VST3_Attach.argtypes = [ctypes.c_char_p, ctypes.c_char_p,
|
||||||
ctypes.POINTER(i32), ctypes.POINTER(i32),
|
ctypes.c_void_p, ctypes.POINTER(i32),
|
||||||
ctypes.c_char_p, i32]
|
ctypes.POINTER(i32), ctypes.c_char_p, i32]
|
||||||
dll.SF_VST3_Load.restype = i32
|
dll.SF_VST3_Attach.restype = i32
|
||||||
dll.SF_VST3_SendNoteOn.argtypes = [i32, i32, i32, i32]
|
dll.SF_VST3_SendNoteOn.argtypes = [i32, i32, i32, i32]
|
||||||
dll.SF_VST3_SendNoteOn.restype = i32
|
dll.SF_VST3_SendNoteOn.restype = i32
|
||||||
dll.SF_VST3_SendNoteOff.argtypes = [i32, i32, i32]
|
dll.SF_VST3_SendNoteOff.argtypes = [i32, i32, i32]
|
||||||
@@ -543,7 +586,7 @@ class NativeAudioService:
|
|||||||
ctx = _Vst3Ctx(dll)
|
ctx = _Vst3Ctx(dll)
|
||||||
h = ctx.create(plugin_path)
|
h = ctx.create(plugin_path)
|
||||||
if h <= 0:
|
if h <= 0:
|
||||||
raise RuntimeError(f"SF_VST3_Load fail: {_errval(_ERR)}")
|
raise RuntimeError(f"SF_VST3_Attach fail: {_errval(_ERR)}")
|
||||||
ctx.SetMasterGain(h, self._master_lin)
|
ctx.SetMasterGain(h, self._master_lin)
|
||||||
if live:
|
if live:
|
||||||
rc = ctx.AudioStart(h, self.sample_rate, self.block_size, _ERR, 256)
|
rc = ctx.AudioStart(h, self.sample_rate, self.block_size, _ERR, 256)
|
||||||
|
|||||||
@@ -134,6 +134,15 @@ async def get_index():
|
|||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/vst_gui.html", response_class=HTMLResponse)
|
||||||
|
async def get_vst_gui():
|
||||||
|
"""Cửa sổ VST GUI (Bug 1): Rust mở WebviewUrl::App("vst_gui.html?...") —
|
||||||
|
trong devUrl (localhost:8000) cần route riêng vì file nằm /static/vst_gui.html."""
|
||||||
|
vst_gui_path = os.path.join(STATIC_DIR, "vst_gui.html")
|
||||||
|
if os.path.exists(vst_gui_path):
|
||||||
|
return FileResponse(vst_gui_path, media_type="text/html")
|
||||||
|
return HTMLResponse(content="<h1>vst_gui.html not found</h1>", status_code=404)
|
||||||
@app.get("/favicon.svg")
|
@app.get("/favicon.svg")
|
||||||
async def get_favicon():
|
async def get_favicon():
|
||||||
favicon_path = os.path.join(settings.TEMPLATES_DIR, "favicon.svg")
|
favicon_path = os.path.join(settings.TEMPLATES_DIR, "favicon.svg")
|
||||||
|
|||||||
+121
-41
@@ -68,6 +68,25 @@ const resolveTrackInstrumentCtx = (track, tracks) => {
|
|||||||
return { ch, program: undefined, synthEngine: undefined, sfId: undefined, bank: 0, prog: 0 };
|
return { ch, program: undefined, synthEngine: undefined, sfId: undefined, bank: 0, prog: 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sub-tab (piano roll / section) instrument override: uu tien instrument cua
|
||||||
|
// CHINH TAB (st.instrumentProgram / st.instrumentId sf_*) — section scope va
|
||||||
|
// tab co the load instrument rieng khac main track. Khong co -> fallback track.
|
||||||
|
const resolveSubTabInstrumentCtx = (st, fallbackTrack, tracks) => {
|
||||||
|
const all = tracks || [];
|
||||||
|
const ch = fallbackTrack ? assignTrackMidiChannel(fallbackTrack, all) : 0;
|
||||||
|
const stInstId = st && st.instrumentId;
|
||||||
|
if (st && stInstId && typeof stInstId === 'string' && stInstId.startsWith('sf_')) {
|
||||||
|
const sfId = stInstId.replace('sf_', '');
|
||||||
|
const sfProg = (st.instrumentProgram !== undefined && st.instrumentProgram !== null) ? st.instrumentProgram : 0;
|
||||||
|
const se = { type: 'soundfont', plugin_id: stInstId, soundfont_bank: 0, soundfont_program: sfProg, soundfont_id: sfId };
|
||||||
|
return { ch, program: undefined, synthEngine: se, sfId, bank: 0, prog: sfProg };
|
||||||
|
}
|
||||||
|
if (st && st.instrumentProgram !== undefined && st.instrumentProgram !== null) {
|
||||||
|
return { ch, program: st.instrumentProgram, synthEngine: undefined, sfId: undefined, bank: 0, prog: st.instrumentProgram };
|
||||||
|
}
|
||||||
|
return resolveTrackInstrumentCtx(fallbackTrack, all);
|
||||||
|
};
|
||||||
|
|
||||||
// Đảm bảo FluidSynth channel của track đã select ĐÚNG instrument trước khi
|
// Đảm bảo FluidSynth channel của track đã select ĐÚNG instrument trước khi
|
||||||
// notes bắn. Fire-and-forget: playNote tự load + retry nếu SF chưa load xong
|
// notes bắn. Fire-and-forget: playNote tự load + retry nếu SF chưa load xong
|
||||||
// (dedup sẵn trong loadSoundFont) — không chặn, không gây stall khi mở tab.
|
// (dedup sẵn trong loadSoundFont) — không chặn, không gây stall khi mở tab.
|
||||||
@@ -126,6 +145,25 @@ const scheduleCarlaNote = (synthEngine, channel, pitch, velocity, startWallTime,
|
|||||||
} catch (e) { console.warn('[Carla] scheduleCarlaNote error:', e); }
|
} catch (e) { console.warn('[Carla] scheduleCarlaNote error:', e); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Native VST GUI (Bug 1) ──────────────────────────────────────────────
|
||||||
|
// Standalone (Tauri): mo cua so native editor qua command Rust open_vst_gui
|
||||||
|
// (VST3 .vst3 / VST2 .dll attach HWND, khong spawn Carla ngoai app).
|
||||||
|
// Browser: fallback ve Carla bridge cu.
|
||||||
|
const kindFromPath = (p) => (p || '').toLowerCase().endsWith('.vst3') ? 'vst3' : 'vst2';
|
||||||
|
const canOpenNativeVstGui = () => !!(window.__TAURI__ && window.__TAURI__.core) || !!(window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local);
|
||||||
|
const openNativeVstGui = (pluginId, trackId, pluginPath, pluginKind) => {
|
||||||
|
try {
|
||||||
|
if (window.__TAURI__ && window.__TAURI__.core) {
|
||||||
|
return window.__TAURI__.core.invoke('open_vst_gui', {
|
||||||
|
pluginId: pluginId || '',
|
||||||
|
trackId: trackId || '',
|
||||||
|
pluginPath: pluginPath || '',
|
||||||
|
pluginKind: pluginKind || kindFromPath(pluginPath),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) { console.warn('[VstGui] invoke error, fallback Carla:', e); }
|
||||||
|
return window.SonicAPI && window.SonicAPI.openInCarla ? window.SonicAPI.openInCarla(pluginId, pluginPath) : Promise.resolve({ success: false });
|
||||||
|
};
|
||||||
// ── Carla bridge alive tracking + auto-open ────────────────────────────────
|
// ── Carla bridge alive tracking + auto-open ────────────────────────────────
|
||||||
// window.__carlaRunning: undefined = chưa biết | true = đang chạy | false = đã chết.
|
// window.__carlaRunning: undefined = chưa biết | true = đang chạy | false = đã chết.
|
||||||
// window.__carlaNoteQueue: nốt chờ flush khi Carla chưa ready (cold start).
|
// window.__carlaNoteQueue: nốt chờ flush khi Carla chưa ready (cold start).
|
||||||
@@ -277,7 +315,11 @@ const _routePreviewNote = (trackId, ctx, pitch, velocity, durationMs, sourceType
|
|||||||
};
|
};
|
||||||
const _stopMidiFilePreview = () => {
|
const _stopMidiFilePreview = () => {
|
||||||
if (_midiFilePreviewAudio) {
|
if (_midiFilePreviewAudio) {
|
||||||
try { _midiFilePreviewAudio.pause(); _midiFilePreviewAudio.currentTime = 0; } catch (e) {}
|
try {
|
||||||
|
// AudioBufferSourceNode chi co stop(); HTMLAudioElement chi co pause().
|
||||||
|
if (_midiFilePreviewAudio.stop) { _midiFilePreviewAudio.stop(); }
|
||||||
|
else { _midiFilePreviewAudio.pause(); _midiFilePreviewAudio.currentTime = 0; }
|
||||||
|
} catch (e) {}
|
||||||
_midiFilePreviewAudio = null;
|
_midiFilePreviewAudio = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -5945,7 +5987,7 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
|
|||||||
),
|
),
|
||||||
React.createElement('div', { className: 'flex items-center gap-2' },
|
React.createElement('div', { className: 'flex items-center gap-2' },
|
||||||
(window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) && React.createElement('button', {
|
(window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) && React.createElement('button', {
|
||||||
onClick: (e) => { e.stopPropagation(); window.SonicAPI.openInCarla(v.id).then(function (r) { if (r && r.success) showToast('Đã mở Carla với ' + (v.name || v.id), 'success'); }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); },
|
onClick: (e) => { e.stopPropagation(); openNativeVstGui(v.id, selectedTrackId, v.path, v.type).then(function (r) { if (r && (r.success || typeof r === 'string')) showToast('Đã mở GUI với ' + (v.name || v.id), 'success'); }).catch(function (err) { showToast('Lỗi mở VST GUI: ' + (err.message || err), 'error'); }); },
|
||||||
className: 'text-[10px] bg-teal-800 hover:bg-teal-700 text-white px-2 py-1 rounded transition shrink-0',
|
className: 'text-[10px] bg-teal-800 hover:bg-teal-700 text-white px-2 py-1 rounded transition shrink-0',
|
||||||
title: 'Mở trong Carla (native GUI)'
|
title: 'Mở trong Carla (native GUI)'
|
||||||
}, '🎛 Carla'),
|
}, '🎛 Carla'),
|
||||||
@@ -6102,7 +6144,7 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
|
|||||||
React.createElement('span', { className: 'truncate' }, v.name),
|
React.createElement('span', { className: 'truncate' }, v.name),
|
||||||
React.createElement('span', { className: 'text-[9px] text-zinc-600 font-mono truncate ml-auto' }, v.dir),
|
React.createElement('span', { className: 'text-[9px] text-zinc-600 font-mono truncate ml-auto' }, v.dir),
|
||||||
(window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) && React.createElement('button', {
|
(window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) && React.createElement('button', {
|
||||||
onClick: () => { window.SonicAPI.openInCarla(null, v.path).then(function (r) { if (r && r.success) showToast('Đã mở Carla với ' + v.name, 'success'); }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); },
|
onClick: () => { openNativeVstGui(v.name || v.id, selectedTrackId, v.path, v.type).then(function (r) { if (r && (r.success || typeof r === 'string')) showToast('Đã mở GUI với ' + v.name, 'success'); }).catch(function (err) { showToast('Lỗi mở VST GUI: ' + (err.message || err), 'error'); }); },
|
||||||
className: 'text-[9px] bg-teal-800 hover:bg-teal-700 text-white px-1.5 py-0.5 rounded transition shrink-0',
|
className: 'text-[9px] bg-teal-800 hover:bg-teal-700 text-white px-1.5 py-0.5 rounded transition shrink-0',
|
||||||
title: 'Mở trong Carla (native GUI)'
|
title: 'Mở trong Carla (native GUI)'
|
||||||
}, '🎛')
|
}, '🎛')
|
||||||
@@ -8287,7 +8329,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
currentBeat < n.start_beat && newBeat >= n.start_beat
|
currentBeat < n.start_beat && newBeat >= n.start_beat
|
||||||
);
|
);
|
||||||
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
||||||
var pvCtx = resolveTrackInstrumentCtx(pvTrk, activeTracks);
|
var pvCtx = resolveSubTabInstrumentCtx(st, pvTrk, activeTracks);
|
||||||
ensureSonicInstrument(pvCtx);
|
ensureSonicInstrument(pvCtx);
|
||||||
playing.forEach(n => {
|
playing.forEach(n => {
|
||||||
if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine)) {
|
if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine)) {
|
||||||
@@ -8660,7 +8702,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
if (window.SonicSF) {
|
if (window.SonicSF) {
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
var clTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
var clTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
||||||
var clCtx = resolveTrackInstrumentCtx(clTrk, activeTracks);
|
var clCtx = resolveSubTabInstrumentCtx(st, clTrk, activeTracks);
|
||||||
ensureSonicInstrument(clCtx);
|
ensureSonicInstrument(clCtx);
|
||||||
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine)) {
|
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine)) {
|
||||||
if (!_routePreviewNote(clTrk && clTrk.id, clCtx, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, 'CLICK')) {
|
if (!_routePreviewNote(clTrk && clTrk.id, clCtx, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, 'CLICK')) {
|
||||||
@@ -8925,15 +8967,15 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
previewNodesRef.current = null;
|
previewNodesRef.current = null;
|
||||||
}
|
}
|
||||||
var dwTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
var dwTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
||||||
var dwCtx = resolveTrackInstrumentCtx(dwTrk, activeTracks);
|
var dwCtx = resolveSubTabInstrumentCtx(st, dwTrk, activeTracks);
|
||||||
var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
|
var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
|
||||||
var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
|
var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
|
||||||
var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000));
|
var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000));
|
||||||
if (isStandaloneSf() && isSfTrackEngine(dwTrk && dwTrk.synth_engine) && !shouldRouteCarla(dwTrk && dwTrk.synth_engine)) {
|
if (isStandaloneSf() && isSfTrackEngine(dwCtx.synthEngine) && !shouldRouteCarla(dwCtx.synthEngine)) {
|
||||||
if (!_routePreviewNote(dwTrk && dwTrk.id, dwCtx, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, 'DRAW')) {
|
if (!_routePreviewNote(dwTrk && dwTrk.id, dwCtx, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, 'DRAW')) {
|
||||||
if (window.SonicSF && window.SonicSF.playNote) {
|
if (window.SonicSF && window.SonicSF.playNote) {
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
window.SonicSF.playNote(dwPitch, Math.round(brushVelocityRef.current * 127), dwDurMs, ctx.currentTime, dwTrk ? dwTrk.instrumentProgram : undefined, null, dwCh, dwTrk ? dwTrk.synth_engine : undefined);
|
window.SonicSF.playNote(dwPitch, Math.round(brushVelocityRef.current * 127), dwDurMs, ctx.currentTime, dwCtx.program, null, dwCh, dwCtx.synthEngine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (window.SonicSF && window.SonicSF.playNote) {
|
} else if (window.SonicSF && window.SonicSF.playNote) {
|
||||||
@@ -8941,7 +8983,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
// playNote (FluidSynth — nhạc cụ THẬT của track). _playNoteFallback chỉ
|
// playNote (FluidSynth — nhạc cụ THẬT của track). _playNoteFallback chỉ
|
||||||
// là oscillator beep (sai âm với percussion/soundfont — user: note vẽ
|
// là oscillator beep (sai âm với percussion/soundfont — user: note vẽ
|
||||||
// mới nghe nhạc cụ track trước).
|
// mới nghe nhạc cụ track trước).
|
||||||
window.SonicSF.playNote(dwPitch, Math.round(brushVelocityRef.current * 127), dwDurMs, ctx.currentTime, dwTrk ? dwTrk.instrumentProgram : undefined, null, dwCh, dwTrk ? dwTrk.synth_engine : undefined);
|
window.SonicSF.playNote(dwPitch, Math.round(brushVelocityRef.current * 127), dwDurMs, ctx.currentTime, dwCtx.program, null, dwCh, dwCtx.synthEngine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -9034,7 +9076,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
// mastering FX của main out khi chain bật)
|
// mastering FX của main out khi chain bật)
|
||||||
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
|
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
|
||||||
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
||||||
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
|
var pvCtxInst = resolveSubTabInstrumentCtx(st, pvTrk, activeTracks);
|
||||||
if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine)) {
|
if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine)) {
|
||||||
if (!_routePreviewNote(pvTrk && pvTrk.id, pvCtxInst, p, brushVelocityRef.current || 0.8, durMs, 'DRAW')) {
|
if (!_routePreviewNote(pvTrk && pvTrk.id, pvCtxInst, p, brushVelocityRef.current || 0.8, durMs, 'DRAW')) {
|
||||||
if (window.SonicSF && window.SonicSF.playNote) {
|
if (window.SonicSF && window.SonicSF.playNote) {
|
||||||
@@ -9409,7 +9451,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
|
|
||||||
const renderKeybed = () => {
|
const renderKeybed = () => {
|
||||||
var kbTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
var kbTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
||||||
var kbCtx = resolveTrackInstrumentCtx(kbTrk, activeTracks);
|
var kbCtx = resolveSubTabInstrumentCtx(st, kbTrk, activeTracks);
|
||||||
ensureSonicInstrument(kbCtx);
|
ensureSonicInstrument(kbCtx);
|
||||||
const keys = [];
|
const keys = [];
|
||||||
for (let pitch = 127; pitch >= PITCH_START; pitch--) {
|
for (let pitch = 127; pitch >= PITCH_START; pitch--) {
|
||||||
@@ -11823,10 +11865,10 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!ap.plugin) { window.showToast && window.showToast('Chọn VST FX trước khi Load Carla', 'warning'); return; }
|
if (!ap.plugin) { window.showToast && window.showToast('Chọn VST FX trước khi Load Carla', 'warning'); return; }
|
||||||
window.SonicAPI.openInCarla(ap.plugin, ap.plugin_path).then(r => {
|
openNativeVstGui(ap.plugin, track.id, ap.plugin_path, kindFromPath(ap.plugin_path)).then(r => {
|
||||||
if (r && r.success) { window.showToast && window.showToast('Đã mở Carla với ' + ap.plugin + ' (VST FX) — chỉnh âm thanh trong Carla', 'success'); }
|
if (r && (r.success || typeof r === 'string')) { window.showToast && window.showToast('Đã mở GUI với ' + ap.plugin + ' (VST FX) — chỉnh âm thanh trong cửa sổ', 'success'); }
|
||||||
else { window.showToast && window.showToast('Không mở được Carla', 'error'); }
|
else { window.showToast && window.showToast('Không mở được VST GUI', 'error'); }
|
||||||
}).catch(err => window.showToast && window.showToast('Lỗi mở Carla: ' + (err.message || err), 'error'));
|
}).catch(err => window.showToast && window.showToast('Lỗi mở VST GUI: ' + (err.message || err), 'error'));
|
||||||
}}
|
}}
|
||||||
className="px-4 py-1.5 bg-teal-700 hover:bg-teal-600 text-white text-xs font-semibold rounded transition flex items-center gap-1 shrink-0"
|
className="px-4 py-1.5 bg-teal-700 hover:bg-teal-600 text-white text-xs font-semibold rounded transition flex items-center gap-1 shrink-0"
|
||||||
>
|
>
|
||||||
@@ -12952,12 +12994,12 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
const plugin = (ozState.carlaBridge && ozState.carlaBridge.plugin) || '';
|
const plugin = (ozState.carlaBridge && ozState.carlaBridge.plugin) || '';
|
||||||
if (!plugin) { window.showToast && window.showToast('Chọn VST FX trước khi Load Carla', 'warning'); return; }
|
if (!plugin) { window.showToast && window.showToast('Chọn VST FX trước khi Load Carla', 'warning'); return; }
|
||||||
window.SonicAPI.openInCarla(plugin, ozState.carlaBridge.plugin_path).then(r => {
|
openNativeVstGui(plugin, '', ozState.carlaBridge.plugin_path, kindFromPath(ozState.carlaBridge.plugin_path)).then(r => {
|
||||||
if (r && r.success) {
|
if (r && (r.success || typeof r === 'string')) {
|
||||||
setOzState(prev => ({ ...prev, carlaBridge: { ...(prev.carlaBridge || {}), connected: true } }));
|
setOzState(prev => ({ ...prev, carlaBridge: { ...(prev.carlaBridge || {}), connected: true } }));
|
||||||
window.showToast && window.showToast('Đã mở Carla với ' + plugin + ' (VST FX) — chỉnh âm thanh trong Carla', 'success');
|
window.showToast && window.showToast('Đã mở GUI với ' + plugin + ' (VST FX) — chỉnh âm thanh trong cửa sổ', 'success');
|
||||||
} else { window.showToast && window.showToast('Không mở được Carla', 'error'); }
|
} else { window.showToast && window.showToast('Không mở được VST GUI', 'error'); }
|
||||||
}).catch(err => window.showToast && window.showToast('Lỗi mở Carla: ' + (err.message || err), 'error'));
|
}).catch(err => window.showToast && window.showToast('Lỗi mở VST GUI: ' + (err.message || err), 'error'));
|
||||||
}}
|
}}
|
||||||
className="px-4 py-1.5 bg-teal-700 hover:bg-teal-600 text-white text-xs font-semibold rounded transition flex items-center gap-1 shrink-0"
|
className="px-4 py-1.5 bg-teal-700 hover:bg-teal-600 text-white text-xs font-semibold rounded transition flex items-center gap-1 shrink-0"
|
||||||
>
|
>
|
||||||
@@ -14281,14 +14323,34 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (nativeNotes.length) {
|
if (nativeNotes.length) {
|
||||||
window.SonicAPI.soundfontRender({ soundfont_id: sfId, bank: bank, program: prog !== undefined ? prog : 0, bpm: bpmVal, notes: nativeNotes }).then(function (res) {
|
window.SonicAPI.soundfontRender({ soundfont_id: sfId, bank: bank, program: prog !== undefined ? prog : 0, bpm: bpmVal, notes: nativeNotes }).then(async function (res) {
|
||||||
if (!res || !res.success || !res.url) return;
|
if (!res || !res.success || !res.url) return;
|
||||||
if (selectTokenRef.current !== (token || selectTokenRef.current)) return;
|
if (selectTokenRef.current !== (token || selectTokenRef.current)) return;
|
||||||
const audio = new Audio(API_BASE_URL + res.url);
|
// Preview qua masterBus (mastering FX main out khi chain bat; nguoc
|
||||||
audio.loop = !!isLoopingRef.current;
|
// lai qua dryInput) — truoc day new Audio() ra thang loa.
|
||||||
if (_midiFilePreviewAudio) { try { _midiFilePreviewAudio.pause(); } catch (e) {} }
|
try {
|
||||||
_midiFilePreviewAudio = audio;
|
const resp = await fetch(API_BASE_URL + res.url);
|
||||||
audio.play().catch(function () {});
|
const arrBuf = await resp.arrayBuffer();
|
||||||
|
const ctx2 = getAudioContext();
|
||||||
|
const decoded = await ctx2.decodeAudioData(arrBuf);
|
||||||
|
if (selectTokenRef.current !== (token || selectTokenRef.current)) return;
|
||||||
|
const src = ctx2.createBufferSource();
|
||||||
|
src.buffer = decoded;
|
||||||
|
src.loop = !!isLoopingRef.current;
|
||||||
|
const g = ctx2.createGain();
|
||||||
|
src.connect(g);
|
||||||
|
const mb = window.masterBus;
|
||||||
|
if (mb && mb.input && mb.dryInput) {
|
||||||
|
g.connect(masteringChainOn() ? mb.input : mb.dryInput);
|
||||||
|
} else {
|
||||||
|
g.connect(ctx2.destination);
|
||||||
|
}
|
||||||
|
if (_midiFilePreviewAudio) {
|
||||||
|
try { if (_midiFilePreviewAudio.stop) _midiFilePreviewAudio.stop(); else _midiFilePreviewAudio.pause(); } catch (e) {}
|
||||||
|
}
|
||||||
|
_midiFilePreviewAudio = src;
|
||||||
|
src.start();
|
||||||
|
} catch (e) { console.error('MIDI preview audio failed', e); }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const startedAtTime = startWallTime - loopStartSec;
|
const startedAtTime = startWallTime - loopStartSec;
|
||||||
@@ -22202,7 +22264,7 @@ const App = () => {
|
|||||||
// phải chơi ĐÚNG instrument đó. ensureSonicInstrument select channel đúng
|
// phải chơi ĐÚNG instrument đó. ensureSonicInstrument select channel đúng
|
||||||
// trước khi notes bắn (fire-and-forget — playNote tự load+retry nếu SF
|
// trước khi notes bắn (fire-and-forget — playNote tự load+retry nếu SF
|
||||||
// chưa xong).
|
// chưa xong).
|
||||||
const instCtx = resolveTrackInstrumentCtx(track, activeTracksRef.current || []);
|
const instCtx = resolveSubTabInstrumentCtx(st, track, activeTracksRef.current || []);
|
||||||
const instrumentProgram = instCtx.program;
|
const instrumentProgram = instCtx.program;
|
||||||
const synthEngine = instCtx.synthEngine;
|
const synthEngine = instCtx.synthEngine;
|
||||||
const mainCh = instCtx.ch;
|
const mainCh = instCtx.ch;
|
||||||
@@ -22215,7 +22277,10 @@ const App = () => {
|
|||||||
// the item window, so no absolute-session offset is applied anywhere here.
|
// the item window, so no absolute-session offset is applied anywhere here.
|
||||||
const routeCarla = shouldRouteCarla(synthEngine);
|
const routeCarla = shouldRouteCarla(synthEngine);
|
||||||
if (isStandaloneSf() && !routeCarla && isSfTrackEngine(synthEngine)) {
|
if (isStandaloneSf() && !routeCarla && isSfTrackEngine(synthEngine)) {
|
||||||
scheduleNativeSfItem(track, { startTime: 0, notes: midiNotes }, offsetSeconds, context, destNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
|
// synthEngine co the la instrument cua CHINH TAB (resolveSubTabInstrumentCtx)
|
||||||
|
// — pseudo-track de scheduleNativeSfItem render dung SF cua tab.
|
||||||
|
const nativeTrack = synthEngine ? Object.assign({}, track, { synth_engine: synthEngine }) : track;
|
||||||
|
scheduleNativeSfItem(nativeTrack, { startTime: 0, notes: midiNotes }, offsetSeconds, context, destNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current });
|
||||||
return; // native render cả tab → audio buffer (không per-note WASM/Carla)
|
return; // native render cả tab → audio buffer (không per-note WASM/Carla)
|
||||||
}
|
}
|
||||||
midiNotes.forEach(note => {
|
midiNotes.forEach(note => {
|
||||||
@@ -24476,7 +24541,9 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('Không thể nạp file từ Media Explorer: ' + e.message, 'error');
|
showToast('Không thể nạp file từ Media Explorer: ' + e.message, 'error');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
showToast('Không thể nạp file từ Media Explorer: thiếu dữ liệu file.', 'error');
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29798,7 +29865,14 @@ STRICT CONSTRAINTS:
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const f = e.dataTransfer.files && e.dataTransfer.files[0];
|
const dt = e.dataTransfer;
|
||||||
|
let f = dt && dt.files && dt.files[0];
|
||||||
|
if (!f && dt && dt.items) {
|
||||||
|
for (let i = 0; i < dt.items.length; i++) {
|
||||||
|
const it = dt.items[i];
|
||||||
|
if (it.kind === 'file' && typeof it.getAsFile === 'function') { f = it.getAsFile(); if (f) break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
if (/\.mid$|\.midi$/i.test(f.name || '')) {
|
if (/\.mid$|\.midi$/i.test(f.name || '')) {
|
||||||
handleDropMidiToNewTracks(f);
|
handleDropMidiToNewTracks(f);
|
||||||
@@ -29838,7 +29912,14 @@ STRICT CONSTRAINTS:
|
|||||||
if (f) loadFileOnTrack(track.id, f);
|
if (f) loadFileOnTrack(track.id, f);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const f = e.dataTransfer.files && e.dataTransfer.files[0];
|
const dt = e.dataTransfer;
|
||||||
|
let f = dt && dt.files && dt.files[0];
|
||||||
|
if (!f && dt && dt.items) {
|
||||||
|
for (let i = 0; i < dt.items.length; i++) {
|
||||||
|
const it = dt.items[i];
|
||||||
|
if (it.kind === 'file' && typeof it.getAsFile === 'function') { f = it.getAsFile(); if (f) break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
loadFileOnTrack(track.id, f);
|
loadFileOnTrack(track.id, f);
|
||||||
},
|
},
|
||||||
@@ -31295,10 +31376,10 @@ STRICT CONSTRAINTS:
|
|||||||
closeInstrumentSelector();
|
closeInstrumentSelector();
|
||||||
// TỰ ĐỘNG mở Carla với VSTi vừa chọn (desktop + Carla local) —
|
// TỰ ĐỘNG mở Carla với VSTi vừa chọn (desktop + Carla local) —
|
||||||
// Carla load sẵn plugin + keyboard ảo để preview realtime.
|
// Carla load sẵn plugin + keyboard ảo để preview realtime.
|
||||||
if (window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) {
|
if (canOpenNativeVstGui()) {
|
||||||
window.SonicAPI.openInCarla(v.id).then(function (r) {
|
openNativeVstGui(v.id, instrumentSelectorTrackId, v.path, v.type).then(function (r) {
|
||||||
if (r && r.success) showToast('Đã mở Carla với ' + (v.name || v.id) + ' — chọn preset, bấm keyboard để preview', 'success');
|
if (r && (r.success || typeof r === 'string')) showToast('Đã mở GUI với ' + (v.name || v.id) + ' — chỉnh tham số, bấm keyboard để preview', 'success');
|
||||||
}).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); });
|
}).catch(function (err) { showToast('Lỗi mở VST GUI: ' + (err.message || err), 'error'); });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
className: "w-full text-left px-3 py-2 text-sm rounded bg-zinc-800 hover:bg-violet-800 text-zinc-300 flex items-center justify-between gap-2"
|
className: "w-full text-left px-3 py-2 text-sm rounded bg-zinc-800 hover:bg-violet-800 text-zinc-300 flex items-center justify-between gap-2"
|
||||||
@@ -31386,18 +31467,17 @@ STRICT CONSTRAINTS:
|
|||||||
setInstrumentDropdownTrackId(null);
|
setInstrumentDropdownTrackId(null);
|
||||||
setInstrumentDropdownBtnRect(null);
|
setInstrumentDropdownBtnRect(null);
|
||||||
setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id);
|
setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id);
|
||||||
// TỰ ĐỘNG mở Carla với VSTi vừa chọn (desktop + Carla local) —
|
// TỰ ĐỘNG mở native GUI với VSTi vừa chọn (standalone) / Carla (browser).
|
||||||
// Carla load sẵn plugin, native GUI + keyboard ảo để preview realtime.
|
if (canOpenNativeVstGui()) {
|
||||||
if (window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) {
|
openNativeVstGui(v.id, instrumentDropdownTrackId, v.path, v.type).then(function (r) {
|
||||||
window.SonicAPI.openInCarla(v.id).then(function (r) {
|
if (r && (r.success || typeof r === 'string')) showToast('Đã mở GUI với ' + (v.name || v.id) + ' — chọn preset, bấm keyboard để preview', 'success');
|
||||||
if (r && r.success) showToast('Đã mở Carla với ' + (v.name || v.id) + ' — chọn preset, bấm keyboard để preview', 'success');
|
}).catch(function (err) { showToast('Lỗi mở VST GUI: ' + (err.message || err), 'error'); });
|
||||||
}).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); });
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
className: "flex-1 min-w-0 text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between"
|
className: "flex-1 min-w-0 text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between"
|
||||||
}, /*#__PURE__*/React.createElement("span", { className: "truncate" }, v.name || v.id), /*#__PURE__*/React.createElement("span", { className: "text-[10px] text-cyan-400 shrink-0 ml-1" }, v.type || "VST")),
|
}, /*#__PURE__*/React.createElement("span", { className: "truncate" }, v.name || v.id), /*#__PURE__*/React.createElement("span", { className: "text-[10px] text-cyan-400 shrink-0 ml-1" }, v.type || "VST")),
|
||||||
window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local ? /*#__PURE__*/React.createElement("button", {
|
window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local ? /*#__PURE__*/React.createElement("button", {
|
||||||
onClick: (e) => { e.stopPropagation(); window.SonicAPI.openInCarla(v.id).then(function (r) { if (r && r.success) { showToast('Đã mở Carla: ' + (v.name || v.id), 'success'); } }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); },
|
onClick: (e) => { e.stopPropagation(); openNativeVstGui(v.id, instrumentDropdownTrackId, v.path, v.type).then(function (r) { if (r && (r.success || typeof r === 'string')) { showToast('Đã mở GUI: ' + (v.name || v.id), 'success'); } }).catch(function (err) { showToast('Lỗi mở VST GUI: ' + (err.message || err), 'error'); }); },
|
||||||
className: "shrink-0 px-2 text-xs bg-zinc-800 hover:bg-teal-700 text-teal-300 border-l border-zinc-700",
|
className: "shrink-0 px-2 text-xs bg-zinc-800 hover:bg-teal-700 text-teal-300 border-l border-zinc-700",
|
||||||
title: "Mở trong Carla (native GUI)"
|
title: "Mở trong Carla (native GUI)"
|
||||||
}, "\uD83C\uDF9B") : null
|
}, "\uD83C\uDF9B") : null
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -58,16 +58,30 @@ window.SonicNativeAudio = window.SonicNativeAudio || {};
|
|||||||
console.warn('[NativeAudio] sf/ensure:', e.message);
|
console.warn('[NativeAudio] sf/ensure:', e.message);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
ensureVst2: function (trackId, pluginId, pluginPath) {
|
||||||
|
return post('/vst2/ensure', { track_id: trackId, plugin_id: pluginId || null, plugin_path: pluginPath || null, live: true }).catch(function (e) {
|
||||||
|
console.warn('[NativeAudio] vst2/ensure:', e.message);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ensureVst3: function (trackId, pluginId, pluginPath) {
|
||||||
|
return post('/vst3/ensure', { track_id: trackId, plugin_id: pluginId || null, plugin_path: pluginPath || null, live: true }).catch(function (e) {
|
||||||
|
console.warn('[NativeAudio] vst3/ensure:', e.message);
|
||||||
|
});
|
||||||
|
},
|
||||||
noteOn: function (kind, trackId, channel, pitch, velocity) {
|
noteOn: function (kind, trackId, channel, pitch, velocity) {
|
||||||
var path = kind === 'vst2' ? '/vst2/note_on' : '/sf/note_on';
|
var path = kind === 'vst2' ? '/vst2/note_on' : (kind === 'vst3' ? '/vst3/note_on' : '/sf/note_on');
|
||||||
|
// Rethrow sau warn: TrackInstrument can fallback autosample/WASM
|
||||||
|
// khi native khong san sang (DLL/plugin loi).
|
||||||
return post(path, { track_id: trackId, channel: channel || 0, pitch: pitch, velocity: velocity != null ? velocity : 100 }).catch(function (e) {
|
return post(path, { track_id: trackId, channel: channel || 0, pitch: pitch, velocity: velocity != null ? velocity : 100 }).catch(function (e) {
|
||||||
console.warn('[NativeAudio] note_on:', e.message);
|
console.warn('[NativeAudio] note_on:', e.message);
|
||||||
|
throw e;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
noteOff: function (kind, trackId, channel, pitch) {
|
noteOff: function (kind, trackId, channel, pitch) {
|
||||||
var path = kind === 'vst2' ? '/vst2/note_off' : '/sf/note_off';
|
var path = kind === 'vst2' ? '/vst2/note_off' : (kind === 'vst3' ? '/vst3/note_off' : '/sf/note_off');
|
||||||
return post(path, { track_id: trackId, channel: channel || 0, pitch: pitch }).catch(function (e) {
|
return post(path, { track_id: trackId, channel: channel || 0, pitch: pitch }).catch(function (e) {
|
||||||
console.warn('[NativeAudio] note_off:', e.message);
|
console.warn('[NativeAudio] note_off:', e.message);
|
||||||
|
throw e;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sfAudioStop: function (trackId) {
|
sfAudioStop: function (trackId) {
|
||||||
|
|||||||
@@ -20,9 +20,23 @@ window.TrackInstrument = window.TrackInstrument || {};
|
|||||||
this.dest = ctx ? (ctx.dest || null) : null;
|
this.dest = ctx ? (ctx.dest || null) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Native engine sẵn sàng cho track SF? (chỉ khi có sfId — đường native)
|
// Loại engine native cho track: 'sf' (soundfont), 'vst3'/'vst2' (VSTi có
|
||||||
|
// plugin_id) hay null (không native được → fallback WASM/autosample).
|
||||||
|
TrackInstrument.prototype._nativeKind = function () {
|
||||||
|
try {
|
||||||
|
if (this.sfId) return 'sf';
|
||||||
|
if (window.SonicNativeAudio && this.synthEngine && this.synthEngine.plugin_id) {
|
||||||
|
var t = String(this.synthEngine.type || '');
|
||||||
|
if (t.indexOf('vst2') !== -1) return 'vst2';
|
||||||
|
if (t.indexOf('vst3') !== -1 || t.indexOf('vst') !== -1) return 'vst3';
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Native engine sẵn sàng cho track? (sfId cho SF, plugin_id cho VSTi)
|
||||||
TrackInstrument.prototype._nativeReady = function () {
|
TrackInstrument.prototype._nativeReady = function () {
|
||||||
try { return !!(window.SonicNativeAudio && this.sfId); } catch (e) { return false; }
|
return this._nativeKind() !== null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Đảm bảo channel đã select đúng instrument trước khi play (fire-and-forget:
|
// Đảm bảo channel đã select đúng instrument trước khi play (fire-and-forget:
|
||||||
@@ -40,17 +54,48 @@ window.TrackInstrument = window.TrackInstrument || {};
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Phát qua SonicSF WASM (fallback khi native fail). SonicSF tự autosample
|
||||||
|
// VSTi (soundfontPlayer._playNoteFluid) nên không cần ensure riêng ở đây.
|
||||||
|
TrackInstrument.prototype._fallbackPlayNote = function (pitch, velocity, durationMs, startTime) {
|
||||||
|
try {
|
||||||
|
if (!window.SonicSF || !window.SonicSF.playNote) return;
|
||||||
|
this._ensure();
|
||||||
|
var durF = (durationMs != null ? durationMs : 500) || 500;
|
||||||
|
window.SonicSF.playNote(pitch, velocity != null ? velocity : 0.8, durF, startTime, this.program, this.dest, this.ch, this.synthEngine);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[TrackInstrument] fallback playNote error:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// velocity: router đã normalize int 1-127 (unifiedMidiRouter.normalizeVelocity).
|
// velocity: router đã normalize int 1-127 (unifiedMidiRouter.normalizeVelocity).
|
||||||
TrackInstrument.prototype.playNote = function (pitch, velocity, durationMs, startTime) {
|
TrackInstrument.prototype.playNote = function (pitch, velocity, durationMs, startTime) {
|
||||||
var vel = velocity != null ? velocity : 100;
|
var vel = velocity != null ? velocity : 100;
|
||||||
if (this._nativeReady()) {
|
var kind = this._nativeKind();
|
||||||
|
if (kind) {
|
||||||
try {
|
try {
|
||||||
var self = this;
|
var self = this;
|
||||||
var dur = (durationMs != null ? durationMs : 500) || 500;
|
var dur = (durationMs != null ? durationMs : 500) || 500;
|
||||||
// ensure native SF engine cho track (server dedup theo track_id)
|
if (kind === 'sf') {
|
||||||
window.SonicNativeAudio.ensureSf(this.trackId, this.sfId, this.bank, this.prog)
|
// ensure native SF engine cho track (server dedup theo track_id)
|
||||||
.catch(function () {});
|
window.SonicNativeAudio.ensureSf(this.trackId, this.sfId, this.bank, this.prog)
|
||||||
window.SonicNativeAudio.noteOn('sf', this.trackId, this.ch, pitch, vel);
|
.catch(function () {});
|
||||||
|
} else {
|
||||||
|
// VSTi live native (Phase 2): ensure + note-on qua bridge DLL.
|
||||||
|
// Server resolve plugin_id -> path; lỗi -> fallback WASM autosample.
|
||||||
|
if (kind === 'vst3') {
|
||||||
|
window.SonicNativeAudio.ensureVst3(this.trackId, this.synthEngine.plugin_id, this.synthEngine.plugin_path)
|
||||||
|
.catch(function () {});
|
||||||
|
} else {
|
||||||
|
window.SonicNativeAudio.ensureVst2(this.trackId, this.synthEngine.plugin_id, this.synthEngine.plugin_path)
|
||||||
|
.catch(function () {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.SonicNativeAudio.noteOn(kind, this.trackId, this.ch, pitch, vel).catch(function (err) {
|
||||||
|
// Native khong phat duoc (DLL/plugin loi) -> fallback WASM:
|
||||||
|
// SF track di SonicSF, VSTi di autosample (SonicSF tu ensure).
|
||||||
|
console.warn('[TrackInstrument] native noteOn fail -> WASM fallback:', err && err.message);
|
||||||
|
self._fallbackPlayNote(pitch, velocity, dur, startTime);
|
||||||
|
});
|
||||||
// ponytail: TrackInstrument không biết audioCtx → bỏ startTime
|
// ponytail: TrackInstrument không biết audioCtx → bỏ startTime
|
||||||
// offset (delay = duration); thêm scheduling chính xác khi cần
|
// offset (delay = duration); thêm scheduling chính xác khi cần
|
||||||
setTimeout(function () { self.noteOff(pitch); }, dur + 40);
|
setTimeout(function () { self.noteOff(pitch); }, dur + 40);
|
||||||
@@ -60,20 +105,20 @@ window.TrackInstrument = window.TrackInstrument || {};
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback SonicSF (WASM)
|
// Fallback SonicSF (WASM)
|
||||||
try {
|
this._fallbackPlayNote(pitch, velocity, durationMs, startTime);
|
||||||
if (!window.SonicSF || !window.SonicSF.playNote) return;
|
|
||||||
this._ensure();
|
|
||||||
var durF = (durationMs != null ? durationMs : 500) || 500;
|
|
||||||
window.SonicSF.playNote(pitch, velocity != null ? velocity : 0.8, durF, startTime, this.program, this.dest, this.ch, this.synthEngine);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('[TrackInstrument] playNote error:', e);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
TrackInstrument.prototype.noteOff = function (pitch) {
|
TrackInstrument.prototype.noteOff = function (pitch) {
|
||||||
if (this._nativeReady()) {
|
var kind = this._nativeKind();
|
||||||
try { window.SonicNativeAudio.noteOff('sf', this.trackId, this.ch, pitch); return; } catch (e) {}
|
if (kind) {
|
||||||
|
try {
|
||||||
|
window.SonicNativeAudio.noteOff(kind, this.trackId, this.ch, pitch).catch(function () {
|
||||||
|
// Native note-off fail -> stop qua WASM (neu note da fallback)
|
||||||
|
try { if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(this.ch, pitch); } catch (e) {}
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
// Belt-and-suspenders: stopNote WASM vo hai neu khong co note dang phat.
|
||||||
try {
|
try {
|
||||||
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(this.ch, pitch);
|
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(this.ch, pitch);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|||||||
@@ -3,7 +3,12 @@
|
|||||||
// hóa UnifiedMidiEvent → UnifiedMidiRouter → engine theo trackId. Một điểm
|
// hóa UnifiedMidiEvent → UnifiedMidiRouter → engine theo trackId. Một điểm
|
||||||
// dispatch duy nhất: activeVoiceTracker đếm note-on/off đúng (hết stuck
|
// dispatch duy nhất: activeVoiceTracker đếm note-on/off đúng (hết stuck
|
||||||
// notes), panicAllNotesOff() quét toàn bộ voice khi đổi instrument/engine
|
// notes), panicAllNotesOff() quét toàn bộ voice khi đổi instrument/engine
|
||||||
// giữa chừng. Chưa nối vào app (T5–T7 sẽ đăng ký engine + dispatch).
|
// giữa chừng.
|
||||||
|
// Trạng thái nối (đối chiếu GIAI_PHAP 2026-08): ĐÃ nối cho PREVIEW — app.jsx
|
||||||
|
// _routeNoteOn/_routeNoteOff (L212-260) đăng ký TrackInstrument per-track và
|
||||||
|
// dispatch qua router (keybed/click/draw/timeline scheduler gọi _routePreviewNote
|
||||||
|
// trước, chỉ fallback SonicSF.playNote khi router không xử lý). CHƯA nối cho
|
||||||
|
// timeline scheduler per-item (vẫn gọi thẳng SonicSF.playNote / scheduleNativeSfItem).
|
||||||
window.SonicUnifiedMidiRouter = window.SonicUnifiedMidiRouter || {};
|
window.SonicUnifiedMidiRouter = window.SonicUnifiedMidiRouter || {};
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="vi">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>VST GUI</title>
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: dark; }
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; background: #16161a; color: #e4e4e7; }
|
||||||
|
header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: #1e1e24; border-bottom: 1px solid #2d2d35; position: sticky; top: 0; z-index: 10; }
|
||||||
|
header h1 { font-size: 13px; margin: 0; font-weight: 600; color: #7dd3fc; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
header .meta { font-size: 10px; color: #71717a; margin-top: 2px; }
|
||||||
|
#params { padding: 10px 12px; }
|
||||||
|
.param { display: grid; grid-template-columns: minmax(0,1fr) 60px; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid #26262d; }
|
||||||
|
.param .title { font-size: 11px; color: #d4d4d8; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.param .val { font-size: 10px; font-family: ui-monospace, monospace; color: #7dd3fc; text-align: right; }
|
||||||
|
input[type=range] { width: 100%; accent-color: #0ea5e9; }
|
||||||
|
#status { padding: 8px 12px; font-size: 11px; color: #a1a1aa; }
|
||||||
|
#status.err { color: #f87171; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<h1 id="title">VST GUI</h1>
|
||||||
|
<div class="meta" id="meta"></div>
|
||||||
|
</div>
|
||||||
|
<button id="closeBtn" style="background:#3f3f46;border:1px solid #52525b;color:#f4f4f5;font-size:11px;padding:4px 10px;border-radius:4px;cursor:pointer;">Đóng</button>
|
||||||
|
</header>
|
||||||
|
<div id="params"></div>
|
||||||
|
<div id="status">Đang tải tham số…</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const qs = new URLSearchParams(window.location.search);
|
||||||
|
const trackId = qs.get('track') || '';
|
||||||
|
const pluginId = qs.get('plugin') || '';
|
||||||
|
const el = (id) => document.getElementById(id);
|
||||||
|
|
||||||
|
el('title').textContent = 'VST GUI - ' + pluginId;
|
||||||
|
el('meta').textContent = 'track=' + trackId;
|
||||||
|
|
||||||
|
const invoke = (cmd, args) => {
|
||||||
|
if (window.__TAURI__ && window.__TAURI__.core) {
|
||||||
|
return window.__TAURI__.core.invoke(cmd, args);
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('Tauri core unavailable'));
|
||||||
|
};
|
||||||
|
|
||||||
|
let params = [];
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const box = el('params');
|
||||||
|
box.innerHTML = '';
|
||||||
|
if (!params.length) {
|
||||||
|
el('status').textContent = 'Plugin không có tham số (hoặc chưa nạp).';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el('status').textContent = params.length + ' tham số — kéo slider để đổi giá trị.';
|
||||||
|
params.forEach((p) => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'param';
|
||||||
|
const title = document.createElement('div');
|
||||||
|
title.className = 'title';
|
||||||
|
title.textContent = p.title || ('Param ' + p.param_id);
|
||||||
|
title.title = title.textContent;
|
||||||
|
const val = document.createElement('div');
|
||||||
|
val.className = 'val';
|
||||||
|
const range = document.createElement('input');
|
||||||
|
range.type = 'range';
|
||||||
|
range.min = 0;
|
||||||
|
range.max = 1;
|
||||||
|
range.step = 0.001;
|
||||||
|
range.value = Math.min(1, Math.max(0, p.value || 0));
|
||||||
|
val.textContent = (p.value || 0).toFixed(3);
|
||||||
|
let dragging = false;
|
||||||
|
range.addEventListener('input', () => {
|
||||||
|
val.textContent = Number(range.value).toFixed(3);
|
||||||
|
});
|
||||||
|
range.addEventListener('change', () => {
|
||||||
|
invoke('set_vst_param', { trackId: trackId, pluginId: pluginId, paramId: p.param_id, value: Number(range.value) })
|
||||||
|
.catch((err) => { el('status').className = 'err'; el('status').textContent = 'set_vst_param lỗi: ' + (err && (err.message || err)); });
|
||||||
|
});
|
||||||
|
row.appendChild(title);
|
||||||
|
row.appendChild(val);
|
||||||
|
row.appendChild(range);
|
||||||
|
box.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync 2 chiều: native editor đổi param → Rust emit vst_param_changed → cập nhật slider.
|
||||||
|
function onParamChanged(e) {
|
||||||
|
const d = e.payload || {};
|
||||||
|
if (d.param_id === undefined) return;
|
||||||
|
const p = params.find((x) => x.param_id === d.param_id);
|
||||||
|
if (p) {
|
||||||
|
p.value = d.value;
|
||||||
|
const ranges = document.querySelectorAll('input[type=range]');
|
||||||
|
const idx = params.indexOf(p);
|
||||||
|
if (ranges[idx]) ranges[idx].value = Math.min(1, Math.max(0, d.value));
|
||||||
|
const vals = document.querySelectorAll('.val');
|
||||||
|
if (vals[idx]) vals[idx].textContent = Number(d.value).toFixed(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
invoke('get_vst_params', { trackId: trackId, pluginId: pluginId }).then((list) => {
|
||||||
|
params = list || [];
|
||||||
|
render();
|
||||||
|
}).catch((err) => {
|
||||||
|
el('status').className = 'err';
|
||||||
|
el('status').textContent = 'get_vst_params lỗi: ' + (err && (err.message || err)) + ' — plugin chưa mở được (kiểm tra log Rust / bridge DLL).';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
el('closeBtn').addEventListener('click', () => {
|
||||||
|
invoke('close_vst_editor', { trackId: trackId, pluginId: pluginId }).catch(() => {});
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
invoke('close_vst_editor', { trackId: trackId, pluginId: pluginId }).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.__TAURI__ && window.__TAURI__.event) {
|
||||||
|
window.__TAURI__.event.listen('vst_param_changed', onParamChanged).catch(() => {});
|
||||||
|
}
|
||||||
|
init();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+2
@@ -2880,6 +2880,8 @@ dependencies = [
|
|||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
|
"webview2-com",
|
||||||
|
"windows-core 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ tauri = { version = "2", features = [] }
|
|||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
raw-window-handle = "0.6"
|
raw-window-handle = "0.6"
|
||||||
|
webview2-com = "0.38"
|
||||||
|
windows-core = "0.61"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
mod vst_gui;
|
mod vst_gui;
|
||||||
|
mod permissions;
|
||||||
|
|
||||||
struct EngineProcess(Mutex<Option<CommandChild>>);
|
struct EngineProcess(Mutex<Option<CommandChild>>);
|
||||||
|
|
||||||
@@ -77,6 +78,9 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.invoke_handler(tauri::generate_handler![vst_gui::open_vst_gui, vst_gui::set_vst_param, vst_gui::get_vst_params, vst_gui::close_vst_editor])
|
.invoke_handler(tauri::generate_handler![vst_gui::open_vst_gui, vst_gui::set_vst_param, vst_gui::get_vst_params, vst_gui::close_vst_editor])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
permissions::install(
|
||||||
|
&app.get_webview_window("main").expect("main window missing"),
|
||||||
|
);
|
||||||
vst_gui::init(app.handle());
|
vst_gui::init(app.handle());
|
||||||
let res_dir = app
|
let res_dir = app
|
||||||
.path()
|
.path()
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Standalone permissions (Bug 5): WebView2 auto-ALLOW mọi permission request
|
||||||
|
// (MIDI input, microphone, File System Access) — không hiện popup hỏi quyền.
|
||||||
|
// WebView2 mặc định DENY khi không có handler ICoreWebView2::add_PermissionRequested.
|
||||||
|
#![cfg(windows)]
|
||||||
|
|
||||||
|
use webview2_com::Microsoft::Web::WebView2::Win32::*;
|
||||||
|
use webview2_com::PermissionRequestedEventHandler;
|
||||||
|
|
||||||
|
pub fn install(window: &tauri::WebviewWindow) {
|
||||||
|
let _ = window.with_webview(|webview| {
|
||||||
|
unsafe {
|
||||||
|
if let Ok(core) = webview.controller().CoreWebView2() {
|
||||||
|
let handler = PermissionRequestedEventHandler::create(Box::new(
|
||||||
|
|_sender: Option<ICoreWebView2>,
|
||||||
|
args: Option<ICoreWebView2PermissionRequestedEventArgs>| {
|
||||||
|
if let Some(args) = args {
|
||||||
|
args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
));
|
||||||
|
let mut token = 0i64;
|
||||||
|
let _ = core.add_PermissionRequested(&handler, &mut token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user