feat: native host bridge integration — C++ bridge, Rust SHM, JS routing, build scripts, docs

- native_bridge/: InstrumentEngineManager multi-channel, sample-accurate, CC/program/pitchbend, transport, Vst3Instrument stub (HAVE_VST3SDK)
- src-tauri: shm.rs, bridge spawn + audio pump + health monitor, open_vst_gui, externalBin, commands
- app: UnifiedMidiRouter, NativeBridgeService, bridgeAudioNode, audioRoutingEngine, Plugin Manager UI, Bridge/WASM indicator, set_position sync
- build: 3 ps1 (force-added, build/ ignored), verify_bundle --check-bridge, CI workflow
- docs: TASKS.md, TEST_NOTES.md (Windows verify checklist), install/report updates
This commit is contained in:
locpham
2026-08-11 23:37:29 +07:00
parent aae0b05473
commit c3368d0a91
37 changed files with 2970 additions and 80 deletions
+118 -1
View File
@@ -1,7 +1,7 @@
import os, sys, uuid, json, tempfile, subprocess, time as _time, threading
import numpy as np
import soundfile as sf
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Header
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Header, Query
from fastapi.responses import FileResponse
from pydantic import BaseModel
from typing import Optional, Any
@@ -1178,3 +1178,120 @@ async def render_project(
return {"url": f"/static/audio/processed/{os.path.basename(result_path)}", "path": result_path}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Render failed: {str(e)}")
# ── E1/E2: NATIVE HOST BRIDGE (daw_vst_bridge) ──────────────────────────────
# IPC dir giống _pick_dir_via_tauri_bridge: %APPDATA%/SonicForgeDAW/ipc — Rust
# (src-tauri) tạo, engine chỉ đọc/ghi file request/response.
BRIDGE_IPC_DIR = os.path.join(os.environ.get("APPDATA") or os.path.expanduser("~"), "SonicForgeDAW", "ipc")
def _bridge_ipc_dir() -> Optional[str]:
if os.path.isdir(BRIDGE_IPC_DIR):
return BRIDGE_IPC_DIR
return None
@router.get("/bridge/status")
async def bridge_status(current_user: dict = Depends(get_current_user)):
"""E1: trạng thái native bridge. Rust ghi bridge_status (JSON) khi spawn/
health-check; nếu chưa có → probe tail bridge.log (dòng started/exists)."""
ipc = _bridge_ipc_dir()
if not ipc:
return {"connected": False, "reason": "no_ipc_dir"}
st = os.path.join(ipc, "bridge_status")
if os.path.exists(st):
try:
with open(st, "r", encoding="utf-8") as fh:
raw = fh.read() or "{}"
try:
data = json.loads(raw)
except Exception:
data = {}
data.setdefault("connected", True)
return data
except Exception:
pass
log = os.path.normpath(os.path.join(ipc, "..", "logs", "bridge.log"))
if os.path.exists(log):
try:
with open(log, "r", encoding="utf-8", errors="ignore") as fh:
tail = fh.read().splitlines()[-5:]
joined = "\n".join(tail).lower()
return {"connected": ("started" in joined or "exists=true" in joined), "log_tail": tail}
except Exception:
pass
return {"connected": False, "reason": "no_status_no_log"}
class BridgeLoadRequest(BaseModel):
name: str # tên asset hiển thị (sf_xxx / Vital.vst3 / ...)
path: Optional[str] = None # path tuyệt đối nếu đã biết
instrumentType: str = "SF2" # 'VST3'|'VST2'|'SF2'|'SF3'|'SFZ'
channel: Optional[int] = None
def _resolve_bridge_asset(name: str, instrument_type: str, path: Optional[str]) -> Optional[str]:
if path and os.path.exists(path):
return path
base = os.path.basename((path or name).replace("\\", "/"))
base_noext = os.path.splitext(base)[0].lower()
# sf2/sf3/sfz: storage/soundfonts + system + user plugin_dirs
for base_dir in [UPLOAD_SF_DIR, SYSTEM_SF_DIR]:
if not os.path.isdir(base_dir):
continue
for fname in os.listdir(base_dir):
cand = os.path.join(base_dir, fname)
if fname.lower().endswith((".sf2", ".sf3", ".sfz")) and \
(fname.lower() == base.lower() or os.path.splitext(fname)[0].lower() == base_noext):
return cand
ext = (".vst3" if instrument_type.upper() == "VST3" else
".vst2" if instrument_type.upper() == "VST2" else
".sfz" if instrument_type.upper() == "SFZ" else None)
if ext:
try:
from app.core.vst_engine import _load_user_plugin_dirs
dirs = _load_user_plugin_dirs() or []
except Exception:
dirs = []
if not dirs:
dirs = [settings.VST_DIR, settings.SOUNDFONT_DIR]
for base_dir in dirs:
if not os.path.isdir(base_dir):
continue
for root, _, files in os.walk(base_dir):
for fn in files:
if fn.lower().endswith(ext) and \
(fn.lower() == base.lower() or os.path.splitext(fn)[0].lower() == base_noext):
return os.path.join(root, fn)
return None
@router.post("/bridge/load")
async def bridge_load(req: BridgeLoadRequest, current_user: dict = Depends(get_current_user)):
"""E2: resolve path thật của asset rồi (a) ghi bridge_load.request cho Rust
watcher (nếu có) và (b) trả path để JS gọi invoke load_native_instrument
trực tiếp — pipeline hoạt động ngay, không chờ watcher."""
resolved = _resolve_bridge_asset(req.name, req.instrumentType, req.path)
if not resolved:
raise HTTPException(status_code=404, detail=f"Không tìm thấy asset: {req.name}")
wrote_request = False
ipc = _bridge_ipc_dir()
if ipc:
try:
req_file = os.path.join(ipc, "bridge_load.request")
if os.path.exists(req_file):
os.remove(req_file)
with open(req_file, "w", encoding="utf-8") as fh:
json.dump({"path": resolved, "type": req.instrumentType, "channel": req.channel}, fh)
wrote_request = True
except Exception:
pass
return {"success": True, "path": resolved, "type": req.instrumentType, "ipc_request_written": wrote_request}
@router.get("/bridge/log")
async def bridge_log(lines: int = Query(100, ge=1, le=2000), current_user: dict = Depends(get_current_user)):
"""E5: tail bridge.log cho UI debug. Log Rust ghi tại %APPDATA%/SonicForgeDAW/logs/bridge.log."""
log = os.path.normpath(os.path.join(BRIDGE_IPC_DIR, "..", "logs", "bridge.log"))
if not os.path.exists(log):
return {"lines": [], "path": log, "exists": False}
try:
with open(log, "r", encoding="utf-8", errors="ignore") as fh:
all_lines = fh.read().splitlines()
return {"lines": all_lines[-lines:], "path": log, "exists": True}
except Exception as e:
return {"lines": [], "path": log, "exists": False, "error": str(e)}
+30
View File
@@ -551,6 +551,36 @@ def resolve_preset_path(preset_id_or_path: str) -> str:
return ""
def resolve_plugin_path(name_or_path: str, ext: str = "") -> str:
"""E3: resolve path thật của plugin/soundfont cho Native Bridge (E2).
ext: '.vst3' | '.vst2' | '.sf2' | '.sf3' | '.sfz' — filter phần mở rộng
khi quét plugin_dirs. Trả '' nếu không tìm thấy. Chống path traversal:
chỉ tên file (không separator) hoặc path tồn tại được chấp nhận."""
if not name_or_path:
return ""
p = name_or_path
if os.path.isfile(p):
return p
base = os.path.basename(p.replace("\\", "/"))
base_noext = os.path.splitext(base)[0].lower()
want_ext = ext.lower() if ext else None
dirs = []
try:
dirs = list(_load_user_plugin_dirs()) or []
except Exception:
pass
dirs += [settings.VST_DIR, settings.SOUNDFONT_DIR]
for base_dir in dirs:
if not os.path.isdir(base_dir):
continue
for root, _, files in os.walk(base_dir):
for fn in files:
if want_ext and not fn.lower().endswith(want_ext):
continue
if fn.lower() == base.lower() or os.path.splitext(fn)[0].lower() == base_noext:
return os.path.join(root, fn)
return ""
def apply_preset_to_plugin(plugin, preset_id=None, preset_path=None, preset_data_b64=None) -> bool:
"""Gán preset lên plugin pedalboard: bytes nhúng (base64 .vstpreset) ưu
tiên, sau đó preset_id (thư viện), sau preset_path (file). Trả True nếu
+225 -35
View File
@@ -1432,6 +1432,32 @@ function getAudioContext() {
if (window.SonicSF && window.SonicSF.init) {
window.SonicSF.init(audioCtx);
}
// C6: bootstrap native bridge 1 lần — query status, nối audio sink + router.
if (window.NativeBridgeService && window.SonicMidiRouter && !window.__bridgeBootstrapped) {
window.__bridgeBootstrapped = true;
window.SonicMidiRouter.onFallback = function (cmd, ch, pitch, vel) {
if (!window.SonicSF) return;
if (cmd === 'PANIC') { if (window.SonicSF.stopAll) window.SonicSF.stopAll(); return; }
if (cmd === 'NOTE_ON') window.SonicSF.playNote(pitch, vel, undefined, undefined, undefined, undefined, ch);
else window.SonicSF.stopNote(ch, pitch);
};
(async function () {
try {
var st = await window.NativeBridgeService.queryStatus();
var connected = !!(st && st.connected);
window.SonicMidiRouter.setBridgeConnected(connected);
console.log('[Bridge] bootstrap connected=', connected);
if (connected) {
window.BridgeAudioNode.init(audioCtx);
window.NativeBridgeService.onAudio(function (l, r) { window.BridgeAudioNode.onAudio(l, r); });
if (window.AudioRoutingEngine) window.AudioRoutingEngine.connect(window.BridgeAudioNode, null, null);
}
} catch (e) {
console.warn('[Bridge] bootstrap error:', e);
window.SonicMidiRouter.setBridgeConnected(false);
}
})();
}
return audioCtx;
}
const formatTime = secs => {
@@ -5525,6 +5551,13 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
const [pmCarlaVersion, setPmCarlaVersion] = React.useState(0);
// Khai báo trực tiếp thư mục chứa carla.exe (nhập tay, không cần picker)
const [pmCarlaPathInput, setPmCarlaPathInput] = React.useState('');
// D7: trạng thái Native Bridge (query khi mở modal) — badge header.
const [bridgeStatus, setBridgeStatus] = React.useState(null);
React.useEffect(() => {
if (isOpen) {
window.SonicAPI.bridgeStatus().then(s => setBridgeStatus(s)).catch(() => setBridgeStatus(null));
}
}, [isOpen]);
React.useEffect(() => {
if (isOpen) {
window.SonicAPI.listPlugins()
@@ -5715,6 +5748,21 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
setSfUploadStatus('Error: ' + err.message);
}
};
// D7: Load asset vào Native Bridge — backend resolve path thật (E2) → JS
// invoke load_native_instrument (C2) → bridge C++ load vào channel 0.
const loadToBridge = async (name, path, type) => {
try {
const r = await window.SonicAPI.bridgeLoad({ name: name, path: path || null, instrumentType: type, channel: 0 });
if (!r || !r.path) { window.showToast && window.showToast('Không resolve được asset: ' + name, 'error'); return false; }
const ok = await window.NativeBridgeService.loadInstrument(r.path, type, 0);
window.showToast && window.showToast(ok ? ('Đã load vào Native Bridge: ' + r.path) : 'Bridge không khả dụng (dev mode?)', ok ? 'success' : 'warning');
if (ok) window.SonicAPI.bridgeStatus().then(s => setBridgeStatus(s)).catch(() => {});
return ok;
} catch (err) {
window.showToast && window.showToast('Lỗi load bridge: ' + (err.message || err), 'error');
return false;
}
};
const [pmTab, setPmTab] = React.useState('soundfont');
return React.createElement('div', {
className: 'fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm',
@@ -5799,11 +5847,16 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
React.createElement('h3', {
className: 'text-base font-bold text-cyan-400 flex items-center gap-2'
}, React.createElement('i', { 'data-lucide': 'zap', className: 'w-4 h-4' }), 'Plugin Manager (SoundFont / VSTi)'),
React.createElement('button', {
onClick: onClose,
className: 'text-zinc-500 hover:text-zinc-200 transition'
}, React.createElement('i', { 'data-lucide': 'x', className: 'w-4 h-4' }))
),
React.createElement('div', { className: 'flex items-center gap-2' },
React.createElement('span', {
className: `text-[9px] font-bold uppercase tracking-wider px-2 py-0.5 rounded border ${bridgeStatus && bridgeStatus.connected ? 'text-emerald-400 border-emerald-700 bg-emerald-900/30' : 'text-zinc-500 border-zinc-700 bg-zinc-800'}`,
title: 'Native Bridge (C++ engine) — ' + (bridgeStatus ? JSON.stringify(bridgeStatus).slice(0, 120) : 'chưa query')
}, bridgeStatus && bridgeStatus.connected ? '● Bridge ON' : '● Bridge OFF'),
React.createElement('button', {
onClick: onClose,
className: 'text-zinc-500 hover:text-zinc-200 transition'
}, React.createElement('i', { 'data-lucide': 'x', className: 'w-4 h-4' }))
)),
// Left-right body
React.createElement('div', {
className: 'flex flex-1 overflow-hidden',
@@ -5853,6 +5906,11 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
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'),
React.createElement('button', {
onClick: (e) => { e.stopPropagation(); loadToBridge(v.name || v.id, v.path, v.type || 'VST3'); },
className: 'text-[10px] bg-cyan-800 hover:bg-cyan-700 text-white px-2 py-1 rounded transition shrink-0',
title: 'Load vào Native Bridge (C++ engine)'
}, 'Load Bridge'),
React.createElement('span', { className: 'text-[10px] bg-violet-950/40 text-violet-400 px-2 py-0.5 rounded-full border border-violet-800/30' }, v.type || 'VST3')
)
)
@@ -5883,6 +5941,11 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
)
),
React.createElement('div', { className: 'flex items-center gap-2' },
React.createElement('button', {
onClick: (e) => { e.stopPropagation(); loadToBridge(sf.id, sf.file || sf.path, 'SF2'); },
className: 'text-[10px] bg-cyan-800 hover:bg-cyan-700 text-white px-2 py-1 rounded transition shrink-0',
title: 'Load vào Native Bridge (C++ engine)'
}, 'Load Bridge'),
React.createElement('span', { className: 'text-[10px] text-zinc-500' }, expanded ? '▾' : '▸'),
React.createElement('button', {
onClick: (e) => { e.stopPropagation(); setSfToDelete(sf); },
@@ -8192,7 +8255,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
playNativeSfNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
return;
}
window.SonicSF.playNote(n.pitch, (n.velocity || 0.8) * 127, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine);
scheduleMidiNoteDispatch(pvTrk, n.pitch, n.velocity || 0.8, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine);
});
}
}
@@ -8561,7 +8624,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine)) {
playNativeSfNote(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, undefined, 'pv_' + st.trackId);
} else {
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
scheduleMidiNoteDispatch(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
}
}
}
@@ -8829,7 +8892,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);
scheduleMidiNoteDispatch(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, ctx.currentTime, dwTrk ? dwTrk.instrumentProgram : undefined, null, dwCh, dwTrk ? dwTrk.synth_engine : undefined);
}
}
};
@@ -8927,10 +8990,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
playNativeSfNote(pvTrk, p, brushVelocityRef.current || 0.8, durMs, undefined, 'pvdraw_' + st.trackId);
} else if (window.SonicSF && window.SonicSF.playNote) {
var pvCtx = getAudioContext();
var pvVel = Math.round(brushVelocityRef.current * 127);
// playNote (FluidSynth — nhạc cụ THẬT). _playNoteFallback = oscillator
// beep sai âm (percussion/soundfont).
window.SonicSF.playNote(p, pvVel, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine);
scheduleMidiNoteDispatch(pvTrk, p, brushVelocityRef.current || 0.8, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine);
previewPitchRef.current = p;
}
// MIDI → Carla (track VSTi + ARM + Carla local): preview realtime
@@ -9320,7 +9382,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (kbNative) {
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
}
if (window.SonicSF && !kbNative) {
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127 });
} else if (window.SonicSF && !kbNative) {
// ⚠️ FIX: giữ note theo thời gian bấm phím — durationMs lớn (5s)
// chỉ là auto-off phòng hờ; mouseup/mouseleave gọi stopNote dừng
// NGAY (trước đây 500ms → note tự tắt giữa chừng khi giữ phím).
@@ -9345,7 +9409,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine)) {
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
}
if (window.SonicSF && !(isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine))) {
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127 });
} else if (window.SonicSF && !(isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine))) {
// giữ note khi kéo qua phím (mouse enter) — dừng bằng mouseup/leave
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
}
@@ -9362,7 +9428,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Dừng note khi thả phím — tránh kẹt âm (loop liên tục) với soundfont
try {
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0 });
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
} catch (e) {}
if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; }
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
@@ -9372,7 +9440,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
// Kéo chuột ra khỏi phím → dừng note của phím đó
try {
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0 });
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
} catch (e) {}
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
}
@@ -14063,7 +14133,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
if (isPlayingRef.current && cur && isMidiFile(cur) && (cur.handle || cur.path || cur.file_id || cur.fileId || (cur.kind === 'midi' && cur.name))) {
selectTokenRef.current++;
const token = selectTokenRef.current;
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
playMidiPreview(cur, token);
}
@@ -14539,7 +14609,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
if (isPlayingRef.current && cur && isMidiFile(cur) && (cur.handle || cur.path || cur.file_id || cur.fileId || (cur.kind === 'midi' && cur.name))) {
selectTokenRef.current++;
const token = selectTokenRef.current;
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
playMidiPreview(cur, token);
}
@@ -14710,7 +14780,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
if (cur && (cur.handle || cur.path || cur.file_id || cur.fileId || (cur.kind === 'midi' && cur.name))) {
selectTokenRef.current++;
const token = selectTokenRef.current;
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
playMidiPreview(cur, token);
}
@@ -15368,7 +15438,7 @@ const App = () => {
const _nowVst = _hasInst && !isSfInstrument;
if (_wasVst && !_nowVst && window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) {
window.SonicCarlaMidi.stopBridge();
try { if (window.SonicSF && window.SonicSF.stopAll) window.SonicSF.stopAll(); } catch (e) {}
try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && window.SonicSF.stopAll) window.SonicSF.stopAll(); } catch (e) {}
stopAllNativeSfNotes();
console.log('[Instrument] Carla bridge unloaded — track', trackId, 'switched from VSTi to non-VST');
}
@@ -15608,7 +15678,15 @@ const App = () => {
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
// Route MIDI input to ALL armed tracks on their dedicated channels
// Do NOT use raw MIDI hardware channel (msg.data[0] & 0x0F)
if (window.SonicSF) {
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
// D1: bridge active -> 1 cổng router (per-track channel alloc) →
// Rust SHM → C++ bridge; giữ SonicSF/Carla fallback ở nhánh else.
var bTracks = activeTracksRef.current || [];
bTracks.forEach(function (bt) {
if (!bt.isArmed) return;
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: bt.id, pitch: pitch, velocity: scaledVel / 127 }); } catch (e) {}
});
} else if (window.SonicSF) {
var allTracks = activeTracksRef.current || [];
var arSubs = subTabsRef && subTabsRef.current ? subTabsRef.current.filter(function(s) { return s.type === 'PIANO_ROLL' && s.isArmed; }) : [];
var armedTracks = allTracks.filter(function(t) { return t.isArmed; });
@@ -15677,7 +15755,13 @@ const App = () => {
}
// Stop the note on ALL tracks (not just armed) to prevent stuck notes
// when ARM is toggled off while a key is held
if (window.SonicSF && window.SonicSF.stopNote) {
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
// D1: bridge note-off qua router (channel trùng NOTE_ON — track.id).
var bStopTracks = activeTracksRef.current || [];
bStopTracks.forEach(function (bst) {
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: bst.id, pitch: pitch, velocity: 0 }); } catch (e) {}
});
} else if (window.SonicSF && window.SonicSF.stopNote) {
var stopTracks = activeTracksRef.current || [];
stopTracks.forEach(function(st) {
// Only stop channels that actually carry this track's notes —
@@ -16894,6 +16978,16 @@ const App = () => {
const [pluginManagerModalOpen, setPluginManagerModalOpen] = useState(false);
const [pluginsData, setPluginsData] = useState(null);
// D10: bridge indicator + "Ép dùng WASM" debug toggle.
const [bridgeUi, setBridgeUi] = useState({ connected: false, forceWasm: false });
React.useEffect(() => {
if (!window.NativeBridgeService) return;
setBridgeUi(s => ({ ...s, connected: !!window.NativeBridgeService.isBridgeConnected }));
window.NativeBridgeService.onStatusChange(function (st) {
setBridgeUi(s => ({ ...s, connected: !!st.connected }));
});
return () => window.NativeBridgeService.onStatusChange(null);
}, []);
const loadAudioBuffersForTracks = async (tracksList) => {
let hasLoadedAny = false;
@@ -17641,6 +17735,9 @@ const App = () => {
const isDraggingSubTabRef = useRef(false);
const handlePlayPauseRef = useRef(null);
const currentTimeRef = useRef(currentTime);
// D9: playhead sample counter cho native bridge (A11/A13 dùng để đồng bộ
// timeline sample-accurate); cập nhật trong updatePlayhead khi bridge active.
const bridgePlayheadSampleRef = useRef(0);
// Resume main/session play khi rời PIANO ROLL tab: mở piano roll lúc main
// đang play → bấm Space (play piano roll) → stopAllPlayback dừng main →
// quay lại MAIN/SECTION → CÂM. Lưu {offset, audioTime} lúc mở tab → khi
@@ -20624,6 +20721,25 @@ const App = () => {
};
const updatePlayhead = () => {
// D9: playhead sample counter cho bridge — C++ A11/A13 dùng để đồng bộ
// timeline sample-accurate (scheduling theo sampleOffset).
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected && (isPlaying || isPlayingRef.current)) {
try {
const _pctx = getAudioContext();
const _elapsed = _pctx.currentTime - (startAudioTimeRef.current || _pctx.currentTime);
bridgePlayheadSampleRef.current = Math.max(0, Math.floor((startOffsetTimeRef.current + _elapsed) * (_pctx.sampleRate || 44100)));
// set_position ~mỗi giây: C++ A13 cập nhật timeline anchor, tối ưu cho
// seek/loop (Rust pump + C++ không tự biết vị trí; JS là nguồn duy nhất).
try {
const _sr = _pctx.sampleRate || 44100;
if (!window.__bridgeLastPosSent) window.__bridgeLastPosSent = 0;
if (bridgePlayheadSampleRef.current - window.__bridgeLastPosSent > _sr) {
window.__bridgeLastPosSent = bridgePlayheadSampleRef.current;
window.NativeBridgeService.transport('set_position', bridgePlayheadSampleRef.current);
}
} catch (e2) {}
} catch (e) {}
}
// Realtime re-schedule: khi đang play (loop play) mà items (vị trí/speed/
// duration — kể cả nội dung section tab) thay đổi → dừng + schedule lại từ
// playhead hiện tại để item mới phát đúng vị trí mới (không phát nội dung
@@ -21287,6 +21403,12 @@ const App = () => {
const _prSubKeys = Object.keys(activeTrackNodesRef.current).filter(k => k.endsWith('_sub_' + _activeSub.trackId));
_prNode = _prSubKeys.length ? activeTrackNodesRef.current[_prSubKeys[0]] : null;
}
// D5: bridge active → nối BridgeAudioNode vào node track (sfEntry →
// FX chain → mastering) thay vì SonicSF.setOutputDestination.
if (_prNode && window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive() && window.BridgeAudioNode) {
window.AudioRoutingEngine.connect(window.BridgeAudioNode, _prNode, _activeSub.trackId);
return;
}
if (_prNode && window.SonicSF && window.SonicSF.setOutputDestination) {
if (_prNode.sfEntry) {
window.SonicSF.setOutputDestination(_prNode.sfEntry);
@@ -21333,6 +21455,11 @@ const App = () => {
// SF ALWAYS enters the track's own FX chain (sfEntry → sfModules when
// PWR ON). The ♪ button only switches the post-FX route (sfRouteGain /
// sfDryGain) to skip or include the Mastering FX Chain.
// D5: bridge active → nối vào node track (sfEntry → FX chain).
if (node && window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive() && window.BridgeAudioNode) {
window.AudioRoutingEngine.connect(window.BridgeAudioNode, node, t.id);
return;
}
if (node && node.sfEntry && window.SonicSF && window.SonicSF.setOutputDestination) {
console.log('[SFRoute] dest=sfEntry node', t.id, 'sfRouteGain=' + (node.sfRouteGain ? node.sfRouteGain.gain.value : 'MISSING'), 'sfDryGain=' + (node.sfDryGain ? node.sfDryGain.gain.value : 'MISSING'));
window.SonicSF.setOutputDestination(node.sfEntry);
@@ -21362,7 +21489,11 @@ const App = () => {
return;
}
}
if (window.SonicSF && window.SonicSF.setOutputDestination) {
// D5: bridge active + không track nào audible → ngắt khỏi graph (về
// masterBus qua updateSfRouting lần sau khi có track).
if (window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.AudioRoutingEngine.disconnect();
} else if (window.SonicSF && window.SonicSF.setOutputDestination) {
window.SonicSF.setOutputDestination(null);
}
} catch (e) {
@@ -21578,8 +21709,49 @@ const App = () => {
);
};
// D2: timeline MIDI note -> native bridge (khi bridge active) thay vì
// SonicSF (FluidSynth WASM). Giữ setTimeout scheduling (như SonicSF cũ);
// bridge tự render note-off sau duration. A11 (sample-accurate) sẽ thay
// setTimeout bằng sampleOffset đẩy thẳng vào SHM.
const scheduleMidiNoteDispatch = (track, pitch, velocity, durMs, startTime, program, destNode, ch, synthEngine) => {
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
const ctx = getAudioContext();
const startAt = startTime || ctx.currentTime; // undefined/null = phát ngay
const delayMs = Math.max(0, (startAt - ctx.currentTime) * 1000);
// Guard chống note-on trễ sau Stop: CHỈ khi là timeline (startTime thật).
const guardPlay = startTime != null ? function () { return isPlayingRef.current; } : function () { return true; };
const trkId = track ? track.id : 0;
// D8: track đổi instrument → gửi CC0/CC32 (bank 0) + program change qua
// bridge (A12) một lần mỗi program mới mỗi channel.
if (program !== undefined && program !== null) {
const lastP = window.__bridgeLastProgram || (window.__bridgeLastProgram = {});
if (lastP[trkId] !== program) {
lastP[trkId] = program;
try { window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: trkId, pitch: 0, velocity: 0, data2: 0 }); } catch (e) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: trkId, pitch: 32, velocity: 0, data2: 0 }); } catch (e) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'PROGRAM', channel: trkId, pitch: 0, velocity: 0, data2: program }); } catch (e) {}
}
}
setTimeout(function () {
if (!guardPlay()) return;
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: trkId, pitch: pitch || 60, velocity: velocity || 0.8 }); } catch (e) {}
}, delayMs);
setTimeout(function () {
if (!guardPlay()) return;
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: trkId, pitch: pitch || 60, velocity: 0 }); } catch (e) {}
}, delayMs + (durMs || 1000) + 30);
return;
}
window.SonicSF.playNote(pitch, velocity, durMs, startTime, program, destNode, ch, synthEngine);
};
const startTrackPlayback = offsetTime => {
const context = getAudioContext();
// D2: bridge active -> báo transport PLAY (bridge flush note-off cũ + đồng
// bộ timeline; A13 C++ xử lý arg1=playheadSamples sau này).
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
try { window.NativeBridgeService.transport('play'); } catch (e) {}
}
// ⚠️ FIX: đồng bộ mastering + Carla status NGAY khi play — MIDI item phải
// qua mastering FX (khi bật) và qua Carla bridge (khi VSTi loaded).
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
@@ -21675,8 +21847,8 @@ const App = () => {
const delay = noteStartSec - offsetTime;
const startTime = context.currentTime + delay;
if (!routeToCarla && window.SonicSF) {
window.SonicSF.playNote(
note.pitch || 60,
scheduleMidiNoteDispatch(
track, note.pitch || 60,
note.velocity || 0.8,
durationMs,
startTime,
@@ -21703,8 +21875,8 @@ const App = () => {
const playOffset = offsetTime - noteStartSec;
const remainingDurMs = (noteEndSec - offsetTime) * 1000;
if (!routeToCarla && window.SonicSF) {
window.SonicSF.playNote(
note.pitch || 60,
scheduleMidiNoteDispatch(
track, note.pitch || 60,
note.velocity || 0.8,
remainingDurMs,
context.currentTime,
@@ -21832,8 +22004,8 @@ const App = () => {
const startTime = context.currentTime + delay;
const playDurMs = (notePlayEndMain - noteStartMain) * 1000;
if (!subRouteCarla && window.SonicSF) {
window.SonicSF.playNote(
note.pitch || 60,
scheduleMidiNoteDispatch(
subTrack, note.pitch || 60,
note.velocity || 0.8,
playDurMs,
startTime,
@@ -21862,8 +22034,8 @@ const App = () => {
} else {
const remainingDurMs = (notePlayEndMain - offsetTime) * 1000;
if (!subRouteCarla && window.SonicSF) {
window.SonicSF.playNote(
note.pitch || 60,
scheduleMidiNoteDispatch(
subTrack, note.pitch || 60,
note.velocity || 0.8,
remainingDurMs,
context.currentTime,
@@ -21961,8 +22133,8 @@ const App = () => {
const delay = noteStartSec - offsetTime;
const startTime = context.currentTime + delay;
if (!routeToCarla && window.SonicSF) {
window.SonicSF.playNote(
note.pitch || 60,
scheduleMidiNoteDispatch(
track, note.pitch || 60,
note.velocity || 0.8,
durationMs,
startTime,
@@ -21977,8 +22149,8 @@ const App = () => {
} else {
const remainingDurMs = (noteEndSec - offsetTime) * 1000;
if (!routeToCarla && window.SonicSF) {
window.SonicSF.playNote(
note.pitch || 60,
scheduleMidiNoteDispatch(
track, note.pitch || 60,
note.velocity || 0.8,
remainingDurMs,
context.currentTime,
@@ -22043,7 +22215,7 @@ const App = () => {
// Carla — không play FluidSynth GM sai âm chồng lên)
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine));
if (!routeToCarla && window.SonicSF) {
window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine);
scheduleMidiNoteDispatch(track, note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine);
}
// MIDI items → Carla (track VSTi + Carla local): phát VSTi realtime
// (schedule theo audio clock bằng setTimeout — preview, timing gần đúng).
@@ -22079,7 +22251,7 @@ const App = () => {
// Ghost note → Carla bridge khi ghost track VSTi (chỉ route Carla)
var gRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth));
if (!gRouteCarla && window.SonicSF) {
window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth);
scheduleMidiNoteDispatch(ghostTrack, note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth);
}
if (gRouteCarla) scheduleCarlaNote(ghostSynth, ghostCh, note.pitch || 60, note.velocity || 0.8, schedTime, durMs);
}
@@ -22210,6 +22382,11 @@ const App = () => {
try { heldMidiNotesRef.current = {}; } catch (e) { }
stopMidiCapture();
stopAllNativeSfNotes();
// D2/D6: bridge active -> transport STOP (bridge flush toàn pitch, hết âm
// ngân tức thì; A13 C++ flush note-off).
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
try { window.NativeBridgeService.transport('stop'); } catch (e) {}
}
if (window.SonicSF) {
try { window.SonicSF.stopAll(); } catch (e) { console.warn('[Stop] stopAll error:', e); }
// Dừng triệt để: noteoff từng note + hủy scheduled note-on (hết âm stuck)
@@ -28515,6 +28692,19 @@ STRICT CONSTRAINTS:
className: "font-mono text-[20px] text-zinc-400 tabular-nums",
title: "Tổng thời gian dự án"
}, formatTime(projectEnd)), /*#__PURE__*/React.createElement("div", {
className: "flex items-center gap-1.5 ml-3",
title: bridgeUi.connected && !bridgeUi.forceWasm ? "Engine phát qua Native Bridge (C++ FluidSynth/sfizz/VST3)" : "Engine phát qua SonicSF WASM"
}, /*#__PURE__*/React.createElement("span", {
className: "text-[9px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded border " + (bridgeUi.connected && !bridgeUi.forceWasm ? "text-emerald-400 border-emerald-700 bg-emerald-900/30" : "text-zinc-500 border-zinc-700 bg-zinc-800")
}, bridgeUi.connected && !bridgeUi.forceWasm ? "Bridge" : "WASM"), /*#__PURE__*/React.createElement("button", {
onClick: () => {
const v = !bridgeUi.forceWasm;
setBridgeUi(s => ({ ...s, forceWasm: v }));
if (window.SonicMidiRouter && window.SonicMidiRouter.setForceWasm) window.SonicMidiRouter.setForceWasm(v);
},
className: `px-1.5 py-0.5 rounded text-[9px] font-bold border transition ${bridgeUi.forceWasm ? "bg-amber-600 text-black border-amber-500" : "bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-zinc-200"}`,
title: "Debug: ép dùng SonicSF WASM bỏ qua bridge"
}, "Ép WASM")), /*#__PURE__*/React.createElement("div", {
className: "flex-1"
})),(() => {
const dockPanels = {
File diff suppressed because one or more lines are too long
+5
View File
@@ -113,6 +113,11 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
getSoundfontCatalog: () => apiRequest('/api/v1/plugins/soundfonts/catalog', { method: 'GET' }),
listDefaultSoundfonts: () => apiRequest('/api/v1/plugins/default-soundfonts', { method: 'GET' }),
listSoundfontInstruments: (sfId) => apiRequest(`/api/v1/plugins/soundfont-instruments/${sfId}`, { method: 'GET' }),
// Native Host Bridge (daw_vst_bridge C++): trạng thái + load asset +
// tail bridge.log (E1/E2/E5).
bridgeStatus: () => apiRequest('/api/v1/bridge/status', { method: 'GET' }),
bridgeLoad: (payload) => apiRequest('/api/v1/bridge/load', { method: 'POST', body: JSON.stringify(payload) }),
bridgeLog: (lines = 100) => apiRequest(`/api/v1/bridge/log?lines=${lines}`, { method: 'GET' }),
getAIPresets: () => apiRequest('/api/v1/ai/presets', { method: 'GET' }),
saveAIPreset: (preset) => apiRequest('/api/v1/ai/presets', { method: 'POST', body: JSON.stringify(preset) }),
deleteAIPreset: (presetId) => apiRequest(`/api/v1/ai/presets/${presetId}`, { method: 'DELETE' }),
@@ -0,0 +1,40 @@
// app/static/js/services/audioRoutingEngine.js
// Routes the native bridge audio node into the DAW graph:
// bridge node -> track.sfEntry (FX rack chain) -> fader/pan -> masterBus
// Falls back to masterBus.input when no per-track sfEntry exists.
(function () {
var engine = {
_connected: false,
_trackId: null,
isConnected: function () { return this._connected; },
/** bridgeNode = window.BridgeAudioNode; trackCtx = track node ({ sfEntry, gainNode }). */
connect: function (bridgeNode, trackCtx, trackId) {
if (!bridgeNode || !bridgeNode.getOutputNode) return false;
this.disconnect();
var dest = null;
if (trackCtx && trackCtx.sfEntry) dest = trackCtx.sfEntry;
else if (trackCtx && trackCtx.gainNode) dest = trackCtx.gainNode;
else if (window.masterBus && window.masterBus.input) dest = window.masterBus.input;
if (!dest) {
console.warn('[AudioRoutingEngine] no destination (no trackCtx / masterBus)');
return false;
}
if (!bridgeNode.isReady()) bridgeNode.init();
bridgeNode.connect(dest);
this._connected = true;
this._trackId = trackId || null;
console.log('[AudioRoutingEngine] bridge audio -> ' + (trackId || 'masterBus'));
return true;
},
disconnect: function () {
if (window.BridgeAudioNode) window.BridgeAudioNode.disconnect();
this._connected = false;
this._trackId = null;
}
};
window.AudioRoutingEngine = engine;
})();
+72
View File
@@ -0,0 +1,72 @@
// app/static/js/services/bridgeAudioNode.js
// Audio sink: consumes 'bridge-audio' PCM frames (native bridge) and plays them
// through a ScriptProcessorNode into the WebAudio graph (track FX / master bus).
(function () {
var RING_DEPTH = 8; // 8 blocks * 256 samples ~= 46ms anti-underrun
var SP_BUFFER = 4096; // ScriptProcessor chunk (16 bridge blocks)
var _queue = [];
var _spn = null;
var _gainNode = null;
var _ctx = null;
var _initialized = false;
function _getCtx() {
if (_ctx) return _ctx;
if (typeof getAudioContext === 'function') _ctx = getAudioContext();
else if (window.__sharedAudioCtx) _ctx = window.__sharedAudioCtx;
else _ctx = new (window.AudioContext || window.webkitAudioContext)();
return _ctx;
}
function init(audioCtx) {
if (_initialized) return;
_ctx = audioCtx || _getCtx();
_gainNode = _ctx.createGain();
_gainNode.gain.value = 1.0;
_spn = _ctx.createScriptProcessor(SP_BUFFER, 0, 2);
_spn.onaudioprocess = function (e) {
var L = e.outputBuffer.getChannelData(0);
var R = e.outputBuffer.getChannelData(1);
L.fill(0); R.fill(0);
if (!_queue.length) return;
var f = _queue.shift();
L.set(f.l); R.set(f.r);
};
_spn.connect(_gainNode);
_initialized = true;
console.log('[BridgeAudioNode] initialized (ring depth ' + RING_DEPTH + ')');
}
function onAudio(l, r) {
if (!_initialized) init();
_queue.push({ l: new Float32Array(l), r: new Float32Array(r) });
if (_queue.length > RING_DEPTH) _queue.shift(); // drop oldest on overrun
}
function flush() { _queue = []; }
function getOutputNode() { return _gainNode; }
function disconnect() {
flush();
if (_spn && _gainNode) {
try { _spn.disconnect(); } catch (e) {}
try { _gainNode.disconnect(); } catch (e) {}
}
// Reset state so a later init() can rebuild (AudioRoutingEngine re-connect).
_initialized = false;
_spn = null;
_gainNode = null;
_ctx = null;
}
window.BridgeAudioNode = {
init: init,
onAudio: onAudio,
flush: flush,
getOutputNode: getOutputNode,
connect: function (dest) { if (_gainNode) _gainNode.connect(dest); },
disconnect: disconnect,
isReady: function () { return _initialized; }
};
})();
@@ -0,0 +1,150 @@
// app/static/js/services/nativeBridgeService.js
// JS client for the C++ Native Host Bridge (daw_vst_bridge.exe).
// All IPC goes through Tauri commands (Rust writes the shared memory) —
// WebView2 JS cannot map Windows shared memory directly.
(function () {
// Must match native_bridge/include/INativeInstrument.h enum InstrumentType
var INSTRUMENT_TYPE = { VST3: 0, VST2: 1, SF2: 2, SF3: 2, SFZ: 3 };
var service = {
isBridgeConnected: false,
activeInstrumentType: 'VST3',
_audioCb: null,
_statusCb: null,
_tauri: function () { return window.__TAURI__; },
/** D10: UI subscribes to bridge connection changes. cb({connected}) or null to clear. */
onStatusChange: function (cb) { this._statusCb = cb; },
_notifyStatus: function (connected) {
this.isBridgeConnected = !!connected;
if (this._statusCb) this._statusCb({ connected: !!connected });
},
_init: function () {
if (!this._tauri()) {
// Dev mode (Linux / plain browser): no Tauri -> log-only, app uses SonicSF.
console.log('[BridgeService] Dev mode: no __TAURI__, native bridge disabled.');
return;
}
var self = this;
try {
window.__TAURI__.event.listen('bridge-audio', function (e) {
if (self._audioCb) self._audioCb(e.payload.l, e.payload.r);
});
window.__TAURI__.event.listen('bridge-down', function () {
self._notifyStatus(false);
if (window.SonicMidiRouter) window.SonicMidiRouter.setBridgeConnected(false);
if (window.AudioRoutingEngine) window.AudioRoutingEngine.disconnect();
console.warn('[BridgeService] bridge-down event received.');
});
} catch (e) {
console.warn('[BridgeService] event listen failed:', e);
}
this.queryStatus();
},
queryStatus: async function () {
if (!this._tauri()) return { connected: false };
try {
var s = await window.__TAURI__.core.invoke('bridge_status');
this._notifyStatus(!!s.connected);
return s;
} catch (e) {
this._notifyStatus(false);
return { connected: false };
}
},
/**
* 1. LOAD NEW INSTRUMENT INTO BRIDGE (VST3 / VST2 / SF2 / SF3 / SFZ)
* instrumentType: 'VST3' | 'VST2' | 'SF2' | 'SF3' | 'SFZ'
* channel: MIDI channel to assign this instrument to (A10 multi-instance).
*/
loadInstrument: async function (filePath, instrumentType, channel) {
this.activeInstrumentType = instrumentType;
console.log('[BridgeService] Loading ' + instrumentType + ' asset: ' + filePath + ' ch=' + channel);
if (!this._tauri()) return false;
try {
await window.__TAURI__.core.invoke('load_native_instrument', {
path: filePath,
instrumentType: INSTRUMENT_TYPE[instrumentType] !== undefined ? INSTRUMENT_TYPE[instrumentType] : 0,
channel: channel === undefined ? 0 : channel
});
return true;
} catch (e) {
console.warn('[BridgeService] loadInstrument failed:', e);
return false;
}
},
/**
* 2. UNIFIED MIDI EVENT DISPATCH
* cmd: 'NOTE_ON' | 'NOTE_OFF' | 'CC' | 'PROGRAM' | 'PITCH_BEND'
* velocity 0..1; sampleOffset in samples within the current audio block.
* data2/data3 (A12): CC value / program / PB LSB|MSB.
*/
dispatchMidiEvent: function (cmd, channel, pitch, velocity, sampleOffset, data2, data3) {
if (!this._tauri()) return false;
var safeVelocity = Math.floor(Math.min(1.0, Math.max(0.0, velocity)) * 127);
var byteCmd;
switch (cmd) {
case 'CC': byteCmd = 0xB; break;
case 'PROGRAM': byteCmd = 0xC; break;
case 'PITCH_BEND': byteCmd = 0xE; break;
default: byteCmd = cmd === 'NOTE_ON' ? 0x9 : 0x8; break;
}
window.__TAURI__.core.invoke('push_midi_event', {
command: byteCmd,
channel: channel,
pitch: pitch,
velocity: safeVelocity,
data2: data2 === undefined ? 0 : data2,
data3: data3 === undefined ? 0 : data3,
sampleOffset: sampleOffset || 0
}).catch(function (e) { console.warn('[BridgeService] push_midi_event:', e); });
return true;
},
/**
* 3. OPEN FLOATING CHILD WINDOW NATIVE GUI
*/
openNativeGUI: async function (pluginId) {
if (!this._tauri()) return false;
try {
await window.__TAURI__.core.invoke('open_vst_gui', { pluginId: pluginId });
return true;
} catch (e) {
console.warn('[BridgeService] open_vst_gui:', e);
return false;
}
},
/**
* 4. TRANSPORT CONTROL ('play' | 'stop' | 'panic' | 'set_position')
* playhead: sample position (A13) — used by 'play' and 'set_position'.
*/
transport: function (kind, playhead) {
if (!this._tauri()) return false;
var args = { kind: kind };
if (playhead !== undefined) args.playhead = playhead;
window.__TAURI__.core.invoke('transport_control', args)
.catch(function (e) { console.warn('[BridgeService] transport_control:', e); });
return true;
},
/**
* 5. AUDIO SINK: register consumer of PCM frames from the bridge.
* cb(l: Float32Array, r: Float32Array)
*/
onAudio: function (cb) {
this._audioCb = cb;
}
};
window.NativeBridgeService = service;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { service._init(); });
} else {
service._init();
}
})();
+5
View File
@@ -74,6 +74,9 @@ window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null
window.SonicCarlaMidi = window.SonicCarlaMidi || {
shouldRoute: function (synthEngine, isArmed) {
try {
// D4: bridge active → VSTi do native bridge host, KHÔNG route Carla
// (tránh kép âm).
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) return false;
var c = window.SonicRuntime && window.SonicRuntime.capabilities;
if (!c || !c.features || !c.features.carla_local) return false;
if (!isArmed) return false;
@@ -85,6 +88,8 @@ window.SonicCarlaMidi = window.SonicCarlaMidi || {
// user đã chủ động bấm Play trên item đó).
shouldRoutePlayback: function (synthEngine) {
try {
// D4: bridge active → KHÔNG route Carla (bridge host VSTi).
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) return false;
var c = window.SonicRuntime && window.SonicRuntime.capabilities;
if (!c || !c.features || !c.features.carla_local) return false;
var se = synthEngine || {};
@@ -0,0 +1,73 @@
// app/static/js/services/unifiedMidiRouter.js
// Single MIDI entry point for Web MIDI keyboard + timeline playback.
// bridge connected -> NativeBridgeService.dispatchMidiEvent (Rust SHM -> C++ bridge)
// bridge down -> onFallback callback (app.jsx wires SonicSF/Carla path).
(function () {
var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15]; // skip 9 (percussion)
var router = {
bridgeConnected: false,
onFallback: null, // function(cmd, channel, pitch, velocity) — set by app.jsx
forceWasm: false, // debug: D10 "Ép dùng WASM"
_channels: {}, // trackId -> { ch, percussion }
_nextMelodicIdx: 0,
setBridgeConnected: function (flag) {
this.bridgeConnected = !!flag;
},
setForceWasm: function (flag) { this.forceWasm = !!flag; },
isBridgeActive: function () {
return this.bridgeConnected && !this.forceWasm && !!window.NativeBridgeService;
},
/** trackId -> MIDI channel; percussion (bank 128) -> ch 9. */
allocateChannel: function (trackId, isPercussion) {
if (this._channels[trackId]) return this._channels[trackId].ch;
var ch;
if (isPercussion) {
ch = 9;
} else {
ch = MELODIC_CHANNELS[this._nextMelodicIdx % MELODIC_CHANNELS.length];
this._nextMelodicIdx++;
}
this._channels[trackId] = { ch: ch, percussion: !!isPercussion };
return ch;
},
resetChannels: function () {
this._channels = {};
this._nextMelodicIdx = 0;
},
/**
* cmd: 'NOTE_ON' | 'NOTE_OFF' | 'CC' | 'PROGRAM' | 'PITCH_BEND'
* channel: MIDI channel (or trackId -> allocateChannel first)
* velocity: 0..1 (normalized); sampleOffset: samples within current block.
* data2/data3 (A12): CC value / program / PB LSB|MSB — passed to bridge only.
*/
pushEvent: function (opts) {
var cmd = opts.cmd, ch = opts.channel, pitch = opts.pitch;
var vel = (opts.velocity === undefined ? 1.0 : opts.velocity);
var sampleOffset = opts.sampleOffset || 0;
if (typeof ch === 'string') ch = this.allocateChannel(ch, opts.percussion);
if (ch === undefined || ch === null) ch = 0;
if (this.isBridgeActive()) {
window.NativeBridgeService.dispatchMidiEvent(cmd, ch, pitch, vel, sampleOffset, opts.data2, opts.data3);
return;
}
if (this.onFallback) this.onFallback(cmd, ch, pitch, vel);
},
/** Stop/panic -> bridge transport panic (flush all notes) + fallback local stopAll. */
panic: function () {
if (this.isBridgeActive() && window.NativeBridgeService.transport) {
window.NativeBridgeService.transport('panic');
}
if (this.onFallback) this.onFallback('PANIC', 0, 0, 0);
}
};
window.SonicMidiRouter = router;
})();
+4
View File
@@ -39,6 +39,10 @@
<script src="/static/js/services/storage.js?v=202608038200"></script>
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608101800"></script>
<script src="/static/js/services/nativeBridgeService.js?v=202608112200"></script>
<script src="/static/js/services/bridgeAudioNode.js?v=202608112200"></script>
<script src="/static/js/services/unifiedMidiRouter.js?v=202608112200"></script>
<script src="/static/js/services/audioRoutingEngine.js?v=202608112200"></script>
<script src="/static/js/services/aiGateway.js?v=202608037200"></script>
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>