diff --git a/app/api/v1/native.py b/app/api/v1/native.py
index 4e94b98..6b61fac 100644
--- a/app/api/v1/native.py
+++ b/app/api/v1/native.py
@@ -8,6 +8,7 @@ import os
from app.core.native_audio_service import get_service
from app.core.render_engine import _find_sf2_path
+from app.core.vst_engine import get_plugin_manager
router = APIRouter()
@@ -47,7 +48,8 @@ class SfNoteOffRequest(BaseModel):
class Vst2EnsureRequest(BaseModel):
track_id: str
- plugin_path: str
+ plugin_path: Optional[str] = None
+ plugin_id: Optional[str] = None # resolve path server-side
live: bool = True
@@ -63,6 +65,23 @@ class Vst2NoteOffRequest(BaseModel):
channel: int = 0
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):
note: int = 60
@@ -89,6 +108,24 @@ def _svc():
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")
async def status():
return _svc().status()
@@ -142,8 +179,12 @@ async def sf_audio_stop(req: SfNoteOffRequest):
@router.post("/vst2/ensure")
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:
- 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:
raise HTTPException(status_code=400, detail=str(e))
@@ -163,6 +204,31 @@ async def vst2_note_off(req: Vst2NoteOffRequest):
except RuntimeError as 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")
async def render(req: RenderRequest):
diff --git a/app/core/native_audio_service.py b/app/core/native_audio_service.py
index 420f0a2..4bbd8a9 100644
--- a/app/core/native_audio_service.py
+++ b/app/core/native_audio_service.py
@@ -30,8 +30,8 @@ _NATIVE_DIR = (
or os.path.join(settings.BASE_DIR, "native_host", "build", "Release")
)
-_ERR = None
-_LOCK = threading.Lock()
+_ERR = None # khoi tao buffer sau khi dinh nghia _errbuf()
+_LOCK = threading.RLock() # reentrant: _hidden_hwnd() goi trong ensure_*
def native_dir() -> str:
@@ -49,6 +49,39 @@ def _errbuf():
def _errval(buf) -> str:
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:
return os.path.isfile(_dll("sf_host_bridge.dll"))
@@ -133,11 +166,21 @@ class _Vst3Ctx:
self.handle = 0
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)
h = ctypes.c_int32(0)
- self.handle = self.dll.SF_VST3_Load(plugin_path.encode("utf-8"), 0,
- ctypes.byref(w), ctypes.byref(h),
- _ERR, 256)
+ self.handle = self.dll.SF_VST3_Attach(plugin_path.encode("utf-8"),
+ plugin_name.encode("utf-8"),
+ _hidden_hwnd(),
+ ctypes.byref(w), ctypes.byref(h),
+ _ERR, 256)
return self.handle
def close(self):
@@ -509,10 +552,10 @@ class NativeAudioService:
raise RuntimeError(f"thiếu {_dll('vst3_host_bridge.dll')}")
dll = ctypes.WinDLL(_dll("vst3_host_bridge.dll"))
i32 = ctypes.c_int32
- dll.SF_VST3_Load.argtypes = [ctypes.c_char_p, ctypes.c_void_p,
- ctypes.POINTER(i32), ctypes.POINTER(i32),
- ctypes.c_char_p, i32]
- dll.SF_VST3_Load.restype = i32
+ dll.SF_VST3_Attach.argtypes = [ctypes.c_char_p, ctypes.c_char_p,
+ ctypes.c_void_p, ctypes.POINTER(i32),
+ ctypes.POINTER(i32), ctypes.c_char_p, i32]
+ dll.SF_VST3_Attach.restype = i32
dll.SF_VST3_SendNoteOn.argtypes = [i32, i32, i32, i32]
dll.SF_VST3_SendNoteOn.restype = i32
dll.SF_VST3_SendNoteOff.argtypes = [i32, i32, i32]
@@ -543,7 +586,7 @@ class NativeAudioService:
ctx = _Vst3Ctx(dll)
h = ctx.create(plugin_path)
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)
if live:
rc = ctx.AudioStart(h, self.sample_rate, self.block_size, _ERR, 256)
diff --git a/app/main.py b/app/main.py
index 5e6393b..7ed1b6e 100644
--- a/app/main.py
+++ b/app/main.py
@@ -134,6 +134,15 @@ async def get_index():
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="
vst_gui.html not found
", status_code=404)
@app.get("/favicon.svg")
async def get_favicon():
favicon_path = os.path.join(settings.TEMPLATES_DIR, "favicon.svg")
diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx
index c0ca719..2aaa5c5 100644
--- a/app/static/js/app.jsx
+++ b/app/static/js/app.jsx
@@ -68,6 +68,25 @@ const resolveTrackInstrumentCtx = (track, tracks) => {
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
// 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.
@@ -126,6 +145,25 @@ const scheduleCarlaNote = (synthEngine, channel, pitch, velocity, startWallTime,
} 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 ────────────────────────────────
// 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).
@@ -277,7 +315,11 @@ const _routePreviewNote = (trackId, ctx, pitch, velocity, durationMs, sourceType
};
const _stopMidiFilePreview = () => {
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;
}
};
@@ -5945,7 +5987,7 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
),
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', {
- 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',
title: 'Mở trong Carla (native GUI)'
}, '🎛 Carla'),
@@ -6102,7 +6144,7 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
React.createElement('span', { className: 'truncate' }, v.name),
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', {
- 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',
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
);
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
- var pvCtx = resolveTrackInstrumentCtx(pvTrk, activeTracks);
+ var pvCtx = resolveSubTabInstrumentCtx(st, pvTrk, activeTracks);
ensureSonicInstrument(pvCtx);
playing.forEach(n => {
if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine)) {
@@ -8660,7 +8702,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (window.SonicSF) {
const ctx = getAudioContext();
var clTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
- var clCtx = resolveTrackInstrumentCtx(clTrk, activeTracks);
+ var clCtx = resolveSubTabInstrumentCtx(st, clTrk, activeTracks);
ensureSonicInstrument(clCtx);
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine)) {
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;
}
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 dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
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 (window.SonicSF && window.SonicSF.playNote) {
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) {
@@ -8941,7 +8983,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// playNote (FluidSynth — nhạc cụ THẬT của track). _playNoteFallback chỉ
// là oscillator beep (sai âm với percussion/soundfont — user: note vẽ
// 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)
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
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 (!_routePreviewNote(pvTrk && pvTrk.id, pvCtxInst, p, brushVelocityRef.current || 0.8, durMs, 'DRAW')) {
if (window.SonicSF && window.SonicSF.playNote) {
@@ -9409,7 +9451,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
const renderKeybed = () => {
var kbTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
- var kbCtx = resolveTrackInstrumentCtx(kbTrk, activeTracks);
+ var kbCtx = resolveSubTabInstrumentCtx(st, kbTrk, activeTracks);
ensureSonicInstrument(kbCtx);
const keys = [];
for (let pitch = 127; pitch >= PITCH_START; pitch--) {
@@ -11823,10 +11865,10 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {