From 2f7861820ee16b4d1408321953a414d7cdc963dd Mon Sep 17 00:00:00 2001 From: locphamtran Date: Sat, 15 Aug 2026 18:45:25 +0700 Subject: [PATCH] G0.2: openVstGuiRetry toi da 2 lan + backoff 800ms + skip khi PLAY --- app/static/js/app.precompiled.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 7da25d8..25b7032 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -467,7 +467,8 @@ let knownPath=track.synth_engine&&track.synth_engine.plugin_path;if(!knownPath&& // som → C++ defer va tu fulfil khi assign() xong (pending-GUI sweep) → khong // can spam. 20 lan x 500ms tao deferred storm (50+ event/moi restart) va // re-attach view dang attached (crash USER32 0xc000041d). Cap 5. -const openVstGuiRetry=async(instrumentId,bch,attempts)=>{const n=Math.min(attempts&&attempts>0?attempts:5,5);for(let i=0;isetTimeout(r,500));}return true;};// Module-level so both ProfileModal (open project) and App (auto-restore) can +const openVstGuiRetry=async(instrumentId,bch,attempts)=>{if(window.__bridgePlaying)return true;// G0.2: khong retry khi PLAY (G0.1 da defer) +const n=Math.min(attempts&&attempts>0?attempts:2,2);for(let i=0;isetTimeout(r,800));}return true;};// Module-level so both ProfileModal (open project) and App (auto-restore) can // warm the FluidSynth font cache for each track's instrument. This only loads // the soundfonts; the per-track channel re-selection happens at note time. const preloadTrackInstruments=async tracks=>{const list=tracks||[];if(window.SonicSF&&window.SonicSF.selectInstrument){for(let i=0;i{if(st.type!=='PIANO_ROLL')return;const context=getAudioContext();// V10: piano roll loop restart cũng gọi stopAllPlayback() trước (STOP#1 → // bridge transportStopped=true) → mọi note-on bị drop nếu không PLAY lại. // Hủy STOP#2 (150ms) + báo PLAY để bridge nhận note mới (loop liên tục). -if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected){if(bridgeStopRetryTimerRef.current){clearTimeout(bridgeStopRetryTimerRef.current);bridgeStopRetryTimerRef.current=null;}try{window.NativeBridgeService.transport('play');}catch(e){}}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 "đã +if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected){if(bridgeStopRetryTimerRef.current){clearTimeout(bridgeStopRetryTimerRef.current);bridgeStopRetryTimerRef.current=null;}window.__bridgePlaying=true;try{window.NativeBridgeService.transport('play');}catch(e){}}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 @@ -1586,11 +1587,11 @@ try{midiVuActivityRef.current={};}catch(e){}// Clear luôn counter note đang gi // tick vẫn thấy heldCnt > 0 → giữ peak → VU dính mãi (user bug 08:08). try{heldMidiNotesRef.current={};}catch(e){}stopMidiCapture();stopAllNativeSfNotes();// D2/D6: bridge active -> transport STOP (bridge flush toàn pitch, hết âm // ngân tức thì; A13 C++ flush note-off). -if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected){try{window.NativeBridgeService.transport('stop');}catch(e){}// V8 bug 3: transport stop thu 2 sau 150ms — note-on timer guard sync qua +if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected){window.__bridgePlaying=false;try{window.NativeBridgeService.transport('stop');}catch(e){}// V8 bug 3: transport stop thu 2 sau 150ms — note-on timer guard sync qua // React effect (cham) van bay toi bridge sau STOP → retrigger am treo. // V10: timer lưu ref — startTrackPlayback (loop restart) hủy khi PLAY, // tránh STOP#2 giết loop tiếp theo (VSTi chỉ play 1 vòng). -if(bridgeStopRetryTimerRef.current){clearTimeout(bridgeStopRetryTimerRef.current);}bridgeStopRetryTimerRef.current=setTimeout(()=>{bridgeStopRetryTimerRef.current=null;try{window.NativeBridgeService.transport('stop');}catch(e){}},150);}if(window.SonicSF){try{window.SonicSF.stopAll();}catch(e){console.warn('[Stop] stopAll error:',e);}// Dừng triệt để: noteoff từng note + hủy scheduled note-on (hết âm stuck) +if(bridgeStopRetryTimerRef.current){clearTimeout(bridgeStopRetryTimerRef.current);}bridgeStopRetryTimerRef.current=setTimeout(()=>{bridgeStopRetryTimerRef.current=null;window.__bridgePlaying=false;try{window.NativeBridgeService.transport('stop');}catch(e){}},150);}if(window.SonicSF){try{window.SonicSF.stopAll();}catch(e){console.warn('[Stop] stopAll error:',e);}// Dừng triệt để: noteoff từng note + hủy scheduled note-on (hết âm stuck) if(window.SonicSF.panic){try{window.SonicSF.panic();}catch(e){console.warn('[Stop] panic error:',e);}}}}catch(e){console.warn('[Stop] stopAllPlayback error:',e);}setIsPlaying(false);setSubTabs(prev=>prev.map(s=>({...s,isPlaying:false})));updateSfRouting();};const seekPlaybackTo=time=>{const isSubTab=subTabs.some(sub=>sub.id===activeTab);if(isSubTab){const st=subTabs.find(s=>s.id===activeTab);if(!st)return;if(st.isPlaying){stopAllPlayback();window.SonicSF.stopAll();stopAllNativeSfNotes();setSubTabs(prev=>prev.map(s=>s.id===activeTab?{...s,currentTime:time,isPlaying:true}:s));const ctx=getAudioContext();startOffsetTimeRef.current=time;startAudioTimeRef.current=ctx.currentTime;startBufferOffsetRef.current=time*(st.speed||1.0);if(st.type==='PIANO_ROLL'){schedulePianoRollMidi(st,time);}startSubTabPlayback(st,time);}else{setSubTabs(prev=>prev.map(s=>s.id===activeTab?{...s,currentTime:time}:s));}}else{if(isPlaying){stopAllPlayback();setCurrentTime(time);startOffsetTimeRef.current=time;startAudioTimeRef.current=getAudioContext().currentTime;startTrackPlayback(time);setIsPlaying(true);}else{setCurrentTime(time);}}};const handleStop=()=>{if(recordingStateRef.current==='RECORDING'||recordingStateRef.current==='COUNT_IN'){stopRecordingTake();return;}stopAllPlayback();if(activeTab!=='main'&&!activeTab.startsWith('session_')){setSubTabs(prev=>prev.map(s=>s.id===activeTab?{...s,currentTime:0}:s));}else{setCurrentTime(0);}};const drawVuMeter=(canvas,db)=>{if(!canvas)return;const ctx=canvas.getContext('2d');if(!ctx)return;const w=canvas.width;const h=canvas.height;ctx.clearRect(0,0,w,h);const minDb=-60;const maxDb=0;const frac=Math.max(0,Math.min(1,(db-minDb)/(maxDb-minDb)));ctx.fillStyle='#18181b';ctx.fillRect(0,0,w,h);const grad=ctx.createLinearGradient(0,0,w,0);grad.addColorStop(0,'#10b981');grad.addColorStop(0.7,'#eab308');grad.addColorStop(0.95,'#ef4444');ctx.fillStyle=grad;ctx.fillRect(0,0,w*frac,h);if(db>=-0.5){ctx.fillStyle='#ff0000';ctx.fillRect(w-6,0,6,h);}};const handleRecordClick=async()=>{if(recordingState==='RECORDING'||recordingState==='COUNT_IN'){await stopRecordingTake();return;}// Check if piano roll tab is active and armed const activePianoRoll=subTabs.find(s=>s.id===activeTab&&s.type==='PIANO_ROLL'&&s.isArmed);if(activePianoRoll&&selectedMidiInputId){setRecordingState('COUNT_IN');const secondsPerBeat=60.0/(parseInt(bpm)||120);const countInDuration=secondsPerBeat*4;const audioCtx=getAudioContext();const now=audioCtx.currentTime;showToast('Metronome Count-in: 4... 3... 2... 1... (MIDI Piano Roll)','info');for(let i=0;i<4;i++){playMetronomeClick(now+i*secondsPerBeat,i===0);}setTimeout(()=>{startPianoRollRecording(activePianoRoll);},countInDuration*1000);return;}const armed=activeTracks.filter(t=>t.isArmed&&t.inputSource?.deviceType&&t.inputSource.deviceType!=='NONE');if(armed.length===0){showToast('Vui lòng Arm (R) ít nhất một track và chọn cổng Input để ghi âm.','warning');return;}setRecordingState('COUNT_IN');const secondsPerBeat=60.0/(parseInt(bpm)||120);const countInDuration=secondsPerBeat*4;const audioCtx=getAudioContext();const now=audioCtx.currentTime;showToast('Metronome Count-in: 4... 3... 2... 1...','info');for(let i=0;i<4;i++){playMetronomeClick(now+i*secondsPerBeat,i===0);}setTimeout(()=>{startRecordingTake(armed);},countInDuration*1000);};const startPianoRollRecording=tab=>{try{const context=getAudioContext();const secondsPerBeat=60.0/(parseInt(bpm)||120);const startTime=currentTime;const startBeat=startTime/secondsPerBeat;nextMetronomeBeatRef.current=Math.ceil(startBeat);const midiRec=new ClientMIDIRecorder(context,parseInt(bpm)||120,4);midiRec.tempTabId=tab.id;midiRec.selectedMidiInputId=selectedMidiInputId||'ALL';midiRec.start(startTime/secondsPerBeat,midiRec.selectedMidiInputId);pianoRollRecorderRef.current=midiRec;activeMIDIRecordersRef.current['piano_roll']=midiRec;setRecStartTimelineTime(startTime);recordingStartTimeRef.current=startTime;setRecordingState('RECORDING');setRecTempMidiNotes([]);const ctx=getAudioContext();const silentBuf=ctx.createBuffer(1,128,ctx.sampleRate);setSubTabs(prev=>prev.map(s=>s.id===tab.id?{...s,buffer:silentBuf,isPlaying:true}:s));startSubTabPlayback({...tab,buffer:silentBuf},startTime);startOffsetTimeRef.current=startTime;startAudioTimeRef.current=context.currentTime;setIsPlaying(true);midiRec.onNoteOn=(pitch,currentBeat)=>{const elapsedBeats=Math.max(0,currentBeat);const sec=elapsedBeats*(60.0/(parseInt(bpm)||120));const activeNotes=Array.from(midiRec.activeNotes.values()).map(n=>({id:'rec_'+n.pitch+'_'+currentBeat,pitch:n.pitch,start_beat:n.start_beat,duration_beats:Math.max(0.125,currentBeat-n.start_beat),velocity:Math.min(1,n.velocity||0.8),pan:0.0}));const rec=midiRec.recordedNotes.map((n,i)=>({id:'rec_'+Date.now()+'_'+i,pitch:n.pitch,start_beat:n.start_beat,duration_beats:n.duration_beats,velocity:Math.min(1,n.velocity||0.8),pan:0.0}));const allNotes=[...rec,...activeNotes];setRecTempMidiNotes(allNotes);setCanvasRedrawCount(n=>n+1);setSubTabs(prev=>prev.map(s=>s.id===tab.id?{...s,currentTime:startTime+sec,isDirty:true}:s));};if(!tab._recordingStarted){tab._recordingStarted=true;}showToast('Recording MIDI to Piano Roll...','info');}catch(err){console.error('startPianoRollRecording error:',err);showToast('Lỗi khi bắt đầu ghi âm Piano Roll: '+err.message,'warning');setRecordingState('IDLE');}};handleRecordClickRef.current=handleRecordClick;const startRecordingTake=async armedTracks=>{const context=getAudioContext();if(context.state==='suspended'){await context.resume();}setRecordingState('RECORDING');setRecTempMidiNotes([]);setRecTempAudioBuffer(null);const startTimelineTime=currentTime;setRecStartTimelineTime(startTimelineTime);recordingStartTimeRef.current=startTimelineTime;const secondsPerBeat=60.0/(parseInt(bpm)||120);const startBeat=startTimelineTime/secondsPerBeat;nextMetronomeBeatRef.current=Math.ceil(startBeat);activeMIDIRecordersRef.current={};activeAudioRecordersRef.current={};recordingPCMDataRef.current={};startOffsetTimeRef.current=startTimelineTime;startAudioTimeRef.current=context.currentTime;startTrackPlayback(startTimelineTime);setIsPlaying(true);let midiRecList=[];for(let track of armedTracks){if(track.inputSource.deviceType==='MIDI_KEYBOARD'){const midiRec=new ClientMIDIRecorder(context,parseInt(bpm)||120,4);const tempMidiItemId='midi_rec_'+Date.now()+'_'+track.id;midiRec.tempMidiItemId=tempMidiItemId;updateActiveTracks(prev=>prev.map(t=>{if(t.id!==track.id)return t;return{...t,midiItems:[...(t.midiItems||[]),{id:tempMidiItemId,name:'Recording...',startTime:startTimelineTime,duration:4*(secondsPerBeat*4),notes:[]}]};}));midiRec.onNoteOn=(pitch,currentBeat)=>{const canvas=trackVuRefs.current[track.id];if(canvas){drawVuMeter(canvas,0);setTimeout(()=>drawVuMeter(canvas,-60),100);}const elapsedBeats=Math.max(0,currentBeat);const elapsedSec=elapsedBeats*secondsPerBeat;const durationSec=Math.max(4*(secondsPerBeat*4),elapsedSec);const allNotes=[...midiRec.recordedNotes,...Array.from(midiRec.activeNotes.values()).map(n=>({...n,duration_beats:currentBeat-n.start_beat}))];setRecTempMidiNotes(allNotes);setCanvasRedrawCount(n=>n+1);if(midiRec.tempMidiItemId){updateActiveTracks(prev=>prev.map(t=>{if(t.id!==track.id)return t;return{...t,midiItems:t.midiItems.map(m=>m.id===midiRec.tempMidiItemId?{...m,notes:allNotes,duration:durationSec}:m)};}));}};midiRec.onNoteOff=()=>{const currentBeat=(context.currentTime-midiRec.recStartAudioTime-midiRec.latencyCompSec)/(60.0/midiRec.bpm);const elapsedBeats=Math.max(0,currentBeat);const elapsedSec=elapsedBeats*secondsPerBeat;const durationSec=Math.max(4*(secondsPerBeat*4),elapsedSec);const activeNotesArray=Array.from(midiRec.activeNotes.values()).map(n=>({...n,duration_beats:currentBeat-n.start_beat}));const allNotes=[...midiRec.recordedNotes,...activeNotesArray];setRecTempMidiNotes(allNotes);setCanvasRedrawCount(n=>n+1);if(midiRec.tempMidiItemId){updateActiveTracks(prev=>prev.map(t=>{if(t.id!==track.id)return t;return{...t,midiItems:t.midiItems.map(m=>m.id===midiRec.tempMidiItemId?{...m,notes:allNotes,duration:durationSec}:m)};}));}};midiRec.start(startTimelineTime/(secondsPerBeat*4),track.inputSource.deviceId);activeMIDIRecordersRef.current[track.id]=midiRec;midiRecList.push({trackId:track.id,midiRec});}else if(track.inputSource.deviceType==='MICROPHONE'){const audioRec=new ClientAudioRecorder(context);try{await audioRec.initializeInput(track.inputSource.deviceId);recordingPCMDataRef.current[track.id]=[];audioRec.onLevelUpdate=db=>{const canvas=trackVuRefs.current[track.id];if(canvas){drawVuMeter(canvas,db);}};audioRec.onPCMChunk=chunk=>{if(recordingPCMDataRef.current[track.id]){const currentData=recordingPCMDataRef.current[track.id];const newData=new Float32Array(currentData.length+chunk.length);newData.set(currentData);newData.set(chunk,currentData.length);recordingPCMDataRef.current[track.id]=newData;}};const monitorGain=track.monitoringEnabled?masterBus?masterBus.input:context.destination:null;await audioRec.start(monitorGain,track.monitoringEnabled);activeAudioRecordersRef.current[track.id]=audioRec;}catch(err){console.error('Failed to initialize microphone:',err);showToast('Không khởi động được micro: '+err.message,'warning');}}}recordingSyncRef.current=setInterval(()=>{const secondsPerBeatInt=60.0/(parseInt(bpm)||120);for(let{trackId,midiRec}of midiRecList){if(!midiRec.isRecording||!midiRec.tempMidiItemId)continue;const currentTimeSec=Math.max(0,getAudioContext().currentTime-midiRec.recStartAudioTime-midiRec.latencyCompSec);const currentBeat=currentTimeSec/secondsPerBeatInt;const allNotes=[...midiRec.recordedNotes,...Array.from(midiRec.activeNotes.values()).map(n=>({...n,duration_beats:currentBeat-n.start_beat}))];const durationSec=Math.max(4*(secondsPerBeatInt*4),currentTimeSec);if(Math.random()<0.2){// Throttle log to prevent flooding (approx 2 logs/sec) console.log(`[DevLog] [MIDI Rec Sync] Temp Item ID: ${midiRec.tempMidiItemId}, Duration: ${durationSec.toFixed(2)}s, ActiveNotes: ${midiRec.activeNotes.size}, RecordedNotes: ${midiRec.recordedNotes.length}`);}setRecTempMidiNotes(allNotes);setCanvasRedrawCount(n=>n+1);updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,midiItems:t.midiItems.map(m=>m.id===midiRec.tempMidiItemId?{...m,notes:allNotes,duration:durationSec}:m)};}));}},100);showToast('Đang ghi âm...','info');};const stopRecordingTake=async()=>{setRecordingState('IDLE');stopAllPlayback();const context=getAudioContext();const secondsPerBeat=60.0/(parseInt(bpm)||120);const secondsPerBar=secondsPerBeat*4;const midiRecorders=activeMIDIRecordersRef.current;const audioRecorders=activeAudioRecordersRef.current;Object.keys(trackVuRefs.current).forEach(tid=>{const canvas=trackVuRefs.current[tid];if(canvas)drawVuMeter(canvas,-60);});let hasRecordedAnything=false;for(let trackId in midiRecorders){const midiRec=midiRecorders[trackId];const recordedNotes=midiRec.stop();if(midiRec.tempTabId){if(recordedNotes.length>0){const newNotes=recordedNotes.map((n,i)=>({id:'rec_'+Date.now()+'_'+i,pitch:n.pitch,start_beat:Math.max(0,n.start_beat||0),duration_beats:Math.max(0.125,n.duration_beats||0.25),velocity:Math.min(1,n.velocity||0.8),pan:0.0}));setSubTabs(prev=>prev.map(s=>s.id===midiRec.tempTabId?{...s,notes:[...(s.notes||[]),...newNotes],isDirty:true}:s));setCanvasRedrawCount(n=>n+1);showToast(`Đã ghi ${recordedNotes.length} notes vào Piano Roll.`,'success');}hasRecordedAnything=true;}else if(midiRec.tempMidiItemId){const recCurrentTimeSec=Math.max(0,context.currentTime-midiRec.recStartAudioTime-midiRec.latencyCompSec);const recElapsedBeats=recCurrentTimeSec/(60.0/midiRec.bpm);const totalDurationBeats=Math.max(4.0,recordedNotes.length>0?Math.max(recElapsedBeats,...recordedNotes.map(n=>n.start_beat+n.duration_beats)):recElapsedBeats);updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;const itemIndex=(t.midiItems||[]).findIndex(m=>m.id===midiRec.tempMidiItemId);if(itemIndex>=0){const updatedItems=[...t.midiItems];updatedItems[itemIndex]={...updatedItems[itemIndex],name:recordedNotes.length>0?'Recorded MIDI':'Empty MIDI',notes:recordedNotes,duration:Math.ceil(totalDurationBeats/4)*secondsPerBar};return{...t,midiItems:updatedItems};}const newMidiItem={id:'midi_rec_'+Date.now(),name:'Recorded MIDI',parent_track_id:t.id,startTime:recordingStartTimeRef.current,duration:Math.ceil(totalDurationBeats/4)*secondsPerBar,length_bars:Math.ceil(totalDurationBeats/4),notes:recordedNotes};return{...t,midiItems:[...(t.midiItems||[]),newMidiItem]};}));if(recordedNotes.length>0){hasRecordedAnything=true;}}else if(recordedNotes.length>0){hasRecordedAnything=true;const totalDurationBeats=Math.max(4.0,...recordedNotes.map(n=>n.start_beat+n.duration_beats));const newMidiItem={id:'midi_rec_'+Date.now(),name:'Recorded MIDI',parent_track_id:trackId,startTime:recordingStartTimeRef.current,duration:Math.ceil(totalDurationBeats/4)*secondsPerBar,length_bars:Math.ceil(totalDurationBeats/4),notes:recordedNotes};updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,midiItems:[...(t.midiItems||[]),newMidiItem]};}));}}for(let trackId in audioRecorders){const audioRec=audioRecorders[trackId];const audioBuffer=await audioRec.stop();if(audioBuffer&&audioBuffer.duration>0.05){hasRecordedAnything=true;const newClip={id:'clip_rec_'+Date.now(),name:'Recorded Audio.wav',buffer:audioBuffer,startTime:recordingStartTimeRef.current,speed:1.0};updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;const updatedClips=[...(t.clips||[]),newClip];return{...t,clips:updatedClips,buffer:updatedClips[0]?.buffer||null,startTime:updatedClips[0]?.startTime||0,name:updatedClips[0]?.name||t.name};}));}}if(recordingSyncRef.current){clearInterval(recordingSyncRef.current);recordingSyncRef.current=null;}activeMIDIRecordersRef.current={};activeAudioRecordersRef.current={};recordingPCMDataRef.current={};setRecTempMidiNotes([]);setRecTempAudioBuffer(null);if(hasRecordedAnything){showToast('Đã thu và lưu bản ghi vào timeline.','success');}else{showToast('Đã dừng ghi âm (không phát hiện tín hiệu đầu vào).','info');}};const handleSubTabResizeMouseDown=e=>{e.preventDefault();e.stopPropagation();const startY=e.clientY;const startHeight=subTabHeight;const handleMouseMove=moveEvent=>{const deltaY=moveEvent.clientY-startY;const newHeight=Math.max(48,Math.min(400,startHeight+deltaY));setSubTabHeight(newHeight);};const handleMouseUp=()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);};// ── Playhead set with seek+play ──