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
+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 = {