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
+22 -9
View File
@@ -118,25 +118,38 @@ const ensureSonicInstrument = (ctx) => {
// 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) => {
// V18: note-off cũ cùng key KHÔNG bị hủy mù — hủy mù làm note cùng pitch lặp
// (bass: root note chạy liên tục) không bao giờ nhận note-off -> kêu dài chồng
// âm echo, không decay. So sánh thời điểm: note-off cũ bắn TRƯỚC note-on mới →
// giữ nguyên (decay đúng hạn). Note-off cũ bắn SAU note-on mới (overlap — Bug 2
// cũ) → dời về NGAY TRƯỚC note-on mới: note cũ decay sạch, note mới ngân đủ.
const scheduleNoteOff = (key, fn, noteOnDelayMs, durMs) => {
const offDelay = noteOnDelayMs + (durMs || 1000) + 30;
const prev = pendingNoteOffMap[key];
if (prev) { try { clearTimeout(prev); } catch (e) {} }
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] === timer) delete pendingNoteOffMap[key];
if (pendingNoteOffMap[key] && pendingNoteOffMap[key].timer === timer) delete pendingNoteOffMap[key];
fn();
}, delayMs);
pendingNoteOffMap[key] = timer;
}, offDelay);
pendingNoteOffMap[key] = { timer: timer, fn: fn, offDelay: offDelay };
return timer;
};
const clearAllPendingNoteOffs = () => {
Object.keys(pendingNoteOffMap).forEach(function (k) {
try { clearTimeout(pendingNoteOffMap[k]); } catch (e) {}
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 + 30);
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
@@ -22681,7 +22694,7 @@ const App = () => {
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);
}, delayMs, durMs || 1000);
return;
}
// Native VSTi (standalone browser, bridge inactive) → midi-render thay vì
@@ -23215,7 +23228,7 @@ const App = () => {
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);
scheduleNoteOff('C:' + carlaCh + ':' + carlaPitch, function () { window.SonicCarlaMidi.noteOff(carlaCh, carlaPitch); }, carlaDelay, durMs);
}
}
});
+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ở