FIX: sửa lỗi đồng bộ playhead vị trí play giữa MAIN SESSION/SECTION-TAB/PIANO ROLL TAB

This commit is contained in:
2026-08-07 10:37:34 +07:00
parent 4981785913
commit 6c16bf8410
4 changed files with 116 additions and 40 deletions
+71 -17
View File
@@ -14750,6 +14750,31 @@ const App = () => {
const sessionTabsRef = useRef(sessionTabs); const sessionTabsRef = useRef(sessionTabs);
sessionTabsRef.current = sessionTabs; sessionTabsRef.current = sessionTabs;
// V trí MIDI item (trong context ca piano roll) dùng đ đng b playhead
// 2 chiu SECTION-TAB PIANO ROLL: item.startTime (local section / main) +
// st.currentTime (v trí TRONG item). Dùng REFS (activeTracksRef/
// sessionTabsRef) an toàn vi closure cũ (updatePlayhead/rAF loop).
const getPianoRollItemInfo = st => {
try {
if (!st) return null;
let list = null;
if (st.parent_tab_id && st.parent_tab_id.startsWith('session_')) {
const _pst = sessionTabsRef.current.find(s => s.id === st.parent_tab_id);
list = _pst ? _pst.tracks : null;
} else {
list = activeTracksRef.current;
}
if (!list) return null;
const _trk = list.find(t => t.id === st.trackId);
if (!_trk) return null;
const _item = (_trk.midiItems || []).find(m => m.id === st.target_id);
if (!_item) return null;
return { startTime: _item.startTime || 0, duration: _item.duration || 4 };
} catch (e) {
return null;
}
};
// Track mute/solo/volume signature: only re-apply gains when something that // Track mute/solo/volume signature: only re-apply gains when something that
// affects audibility actually changed (loads, undo, AI ops, section tabs). // affects audibility actually changed (loads, undo, AI ops, section tabs).
const trackMuteSoloSigRef = useRef({}); const trackMuteSoloSigRef = useRef({});
@@ -14851,29 +14876,42 @@ const App = () => {
// trưc đó m tab lúc main đang play (mainResumeRef đã set). // trưc đó m tab lúc main đang play (mainResumeRef đã set).
const _resume = mainResumeRef.current; const _resume = mainResumeRef.current;
mainResumeRef.current = null; mainResumeRef.current = null;
if (_resume && prevSub.type === 'PIANO_ROLL' && (activeTab === 'main' || (activeTab && activeTab.startsWith('session_')))) { if (prevSub.type === 'PIANO_ROLL' && (activeTab === 'main' || (activeTab && activeTab.startsWith('session_')))) {
// Resume section/main play ti v trí TƯƠNG NG vi piano roll:
// resumeAt = item.startTime + st.currentTime (v trí TRONG item
// yêu cu: play piano roll v trí nào v SECTION play đúng v trí
// đó). Ưu tiên item info; fallback mainResumeRef (offset+elapsed);
// fallback cui: setIsPlaying(true) playhead chy theo âm còn kêu.
let _resumeAt = null;
const _itemInfo = getPianoRollItemInfo(prevSub);
if (_itemInfo) {
_resumeAt = _itemInfo.startTime + (prevSub.currentTime || 0);
// V MAIN mà item thuc SECTION: v trí main = secStart + local
if (activeTab === 'main' && prevSub.parent_tab_id && prevSub.parent_tab_id.startsWith('session_')) {
const _pst = sessionTabsRef.current.find(s => s.id === prevSub.parent_tab_id);
if (_pst && _pst.sectionId) {
(activeTracksRef.current || []).forEach(_t => (_t.sections || []).forEach(_s => {
if (_s.sectionId === _pst.sectionId || _s.id === _pst.sectionId) _resumeAt += (_s.start || 0);
}));
}
}
} else if (_resume) {
_resumeAt = _resume.offset + (getAudioContext().currentTime - _resume.audioTime);
}
if (_resumeAt !== null) {
try { try {
const _elapsed = getAudioContext().currentTime - _resume.audioTime; console.log('[Resume] play tại', _resumeAt.toFixed(2), 's (item.startTime=' + (_itemInfo ? _itemInfo.startTime.toFixed(2) : '?') + ' + st.currentTime=' + (prevSub.currentTime || 0).toFixed(2) + ')');
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(); stopAllPlayback();
startOffsetTimeRef.current = _resumeAt; startOffsetTimeRef.current = _resumeAt;
startAudioTimeRef.current = getAudioContext().currentTime; startAudioTimeRef.current = getAudioContext().currentTime;
startTrackPlayback(_resumeAt); startTrackPlayback(_resumeAt);
setIsPlaying(true); setIsPlaying(true);
} catch (e) { console.warn('[Resume] error:', e); } } catch (e) { console.warn('[Resume] error:', e); }
} else if (prevSub.type === 'PIANO_ROLL' && (activeTab === 'main' || (activeTab && activeTab.startsWith('session_')))) { } else {
// Play THUN piano roll (main chưa tng play mainResumeRef null):
// ri tab KHÔNG stop âm (0415) NHƯNG isPlaying=false nhánh
// main updatePlayhead `if (!isPlaying) return` playhead NGNG dù
// âm piano roll còn kêu (user: playhead không đng b/ngng khi
// chuyn tab). setIsPlaying(true): playhead main/section chy theo
// elapsed (startOffsetTimeRef/startAudioTimeRef đang là ca piano
// roll play đng b vi âm đang phát). Hết bài (projectEnd)
// nhánh main t stop.
console.log('[Playhead] tiếp tục theo piano roll (không stop âm) — activeTab=' + activeTab); console.log('[Playhead] tiếp tục theo piano roll (không stop âm) — activeTab=' + activeTab);
setIsPlaying(true); setIsPlaying(true);
} }
}
} else if (prevSub && prevSub.type === 'PIANO_ROLL') { } else if (prevSub && prevSub.type === 'PIANO_ROLL') {
// Ri PIANO ROLL tab KHÔNG play: main/section play ngm vn chy (âm // Ri PIANO ROLL tab KHÔNG play: main/section play ngm vn chy (âm
// tiếp tc t nhiên không cn resume) clear resume point. // tiếp tc t nhiên không cn resume) clear resume point.
@@ -18495,13 +18533,29 @@ const App = () => {
if (!st || !st.buffer) return; if (!st || !st.buffer) return;
if (!st.isPlaying) { if (!st.isPlaying) {
// PIANO ROLL / sub-tab KHÔNG play: playhead theo MAIN/SECTION nếu đang // PIANO ROLL / sub-tab KHÔNG play: playhead theo MAIN/SECTION nếu đang
// play (đng b v trí âm THT user: di chuyn tab qua li quay // play ĐNG B 2 CHIU: v trí play (section/main) v trí TRONG
// li piano roll tab playhead phi chy theo âm đang phát); không // item (st.currentTime = playPos - item.startTime); playhead vưt ra
// có play nào đng yên (chế đ edit). Loop luôn tiếp tc (rAF). // ngoài duration item bt đu t đu (0). Không có play nào đng
// yên (chế đ edit). Loop luôn tiếp tc (rAF).
if (isPlaying) { if (isPlaying) {
const context = getAudioContext(); const context = getAudioContext();
const _ph = startOffsetTimeRef.current + (context.currentTime - startAudioTimeRef.current); const _ph = startOffsetTimeRef.current + (context.currentTime - startAudioTimeRef.current);
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: _ph } : s)); let _local = _ph;
const _itemInfo = getPianoRollItemInfo(st);
if (_itemInfo) {
if (_ph < _itemInfo.startTime) _local = 0;
else if (_ph >= _itemInfo.startTime + _itemInfo.duration) _local = 0; // vưt duration item đu
else _local = _ph - _itemInfo.startTime;
}
// Log chn đoán (rate-limited ~2 ln/giây): xác nhn đng b MAIN/
// SECTION PIANO ROLL ph (v trí play) local (trong item).
try {
const _sf = (window.__syncFrame = (window.__syncFrame || 0) + 1);
if (_sf % 45 === 0) {
console.log('[Sync] PR', st.id, 'ph=', _ph.toFixed(2), 'item.start=', _itemInfo ? _itemInfo.startTime.toFixed(2) : '-', 'dur=', _itemInfo ? _itemInfo.duration.toFixed(2) : '-', '→ local=', _local.toFixed(2), 'tab=', activeTabRef.current);
}
} catch (e) { }
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: _local } : s));
} }
animationFrameIdRef.current = requestAnimationFrame(updatePlayhead); animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
return; return;
+20 -14
View File
@@ -670,7 +670,11 @@ const[activeTab,setActiveTab]=useState('main');const[subTabSelectedNodeTime,setS
const[pianoRollClipboard,setPianoRollClipboard]=useState(null);// notes copy giữa các PIANO ROLL TAB const[pianoRollClipboard,setPianoRollClipboard]=useState(null);// notes copy giữa các PIANO ROLL TAB
const[sessionTabs,setSessionTabs]=useState([]);// [{id, name, tracks}, ...] const[sessionTabs,setSessionTabs]=useState([]);// [{id, name, tracks}, ...]
const[tabContextMenu,setTabContextMenu]=useState(null);// { x, y, tabId, tabType } const[tabContextMenu,setTabContextMenu]=useState(null);// { x, y, tabId, tabType }
const handleSetTabColor=(tabId,tabType,color)=>{if(tabType==='session'){setSessionTabs(prev=>prev.map(s=>s.id===tabId?{...s,color:color}:s));const tab=sessionTabs.find(s=>s.id===tabId);if(tab){setTracks(prev=>prev.map(t=>{if(!t.sections||t.sections.length===0)return t;return{...t,sections:t.sections.map(s=>{if(s.sectionId===tab.sectionId){return{...s,color:color};}return s;})};}));}}else{setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,color:color}:s));}};const activeTracks=useMemo(()=>{var st=sessionTabs.find(s=>s.id===activeTab);if(st)return st.tracks;var pr=subTabs.find(s=>s.id===activeTab&&s.type==='PIANO_ROLL');if(pr&&pr.parent_tab_id&&pr.parent_tab_id.startsWith('session_')){var parentSt=sessionTabs.find(s=>s.id===pr.parent_tab_id);if(parentSt)return parentSt.tracks;}return tracks;},[activeTab,sessionTabs,subTabs,tracks]);const activeTracksRef=useRef([]);activeTracksRef.current=activeTracks;const sessionTabsRef=useRef(sessionTabs);sessionTabsRef.current=sessionTabs;// Track mute/solo/volume signature: only re-apply gains when something that const handleSetTabColor=(tabId,tabType,color)=>{if(tabType==='session'){setSessionTabs(prev=>prev.map(s=>s.id===tabId?{...s,color:color}:s));const tab=sessionTabs.find(s=>s.id===tabId);if(tab){setTracks(prev=>prev.map(t=>{if(!t.sections||t.sections.length===0)return t;return{...t,sections:t.sections.map(s=>{if(s.sectionId===tab.sectionId){return{...s,color:color};}return s;})};}));}}else{setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,color:color}:s));}};const activeTracks=useMemo(()=>{var st=sessionTabs.find(s=>s.id===activeTab);if(st)return st.tracks;var pr=subTabs.find(s=>s.id===activeTab&&s.type==='PIANO_ROLL');if(pr&&pr.parent_tab_id&&pr.parent_tab_id.startsWith('session_')){var parentSt=sessionTabs.find(s=>s.id===pr.parent_tab_id);if(parentSt)return parentSt.tracks;}return tracks;},[activeTab,sessionTabs,subTabs,tracks]);const activeTracksRef=useRef([]);activeTracksRef.current=activeTracks;const sessionTabsRef=useRef(sessionTabs);sessionTabsRef.current=sessionTabs;// Vị trí MIDI item (trong context của piano roll) — dùng để đồng bộ playhead
// 2 chiều SECTION-TAB ↔ PIANO ROLL: item.startTime (local section / main) +
// st.currentTime (vị trí TRONG item). Dùng REFS (activeTracksRef/
// sessionTabsRef) — an toàn với closure cũ (updatePlayhead/rAF loop).
const getPianoRollItemInfo=st=>{try{if(!st)return null;let list=null;if(st.parent_tab_id&&st.parent_tab_id.startsWith('session_')){const _pst=sessionTabsRef.current.find(s=>s.id===st.parent_tab_id);list=_pst?_pst.tracks:null;}else{list=activeTracksRef.current;}if(!list)return null;const _trk=list.find(t=>t.id===st.trackId);if(!_trk)return null;const _item=(_trk.midiItems||[]).find(m=>m.id===st.target_id);if(!_item)return null;return{startTime:_item.startTime||0,duration:_item.duration||4};}catch(e){return null;}};// Track mute/solo/volume signature: only re-apply gains when something that
// affects audibility actually changed (loads, undo, AI ops, section tabs). // affects audibility actually changed (loads, undo, AI ops, section tabs).
const trackMuteSoloSigRef=useRef({});// Last-known audibility per track id (used to detect inaudible→audible const trackMuteSoloSigRef=useRef({});// Last-known audibility per track id (used to detect inaudible→audible
// transitions that need a playback re-schedule). // transitions that need a playback re-schedule).
@@ -701,15 +705,13 @@ if(prevSub.type!=='PIANO_ROLL'){try{stopAllPlayback();}catch(e){}}setSubTabs(pre
// play piano roll → stopAllPlayback dừng main → quay lại MAIN/SECTION // play piano roll → stopAllPlayback dừng main → quay lại MAIN/SECTION
// bị CÂM. Resume startTrackPlayback(offset + elapsed thật) — chỉ khi // bị CÂM. Resume startTrackPlayback(offset + elapsed thật) — chỉ khi
// trước đó mở tab lúc main đang play (mainResumeRef đã set). // 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.type==='PIANO_ROLL'&&(activeTab==='main'||activeTab&&activeTab.startsWith('session_'))){// Play THUẦN piano roll (main chưa từng play — mainResumeRef null): const _resume=mainResumeRef.current;mainResumeRef.current=null;if(prevSub.type==='PIANO_ROLL'&&(activeTab==='main'||activeTab&&activeTab.startsWith('session_'))){// Resume section/main play tại vị trí TƯƠNG ỨNG với piano roll:
// rời tab → KHÔNG stop âm (0415) — NHƯNG isPlaying=false → nhánh // resumeAt = item.startTime + st.currentTime (vị trí TRONG item —
// main updatePlayhead `if (!isPlaying) return` → playhead NGỪNG dù // yêu cầu: play piano roll ở vị trí nào → về SECTION play đúng vị trí
// âm piano roll còn kêu (user: playhead không đồng bộ/ngừng khi // đó). Ưu tiên item info; fallback mainResumeRef (offset+elapsed);
// chuyển tab). → setIsPlaying(true): playhead main/section chạy theo // fallback cuối: setIsPlaying(true) playhead chạy theo âm còn kêu.
// elapsed (startOffsetTimeRef/startAudioTimeRef đang là của piano let _resumeAt=null;const _itemInfo=getPianoRollItemInfo(prevSub);if(_itemInfo){_resumeAt=_itemInfo.startTime+(prevSub.currentTime||0);// Về MAIN mà item thuộc SECTION: vị trí main = secStart + local
// roll play — đồng bộ với âm đang phát). Hết bài (projectEnd) → if(activeTab==='main'&&prevSub.parent_tab_id&&prevSub.parent_tab_id.startsWith('session_')){const _pst=sessionTabsRef.current.find(s=>s.id===prevSub.parent_tab_id);if(_pst&&_pst.sectionId){(activeTracksRef.current||[]).forEach(_t=>(_t.sections||[]).forEach(_s=>{if(_s.sectionId===_pst.sectionId||_s.id===_pst.sectionId)_resumeAt+=_s.start||0;}));}}}else if(_resume){_resumeAt=_resume.offset+(getAudioContext().currentTime-_resume.audioTime);}if(_resumeAt!==null){try{console.log('[Resume] play tại',_resumeAt.toFixed(2),'s (item.startTime='+(_itemInfo?_itemInfo.startTime.toFixed(2):'?')+' + st.currentTime='+(prevSub.currentTime||0).toFixed(2)+')');stopAllPlayback();startOffsetTimeRef.current=_resumeAt;startAudioTimeRef.current=getAudioContext().currentTime;startTrackPlayback(_resumeAt);setIsPlaying(true);}catch(e){console.warn('[Resume] error:',e);}}else{console.log('[Playhead] tiếp tục theo piano roll (không stop âm) — activeTab='+activeTab);setIsPlaying(true);}}}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
// nhánh main tự stop.
console.log('[Playhead] tiếp tục theo piano roll (không stop âm) — activeTab='+activeTab);setIsPlaying(true);}}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. // 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 // ⚠️ 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 ở // roll — mainResumeRef vừa set bởi handleEditMidiInTab — clear nhầm ở
@@ -933,10 +935,14 @@ while(true){const beatNum=nextMetronomeBeatRef.current;const elapsedBeats=beatNu
const now=Date.now();if(now-lastTempCompileTimeRef.current>200){lastTempCompileTimeRef.current=now;// Audio preview const now=Date.now();if(now-lastTempCompileTimeRef.current>200){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 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.buffer)return;if(!st.isPlaying){// PIANO ROLL / sub-tab KHÔNG play: playhead theo MAIN/SECTION nếu đang 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: playhead theo MAIN/SECTION nếu đang
// play (đồng bộ vị trí âm THẬT — user: di chuyển tab qua lại → quay // play — ĐỒNG BỘ 2 CHIỀU: vị trí play (section/main) → vị trí TRONG
// lại piano roll tab → playhead phải chạy theo âm đang phát); không // item (st.currentTime = playPos - item.startTime); playhead vượt ra
// có play nào → đứng yên (chế độ edit). Loop luôn tiếp tục (rAF). // ngoài duration item → bắt đầu từ đầu (0). Không có play nào → đứng
if(isPlaying){const context=getAudioContext();const _ph=startOffsetTimeRef.current+(context.currentTime-startAudioTimeRef.current);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:_ph}:s));}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) // yên (chế độ edit). Loop luôn tiếp tục (rAF).
if(isPlaying){const context=getAudioContext();const _ph=startOffsetTimeRef.current+(context.currentTime-startAudioTimeRef.current);let _local=_ph;const _itemInfo=getPianoRollItemInfo(st);if(_itemInfo){if(_ph<_itemInfo.startTime)_local=0;else if(_ph>=_itemInfo.startTime+_itemInfo.duration)_local=0;// vượt duration item → đầu
else _local=_ph-_itemInfo.startTime;}// Log chẩn đoán (rate-limited ~2 lần/giây): xác nhận đồng bộ MAIN/
// SECTION ↔ PIANO ROLL — ph (vị trí play) → local (trong item).
try{const _sf=window.__syncFrame=(window.__syncFrame||0)+1;if(_sf%45===0){console.log('[Sync] PR',st.id,'ph=',_ph.toFixed(2),'item.start=',_itemInfo?_itemInfo.startTime.toFixed(2):'-','dur=',_itemInfo?_itemInfo.duration.toFixed(2):'-','→ local=',_local.toFixed(2),'tab=',activeTabRef.current);}}catch(e){}setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:_local}:s));}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 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 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). // nếu chỉ chạy silent buffer).
+1 -1
View File
@@ -24,7 +24,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script> <script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script> <script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script> <script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608070430" defer></script> <script src="/static/js/app.precompiled.js?v=202608070440" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016"> <link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style> <style>
:root { :root {
+16
View File
@@ -2430,3 +2430,19 @@
- **FIX (app.jsx handlePlayPause nhánh sub-tab):** đầu nhánh — nếu `isPlaying` (main/section đang play ngầm) → `stopAllPlayback()` + setSubTabs isPlaying:false (mọi tab) + `setIsPlaying(false)` → return — Space = STOP trước, không play sub-tab. Space lần sau (đã stop) → play sub-tab bình thường. - **FIX (app.jsx handlePlayPause nhánh sub-tab):** đầu nhánh — nếu `isPlaying` (main/section đang play ngầm) → `stopAllPlayback()` + setSubTabs isPlaying:false (mọi tab) + `setIsPlaying(false)` → return — Space = STOP trước, không play sub-tab. Space lần sau (đã stop) → play sub-tab bình thường.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070430), `wiki.md`. Rebuild precompiled. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070430), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN → mở PIANO ROLL tab (main play ngầm) → Space 1 lần → DỪNG hẳn (không còn âm); Space lần nữa → play piano roll (toggle bình thường). - **Ghi chú/Test:** hard refresh → play MAIN → mở PIANO ROLL tab (main play ngầm) → Space 1 lần → DỪNG hẳn (không còn âm); Space lần nữa → play piano roll (toggle bình thường).
### [2026-08-07 04:35] Task: ĐỒNG BỘ playhead 2 chiều SECTION-TAB ↔ PIANO ROLL (vị trí item)
- **Yêu cầu user:** (1) play MIDI item trên SECTION-TAB ở vị trí X → sang PIANO ROLL → play tiếp tại vị trí tương ứng (X - item.startTime); (2) play PIANO ROLL tại Y → quay lại SECTION-TAB → play tại item.startTime + Y; (3) SECTION play vượt ngoài duration item → PIANO ROLL bắt đầu từ đầu (0).
- **FIX (app.jsx):**
(1) Helper `getPianoRollItemInfo(st)` — tìm item (trackId + target_id) trong context đúng (parent session tab → sessionTabsRef tracks; main → activeTracksRef) → `{startTime, duration}`. Dùng REFS — an toàn closure cũ (rAF loop).
(2) updatePlayhead nhánh sub-tab không-play: `st.currentTime = playPos - item.startTime` (clamp: < startTime hoặc ≥ startTime+duration → 0 — vượt duration → đầu).
(3) Effect đổi tab — rời PIANO ROLL play về MAIN/SECTION: resumeAt = `item.startTime + st.currentTime` (ưu tiên); về MAIN mà item thuộc section → cộng thêm secStart (vị trí section trên main); fallback mainResumeRef; fallback cuối setIsPlaying(true) (playhead chạy theo âm còn kêu).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070435), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → SECTION-TAB play (playhead trong item) → sang PIANO ROLL → playhead đúng vị trí trong item; play piano roll → về SECTION-TAB → play đúng item.startTime + vị trí piano roll (log `[Resume] play tại X.XX s (item.startTime=... + st.currentTime=...)`); SECTION play vượt quá item → piano roll về 0.
### [2026-08-07 04:40] Task: Xác nhận + log chẩn đoán — Đồng bộ 2 chiều MAIN SESSION ↔ PIANO ROLL
- **Yêu cầu user:** đồng bộ 2 chiều MAIN SESSION ↔ PIANO ROLL (tương tự SECTION-TAB ↔ PIANO ROLL 0435): main play ở vị trí X → mở piano roll → playhead trong item đúng (X item.startTime); piano roll play tại Y → về MAIN → play tại item.startTime + Y; main play vượt duration item → piano roll về 0.
- **Trạng thái:** fix 0435 ĐÃ generic — `getPianoRollItemInfo` (parent_tab_id null → `activeTracksRef` = MAIN tracks — item.startTime main global) + 2 nhánh (vào tab: local = ph item.startTime; rời tab: resumeAt = item.startTime + st.currentTime) tự áp dụng cho MAIN. Không cần sửa logic — thêm LOG chẩn đoán để xác nhận.
- **FIX (app.jsx):** nhánh updatePlayhead sub-tab không-play — log `[Sync] PR <id> ph=<vị trí play> item.start=<...> dur=<...> → local=<...>` (rate-limited ~2 lần/giây) — xác nhận giá trị đồng bộ realtime.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070440), `wiki.md`. Rebuild precompiled.
- **Ghi chú/Test:** hard refresh → play MAIN (playhead trong MIDI item) → mở piano roll → log `[Sync]` cho thấy local tăng theo (ph item.start); play piano roll → về MAIN → log `[Resume] play tại item.startTime + st.currentTime`; main play vượt item → local = 0. Nếu giá trị sai → dán log.