diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 77b2132..2ae619f 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -14840,6 +14840,31 @@ const App = () => { if (prevSub && prevSub.isPlaying) { try { stopAllPlayback(); } catch (e) {} setSubTabs(prev => prev.map(s => s.id === prevTab ? { ...s, isPlaying: false } : s)); + // Resume main/session play khi rời PIANO ROLL tab đã play (Space): + // play piano roll → stopAllPlayback dừng main → quay lại MAIN/SECTION + // bị CÂM. Resume startTrackPlayback(offset + elapsed thật) — chỉ khi + // trước đó mở tab lúc main đang play (mainResumeRef đã set). + const _resume = mainResumeRef.current; + mainResumeRef.current = null; + if (_resume && prevSub.type === 'PIANO_ROLL' && (activeTab === 'main' || (activeTab && activeTab.startsWith('session_')))) { + try { + const _elapsed = getAudioContext().currentTime - _resume.audioTime; + const _resumeAt = _resume.offset + _elapsed; + console.log('[Resume] main play từ', _resumeAt.toFixed(2), 's (offset', _resume.offset.toFixed(2), '+ elapsed', _elapsed.toFixed(2), ')'); + stopAllPlayback(); + startOffsetTimeRef.current = _resumeAt; + startAudioTimeRef.current = getAudioContext().currentTime; + startTrackPlayback(_resumeAt); + setIsPlaying(true); + } catch (e) { console.warn('[Resume] error:', e); } + } + } else if (prevSub && prevSub.type === 'PIANO_ROLL') { + // Rời PIANO ROLL tab KHÔNG play: main/section play ngầm vẫn chạy (âm + // tiếp tục tự nhiên — không cần resume) → clear resume point. + // ⚠️ KHÔNG clear khi prevTab = main/session (đang DI CHUYỂN VÀO piano + // roll — mainResumeRef vừa set bởi handleEditMidiInTab — clear nhầm ở + // đây làm resume chết → quay lại main/section bị CÂM). + mainResumeRef.current = null; } } }, [activeTab]); @@ -15562,6 +15587,11 @@ const App = () => { const isDraggingSubTabRef = useRef(false); const handlePlayPauseRef = useRef(null); const currentTimeRef = useRef(currentTime); + // Resume main/session play khi rời PIANO ROLL tab: mở piano roll lúc main + // đang play → bấm Space (play piano roll) → stopAllPlayback dừng main → + // quay lại MAIN/SECTION → CÂM. Lưu {offset, audioTime} lúc mở tab → khi + // rời tab resume startTrackPlayback(offset + elapsed thật). + const mainResumeRef = useRef(null); currentTimeRef.current = currentTime; // ── Keyboard Shortcuts ── @@ -16424,7 +16454,15 @@ const App = () => { // mở PIANO ROLL tab KHÔNG được dừng âm (user: dblclick mở tab lúc main // play → âm main phải tiếp tục). Nhánh play sub-tab tự stopAllPlayback // trước khi play nên không xung đột route. - if (!isPlaying) stopAllPlayback(); + if (isPlaying) { + // Lưu điểm resume: nếu user bấm Space trong piano roll (play piano roll + // → stopAllPlayback dừng main) → khi quay lại MAIN/SECTION resume main + // từ đúng vị trí âm (offset + elapsed thật) — hết câm. + mainResumeRef.current = { offset: startOffsetTimeRef.current, audioTime: getAudioContext().currentTime }; + } else { + mainResumeRef.current = null; + stopAllPlayback(); + } const ctx = getAudioContext(); const silentBuffer = ctx.createBuffer(1, 128, ctx.sampleRate); @@ -18425,7 +18463,19 @@ const App = () => { const isSubTab = subTabsRef.current.some(sub => sub.id === activeTabRef.current); if (isSubTab) { const st = subTabsRef.current.find(s => s.id === activeTabRef.current); - if (!st || !st.isPlaying || !st.buffer) return; + if (!st || !st.buffer) return; + if (!st.isPlaying) { + // PIANO ROLL / sub-tab KHÔNG play (mới mở, đang edit): playhead của + // MAIN/SECTION KHÔNG được tiếp tục di chuyển (yêu cầu user) — KHÔNG + // setCurrentTime. Loop phải TIẾP TỤC (rAF) để khi quay lại tab playhead + // không nhảy vọt sai lệch. ⚠️ KHÔNG check projectEnd ở đây: projectEnd + // tính theo activeTab — piano roll tab (sub-tab) → projectEnd = 1s → + // updatedTime (elapsed thật) >= 1 → stopAllPlayback NGAY → CÂM + panic + // notes. Âm main đã schedule hết từ startTrackPlayback — tự hết đúng + // lúc; khi quay lại MAIN nhánh main tự stop tại projectEnd thật. + animationFrameIdRef.current = requestAnimationFrame(updatePlayhead); + return; + } const context = getAudioContext(); const speedFactor = st.speed || 1.0; const elapsed = context.currentTime - startAudioTimeRef.current; diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 3ac06ad..12afbe3 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -694,7 +694,16 @@ if(t.midiItems&&t.midiItems.length>0||t.type==='MIDI'){try{if(window.SonicSF&&ty // (PIANO_ROLL/section/audio tab) đang play → stop playback của tab đó. // Tab mới hoạt động bình thường; MAIN giữ hành vi cũ (mở piano-roll lúc // main play → main tiếp tục — handleEditMidiInTab đã xử lý). -if(prevTab!==activeTab){const prevSub=subTabsRef.current.find(s=>s.id===prevTab);if(prevSub&&prevSub.isPlaying){try{stopAllPlayback();}catch(e){}setSubTabs(prev=>prev.map(s=>s.id===prevTab?{...s,isPlaying:false}:s));}}},[activeTab]);const[selectionCleared,setSelectionCleared]=useState(false);// LOOP_EDITOR_2.md §4.2 +if(prevTab!==activeTab){const prevSub=subTabsRef.current.find(s=>s.id===prevTab);if(prevSub&&prevSub.isPlaying){try{stopAllPlayback();}catch(e){}setSubTabs(prev=>prev.map(s=>s.id===prevTab?{...s,isPlaying:false}:s));// Resume main/session play khi rời PIANO ROLL tab đã play (Space): +// play piano roll → stopAllPlayback dừng main → quay lại MAIN/SECTION +// bị CÂM. Resume startTrackPlayback(offset + elapsed thật) — chỉ khi +// trước đó mở tab lúc main đang play (mainResumeRef đã set). +const _resume=mainResumeRef.current;mainResumeRef.current=null;if(_resume&&prevSub.type==='PIANO_ROLL'&&(activeTab==='main'||activeTab&&activeTab.startsWith('session_'))){try{const _elapsed=getAudioContext().currentTime-_resume.audioTime;const _resumeAt=_resume.offset+_elapsed;console.log('[Resume] main play từ',_resumeAt.toFixed(2),'s (offset',_resume.offset.toFixed(2),'+ elapsed',_elapsed.toFixed(2),')');stopAllPlayback();startOffsetTimeRef.current=_resumeAt;startAudioTimeRef.current=getAudioContext().currentTime;startTrackPlayback(_resumeAt);setIsPlaying(true);}catch(e){console.warn('[Resume] error:',e);}}}else if(prevSub&&prevSub.type==='PIANO_ROLL'){// Rời PIANO ROLL tab KHÔNG play: main/section play ngầm vẫn chạy (âm +// tiếp tục tự nhiên — không cần resume) → clear resume point. +// ⚠️ KHÔNG clear khi prevTab = main/session (đang DI CHUYỂN VÀO piano +// roll — mainResumeRef vừa set bởi handleEditMidiInTab — clear nhầm ở +// đây làm resume chết → quay lại main/section bị CÂM). +mainResumeRef.current=null;}}},[activeTab]);const[selectionCleared,setSelectionCleared]=useState(false);// LOOP_EDITOR_2.md §4.2 const midiVuActivityRef=useRef({});const triggerMidiVuActivity=(trackId,velocity)=>{if(!trackId)return;const velFactor=typeof velocity==='number'?velocity>1?velocity/127:velocity:0.8;const peak=Math.min(1.0,Math.max(0.15,velFactor));midiVuActivityRef.current[trackId]=peak;};window.triggerMidiVuActivity=triggerMidiVuActivity;// ── Temp Edit Tab (LOOP_EDITOR_2.md §1.2 Sub Tab) ── const[tempTabActive,setTempTabActive]=useState(false);const[tempTabBuffer,setTempTabBuffer]=useState(null);const[tempTabTrackId,setTempTabTrackId]=useState(null);const[tempTabOrigStart,setTempTabOrigStart]=useState(0);const[tempTabOrigEnd,setTempTabOrigEnd]=useState(0);const tempTabCanvasRef=useRef(null);// Effect parameters for temp tab const[tempTabEffects,setTempTabEffects]=useState({reverse:false,gainDb:0,fadeInMs:0,fadeOutMs:0});// ── Auth / User State ── @@ -730,7 +739,11 @@ useEffect(()=>{setTimeout(()=>{if(window.lucide){window.lucide.createIcons();}}, const startColResize=e=>{e.preventDefault();const startX=e.clientX;const startWidth=rightSidebarWidth;const onMove=ev=>{const deltaX=startX-ev.clientX;const newWidth=Math.max(200,Math.min(600,startWidth+deltaX));setRightSidebarWidth(newWidth);};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);};// ── TCP Resizer ── const startTcpResize=e=>{e.preventDefault();const startX=e.clientX;const startWidth=tcpWidth;const onMove=ev=>{const deltaX=ev.clientX-startX;const newWidth=Math.max(280,Math.min(600,startWidth+deltaX));setTcpWidth(newWidth);};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);};// ── Right Sidebar Row Resizer (Media Explorer / AI Panel) ── const startRowResize=e=>{e.preventDefault();const startY=e.clientY;const startHeight=mediaExplorerHeight;const sidebarEl=document.getElementById('right-sidebar');const sidebarHeight=sidebarEl?sidebarEl.getBoundingClientRect().height:400;const onMove=ev=>{const deltaY=startY-ev.clientY;const pct=(startHeight/100*sidebarHeight+deltaY)/sidebarHeight*100;const newPct=Math.max(20,Math.min(80,pct));setMediaExplorerHeight(newPct);};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);};const rulerRef=useRef(null);const activeSourcesRef=useRef([]);const activeTrackNodesRef=useRef({});// { [trackId]: { gainNode, pannerNode } } -const startOffsetTimeRef=useRef(0);const startBufferOffsetRef=useRef(0);const startAudioTimeRef=useRef(0);const animationFrameIdRef=useRef(null);const toastTimeoutRef=useRef(null);const rulerDragStartRef=useRef(null);const rulerAnchorRef=useRef(null);const isDraggingRulerRef=useRef(false);const subTabDragStartRef=useRef(null);const isDraggingSubTabRef=useRef(false);const handlePlayPauseRef=useRef(null);const currentTimeRef=useRef(currentTime);currentTimeRef.current=currentTime;// ── Keyboard Shortcuts ── +const startOffsetTimeRef=useRef(0);const startBufferOffsetRef=useRef(0);const startAudioTimeRef=useRef(0);const animationFrameIdRef=useRef(null);const toastTimeoutRef=useRef(null);const rulerDragStartRef=useRef(null);const rulerAnchorRef=useRef(null);const isDraggingRulerRef=useRef(false);const subTabDragStartRef=useRef(null);const isDraggingSubTabRef=useRef(false);const handlePlayPauseRef=useRef(null);const currentTimeRef=useRef(currentTime);// Resume main/session play khi rời PIANO ROLL tab: mở piano roll lúc main +// đang play → bấm Space (play piano roll) → stopAllPlayback dừng main → +// quay lại MAIN/SECTION → CÂM. Lưu {offset, audioTime} lúc mở tab → khi +// rời tab resume startTrackPlayback(offset + elapsed thật). +const mainResumeRef=useRef(null);currentTimeRef.current=currentTime;// ── Keyboard Shortcuts ── const handleUndoRef=useRef(handleUndo);const handleRedoRef=useRef(handleRedo);handleUndoRef.current=handleUndo;handleRedoRef.current=handleRedo;const handleDeleteSelectedItemsRef=useRef(()=>{});handleDeleteSelectedItemsRef.current=idsToDelete=>{const count=idsToDelete.size;setSelectedItemIds(new Set());updateActiveTracks(prev=>prev.map(t=>{let changed=false;const sections=(t.sections||[]).filter(s=>{if(idsToDelete.has(s.id)){changed=true;return false;}return true;});const midiItems=(t.midiItems||[]).filter(m=>{if(idsToDelete.has(m.id)){changed=true;return false;}return true;});let clips=t.clips||[];const hasVirtualClip=!t.clips||t.clips.length===0;if(hasVirtualClip&&t.buffer){const canonical='default_'+t.id;if(idsToDelete.has(canonical)){changed=true;return{...t,sections,midiItems,clips:[],buffer:null,startTime:0};}}const updatedClips=clips.filter(c=>{const canonical=c.id==='default'?'default_'+t.id:c.id;if(idsToDelete.has(canonical)){changed=true;return false;}return true;});if(!changed&§ions.length===(t.sections||[]).length&&midiItems.length===(t.midiItems||[]).length){return t;}return{...t,sections,midiItems,clips:updatedClips,buffer:updatedClips.length>0?updatedClips[0].buffer:null,startTime:updatedClips.length>0?updatedClips[0].startTime:0,name:updatedClips.length>0?updatedClips[0].name:t.name};}));showToast(count===1?'Đã xóa 1 item.':`Đã xóa ${count} items.`,'info');};const selectedClipIdRef=useRef(null);selectedClipIdRef.current=selectedClipId;const selectedTrackIdRef=useRef(selectedTrackId);selectedTrackIdRef.current=selectedTrackId;const handleDeleteTrackRef=useRef(()=>{});const handleSplitTrackRef=useRef(()=>{});const handleRecordClickRef=useRef(()=>{});const activeTabRef=useRef(activeTab);activeTabRef.current=activeTab;const activePlaybackSpeedRef=useRef(1.0);const subTabsRef=useRef(subTabs);subTabsRef.current=subTabs;const subTabSelectedNodeTimeRef=useRef(null);subTabSelectedNodeTimeRef.current=subTabSelectedNodeTime;const handleSubTabNormalize=tabId=>{setSubTabs(prev=>prev.map(st=>{if(st.id!==tabId||!st.buffer)return st;const ctx=getAudioContext();const data=st.buffer.getChannelData(0);const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):0;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):st.buffer.duration;const startSample=Math.floor(left*st.buffer.sampleRate);const endSample=Math.floor(right*st.buffer.sampleRate);let maxVal=0;for(let i=startSample;imaxVal)maxVal=abs;}if(maxVal===0)return st;const scale=1.0/maxVal;const clonedBuffer=ctx.createBuffer(1,Math.max(1,data.length),st.buffer.sampleRate);const clonedData=clonedBuffer.getChannelData(0);clonedData.set(data);for(let i=startSample;i{setSubTabs(prev=>prev.map(st=>{if(st.id!==tabId||!st.buffer)return st;const ctx=getAudioContext();const data=st.buffer.getChannelData(0);const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):0;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):st.buffer.duration;const startSample=Math.floor(left*st.buffer.sampleRate);const endSample=Math.floor(right*st.buffer.sampleRate);const scale=Math.pow(10,gainDb/20);const clonedBuffer=ctx.createBuffer(1,Math.max(1,data.length),st.buffer.sampleRate);const clonedData=clonedBuffer.getChannelData(0);clonedData.set(data);for(let i=startSample;i{setSubTabs(prev=>prev.map(st=>{if(st.id!==tabId||!st.buffer)return st;const ctx=getAudioContext();const data=st.buffer.getChannelData(0);const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):0;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):st.buffer.duration;const startSample=Math.floor(left*st.buffer.sampleRate);const endSample=Math.floor(right*st.buffer.sampleRate);const durationSamples=endSample-startSample;if(durationSamples<=0)return st;const clonedBuffer=ctx.createBuffer(1,Math.max(1,data.length),st.buffer.sampleRate);const clonedData=clonedBuffer.getChannelData(0);clonedData.set(data);if(type==='in'){for(let i=0;i{const st=subTabsRef.current.find(s=>s.id===tabId);if(!st||!st.buffer)return;const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):null;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):null;if(left===null||right===null||left===right){showToast('Vui lòng chọn vùng để Cut.','warning');return;}const ctx=getAudioContext();const sr=st.buffer.sampleRate;const data=st.buffer.getChannelData(0);const startSample=Math.floor(left*sr);const endSample=Math.floor(right*sr);const len=Math.max(1,endSample-startSample);// guard 0 frames → createBuffer crash const cutBuffer=ctx.createBuffer(1,len,sr);cutBuffer.copyToChannel(data.subarray(startSample,endSample),0);clipboardRef.current={buffer:cutBuffer,name:'Subtab Clip'};const newBuffer=ctx.createBuffer(1,Math.max(1,data.length-len),sr);const newData=newBuffer.getChannelData(0);let idx=0;for(let i=0;iprev.map(s=>s.id===tabId?{...s,buffer:newBuffer,currentTime:left,selectionStart:null,selectionEnd:null}:s));showToast('Đã Cut vùng chọn.','success');};const handleSubTabCopy=tabId=>{const st=subTabsRef.current.find(s=>s.id===tabId);if(!st||!st.buffer)return;const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):null;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):null;if(left===null||right===null||left===right){showToast('Vui lòng chọn vùng để Copy.','warning');return;}const ctx=getAudioContext();const sr=st.buffer.sampleRate;const data=st.buffer.getChannelData(0);const startSample=Math.floor(left*sr);const endSample=Math.floor(right*sr);const len=Math.max(1,endSample-startSample);// guard 0 frames → createBuffer crash const copyBuffer=ctx.createBuffer(1,len,sr);copyBuffer.copyToChannel(data.subarray(startSample,endSample),0);clipboardRef.current={buffer:copyBuffer,name:'Subtab Clip',sampleRate:sr,channels:1,speed:1.0};showToast('Đã Copy vùng chọn.','success');};const handleSubTabPaste=tabId=>{const st=subTabsRef.current.find(s=>s.id===tabId);if(!st)return;// Check if Piano Roll Tab @@ -753,7 +766,10 @@ const existing=subTabs.find(s=>s.trackId===trackId&&s.startTime===selLeft&&s.end // mở PIANO ROLL tab KHÔNG được dừng âm (user: dblclick mở tab lúc main // play → âm main phải tiếp tục). Nhánh play sub-tab tự stopAllPlayback // trước khi play nên không xung đột route. -if(!isPlaying)stopAllPlayback();const ctx=getAudioContext();const silentBuffer=ctx.createBuffer(1,128,ctx.sampleRate);const existing=subTabs.find(s=>s.type==='PIANO_ROLL'&&s.target_id===midiItemId);if(existing){setSubTabs(prev=>prev.map(s=>s.id===existing.id?{...s,buffer:s.buffer||silentBuffer,instrumentProgram:track.instrumentProgram!==undefined?track.instrumentProgram:track.synth_engine?track.synth_engine.soundfont_program:undefined,instrumentName:track.instrumentName,instrumentId:track.instrumentId,synth_engine:track.synth_engine}:s));setActiveTab(existing.id);showToast('Piano Roll cho nốt MIDI đã được mở.','info');return;}const tabId='midi_'+Date.now();const tabLabel=`Piano Roll: ${midiItem.name||'MIDI'}`;const newTab={id:tabId,label:tabLabel,type:'PIANO_ROLL',trackId:trackId,target_id:midiItemId,parent_tab_id:activeTab==='main'?null:activeTab,notes:midiItem.notes||[],duration:midiItem.duration||4,buffer:silentBuffer,instrumentProgram:track.instrumentProgram!==undefined?track.instrumentProgram:track.synth_engine?track.synth_engine.soundfont_program:undefined,instrumentName:track.instrumentName,instrumentId:track.instrumentId,synth_engine:track.synth_engine,currentTime:0,isPlaying:false,isLooping:true,selectionStart:null,selectionEnd:null,viewport_start_bar:0.0,viewport_bar_width:8.0,scroll_y_pitch:60,snap_resolution:snapValue||"1/16",note_selection:[]};setSubTabs(prev=>[...prev,newTab]);setActiveTab(tabId);// KHÔNG loadSoundFont ở đây (kể cả nền): sfload SF mới bất đồng bộ làm +if(isPlaying){// Lưu điểm resume: nếu user bấm Space trong piano roll (play piano roll +// → stopAllPlayback dừng main) → khi quay lại MAIN/SECTION resume main +// từ đúng vị trí âm (offset + elapsed thật) — hết câm. +mainResumeRef.current={offset:startOffsetTimeRef.current,audioTime:getAudioContext().currentTime};}else{mainResumeRef.current=null;stopAllPlayback();}const ctx=getAudioContext();const silentBuffer=ctx.createBuffer(1,128,ctx.sampleRate);const existing=subTabs.find(s=>s.type==='PIANO_ROLL'&&s.target_id===midiItemId);if(existing){setSubTabs(prev=>prev.map(s=>s.id===existing.id?{...s,buffer:s.buffer||silentBuffer,instrumentProgram:track.instrumentProgram!==undefined?track.instrumentProgram:track.synth_engine?track.synth_engine.soundfont_program:undefined,instrumentName:track.instrumentName,instrumentId:track.instrumentId,synth_engine:track.synth_engine}:s));setActiveTab(existing.id);showToast('Piano Roll cho nốt MIDI đã được mở.','info');return;}const tabId='midi_'+Date.now();const tabLabel=`Piano Roll: ${midiItem.name||'MIDI'}`;const newTab={id:tabId,label:tabLabel,type:'PIANO_ROLL',trackId:trackId,target_id:midiItemId,parent_tab_id:activeTab==='main'?null:activeTab,notes:midiItem.notes||[],duration:midiItem.duration||4,buffer:silentBuffer,instrumentProgram:track.instrumentProgram!==undefined?track.instrumentProgram:track.synth_engine?track.synth_engine.soundfont_program:undefined,instrumentName:track.instrumentName,instrumentId:track.instrumentId,synth_engine:track.synth_engine,currentTime:0,isPlaying:false,isLooping:true,selectionStart:null,selectionEnd:null,viewport_start_bar:0.0,viewport_bar_width:8.0,scroll_y_pitch:60,snap_resolution:snapValue||"1/16",note_selection:[]};setSubTabs(prev=>[...prev,newTab]);setActiveTab(tabId);// KHÔNG loadSoundFont ở đây (kể cả nền): sfload SF mới bất đồng bộ làm // WASM heap 256MB đầy → FluidSynth stall → CÂM TOÀN CỤC (mọi âm thanh // chết sau khi mở tab). playNote TỰ load + retry đúng lúc note cần. };const handleUpdateMidiNotes=(tabId,notes)=>{setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,notes:notes,isDirty:true}:s));};const handleSaveMidiNotes=(tabId,trackId,midiItemId,updatedNotes)=>{const pianoRollTab=subTabs.find(s=>s.id===tabId);const parentTabId=pianoRollTab?pianoRollTab.parent_tab_id:null;if(parentTabId&&parentTabId.startsWith('session_')){setSessionTabs(prev=>prev.map(s=>{if(s.id!==parentTabId)return s;return{...s,tracks:s.tracks.map(t=>{if(t.id!==trackId)return t;return{...t,midiItems:(t.midiItems||[]).map(m=>{if(m.id!==midiItemId)return m;return{...m,notes:updatedNotes};})};})};}));}else{setTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,midiItems:(t.midiItems||[]).map(m=>{if(m.id!==midiItemId)return m;return{...m,notes:updatedNotes};})};}));}setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,isDirty:false}:s));showToast('Đã lưu các chỉnh sửa nốt MIDI vào item cha!','success');};const handleSaveSectionTab=async tabId=>{const tab=sessionTabs.find(s=>s.id===tabId);if(!tab)return;const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;const secondsPerBar=secondsPerBeat*4;// Upload buffer-only clips (chưa có serverFileId — upload sớm thất bại/ @@ -898,7 +914,15 @@ const secondsPerBeat=60.0/(parseInt(bpmRef.current)||120);// Metronome Click Sch while(true){const beatNum=nextMetronomeBeatRef.current;const elapsedBeats=beatNum-recordingStartTimeRef.current/secondsPerBeat;const beatTime=startAudioTimeRef.current+elapsedBeats*secondsPerBeat;if(beatTime200){lastTempCompileTimeRef.current=now;// Audio preview for(let trackId in recordingPCMDataRef.current){const data=recordingPCMDataRef.current[trackId];if(data&&data.length>0){const tempBuf=audioCtx.createBuffer(1,data.length,audioCtx.sampleRate);tempBuf.getChannelData(0).set(data);setRecTempAudioBuffer(tempBuf);}}// MIDI preview -let combinedNotes=[];const armedTracks=activeTracksRef.current.filter(t=>t.isArmed);for(let track of armedTracks){const midiRec=activeMIDIRecordersRef.current[track.id];if(midiRec){const currentBeat=(audioCtx.currentTime-midiRec.recStartAudioTime-midiRec.latencyCompSec)/secondsPerBeat;const notes=[...midiRec.recordedNotes,...Array.from(midiRec.activeNotes.values()).map(n=>({...n,duration_beats:currentBeat-n.start_beat}))];combinedNotes=combinedNotes.concat(notes);}}if(combinedNotes.length>0||armedTracks.some(t=>activeMIDIRecordersRef.current[t.id])){setRecTempMidiNotes(combinedNotes);setCanvasRedrawCount(n=>n+1);}}}const isSubTab=subTabsRef.current.some(sub=>sub.id===activeTabRef.current);if(isSubTab){const st=subTabsRef.current.find(s=>s.id===activeTabRef.current);if(!st||!st.isPlaying||!st.buffer)return;const context=getAudioContext();const speedFactor=st.speed||1.0;const elapsed=context.currentTime-startAudioTimeRef.current;const wallTime=startOffsetTimeRef.current+elapsed;const bufferPos=startBufferOffsetRef.current+elapsed*speedFactor;// Loop sub-tab selection (bufferPos is buffer-time) +let combinedNotes=[];const armedTracks=activeTracksRef.current.filter(t=>t.isArmed);for(let track of armedTracks){const midiRec=activeMIDIRecordersRef.current[track.id];if(midiRec){const currentBeat=(audioCtx.currentTime-midiRec.recStartAudioTime-midiRec.latencyCompSec)/secondsPerBeat;const notes=[...midiRec.recordedNotes,...Array.from(midiRec.activeNotes.values()).map(n=>({...n,duration_beats:currentBeat-n.start_beat}))];combinedNotes=combinedNotes.concat(notes);}}if(combinedNotes.length>0||armedTracks.some(t=>activeMIDIRecordersRef.current[t.id])){setRecTempMidiNotes(combinedNotes);setCanvasRedrawCount(n=>n+1);}}}const isSubTab=subTabsRef.current.some(sub=>sub.id===activeTabRef.current);if(isSubTab){const st=subTabsRef.current.find(s=>s.id===activeTabRef.current);if(!st||!st.buffer)return;if(!st.isPlaying){// PIANO ROLL / sub-tab KHÔNG play (mới mở, đang edit): playhead của +// MAIN/SECTION KHÔNG được tiếp tục di chuyển (yêu cầu user) — KHÔNG +// setCurrentTime. Loop phải TIẾP TỤC (rAF) để khi quay lại tab playhead +// không nhảy vọt sai lệch. ⚠️ KHÔNG check projectEnd ở đây: projectEnd +// tính theo activeTab — piano roll tab (sub-tab) → projectEnd = 1s → +// updatedTime (elapsed thật) >= 1 → stopAllPlayback NGAY → CÂM + panic +// notes. Âm main đã schedule hết từ startTrackPlayback — tự hết đúng +// lúc; khi quay lại MAIN nhánh main tự stop tại projectEnd thật. +animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}const context=getAudioContext();const speedFactor=st.speed||1.0;const elapsed=context.currentTime-startAudioTimeRef.current;const wallTime=startOffsetTimeRef.current+elapsed;const bufferPos=startBufferOffsetRef.current+elapsed*speedFactor;// Loop sub-tab selection (bufferPos is buffer-time) if(st.selectionStart!==null&&st.selectionEnd!==null&&st.selectionStart!==st.selectionEnd&&st.isLooping){const start=Math.min(st.selectionStart,st.selectionEnd);const end=Math.max(st.selectionStart,st.selectionEnd);if(bufferPos>=end){stopAllPlayback();setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,buffer:s.buffer||getAudioContext().createBuffer(1,128,getAudioContext().sampleRate),currentTime:start,isPlaying:true}:s));schedulePianoRollMidi(st,start);startSubTabPlayback(st,start);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}}const effectiveDuration=st.type==='PIANO_ROLL'?(()=>{const notes=st.notes||[];const bpmVal=parseInt(bpmRef?.current||bpm)||120;const beatSec=60.0/bpmVal;if(recordingStateRef.current==='RECORDING')return 600.0;// 10 min during recording let maxEnd=0;notes.forEach(n=>{const end=(n.start_beat||0)+(n.duration_beats||1);if(end>maxEnd)maxEnd=end;});return Math.max(maxEnd*beatSec,16*beatSec*4)+1.0;})():st.buffer.duration;if(bufferPos>=effectiveDuration){stopAllPlayback();if(st.isLooping){setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,buffer:s.buffer||getAudioContext().createBuffer(1,128,getAudioContext().sampleRate),currentTime:0,isPlaying:true}:s));// Loop lại: phải schedule notes MỚI (âm piano roll mất ở lần loop 2 // nếu chỉ chạy silent buffer). diff --git a/app/templates/index.html b/app/templates/index.html index e98a6de..d83cfd3 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -24,7 +24,7 @@ - +