fix: SF2 bank select, midi multi-key cut, VSTi reopen hang, multi-track stuck

- NativeInstrumentEngine: track GM bank per channel (CC0/CC32), use bank in programChange
- app.jsx: send CC0/CC32+PROGRAM before notes via __ensureBridgeProgram, dedupe, clear dedupe after async LOAD
- audioRoutingEngine/bridgeAudioNode: idempotent connect (no disconnect-flush on re-connect), fixes note cut & multi-track stuck
- main.cpp: remove 10s poll in OPEN_GUI control job (blocked realtime loop, watchdog race), VstWindowProc stores channel not inst pointer, cleanup gui maps on WM_DESTROY
This commit is contained in:
2026-08-13 12:19:34 +07:00
parent d168328004
commit 3f59c2c4c2
7 changed files with 136 additions and 50 deletions
+37 -9
View File
@@ -68,6 +68,30 @@ const resolveTrackInstrumentCtx = (track, tracks) => {
return { ch, program: undefined, synthEngine: undefined, sfId: undefined, bank: 0, prog: 0 }; return { ch, program: undefined, synthEngine: undefined, sfId: undefined, bank: 0, prog: 0 };
}; };
// Bridge program-change helper (D8/A12): gi CC0/CC32 (bank THT) + PROGRAM
// qua bridge trưc note-on dedupe theo track+bank+program (không spam mi
// note; reset khi load instrument mi). Dùng chung mi đưng: timeline,
// preview, keybed. Trưc đây bank hardcode 0 + preview/keybed KHÔNG gi
// program change SF2 preset bank0 ra piano sai.
window.__ensureBridgeProgram = (trkId, program, synthEngine) => {
try {
if (!window.SonicMidiRouter || !window.SonicMidiRouter.isBridgeActive()) return;
const _bank = synthEngine && synthEngine.soundfont_bank !== undefined ? synthEngine.soundfont_bank : 0;
const _prog = (program !== undefined && program !== null)
? program
: (synthEngine && synthEngine.soundfont_program !== undefined ? synthEngine.soundfont_program : undefined);
if (_prog === undefined || _prog === null) return;
const _key = trkId + ':' + _bank + ':' + _prog;
const _lp = window.__bridgeLastProgram || (window.__bridgeLastProgram = {});
if (_lp[_key]) return;
_lp[_key] = true;
const _isPerc = _bank === 128;
window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: trkId, pitch: 0, velocity: 0, data2: (_bank >> 7) & 0x7F, percussion: _isPerc });
window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: trkId, pitch: 32, velocity: 0, data2: _bank & 0x7F, percussion: _isPerc });
window.SonicMidiRouter.pushEvent({ cmd: 'PROGRAM', channel: trkId, pitch: 0, velocity: 0, data2: _prog, percussion: _isPerc });
} catch (e) { console.warn('[Bridge] ensureBridgeProgram error:', e); }
};
// Đm bo FluidSynth channel ca track đã select ĐÚNG instrument trưc khi // Đm bo FluidSynth channel ca track đã select ĐÚNG instrument trưc khi
// notes bn. Fire-and-forget: playNote t load + retry nếu SF chưa load xong // notes bn. Fire-and-forget: playNote t load + retry nếu SF chưa load xong
// (dedup sn trong loadSoundFont) không chn, không gây stall khi m tab. // (dedup sn trong loadSoundFont) không chn, không gây stall khi m tab.
@@ -9327,6 +9351,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch); playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
} }
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) { if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(kbTrk ? kbTrk.id : 0, kbCtx.program, kbCtx.synthEngine); } catch (er) {}
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) }); 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) { } else if (window.SonicSF && !kbNative) {
// FIX: gi note theo thi gian bm phím durationMs ln (5s) // FIX: gi note theo thi gian bm phím durationMs ln (5s)
@@ -9354,6 +9379,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch); playNativeSfNote(kbTrk, pitch, 100 / 127, 5000, undefined, 'kb_' + st.trackId + '_' + pitch);
} }
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) { if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(kbTrk ? kbTrk.id : 0, kbCtx.program, kbCtx.synthEngine); } catch (er) {}
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) }); 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))) { } 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 // gi note khi kéo qua phím (mouse enter) dng bng mouseup/leave
@@ -15389,6 +15415,13 @@ const App = () => {
if (p) { if (p) {
const ok = await window.NativeBridgeService.loadInstrument(p, btype, bch); const ok = await window.NativeBridgeService.loadInstrument(p, btype, bch);
console.log('[Bridge] track', trackId, '-> bridge', btype, 'ch', bch, ok ? 'OK' : 'FAIL', p); console.log('[Bridge] track', trackId, '-> bridge', btype, 'ch', bch, ok ? 'OK' : 'FAIL', p);
// Reset program-change dedupe ca track LOAD async (C++ worker): CC/
// PROGRAM gi trưc khi load xong b drop (inst chưa có) note kế tiếp
// phi gi li program change đ ra ĐÚNG instrument.
if (ok && window.__bridgeLastProgram) {
const _pfx = trackId + ':';
Object.keys(window.__bridgeLastProgram).forEach(k => { if (k.indexOf(_pfx) === 0) delete window.__bridgeLastProgram[k]; });
}
// Requirement 2: chn VST3 qua nút Synth load xong m native GUI // Requirement 2: chn VST3 qua nút Synth load xong m native GUI
// (C++ attach editor vào ca s bridge t to control type=4, hwnd=0). // (C++ attach editor vào ca s bridge t to control type=4, hwnd=0).
if (ok && btype === 'VST3' && window.NativeBridgeService.openNativeGUI) { if (ok && btype === 'VST3' && window.NativeBridgeService.openNativeGUI) {
@@ -21690,6 +21723,7 @@ const App = () => {
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) { if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
const isPerc = !!(track && track.synth_engine && track.synth_engine.soundfont_bank === 128); const isPerc = !!(track && track.synth_engine && track.synth_engine.soundfont_bank === 128);
const tch = track ? track.id : 0; const tch = track ? track.id : 0;
try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(tch, program, track && track.synth_engine); } catch (e) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: tch, pitch: pitch || 60, velocity: velocity, percussion: isPerc }); } catch (e) {} try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: tch, pitch: pitch || 60, velocity: velocity, percussion: isPerc }); } catch (e) {}
setTimeout(function () { setTimeout(function () {
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: tch, pitch: pitch || 60, velocity: 0, percussion: isPerc }); } catch (e) {} try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: tch, pitch: pitch || 60, velocity: 0, percussion: isPerc }); } catch (e) {}
@@ -21726,16 +21760,10 @@ const App = () => {
const trkId = track ? track.id : 0; const trkId = track ? track.id : 0;
// Percussion (bank 128) router allocateChannel ch 9; melodic round-robin. // Percussion (bank 128) router allocateChannel ch 9; melodic round-robin.
const isPerc = !!(synthEngine && synthEngine.soundfont_bank === 128); const isPerc = !!(synthEngine && synthEngine.soundfont_bank === 128);
// D8: track đi instrument gi CC0/CC32 (bank 0) + program change qua // D8: track đi instrument gi CC0/CC32 (bank THT t synthEngine) +
// bridge (A12) mt ln mi program mi mi channel. // program change qua bridge (A12) helper dedupe theo track+bank+program.
if (program !== undefined && program !== null) { if (program !== undefined && program !== null) {
const lastP = window.__bridgeLastProgram || (window.__bridgeLastProgram = {}); try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(trkId, program, synthEngine); } catch (e) {}
if (lastP[trkId] !== program) {
lastP[trkId] = program;
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 () { setTimeout(function () {
if (!guardPlay()) return; if (!guardPlay()) return;
+16 -8
View File
@@ -12,7 +12,12 @@ const trackMidiChannelsRef={current:{}};const ensureTrackMidiChannel=(track,trac
// context thống nhất cho schedulePianoRollMidi + preview (wheel/click/keybed) // context thống nhất cho schedulePianoRollMidi + preview (wheel/click/keybed)
// để note PHẢI chơi đúng instrument của track — không phụ thuộc st snapshot. // để note PHẢI chơi đúng instrument của track — không phụ thuộc st snapshot.
const resolveTrackInstrumentCtx=(track,tracks)=>{const all=tracks||[];const ch=track?assignTrackMidiChannel(track,all):0;const se=track?track.synth_engine:undefined;const isSf=!!(se&&(se.type==='soundfont'||se.soundfont_id));if(isSf){return{ch,program:undefined,// SF path — synthEngine được ưu tiên trong _playNoteFluid const resolveTrackInstrumentCtx=(track,tracks)=>{const all=tracks||[];const ch=track?assignTrackMidiChannel(track,all):0;const se=track?track.synth_engine:undefined;const isSf=!!(se&&(se.type==='soundfont'||se.soundfont_id));if(isSf){return{ch,program:undefined,// SF path — synthEngine được ưu tiên trong _playNoteFluid
synthEngine:se,sfId:se.soundfont_id,bank:se.soundfont_bank||0,prog:se.soundfont_program||0};}if(track&&track.instrumentProgram!==undefined){return{ch,program:track.instrumentProgram,synthEngine:undefined,sfId:undefined,bank:0,prog:track.instrumentProgram};}return{ch,program:undefined,synthEngine:undefined,sfId:undefined,bank:0,prog:0};};// Đảm bảo FluidSynth channel của track đã select ĐÚNG instrument trước khi synthEngine:se,sfId:se.soundfont_id,bank:se.soundfont_bank||0,prog:se.soundfont_program||0};}if(track&&track.instrumentProgram!==undefined){return{ch,program:track.instrumentProgram,synthEngine:undefined,sfId:undefined,bank:0,prog:track.instrumentProgram};}return{ch,program:undefined,synthEngine:undefined,sfId:undefined,bank:0,prog:0};};// ── Bridge program-change helper (D8/A12): gửi CC0/CC32 (bank THẬT) + PROGRAM
// qua bridge trước note-on — dedupe theo track+bank+program (không spam mỗi
// note; reset khi load instrument mới). Dùng chung mọi đường: timeline,
// preview, keybed. Trước đây bank hardcode 0 + preview/keybed KHÔNG gửi
// program change → SF2 preset ở bank≠0 ra piano sai.
window.__ensureBridgeProgram=(trkId,program,synthEngine)=>{try{if(!window.SonicMidiRouter||!window.SonicMidiRouter.isBridgeActive())return;const _bank=synthEngine&&synthEngine.soundfont_bank!==undefined?synthEngine.soundfont_bank:0;const _prog=program!==undefined&&program!==null?program:synthEngine&&synthEngine.soundfont_program!==undefined?synthEngine.soundfont_program:undefined;if(_prog===undefined||_prog===null)return;const _key=trkId+':'+_bank+':'+_prog;const _lp=window.__bridgeLastProgram||(window.__bridgeLastProgram={});if(_lp[_key])return;_lp[_key]=true;const _isPerc=_bank===128;window.SonicMidiRouter.pushEvent({cmd:'CC',channel:trkId,pitch:0,velocity:0,data2:_bank>>7&0x7F,percussion:_isPerc});window.SonicMidiRouter.pushEvent({cmd:'CC',channel:trkId,pitch:32,velocity:0,data2:_bank&0x7F,percussion:_isPerc});window.SonicMidiRouter.pushEvent({cmd:'PROGRAM',channel:trkId,pitch:0,velocity:0,data2:_prog,percussion:_isPerc});}catch(e){console.warn('[Bridge] ensureBridgeProgram error:',e);}};// Đảm bảo FluidSynth channel của track đã select ĐÚNG instrument trước khi
// notes bắn. Fire-and-forget: playNote tự load + retry nếu SF chưa load xong // notes bắn. Fire-and-forget: playNote tự load + retry nếu SF chưa load xong
// (dedup sẵn trong loadSoundFont) — không chặn, không gây stall khi mở tab. // (dedup sẵn trong loadSoundFont) — không chặn, không gây stall khi mở tab.
const ensureSonicInstrument=ctx=>{try{if(!window.SonicSF||!window.SonicSF.selectInstrument)return;if(ctx.sfId){window.SonicSF.selectInstrument(ctx.ch,ctx.bank,ctx.prog,ctx.sfId);}else if(ctx.program!==undefined){window.SonicSF.selectInstrument(ctx.ch,0,ctx.program,null);}}catch(e){console.warn('[Instrument] ensureSonicInstrument error:',e);}};// ── Carla bridge MIDI scheduling helper ──────────────────────────────────── const ensureSonicInstrument=ctx=>{try{if(!window.SonicSF||!window.SonicSF.selectInstrument)return;if(ctx.sfId){window.SonicSF.selectInstrument(ctx.ch,ctx.bank,ctx.prog,ctx.sfId);}else if(ctx.program!==undefined){window.SonicSF.selectInstrument(ctx.ch,0,ctx.program,null);}}catch(e){console.warn('[Instrument] ensureSonicInstrument error:',e);}};// ── Carla bridge MIDI scheduling helper ────────────────────────────────────
@@ -555,11 +560,11 @@ const idxs=findCCNoteIndicesAtBeat(beat);ccDragRef.current={active:true,lastBeat
const candidateIdxs=findCCNoteIndicesAtBeat(beat);const unpainted=candidateIdxs.filter(ci=>!painted.includes(ci));if(unpainted.length>0){const updatedNotes=notes.map((n,i)=>{if(!unpainted.includes(i))return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);drag.lastPainted=[...painted,...unpainted];}};const renderKeybed=()=>{var kbTrk=activeTracks.find(function(t){return t.id===st.trackId;});var kbCtx=resolveTrackInstrumentCtx(kbTrk,activeTracks);ensureSonicInstrument(kbCtx);const keys=[];for(let pitch=127;pitch>=PITCH_START;pitch--){const isBlack=[1,3,6,8,10].includes(pitch%12);const notesArray=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;const label=`${notesArray[pitch%12]}${octave}`;const showLabel=pitch%12===0;keys.push(/*#__PURE__*/React.createElement("div",{key:pitch,style:{height:`${NoteHeight}px`},className:`w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition cursor-pointer ${activeMidiPitches&&activeMidiPitches.has(pitch)?'bg-emerald-500 text-white border-emerald-400':isBlack?'bg-zinc-950 text-slate-500 border-zinc-900 hover:bg-zinc-800':'bg-white text-zinc-800 border-r border-zinc-400 hover:bg-amber-100'}`,onMouseDown:e=>{e.stopPropagation();keybedMouseDownRef.current=true;try{// Đồng bộ mastering + routing SF NGAY trước khi preview — âm const candidateIdxs=findCCNoteIndicesAtBeat(beat);const unpainted=candidateIdxs.filter(ci=>!painted.includes(ci));if(unpainted.length>0){const updatedNotes=notes.map((n,i)=>{if(!unpainted.includes(i))return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);drag.lastPainted=[...painted,...unpainted];}};const renderKeybed=()=>{var kbTrk=activeTracks.find(function(t){return t.id===st.trackId;});var kbCtx=resolveTrackInstrumentCtx(kbTrk,activeTracks);ensureSonicInstrument(kbCtx);const keys=[];for(let pitch=127;pitch>=PITCH_START;pitch--){const isBlack=[1,3,6,8,10].includes(pitch%12);const notesArray=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;const label=`${notesArray[pitch%12]}${octave}`;const showLabel=pitch%12===0;keys.push(/*#__PURE__*/React.createElement("div",{key:pitch,style:{height:`${NoteHeight}px`},className:`w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition cursor-pointer ${activeMidiPitches&&activeMidiPitches.has(pitch)?'bg-emerald-500 text-white border-emerald-400':isBlack?'bg-zinc-950 text-slate-500 border-zinc-900 hover:bg-zinc-800':'bg-white text-zinc-800 border-r border-zinc-400 hover:bg-amber-100'}`,onMouseDown:e=>{e.stopPropagation();keybedMouseDownRef.current=true;try{// Đồng bộ mastering + routing SF NGAY trước khi preview — âm
// keybed PHẢI qua mastering FX của main out khi chain đang bật // keybed PHẢI qua mastering FX của main out khi chain đang bật
// (trước đây phải bật/tắt power mới có tác dụng). // (trước đây phải bật/tắt power mới có tác dụng).
try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(er){}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)&&!(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,percussion:!!(kbCtx&&kbCtx.synthEngine&&kbCtx.synthEngine.soundfont_bank===128)});}else if(window.SonicSF&&!kbNative){// ⚠️ FIX: giữ note theo thời gian bấm phím — durationMs lớn (5s) try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(er){}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)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive());if(kbNative){playNativeSfNote(kbTrk,pitch,100/127,5000,undefined,'kb_'+st.trackId+'_'+pitch);}if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(kbTrk?kbTrk.id:0,kbCtx.program,kbCtx.synthEngine);}catch(er){}window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:kbTrk?kbTrk.id:0,pitch:pitch,velocity:100/127,percussion:!!(kbCtx&&kbCtx.synthEngine&&kbCtx.synthEngine.soundfont_bank===128)});}else if(window.SonicSF&&!kbNative){// ⚠️ FIX: giữ note theo thời gian bấm phím — durationMs lớn (5s)
// chỉ là auto-off phòng hờ; mouseup/mouseleave gọi stopNote dừng // 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). // NGAY (trước đây 500ms → note tự tắt giữa chừng khi giữ phím).
window.SonicSF.playNote(pitch,100,5000,undefined,kbCtx.program,null,kbCtx.ch,kbCtx.synthEngine);}// MIDI → Carla (track VSTi + ARM + Carla local): preview realtime window.SonicSF.playNote(pitch,100,5000,undefined,kbCtx.program,null,kbCtx.ch,kbCtx.synthEngine);}// MIDI → Carla (track VSTi + ARM + Carla local): preview realtime
if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine,st.isArmed)){window.SonicCarlaMidi.noteOn(kbCtx.ch,pitch,100);if(window.__carlaKeybedTimer)clearTimeout(window.__carlaKeybedTimer);window.__carlaKeybedTimer=setTimeout(function(){window.SonicCarlaMidi.noteOff(kbCtx.ch,pitch);},650);}}catch(err){console.error('playNote error:',err);}},onMouseEnter:e=>{if(keybedMouseDownRef.current){try{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)&&!(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,percussion:!!(kbCtx&&kbCtx.synthEngine&&kbCtx.synthEngine.soundfont_bank===128)});}else if(window.SonicSF&&!(isStandaloneSf()&&isSfTrackEngine(kbCtx.synthEngine)&&!shouldRouteCarla(kbCtx.synthEngine))){// giữ note khi kéo qua phím (mouse enter) — dừng bằng mouseup/leave if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine,st.isArmed)){window.SonicCarlaMidi.noteOn(kbCtx.ch,pitch,100);if(window.__carlaKeybedTimer)clearTimeout(window.__carlaKeybedTimer);window.__carlaKeybedTimer=setTimeout(function(){window.SonicCarlaMidi.noteOff(kbCtx.ch,pitch);},650);}}catch(err){console.error('playNote error:',err);}},onMouseEnter:e=>{if(keybedMouseDownRef.current){try{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)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeSfNote(kbTrk,pitch,100/127,5000,undefined,'kb_'+st.trackId+'_'+pitch);}if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(kbTrk?kbTrk.id:0,kbCtx.program,kbCtx.synthEngine);}catch(er){}window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:kbTrk?kbTrk.id:0,pitch:pitch,velocity:100/127,percussion:!!(kbCtx&&kbCtx.synthEngine&&kbCtx.synthEngine.soundfont_bank===128)});}else if(window.SonicSF&&!(isStandaloneSf()&&isSfTrackEngine(kbCtx.synthEngine)&&!shouldRouteCarla(kbCtx.synthEngine))){// giữ note khi kéo qua phím (mouse enter) — dừng bằng mouseup/leave
window.SonicSF.playNote(pitch,100,5000,undefined,kbCtx.program,null,kbCtx.ch,kbCtx.synthEngine);}if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine,st.isArmed)){window.SonicCarlaMidi.noteOn(kbCtx.ch,pitch,100);if(window.__carlaKeybedTimer)clearTimeout(window.__carlaKeybedTimer);window.__carlaKeybedTimer=setTimeout(function(){window.SonicCarlaMidi.noteOff(kbCtx.ch,pitch);},650);}}catch(err){console.error('playNote error:',err);}}},onMouseUp:()=>{keybedMouseDownRef.current=false;// Dừng note khi thả phím — tránh kẹt âm (loop liên tục) với soundfont window.SonicSF.playNote(pitch,100,5000,undefined,kbCtx.program,null,kbCtx.ch,kbCtx.synthEngine);}if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(kbCtx.synthEngine,st.isArmed)){window.SonicCarlaMidi.noteOn(kbCtx.ch,pitch,100);if(window.__carlaKeybedTimer)clearTimeout(window.__carlaKeybedTimer);window.__carlaKeybedTimer=setTimeout(function(){window.SonicCarlaMidi.noteOff(kbCtx.ch,pitch);},650);}}catch(err){console.error('playNote error:',err);}}},onMouseUp:()=>{keybedMouseDownRef.current=false;// 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.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){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;}if(window.SonicCarlaMidi){try{window.SonicCarlaMidi.noteOff(kbCtx.ch,pitch);}catch(e){}}},onMouseLeave:()=>{if(!keybedMouseDownRef.current)return;// 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.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){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;}if(window.SonicCarlaMidi){try{window.SonicCarlaMidi.noteOff(kbCtx.ch,pitch);}catch(e){}}},onMouseLeave:()=>{if(!keybedMouseDownRef.current)return;// 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.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){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){}}}},showLabel&&label));}return keys;};const handleScroll=e=>{if(keybedRef.current){keybedRef.current.scrollTop=e.currentTarget.scrollTop;}if(rulerScrollRef.current){rulerScrollRef.current.scrollLeft=e.currentTarget.scrollLeft;}if(ccWrapperRef.current){ccWrapperRef.current.scrollLeft=e.currentTarget.scrollLeft;}const el=e.currentTarget;const THRESHOLD=200;if(el.scrollLeft+el.clientWidth>=el.scrollWidth-THRESHOLD){const newBeats=rollBeatsRef.current+16;setRollBeats(newBeats);}};const handleKeybedScroll=e=>{if(gridScrollRef.current){gridScrollRef.current.scrollTop=e.currentTarget.scrollTop;}};const SCALES={"None":null,"Diatonic":{"Major":[0,2,4,5,7,9,11],"Minor":[0,2,3,5,7,8,10],"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10],"Locrian":[0,1,3,5,6,8,10]},"Pentatonic":{"Major":[0,2,4,7,9],"Minor":[0,3,5,7,10],"Blues":[0,3,5,6,7,10],"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"India":[0,2,5,7,9],"Japan":[0,2,5,7,9],"Africa":[0,3,5,7,10]},"Church":{"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10]},"Jazz":{"Blues":[0,3,5,6,7,10],"Bebop":[0,2,4,5,7,9,10,11],"Diminished":[0,2,3,5,6,8,9,11]},"Asian Traditional":{"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"Japan":[0,2,5,7,9],"India":[0,2,5,7,9],"Gamelan":[0,2,4,7,9],"Korea":[0,2,4,5,7,9]},"Middle Eastern":{"Hijaz":[0,1,4,5,7,8,11],"Nikriz":[0,1,4,5,7,8,10],"Rast":[0,2,4,5,7,8,10],"Saba":[0,1,3,4,7,8,10],"Bayati":[0,2,3,4,7,8,10]}};const[selectedScale,setSelectedScale]=React.useState(null);const selectedScaleRef=React.useRef(null);selectedScaleRef.current=selectedScale;// Root note (0-11 — C..B) — transpose scale highlight + snap (spec 20:12) 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,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){}}}},showLabel&&label));}return keys;};const handleScroll=e=>{if(keybedRef.current){keybedRef.current.scrollTop=e.currentTarget.scrollTop;}if(rulerScrollRef.current){rulerScrollRef.current.scrollLeft=e.currentTarget.scrollLeft;}if(ccWrapperRef.current){ccWrapperRef.current.scrollLeft=e.currentTarget.scrollLeft;}const el=e.currentTarget;const THRESHOLD=200;if(el.scrollLeft+el.clientWidth>=el.scrollWidth-THRESHOLD){const newBeats=rollBeatsRef.current+16;setRollBeats(newBeats);}};const handleKeybedScroll=e=>{if(gridScrollRef.current){gridScrollRef.current.scrollTop=e.currentTarget.scrollTop;}};const SCALES={"None":null,"Diatonic":{"Major":[0,2,4,5,7,9,11],"Minor":[0,2,3,5,7,8,10],"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10],"Locrian":[0,1,3,5,6,8,10]},"Pentatonic":{"Major":[0,2,4,7,9],"Minor":[0,3,5,7,10],"Blues":[0,3,5,6,7,10],"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"India":[0,2,5,7,9],"Japan":[0,2,5,7,9],"Africa":[0,3,5,7,10]},"Church":{"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10]},"Jazz":{"Blues":[0,3,5,6,7,10],"Bebop":[0,2,4,5,7,9,10,11],"Diminished":[0,2,3,5,6,8,9,11]},"Asian Traditional":{"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"Japan":[0,2,5,7,9],"India":[0,2,5,7,9],"Gamelan":[0,2,4,7,9],"Korea":[0,2,4,5,7,9]},"Middle Eastern":{"Hijaz":[0,1,4,5,7,8,11],"Nikriz":[0,1,4,5,7,8,10],"Rast":[0,2,4,5,7,8,10],"Saba":[0,1,3,4,7,8,10],"Bayati":[0,2,3,4,7,8,10]}};const[selectedScale,setSelectedScale]=React.useState(null);const selectedScaleRef=React.useRef(null);selectedScaleRef.current=selectedScale;// Root note (0-11 — C..B) — transpose scale highlight + snap (spec 20:12)
@@ -775,7 +780,10 @@ const[selectedSoundFontId,setSelectedSoundFontId]=useState(null);const[sfPresets
const[instrumentDropdownTrackId,setInstrumentDropdownTrackId]=useState(null);const[instrumentDropdownBtnRect,setInstrumentDropdownBtnRect]=useState(null);const[instrumentSearchQuery,setInstrumentSearchQuery]=useState('');const[sfPresetSearchQuery,setSfPresetSearchQuery]=useState('');const filteredInstruments=useMemo(()=>{if(!instrumentSelectorData||!instrumentSearchQuery)return{soundfonts:instrumentSelectorData?.soundfonts||[],vst:instrumentSelectorData?.vst_instruments||[]};const q=instrumentSearchQuery.toLowerCase();return{soundfonts:(instrumentSelectorData.soundfonts||[]).filter(sf=>(sf.display||sf.name||sf.id).toLowerCase().includes(q)),vst:(instrumentSelectorData.vst_instruments||[]).filter(v=>(v.name||v.id).toLowerCase().includes(q))};},[instrumentSearchQuery,instrumentSelectorData]);useEffect(()=>{window.SonicAPI.listPlugins().then(async data=>{try{const catResp=(await window.SonicAPI.getSoundfontCatalog?.())??(await fetch('/api/v1/plugins/soundfonts/catalog').then(r=>r.json()));const catalog=catResp.full_catalog||{};data.soundfonts=(data.soundfonts||[]).map(sf=>{const sfId=sf.id.replace('sf_','');const catEntry=catalog[sfId.toLowerCase()]||catalog[sfId];if(catEntry&&catEntry.instruments){return{...sf,presets:catEntry.instruments};}return sf;});}catch(e){console.warn('Catalog fetch error:',e);}setInstrumentSelectorData(data);}).catch(()=>{});},[instrumentRefreshKey]);useEffect(()=>{if(!instrumentDropdownTrackId)return;const handler=e=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setInstrumentSearchQuery('');};document.addEventListener('click',handler);return()=>document.removeEventListener('click',handler);},[instrumentDropdownTrackId]);const GM_INSTRUMENTS=["Acoustic Grand Piano","Bright Acoustic Piano","Electric Grand Piano","Honky-tonk Piano","Electric Piano 1","Electric Piano 2","Harpsichord","Clavi","Celesta","Glockenspiel","Music Box","Vibraphone","Marimba","Xylophone","Tubular Bells","Dulcimer","Drawbar Organ","Percussive Organ","Rock Organ","Church Organ","Reed Organ","Accordion","Harmonica","Tango Accordion","Acoustic Guitar (nylon)","Acoustic Guitar (steel)","Electric Guitar (jazz)","Electric Guitar (clean)","Electric Guitar (muted)","Overdriven Guitar","Distortion Guitar","Guitar harmonics","Acoustic Bass","Electric Bass (finger)","Electric Bass (pick)","Fretless Bass","Slap Bass 1","Slap Bass 2","Synth Bass 1","Synth Bass 2","Violin","Viola","Cello","Contrabass","Tremolo Strings","Pizzicato Strings","Orchestral Harp","Timpani","String Ensemble 1","String Ensemble 2","Synth Strings 1","Synth Strings 2","Choir Aahs","Voice Oohs","Synth Voice","Orchestra Hit","Trumpet","Trombone","Tuba","Muted Trumpet","French Horn","Brass Section","Synth Brass 1","Synth Brass 2","Soprano Sax","Alto Sax","Tenor Sax","Baritone Sax","Oboe","English Horn","Bassoon","Clarinet","Piccolo","Flute","Recorder","Pan Flute","Blown Bottle","Shakuhachi","Whistle","Ocarina","Lead 1 (square)","Lead 2 (sawtooth)","Lead 3 (calliope)","Lead 4 (chiff)","Lead 5 (charang)","Lead 6 (voice)","Lead 7 (fifths)","Lead 8 (bass+lead)","Pad 1 (new age)","Pad 2 (warm)","Pad 3 (polysynth)","Pad 4 (choir)","Pad 5 (bowed)","Pad 6 (metallic)","Pad 7 (halo)","Pad 8 (sweep)","FX 1 (rain)","FX 2 (soundtrack)","FX 3 (crystal)","FX 4 (atmosphere)","FX 5 (brightness)","FX 6 (goblins)","FX 7 (echoes)","FX 8 (sci-fi)","Sitar","Banjo","Shamisen","Koto","Kalimba","Bag pipe","Fiddle","Shanai","Tinkle Bell","Agogo","Steel Drums","Woodblock","Taiko Drum","Melodic Tom","Synth Drum","Reverse Cymbal","Guitar Fret Noise","Breath Noise","Seashore","Bird Tweet","Telephone Ring","Helicopter","Applause","Gunshot"];// D7: bridge active → load instrument vào bridge tại channel router dùng cho const[instrumentDropdownTrackId,setInstrumentDropdownTrackId]=useState(null);const[instrumentDropdownBtnRect,setInstrumentDropdownBtnRect]=useState(null);const[instrumentSearchQuery,setInstrumentSearchQuery]=useState('');const[sfPresetSearchQuery,setSfPresetSearchQuery]=useState('');const filteredInstruments=useMemo(()=>{if(!instrumentSelectorData||!instrumentSearchQuery)return{soundfonts:instrumentSelectorData?.soundfonts||[],vst:instrumentSelectorData?.vst_instruments||[]};const q=instrumentSearchQuery.toLowerCase();return{soundfonts:(instrumentSelectorData.soundfonts||[]).filter(sf=>(sf.display||sf.name||sf.id).toLowerCase().includes(q)),vst:(instrumentSelectorData.vst_instruments||[]).filter(v=>(v.name||v.id).toLowerCase().includes(q))};},[instrumentSearchQuery,instrumentSelectorData]);useEffect(()=>{window.SonicAPI.listPlugins().then(async data=>{try{const catResp=(await window.SonicAPI.getSoundfontCatalog?.())??(await fetch('/api/v1/plugins/soundfonts/catalog').then(r=>r.json()));const catalog=catResp.full_catalog||{};data.soundfonts=(data.soundfonts||[]).map(sf=>{const sfId=sf.id.replace('sf_','');const catEntry=catalog[sfId.toLowerCase()]||catalog[sfId];if(catEntry&&catEntry.instruments){return{...sf,presets:catEntry.instruments};}return sf;});}catch(e){console.warn('Catalog fetch error:',e);}setInstrumentSelectorData(data);}).catch(()=>{});},[instrumentRefreshKey]);useEffect(()=>{if(!instrumentDropdownTrackId)return;const handler=e=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setInstrumentSearchQuery('');};document.addEventListener('click',handler);return()=>document.removeEventListener('click',handler);},[instrumentDropdownTrackId]);const GM_INSTRUMENTS=["Acoustic Grand Piano","Bright Acoustic Piano","Electric Grand Piano","Honky-tonk Piano","Electric Piano 1","Electric Piano 2","Harpsichord","Clavi","Celesta","Glockenspiel","Music Box","Vibraphone","Marimba","Xylophone","Tubular Bells","Dulcimer","Drawbar Organ","Percussive Organ","Rock Organ","Church Organ","Reed Organ","Accordion","Harmonica","Tango Accordion","Acoustic Guitar (nylon)","Acoustic Guitar (steel)","Electric Guitar (jazz)","Electric Guitar (clean)","Electric Guitar (muted)","Overdriven Guitar","Distortion Guitar","Guitar harmonics","Acoustic Bass","Electric Bass (finger)","Electric Bass (pick)","Fretless Bass","Slap Bass 1","Slap Bass 2","Synth Bass 1","Synth Bass 2","Violin","Viola","Cello","Contrabass","Tremolo Strings","Pizzicato Strings","Orchestral Harp","Timpani","String Ensemble 1","String Ensemble 2","Synth Strings 1","Synth Strings 2","Choir Aahs","Voice Oohs","Synth Voice","Orchestra Hit","Trumpet","Trombone","Tuba","Muted Trumpet","French Horn","Brass Section","Synth Brass 1","Synth Brass 2","Soprano Sax","Alto Sax","Tenor Sax","Baritone Sax","Oboe","English Horn","Bassoon","Clarinet","Piccolo","Flute","Recorder","Pan Flute","Blown Bottle","Shakuhachi","Whistle","Ocarina","Lead 1 (square)","Lead 2 (sawtooth)","Lead 3 (calliope)","Lead 4 (chiff)","Lead 5 (charang)","Lead 6 (voice)","Lead 7 (fifths)","Lead 8 (bass+lead)","Pad 1 (new age)","Pad 2 (warm)","Pad 3 (polysynth)","Pad 4 (choir)","Pad 5 (bowed)","Pad 6 (metallic)","Pad 7 (halo)","Pad 8 (sweep)","FX 1 (rain)","FX 2 (soundtrack)","FX 3 (crystal)","FX 4 (atmosphere)","FX 5 (brightness)","FX 6 (goblins)","FX 7 (echoes)","FX 8 (sci-fi)","Sitar","Banjo","Shamisen","Koto","Kalimba","Bag pipe","Fiddle","Shanai","Tinkle Bell","Agogo","Steel Drums","Woodblock","Taiko Drum","Melodic Tom","Synth Drum","Reverse Cymbal","Guitar Fret Noise","Breath Noise","Seashore","Bird Tweet","Telephone Ring","Helicopter","Applause","Gunshot"];// D7: bridge active → load instrument vào bridge tại channel router dùng cho
// track (SonicMidiRouter.allocateChannel — cùng allocation scheduleMidiNoteDispatch/ // track (SonicMidiRouter.allocateChannel — cùng allocation scheduleMidiNoteDispatch/
// keybed dùng khi push NOTE_ON). Không load → MIDI tới channel rỗng → C++ silent. // keybed dùng khi push NOTE_ON). Không load → MIDI tới channel rỗng → C++ silent.
const loadTrackInstrumentToBridge=(trackId,instrumentId,bank)=>{if(!window.SonicMidiRouter||!window.SonicMidiRouter.isBridgeActive()||!instrumentId)return;const isSf=typeof instrumentId==='string'&&instrumentId.startsWith('sf_');const bch=window.SonicMidiRouter.allocateChannel(trackId,bank===128);const btype=isSf?'SF2':'VST3';let bpath=null;if(instrumentSelectorData){if(isSf){const sid=instrumentId.replace('sf_','');const s0=(instrumentSelectorData.soundfonts||[]).find(s=>String(s.id).replace('sf_','')===sid);bpath=s0&&(s0.file||s0.path);}else{const v0=(instrumentSelectorData.vst_instruments||[]).find(v=>v.id===instrumentId||v.name===instrumentId);bpath=v0&&v0.path;}}(async()=>{let p=bpath;try{const r=await window.SonicAPI.bridgeLoad({name:instrumentId,path:bpath||null,instrumentType:btype,channel:bch});if(r&&r.path)p=r.path;}catch(e){console.warn('[Bridge] resolve fail:',e);}if(p){const ok=await window.NativeBridgeService.loadInstrument(p,btype,bch);console.log('[Bridge] track',trackId,'-> bridge',btype,'ch',bch,ok?'OK':'FAIL',p);// Requirement 2: chọn VST3 qua nút Synth → load xong mở native GUI 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);// Reset program-change dedupe của track — LOAD async (C++ worker): CC/
// PROGRAM gửi trước khi load xong bị drop (inst chưa có) → note kế tiếp
// phải gửi lại program change để ra ĐÚNG instrument.
if(ok&&window.__bridgeLastProgram){const _pfx=trackId+':';Object.keys(window.__bridgeLastProgram).forEach(k=>{if(k.indexOf(_pfx)===0)delete window.__bridgeLastProgram[k];});}// Requirement 2: chọn VST3 qua nút Synth → load xong mở native GUI
// (C++ attach editor vào cửa sổ bridge tự tạo — control type=4, hwnd=0). // (C++ attach editor vào cửa sổ bridge tự tạo — control type=4, hwnd=0).
if(ok&&btype==='VST3'&&window.NativeBridgeService.openNativeGUI){try{await window.NativeBridgeService.openNativeGUI(instrumentId,bch);}catch(e){}}}})();};const setTrackInstrumentWithProgram=(trackId,instrumentId,programNumber,displayName,bankNumber)=>{const isSfInstrument=instrumentId&&typeof instrumentId==='string'&&instrumentId.startsWith('sf_');const sfBank=bankNumber!==undefined?bankNumber:isSfInstrument?0:undefined;const sfProg=programNumber!==undefined?programNumber:undefined;var mt=activeTracksRef.current||tracks;var curTrk=null;for(var ci=0;ci<mt.length;ci++){if(mt[ci].id===trackId){curTrk=mt[ci];break;}}// ── Unload Carla bridge khi chuyển từ VSTi sang instrument KHÔNG phải VST ── if(ok&&btype==='VST3'&&window.NativeBridgeService.openNativeGUI){try{await window.NativeBridgeService.openNativeGUI(instrumentId,bch);}catch(e){}}}})();};const setTrackInstrumentWithProgram=(trackId,instrumentId,programNumber,displayName,bankNumber)=>{const isSfInstrument=instrumentId&&typeof instrumentId==='string'&&instrumentId.startsWith('sf_');const sfBank=bankNumber!==undefined?bankNumber:isSfInstrument?0:undefined;const sfProg=programNumber!==undefined?programNumber:undefined;var mt=activeTracksRef.current||tracks;var curTrk=null;for(var ci=0;ci<mt.length;ci++){if(mt[ci].id===trackId){curTrk=mt[ci];break;}}// ── Unload Carla bridge khi chuyển từ VSTi sang instrument KHÔNG phải VST ──
// (soundfont/GM/default). Nếu không, Carla vẫn chạy với VSTi cũ → MIDI vẫn // (soundfont/GM/default). Nếu không, Carla vẫn chạy với VSTi cũ → MIDI vẫn
@@ -1429,16 +1437,16 @@ const subAnalyser=context.createAnalyser();subAnalyser.fftSize=256;subAnalyser.s
// mastering dù sub-track ♪/A đang tắt). // mastering dù sub-track ♪/A đang tắt).
if(window.masterBus&&window.masterBus.input){routeGain.connect(window.masterBus.input);}else{routeGain.connect(parentNode);}if(window.masterBus&&window.masterBus.dryInput){dryGain.connect(window.masterBus.dryInput);}else{dryGain.connect(parentNode);}node={gainNode,pannerNode:null,analyserNode:subAnalyser,route:{routeGain:routeGain,dryGain:dryGain,_trackId:subTrack.id}};activeTrackNodesRef.current[subKey]=node;}return node.gainNode;};const playMidiPreviewNote=(pitch,velocity=0.8,durationMs=500)=>{if(!window.SonicSF)return;const context=getAudioContext();const tab=subTabs.find(s=>s.id===activeTabRef.current);const trackId=tab?tab.trackId:selectedTrackId;const track=activeTracks.find(t=>t.id===trackId);const destNode=getOrCreateTrackNode(track,context);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 if(window.masterBus&&window.masterBus.input){routeGain.connect(window.masterBus.input);}else{routeGain.connect(parentNode);}if(window.masterBus&&window.masterBus.dryInput){dryGain.connect(window.masterBus.dryInput);}else{dryGain.connect(parentNode);}node={gainNode,pannerNode:null,analyserNode:subAnalyser,route:{routeGain:routeGain,dryGain:dryGain,_trackId:subTrack.id}};activeTrackNodesRef.current[subKey]=node;}return node.gainNode;};const playMidiPreviewNote=(pitch,velocity=0.8,durationMs=500)=>{if(!window.SonicSF)return;const context=getAudioContext();const tab=subTabs.find(s=>s.id===activeTabRef.current);const trackId=tab?tab.trackId:selectedTrackId;const track=activeTracks.find(t=>t.id===trackId);const destNode=getOrCreateTrackNode(track,context);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). // 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;}window.SonicSF.playNote(pitch,velocity,durationMs,context.currentTime,program,destNode,prevCh,track?track.synth_engine:undefined);};// D2: timeline MIDI note -> native bridge (khi bridge active) thay vì 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{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(tch,program,track&&track.synth_engine);}catch(e){}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;}window.SonicSF.playNote(pitch,velocity,durationMs,context.currentTime,program,destNode,prevCh,track?track.synth_engine:undefined);};// D2: timeline MIDI note -> native bridge (khi bridge active) thay vì
// SonicSF (FluidSynth WASM). Giữ setTimeout scheduling (như SonicSF cũ); // SonicSF (FluidSynth WASM). Giữ setTimeout scheduling (như SonicSF cũ);
// bridge tự render note-off sau duration. A11 (sample-accurate) sẽ thay // bridge tự render note-off sau duration. A11 (sample-accurate) sẽ thay
// setTimeout bằng sampleOffset đẩy thẳng vào SHM. // 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 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 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||subTabsRef.current&&subTabsRef.current.some(s=>s.isPlaying);}:function(){return true;};const trkId=track?track.id:0;// Percussion (bank 128) → router allocateChannel ch 9; melodic → round-robin. const guardPlay=startTime!=null?function(){return isPlayingRef.current||subTabsRef.current&&subTabsRef.current.some(s=>s.isPlaying);}:function(){return true;};const trkId=track?track.id:0;// Percussion (bank 128) → router allocateChannel ch 9; melodic → round-robin.
const isPerc=!!(synthEngine&&synthEngine.soundfont_bank===128);// D8: track đổi instrument → gửi CC0/CC32 (bank 0) + program change qua const isPerc=!!(synthEngine&&synthEngine.soundfont_bank===128);// D8: track đổi instrument → gửi CC0/CC32 (bank THẬT từ synthEngine) +
// bridge (A12) một lần mỗi program mới mỗi channel. // program change qua bridge (A12) — helper dedupe theo track+bank+program.
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,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,percussion:isPerc});}catch(e){}},delayMs);setTimeout(function(){if(!guardPlay())return;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:trkId,pitch:pitch||60,velocity:0,percussion:isPerc});}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 if(program!==undefined&&program!==null){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(trkId,program,synthEngine);}catch(e){}}setTimeout(function(){if(!guardPlay())return;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,percussion:isPerc});}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). // 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 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). // qua mastering FX (khi bật) và qua Carla bridge (khi VSTi loaded).
+8 -1
View File
@@ -6,13 +6,13 @@
var engine = { var engine = {
_connected: false, _connected: false,
_trackId: null, _trackId: null,
_dest: null,
isConnected: function () { return this._connected; }, isConnected: function () { return this._connected; },
/** bridgeNode = window.BridgeAudioNode; trackCtx = track node ({ sfEntry, gainNode }). */ /** bridgeNode = window.BridgeAudioNode; trackCtx = track node ({ sfEntry, gainNode }). */
connect: function (bridgeNode, trackCtx, trackId) { connect: function (bridgeNode, trackCtx, trackId) {
if (!bridgeNode || !bridgeNode.getOutputNode) return false; if (!bridgeNode || !bridgeNode.getOutputNode) return false;
this.disconnect();
var dest = null; var dest = null;
if (trackCtx && trackCtx.sfEntry) dest = trackCtx.sfEntry; if (trackCtx && trackCtx.sfEntry) dest = trackCtx.sfEntry;
else if (trackCtx && trackCtx.gainNode) dest = trackCtx.gainNode; else if (trackCtx && trackCtx.gainNode) dest = trackCtx.gainNode;
@@ -21,10 +21,16 @@
console.warn('[AudioRoutingEngine] no destination (no trackCtx / masterBus)'); console.warn('[AudioRoutingEngine] no destination (no trackCtx / masterBus)');
return false; return false;
} }
// Idempotent: updateSfRouting/ensureMasteringRouting chay tren MOI keydown/
// note-on — neu da noi dung dest thi KHONG disconnect (disconnect flush ring
// -> am phim dang vang bi cat). Chi doi khi dest/track thuc su khac.
if (this._connected && this._dest === dest && this._trackId === (trackId || null)) return true;
this.disconnect();
if (!bridgeNode.isReady()) bridgeNode.init(); if (!bridgeNode.isReady()) bridgeNode.init();
bridgeNode.connect(dest); bridgeNode.connect(dest);
this._connected = true; this._connected = true;
this._trackId = trackId || null; this._trackId = trackId || null;
this._dest = dest;
console.log('[AudioRoutingEngine] bridge audio -> ' + (trackId || 'masterBus')); console.log('[AudioRoutingEngine] bridge audio -> ' + (trackId || 'masterBus'));
return true; return true;
}, },
@@ -33,6 +39,7 @@
if (window.BridgeAudioNode) window.BridgeAudioNode.disconnect(); if (window.BridgeAudioNode) window.BridgeAudioNode.disconnect();
this._connected = false; this._connected = false;
this._trackId = null; this._trackId = null;
this._dest = null;
} }
}; };
+9 -1
View File
@@ -12,6 +12,7 @@
var _gainNode = null; var _gainNode = null;
var _ctx = null; var _ctx = null;
var _initialized = false; var _initialized = false;
var _dest = null; // destination gain node da noi (chong connect trung)
function _getCtx() { function _getCtx() {
if (_ctx) return _ctx; if (_ctx) return _ctx;
@@ -78,6 +79,7 @@
_spn = null; _spn = null;
_gainNode = null; _gainNode = null;
_ctx = null; _ctx = null;
_dest = null;
} }
window.BridgeAudioNode = { window.BridgeAudioNode = {
@@ -85,7 +87,13 @@
onAudio: onAudio, onAudio: onAudio,
flush: flush, flush: flush,
getOutputNode: getOutputNode, getOutputNode: getOutputNode,
connect: function (dest) { if (_gainNode) _gainNode.connect(dest); }, connect: function (dest) {
if (!_gainNode) return;
if (_dest === dest) return; // da noi — khong noi trung (double-sum)
if (_dest) { try { _gainNode.disconnect(_dest); } catch (e) {} }
_gainNode.connect(dest);
_dest = dest;
},
disconnect: disconnect, disconnect: disconnect,
isReady: function () { return _initialized; } isReady: function () { return _initialized; }
}; };
@@ -35,6 +35,9 @@ private:
void* settings; // fluid_settings_t* void* settings; // fluid_settings_t*
void* synth; // fluid_synth_t* void* synth; // fluid_synth_t*
int sfontId; int sfontId;
// Per-channel bank select (CC0<<7 | CC32) — programChange phai dung bank
// that (truoc day hardcode bank 0 -> preset o bank != 0 khong chon duoc).
uint32_t bank_[16];
}; };
// sfizz (.sfz) // sfizz (.sfz)
+14 -5
View File
@@ -16,7 +16,9 @@
// 1. SOUNDFONT ENGINE (.SF2 / .SF3) VIA FLUIDSYNTH C API // 1. SOUNDFONT ENGINE (.SF2 / .SF3) VIA FLUIDSYNTH C API
// ----------------------------------------------------------------- // -----------------------------------------------------------------
FluidSynthInstrument::FluidSynthInstrument() FluidSynthInstrument::FluidSynthInstrument()
: settings(nullptr), synth(nullptr), sfontId(-1) {} : settings(nullptr), synth(nullptr), sfontId(-1) {
for (uint32_t i = 0; i < 16; ++i) bank_[i] = 0;
}
FluidSynthInstrument::~FluidSynthInstrument() { FluidSynthInstrument::~FluidSynthInstrument() {
if (synth) delete_fluid_synth(FS_SYNTH); if (synth) delete_fluid_synth(FS_SYNTH);
@@ -36,6 +38,7 @@ bool FluidSynthInstrument::loadSoundFontFile(const std::string& path, double sam
if (sfontId == -1) return false; if (sfontId == -1) return false;
// Reset all channels to font preset 0 (spec §VII: bank0/prog0 piano) // Reset all channels to font preset 0 (spec §VII: bank0/prog0 piano)
for (uint32_t ch = 0; ch < 16; ++ch) { for (uint32_t ch = 0; ch < 16; ++ch) {
bank_[ch] = 0;
fluid_synth_program_select(FS_SYNTH, ch, sfontId, 0, 0); fluid_synth_program_select(FS_SYNTH, ch, sfontId, 0, 0);
} }
return true; return true;
@@ -46,7 +49,8 @@ bool FluidSynthInstrument::init(double sampleRate, uint32_t maxBlockSize) {
} }
void FluidSynthInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) { void FluidSynthInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32_t program) {
if (!synth) return; if (!synth || channel >= 16) return;
bank_[channel] = bank;
fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank, program); fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank, program);
} }
@@ -62,13 +66,18 @@ void FluidSynthInstrument::noteOff(uint32_t channel, uint32_t pitch, uint32_t sa
} }
void FluidSynthInstrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) { void FluidSynthInstrument::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
if (!synth) return; if (!synth || channel >= 16) return;
// Bank select MSB/LSB (A12): CC0 = (bank>>7)&0x7F, CC32 = bank&0x7F
if (cc == 0) bank_[channel] = (bank_[channel] & 0x7Fu) | ((value & 0x7Fu) << 7);
else if (cc == 32) bank_[channel] = (bank_[channel] & ~0x7Fu) | (value & 0x7Fu);
fluid_synth_cc(FS_SYNTH, channel, cc, value); fluid_synth_cc(FS_SYNTH, channel, cc, value);
} }
void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) { void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) {
if (!synth) return; if (!synth || channel >= 16) return;
fluid_synth_program_select(FS_SYNTH, channel, sfontId, 0, program); // Dung bank da nhan tu CC0/CC32 — bank hardcode 0 lam preset o bank != 0
// khong duoc chon (fluid giu preset cu -> ra piano sai).
fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank_[channel], program);
} }
void FluidSynthInstrument::pitchBend(uint32_t channel, uint32_t bend14) { void FluidSynthInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
+48 -25
View File
@@ -49,14 +49,30 @@ static void sleep_ms(uint32_t ms) {
} }
#ifdef _WIN32 #ifdef _WIN32
// Native VST editor windows registry — global de WM_DESTROY (chay tren worker
// thread cua channel tao window) co the don map. USERDATA luu channel+1 (KHONG
// luu con tro inst truc tiep: assign() thay inst moi moi lan load — con tro cu
// bi huy → WM_DESTROY tren con tro dangling → crash/hang bridge).
static InstrumentEngineManager* g_engine = nullptr;
static std::mutex g_guiMutex;
static std::map<uint32_t, void*> g_guiWindows; // channel -> HWND (keep window alive)
static std::map<HWND, uint32_t> g_hwndToCh; // HWND -> channel (WM_DESTROY cleanup)
static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { static LRESULT CALLBACK VstWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
if (uMsg == WM_DESTROY) { if (uMsg == WM_DESTROY) {
void* ptr = (void*)GetWindowLongPtrA(hwnd, GWLP_USERDATA); INativeInstrument* instToClose = nullptr;
if (ptr) { {
INativeInstrument* inst = static_cast<INativeInstrument*>(ptr); std::lock_guard<std::mutex> lock(g_guiMutex);
inst->closeGUI(); auto it = g_hwndToCh.find(hwnd);
if (it != g_hwndToCh.end()) {
uint32_t ch = it->second;
g_hwndToCh.erase(it);
g_guiWindows.erase(ch);
if (g_engine) instToClose = g_engine->get(ch);
} }
} }
if (instToClose) instToClose->closeGUI();
}
return DefWindowProcA(hwnd, uMsg, wParam, lParam); return DefWindowProcA(hwnd, uMsg, wParam, lParam);
} }
#endif #endif
@@ -184,12 +200,15 @@ int main(int argc, char* argv[]) {
#endif #endif
InstrumentEngineManager instruments; InstrumentEngineManager instruments;
#ifdef _WIN32
g_engine = &instruments;
#endif
// Per-channel persistent workers: loadPlugin + openGUI run on the SAME // Per-channel persistent workers: loadPlugin + openGUI run on the SAME
// thread whose COM STA apartment stays alive for the channel's lifetime // thread whose COM STA apartment stays alive for the channel's lifetime
// (see ChannelWorker comment — a dead apartment hangs Nexus attached()). // (see ChannelWorker comment — a dead apartment hangs Nexus attached()).
std::map<uint32_t, std::unique_ptr<ChannelWorker>> workers; std::map<uint32_t, std::unique_ptr<ChannelWorker>> workers;
// B9: native editor windows per channel — keep alive (HWND outlives the job). // B9: native editor windows per channel — keep alive (HWND outlives the job).
std::map<uint32_t, void*> guiWindows; // Registry la global (g_guiWindows) — WM_DESTROY cleanup can tu VstWindowProc.
// B8: sample rate from the DAW (Rust spawns us with SF_SAMPLE_RATE). // B8: sample rate from the DAW (Rust spawns us with SF_SAMPLE_RATE).
// Block size is fixed by the SHM layout (AUDIO_BLOCK_SIZE) — SF_BLOCK_SIZE // Block size is fixed by the SHM layout (AUDIO_BLOCK_SIZE) — SF_BLOCK_SIZE
// is accepted but must match, otherwise warned and ignored. // is accepted but must match, otherwise warned and ignored.
@@ -310,50 +329,55 @@ int main(int argc, char* argv[]) {
// cho openGUI lai tao COM apartment moi, con plugin thi song o // cho openGUI lai tao COM apartment moi, con plugin thi song o
// apartment cu da chet (load thread exit) -> Nexus attached() // apartment cu da chet (load thread exit) -> Nexus attached()
// hang (gui_probe: bridge_like treo, same_thread OK). // hang (gui_probe: bridge_like treo, same_thread OK).
// LOAD va OPEN_GUI duoc drain trong cung vong lap: LOAD post job
// len worker (async, VST3 init co the mat giay) truoc khi type=4
// duoc xu ly — khong doi, instruments.get() con rong -> "no
// instrument loaded". Poll toi da 10s cho LOAD hoan tat.
uint32_t guiCh = c.channel; uint32_t guiCh = c.channel;
if (guiCh >= 16) guiCh = 0; if (guiCh >= 16) guiCh = 0;
for (int tries = 0; tries < 200 && !instruments.get(guiCh); ++tries) { // Bo poll 10s tren real-time loop (writeIndex stall > 3s -> Rust
sleep_ms(50); // tuong bridge chet va restart -> 2 bridge cung map SHM -> race).
} // LOAD (type=2) post truoc OPEN_GUI tren CUNG ChannelWorker (FIFO)
if (!instruments.get(guiCh)) { // -> job openGUI chay sau khi LOAD xong -> kiem tra inst trong job.
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << c.arg1
<< " plugin=" << c.arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl;
} else {
if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>(); if (!workers[guiCh]) workers[guiCh] = std::make_unique<ChannelWorker>();
workers[guiCh]->post([&instruments, &guiWindows, guiCh, arg1 = c.arg1, arg2 = std::string(c.arg2)]() { workers[guiCh]->post([&instruments, guiCh, arg1 = c.arg1, arg2 = std::string(c.arg2)]() {
if (!instruments.get(guiCh)) {
std::cerr << "[NativeBridge] GUI attach FAILED hwnd=" << arg1
<< " plugin=" << arg2 << " ch=" << guiCh << " (no instrument loaded)" << std::endl;
return;
}
std::cerr << "[dbg] openGUI thread start hwnd=" << arg1 std::cerr << "[dbg] openGUI thread start hwnd=" << arg1
<< " plugin=" << arg2 << " ch=" << guiCh << std::endl; << " plugin=" << arg2 << " ch=" << guiCh << std::endl;
void* hwnd = (void*)(uintptr_t)arg1; void* hwnd = (void*)(uintptr_t)arg1;
#ifdef _WIN32 #ifdef _WIN32
if (arg1 == 0) { if (arg1 == 0) {
HWND existingHwnd = nullptr; HWND existingHwnd = nullptr;
auto it = guiWindows.find(guiCh); {
if (it != guiWindows.end()) { std::lock_guard<std::mutex> lock(g_guiMutex);
existingHwnd = (HWND)it->second; auto it = g_guiWindows.find(guiCh);
if (it != g_guiWindows.end()) existingHwnd = (HWND)it->second;
} }
if (existingHwnd && IsWindow(existingHwnd)) { if (existingHwnd && IsWindow(existingHwnd)) {
hwnd = existingHwnd; hwnd = existingHwnd;
SetWindowTextA((HWND)hwnd, arg2.c_str()); SetWindowTextA((HWND)hwnd, arg2.c_str());
ShowWindow((HWND)hwnd, SW_SHOW); ShowWindow((HWND)hwnd, SW_SHOW);
SetForegroundWindow((HWND)hwnd); SetForegroundWindow((HWND)hwnd);
// Reuse: cap nhat USERDATA (channel+1) — inst CU
// da bi thay the boi assign() -> WM_DESTROY sau
// nay lookup inst MOI, khong dung con tro dangling.
SetWindowLongPtrA((HWND)hwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
} else { } else {
hwnd = create_native_vst_window(arg2.c_str()); hwnd = create_native_vst_window(arg2.c_str());
if (!hwnd) { if (!hwnd) {
std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl; std::cerr << "[NativeBridge] GUI create window FAILED plugin=" << arg2 << std::endl;
return; return;
} }
guiWindows[guiCh] = hwnd; // keep window alive {
if (auto* inst = instruments.get(guiCh)) { std::lock_guard<std::mutex> lock(g_guiMutex);
SetWindowLongPtrA((HWND)hwnd, GWLP_USERDATA, (LONG_PTR)inst); g_guiWindows[guiCh] = hwnd; // keep window alive
g_hwndToCh[(HWND)hwnd] = guiCh; // WM_DESTROY cleanup
} }
SetWindowLongPtrA((HWND)hwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));
} }
} }
#else #else
(void)guiWindows; (void)0;
#endif #endif
if (auto* inst = instruments.get(guiCh)) { if (auto* inst = instruments.get(guiCh)) {
if (inst->openGUI(hwnd)) if (inst->openGUI(hwnd))
@@ -368,7 +392,6 @@ int main(int argc, char* argv[]) {
}); });
} }
} }
}
shmIPC->controlQueueCount = 0; shmIPC->controlQueueCount = 0;
// B. Snapshot queued MIDI events (bounded copy, queue reset immediately) // B. Snapshot queued MIDI events (bounded copy, queue reset immediately)