From fb59184dd25ea0b0ec3b2f36dc9879c97cab0364 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Tue, 25 Aug 2026 07:28:37 +0700 Subject: [PATCH] =?UTF-8?q?fix(bounce):=20guardPlay=20drop=20het=20note-on?= =?UTF-8?q?=20=E2=80=94=20isPlayingRef=20bi=20render=20ghi=20de=20=3Dfalse?= =?UTF-8?q?=20khi=20bounce=20setState;=20them=20isExportingRef=20vao=20gua?= =?UTF-8?q?rdPlay=20(bounce=20=3D=20dang=20phat=20that)=20+=20TMR=5FFIRE?= =?UTF-8?q?=20diag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 9 ++++++++- app/static/js/app.precompiled.js | 13 ++++++++----- app/templates/index.html | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 33c3432..d98df55 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -17850,6 +17850,10 @@ const App = () => { // khi hết âm). const isPlayingRef = useRef(false); isPlayingRef.current = isPlaying; + // Bounce realtime = project đang phát thật; guardPlay (scheduleMidiNoteDispatch) + // phải cho note fire dù isPlayingRef bị render ghi đè =false khi bounce setState. + const isExportingRef = useRef(false); + isExportingRef.current = isExporting; const [selectionStart, setSelectionStart] = useState(null); const [selectionEnd, setSelectionEnd] = useState(null); const [selectionFollowsTempo, setSelectionFollowsTempo] = useState(true); @@ -24597,7 +24601,7 @@ const App = () => { const startAt = startTime || ctx.currentTime; // undefined/null = phát ngay const delayMs = Math.max(0, (startAt - ctx.currentTime) * 1000); // Guard chống note-on trễ sau Stop: CHỈ khi là timeline (startTime thật). - const guardPlay = startTime != null ? function () { return isPlayingRef.current || (subTabsRef.current && subTabsRef.current.some(s => s.isPlaying)); } : function () { return true; }; + const guardPlay = startTime != null ? function () { return isPlayingRef.current || (subTabsRef.current && subTabsRef.current.some(s => s.isPlaying)) || isExportingRef.current; } : function () { return true; }; const trkId = track ? track.id : 0; // Percussion (bank 128) → router allocateChannel ch 9; melodic → round-robin. const isPerc = !!(synthEngine && synthEngine.soundfont_bank === 128); @@ -24609,6 +24613,7 @@ const App = () => { // V11: epoch guard — hủy timer cũ khi stop/loop restart (xem ref). const epoch = bridgeNoteEpochRef.current; setTimeout(function () { + if (window.__sfDiag) window.__sfDiag('TMR_FIRE track=' + trkId + ' p=' + (pitch || 60) + ' epOk=' + (epoch === bridgeNoteEpochRef.current) + ' guard=' + (guardPlay() ? 1 : 0)); if (epoch !== bridgeNoteEpochRef.current) return; if (!guardPlay()) return; if (window.__sfDiag) window.__sfDiag('DISPATCH_ON track=' + trkId + ' p=' + (pitch || 60)); @@ -28282,6 +28287,7 @@ const App = () => { if (!masterBus) { showToast("Hãy phát thử một lần để khởi tạo audio engine trước.", "warning"); return; } if (isExporting) return; if (window.__sfDiag) window.__sfDiag('BOUNCE_START'); + isExportingRef.current = true; // guardPlay: bounce = đang phát thật setIsExporting(true); setShowExportPanel(false); setShowExportProgress(true); @@ -28385,6 +28391,7 @@ const App = () => { } catch (err) { showToast("Lỗi bounce: " + err.message, "error"); } finally { + isExportingRef.current = false; setIsExporting(false); setShowExportProgress(false); } diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 45b840c..30ebedb 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -1138,7 +1138,9 @@ const[selectedTrackId,setSelectedTrackId]=useState('1');const[selectedItemIds,se // delayed) check ref này trước khi fire: stop → isPlaying=false → setTimeout // sót không fire → VU không nhảy sau khi âm đã dừng (user: VU vẫn animation // khi hết âm). -const isPlayingRef=useRef(false);isPlayingRef.current=isPlaying;const[selectionStart,setSelectionStart]=useState(null);const[selectionEnd,setSelectionEnd]=useState(null);const[selectionFollowsTempo,setSelectionFollowsTempo]=useState(true);const selectionRef=useRef({start:null,end:null});selectionRef.current={start:selectionStart,end:selectionEnd};const[selectionMode,setSelectionMode]=useState(null);// 'global' (from ruler) | 'local' (from track) +const isPlayingRef=useRef(false);isPlayingRef.current=isPlaying;// Bounce realtime = project đang phát thật; guardPlay (scheduleMidiNoteDispatch) +// phải cho note fire dù isPlayingRef bị render ghi đè =false khi bounce setState. +const isExportingRef=useRef(false);isExportingRef.current=isExporting;const[selectionStart,setSelectionStart]=useState(null);const[selectionEnd,setSelectionEnd]=useState(null);const[selectionFollowsTempo,setSelectionFollowsTempo]=useState(true);const selectionRef=useRef({start:null,end:null});selectionRef.current={start:selectionStart,end:selectionEnd};const[selectionMode,setSelectionMode]=useState(null);// 'global' (from ruler) | 'local' (from track) const[sweepSelect,setSweepSelect]=useState(null);const isSweepingRef=useRef(false);const sweepStartRef=useRef(0);const sweepTrackIdRef=useRef(null);const sweepStartYRef=useRef(0);const sweepEndYRef=useRef(0);const sweepSelectRef=useRef(null);const pendingDragRef=useRef(null);// { trackId, itemType, itemId, clickOffset, startX, startY } const handleSectionItemDragStartRef=useRef(null);const[localSelectionTrackId,setLocalSelectionTrackId]=useState(null);const[localSelectionStart,setLocalSelectionStart]=useState(null);const[localSelectionEnd,setLocalSelectionEnd]=useState(null);const[zoom,setZoom]=useState(()=>{// Persist zoom qua reload (user yêu cầu giữ kích thước items sau refresh) try{const v=parseFloat(localStorage.getItem('sf_zoom'));if(isFinite(v)&&v>0)return v;}catch(e){}return 100;});React.useEffect(()=>{try{localStorage.setItem('sf_zoom',String(zoom));}catch(e){}// Zoom TIMELINE (App) — serialize project dùng cái này (window.__currentZoom @@ -1881,11 +1883,11 @@ if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){const isPerc // setTimeout bằng sampleOffset đẩy thẳng vào SHM. const scheduleMidiNoteDispatch=(track,pitch,velocity,durMs,startTime,program,destNode,ch,synthEngine)=>{if(window.__sfDiag)window.__sfDiag('schedMidi track='+(track&&track.id)+' p='+pitch+' ch='+ch+' prog='+program+' bridgeActive='+!!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()));if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){const ctx=getAudioContext();const startAt=startTime||ctx.currentTime;// undefined/null = phát ngay const delayMs=Math.max(0,(startAt-ctx.currentTime)*1000);// Guard chống note-on trễ sau Stop: CHỈ khi là timeline (startTime thật). -const guardPlay=startTime!=null?function(){return isPlayingRef.current||subTabsRef.current&&subTabsRef.current.some(s=>s.isPlaying);}:function(){return true;};const trkId=track?track.id:0;// Percussion (bank 128) → router allocateChannel ch 9; melodic → round-robin. +const guardPlay=startTime!=null?function(){return isPlayingRef.current||subTabsRef.current&&subTabsRef.current.some(s=>s.isPlaying)||isExportingRef.current;}:function(){return true;};const trkId=track?track.id:0;// Percussion (bank 128) → router allocateChannel ch 9; melodic → round-robin. const isPerc=!!(synthEngine&&synthEngine.soundfont_bank===128);// D8: track đổi instrument → gửi CC0/CC32 (bank 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;if(window.__sfDiag)window.__sfDiag('DISPATCH_ON track='+trkId+' p='+(pitch||60));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ì +const epoch=bridgeNoteEpochRef.current;setTimeout(function(){if(window.__sfDiag)window.__sfDiag('TMR_FIRE track='+trkId+' p='+(pitch||60)+' epOk='+(epoch===bridgeNoteEpochRef.current)+' guard='+(guardPlay()?1:0));if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;if(window.__sfDiag)window.__sfDiag('DISPATCH_ON track='+trkId+' p='+(pitch||60));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();if(window.__sfDiag)window.__sfDiag('startPlay offset='+offsetTime+' isBridgeConn='+!!(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected)+' routerActive='+!!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())+' sonicSF='+!!window.SonicSF+' standalone='+isStandaloneSf());_nativePlayEpoch++;// new play — vô hiệu doHardStop đang fade (play trong cửa sổ 80ms) restorePendingFadeSession();// V17: huy fade dang treo — replay khong bi dip gain @@ -2152,7 +2154,8 @@ try{const br=await fetch(res.url);if(br.ok){const b=await br.blob();const sp=awa // trong OfflineAudioContext. Vì vậy bounce REALTIME: chạy lại toàn bộ project // (audio + MIDI + sections qua FX racks + mastering chain, đúng như nghe) và // capture PCM từ master bus, sau đó encode WAV trung thực. -const triggerBounceExport=async()=>{if(!masterBus){showToast("Hãy phát thử một lần để khởi tạo audio engine trước.","warning");return;}if(isExporting)return;if(window.__sfDiag)window.__sfDiag('BOUNCE_START');setIsExporting(true);setShowExportPanel(false);setShowExportProgress(true);const audioCtx=getAudioContext();try{const allTracks=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;// MAIN SESSION end time (items trên track main — sections tính theo bounds, +const triggerBounceExport=async()=>{if(!masterBus){showToast("Hãy phát thử một lần để khởi tạo audio engine trước.","warning");return;}if(isExporting)return;if(window.__sfDiag)window.__sfDiag('BOUNCE_START');isExportingRef.current=true;// guardPlay: bounce = đang phát thật +setIsExporting(true);setShowExportPanel(false);setShowExportProgress(true);const audioCtx=getAudioContext();try{const allTracks=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;// MAIN SESSION end time (items trên track main — sections tính theo bounds, // không kéo dài theo nội dung SECTION-TAB) — bounce không cắt sớm/cắt thiếu. // LOOP export: độ dài = loopBars*secondsPerBar, bắt đầu từ selection/playhead, // cắt cứng biên — không fade, không kéo tail. @@ -2168,7 +2171,7 @@ const totalFrames=chunks.reduce((a,c)=>a+c.length/2,0);let stereoBuf=new Float32 if(loopDur>0){const sf=Math.max(0,Math.floor(loopStart*captureRate))*2;const ef=Math.min(stereoBuf.length,Math.floor((loopStart+loopDur)*captureRate)*2);if(ef>sf)stereoBuf=stereoBuf.subarray(sf,ef);}const frames=stereoBuf.length/2;const bytesPerSample=isFloatBit?4:bitDepth/8;const headerSize=44;const fileSizeBytes=headerSize+frames*bytesPerSample*outChannels;const view=new DataView(new ArrayBuffer(fileSizeBytes));const writeString=(o,s)=>{for(let i=0;i>8&0xFF);view.setUint8(o+2,v>>16&0xFF);}else if(bitDepth===16)view.setInt16(o,Math.floor(s<0?s*0x8000:s*0x7FFF),true);else view.setUint8(o,Math.floor((s+1)*127.5),true);}o+=bytesPerSample;}}const blob=new Blob([view],{type:'audio/wav'});await downloadBlobViaServer(blob,`sonicforge-bounce-${captureRate}hz-${bitDepth}bit.wav`).catch(()=>{});if(window.__sfDiag)window.__sfDiag('BOUNCE_END');try{window.__sfDiagShow();}catch(e){}showToast("Bounce hoàn tất — file gồm MIDI + FX Rack + Mastering Chain!","success");}catch(err){showToast("Lỗi bounce: "+err.message,"error");}finally{setIsExporting(false);setShowExportProgress(false);}};const handleSaveLocalProject=name=>{const finalName=name||projectName||'Dự án mới';const localId=currentProjectId&¤tProjectId.startsWith('local_')?currentProjectId:'local_'+Date.now();const projectSchemaObj=serializeProjectToSchema(localId,finalName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});const dataStr=JSON.stringify(projectSchemaObj);localStorage.setItem('sonic_local_project_data',dataStr);localStorage.setItem('sonic_project_id',localId);localStorage.setItem('sonic_project_name',finalName);setCurrentProjectId(localId);setProjectName(finalName);window.SonicStorage.exportProjectToSFS(projectSchemaObj);showToast(`Đã lưu dự án local "${finalName}" thành công!`,"success");// Đã đăng nhập → đồng bộ lên Cloud (fire-and-forget): project mở được từ +const s=Math.max(-1,Math.min(1,tpdfDither(raw,lsb)));if(bitDepth===24){const v=Math.floor(s<0?s*0x800000:s*0x7FFFFF)&0xFFFFFF;view.setUint8(o,v&0xFF);view.setUint8(o+1,v>>8&0xFF);view.setUint8(o+2,v>>16&0xFF);}else if(bitDepth===16)view.setInt16(o,Math.floor(s<0?s*0x8000:s*0x7FFF),true);else view.setUint8(o,Math.floor((s+1)*127.5),true);}o+=bytesPerSample;}}const blob=new Blob([view],{type:'audio/wav'});await downloadBlobViaServer(blob,`sonicforge-bounce-${captureRate}hz-${bitDepth}bit.wav`).catch(()=>{});if(window.__sfDiag)window.__sfDiag('BOUNCE_END');try{window.__sfDiagShow();}catch(e){}showToast("Bounce hoàn tất — file gồm MIDI + FX Rack + Mastering Chain!","success");}catch(err){showToast("Lỗi bounce: "+err.message,"error");}finally{isExportingRef.current=false;setIsExporting(false);setShowExportProgress(false);}};const handleSaveLocalProject=name=>{const finalName=name||projectName||'Dự án mới';const localId=currentProjectId&¤tProjectId.startsWith('local_')?currentProjectId:'local_'+Date.now();const projectSchemaObj=serializeProjectToSchema(localId,finalName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});const dataStr=JSON.stringify(projectSchemaObj);localStorage.setItem('sonic_local_project_data',dataStr);localStorage.setItem('sonic_project_id',localId);localStorage.setItem('sonic_project_name',finalName);setCurrentProjectId(localId);setProjectName(finalName);window.SonicStorage.exportProjectToSFS(projectSchemaObj);showToast(`Đã lưu dự án local "${finalName}" thành công!`,"success");// Đã đăng nhập → đồng bộ lên Cloud (fire-and-forget): project mở được từ // máy khác / browser ẩn danh khi đăng nhập cùng tài khoản. if(currentUser&&window.SonicAPI&&window.SonicAPI.saveCloudProject){window.SonicAPI.saveCloudProject(finalName,dataStr).then(function(res){if(res&&res.project_id){console.log('[Cloud] local project synced:',res.project_id);}}).catch(function(e){console.warn('Cloud sync local project failed:',e);});}};const handleSaveCloudProject=async(name,existingProjectId)=>{const finalName=name||projectName||'Dự án mới';var useProjectId=existingProjectId||currentProjectId;const projectSchemaObj=serializeProjectToSchema(useProjectId||'project_'+Date.now(),finalName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});// Diagnostic: count source vs serialized items var srcMidi=0,srcClip=0,srcSec=0;tracks.forEach(function(t){srcMidi+=(t.midiItems||[]).length;srcClip+=(t.clips||[]).length;srcSec+=(t.sections||[]).length;});var serItems=0;if(projectSchemaObj.main_session&&projectSchemaObj.main_session.tracks){projectSchemaObj.main_session.tracks.forEach(function(st){if(st.items)serItems+=st.items.length;});}Object.keys(projectSchemaObj.section_store||{}).forEach(function(sk){(projectSchemaObj.section_store[sk].tracks||[]).forEach(function(st){if(st.items)serItems+=st.items.length;});});const dataJson=JSON.stringify(projectSchemaObj);try{if(existingProjectId){await window.SonicAPI.updateCloudProject(existingProjectId,finalName,dataJson);setProjectName(finalName);setCurrentProjectId(existingProjectId);localStorage.setItem('sonic_project_name',finalName);localStorage.setItem('sonic_project_id',existingProjectId);showToast('Đã ghi đè Cloud "'+finalName+'" (src midi='+srcMidi+' clip='+srcClip+' sec='+srcSec+' | ser='+serItems+').','success');}else if(currentProjectId&&!currentProjectId.startsWith('local_')){await window.SonicAPI.updateCloudProject(currentProjectId,finalName,dataJson);setProjectName(finalName);localStorage.setItem('sonic_project_name',finalName);localStorage.setItem('sonic_project_id',currentProjectId);showToast('Đã lưu Cloud "'+finalName+'" (src midi='+srcMidi+' clip='+srcClip+' sec='+srcSec+' | ser='+serItems+' id='+currentProjectId+').',serItems===0?'warning':'success');}else{const res=await window.SonicAPI.saveCloudProject(finalName,dataJson);const newProjId=res.project_id;setProjectName(finalName);localStorage.setItem('sonic_project_name',finalName);if(newProjId){setCurrentProjectId(newProjId);localStorage.setItem('sonic_project_id',newProjId);}showToast('Đã lưu Cloud mới "'+finalName+'" (src midi='+srcMidi+' clip='+srcClip+' sec='+srcSec+' | ser='+serItems+').',serItems===0?'warning':'success');}}catch(err){showToast(err.message||"Lỗi lưu dự án lên Cloud","error");}};const handleSaveProject=async()=>{if(!currentProjectId){setSaveProjectModalOpen(true);return;}if(currentProjectId.startsWith('local_')){handleSaveLocalProject(projectName);}else{if(!currentUser){showToast('Cần đăng nhập để lưu Cloud. currentUser='+(currentUser?'OK':'NULL')+' token='+(localStorage.getItem('sonic_token')?'exists':'missing'),'warning');return;}handleSaveCloudProject(projectName);}};const handleSaveProjectRef=useRef(handleSaveProject);handleSaveProjectRef.current=handleSaveProject;// ── Ctrl-S (Save) toàn cục ── diff --git a/app/templates/index.html b/app/templates/index.html index 280ce1a..b457484 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -51,7 +51,7 @@ - +