feat(piano-roll): vào tab lúc main play -> chỉ play MIDI notes track đang edit, các track khác mute

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).
This commit is contained in:
2026-08-16 17:47:31 +07:00
parent e541d16132
commit cca58c4893
2 changed files with 37 additions and 9 deletions
+27 -4
View File
@@ -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
+10 -5
View File
@@ -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;px<w;px++){const startSample=Math.floor(px/w*totalSamples);const endSample=Math.floor((px+1)/w*totalSamples);let maxVal=0;for(let i=startSample;i<endSample&&i<totalSamples;i++){const abs=Math.abs(data[i]);if(abs>maxVal)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<numChannels;c++){subBuffer.copyToChannel(t.buffer.getChannelData(c).subarray(startSample,endSample),c);}const subChannelInfo=window.SonicAudio.analyzeAudioBufferChannels(subBuffer);const tabId='subtab_'+Date.now();const tabLabel=`Edit_${t.name.replace('.wav','').slice(0,10)}_${selLeft.toFixed(1)}s`;setSubTabs(prev=>[...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<numChannels;c++){subBuffer.copyToChannel(clip.buffer.getChannelData(c),c);}const subChannelInfo=window.SonicAudio.analyzeAudioBufferChannels(subBuffer);const tabId='subtab_'+Date.now();const tabLabel=`Edit_${clip.name.replace('.wav','').slice(0,10)}`;setSubTabs(prev=>[...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<numChannels;c++){subBuffer.copyToChannel(t.buffer.getChannelData(c).subarray(startSample,endSample),c);}const subChannelInfo=window.SonicAudio.analyzeAudioBufferChannels(subBuffer);const tabId='subtab_'+Date.now();const tabLabel=`Edit_${t.name.replace('.wav','').slice(0,10)}_${selLeft.toFixed(1)}s`;setSubTabs(prev=>[...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<numChannels;c++){subBuffer.copyToChannel(clip.buffer.getChannelData(c),c);}const subChannelInfo=window.SonicAudio.analyzeAudioBufferChannels(subBuffer);const tabId='subtab_'+Date.now();const tabLabel=`Edit_${clip.name.replace('.wav','').slice(0,10)}`;setSubTabs(prev=>[...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.