From f492104869922b573dac0a65c20b0c5ea5ff5d25 Mon Sep 17 00:00:00 2001 From: locpham Date: Mon, 10 Aug 2026 09:07:42 +0000 Subject: [PATCH] =?UTF-8?q?FIX:=20(1)=20MIDI=20item=20route=20CH=E1=BB=88?= =?UTF-8?q?=20qua=20Carla=20bridge=20khi=20VSTi=20loaded=20(b=E1=BB=8F=20F?= =?UTF-8?q?luidSynth=20GM=20sai=20=C3=A2m)=20=E1=BB=9F=20m=E1=BB=8Di=20?= =?UTF-8?q?=C4=91=C6=B0=E1=BB=9Dng=20playback=20+=20ghosts;=20(2)=20update?= =?UTF-8?q?SfRouting=20sync=20route=20gains=20nh=C3=A1nh=20gainNode=20+=20?= =?UTF-8?q?ensureMasteringRouting=20force-sync=20m=E1=BB=8Di=20node=20?= =?UTF-8?q?=E2=80=94=20preview=20MIDI=20keyboard=20qua=20mastering=20kh?= =?UTF-8?q?=C3=B4ng=20c=E1=BA=A7n=20b=E1=BA=ADt/t=E1=BA=AFt=20power;=20(3)?= =?UTF-8?q?=20stopNote/panic=20d=E1=BB=ABng=20fallback=20oscillator=20theo?= =?UTF-8?q?=20channel:pitch=20=E2=80=94=20h=E1=BA=BFt=20=C3=A2m=20k=C3=AAu?= =?UTF-8?q?=20li=C3=AAn=20t=E1=BB=A5c=20sau=20khi=20th=E1=BA=A3=20ph=C3=AD?= =?UTF-8?q?m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 204 ++++++++++++++-------- app/static/js/app.precompiled.js | 51 ++++-- app/static/js/services/soundfontPlayer.js | 48 ++++- app/templates/index.html | 8 +- wiki.md | 8 + 5 files changed, 229 insertions(+), 90 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 15b64f1..0763db4 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -20805,6 +20805,16 @@ const App = () => { } if (_prNode.gainNode) { window.SonicSF.setOutputDestination(_prNode.gainNode); + // ⚠️ FIX: sync route gains NGAY tại thời điểm routing — trước đây + // bỏ qua → node giữ bypass cũ (tạo khi mastering OFF / audioBypass + // true) → preview âm soundfont KHÔNG qua mastering cho tới khi bật/ + // tắt power (effect re-sync). MIDI preview route theo ♪ (midiBypass). + if (_prNode.route && _prNode.route.routeGain && _prNode.route.dryGain) { + const _prTrk2 = list.find(t => t.id === _activeSub.trackId); + const _b2 = effMidiBypass(_prTrk2 || { id: _activeSub.trackId }); + _prNode.route.routeGain.gain.value = _b2 ? 0 : 1; + _prNode.route.dryGain.gain.value = _b2 ? 1 : 0; + } return; } } @@ -20845,6 +20855,13 @@ const App = () => { if (node && node.gainNode && window.SonicSF && window.SonicSF.setOutputDestination) { console.log('[SFRoute] dest=gainNode node', t.id, '(no sfEntry) — route.routeGain=' + (node.route ? node.route.routeGain.gain.value : 'MISSING'), 'route.dryGain=' + (node.route ? node.route.dryGain.gain.value : 'MISSING')); window.SonicSF.setOutputDestination(node.gainNode); + // ⚠️ FIX: sync route gains — node giữ bypass cũ → preview không qua + // mastering cho tới khi bật/tắt power. MIDI preview theo ♪. + if (node.route && node.route.routeGain && node.route.dryGain) { + const _b3 = effMidiBypass(t); + node.route.routeGain.gain.value = _b3 ? 0 : 1; + node.route.dryGain.gain.value = _b3 ? 1 : 0; + } return; } } @@ -20875,6 +20892,27 @@ const App = () => { toggleMasteringOnMaster(!!_s.masterConnected, !!_s.isBypassed); applyMasteringSettings(_s); updateSfRouting(); + // Force-sync MỌI node đang tồn tại (route + sf route) theo trạng thái + // hiện tại — phòng node tạo từ trước với bypass cũ (mastering OFF lúc + // tạo node / audioBypass default true của section) → preview LUÔN qua + // mastering khi chain bật, không cần bật/tắt power. + try { + const _list2 = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks; + _list2.forEach(_t => { + const _n = activeTrackNodesRef.current[_t.id]; + if (!_n) return; + if (_n.sfRouteGain && _n.sfDryGain) { + const _mb = effMidiBypass(_t); + _n.sfRouteGain.gain.value = _mb ? 0 : 1; + _n.sfDryGain.gain.value = _mb ? 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; + } + }); + } catch (e2) {} } catch (e) { console.warn('ensureMasteringRouting error:', e); } @@ -21115,22 +21153,28 @@ const App = () => { if (offsetTime < noteEndSec) { const durationMs = noteDurSec * 1000; const program = track._isSectionClone ? track.instrumentProgram : (track.instrumentProgram !== undefined ? track.instrumentProgram : 0); + // 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)); if (offsetTime < noteStartSec) { const delay = noteStartSec - offsetTime; const startTime = context.currentTime + delay; - window.SonicSF.playNote( - note.pitch || 60, - note.velocity || 0.8, - durationMs, - startTime, - program, - gainNode, - trkCh, - track.synth_engine - ); + if (!routeToCarla && window.SonicSF) { + window.SonicSF.playNote( + note.pitch || 60, + note.velocity || 0.8, + durationMs, + startTime, + program, + gainNode, + trkCh, + track.synth_engine + ); + } // MIDI items → Carla bridge (track VSTi + Carla local): phát VSTi // realtime — yêu cầu: MIDI item PHẢI play qua Carla khi VSTi loaded. - scheduleCarlaNote(track.synth_engine, trkCh, note.pitch || 60, note.velocity || 0.8, startTime, durationMs); + if (routeToCarla) scheduleCarlaNote(track.synth_engine, trkCh, note.pitch || 60, note.velocity || 0.8, startTime, durationMs); // Trigger VU meter flash when the note starts playing setTimeout(() => { // ⚠️ Guard: stop → setTimeout sót không được fire (VU nhảy @@ -21144,18 +21188,20 @@ const App = () => { } else { const playOffset = offsetTime - noteStartSec; const remainingDurMs = (noteEndSec - offsetTime) * 1000; - window.SonicSF.playNote( - note.pitch || 60, - note.velocity || 0.8, - remainingDurMs, - context.currentTime, - program, - gainNode, - trkCh, - track.synth_engine - ); + if (!routeToCarla && window.SonicSF) { + window.SonicSF.playNote( + note.pitch || 60, + note.velocity || 0.8, + remainingDurMs, + context.currentTime, + program, + gainNode, + trkCh, + track.synth_engine + ); + } // MIDI items → Carla bridge (track VSTi + Carla local) - scheduleCarlaNote(track.synth_engine, trkCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs); + if (routeToCarla) scheduleCarlaNote(track.synth_engine, trkCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs); // Trigger VU meter flash instantly if (window.triggerMidiVuActivity) { window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + track.id, note.velocity || 0.8); @@ -21255,21 +21301,27 @@ const App = () => { const notePlayEndMain = Math.min(noteEndMain, secEnd); const program = subTrack.instrumentProgram; 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)); if (offsetTime < noteStartMain) { const delay = noteStartMain - offsetTime; const startTime = context.currentTime + delay; const playDurMs = (notePlayEndMain - noteStartMain) * 1000; - window.SonicSF.playNote( - note.pitch || 60, - note.velocity || 0.8, - playDurMs, - startTime, - program, - subNode, - subCh, - subTrack.synth_engine - ); + if (!subRouteCarla && window.SonicSF) { + window.SonicSF.playNote( + note.pitch || 60, + note.velocity || 0.8, + playDurMs, + startTime, + program, + subNode, + subCh, + subTrack.synth_engine + ); + } + if (subRouteCarla) scheduleCarlaNote(subTrack.synth_engine, subCh, note.pitch || 60, note.velocity || 0.8, startTime, playDurMs); // Trigger VU meter flash đúng nhịp (cả subTrack — VU // section tab + parent track — VU MAIN): MIDI section đi // SF → masterBus.input (không qua sub-node analyser) → @@ -21287,16 +21339,19 @@ const App = () => { }, delay * 1000); } else { const remainingDurMs = (notePlayEndMain - offsetTime) * 1000; - window.SonicSF.playNote( - note.pitch || 60, - note.velocity || 0.8, - remainingDurMs, - context.currentTime, - program, - subNode, - subCh, - subTrack.synth_engine - ); + 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); @@ -21369,35 +21424,42 @@ const App = () => { if (offsetTime < noteEndSec) { const durationMs = noteDurSec * 1000; 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)); if (offsetTime < noteStartSec) { const delay = noteStartSec - offsetTime; const startTime = context.currentTime + delay; - window.SonicSF.playNote( - note.pitch || 60, - note.velocity || 0.8, - durationMs, - startTime, - program, - gainNode, - lcCh, - track.synth_engine - ); + if (!routeToCarla && window.SonicSF) { + window.SonicSF.playNote( + note.pitch || 60, + note.velocity || 0.8, + durationMs, + startTime, + program, + gainNode, + lcCh, + track.synth_engine + ); + } // MIDI items → Carla bridge (track VSTi + Carla local) - scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, startTime, durationMs); + if (routeToCarla) scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, startTime, durationMs); } else { const remainingDurMs = (noteEndSec - offsetTime) * 1000; - window.SonicSF.playNote( - note.pitch || 60, - note.velocity || 0.8, - remainingDurMs, - context.currentTime, - program, - gainNode, - lcCh, - track.synth_engine - ); + if (!routeToCarla && window.SonicSF) { + window.SonicSF.playNote( + note.pitch || 60, + note.velocity || 0.8, + remainingDurMs, + context.currentTime, + program, + gainNode, + lcCh, + track.synth_engine + ); + } // MIDI items → Carla bridge (track VSTi + Carla local) - scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs); + if (routeToCarla) scheduleCarlaNote(track.synth_engine, lcCh, note.pitch || 60, note.velocity || 0.8, context.currentTime, remainingDurMs); } } }); @@ -21440,12 +21502,15 @@ const App = () => { const effectiveDur = noteDurSec - Math.max(0, offsetSeconds - noteStartSec); const scheduledTime = startWallTime + effectiveStart; const durMs = effectiveDur * 1000; - if (window.SonicSF) { + // 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 // (schedule theo audio clock bằng setTimeout — preview, timing gần đúng). - if (window.SonicCarlaMidi && window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)) { + 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; @@ -21474,11 +21539,12 @@ const App = () => { var effDur = durSec - Math.max(0, offsetSeconds - startSec); var schedTime = startWallTime + effStart; var durMs = effDur * 1000; - if (window.SonicSF) { + // 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); } - // Ghost notes → Carla bridge (ghost track VSTi + Carla local) - scheduleCarlaNote(ghostSynth, ghostCh, note.pitch || 60, note.velocity || 0.8, schedTime, durMs); + if (gRouteCarla) scheduleCarlaNote(ghostSynth, ghostCh, note.pitch || 60, note.velocity || 0.8, schedTime, durMs); } }); }); diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 889012e..1b70ff6 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -1269,7 +1269,11 @@ if(_activeSub&&_activeSub.type==='PIANO_ROLL'&&!isPlayingRef.current){// PIANO R // không có sub-node → null (masterBus.input — qua mastering). let _prNode=activeTrackNodesRef.current[_activeSub.trackId];if(_activeSub.parent_tab_id&&_activeSub.parent_tab_id.startsWith('session_')){const _prSubKeys=Object.keys(activeTrackNodesRef.current).filter(k=>k.endsWith('_sub_'+_activeSub.trackId));_prNode=_prSubKeys.length?activeTrackNodesRef.current[_prSubKeys[0]]:null;}if(_prNode&&window.SonicSF&&window.SonicSF.setOutputDestination){if(_prNode.sfEntry){window.SonicSF.setOutputDestination(_prNode.sfEntry);// Ép route qua mastering chain NGAY tại thời điểm routing (node có // thể tạo khi mastering OFF → route dry — sửa ngay nếu chain ON). -if(_prNode.sfRouteGain&&_prNode.sfDryGain){const _prTrk=list.find(t=>t.id===_activeSub.trackId);const _b=effMidiBypass(_prTrk||{id:_activeSub.trackId});_prNode.sfRouteGain.gain.value=_b?0:1;_prNode.sfDryGain.gain.value=_b?1:0;}return;}if(_prNode.gainNode){window.SonicSF.setOutputDestination(_prNode.gainNode);return;}}}const midiAudible=list.filter(t=>t.midiItems&&t.midiItems.length>0&&computeTrackAudibleGain(list,t)>0);console.log('[SFRoute] activeTab='+_activeTabId,'midiAudible='+midiAudible.map(t=>t.id+'(midi'+(t.midiItems||[]).length+')').join(',')||'none');if(midiAudible.length===1){const t=midiAudible[0];// SECTION-TAB: track id của section clone TRÙNG main track id → node +if(_prNode.sfRouteGain&&_prNode.sfDryGain){const _prTrk=list.find(t=>t.id===_activeSub.trackId);const _b=effMidiBypass(_prTrk||{id:_activeSub.trackId});_prNode.sfRouteGain.gain.value=_b?0:1;_prNode.sfDryGain.gain.value=_b?1:0;}return;}if(_prNode.gainNode){window.SonicSF.setOutputDestination(_prNode.gainNode);// ⚠️ FIX: sync route gains NGAY tại thời điểm routing — trước đây +// bỏ qua → node giữ bypass cũ (tạo khi mastering OFF / audioBypass +// true) → preview âm soundfont KHÔNG qua mastering cho tới khi bật/ +// tắt power (effect re-sync). MIDI preview route theo ♪ (midiBypass). +if(_prNode.route&&_prNode.route.routeGain&&_prNode.route.dryGain){const _prTrk2=list.find(t=>t.id===_activeSub.trackId);const _b2=effMidiBypass(_prTrk2||{id:_activeSub.trackId});_prNode.route.routeGain.gain.value=_b2?0:1;_prNode.route.dryGain.gain.value=_b2?1:0;}return;}}}const midiAudible=list.filter(t=>t.midiItems&&t.midiItems.length>0&&computeTrackAudibleGain(list,t)>0);console.log('[SFRoute] activeTab='+_activeTabId,'midiAudible='+midiAudible.map(t=>t.id+'(midi'+(t.midiItems||[]).length+')').join(',')||'none');if(midiAudible.length===1){const t=midiAudible[0];// SECTION-TAB: track id của section clone TRÙNG main track id → node // chính (key = id) là node MAIN (route theo ♪ main — SAI context, MIDI // section bị kéo vào dry). ƯU TIÊN SUB-NODE (key _sub_ — // route theo ♪ section + thẳng masterBus.input) — không có sub-node → @@ -1282,7 +1286,9 @@ if(node.sfRouteGain&&node.sfDryGain){// ⚠️ FIX: truyền track OBJECT (t) // → effMidiBypass đọc track.id=undefined → map lookup sai → _b luôn // false → sfRouteGain bị ép 1 (không bypass) — nút ♪ vô hiệu qua // đường này. -const _b=effMidiBypass(t);node.sfRouteGain.gain.value=_b?0:1;node.sfDryGain.gain.value=_b?1:0;console.log('[Bypass] sf route sync track',t.id,'midiBypass='+_b,_b?'DRY':'MASTERING');}return;}if(node&&node.gainNode&&window.SonicSF&&window.SonicSF.setOutputDestination){console.log('[SFRoute] dest=gainNode node',t.id,'(no sfEntry) — route.routeGain='+(node.route?node.route.routeGain.gain.value:'MISSING'),'route.dryGain='+(node.route?node.route.dryGain.gain.value:'MISSING'));window.SonicSF.setOutputDestination(node.gainNode);return;}}if(window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(null);}}catch(e){console.warn('updateSfRouting error:',e);}};const updateSfRoutingRef=useRef(null);// ── Đồng bộ mastering + routing NGAY TRƯỚC KHI PREVIEW (keybed/draw/MIDI file) ── +const _b=effMidiBypass(t);node.sfRouteGain.gain.value=_b?0:1;node.sfDryGain.gain.value=_b?1:0;console.log('[Bypass] sf route sync track',t.id,'midiBypass='+_b,_b?'DRY':'MASTERING');}return;}if(node&&node.gainNode&&window.SonicSF&&window.SonicSF.setOutputDestination){console.log('[SFRoute] dest=gainNode node',t.id,'(no sfEntry) — route.routeGain='+(node.route?node.route.routeGain.gain.value:'MISSING'),'route.dryGain='+(node.route?node.route.dryGain.gain.value:'MISSING'));window.SonicSF.setOutputDestination(node.gainNode);// ⚠️ FIX: sync route gains — node giữ bypass cũ → preview không qua +// mastering cho tới khi bật/tắt power. MIDI preview theo ♪. +if(node.route&&node.route.routeGain&&node.route.dryGain){const _b3=effMidiBypass(t);node.route.routeGain.gain.value=_b3?0:1;node.route.dryGain.gain.value=_b3?1:0;}return;}}if(window.SonicSF&&window.SonicSF.setOutputDestination){window.SonicSF.setOutputDestination(null);}}catch(e){console.warn('updateSfRouting error:',e);}};const updateSfRoutingRef=useRef(null);// ── Đồng bộ mastering + routing NGAY TRƯỚC KHI PREVIEW (keybed/draw/MIDI file) ── // Bug: preview instrument soundfont với MIDI keyboard KHÔNG tự động đi qua // mastering FX của main out — phải bật/tắt nút power mới có tác dụng. Nguyên // nhân: routing SF (setOutputDestination + sfRouteGain/sfDryGain) và graph @@ -1292,7 +1298,11 @@ const _b=effMidiBypass(t);node.sfRouteGain.gain.value=_b?0:1;node.sfDryGain.gain // gains) — gọi tại mọi điểm preview → âm preview LUÔN qua mastering khi bật. const ensureMasteringRouting=()=>{try{const _s=window.currentMasteringSettings;if(!_s)return;// Đảm bảo audioCtx + masterBus đã tồn tại trước khi sync (lần preview // đầu tiên audio chưa init → getAudioContext tạo ngay) -getAudioContext();if(!masterBus||!audioCtx)return;toggleMasteringOnMaster(!!_s.masterConnected,!!_s.isBypassed);applyMasteringSettings(_s);updateSfRouting();}catch(e){console.warn('ensureMasteringRouting error:',e);}};const ensureMasteringRoutingRef=useRef(null);ensureMasteringRoutingRef.current=ensureMasteringRouting;window.__ensureMasteringRouting=ensureMasteringRouting;// ── MIDI preview cache (bounce nhanh offline) ── +getAudioContext();if(!masterBus||!audioCtx)return;toggleMasteringOnMaster(!!_s.masterConnected,!!_s.isBypassed);applyMasteringSettings(_s);updateSfRouting();// Force-sync MỌI node đang tồn tại (route + sf route) theo trạng thái +// hiện tại — phòng node tạo từ trước với bypass cũ (mastering OFF lúc +// tạo node / audioBypass default true của section) → preview LUÔN qua +// mastering khi chain bật, không cần bật/tắt power. +try{const _list2=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;_list2.forEach(_t=>{const _n=activeTrackNodesRef.current[_t.id];if(!_n)return;if(_n.sfRouteGain&&_n.sfDryGain){const _mb=effMidiBypass(_t);_n.sfRouteGain.gain.value=_mb?0:1;_n.sfDryGain.gain.value=_mb?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;}});}catch(e2){}}catch(e){console.warn('ensureMasteringRouting error:',e);}};const ensureMasteringRoutingRef=useRef(null);ensureMasteringRoutingRef.current=ensureMasteringRouting;window.__ensureMasteringRouting=ensureMasteringRouting;// ── MIDI preview cache (bounce nhanh offline) ── // Khi preview: capture tiếng đàn (SF output — pre track-FX) vào cache theo // track. Khi export: nếu cache đủ độ dài → render OFFLINE NHANH bằng cache // (qua FX Rack + Mastering như playback) — không cần chờ bounce realtime. @@ -1330,14 +1340,17 @@ const midiItems=track.midiItems||[];if((track.type==='MIDI'||midiItems.length>0) ensureMidiCapture(track,activeTrackNodesRef.current[track.id]);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 // sau khi âm đã dừng — user bug 05:45). if(!isPlayingRef.current)return;if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — user 07:20) -window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}},delay*1000);}else{const playOffset=offsetTime-noteStartSec;const remainingDurMs=(noteEndSec-offsetTime)*1000;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,gainNode,trkCh,track.synth_engine);// MIDI items → Carla bridge (track VSTi + Carla local) -scheduleCarlaNote(track.synth_engine,trkCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);// Trigger VU meter flash instantly +window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}},delay*1000);}else{const playOffset=offsetTime-noteStartSec;const remainingDurMs=(noteEndSec-offsetTime)*1000;if(!routeToCarla&&window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,gainNode,trkCh,track.synth_engine);}// MIDI items → Carla bridge (track VSTi + Carla local) +if(routeToCarla)scheduleCarlaNote(track.synth_engine,trkCh,note.pitch||60,note.velocity||0.8,context.currentTime,remainingDurMs);// Trigger VU meter flash instantly if(window.triggerMidiVuActivity){window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}}}});});}// If track has instrumentId set but no MIDI items, create scheduled oscillators if(track.instrumentId&&midiItems.length===0&&track.buffer){// Play audio normally (the buffer has the audio data) // This block is intentionally empty - audio clips already play above @@ -1349,7 +1362,9 @@ const subNodeObj=activeTrackNodesRef.current[track.id+'_sub_'+subTrack.id];if(su const subClips=subTrack.clips||[];subClips.forEach(clip=>{if(!clip.buffer)return;const clipStartLocal=clip.startTime||0;const clipDurLocal=clip.buffer.duration/(clip.speed||1.0);const clipStartMain=secStart+clipStartLocal;const clipEndMain=clipStartMain+clipDurLocal;// Only play within the bounds of [secStart, secEnd] const activeStart=Math.max(clipStartMain,secStart);const activeEnd=Math.min(clipEndMain,secEnd);if(activeStart0){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=secStart&¬eStartMain=secStart&¬eStartMain{// ⚠️ Guard: stop → setTimeout sót không được fire (VU // nhảy sau khi âm đã dừng — user bug 05:45). 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;window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,subNode,subCh,subTrack.synth_engine);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 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 @@ -1365,9 +1380,11 @@ const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackN // chorus/reverb → pannerNode. A direct tap would BYPASS the FX Rack // chain for audio clips (double path: with + without FX). 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 "đã +const midiItems=track.midiItems||[];if((track.type==='MIDI'||midiItems.length>0)&&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 "đã // 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 @@ -1376,11 +1393,13 @@ const instCtx=resolveTrackInstrumentCtx(track,activeTracksRef.current||[]);const // items placed later in the project play `item.startTime` seconds in the // future (silence when pressing play). Ghost notes are already relative to // 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;if(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 +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 // (schedule theo audio clock bằng setTimeout — preview, timing gần đúng). -if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(synthEngine)){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;if(window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,schedTime,ghostProg,ghostDest,ghostCh,ghostSynth);}// Ghost notes → Carla bridge (ghost track VSTi + Carla local) -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 +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 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/soundfontPlayer.js b/app/static/js/services/soundfontPlayer.js index ea4b5b5..1c7e967 100644 --- a/app/static/js/services/soundfontPlayer.js +++ b/app/static/js/services/soundfontPlayer.js @@ -21,6 +21,10 @@ let _pendingNoteTimers = []; let _loadedFonts = {}; let _activeOscillators = {}; + // Fallback oscillator theo 'channel:pitch' — để stopNote/stopAll dừng được + // 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 = {}; let _gainNode = null; let _pendingOutputDestination = null; let _outputDestination = null; // cache đích route — dedupe swap dư giữa stream @@ -479,6 +483,23 @@ // Hủy note đang CHỜ LOAD soundfont (chưa noteon) — trước đây note // này vẫn bắn TRỄ sau khi thả phím → âm loop không dừng được. if (_pendingNoteOns[key]) delete _pendingNoteOns[key]; + // ⚠️ FIX: dừng fallback oscillator theo 'channel:pitch' — trước đây + // stopNote chỉ noteoff FluidSynth, KHÔNG dừng oscillator (khi WASM + // chưa sẵn sàng / font load fail → _playNoteFallback) → âm kêu liên + // tục mặc dù đã thả phím (không xử lí noteoff). + var _oscIds = _activeOscillatorsByKey[key]; + if (_oscIds && _oscIds.length) { + var _nowT = getCtx().currentTime; + _oscIds.slice().forEach(function (oid) { + var entry = _activeOscillators[oid]; + if (entry) { + try { entry.gain.gain.cancelScheduledValues(_nowT); entry.gain.gain.setValueAtTime(0, _nowT); } catch (e) {} + try { entry.osc.stop(_nowT); } catch (e) {} + delete _activeOscillators[oid]; + } + }); + delete _activeOscillatorsByKey[key]; + } if (_initialized && _fluidModule) { var mappedChs = _activeNotes[key]; if (mappedChs === undefined) mappedChs = [channel]; @@ -711,6 +732,17 @@ for (var c = 0; c < 16; c++) _fluidModule._fluid_synth_all_notes_off(_synthPtr, c); } catch (e) {} } + // Dừng fallback oscillator (FluidSynth chưa sẵn sàng / font fail) + try { + var _pctx = getCtx(); + var _pnow = _pctx.currentTime; + Object.values(_activeOscillators).forEach(function (entry) { + try { if (entry.gain) { entry.gain.gain.cancelScheduledValues(_pnow); entry.gain.gain.setValueAtTime(0, _pnow); } } catch (e) {} + try { if (entry.osc) entry.osc.stop(_pnow); } catch (e) {} + }); + Object.keys(_activeOscillators).forEach(function (k) { delete _activeOscillators[k]; }); + _activeOscillatorsByKey = {}; + } catch (e) {} _activeNotes = {}; }, @@ -768,7 +800,20 @@ osc.stop(stopAt); var oscId = note + '_' + Date.now() + '_' + Math.random(); _activeOscillators[oscId] = { osc: osc, gain: noteGain }; - setTimeout(function () { delete _activeOscillators[oscId]; }, (stopAt - ctx.currentTime) * 1000 + 100); + // Đăng ký theo 'channel:pitch' để stopNote dừng được khi thả phím + var oscKey = (channel !== undefined && channel >= 0 && channel < 16) ? (channel + ':' + note) : null; + if (oscKey) { + if (!_activeOscillatorsByKey[oscKey]) _activeOscillatorsByKey[oscKey] = []; + _activeOscillatorsByKey[oscKey].push(oscId); + } + setTimeout(function () { + delete _activeOscillators[oscId]; + if (oscKey && _activeOscillatorsByKey[oscKey]) { + var _oi = _activeOscillatorsByKey[oscKey].indexOf(oscId); + if (_oi >= 0) _activeOscillatorsByKey[oscKey].splice(_oi, 1); + if (_activeOscillatorsByKey[oscKey].length === 0) delete _activeOscillatorsByKey[oscKey]; + } + }, (stopAt - ctx.currentTime) * 1000 + 100); return osc; }, @@ -805,6 +850,7 @@ } catch (e) {} }); Object.keys(_activeOscillators).forEach(function (k) { delete _activeOscillators[k]; }); + _activeOscillatorsByKey = {}; _activeNotes = {}; }, diff --git a/app/templates/index.html b/app/templates/index.html index dc32e5a..ca15cd9 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -31,12 +31,12 @@ - - + + - + @@ -44,7 +44,7 @@ - +