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:
+225
-35
@@ -1432,6 +1432,32 @@ function getAudioContext() {
|
||||
if (window.SonicSF && window.SonicSF.init) {
|
||||
window.SonicSF.init(audioCtx);
|
||||
}
|
||||
// C6: bootstrap native bridge 1 lần — query status, nối audio sink + router.
|
||||
if (window.NativeBridgeService && window.SonicMidiRouter && !window.__bridgeBootstrapped) {
|
||||
window.__bridgeBootstrapped = true;
|
||||
window.SonicMidiRouter.onFallback = function (cmd, ch, pitch, vel) {
|
||||
if (!window.SonicSF) return;
|
||||
if (cmd === 'PANIC') { if (window.SonicSF.stopAll) window.SonicSF.stopAll(); return; }
|
||||
if (cmd === 'NOTE_ON') window.SonicSF.playNote(pitch, vel, undefined, undefined, undefined, undefined, ch);
|
||||
else window.SonicSF.stopNote(ch, pitch);
|
||||
};
|
||||
(async function () {
|
||||
try {
|
||||
var st = await window.NativeBridgeService.queryStatus();
|
||||
var connected = !!(st && st.connected);
|
||||
window.SonicMidiRouter.setBridgeConnected(connected);
|
||||
console.log('[Bridge] bootstrap connected=', connected);
|
||||
if (connected) {
|
||||
window.BridgeAudioNode.init(audioCtx);
|
||||
window.NativeBridgeService.onAudio(function (l, r) { window.BridgeAudioNode.onAudio(l, r); });
|
||||
if (window.AudioRoutingEngine) window.AudioRoutingEngine.connect(window.BridgeAudioNode, null, null);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Bridge] bootstrap error:', e);
|
||||
window.SonicMidiRouter.setBridgeConnected(false);
|
||||
}
|
||||
})();
|
||||
}
|
||||
return audioCtx;
|
||||
}
|
||||
const formatTime = secs => {
|
||||
@@ -5525,6 +5551,13 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
|
||||
const [pmCarlaVersion, setPmCarlaVersion] = React.useState(0);
|
||||
// Khai báo trực tiếp thư mục chứa carla.exe (nhập tay, không cần picker)
|
||||
const [pmCarlaPathInput, setPmCarlaPathInput] = React.useState('');
|
||||
// D7: trạng thái Native Bridge (query khi mở modal) — badge header.
|
||||
const [bridgeStatus, setBridgeStatus] = React.useState(null);
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
window.SonicAPI.bridgeStatus().then(s => setBridgeStatus(s)).catch(() => setBridgeStatus(null));
|
||||
}
|
||||
}, [isOpen]);
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
window.SonicAPI.listPlugins()
|
||||
@@ -5715,6 +5748,21 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
|
||||
setSfUploadStatus('Error: ' + err.message);
|
||||
}
|
||||
};
|
||||
// D7: Load asset vào Native Bridge — backend resolve path thật (E2) → JS
|
||||
// invoke load_native_instrument (C2) → bridge C++ load vào channel 0.
|
||||
const loadToBridge = async (name, path, type) => {
|
||||
try {
|
||||
const r = await window.SonicAPI.bridgeLoad({ name: name, path: path || null, instrumentType: type, channel: 0 });
|
||||
if (!r || !r.path) { window.showToast && window.showToast('Không resolve được asset: ' + name, 'error'); return false; }
|
||||
const ok = await window.NativeBridgeService.loadInstrument(r.path, type, 0);
|
||||
window.showToast && window.showToast(ok ? ('Đã load vào Native Bridge: ' + r.path) : 'Bridge không khả dụng (dev mode?)', ok ? 'success' : 'warning');
|
||||
if (ok) window.SonicAPI.bridgeStatus().then(s => setBridgeStatus(s)).catch(() => {});
|
||||
return ok;
|
||||
} catch (err) {
|
||||
window.showToast && window.showToast('Lỗi load bridge: ' + (err.message || err), 'error');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const [pmTab, setPmTab] = React.useState('soundfont');
|
||||
return React.createElement('div', {
|
||||
className: 'fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm',
|
||||
@@ -5799,11 +5847,16 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
|
||||
React.createElement('h3', {
|
||||
className: 'text-base font-bold text-cyan-400 flex items-center gap-2'
|
||||
}, React.createElement('i', { 'data-lucide': 'zap', className: 'w-4 h-4' }), 'Plugin Manager (SoundFont / VSTi)'),
|
||||
React.createElement('button', {
|
||||
onClick: onClose,
|
||||
className: 'text-zinc-500 hover:text-zinc-200 transition'
|
||||
}, React.createElement('i', { 'data-lucide': 'x', className: 'w-4 h-4' }))
|
||||
),
|
||||
React.createElement('div', { className: 'flex items-center gap-2' },
|
||||
React.createElement('span', {
|
||||
className: `text-[9px] font-bold uppercase tracking-wider px-2 py-0.5 rounded border ${bridgeStatus && bridgeStatus.connected ? 'text-emerald-400 border-emerald-700 bg-emerald-900/30' : 'text-zinc-500 border-zinc-700 bg-zinc-800'}`,
|
||||
title: 'Native Bridge (C++ engine) — ' + (bridgeStatus ? JSON.stringify(bridgeStatus).slice(0, 120) : 'chưa query')
|
||||
}, bridgeStatus && bridgeStatus.connected ? '● Bridge ON' : '● Bridge OFF'),
|
||||
React.createElement('button', {
|
||||
onClick: onClose,
|
||||
className: 'text-zinc-500 hover:text-zinc-200 transition'
|
||||
}, React.createElement('i', { 'data-lucide': 'x', className: 'w-4 h-4' }))
|
||||
)),
|
||||
// Left-right body
|
||||
React.createElement('div', {
|
||||
className: 'flex flex-1 overflow-hidden',
|
||||
@@ -5853,6 +5906,11 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
|
||||
className: 'text-[10px] bg-teal-800 hover:bg-teal-700 text-white px-2 py-1 rounded transition shrink-0',
|
||||
title: 'Mở trong Carla (native GUI)'
|
||||
}, '🎛 Carla'),
|
||||
React.createElement('button', {
|
||||
onClick: (e) => { e.stopPropagation(); loadToBridge(v.name || v.id, v.path, v.type || 'VST3'); },
|
||||
className: 'text-[10px] bg-cyan-800 hover:bg-cyan-700 text-white px-2 py-1 rounded transition shrink-0',
|
||||
title: 'Load vào Native Bridge (C++ engine)'
|
||||
}, 'Load Bridge'),
|
||||
React.createElement('span', { className: 'text-[10px] bg-violet-950/40 text-violet-400 px-2 py-0.5 rounded-full border border-violet-800/30' }, v.type || 'VST3')
|
||||
)
|
||||
)
|
||||
@@ -5883,6 +5941,11 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
|
||||
)
|
||||
),
|
||||
React.createElement('div', { className: 'flex items-center gap-2' },
|
||||
React.createElement('button', {
|
||||
onClick: (e) => { e.stopPropagation(); loadToBridge(sf.id, sf.file || sf.path, 'SF2'); },
|
||||
className: 'text-[10px] bg-cyan-800 hover:bg-cyan-700 text-white px-2 py-1 rounded transition shrink-0',
|
||||
title: 'Load vào Native Bridge (C++ engine)'
|
||||
}, 'Load Bridge'),
|
||||
React.createElement('span', { className: 'text-[10px] text-zinc-500' }, expanded ? '▾' : '▸'),
|
||||
React.createElement('button', {
|
||||
onClick: (e) => { e.stopPropagation(); setSfToDelete(sf); },
|
||||
@@ -8192,7 +8255,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
playNativeSfNote(pvTrk, n.pitch, n.velocity || 0.8, 200, undefined, 'pv_' + st.trackId);
|
||||
return;
|
||||
}
|
||||
window.SonicSF.playNote(n.pitch, (n.velocity || 0.8) * 127, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine);
|
||||
scheduleMidiNoteDispatch(pvTrk, n.pitch, n.velocity || 0.8, 200, ctx.currentTime, pvCtx.program, null, pvCtx.ch, pvCtx.synthEngine);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8561,7 +8624,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine)) {
|
||||
playNativeSfNote(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, undefined, 'pv_' + st.trackId);
|
||||
} else {
|
||||
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
|
||||
scheduleMidiNoteDispatch(clTrk, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, ctx.currentTime, clCtx.program, null, clCtx.ch, clCtx.synthEngine);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8829,7 +8892,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
// playNote (FluidSynth — nhạc cụ THẬT của track). _playNoteFallback chỉ
|
||||
// là oscillator beep (sai âm với percussion/soundfont — user: note vẽ
|
||||
// mới nghe nhạc cụ track trước).
|
||||
window.SonicSF.playNote(dwPitch, Math.round(brushVelocityRef.current * 127), dwDurMs, ctx.currentTime, dwTrk ? dwTrk.instrumentProgram : undefined, null, dwCh, dwTrk ? dwTrk.synth_engine : undefined);
|
||||
scheduleMidiNoteDispatch(dwTrk, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, ctx.currentTime, dwTrk ? dwTrk.instrumentProgram : undefined, null, dwCh, dwTrk ? dwTrk.synth_engine : undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -8927,10 +8990,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
playNativeSfNote(pvTrk, p, brushVelocityRef.current || 0.8, durMs, undefined, 'pvdraw_' + st.trackId);
|
||||
} else if (window.SonicSF && window.SonicSF.playNote) {
|
||||
var pvCtx = getAudioContext();
|
||||
var pvVel = Math.round(brushVelocityRef.current * 127);
|
||||
// playNote (FluidSynth — nhạc cụ THẬT). _playNoteFallback = oscillator
|
||||
// beep sai âm (percussion/soundfont).
|
||||
window.SonicSF.playNote(p, pvVel, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine);
|
||||
scheduleMidiNoteDispatch(pvTrk, p, brushVelocityRef.current || 0.8, durMs, pvCtx.currentTime, pvCtxInst.program, null, pvCtxInst.ch, pvCtxInst.synthEngine);
|
||||
previewPitchRef.current = p;
|
||||
}
|
||||
// MIDI → Carla (track VSTi + ARM + Carla local): preview realtime
|
||||
@@ -9320,7 +9382,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
if (kbNative) {
|
||||
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
|
||||
}
|
||||
if (window.SonicSF && !kbNative) {
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127 });
|
||||
} else if (window.SonicSF && !kbNative) {
|
||||
// ⚠️ FIX: giữ note theo thời gian bấm phím — durationMs lớn (5s)
|
||||
// chỉ là auto-off phòng hờ; mouseup/mouseleave gọi stopNote dừng
|
||||
// NGAY (trước đây 500ms → note tự tắt giữa chừng khi giữ phím).
|
||||
@@ -9345,7 +9409,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
if (isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine)) {
|
||||
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
|
||||
}
|
||||
if (window.SonicSF && !(isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine))) {
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127 });
|
||||
} else if (window.SonicSF && !(isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine))) {
|
||||
// giữ note khi kéo qua phím (mouse enter) — dừng bằng mouseup/leave
|
||||
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
|
||||
}
|
||||
@@ -9362,7 +9428,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
// Dừng note khi thả phím — tránh kẹt âm (loop liên tục) với soundfont
|
||||
try {
|
||||
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
|
||||
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0 });
|
||||
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
|
||||
} catch (e) {}
|
||||
if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; }
|
||||
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
|
||||
@@ -9372,7 +9440,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
// Kéo chuột ra khỏi phím → dừng note của phím đó
|
||||
try {
|
||||
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
|
||||
if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0 });
|
||||
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
|
||||
} catch (e) {}
|
||||
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
|
||||
}
|
||||
@@ -14063,7 +14133,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
|
||||
if (isPlayingRef.current && cur && isMidiFile(cur) && (cur.handle || cur.path || cur.file_id || cur.fileId || (cur.kind === 'midi' && cur.name))) {
|
||||
selectTokenRef.current++;
|
||||
const token = selectTokenRef.current;
|
||||
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
|
||||
try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
|
||||
stopAllNativeSfNotes();
|
||||
playMidiPreview(cur, token);
|
||||
}
|
||||
@@ -14539,7 +14609,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
|
||||
if (isPlayingRef.current && cur && isMidiFile(cur) && (cur.handle || cur.path || cur.file_id || cur.fileId || (cur.kind === 'midi' && cur.name))) {
|
||||
selectTokenRef.current++;
|
||||
const token = selectTokenRef.current;
|
||||
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
|
||||
try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
|
||||
stopAllNativeSfNotes();
|
||||
playMidiPreview(cur, token);
|
||||
}
|
||||
@@ -14710,7 +14780,7 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
|
||||
if (cur && (cur.handle || cur.path || cur.file_id || cur.fileId || (cur.kind === 'midi' && cur.name))) {
|
||||
selectTokenRef.current++;
|
||||
const token = selectTokenRef.current;
|
||||
try { if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
|
||||
try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && typeof window.SonicSF.stopAll === 'function') window.SonicSF.stopAll(); } catch (e) {}
|
||||
stopAllNativeSfNotes();
|
||||
playMidiPreview(cur, token);
|
||||
}
|
||||
@@ -15368,7 +15438,7 @@ const App = () => {
|
||||
const _nowVst = _hasInst && !isSfInstrument;
|
||||
if (_wasVst && !_nowVst && window.SonicCarlaMidi && window.SonicCarlaMidi.stopBridge) {
|
||||
window.SonicCarlaMidi.stopBridge();
|
||||
try { if (window.SonicSF && window.SonicSF.stopAll) window.SonicSF.stopAll(); } catch (e) {}
|
||||
try { if (window.SonicMidiRouter) window.SonicMidiRouter.panic(); else if (window.SonicSF && window.SonicSF.stopAll) window.SonicSF.stopAll(); } catch (e) {}
|
||||
stopAllNativeSfNotes();
|
||||
console.log('[Instrument] Carla bridge unloaded — track', trackId, 'switched from VSTi to non-VST');
|
||||
}
|
||||
@@ -15608,7 +15678,15 @@ const App = () => {
|
||||
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {}
|
||||
// Route MIDI input to ALL armed tracks on their dedicated channels
|
||||
// Do NOT use raw MIDI hardware channel (msg.data[0] & 0x0F)
|
||||
if (window.SonicSF) {
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
// D1: bridge active -> 1 cổng router (per-track channel alloc) →
|
||||
// Rust SHM → C++ bridge; giữ SonicSF/Carla fallback ở nhánh else.
|
||||
var bTracks = activeTracksRef.current || [];
|
||||
bTracks.forEach(function (bt) {
|
||||
if (!bt.isArmed) return;
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: bt.id, pitch: pitch, velocity: scaledVel / 127 }); } catch (e) {}
|
||||
});
|
||||
} else if (window.SonicSF) {
|
||||
var allTracks = activeTracksRef.current || [];
|
||||
var arSubs = subTabsRef && subTabsRef.current ? subTabsRef.current.filter(function(s) { return s.type === 'PIANO_ROLL' && s.isArmed; }) : [];
|
||||
var armedTracks = allTracks.filter(function(t) { return t.isArmed; });
|
||||
@@ -15677,7 +15755,13 @@ const App = () => {
|
||||
}
|
||||
// Stop the note on ALL tracks (not just armed) to prevent stuck notes
|
||||
// when ARM is toggled off while a key is held
|
||||
if (window.SonicSF && window.SonicSF.stopNote) {
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
// D1: bridge note-off qua router (channel trùng NOTE_ON — track.id).
|
||||
var bStopTracks = activeTracksRef.current || [];
|
||||
bStopTracks.forEach(function (bst) {
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: bst.id, pitch: pitch, velocity: 0 }); } catch (e) {}
|
||||
});
|
||||
} else if (window.SonicSF && window.SonicSF.stopNote) {
|
||||
var stopTracks = activeTracksRef.current || [];
|
||||
stopTracks.forEach(function(st) {
|
||||
// Only stop channels that actually carry this track's notes —
|
||||
@@ -16894,6 +16978,16 @@ const App = () => {
|
||||
|
||||
const [pluginManagerModalOpen, setPluginManagerModalOpen] = useState(false);
|
||||
const [pluginsData, setPluginsData] = useState(null);
|
||||
// D10: bridge indicator + "Ép dùng WASM" debug toggle.
|
||||
const [bridgeUi, setBridgeUi] = useState({ connected: false, forceWasm: false });
|
||||
React.useEffect(() => {
|
||||
if (!window.NativeBridgeService) return;
|
||||
setBridgeUi(s => ({ ...s, connected: !!window.NativeBridgeService.isBridgeConnected }));
|
||||
window.NativeBridgeService.onStatusChange(function (st) {
|
||||
setBridgeUi(s => ({ ...s, connected: !!st.connected }));
|
||||
});
|
||||
return () => window.NativeBridgeService.onStatusChange(null);
|
||||
}, []);
|
||||
|
||||
const loadAudioBuffersForTracks = async (tracksList) => {
|
||||
let hasLoadedAny = false;
|
||||
@@ -17641,6 +17735,9 @@ const App = () => {
|
||||
const isDraggingSubTabRef = useRef(false);
|
||||
const handlePlayPauseRef = useRef(null);
|
||||
const currentTimeRef = useRef(currentTime);
|
||||
// D9: playhead sample counter cho native bridge (A11/A13 dùng để đồng bộ
|
||||
// timeline sample-accurate); cập nhật trong updatePlayhead khi bridge active.
|
||||
const bridgePlayheadSampleRef = useRef(0);
|
||||
// Resume main/session play khi rời PIANO ROLL tab: mở piano roll lúc main
|
||||
// đang play → bấm Space (play piano roll) → stopAllPlayback dừng main →
|
||||
// quay lại MAIN/SECTION → CÂM. Lưu {offset, audioTime} lúc mở tab → khi
|
||||
@@ -20624,6 +20721,25 @@ const App = () => {
|
||||
};
|
||||
|
||||
const updatePlayhead = () => {
|
||||
// D9: playhead sample counter cho bridge — C++ A11/A13 dùng để đồng bộ
|
||||
// timeline sample-accurate (scheduling theo sampleOffset).
|
||||
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected && (isPlaying || isPlayingRef.current)) {
|
||||
try {
|
||||
const _pctx = getAudioContext();
|
||||
const _elapsed = _pctx.currentTime - (startAudioTimeRef.current || _pctx.currentTime);
|
||||
bridgePlayheadSampleRef.current = Math.max(0, Math.floor((startOffsetTimeRef.current + _elapsed) * (_pctx.sampleRate || 44100)));
|
||||
// set_position ~mỗi giây: C++ A13 cập nhật timeline anchor, tối ưu cho
|
||||
// seek/loop (Rust pump + C++ không tự biết vị trí; JS là nguồn duy nhất).
|
||||
try {
|
||||
const _sr = _pctx.sampleRate || 44100;
|
||||
if (!window.__bridgeLastPosSent) window.__bridgeLastPosSent = 0;
|
||||
if (bridgePlayheadSampleRef.current - window.__bridgeLastPosSent > _sr) {
|
||||
window.__bridgeLastPosSent = bridgePlayheadSampleRef.current;
|
||||
window.NativeBridgeService.transport('set_position', bridgePlayheadSampleRef.current);
|
||||
}
|
||||
} catch (e2) {}
|
||||
} catch (e) {}
|
||||
}
|
||||
// Realtime re-schedule: khi đang play (loop play) mà items (vị trí/speed/
|
||||
// duration — kể cả nội dung section tab) thay đổi → dừng + schedule lại từ
|
||||
// playhead hiện tại để item mới phát đúng vị trí mới (không phát nội dung
|
||||
@@ -21287,6 +21403,12 @@ const App = () => {
|
||||
const _prSubKeys = Object.keys(activeTrackNodesRef.current).filter(k => k.endsWith('_sub_' + _activeSub.trackId));
|
||||
_prNode = _prSubKeys.length ? activeTrackNodesRef.current[_prSubKeys[0]] : null;
|
||||
}
|
||||
// D5: bridge active → nối BridgeAudioNode vào node track (sfEntry →
|
||||
// FX chain → mastering) thay vì SonicSF.setOutputDestination.
|
||||
if (_prNode && window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive() && window.BridgeAudioNode) {
|
||||
window.AudioRoutingEngine.connect(window.BridgeAudioNode, _prNode, _activeSub.trackId);
|
||||
return;
|
||||
}
|
||||
if (_prNode && window.SonicSF && window.SonicSF.setOutputDestination) {
|
||||
if (_prNode.sfEntry) {
|
||||
window.SonicSF.setOutputDestination(_prNode.sfEntry);
|
||||
@@ -21333,6 +21455,11 @@ const App = () => {
|
||||
// SF ALWAYS enters the track's own FX chain (sfEntry → sfModules when
|
||||
// PWR ON). The ♪ button only switches the post-FX route (sfRouteGain /
|
||||
// sfDryGain) to skip or include the Mastering FX Chain.
|
||||
// D5: bridge active → nối vào node track (sfEntry → FX chain).
|
||||
if (node && window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive() && window.BridgeAudioNode) {
|
||||
window.AudioRoutingEngine.connect(window.BridgeAudioNode, node, t.id);
|
||||
return;
|
||||
}
|
||||
if (node && node.sfEntry && window.SonicSF && window.SonicSF.setOutputDestination) {
|
||||
console.log('[SFRoute] dest=sfEntry node', t.id, 'sfRouteGain=' + (node.sfRouteGain ? node.sfRouteGain.gain.value : 'MISSING'), 'sfDryGain=' + (node.sfDryGain ? node.sfDryGain.gain.value : 'MISSING'));
|
||||
window.SonicSF.setOutputDestination(node.sfEntry);
|
||||
@@ -21362,7 +21489,11 @@ const App = () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (window.SonicSF && window.SonicSF.setOutputDestination) {
|
||||
// D5: bridge active + không track nào audible → ngắt khỏi graph (về
|
||||
// masterBus qua updateSfRouting lần sau khi có track).
|
||||
if (window.AudioRoutingEngine && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
window.AudioRoutingEngine.disconnect();
|
||||
} else if (window.SonicSF && window.SonicSF.setOutputDestination) {
|
||||
window.SonicSF.setOutputDestination(null);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -21578,8 +21709,49 @@ const App = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// D2: timeline MIDI note -> native bridge (khi bridge active) thay vì
|
||||
// SonicSF (FluidSynth WASM). Giữ setTimeout scheduling (như SonicSF cũ);
|
||||
// bridge tự render note-off sau duration. A11 (sample-accurate) sẽ thay
|
||||
// setTimeout bằng sampleOffset đẩy thẳng vào SHM.
|
||||
const scheduleMidiNoteDispatch = (track, pitch, velocity, durMs, startTime, program, destNode, ch, synthEngine) => {
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
|
||||
const ctx = getAudioContext();
|
||||
const startAt = startTime || ctx.currentTime; // undefined/null = phát ngay
|
||||
const delayMs = Math.max(0, (startAt - ctx.currentTime) * 1000);
|
||||
// Guard chống note-on trễ sau Stop: CHỈ khi là timeline (startTime thật).
|
||||
const guardPlay = startTime != null ? function () { return isPlayingRef.current; } : function () { return true; };
|
||||
const trkId = track ? track.id : 0;
|
||||
// D8: track đổi instrument → gửi CC0/CC32 (bank 0) + program change qua
|
||||
// bridge (A12) một lần mỗi program mới mỗi channel.
|
||||
if (program !== undefined && program !== null) {
|
||||
const lastP = window.__bridgeLastProgram || (window.__bridgeLastProgram = {});
|
||||
if (lastP[trkId] !== program) {
|
||||
lastP[trkId] = program;
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: trkId, pitch: 0, velocity: 0, data2: 0 }); } catch (e) {}
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: trkId, pitch: 32, velocity: 0, data2: 0 }); } catch (e) {}
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'PROGRAM', channel: trkId, pitch: 0, velocity: 0, data2: program }); } catch (e) {}
|
||||
}
|
||||
}
|
||||
setTimeout(function () {
|
||||
if (!guardPlay()) return;
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: trkId, pitch: pitch || 60, velocity: velocity || 0.8 }); } catch (e) {}
|
||||
}, delayMs);
|
||||
setTimeout(function () {
|
||||
if (!guardPlay()) return;
|
||||
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: trkId, pitch: pitch || 60, velocity: 0 }); } catch (e) {}
|
||||
}, delayMs + (durMs || 1000) + 30);
|
||||
return;
|
||||
}
|
||||
window.SonicSF.playNote(pitch, velocity, durMs, startTime, program, destNode, ch, synthEngine);
|
||||
};
|
||||
|
||||
const startTrackPlayback = offsetTime => {
|
||||
const context = getAudioContext();
|
||||
// D2: bridge active -> báo transport PLAY (bridge flush note-off cũ + đồng
|
||||
// bộ timeline; A13 C++ xử lý arg1=playheadSamples sau này).
|
||||
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
|
||||
try { window.NativeBridgeService.transport('play'); } catch (e) {}
|
||||
}
|
||||
// ⚠️ FIX: đồng bộ mastering + Carla status NGAY khi play — MIDI item phải
|
||||
// qua mastering FX (khi bật) và qua Carla bridge (khi VSTi loaded).
|
||||
try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {}
|
||||
@@ -21675,8 +21847,8 @@ const App = () => {
|
||||
const delay = noteStartSec - offsetTime;
|
||||
const startTime = context.currentTime + delay;
|
||||
if (!routeToCarla && window.SonicSF) {
|
||||
window.SonicSF.playNote(
|
||||
note.pitch || 60,
|
||||
scheduleMidiNoteDispatch(
|
||||
track, note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
durationMs,
|
||||
startTime,
|
||||
@@ -21703,8 +21875,8 @@ const App = () => {
|
||||
const playOffset = offsetTime - noteStartSec;
|
||||
const remainingDurMs = (noteEndSec - offsetTime) * 1000;
|
||||
if (!routeToCarla && window.SonicSF) {
|
||||
window.SonicSF.playNote(
|
||||
note.pitch || 60,
|
||||
scheduleMidiNoteDispatch(
|
||||
track, note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
remainingDurMs,
|
||||
context.currentTime,
|
||||
@@ -21832,8 +22004,8 @@ const App = () => {
|
||||
const startTime = context.currentTime + delay;
|
||||
const playDurMs = (notePlayEndMain - noteStartMain) * 1000;
|
||||
if (!subRouteCarla && window.SonicSF) {
|
||||
window.SonicSF.playNote(
|
||||
note.pitch || 60,
|
||||
scheduleMidiNoteDispatch(
|
||||
subTrack, note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
playDurMs,
|
||||
startTime,
|
||||
@@ -21862,8 +22034,8 @@ const App = () => {
|
||||
} else {
|
||||
const remainingDurMs = (notePlayEndMain - offsetTime) * 1000;
|
||||
if (!subRouteCarla && window.SonicSF) {
|
||||
window.SonicSF.playNote(
|
||||
note.pitch || 60,
|
||||
scheduleMidiNoteDispatch(
|
||||
subTrack, note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
remainingDurMs,
|
||||
context.currentTime,
|
||||
@@ -21961,8 +22133,8 @@ const App = () => {
|
||||
const delay = noteStartSec - offsetTime;
|
||||
const startTime = context.currentTime + delay;
|
||||
if (!routeToCarla && window.SonicSF) {
|
||||
window.SonicSF.playNote(
|
||||
note.pitch || 60,
|
||||
scheduleMidiNoteDispatch(
|
||||
track, note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
durationMs,
|
||||
startTime,
|
||||
@@ -21977,8 +22149,8 @@ const App = () => {
|
||||
} else {
|
||||
const remainingDurMs = (noteEndSec - offsetTime) * 1000;
|
||||
if (!routeToCarla && window.SonicSF) {
|
||||
window.SonicSF.playNote(
|
||||
note.pitch || 60,
|
||||
scheduleMidiNoteDispatch(
|
||||
track, note.pitch || 60,
|
||||
note.velocity || 0.8,
|
||||
remainingDurMs,
|
||||
context.currentTime,
|
||||
@@ -22043,7 +22215,7 @@ const App = () => {
|
||||
// Carla — không play FluidSynth GM sai âm chồng lên)
|
||||
const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine));
|
||||
if (!routeToCarla && window.SonicSF) {
|
||||
window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine);
|
||||
scheduleMidiNoteDispatch(track, note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine);
|
||||
}
|
||||
// MIDI items → Carla (track VSTi + Carla local): phát VSTi realtime
|
||||
// (schedule theo audio clock bằng setTimeout — preview, timing gần đúng).
|
||||
@@ -22079,7 +22251,7 @@ const App = () => {
|
||||
// Ghost note → Carla bridge khi ghost track VSTi (chỉ route Carla)
|
||||
var gRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth));
|
||||
if (!gRouteCarla && window.SonicSF) {
|
||||
window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth);
|
||||
scheduleMidiNoteDispatch(ghostTrack, note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth);
|
||||
}
|
||||
if (gRouteCarla) scheduleCarlaNote(ghostSynth, ghostCh, note.pitch || 60, note.velocity || 0.8, schedTime, durMs);
|
||||
}
|
||||
@@ -22210,6 +22382,11 @@ const App = () => {
|
||||
try { heldMidiNotesRef.current = {}; } catch (e) { }
|
||||
stopMidiCapture();
|
||||
stopAllNativeSfNotes();
|
||||
// D2/D6: bridge active -> transport STOP (bridge flush toàn pitch, hết âm
|
||||
// ngân tức thì; A13 C++ flush note-off).
|
||||
if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) {
|
||||
try { window.NativeBridgeService.transport('stop'); } catch (e) {}
|
||||
}
|
||||
if (window.SonicSF) {
|
||||
try { window.SonicSF.stopAll(); } catch (e) { console.warn('[Stop] stopAll error:', e); }
|
||||
// Dừng triệt để: noteoff từng note + hủy scheduled note-on (hết âm stuck)
|
||||
@@ -28515,6 +28692,19 @@ STRICT CONSTRAINTS:
|
||||
className: "font-mono text-[20px] text-zinc-400 tabular-nums",
|
||||
title: "Tổng thời gian dự án"
|
||||
}, formatTime(projectEnd)), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex items-center gap-1.5 ml-3",
|
||||
title: bridgeUi.connected && !bridgeUi.forceWasm ? "Engine phát qua Native Bridge (C++ FluidSynth/sfizz/VST3)" : "Engine phát qua SonicSF WASM"
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: "text-[9px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded border " + (bridgeUi.connected && !bridgeUi.forceWasm ? "text-emerald-400 border-emerald-700 bg-emerald-900/30" : "text-zinc-500 border-zinc-700 bg-zinc-800")
|
||||
}, bridgeUi.connected && !bridgeUi.forceWasm ? "Bridge" : "WASM"), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => {
|
||||
const v = !bridgeUi.forceWasm;
|
||||
setBridgeUi(s => ({ ...s, forceWasm: v }));
|
||||
if (window.SonicMidiRouter && window.SonicMidiRouter.setForceWasm) window.SonicMidiRouter.setForceWasm(v);
|
||||
},
|
||||
className: `px-1.5 py-0.5 rounded text-[9px] font-bold border transition ${bridgeUi.forceWasm ? "bg-amber-600 text-black border-amber-500" : "bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-zinc-200"}`,
|
||||
title: "Debug: ép dùng SonicSF WASM bỏ qua bridge"
|
||||
}, "Ép WASM")), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex-1"
|
||||
})),(() => {
|
||||
const dockPanels = {
|
||||
|
||||
Reference in New Issue
Block a user