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:
2026-08-12 22:12:43 +07:00
parent c3368d0a91
commit d8f8506077
37 changed files with 2170 additions and 419 deletions
+68 -131
View File
@@ -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 chy T ĐNG m vi VSTi ca track ri CH OSC ready (poll
// carla-status ti đa 10s) nt ch gi khi bridge thc s nhn đưc.
// Có gate __carlaOpening chng spawn trùng (mi play ch m 1 ln).
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 (hoc đã chy) poll ti khi OSC ready, mi flush nt
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 chy: 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 thi gian bm phím durationMs ln (5s)
// ch là auto-off phòng h; mouseup/mouseleave gi stopNote dng
@@ -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) dng bng 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 ti channel router dùng cho
// track (SonicMidiRouter.allocateChannel cùng allocation scheduleMidiNoteDispatch/
// keybed dùng khi push NOTE_ON). Không load MIDI ti channel rng 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 ti 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 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;
// Percussion (bank 128) router allocateChannel ch 9; melodic round-robin.
const isPerc = !!(synthEngine && synthEngine.soundfont_bank === 128);
// 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) {}
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 vi VSTi va chn (desktop + Carla local)
// Carla load sn 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 vi VSTi va chn (desktop + Carla local)
// Carla load sn 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
+4 -4
View File
@@ -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 () {
+4 -18
View File
@@ -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;