fix(playback): het note chong am echo o bass - KHONG huy note-off cu cung pitch (Bug 2 huy mui lam note cu khong bao gio tat); giu neu note-off cu bam truoc note-on moi, doi ve ngay truoc note-on neu overlap; sync 6 ban JS + index.html ?v=202608181900

This commit is contained in:
2026-08-18 20:13:53 +07:00
parent be8af7a6ee
commit 7bc96eebbc
3 changed files with 26 additions and 13 deletions
+3 -3
View File
@@ -29,7 +29,7 @@ const ensureSonicInstrument=ctx=>{try{if(!window.SonicSF||!window.SonicSF.select
// tiếp (bar liền nhau cùng hợp âm, piano roll note chồng) -> cắt note mới →
// câm. Map pending theo key channel:pitch — schedule NOTE_OFF mới cùng key →
// hủy NOTE_OFF cũ (retrigger đúng, note mới ngân đủ duration).
const pendingNoteOffMap={};const scheduleNoteOff=(key,fn,delayMs)=>{const prev=pendingNoteOffMap[key];if(prev){try{clearTimeout(prev);}catch(e){}}const timer=setTimeout(function(){if(pendingNoteOffMap[key]===timer)delete pendingNoteOffMap[key];fn();},delayMs);pendingNoteOffMap[key]=timer;return timer;};const clearAllPendingNoteOffs=()=>{Object.keys(pendingNoteOffMap).forEach(function(k){try{clearTimeout(pendingNoteOffMap[k]);}catch(e){}delete pendingNoteOffMap[k];});};const fireCarlaNote=(ch,pitch,vel,delay,durMs)=>{setTimeout(function(){window.SonicCarlaMidi.noteOn(ch,pitch,vel);},delay);scheduleNoteOff('C:'+ch+':'+pitch,function(){window.SonicCarlaMidi.noteOff(ch,pitch);},delay+durMs+30);};// ⚠️ FIX (Bug 2): Carla mở ASYNC lúc play (ensureCarlaForPlayback) — nốt gửi
const pendingNoteOffMap={};const scheduleNoteOff=(key,fn,noteOnDelayMs,durMs)=>{const offDelay=noteOnDelayMs+(durMs||1000)+30;const prev=pendingNoteOffMap[key];if(prev&&prev.offDelay>noteOnDelayMs){try{clearTimeout(prev.timer);}catch(e){}const t=setTimeout(function(){if(pendingNoteOffMap[key]&&pendingNoteOffMap[key].timer===t)delete pendingNoteOffMap[key];prev.fn();},Math.max(0,noteOnDelayMs-5));}const timer=setTimeout(function(){if(pendingNoteOffMap[key]&&pendingNoteOffMap[key].timer===timer)delete pendingNoteOffMap[key];fn();},offDelay);pendingNoteOffMap[key]={timer:timer,fn:fn,offDelay:offDelay};return timer;};const clearAllPendingNoteOffs=()=>{Object.keys(pendingNoteOffMap).forEach(function(k){const entry=pendingNoteOffMap[k];if(entry){try{clearTimeout(entry.timer);}catch(e){}}delete pendingNoteOffMap[k];});};const fireCarlaNote=(ch,pitch,vel,delay,durMs)=>{setTimeout(function(){window.SonicCarlaMidi.noteOn(ch,pitch,vel);},delay);scheduleNoteOff('C:'+ch+':'+pitch,function(){window.SonicCarlaMidi.noteOff(ch,pitch);},delay,durMs);};// ⚠️ FIX (Bug 2): Carla mở ASYNC lúc play (ensureCarlaForPlayback) — nốt gửi
// trước khi OSC engine bind cổng bị mất → item câm dù keybed kêu. Nốt khi
// chưa ready được nhét queue, flush khi bridge OSC ready (giữ đúng thứ tự).
const flushCarlaNoteQueue=()=>{const q=window.__carlaNoteQueue||[];if(!q.length)return;window.__carlaNoteQueue=[];q.forEach(item=>{const remaining=item.delay-(Date.now()-item.scheduledAt);if(remaining<=0)fireCarlaNote(item.ch,item.pitch,item.vel,0,item.durMs);else fireCarlaNote(item.ch,item.pitch,item.vel,remaining,item.durMs);});};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;// Chẩn đoán: note MIDI item → Carla (bật console.log để verify route)
@@ -1607,7 +1607,7 @@ const guardPlay=startTime!=null?function(){return isPlayingRef.current||subTabsR
const isPerc=!!(synthEngine&&synthEngine.soundfont_bank===128);// D8: track đổi instrument → gửi CC0/CC32 (bank THẬT từ synthEngine) +
// program change qua bridge (A12) — helper dedupe theo track+bank+program.
if(program!==undefined&&program!==null){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(trkId,program,synthEngine);}catch(e){}}// V11: epoch guard — hủy timer cũ khi stop/loop restart (xem ref).
const epoch=bridgeNoteEpochRef.current;setTimeout(function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:trkId,pitch:pitch||60,velocity:velocity||0.8,percussion:isPerc});}catch(e){}},delayMs);scheduleNoteOff('B:'+trkId+':'+(pitch||60),function(){if(epoch!==bridgeNoteEpochRef.current)return;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;}// Native VSTi (standalone browser, bridge inactive) → midi-render thay vì
const epoch=bridgeNoteEpochRef.current;setTimeout(function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:trkId,pitch:pitch||60,velocity:velocity||0.8,percussion:isPerc});}catch(e){}},delayMs);scheduleNoteOff('B:'+trkId+':'+(pitch||60),function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:trkId,pitch:pitch||60,velocity:0,percussion:isPerc});}catch(e){}},delayMs,durMs||1000);return;}// Native VSTi (standalone browser, bridge inactive) → midi-render thay vì
// FluidSynth GM sai âm — safety net cho mọi caller (ghost notes, per-note).
if(isStandaloneSf()&&isVstTrackEngine(synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeVstNote(track,pitch,velocity,durMs,startTime,track?track.id:'global');return;}window.SonicSF.playNote(pitch,velocity,durMs,startTime,program,destNode,ch,synthEngine);};const startTrackPlayback=offsetTime=>{const context=getAudioContext();_nativePlayEpoch++;restorePendingFadeSession();// new play — vô hiệu doHardStop đang fade (play trong cửa sổ 80ms)
// D2: bridge active -> báo transport PLAY (bridge flush note-off cũ + đồng
@@ -1704,7 +1704,7 @@ const routeCarla=shouldRouteCarla(synthEngine);if(isStandaloneSf()&&!routeCarla&
// Carla — không play FluidSynth GM sai âm chồng lên)
const routeToCarla=!!(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoutePlayback(synthEngine));if(!routeToCarla&&window.SonicSF){scheduleMidiNoteDispatch(track,note.pitch||60,note.velocity||0.8,durMs,scheduledTime,instrumentProgram,destNode,mainCh,synthEngine);}// MIDI items → Carla (track VSTi + Carla local): phát VSTi realtime
// (schedule theo audio clock bằng setTimeout — preview, timing gần đúng).
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);scheduleNoteOff('C:'+carlaCh+':'+carlaPitch,function(){window.SonicCarlaMidi.noteOff(carlaCh,carlaPitch);},carlaDelay+durMs+30);}}});// Play ghost notes from active tracks
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);scheduleNoteOff('C:'+carlaCh+':'+carlaPitch,function(){window.SonicCarlaMidi.noteOff(carlaCh,carlaPitch);},carlaDelay,durMs);}}});// 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){scheduleMidiNoteDispatch(ghostTrack,note.pitch||60,note.velocity||0.8,durMs,schedTime,ghostProg,ghostDest,ghostCh,ghostSynth);}if(gRouteCarla)scheduleCarlaNote(ghostSynth,ghostCh,note.pitch||60,note.velocity||0.8,schedTime,durMs);}});});};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ở