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 ln query status, ni 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 trc tiếp thư mc cha carla.exe (nhp tay, không cn picker)
const [pmCarlaPathInput, setPmCarlaPathInput] = React.useState('');
// D7: trng 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 tht (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 nhc c THT ca track). _playNoteFallback ch
// là oscillator beep (sai âm vi percussion/soundfont user: note v
// mi nghe nhc 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 nhc c THT). _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 thi gian bm phím durationMs ln (5s)
// ch là auto-off phòng h; mouseup/mouseleave gi stopNote dng
// NGAY (trưc đây 500ms note t tt gia chng 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) dng bng 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
// Dng note khi th phím tránh kt âm (loop liên tc) vi 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 chut ra khi phím dng note ca 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 cng 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); cp nht trong updatePlayhead khi bridge active.
const bridgePlayheadSampleRef = useRef(0);
// Resume main/session play khi ri PIANO ROLL tab: m piano roll lúc main
// đang play bm Space (play piano roll) stopAllPlayback dng main
// quay li 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 ~mi giây: C++ A13 cp nht timeline anchor, ti ưu cho
// seek/loop (Rust pump + C++ không t biết v trí; JS là ngun duy nht).
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 ni dung section tab) thay đi dng + schedule li t
// playhead hin ti đ item mi phát đúng v trí mi (không phát ni 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 ni 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 ni 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 ngt khi graph (v
// masterBus qua updateSfRouting ln 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 bng sampleOffset đy thng 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 chng note-on tr sau Stop: CH khi là timeline (startTime tht).
const guardPlay = startTime != null ? function () { return isPlayingRef.current; } : function () { return true; };
const trkId = track ? track.id : 0;
// D8: track đi instrument gi CC0/CC32 (bank 0) + program change qua
// bridge (A12) mt ln mi program mi mi 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 phi
// qua mastering FX (khi bt) 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 chng 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 bng setTimeout preview, timing gn đú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 tc 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); }
// Dng trit đ: noteoff tng note + hy 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>