fix: GUI VST native window qua bridge + audio path bridge (SF2 preview/play câm)
- open_vst_gui: bo WebviewWindowBuilder/thread, chi push_control type=4 (hwnd=0 -> bridge tao window) - main.cpp: create_native_vst_window (class SonicForge_Native_VST3_Class, 800x600, khong TOPMOST), tao trong ChannelWorker job, map guiWindows, capture arg2 by value (fix dangling) - app.jsx: guard isBridgeActive() 8 cho -> bridge active thi moi note di router -> pushEvent -> bridge (truoc day SF2 cam vi HAS_PYFLUIDSYNTH=FALSE -> /soundfont-render 501; VST3 path cu dung nativeSf/Carla) - E2E: SF2 NOTE_ON qua dispatchMidiEvent -> SHM peak 0.029745; Nexus GUI native hwnd OK (license/preset) - docs: TASKS.md + TEST_NOTES.md ghi batch fix + ket qua; gitignore vendor/junk
This commit is contained in:
+18
-8
@@ -1232,14 +1232,20 @@ def _resolve_bridge_asset(name: str, instrument_type: str, path: Optional[str])
|
||||
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]:
|
||||
sf_dirs = [UPLOAD_SF_DIR, SYSTEM_SF_DIR]
|
||||
try:
|
||||
from app.core.vst_engine import _load_user_plugin_dirs
|
||||
sf_dirs += _load_user_plugin_dirs() or []
|
||||
except Exception:
|
||||
pass
|
||||
for base_dir in dict.fromkeys(d for d in sf_dirs if d):
|
||||
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
|
||||
for root, _, files in os.walk(base_dir):
|
||||
for fname in files:
|
||||
if fname.lower().endswith((".sf2", ".sf3", ".sfz")) and \
|
||||
(fname.lower() == base.lower() or os.path.splitext(fname)[0].lower() == base_noext):
|
||||
return os.path.join(root, fname)
|
||||
ext = (".vst3" if instrument_type.upper() == "VST3" else
|
||||
".vst2" if instrument_type.upper() == "VST2" else
|
||||
".sfz" if instrument_type.upper() == "SFZ" else None)
|
||||
@@ -1249,8 +1255,12 @@ def _resolve_bridge_asset(name: str, instrument_type: str, path: Optional[str])
|
||||
dirs = _load_user_plugin_dirs() or []
|
||||
except Exception:
|
||||
dirs = []
|
||||
if not dirs:
|
||||
dirs = [settings.VST_DIR, settings.SOUNDFONT_DIR]
|
||||
# Luon gop VST_DIR/SOUNDFONT_DIR: neu chi dung user plugin_dirs,
|
||||
# VST trong C:/Program Files/Common Files/VST3 khong resolve
|
||||
# duoc -> bridge load fail -> VSTi cam / khong mo GUI.
|
||||
for d in (settings.VST_DIR, settings.SOUNDFONT_DIR):
|
||||
if d and d not in dirs:
|
||||
dirs.append(d)
|
||||
for base_dir in dirs:
|
||||
if not os.path.isdir(base_dir):
|
||||
continue
|
||||
|
||||
+68
-131
@@ -7,7 +7,7 @@ const {
|
||||
} = React;
|
||||
|
||||
// ── FastAPI Backend Configuration ──
|
||||
const API_BASE_URL = window.location.origin;
|
||||
const API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\/+$/, '');
|
||||
const API_AUDIO = `${API_BASE_URL}/api/v1/audio`;
|
||||
const API_MULTITRACK = `${API_BASE_URL}/api/v1/multitrack`;
|
||||
const API_TASKS = `${API_BASE_URL}/api/v1/audio/tasks`;
|
||||
@@ -143,38 +143,6 @@ const refreshCarlaStatus = () => {
|
||||
// Carla chưa chạy → TỰ ĐỘNG mở với VSTi của track rồi CHỜ OSC ready (poll
|
||||
// carla-status tối đa 10s) — nốt chỉ gửi khi bridge thực sự nhận được.
|
||||
// Có gate __carlaOpening chống spawn trùng (mỗi play chỉ mở 1 lần).
|
||||
const ensureCarlaForPlayback = (synthEngine) => {
|
||||
try {
|
||||
if (!window.SonicCarlaMidi || !window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)) return;
|
||||
if (!window.SonicAPI || !window.SonicAPI.carlaStatus) return;
|
||||
if (window.__carlaRunning === true) { flushCarlaNoteQueue(); return; }
|
||||
if (window.__carlaOpening) return;
|
||||
window.__carlaOpening = true;
|
||||
const finish = (ok) => {
|
||||
window.__carlaRunning = !!ok;
|
||||
window.__carlaOpening = false;
|
||||
flushCarlaNoteQueue();
|
||||
};
|
||||
window.SonicAPI.carlaStatus().then(st => {
|
||||
if (st && st.running) { finish(true); return; }
|
||||
const pid = synthEngine && synthEngine.plugin_id;
|
||||
if (!pid) { finish(false); return; }
|
||||
window.SonicAPI.openInCarla(pid).then(r => {
|
||||
if (!r || !r.success) { finish(false); return; }
|
||||
// Carla spawn (hoặc đã chạy) — poll tới khi OSC ready, mới flush nốt
|
||||
const deadline = Date.now() + 10000;
|
||||
const tick = () => {
|
||||
window.SonicAPI.carlaStatus().then(s2 => {
|
||||
if (s2 && s2.running) { finish(true); return; }
|
||||
if (Date.now() > deadline) { finish(false); return; }
|
||||
setTimeout(tick, 400);
|
||||
}).catch(() => finish(false));
|
||||
};
|
||||
tick();
|
||||
}).catch(() => finish(false));
|
||||
}).catch(() => finish(false));
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
|
||||
// ── Môi trường chạy: docker vs standalone ─────────────────────────────────
|
||||
@@ -5901,12 +5869,7 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
|
||||
)
|
||||
),
|
||||
React.createElement('div', { className: 'flex items-center gap-2' },
|
||||
(window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) && React.createElement('button', {
|
||||
onClick: (e) => { e.stopPropagation(); window.SonicAPI.openInCarla(v.id).then(function (r) { if (r && r.success) showToast('Đã mở Carla với ' + (v.name || v.id), 'success'); }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); },
|
||||
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', {
|
||||
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)'
|
||||
@@ -5977,39 +5940,6 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
|
||||
})
|
||||
)
|
||||
),
|
||||
// ── Carla Bridge section (desktop) — định vị carla.exe portable ──
|
||||
(window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.runtime === 'desktop') && React.createElement('div', { className: 'pt-4 mt-4 border-t border-[#383838]' },
|
||||
React.createElement('h4', { className: 'text-xs font-bold text-teal-400 uppercase mb-2' }, 'Carla Bridge (VSTi native GUI)'),
|
||||
React.createElement('p', { className: 'text-[10px] text-zinc-500 mb-2 break-all' },
|
||||
(window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_path) ?
|
||||
'Đã định vị: ' + window.SonicRuntime.capabilities.features.carla_path :
|
||||
'Chưa tìm thấy Carla. Bản Windows là zip portable (không cài đặt, không dùng PATH) — nhấn "Định vị Carla..." và chọn thư mục chứa carla.exe.'
|
||||
),
|
||||
React.createElement('div', { className: 'flex gap-2' },
|
||||
React.createElement('button', {
|
||||
onClick: locateCarla,
|
||||
className: 'px-3 py-1.5 bg-teal-800 hover:bg-teal-700 text-white text-xs font-semibold rounded transition flex items-center gap-1'
|
||||
}, React.createElement('i', { 'data-lucide': 'folder-search', className: 'w-3 h-3' }), 'Định vị Carla...'),
|
||||
(window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) && React.createElement('button', {
|
||||
onClick: () => { window.SonicAPI.openInCarla().then(function (r) { if (r && r.success) showToast('Đã mở Carla', 'success'); }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); },
|
||||
className: 'px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-teal-300 text-xs font-semibold rounded transition flex items-center gap-1'
|
||||
}, React.createElement('i', { 'data-lucide': 'play', className: 'w-3 h-3' }), 'Mở Carla')
|
||||
),
|
||||
React.createElement('div', { className: 'flex gap-2 mt-2' },
|
||||
React.createElement('input', {
|
||||
type: 'text',
|
||||
placeholder: 'Hoặc nhập thư mục chứa carla.exe (VD: D:/Tools/Carla)',
|
||||
value: pmCarlaPathInput,
|
||||
onChange: e => setPmCarlaPathInput(e.target.value),
|
||||
onKeyDown: e => { if (e.key === 'Enter') saveCarlaInput(); },
|
||||
className: 'flex-1 bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-xs text-zinc-300 focus:outline-none focus:border-teal-600'
|
||||
}),
|
||||
React.createElement('button', {
|
||||
onClick: saveCarlaInput,
|
||||
className: 'px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-teal-300 text-xs font-semibold rounded transition shrink-0'
|
||||
}, 'Lưu')
|
||||
)
|
||||
),
|
||||
// Plugin directories section (folder picker + save + scan)
|
||||
React.createElement('div', {
|
||||
className: 'pt-4 mt-4 border-t border-[#383838]'
|
||||
@@ -6068,11 +5998,6 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
|
||||
React.createElement('span', { className: 'w-16 shrink-0 text-zinc-500 font-mono text-[9px] truncate' }, v.type || 'VST'),
|
||||
React.createElement('span', { className: 'truncate' }, v.name),
|
||||
React.createElement('span', { className: 'text-[9px] text-zinc-600 font-mono truncate ml-auto' }, v.dir),
|
||||
(window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) && React.createElement('button', {
|
||||
onClick: () => { window.SonicAPI.openInCarla(null, v.path).then(function (r) { if (r && r.success) showToast('Đã mở Carla với ' + v.name, 'success'); }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); },
|
||||
className: 'text-[9px] bg-teal-800 hover:bg-teal-700 text-white px-1.5 py-0.5 rounded transition shrink-0',
|
||||
title: 'Mở trong Carla (native GUI)'
|
||||
}, '🎛')
|
||||
)
|
||||
),
|
||||
React.createElement('div', { className: 'text-[10px] font-bold text-amber-300 uppercase flex items-center gap-1 mt-2' },
|
||||
@@ -8251,7 +8176,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
var pvCtx = resolveTrackInstrumentCtx(pvTrk, activeTracks);
|
||||
ensureSonicInstrument(pvCtx);
|
||||
playing.forEach(n => {
|
||||
if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine)) {
|
||||
if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
playNativeSfNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
|
||||
return;
|
||||
}
|
||||
@@ -8621,7 +8546,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
var clTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
||||
var clCtx = resolveTrackInstrumentCtx(clTrk, activeTracks);
|
||||
ensureSonicInstrument(clCtx);
|
||||
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine)) {
|
||||
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
playNativeSfNote(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, undefined, 'pv_' + st.trackId);
|
||||
} else {
|
||||
scheduleMidiNoteDispatch(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
|
||||
@@ -8885,7 +8810,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0;
|
||||
var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch;
|
||||
var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000));
|
||||
if (isStandaloneSf() && isSfTrackEngine(dwTrk && dwTrk.synth_engine) && !shouldRouteCarla(dwTrk && dwTrk.synth_engine)) {
|
||||
if (isStandaloneSf() && isSfTrackEngine(dwTrk && dwTrk.synth_engine) && !shouldRouteCarla(dwTrk && dwTrk.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
playNativeSfNote(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, undefined, 'pvdraw_' + st.trackId);
|
||||
} else if (window.SonicSF && window.SonicSF.playNote) {
|
||||
const ctx = getAudioContext();
|
||||
@@ -8986,7 +8911,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
|
||||
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
|
||||
var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks);
|
||||
if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine)) {
|
||||
if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
playNativeSfNote(pvTrk, p, brushVelocityRef.current || 0.8, durMs, undefined, 'pvdraw_' + st.trackId);
|
||||
} else if (window.SonicSF && window.SonicSF.playNote) {
|
||||
var pvCtx = getAudioContext();
|
||||
@@ -9378,12 +9303,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
if (window.triggerMidiVuActivity) {
|
||||
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
|
||||
}
|
||||
const kbNative = isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine);
|
||||
const kbNative = isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
if (kbNative) {
|
||||
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
|
||||
}
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127 });
|
||||
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) });
|
||||
} 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
|
||||
@@ -9406,11 +9331,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
if (window.triggerMidiVuActivity) {
|
||||
window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100);
|
||||
}
|
||||
if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine)) {
|
||||
if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive())) {
|
||||
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
|
||||
}
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127 });
|
||||
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) });
|
||||
} 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);
|
||||
@@ -9429,7 +9354,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
try {
|
||||
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0 });
|
||||
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) });
|
||||
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
|
||||
} catch (e) {}
|
||||
if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; }
|
||||
@@ -9441,7 +9366,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
try {
|
||||
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0 });
|
||||
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) });
|
||||
} 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) {} }
|
||||
@@ -15030,11 +14955,6 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
|
||||
<div className={`flex items-center gap-1 px-2 py-1 cursor-pointer hover:bg-blue-100 ${!synthInst ? 'bg-slate-200' : ''}`} onClick={() => selectSynthInst(null)}>
|
||||
<i className="fa-solid fa-ban text-slate-400"></i> None (mặc định)
|
||||
</div>
|
||||
{window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local && (
|
||||
<div className="flex items-center gap-1 px-2 py-1 cursor-pointer hover:bg-teal-100 font-semibold text-teal-700 border-t border-[#e0e0e0]" onClick={() => { setSynthOpen(false); window.SonicAPI.openInCarla().then(r => { if (r && r.success) showToast('Đã mở Carla — chỉnh preset rồi Upload trong app', 'success'); }).catch(err => showToast('Lỗi mở Carla: ' + (err.message || err), 'error')); }}>
|
||||
<i className="fa-solid fa-sliders text-teal-500"></i> 🎛 Carla Bridge (mở Carla.exe)
|
||||
</div>
|
||||
)}
|
||||
{!synthLoading && filteredSynthList && filteredSynthList.map(group => (
|
||||
<div key={group.sf.id || group.sf.name}>
|
||||
<div className="px-2 py-1 bg-[#f0f0f0] font-semibold text-slate-600 border-t border-[#e0e0e0] truncate">{group.sf.display || group.sf.name || group.sf.id}</div>
|
||||
@@ -15422,6 +15342,37 @@ const App = () => {
|
||||
"Tinkle Bell","Agogo","Steel Drums","Woodblock","Taiko Drum","Melodic Tom","Synth Drum","Reverse Cymbal",
|
||||
"Guitar Fret Noise","Breath Noise","Seashore","Bird Tweet","Telephone Ring","Helicopter","Applause","Gunshot"
|
||||
];
|
||||
// D7: bridge active → load instrument vào bridge tại channel router dùng cho
|
||||
// track (SonicMidiRouter.allocateChannel — cùng allocation scheduleMidiNoteDispatch/
|
||||
// keybed dùng khi push NOTE_ON). Không load → MIDI tới channel rỗng → C++ silent.
|
||||
const loadTrackInstrumentToBridge = (trackId, instrumentId, bank) => {
|
||||
if (!window.SonicMidiRouter || !window.SonicMidiRouter.isBridgeActive() || !instrumentId) return;
|
||||
const isSf = typeof instrumentId === 'string' && instrumentId.startsWith('sf_');
|
||||
const bch = window.SonicMidiRouter.allocateChannel(trackId, bank === 128);
|
||||
const btype = isSf ? 'SF2' : 'VST3';
|
||||
let bpath = null;
|
||||
if (instrumentSelectorData) {
|
||||
if (isSf) {
|
||||
const sid = instrumentId.replace('sf_', '');
|
||||
const s0 = (instrumentSelectorData.soundfonts || []).find(s => String(s.id).replace('sf_', '') === sid);
|
||||
bpath = s0 && (s0.file || s0.path);
|
||||
} else {
|
||||
const v0 = (instrumentSelectorData.vst_instruments || []).find(v => v.id === instrumentId || v.name === instrumentId);
|
||||
bpath = v0 && v0.path;
|
||||
}
|
||||
}
|
||||
(async () => {
|
||||
let p = bpath;
|
||||
try {
|
||||
const r = await window.SonicAPI.bridgeLoad({ name: instrumentId, path: bpath || null, instrumentType: btype, channel: bch });
|
||||
if (r && r.path) p = r.path;
|
||||
} catch (e) { console.warn('[Bridge] resolve fail:', e); }
|
||||
if (p) {
|
||||
const ok = await window.NativeBridgeService.loadInstrument(p, btype, bch);
|
||||
console.log('[Bridge] track', trackId, '-> bridge', btype, 'ch', bch, ok ? 'OK' : 'FAIL', p);
|
||||
}
|
||||
})();
|
||||
};
|
||||
const setTrackInstrumentWithProgram = (trackId, instrumentId, programNumber, displayName, bankNumber) => {
|
||||
const isSfInstrument = instrumentId && typeof instrumentId === 'string' && instrumentId.startsWith('sf_');
|
||||
const sfBank = bankNumber !== undefined ? bankNumber : (isSfInstrument ? 0 : undefined);
|
||||
@@ -15465,6 +15416,8 @@ const App = () => {
|
||||
const sfId = instrumentId.replace('sf_', '');
|
||||
window.SonicSF.selectInstrument(mch, sfBank || 0, sfProg || 0, sfId);
|
||||
}
|
||||
// D7: bridge active → load instrument vào bridge tại channel router sẽ dùng
|
||||
loadTrackInstrumentToBridge(trackId, instrumentId, sfBank);
|
||||
setSubTabs(prev => prev.map(s => {
|
||||
if (s.trackId !== trackId) return s;
|
||||
return { ...s, instrumentProgram: programNumber !== undefined ? programNumber : undefined, instrumentName: displayName, instrumentId };
|
||||
@@ -15684,7 +15637,7 @@ const App = () => {
|
||||
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) {}
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: bt.id, pitch: pitch, velocity: scaledVel / 127, percussion: !!(bt.synth_engine && bt.synth_engine.soundfont_bank === 128) }); } catch (e) {}
|
||||
});
|
||||
} else if (window.SonicSF) {
|
||||
var allTracks = activeTracksRef.current || [];
|
||||
@@ -15759,7 +15712,7 @@ const App = () => {
|
||||
// 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) {}
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: bst.id, pitch: pitch, velocity: 0, percussion: !!(bst.synth_engine && bst.synth_engine.soundfont_bank === 128) }); } catch (e) {}
|
||||
});
|
||||
} else if (window.SonicSF && window.SonicSF.stopNote) {
|
||||
var stopTracks = activeTracksRef.current || [];
|
||||
@@ -21693,6 +21646,17 @@ const App = () => {
|
||||
const program = track ? track.instrumentProgram : undefined;
|
||||
var prevCh = track ? assignTrackMidiChannel(track, activeTracks) : 0;
|
||||
const routeCarla = shouldRouteCarla(track && track.synth_engine);
|
||||
// D7: bridge active → preview qua bridge (channel router allocate — đúng
|
||||
// instrument đã load khi chèn vào track).
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
const isPerc = !!(track && track.synth_engine && track.synth_engine.soundfont_bank === 128);
|
||||
const tch = track ? track.id : 0;
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: tch, pitch: pitch || 60, velocity: velocity, percussion: isPerc }); } catch (e) {}
|
||||
setTimeout(function () {
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: tch, pitch: pitch || 60, velocity: 0, percussion: isPerc }); } catch (e) {}
|
||||
}, (durationMs || 500) + 30);
|
||||
return;
|
||||
}
|
||||
if (isStandaloneSf() && !routeCarla && isSfTrackEngine(track && track.synth_engine)) {
|
||||
playNativeSfNote(track, pitch, velocity, durationMs, null, track ? track.id : 'global');
|
||||
return;
|
||||
@@ -21721,24 +21685,26 @@ const App = () => {
|
||||
// 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;
|
||||
// Percussion (bank 128) → router allocateChannel ch 9; melodic → round-robin.
|
||||
const isPerc = !!(synthEngine && synthEngine.soundfont_bank === 128);
|
||||
// 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) {}
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: trkId, pitch: 0, velocity: 0, data2: 0, percussion: isPerc }); } catch (e) {}
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: trkId, pitch: 32, velocity: 0, data2: 0, percussion: isPerc }); } catch (e) {}
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'PROGRAM', channel: trkId, pitch: 0, velocity: 0, data2: program, percussion: isPerc }); } 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) {}
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: trkId, pitch: pitch || 60, velocity: velocity || 0.8, percussion: isPerc }); } catch (e) {}
|
||||
}, delayMs);
|
||||
setTimeout(function () {
|
||||
if (!guardPlay()) return;
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: trkId, pitch: pitch || 60, velocity: 0 }); } catch (e) {}
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: trkId, pitch: pitch || 60, velocity: 0, percussion: isPerc }); } catch (e) {}
|
||||
}, delayMs + (durMs || 1000) + 30);
|
||||
return;
|
||||
}
|
||||
@@ -21810,14 +21776,13 @@ const App = () => {
|
||||
// MIDI items playback
|
||||
const midiItems = track.midiItems || [];
|
||||
const routeCarla = shouldRouteCarla(track.synth_engine);
|
||||
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine);
|
||||
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf)) {
|
||||
if (nativeSf) {
|
||||
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
|
||||
} else {
|
||||
// Preview cache: capture the soundfont (pre track-FX) for fast offline export
|
||||
ensureMidiCapture(track, activeTrackNodesRef.current[track.id]);
|
||||
if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)) ensureCarlaForPlayback(track.synth_engine);
|
||||
var trkCh = assignTrackMidiChannel(track, allPlayTracks);
|
||||
// Ensure instrument is loaded in FluidSynth
|
||||
if (window.SonicSF && track.synth_engine && track.synth_engine.type === 'soundfont' && track.synth_engine.soundfont_id) {
|
||||
@@ -21974,7 +21939,7 @@ const App = () => {
|
||||
// 2. Play MIDI items in subTrack
|
||||
const subMidiItems = subTrack.midiItems || [];
|
||||
const subRouteCarla = shouldRouteCarla(subTrack.synth_engine);
|
||||
const subNativeSf = isStandaloneSf() && !subRouteCarla && isSfTrackEngine(subTrack.synth_engine);
|
||||
const subNativeSf = isStandaloneSf() && !subRouteCarla && isSfTrackEngine(subTrack.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
if (subMidiItems.length > 0 && (window.SonicSF || subRouteCarla || subNativeSf)) {
|
||||
if (subNativeSf) {
|
||||
subMidiItems.forEach(item => scheduleNativeSfItem(subTrack, item, offsetTime, context, subNode, bpm, { baseOffsetSec: secStart, limitSec: secEnd, sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
|
||||
@@ -21997,7 +21962,6 @@ const App = () => {
|
||||
// MIDI item trong SECTION → Carla bridge khi VSTi loaded
|
||||
// (chỉ route Carla — không play FluidSynth GM sai âm)
|
||||
const subRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine));
|
||||
if (subRouteCarla || window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine)) ensureCarlaForPlayback(subTrack.synth_engine);
|
||||
|
||||
if (offsetTime < noteStartMain) {
|
||||
const delay = noteStartMain - offsetTime;
|
||||
@@ -22108,7 +22072,7 @@ const App = () => {
|
||||
// MIDI items playback
|
||||
const midiItems = track.midiItems || [];
|
||||
const routeCarla = shouldRouteCarla(track.synth_engine);
|
||||
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine);
|
||||
const nativeSf = isStandaloneSf() && !routeCarla && isSfTrackEngine(track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
|
||||
if ((track.type === 'MIDI' || midiItems.length > 0) && (window.SonicSF || routeCarla || nativeSf)) {
|
||||
if (nativeSf) {
|
||||
midiItems.forEach(item => scheduleNativeSfItem(track, item, offsetTime, context, gainNode, bpm, { sources: activeSourcesRef.current, isActive: () => !!isPlayingRef.current }));
|
||||
@@ -31271,13 +31235,6 @@ STRICT CONSTRAINTS:
|
||||
onClick: () => {
|
||||
setTrackInstrumentWithUndo(instrumentSelectorTrackId, v.id, v.name || v.id);
|
||||
closeInstrumentSelector();
|
||||
// TỰ ĐỘNG mở Carla với VSTi vừa chọn (desktop + Carla local) —
|
||||
// Carla load sẵn plugin + keyboard ảo để preview realtime.
|
||||
if (window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) {
|
||||
window.SonicAPI.openInCarla(v.id).then(function (r) {
|
||||
if (r && r.success) showToast('Đã mở Carla với ' + (v.name || v.id) + ' — chọn preset, bấm keyboard để preview', 'success');
|
||||
}).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); });
|
||||
}
|
||||
},
|
||||
className: "w-full text-left px-3 py-2 text-sm rounded bg-zinc-800 hover:bg-violet-800 text-zinc-300 flex items-center justify-between gap-2"
|
||||
}, /*#__PURE__*/React.createElement("span", { className: "truncate" }, v.name || v.id), /*#__PURE__*/React.createElement("span", { className: "text-[9px] text-cyan-400 shrink-0" }, v.type || "VST")))
|
||||
@@ -31340,14 +31297,6 @@ STRICT CONSTRAINTS:
|
||||
onClick: () => setTrackInstrumentWithUndo(instrumentDropdownTrackId, null),
|
||||
className: "w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-400"
|
||||
}, "None (Default Synth)"),
|
||||
window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local ? /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => {
|
||||
setInstrumentDropdownTrackId(null); setInstrumentDropdownBtnRect(null);
|
||||
window.SonicAPI.openInCarla().then(function (r) { if (r && r.success) { showToast('Đã mở Carla — chọn VSTi, chỉnh âm, Save preset (.vstpreset) rồi Upload trong app', 'success'); } }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); });
|
||||
},
|
||||
className: "w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-teal-800 text-teal-300 font-semibold flex items-center justify-between",
|
||||
title: "Mở Carla.exe trên hệ thống (native GUI VSTi) — không cần scan VST trong app"
|
||||
}, "\uD83C\uDF9B Carla Bridge (m\u1EDF Carla.exe)", /*#__PURE__*/React.createElement("span", { className: "text-[9px] text-teal-500 shrink-0 ml-1" }, "GUI")) : null,
|
||||
filteredInstruments.soundfonts.length > 0 && /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold" }, "SoundFonts"),
|
||||
filteredInstruments.soundfonts.map((sf, i) => /*#__PURE__*/React.createElement("button", {
|
||||
key: "sfd_" + i,
|
||||
@@ -31364,21 +31313,9 @@ STRICT CONSTRAINTS:
|
||||
setInstrumentDropdownTrackId(null);
|
||||
setInstrumentDropdownBtnRect(null);
|
||||
setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id);
|
||||
// TỰ ĐỘNG mở Carla với VSTi vừa chọn (desktop + Carla local) —
|
||||
// Carla load sẵn plugin, native GUI + keyboard ảo để preview realtime.
|
||||
if (window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) {
|
||||
window.SonicAPI.openInCarla(v.id).then(function (r) {
|
||||
if (r && r.success) showToast('Đã mở Carla với ' + (v.name || v.id) + ' — chọn preset, bấm keyboard để preview', 'success');
|
||||
}).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); });
|
||||
}
|
||||
},
|
||||
className: "flex-1 min-w-0 text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between"
|
||||
}, /*#__PURE__*/React.createElement("span", { className: "truncate" }, v.name || v.id), /*#__PURE__*/React.createElement("span", { className: "text-[10px] text-cyan-400 shrink-0 ml-1" }, v.type || "VST")),
|
||||
window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local ? /*#__PURE__*/React.createElement("button", {
|
||||
onClick: (e) => { e.stopPropagation(); window.SonicAPI.openInCarla(v.id).then(function (r) { if (r && r.success) { showToast('Đã mở Carla: ' + (v.name || v.id), 'success'); } }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); },
|
||||
className: "shrink-0 px-2 text-xs bg-zinc-800 hover:bg-teal-700 text-teal-300 border-l border-zinc-700",
|
||||
title: "Mở trong Carla (native GUI)"
|
||||
}, "\uD83C\uDF9B") : null
|
||||
)),
|
||||
filteredInstruments.vst.length > 0 && /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold" }, "VST Presets (.vstpreset)"),
|
||||
(window.SonicRuntime && window.SonicRuntime.presets || []).map((p, i) => /*#__PURE__*/React.createElement("button", {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
// SonicForge Studio API Service
|
||||
window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
|
||||
window.API_BASE_URL = (window.API_BASE_URL || window.location.origin).replace(/\/+$/, '');
|
||||
|
||||
(function() {
|
||||
function getAuthToken() {
|
||||
@@ -115,9 +115,9 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
|
||||
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' }),
|
||||
bridgeStatus: () => apiRequest('/api/v1/plugins/bridge/status', { method: 'GET' }),
|
||||
bridgeLoad: (payload) => apiRequest('/api/v1/plugins/bridge/load', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
bridgeLog: (lines = 100) => apiRequest(`/api/v1/plugins/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' }),
|
||||
|
||||
@@ -20,6 +20,30 @@
|
||||
if (this._statusCb) this._statusCb({ connected: !!connected });
|
||||
},
|
||||
|
||||
/** Fix C: re-establish bridge routing once audio frames arrive again
|
||||
* after bridge-down. Same wiring as the bootstrap block in app.jsx
|
||||
* (getAudioContext -> BridgeAudioNode.init -> AudioRoutingEngine.connect). */
|
||||
_reconnectInFlight: false,
|
||||
_reconnectIfNeeded: function () {
|
||||
if (this._reconnectInFlight) return;
|
||||
this._reconnectInFlight = true;
|
||||
var self = this;
|
||||
var finish = function () { self._reconnectInFlight = false; };
|
||||
try {
|
||||
var ctx = (typeof getAudioContext === 'function') ? getAudioContext() : (window.__sharedAudioCtx || null);
|
||||
if (ctx && window.BridgeAudioNode && window.AudioRoutingEngine) {
|
||||
window.BridgeAudioNode.init(ctx);
|
||||
window.AudioRoutingEngine.connect(window.BridgeAudioNode, null, null);
|
||||
}
|
||||
if (window.SonicMidiRouter) window.SonicMidiRouter.setBridgeConnected(true);
|
||||
this._notifyStatus(true);
|
||||
console.warn('[BridgeService] bridge recovered — routing re-established.');
|
||||
} catch (e) {
|
||||
console.warn('[BridgeService] reconnect failed:', e);
|
||||
}
|
||||
finish();
|
||||
},
|
||||
|
||||
_init: function () {
|
||||
if (!this._tauri()) {
|
||||
// Dev mode (Linux / plain browser): no Tauri -> log-only, app uses SonicSF.
|
||||
@@ -29,6 +53,9 @@
|
||||
var self = this;
|
||||
try {
|
||||
window.__TAURI__.event.listen('bridge-audio', function (e) {
|
||||
// Bridge recovered after bridge-down (or never bootstrapped):
|
||||
// audio frames flow again -> re-establish routing (Fix C).
|
||||
if (!self.isBridgeConnected) self._reconnectIfNeeded();
|
||||
if (self._audioCb) self._audioCb(e.payload.l, e.payload.r);
|
||||
});
|
||||
window.__TAURI__.event.listen('bridge-down', function () {
|
||||
|
||||
@@ -73,28 +73,14 @@ window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null
|
||||
// (Carla standalone bật OSC UDP mặc định cổng 22752).
|
||||
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;
|
||||
var se = synthEngine || {};
|
||||
return String(se.type || '').indexOf('vst') !== -1 && !!se.plugin_id;
|
||||
} catch (e) { return false; }
|
||||
// Carla bridge removed - all instruments hosted by Native Bridge.
|
||||
return false;
|
||||
},
|
||||
// Playback MIDI items: route khi track VSTi + Carla local (không cần ARM —
|
||||
// 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 || {};
|
||||
return String(se.type || '').indexOf('vst') !== -1 && !!se.plugin_id;
|
||||
} catch (e) { return false; }
|
||||
// Carla bridge removed - all instruments hosted by Native Bridge.
|
||||
return false;
|
||||
},
|
||||
noteOn: function (channel, note, velocity) {
|
||||
if (!window.SonicAPI || !window.SonicAPI.carlaMidi) return;
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608102202" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608121215" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user