From 75eba27dcf821699f9787d113aad41a3c683a771 Mon Sep 17 00:00:00 2001 From: locpham Date: Mon, 10 Aug 2026 09:51:58 +0000 Subject: [PATCH] =?UTF-8?q?FIX:=20(1)=20masteringConnected=20default=20tru?= =?UTF-8?q?e=20+=20ensureMasteringRouting/refreshCarlaStatus=20=E1=BB=9F?= =?UTF-8?q?=20m=E1=BB=8Di=20=C4=91=C6=B0=E1=BB=9Dng=20play=20=E2=80=94=20M?= =?UTF-8?q?IDI=20item/preview=20qua=20mastering=20ngay=20khi=20ch=E1=BA=A1?= =?UTF-8?q?y=20app;=20(2)=20=5FinstrumentEpoch=20ch=E1=BB=91ng=20cache=20s?= =?UTF-8?q?tale=20+=20retry=20bank=200=20=E2=80=94=20h=E1=BA=BFt=20ch?= =?UTF-8?q?=E1=BB=8Dn=20Synth=20String=20nghe=20Piano;=20(3)=20/carla-stat?= =?UTF-8?q?us=20+=20auto-open=20Carla=20+=20route=20exclusive=20ch?= =?UTF-8?q?=E1=BB=89=20khi=20Carla=20s=E1=BB=91ng=20(fallback=20FluidSynth?= =?UTF-8?q?=20tr=C3=A1nh=20c=C3=A2m);=20(4)=20keybed=20gi=E1=BB=AF=20note?= =?UTF-8?q?=20theo=20th=E1=BB=9Di=20gian=20b=E1=BA=A5m=20(auto-off=205s=20?= =?UTF-8?q?ph=C3=B2ng=20h=E1=BB=9D,=20mouseup=20d=E1=BB=ABng=20ngay)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/plugins.py | 15 ++++++ app/static/js/app.jsx | 62 ++++++++++++++++++++--- app/static/js/app.precompiled.js | 39 +++++++++----- app/static/js/services/api.js | 3 ++ app/static/js/services/soundfontPlayer.js | 25 ++++++++- app/templates/index.html | 8 +-- wiki.md | 9 ++++ 7 files changed, 135 insertions(+), 26 deletions(-) diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 40f6db9..9308bbb 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -681,6 +681,21 @@ def _send_carla_all_notes_off() -> bool: return ok +@router.get("/carla-status") +async def carla_status(): + """Kiểm tra Carla bridge còn sống không (do app spawn) + cổng OSC đang dùng. + + Frontend dùng để quyết định: route MIDI item EXCLUSIVE qua Carla (chỉ khi + Carla đang chạy) hay fallback FluidSynth (luôn có âm) — tránh câm toàn + phần khi Carla bị đóng.""" + _prune_carla_processes() + return { + "success": True, + "running": len(_CARLA_PROCESSES) > 0, + "osc_port": _carla_osc_port(), + } + + @router.post("/carla-stop") async def carla_stop(authorization: Optional[str] = Header(None)): """Unload Carla bridge: tắt mọi note đang ngân + terminate tiến trình Carla diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 0763db4..0ab6f8c 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -101,6 +101,38 @@ const scheduleCarlaNote = (synthEngine, channel, pitch, velocity, startWallTime, } catch (e) { /* routing thất bại im lặng — soundfont vẫn chơi qua FluidSynth */ } }; +// ── Carla bridge alive tracking + auto-open ──────────────────────────────── +// window.__carlaRunning: undefined = chưa biết | true = đang chạy | false = đã chết. +// Quyết định route MIDI item EXCLUSIVE qua Carla hay fallback FluidSynth — +// tránh "câm toàn phần" khi Carla bị đóng (route chỉ-Carla mà Carla chết = im lặng). +const refreshCarlaStatus = () => { + try { + if (!window.SonicAPI || !window.SonicAPI.carlaStatus) return; + window.SonicAPI.carlaStatus().then(st => { + window.__carlaRunning = !!(st && st.running); + }).catch(() => { window.__carlaRunning = false; }); + } catch (e) {} +}; +// Carla chưa chạy → TỰ ĐỘNG mở với VSTi của track (fire-and-forget) để MIDI +// item play qua Carla bridge đúng yêu cầu. +const ensureCarlaForPlayback = (synthEngine) => { + try { + if (!window.SonicCarlaMidi || !window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)) return; + if (!window.SonicAPI || !window.SonicAPI.carlaStatus) return; + window.SonicAPI.carlaStatus().then(st => { + if (st && st.running) { window.__carlaRunning = true; return; } + window.__carlaRunning = false; + const pid = synthEngine && synthEngine.plugin_id; + if (pid) { + window.SonicAPI.openInCarla(pid).then(r => { + if (r && r.success) { window.__carlaRunning = true; } + else { window.__carlaRunning = false; } + }).catch(() => { window.__carlaRunning = false; }); + } + }).catch(() => { window.__carlaRunning = false; }); + } catch (e) {} +}; + // Handle ?sfs= from double-clicking a .sfs file (opens domain -> loads project) (function handleSfsDeepLink() { @@ -9101,7 +9133,10 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100); } if (window.SonicSF) { - window.SonicSF.playNote(pitch, 100, 500, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine); + // ⚠️ 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). + 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)) { @@ -9120,7 +9155,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos window.triggerMidiVuActivity((st.parent_tab_id && st.parent_tab_id.startsWith('session_') ? '_sess_' : '') + st.trackId, 100); } if (window.SonicSF) { - window.SonicSF.playNote(pitch, 100, 200, undefined, kbCtx.program, null, kbCtx.ch, 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); @@ -16392,7 +16428,7 @@ const App = () => { const [fxRackTarget, setFxRackTarget] = useState(null); window.__openFxRack = (trackId, trackName) => setFxRackTarget({ trackId, trackName }); const [masteringSettings, setMasteringSettings] = useState({ - masterConnected: false, + masterConnected: true, // ⚠️ FIX: bật ON mặc định — MIDI item/preview PHẢI được xử lí qua mastering FX ngay khi chạy app (không cần bật/tắt power) activeModule: 'eq', eqActive: true, imagerActive: true, @@ -21078,6 +21114,10 @@ const App = () => { const startTrackPlayback = offsetTime => { const context = getAudioContext(); + // ⚠️ 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) {} + refreshCarlaStatus(); const allPlayTracks = activeTracksRef.current && activeTracksRef.current.length ? activeTracksRef.current : activeTracks; // Capture items signature at schedule time — updatePlayhead so sánh với // signature này để phát hiện items đổi vị trí giữa lúc play (re-schedule). @@ -21134,6 +21174,7 @@ const App = () => { if ((track.type === 'MIDI' || midiItems.length > 0) && window.SonicSF) { // Preview cache: capture the soundfont (pre track-FX) for fast offline export ensureMidiCapture(track, activeTrackNodesRef.current[track.id]); + if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)) ensureCarlaForPlayback(track.synth_engine); var trkCh = assignTrackMidiChannel(track, allPlayTracks); // Ensure instrument is loaded in FluidSynth if (track.synth_engine && track.synth_engine.type === 'soundfont' && track.synth_engine.soundfont_id) { @@ -21156,7 +21197,7 @@ const App = () => { // Track VSTi + Carla local → MIDI item PHẢI play qua Carla bridge // (chỉ route Carla — KHÔNG play qua FluidSynth GM nữa, tránh âm // sai instrument chồng lên). Không có Carla → FluidSynth như cũ. - const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)); + const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)) && window.__carlaRunning !== false; if (offsetTime < noteStartSec) { const delay = noteStartSec - offsetTime; const startTime = context.currentTime + delay; @@ -21303,7 +21344,8 @@ const App = () => { var subCh = assignTrackMidiChannel(subTrack, secTracks); // MIDI item trong SECTION → Carla bridge khi VSTi loaded // (chỉ route Carla — không play FluidSynth GM sai âm) - const subRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine)); + const subRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine)) && window.__carlaRunning !== false; + if (subRouteCarla || window.SonicCarlaMidi.shouldRoutePlayback(subTrack.synth_engine)) ensureCarlaForPlayback(subTrack.synth_engine); if (offsetTime < noteStartMain) { const delay = noteStartMain - offsetTime; @@ -21372,6 +21414,8 @@ const App = () => { // Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2) const startLocalTrackPlayback = (trackId, offsetTime) => { const context = getAudioContext(); + try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {} + refreshCarlaStatus(); const track = tracks.find(t => t.id === trackId); if (!track) return; const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{ @@ -21426,7 +21470,7 @@ const App = () => { const program = track.instrumentProgram !== undefined ? track.instrumentProgram : 0; // MIDI item → Carla bridge khi VSTi loaded (chỉ route Carla — không // play FluidSynth GM sai âm chồng lên) - const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)); + const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine)) && window.__carlaRunning !== false; if (offsetTime < noteStartSec) { const delay = noteStartSec - offsetTime; const startTime = context.currentTime + delay; @@ -21469,6 +21513,8 @@ const App = () => { const schedulePianoRollMidi = (st, offsetSeconds, notesOverride) => { if (st.type !== 'PIANO_ROLL') return; const context = getAudioContext(); + try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (e) {} + refreshCarlaStatus(); const midiNotes = notesOverride || st.notes || []; const bpmVal = parseInt(bpm) || 120; const secondsPerBeat = 60.0 / bpmVal; @@ -21504,7 +21550,7 @@ const App = () => { const durMs = effectiveDur * 1000; // Track VSTi + Carla local → note PHẢI play qua Carla bridge (chỉ route // Carla — không play FluidSynth GM sai âm chồng lên) - const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)); + const routeToCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)) && window.__carlaRunning !== false; if (!routeToCarla && window.SonicSF) { window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine); } @@ -21540,7 +21586,7 @@ const App = () => { var schedTime = startWallTime + effStart; var durMs = effDur * 1000; // Ghost note → Carla bridge khi ghost track VSTi (chỉ route Carla) - var gRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth)); + var gRouteCarla = !!(window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth)) && window.__carlaRunning !== false; if (!gRouteCarla && window.SonicSF) { window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth); } diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 1b70ff6..d4a73b4 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -20,7 +20,13 @@ const ensureSonicInstrument=ctx=>{try{if(!window.SonicSF||!window.SonicSF.select // track dùng VSTi + có Carla local. Dùng chung cho mọi đường playback // (main timeline, local loop, piano roll, ghost notes) để MIDI item PHẢI play // qua Carla bridge khi VSTi được loaded. -const scheduleCarlaNote=(synthEngine,channel,pitch,velocity,startWallTime,durMs)=>{try{if(!window.SonicCarlaMidi||!window.SonicCarlaMidi.shouldRoutePlayback(synthEngine))return;const ctx=getAudioContext();const delay=Math.max(0,(startWallTime-ctx.currentTime)*1000);const carlaVel=Math.round((velocity||0.8)*127);const carlaCh=synthEngine&&synthEngine.midi_channel!==undefined?synthEngine.midi_channel:channel||0;setTimeout(function(){window.SonicCarlaMidi.noteOn(carlaCh,pitch,carlaVel);},delay);setTimeout(function(){window.SonicCarlaMidi.noteOff(carlaCh,pitch);},delay+(durMs||300)+30);}catch(e){/* routing thất bại im lặng — soundfont vẫn chơi qua FluidSynth */}};// Handle ?sfs= from double-clicking a .sfs file (opens domain -> loads project) +const scheduleCarlaNote=(synthEngine,channel,pitch,velocity,startWallTime,durMs)=>{try{if(!window.SonicCarlaMidi||!window.SonicCarlaMidi.shouldRoutePlayback(synthEngine))return;const ctx=getAudioContext();const delay=Math.max(0,(startWallTime-ctx.currentTime)*1000);const carlaVel=Math.round((velocity||0.8)*127);const carlaCh=synthEngine&&synthEngine.midi_channel!==undefined?synthEngine.midi_channel:channel||0;setTimeout(function(){window.SonicCarlaMidi.noteOn(carlaCh,pitch,carlaVel);},delay);setTimeout(function(){window.SonicCarlaMidi.noteOff(carlaCh,pitch);},delay+(durMs||300)+30);}catch(e){/* routing thất bại im lặng — soundfont vẫn chơi qua FluidSynth */}};// ── Carla bridge alive tracking + auto-open ──────────────────────────────── +// window.__carlaRunning: undefined = chưa biết | true = đang chạy | false = đã chết. +// Quyết định route MIDI item EXCLUSIVE qua Carla hay fallback FluidSynth — +// tránh "câm toàn phần" khi Carla bị đóng (route chỉ-Carla mà Carla chết = im lặng). +const refreshCarlaStatus=()=>{try{if(!window.SonicAPI||!window.SonicAPI.carlaStatus)return;window.SonicAPI.carlaStatus().then(st=>{window.__carlaRunning=!!(st&&st.running);}).catch(()=>{window.__carlaRunning=false;});}catch(e){}};// Carla chưa chạy → TỰ ĐỘNG mở với VSTi của track (fire-and-forget) để MIDI +// item play qua Carla bridge đúng yêu cầu. +const ensureCarlaForPlayback=synthEngine=>{try{if(!window.SonicCarlaMidi||!window.SonicCarlaMidi.shouldRoutePlayback(synthEngine))return;if(!window.SonicAPI||!window.SonicAPI.carlaStatus)return;window.SonicAPI.carlaStatus().then(st=>{if(st&&st.running){window.__carlaRunning=true;return;}window.__carlaRunning=false;const pid=synthEngine&&synthEngine.plugin_id;if(pid){window.SonicAPI.openInCarla(pid).then(r=>{if(r&&r.success){window.__carlaRunning=true;}else{window.__carlaRunning=false;}}).catch(()=>{window.__carlaRunning=false;});}}).catch(()=>{window.__carlaRunning=false;});}catch(e){}};// Handle ?sfs= from double-clicking a .sfs file (opens domain -> loads project) (function handleSfsDeepLink(){try{const params=new URLSearchParams(window.location.search);const sfsParam=params.get('sfs');if(!sfsParam)return;const decoded=JSON.parse(decodeURIComponent(sfsParam));window.__pendingSfsProject=decoded;// consumed after auth in App if(window.history.replaceState){window.history.replaceState({},document.title,window.location.pathname);}}catch(e){window.__pendingSfsProject=null;}})();// Storage for server-side file IDs mapped to track IDs let serverFileIdMap={};let audioCtx;let masterBus=null;// { input, compressor, analyser, output, masteringActive, dryInput, dryOutput } @@ -521,8 +527,12 @@ 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 // 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). -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);}if(window.SonicSF){window.SonicSF.playNote(pitch,100,500,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(window.SonicSF){window.SonicSF.playNote(pitch,100,200,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(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);}if(window.SonicSF){// ⚠️ 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). +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(window.SonicSF){// 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 try{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(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) const[scaleRoot,setScaleRoot]=React.useState(0);const scaleRootRef=React.useRef(0);scaleRootRef.current=scaleRoot;// Modal states: Arpeggiator / Strummer / Humanize (spec 20:12) @@ -891,7 +901,8 @@ const[aboutModalOpen,setAboutModalOpen]=useState(false);const[helpModalOpen,setH useEffect(()=>{try{document.documentElement.setAttribute('data-theme',prefs.theme||'dark');document.documentElement.setAttribute('data-btnfont',prefs.buttonFontSize||'md');localStorage.setItem('sf_prefs',JSON.stringify(prefs));}catch(e){}},[prefs]);// Load preferences từ server (nếu có tài khoản) khi khởi động useEffect(()=>{let cancelled=false;if(window.SonicAPI&&window.SonicAPI.getPreferences){window.SonicAPI.getPreferences().then(res=>{if(cancelled||!res||!res.success||!res.preferences)return;const p=res.preferences;if(p.theme||p.language||p.buttonFontSize){setPrefs(prev=>({theme:p.theme||prev.theme,language:p.language||prev.language,buttonFontSize:p.buttonFontSize||prev.buttonFontSize}));}}).catch(()=>{});}return()=>{cancelled=true;};},[]);// Lưu preferences lên server khi đổi const handlePrefsChange=next=>{setPrefs(next);try{localStorage.setItem('sf_prefs',JSON.stringify(next));}catch(e){}if(window.SonicAPI&&window.SonicAPI.savePreferences){window.SonicAPI.savePreferences(next).catch(()=>{});}};// Unified FX Rack target context (unified_fx_rack_panel.md): { trackId, trackName } | null -const[fxRackTarget,setFxRackTarget]=useState(null);window.__openFxRack=(trackId,trackName)=>setFxRackTarget({trackId,trackName});const[masteringSettings,setMasteringSettings]=useState({masterConnected:false,activeModule:'eq',eqActive:true,imagerActive:true,maximizerActive:true,eqLowGain:1.5,eqMid1Gain:-1.0,eqMid2Gain:2.0,eqHighGain:1.8,w1:0,w2:115,w3:135,w4:150,imagerScale:'v2',maxGain:5.4,maxUpward:2.0,maxSoftClip:15,maxTransient:25,ceiling:-0.1,isBypassed:false,chain:DEFAULT_MASTER_CHAIN.map(m=>({...m})),compActive:false,compThreshold:-16,compRatio:3,compMakeup:0,limActive:false,limThreshold:-1.0,excActive:false,excDrive:40,rebalActive:false,rebalMid:0,rebalSide:0});useEffect(()=>{window.currentMasteringSettings=masteringSettings;if(audioCtx&&masterBus){toggleMasteringOnMaster(masteringSettings.masterConnected,masteringSettings.isBypassed);applyMasteringSettings(masteringSettings);// Re-sync live track routes: mastering ON → mọi track qua chain (♪ bị +const[fxRackTarget,setFxRackTarget]=useState(null);window.__openFxRack=(trackId,trackName)=>setFxRackTarget({trackId,trackName});const[masteringSettings,setMasteringSettings]=useState({masterConnected:true,// ⚠️ FIX: bật ON mặc định — MIDI item/preview PHẢI được xử lí qua mastering FX ngay khi chạy app (không cần bật/tắt power) +activeModule:'eq',eqActive:true,imagerActive:true,maximizerActive:true,eqLowGain:1.5,eqMid1Gain:-1.0,eqMid2Gain:2.0,eqHighGain:1.8,w1:0,w2:115,w3:135,w4:150,imagerScale:'v2',maxGain:5.4,maxUpward:2.0,maxSoftClip:15,maxTransient:25,ceiling:-0.1,isBypassed:false,chain:DEFAULT_MASTER_CHAIN.map(m=>({...m})),compActive:false,compThreshold:-16,compRatio:3,compMakeup:0,limActive:false,limThreshold:-1.0,excActive:false,excDrive:40,rebalActive:false,rebalMid:0,rebalSide:0});useEffect(()=>{window.currentMasteringSettings=masteringSettings;if(audioCtx&&masterBus){toggleMasteringOnMaster(masteringSettings.masterConnected,masteringSettings.isBypassed);applyMasteringSettings(masteringSettings);// Re-sync live track routes: mastering ON → mọi track qua chain (♪ bị // override bởi effMidiBypass/effAudioBypass) — nút PWR bật/tắt phải áp // ngay lên node đang phát (không chờ tracks effect). try{const _list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;_list.forEach(_t=>{const _n=activeTrackNodesRef.current[_t.id];if(_n){if(_n.sfRouteGain&&_n.sfDryGain){const _b=effMidiBypass(_t);_n.sfRouteGain.gain.value=_b?0:1;_n.sfDryGain.gain.value=_b?1:0;}if(_n.route&&_n.route.routeGain&&_n.route.dryGain){const _ab=routeBypassFor(_t);_n.route.routeGain.gain.value=_ab?0:1;_n.route.dryGain.gain.value=_ab?1:0;}}});updateSfRouting();}catch(e){}}},[masteringSettings]);const[pluginManagerModalOpen,setPluginManagerModalOpen]=useState(false);const[pluginsData,setPluginsData]=useState(null);const loadAudioBuffersForTracks=async tracksList=>{let hasLoadedAny=false;const loadBuffer=async url=>{const res=await fetch(url);if(!res.ok)return null;const blob=await res.blob();return await window.SonicAudio.decodeAudioFile(blob);};const tryLoad=async fileId=>{if(!fileId)return null;try{const result=await loadBuffer('/static/audio/uploads/'+fileId);if(result)return result;}catch(_){}try{const result=await loadBuffer(`${API_AUDIO}/download/${fileId}`);if(result)return result;}catch(_){}return null;};const updatedTracks=await Promise.all(tracksList.map(async t=>{let trackBuffer=t.buffer;let trackChannelInfo=t.channelInfo;if(t.serverFileId&&!trackBuffer){const result=await tryLoad(t.serverFileId);if(result){trackBuffer=result.audioBuffer;trackChannelInfo=result.channelInfo;hasLoadedAny=true;}}const updatedClips=await Promise.all((t.clips||[]).map(async c=>{let clipBuffer=c.buffer;const targetFileId=c.serverFileId||t.serverFileId;if(targetFileId&&!clipBuffer){const result=await tryLoad(targetFileId);if(result){clipBuffer=result.audioBuffer;hasLoadedAny=true;}}return{...c,buffer:clipBuffer};}));// Section items: nạp CẢ audioclip bên trong section (trước đây bỏ sót → @@ -1329,7 +1340,9 @@ const subAnalyser=context.createAnalyser();subAnalyser.fftSize=256;subAnalyser.s // parent có route riêng với bypass riêng — parent audio_bypass=true → // nối qua parent làm MIDI/audio section bị kéo vào dry bus = bypass // 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;window.SonicSF.playNote(pitch,velocity,durationMs,context.currentTime,program,destNode,prevCh,track?track.synth_engine:undefined);};const startTrackPlayback=offsetTime=>{const context=getAudioContext();const allPlayTracks=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:activeTracks;// Capture items signature at schedule time — updatePlayhead so sánh với +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;window.SonicSF.playNote(pitch,velocity,durationMs,context.currentTime,program,destNode,prevCh,track?track.synth_engine:undefined);};const startTrackPlayback=offsetTime=>{const context=getAudioContext();// ⚠️ 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){}refreshCarlaStatus();const allPlayTracks=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:activeTracks;// Capture items signature at schedule time — updatePlayhead so sánh với // signature này để phát hiện items đổi vị trí giữa lúc play (re-schedule). scheduledItemsSigRef.current=buildItemsSignature(allPlayTracks,sessionTabs);console.log('[Play] offset='+offsetTime+' tracks='+allPlayTracks.length+' masterBus='+!!masterBus+' dest='+(context.destination?'ok':'MISSING')+' projectEnd='+(projectEndRef.current||0).toFixed(2));const hasSolo=allPlayTracks.some(t=>t.solo);allPlayTracks.forEach(track=>{const isPlayable=hasSolo?track.solo:!track.muted;if(!isPlayable)return;const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackNodesRef.current[track.id].pannerNode;// Re-apply route gains theo content-type NGAY tại thời điểm play — node // có thể STALE (tạo từ MAIN play với track cùng id — route theo data cũ) @@ -1337,13 +1350,13 @@ scheduledItemsSigRef.current=buildItemsSignature(allPlayTracks,sessionTabs);cons // routeBypassFor(track) mỗi lần schedule sẽ hết chu kỳ "stop+play mới đúng". const playNode=activeTrackNodesRef.current[track.id];if(playNode&&playNode.route&&playNode.route.routeGain){const rb=routeBypassFor(track);playNode.route.routeGain.gain.value=rb?0:1;playNode.route.dryGain.gain.value=rb?1:0;console.log('[Bypass] reapply track',track.id,'contentType='+(routeBypassFor._lastType?'midi':'audio'),'bypass='+rb,'→ routeGain='+playNode.route.routeGain.gain.value+'/dryGain='+playNode.route.dryGain.gain.value);}console.log('[Play] track',track.id,track.name,'node ok:',!!(gainNode&&pannerNode),'muted:',track.muted);const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];clips.forEach(clip=>{if(!clip.buffer)return;const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(gainNode);const clipStart=clip.startTime||0;const clipDuration=clip.buffer.duration/(clip.speed||1.0);const clipEnd=clipStart+clipDuration;if(offsetTime0)&&window.SonicSF){// Preview cache: capture the soundfont (pre track-FX) for fast offline export -ensureMidiCapture(track,activeTrackNodesRef.current[track.id]);var trkCh=assignTrackMidiChannel(track,allPlayTracks);// Ensure instrument is loaded in FluidSynth +ensureMidiCapture(track,activeTrackNodesRef.current[track.id]);if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(track.synth_engine))ensureCarlaForPlayback(track.synth_engine);var trkCh=assignTrackMidiChannel(track,allPlayTracks);// Ensure instrument is loaded in FluidSynth if(track.synth_engine&&track.synth_engine.type==='soundfont'&&track.synth_engine.soundfont_id){window.SonicSF.selectInstrument(trkCh,track.synth_engine.soundfont_bank||0,track.synth_engine.soundfont_program||0,track.synth_engine.soundfont_id);}const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;midiItems.forEach(item=>{const itemEndSec=item.startTime+(item.duration||4);const notes=item.notes||[];notes.forEach(note=>{// note start/duration is in beats (for MIDI items) const noteStartSec=item.startTime+(note.start_beat||0)*secondsPerBeat;if(noteStartSec>=itemEndSec)return;// skip notes outside item's visual duration const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteEndSec=noteStartSec+noteDurSec;if(offsetTime{// ⚠️ Guard: stop → setTimeout sót không được fire (VU nhảy @@ -1364,7 +1377,7 @@ const activeStart=Math.max(clipStartMain,secStart);const activeEnd=Math.min(clip const subMidiItems=subTrack.midiItems||[];if(window.SonicSF&&subMidiItems.length>0){subMidiItems.forEach(item=>{const notes=item.notes||[];notes.forEach(note=>{const noteStartSecLocal=item.startTime+(note.start_beat||0)*secondsPerBeat;const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteStartMain=secStart+noteStartSecLocal;const noteEndMain=noteStartMain+noteDurSec;// Only play if the note starts inside the Section item bounds and hasn't finished yet if(noteStartMain>=secStart&¬eStartMain{// ⚠️ Guard: stop → setTimeout sót không được fire ( if(!isPlayingRef.current)return;if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — 07:20) window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}},delay*1000);}else{const remainingDurMs=(notePlayEndMain-offsetTime)*1000;if(!subRouteCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,subNode,subCh,subTrack.synth_engine);}if(subRouteCarla)scheduleCarlaNote(subTrack.synth_engine,subCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — 07:20) window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}}}});});}});});}});updateSfRouting();};// Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2) -const startLocalTrackPlayback=(trackId,offsetTime)=>{const context=getAudioContext();const track=tracks.find(t=>t.id===trackId);if(!track)return;const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];// Get or create persistent gain & panner for real-time control +const startLocalTrackPlayback=(trackId,offsetTime)=>{const context=getAudioContext();try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(e){}refreshCarlaStatus();const track=tracks.find(t=>t.id===trackId);if(!track)return;const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];// Get or create persistent gain & panner for real-time control const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackNodesRef.current[track.id].pannerNode;clips.forEach(clip=>{if(!clip.buffer)return;const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(gainNode);// NOTE: do NOT connect gainNode → pannerNode directly here — the track // node already wires gainNode → fxEntry → [FX modules] → fxLegacyIn → // chorus/reverb → pannerNode. A direct tap would BYPASS the FX Rack @@ -1382,9 +1395,9 @@ const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackN const clipStart=clip.startTime||0;const clipDuration=clip.buffer.duration/(clip.speed||1.0);const clipEnd=clipStart+clipDuration;if(offsetTime0)&&window.SonicSF){var lcCh=assignTrackMidiChannel(track,tracks);const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;midiItems.forEach(item=>{const notes=item.notes||[];notes.forEach(note=>{const noteStartSec=item.startTime+(note.start_beat||0)*secondsPerBeat;const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteEndSec=noteStartSec+noteDurSec;if(offsetTime{if(st.type!=='PIANO_ROLL')return;const context=getAudioContext();const midiNotes=notesOverride||st.notes||[];const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;const startWallTime=context.currentTime;const track=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===st.trackId):null;if(!track){console.warn('[Play] Piano Roll track not found:',st.trackId);return;}const destNode=getOrCreateTrackNode(track,context);// Instrument context resolve từ TRACK live (nguồn duy nhất) — track "đã +if(routeToCarla)scheduleCarlaNote(track.synth_engine,lcCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);}}});});}};const schedulePianoRollMidi=(st,offsetSeconds,notesOverride)=>{if(st.type!=='PIANO_ROLL')return;const context=getAudioContext();try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(e){}refreshCarlaStatus();const midiNotes=notesOverride||st.notes||[];const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;const startWallTime=context.currentTime;const track=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===st.trackId):null;if(!track){console.warn('[Play] Piano Roll track not found:',st.trackId);return;}const destNode=getOrCreateTrackNode(track,context);// Instrument context resolve từ TRACK live (nguồn duy nhất) — track "đã // loaded instrument" (instrumentProgram GM hoặc synth_engine soundfont) // phải chơi ĐÚNG instrument đó. ensureSonicInstrument select channel đúng // trước khi notes bắn (fire-and-forget — playNote tự load+retry nếu SF @@ -1395,11 +1408,11 @@ const instCtx=resolveTrackInstrumentCtx(track,activeTracksRef.current||[]);const // the item window, so no absolute-session offset is applied anywhere here. midiNotes.forEach(note=>{const noteOnBeat=note.start_beat||0;const noteDurBeat=note.duration_beats||1;const noteStartSec=noteOnBeat*secondsPerBeat;const noteDurSec=noteDurBeat*secondsPerBeat;if(noteStartSec+noteDurSec>offsetSeconds){const effectiveStart=Math.max(0,noteStartSec-offsetSeconds);const effectiveDur=noteDurSec-Math.max(0,offsetSeconds-noteStartSec);const scheduledTime=startWallTime+effectiveStart;const durMs=effectiveDur*1000;// Track VSTi + Carla local → note PHẢI play qua Carla bridge (chỉ route // 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);}// MIDI items → Carla (track VSTi + Carla local): phát VSTi realtime +const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(synthEngine))&&window.__carlaRunning!==false;if(!routeToCarla&&window.SonicSF){window.SonicSF.playNote(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). if(routeToCarla){const carlaDelay=Math.max(0,(scheduledTime-context.currentTime)*1000);const carlaVel=Math.round((note.velocity||0.8)*127);const carlaCh=synthEngine&&synthEngine.midi_channel!==undefined?synthEngine.midi_channel:mainCh;const carlaPitch=note.pitch||60;setTimeout(function(){window.SonicCarlaMidi.noteOn(carlaCh,carlaPitch,carlaVel);},carlaDelay);setTimeout(function(){window.SonicCarlaMidi.noteOff(carlaCh,carlaPitch);},carlaDelay+durMs+30);}}});// Play ghost notes from active tracks var ghostLayers=st.ghostPlayLayers||[];ghostLayers.forEach(function(layer){var ghostTrack=allTracks.find(function(t){return t.id===layer.trackId;});var ghostProg=layer.instrumentProgram!==undefined?layer.instrumentProgram:ghostTrack?ghostTrack.instrumentProgram:undefined;var ghostSynth=layer.synthEngine||(ghostTrack?ghostTrack.synth_engine:undefined);if(ghostProg===undefined&&!ghostSynth)return;var ghostDest=getOrCreateTrackNode(ghostTrack,context);var ghostCh=ghostTrack?assignTrackMidiChannel(ghostTrack,allTracks):0;layer.notes.forEach(function(note){var beat=note.start_beat||0;var dur=note.duration_beats||1;var startSec=beat*secondsPerBeat;var durSec=dur*secondsPerBeat;if(startSec+durSec>offsetSeconds){var effStart=Math.max(0,startSec-offsetSeconds);var effDur=durSec-Math.max(0,offsetSeconds-startSec);var schedTime=startWallTime+effStart;var durMs=effDur*1000;// 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);}if(gRouteCarla)scheduleCarlaNote(ghostSynth,ghostCh,note.pitch||60,note.velocity||0.8,schedTime,durMs);}});});};const handlePlayPause=()=>{console.log('[Play] click activeTab=',activeTab,'isPlaying=',isPlaying,'subPlaying=',subTabs.filter(s=>s.isPlaying).length);if(activeTab!=='main'&&!activeTab.startsWith('session_')){// Sub-tab playback transport +var gRouteCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(ghostSynth))&&window.__carlaRunning!==false;if(!gRouteCarla&&window.SonicSF){window.SonicSF.playNote(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);}});});};const handlePlayPause=()=>{console.log('[Play] click activeTab=',activeTab,'isPlaying=',isPlaying,'subPlaying=',subTabs.filter(s=>s.isPlaying).length);if(activeTab!=='main'&&!activeTab.startsWith('session_')){// Sub-tab playback transport const st=subTabs.find(s=>s.id===activeTab);if(!st||!st.buffer&&st.type!=='PIANO_ROLL')return;// MAIN/SECTION đang play (ngầm — MIDI section/main còn kêu sau khi mở // tab): Space phải STOP trước — không được play sub-tab (user: trong // PIANO ROLL không phát hiện MIDI đang play → space 2 lần mới stop). diff --git a/app/static/js/services/api.js b/app/static/js/services/api.js index c288039..351f346 100644 --- a/app/static/js/services/api.js +++ b/app/static/js/services/api.js @@ -79,6 +79,9 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin; // Unload Carla bridge: tắt mọi note đang ngân + terminate tiến trình Carla // do app spawn (gọi khi track chuyển từ VSTi sang instrument soundfont) stopCarla: () => apiRequest('/api/v1/plugins/carla-stop', { method: 'POST', body: JSON.stringify({}) }), + // Kiểm tra Carla còn sống không (app-spawn) + cổng OSC — quyết định + // route MIDI item exclusive qua Carla hay fallback FluidSynth + carlaStatus: () => apiRequest('/api/v1/plugins/carla-status', { method: 'GET' }), // Gửi MIDI note (track ARM → Carla OSC) để preview VSTi realtime carlaMidi: (payload) => apiRequest('/api/v1/plugins/carla-midi', { method: 'POST', body: JSON.stringify(payload) }), // Quick-render preview VSTi (âm thật = âm export, cùng code path) diff --git a/app/static/js/services/soundfontPlayer.js b/app/static/js/services/soundfontPlayer.js index 1c7e967..19ec458 100644 --- a/app/static/js/services/soundfontPlayer.js +++ b/app/static/js/services/soundfontPlayer.js @@ -25,6 +25,11 @@ // note khi thả phím (trước đây stopNote không dừng oscillator → âm kêu // liên tục dù đã thả phím khi FluidSynth chưa sẵn sàng / font load fail). let _activeOscillatorsByKey = {}; + // Epoch chống cache channel STALE: tăng mỗi khi selectInstrument/load + // soundfont — channel cache chỉ được tin khi khớp epoch hiện tại, nếu + // không → note KẾ TIẾP luôn program_select lại (hết "chọn Synth String + // nghe Piano" do cache cũ skip program_select). + let _instrumentEpoch = 0; let _gainNode = null; let _pendingOutputDestination = null; let _outputDestination = null; // cache đích route — dedupe swap dư giữa stream @@ -332,6 +337,7 @@ _sfHandleMap.set(sfId, cachedOk); _currentSfId = sfId; _loadedFonts[sfId] = true; + _instrumentEpoch++; console.log("[SonicSF] SoundFont loaded from cache:", sfId, "handle:", cachedOk); return true; } @@ -367,6 +373,7 @@ _sfHandleMap.set(sfId, sfHandle); _currentSfId = sfId; _loadedFonts[sfId] = true; + _instrumentEpoch++; console.log("[SonicSF] SoundFont loaded:", sfId, "handle:", sfHandle); return true; } catch (e) { @@ -396,9 +403,17 @@ _engineChMap[engKey] = channel; } var sfHandle = _sfHandleMap.get(sfId); + // ⚠️ FIX âm sai instrument: tăng epoch → channel cache cũ thành + // stale → note kế tiếp LUÔN program_select lại (hết "chọn Synth + // String nghe Piano" do cache cũ skip). + _instrumentEpoch++; if (sfHandle !== undefined) { try { - _fluidModule._fluid_synth_program_select(_synthPtr, channel, sfHandle, bank, program); + var _sr = _fluidModule._fluid_synth_program_select(_synthPtr, channel, sfHandle, bank, program); + if (_sr !== 0) { + // Preset không tồn tại ở bank/program này → thử bank 0 + try { _fluidModule._fluid_synth_program_select(_synthPtr, channel, sfHandle, 0, program); } catch (e2) {} + } } catch (e) {} } else { try { _fluidModule._fluid_synth_bank_select(_synthPtr, channel, bank); } catch (e) {} @@ -408,6 +423,7 @@ _channels[channel].program = program; _channels[channel].isPercussion = (bank === 128); _channels[channel].sfId = sfId; + _channels[channel]._epoch = _instrumentEpoch; }, controllerChange: function (channel, controller, value) { @@ -632,6 +648,7 @@ // ⚠️ Chỉ skip khi handle SF vẫn CÒN HỢP LỆ trong map — nếu // không → vẫn program_select lại (tránh dùng handle đã unload). var progAlreadySet = cachedCh && cachedCh.program === finalProg && cachedCh.bank === finalBank && cachedCh.sfId === finalSfId + && cachedCh._epoch === _instrumentEpoch && (finalSfId ? _sfHandleMap.has(finalSfId) : true); if ((synthEngine || program !== undefined) && !progAlreadySet) { var sfHandle = finalSfId ? _sfHandleMap.get(finalSfId) : undefined; @@ -666,6 +683,11 @@ } } var _selRet = _fluidModule._fluid_synth_program_select(_synthPtr, ch, sfHandle, finalBank, finalProg); + if (_selRet !== 0 && finalBank !== 0) { + // Preset không tồn tại ở bank này → thử bank 0 + // (hết "noteon preset rỗng = âm sai/không đúng") + try { _fluidModule._fluid_synth_program_select(_synthPtr, ch, sfHandle, 0, finalProg); } catch (e2) {} + } } catch (e) {} } else { try { _fluidModule._fluid_synth_bank_select(_synthPtr, ch, finalBank); } catch (e) {} @@ -675,6 +697,7 @@ _channels[ch].bank = finalBank; _channels[ch].program = finalProg; _channels[ch].sfId = finalSfId; + _channels[ch]._epoch = _instrumentEpoch; } console.log('[SonicSF] noteon channel:', ch, 'pitch:', midiPitch, 'vel:', midiVel, 'sfId:', finalSfId); _fluidModule._fluid_synth_noteon(_synthPtr, ch, midiPitch, midiVel); diff --git a/app/templates/index.html b/app/templates/index.html index ca15cd9..5a803f4 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -31,12 +31,12 @@ - - + + - + @@ -44,7 +44,7 @@ - +