From cca58c4893b545f599117ca18870cfcba2f78fa9 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Sun, 16 Aug 2026 17:47:31 +0700 Subject: [PATCH] =?UTF-8?q?feat(piano-roll):=20v=C3=A0o=20tab=20l=C3=BAc?= =?UTF-8?q?=20main=20play=20->=20ch=E1=BB=89=20play=20MIDI=20notes=20track?= =?UTF-8?q?=20=C4=91ang=20edit,=20c=C3=A1c=20track=20kh=C3=A1c=20mute?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useEffect [activeTab] mới: activeTab là PIANO_ROLL và main/section đang play -> stopAllPlayback + restart play CHỈ track piano roll tại vị trí tương ứng trong item (mainPos - item.startTime). mainResumeRef giữ để quay lại main/section resume đúng vị trí (nhánh resume cũ không đổi). --- app/static/js/app.jsx | 31 +++++++++++++++++++++++++++---- app/static/js/app.precompiled.js | 15 ++++++++++----- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 56e645d..cd922e5 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -16943,6 +16943,29 @@ const App = () => { console.log('[SectionTab] play CHỈ section items tại local', _pt.toFixed(2), '(secStart', _secStart.toFixed(2) + ')'); } catch (e) { console.warn('[SectionTab] restart error:', e); } } + // Vào PIANO ROLL lúc MAIN/SECTION đang play → CHỈ play MIDI notes CỦA + // TRACK đang edit, TẮT âm MỌI track khác (user: vào piano roll phải play + // only midi notes đang active). Dừng main → play lại CHỈ track piano roll + // tại vị trí TƯƠNG ỨNG trong item (mainPos − item.startTime). Rời tab → + // nhánh resume (dưới) về main/section đúng vị trí (mainResumeRef vẫn giữ). + const _prTabNew = activeTab && activeTab.startsWith('midi_') ? subTabsRef.current.find(s => s.id === activeTab && s.type === 'PIANO_ROLL') : null; + if (_prTabNew && !_prTabNew.isPlaying && isPlaying) { + try { + const _ctx = getAudioContext(); + const _mainPos = startOffsetTimeRef.current + (_ctx.currentTime - startAudioTimeRef.current); + const _itemInfo = getPianoRollItemInfo(_prTabNew); + const _prPos = _itemInfo ? Math.max(0, _mainPos - (_itemInfo.startTime || 0)) : (_prTabNew.currentTime || 0); + stopAllPlayback(); + startOffsetTimeRef.current = _prPos; + startAudioTimeRef.current = _ctx.currentTime; + startBufferOffsetRef.current = _prPos * (_prTabNew.speed || 1.0); + schedulePianoRollMidi(_prTabNew, _prPos); + startSubTabPlayback(_prTabNew, _prPos); + setSubTabs(prev => prev.map(s => s.id === _prTabNew.id ? { ...s, isPlaying: true, currentTime: _prPos } : s)); + animationFrameIdRef.current = requestAnimationFrame(updatePlayhead); + console.log('[PianoRollTab] play CHỈ track', _prTabNew.trackId, 'tại local', _prPos.toFixed(2), '(mainPos', _mainPos.toFixed(2), 'itemStart', _itemInfo ? _itemInfo.startTime.toFixed(2) : '?', ')'); + } catch (e) { console.warn('[PianoRollTab] restart error:', e); } + } const prevSub = subTabsRef.current.find(s => s.id === prevTab); if (prevSub && prevSub.isPlaying) { // Chuyển sang SUB-TAB KHÁC (piano roll khác/audio tab) → STOP âm tab @@ -18950,10 +18973,10 @@ const App = () => { const midiItem = (track.midiItems || []).find(m => m.id === midiItemId); if (!midiItem) return; - // Clean stop playback ONLY khi KHÔNG đang play main — nếu main đang play, - // 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. + // Main đang play → CHỈ lưu điểm resume; useEffect [activeTab] tự + // stopAllPlayback + play CHỈ track piano roll khi vào tab (user: piano roll + // play only midi notes đang active — mọi track khác mute). mainResumeRef + // giữ để quay lại MAIN/SECTION resume đúng vị trí. 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 diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 045e81f..10c6a69 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -975,7 +975,12 @@ if(prevTab!==activeTab){// Vào SECTION-TAB lúc MAIN đang play → CHỈ play // activeTracks = section clones (activeTab mới → activeTracksRef mới). if(prevTab==='main'&&activeTab&&activeTab.startsWith('session_')&&isPlaying){try{const _ctx=getAudioContext();// Vị trí play → LOCAL của section tab (trừ secStart — vị trí section // trên main): section tab items dùng time local (0-based) -let _secStart=0;const _tab=sessionTabsRef.current.find(st=>st.id===activeTab);if(_tab&&_tab.sectionId){(tracks||[]).forEach(_t=>(_t.sections||[]).forEach(_s=>{if(_s.sectionId===_tab.sectionId||_s.id===_tab.sectionId)_secStart=_s.start||0;}));}const _pt=Math.max(0,startOffsetTimeRef.current+(_ctx.currentTime-startAudioTimeRef.current)-_secStart);stopAllPlayback();startOffsetTimeRef.current=_pt;startAudioTimeRef.current=_ctx.currentTime;startTrackPlayback(_pt);setIsPlaying(true);console.log('[SectionTab] play CHỈ section items tại local',_pt.toFixed(2),'(secStart',_secStart.toFixed(2)+')');}catch(e){console.warn('[SectionTab] restart error:',e);}}const prevSub=subTabsRef.current.find(s=>s.id===prevTab);if(prevSub&&prevSub.isPlaying){// Chuyển sang SUB-TAB KHÁC (piano roll khác/audio tab) → STOP âm tab +let _secStart=0;const _tab=sessionTabsRef.current.find(st=>st.id===activeTab);if(_tab&&_tab.sectionId){(tracks||[]).forEach(_t=>(_t.sections||[]).forEach(_s=>{if(_s.sectionId===_tab.sectionId||_s.id===_tab.sectionId)_secStart=_s.start||0;}));}const _pt=Math.max(0,startOffsetTimeRef.current+(_ctx.currentTime-startAudioTimeRef.current)-_secStart);stopAllPlayback();startOffsetTimeRef.current=_pt;startAudioTimeRef.current=_ctx.currentTime;startTrackPlayback(_pt);setIsPlaying(true);console.log('[SectionTab] play CHỈ section items tại local',_pt.toFixed(2),'(secStart',_secStart.toFixed(2)+')');}catch(e){console.warn('[SectionTab] restart error:',e);}}// Vào PIANO ROLL lúc MAIN/SECTION đang play → CHỈ play MIDI notes CỦA +// TRACK đang edit, TẮT âm MỌI track khác (user: vào piano roll phải play +// only midi notes đang active). Dừng main → play lại CHỈ track piano roll +// tại vị trí TƯƠNG ỨNG trong item (mainPos − item.startTime). Rời tab → +// nhánh resume (dưới) về main/section đúng vị trí (mainResumeRef vẫn giữ). +const _prTabNew=activeTab&&activeTab.startsWith('midi_')?subTabsRef.current.find(s=>s.id===activeTab&&s.type==='PIANO_ROLL'):null;if(_prTabNew&&!_prTabNew.isPlaying&&isPlaying){try{const _ctx=getAudioContext();const _mainPos=startOffsetTimeRef.current+(_ctx.currentTime-startAudioTimeRef.current);const _itemInfo=getPianoRollItemInfo(_prTabNew);const _prPos=_itemInfo?Math.max(0,_mainPos-(_itemInfo.startTime||0)):_prTabNew.currentTime||0;stopAllPlayback();startOffsetTimeRef.current=_prPos;startAudioTimeRef.current=_ctx.currentTime;startBufferOffsetRef.current=_prPos*(_prTabNew.speed||1.0);schedulePianoRollMidi(_prTabNew,_prPos);startSubTabPlayback(_prTabNew,_prPos);setSubTabs(prev=>prev.map(s=>s.id===_prTabNew.id?{...s,isPlaying:true,currentTime:_prPos}:s));animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);console.log('[PianoRollTab] play CHỈ track',_prTabNew.trackId,'tại local',_prPos.toFixed(2),'(mainPos',_mainPos.toFixed(2),'itemStart',_itemInfo?_itemInfo.startTime.toFixed(2):'?',')');}catch(e){console.warn('[PianoRollTab] restart error:',e);}}const prevSub=subTabsRef.current.find(s=>s.id===prevTab);if(prevSub&&prevSub.isPlaying){// Chuyển sang SUB-TAB KHÁC (piano roll khác/audio tab) → STOP âm tab // cũ — play ĐÚNG nội dung TỪNG TAB (user 07:30: piano roll tab 1 → // tab 2 — không được nghe âm tab 1). Về MAIN/SECTION → giữ luật cũ // (04:15+ — âm tiếp/resume). @@ -1136,10 +1141,10 @@ handleSaveSectionTabRef.current(curTab);}showToast('Đã lưu dự án + các ta const st=subTabsRef.current.find(s=>s.id===curTab);if(st){if(st.type==='PIANO_ROLL'){handleSaveMidiNotes(st.id,st.trackId,st.target_id,st.notes||[]);showToast('Đã lưu Piano Roll','success');}else if(st.type==='SECTION'){handleSaveSectionTab(st.id);showToast('Đã lưu Section Tab','success');}else if(st.buffer){const subTrack=activeTracksRef.current.find(t=>t.id===st.trackId);if(subTrack){updateActiveTracks(prev=>prev.map(t=>t.id===st.trackId?{...t,buffer:st.buffer}:t));}showToast('Đã lưu Audio Tab','success');}}}return;}if(!ctrl&&!alt&&e.key==='s'){e.preventDefault();handleSplitTrackRef.current(selectedTrackIdRef.current);return;}if(e.key==='F7'){e.preventDefault();e.stopPropagation();window.__toggleMixerRef();return;}if(e.key==='F6'){e.preventDefault();e.stopPropagation();window.__toggleMediaExplorerRef();return;}};window.addEventListener('keydown',handler,{capture:true});return()=>window.removeEventListener('keydown',handler,{capture:true});},[]);// ── Temp Tab: draw isolated waveform ── useEffect(()=>{if(!tempTabActive||!tempTabBuffer||!tempTabCanvasRef.current)return;const canvas=tempTabCanvasRef.current;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const rect=canvas.getBoundingClientRect();canvas.width=rect.width*dpr;canvas.height=rect.height*dpr;ctx.scale(dpr,dpr);const w=rect.width;const h=rect.height;ctx.fillStyle='#181818';ctx.fillRect(0,0,w,h);const data=tempTabBuffer.getChannelData(0);const sr=tempTabBuffer.sampleRate;const totalSamples=data.length;if(totalSamples===0)return;ctx.strokeStyle='#6ee7b7';ctx.lineWidth=1;for(let px=0;pxmaxVal)maxVal=abs;}const mid=h/2;const peakHeight=maxVal*(h*0.4);ctx.beginPath();ctx.moveTo(px,mid-peakHeight);ctx.lineTo(px,mid+peakHeight);ctx.stroke();}},[tempTabActive,tempTabBuffer]);// ── Sub Tab: open as new tab instead of modal (LOOP_EDITOR_2.md §1) ── const openTempTab=()=>{const useLocal=selectionMode==='local'&&localSelectionTrackId;const trackId=useLocal?localSelectionTrackId:selectedTrackId;const t=tracks.find(x=>x.id===trackId);if(!t||!t.buffer){showToast('Vui lòng chọn track có dữ liệu âm thanh.','warning');return;}if(selLeft===null||selRight===null||selRight<=selLeft){showToast('Vui lòng chọn một khoảng thời gian trên sóng âm.','warning');return;}// Check if a subtab for this track+range already exists -const existing=subTabs.find(s=>s.trackId===trackId&&s.startTime===selLeft&&s.endTime===selRight);if(existing){setActiveTab(existing.id);showToast(`Sub Tab already open.`,'info');return;}const sr=t.buffer.sampleRate;const trackStart=t.startTime||0;const relSelLeft=Math.max(0,selLeft-trackStart);const relSelRight=Math.max(0,selRight-trackStart);const startSample=Math.max(0,Math.floor(relSelLeft*sr));const endSample=Math.min(t.buffer.length,Math.floor(relSelRight*sr));const len=endSample-startSample;if(len<100){showToast('Khoảng chọn quá ngắn.','warning');return;}const ctx=getAudioContext();const numChannels=t.buffer.numberOfChannels||1;const subBuffer=ctx.createBuffer(numChannels,len,sr);for(let c=0;c[...prev,{id:tabId,label:tabLabel,trackId:trackId,startTime:selLeft,endTime:selRight,buffer:subBuffer,channelInfo:subChannelInfo,effects:{reverse:false,gainDb:0,fadeInMs:0,fadeOutMs:0,normalizeDb:0,pitch:0,speedStretch:100},currentTime:0,selectionStart:null,selectionEnd:null,isPlaying:false,speed:1.0,volumeNodes:[],panningNodes:[],fadeInLen:0,fadeOutLen:0,graphMode:null,isLooping:false,loopCount:0}]);setActiveTab(tabId);};const handleEditClipInSubTab=(trackId,clipId)=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return;const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default_'+track.id,buffer:track.buffer,startTime:track.startTime||0,name:track.name}]:[];const clip=clips.find(c=>c.id===clipId||clipId==='default'&&c.id==='default_'+trackId);if(!clip||!clip.buffer)return;const resolvedClipId=clip.id==='default'?'default_'+trackId:clip.id;const existing=subTabs.find(s=>s.clipId===resolvedClipId&&s.trackId===trackId);if(existing){setActiveTab(existing.id);showToast(`Sub Tab for "${clip.name}" already open.`,'info');return;}const sr=clip.buffer.sampleRate;const len=clip.buffer.length;const numChannels=clip.buffer.numberOfChannels||1;const ctx=getAudioContext();const subBuffer=ctx.createBuffer(numChannels,len,sr);for(let c=0;c[...prev,{id:tabId,label:tabLabel,trackId:trackId,clipId:resolvedClipId,startTime:clip.startTime,endTime:clip.startTime+clip.buffer.duration,buffer:subBuffer,channelInfo:subChannelInfo,effects:{reverse:false,gainDb:0,fadeInMs:0,fadeOutMs:0,normalizeDb:0,pitch:0,speedStretch:100},currentTime:0,selectionStart:null,selectionEnd:null,isPlaying:false,speed:1.0,volumeNodes:[],panningNodes:[],fadeInLen:0,fadeOutLen:0,graphMode:null,isLooping:false,loopCount:0}]);setActiveTab(tabId);};const handleEditMidiInTab=(trackId,midiItemId)=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return;const midiItem=(track.midiItems||[]).find(m=>m.id===midiItemId);if(!midiItem)return;// Clean stop playback ONLY khi KHÔNG đang play main — nếu main đang play, -// 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. +const existing=subTabs.find(s=>s.trackId===trackId&&s.startTime===selLeft&&s.endTime===selRight);if(existing){setActiveTab(existing.id);showToast(`Sub Tab already open.`,'info');return;}const sr=t.buffer.sampleRate;const trackStart=t.startTime||0;const relSelLeft=Math.max(0,selLeft-trackStart);const relSelRight=Math.max(0,selRight-trackStart);const startSample=Math.max(0,Math.floor(relSelLeft*sr));const endSample=Math.min(t.buffer.length,Math.floor(relSelRight*sr));const len=endSample-startSample;if(len<100){showToast('Khoảng chọn quá ngắn.','warning');return;}const ctx=getAudioContext();const numChannels=t.buffer.numberOfChannels||1;const subBuffer=ctx.createBuffer(numChannels,len,sr);for(let c=0;c[...prev,{id:tabId,label:tabLabel,trackId:trackId,startTime:selLeft,endTime:selRight,buffer:subBuffer,channelInfo:subChannelInfo,effects:{reverse:false,gainDb:0,fadeInMs:0,fadeOutMs:0,normalizeDb:0,pitch:0,speedStretch:100},currentTime:0,selectionStart:null,selectionEnd:null,isPlaying:false,speed:1.0,volumeNodes:[],panningNodes:[],fadeInLen:0,fadeOutLen:0,graphMode:null,isLooping:false,loopCount:0}]);setActiveTab(tabId);};const handleEditClipInSubTab=(trackId,clipId)=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return;const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default_'+track.id,buffer:track.buffer,startTime:track.startTime||0,name:track.name}]:[];const clip=clips.find(c=>c.id===clipId||clipId==='default'&&c.id==='default_'+trackId);if(!clip||!clip.buffer)return;const resolvedClipId=clip.id==='default'?'default_'+trackId:clip.id;const existing=subTabs.find(s=>s.clipId===resolvedClipId&&s.trackId===trackId);if(existing){setActiveTab(existing.id);showToast(`Sub Tab for "${clip.name}" already open.`,'info');return;}const sr=clip.buffer.sampleRate;const len=clip.buffer.length;const numChannels=clip.buffer.numberOfChannels||1;const ctx=getAudioContext();const subBuffer=ctx.createBuffer(numChannels,len,sr);for(let c=0;c[...prev,{id:tabId,label:tabLabel,trackId:trackId,clipId:resolvedClipId,startTime:clip.startTime,endTime:clip.startTime+clip.buffer.duration,buffer:subBuffer,channelInfo:subChannelInfo,effects:{reverse:false,gainDb:0,fadeInMs:0,fadeOutMs:0,normalizeDb:0,pitch:0,speedStretch:100},currentTime:0,selectionStart:null,selectionEnd:null,isPlaying:false,speed:1.0,volumeNodes:[],panningNodes:[],fadeInLen:0,fadeOutLen:0,graphMode:null,isLooping:false,loopCount:0}]);setActiveTab(tabId);};const handleEditMidiInTab=(trackId,midiItemId)=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return;const midiItem=(track.midiItems||[]).find(m=>m.id===midiItemId);if(!midiItem)return;// Main đang play → CHỈ lưu điểm resume; useEffect [activeTab] tự +// stopAllPlayback + play CHỈ track piano roll khi vào tab (user: piano roll +// play only midi notes đang active — mọi track khác mute). mainResumeRef +// giữ để quay lại MAIN/SECTION resume đúng vị trí. 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.