FIX: sửa lỗi chuyển từ tab PIANO ROLL TAB sang MAIN SESSION/SECTION-TAB không đồng bộ playhead
This commit is contained in:
+32
-10
@@ -14838,7 +14838,12 @@ const App = () => {
|
||||
if (prevTab !== activeTab) {
|
||||
const prevSub = subTabsRef.current.find(s => s.id === prevTab);
|
||||
if (prevSub && prevSub.isPlaying) {
|
||||
try { stopAllPlayback(); } catch (e) {}
|
||||
// PIANO ROLL tab: KHÔNG stop âm khi rời tab (SF notes đã schedule —
|
||||
// tiếp tục kêu tự nhiên — user: chuyển tab qua lại KHÔNG được câm).
|
||||
// Audio sub-tab (buffer source riêng): vẫn stop như cũ.
|
||||
if (prevSub.type !== 'PIANO_ROLL') {
|
||||
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
|
||||
@@ -14857,6 +14862,17 @@ const App = () => {
|
||||
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):
|
||||
// rời tab → KHÔNG stop âm (0415) — NHƯNG isPlaying=false → nhánh
|
||||
// main updatePlayhead `if (!isPlaying) return` → playhead NGỪNG dù
|
||||
// âm piano roll còn kêu (user: playhead không đồng bộ/ngừng khi
|
||||
// chuyển tab). → setIsPlaying(true): playhead main/section chạy theo
|
||||
// elapsed (startOffsetTimeRef/startAudioTimeRef đang là của piano
|
||||
// roll play — đồng bộ với â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);
|
||||
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
|
||||
@@ -15936,7 +15952,12 @@ const App = () => {
|
||||
const handler = e => {
|
||||
// Bypass global hotkeys when typing inside input/textarea/contentEditable elements
|
||||
if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable)) {
|
||||
return;
|
||||
// PIANO ROLL tab: Space LUÔN play/stop (user yêu cầu — transport phải
|
||||
// capture được kể cả khi có input đang focus trong tab) — các phím
|
||||
// khác vẫn bypass để gõ liệu bình thường.
|
||||
if (!(e.key === ' ' || e.code === 'Space') || !activeTabRef.current || !activeTabRef.current.startsWith('midi_')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const ctrl = e.ctrlKey || e.metaKey;
|
||||
const alt = e.altKey;
|
||||
@@ -18465,14 +18486,15 @@ const App = () => {
|
||||
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.
|
||||
// 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
|
||||
// lại piano roll tab → playhead phải chạy theo âm đang phát); không
|
||||
// có play nào → đứng 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);
|
||||
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: _ph } : s));
|
||||
}
|
||||
animationFrameIdRef.current = requestAnimationFrame(updatePlayhead);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -694,11 +694,22 @@ 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));// Resume main/session play khi rời PIANO ROLL tab đã play (Space):
|
||||
if(prevTab!==activeTab){const prevSub=subTabsRef.current.find(s=>s.id===prevTab);if(prevSub&&prevSub.isPlaying){// PIANO ROLL tab: KHÔNG stop âm khi rời tab (SF notes đã schedule —
|
||||
// tiếp tục kêu tự nhiên — user: chuyển tab qua lại KHÔNG được câm).
|
||||
// Audio sub-tab (buffer source riêng): vẫn stop như cũ.
|
||||
if(prevSub.type!=='PIANO_ROLL'){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
|
||||
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):
|
||||
// rời tab → KHÔNG stop âm (0415) — NHƯNG isPlaying=false → nhánh
|
||||
// main updatePlayhead `if (!isPlaying) return` → playhead NGỪNG dù
|
||||
// âm piano roll còn kêu (user: playhead không đồng bộ/ngừng khi
|
||||
// chuyển tab). → setIsPlaying(true): playhead main/section chạy theo
|
||||
// elapsed (startOffsetTimeRef/startAudioTimeRef đang là của piano
|
||||
// roll play — đồng bộ với â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);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.
|
||||
// ⚠️ 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 ở
|
||||
@@ -750,7 +761,10 @@ const copyBuffer=ctx.createBuffer(1,len,sr);copyBuffer.copyToChannel(data.subarr
|
||||
if(st.type==='PIANO_ROLL'){const clip=clipboardRef.current||window.globalStudioClipboard;if(!clip||clip.type!=='midi'||!clip.notes){showToast('Clipboard không chứa dữ liệu MIDI.','warning');return;}const secondsPerBeat=60/(bpm||120);const pasteBeat=(st.currentTime||0)/secondsPerBeat;const newNotes=clip.notes.map(n=>({id:'note_'+Math.random().toString(36).substr(2,9),pitch:n.pitch,start_beat:pasteBeat+n.start_beat,duration_beats:n.duration_beats,velocity:n.velocity||0.8}));setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,notes:[...(s.notes||[]),...newNotes]}:s));showToast('Đã dán note MIDI vào Piano Roll.','success');return;}// Default Waveform Audio Paste
|
||||
if(!st.buffer)return;const clip=clipboardRef.current||window.globalStudioClipboard;if(!clip||!clip.buffer){showToast('Clipboard trống hoặc không chứa dữ liệu âm thanh.','warning');return;}const ctx=getAudioContext();const clipBuf=clip.buffer;const sr=st.buffer.sampleRate;const data=st.buffer.getChannelData(0);const insertTime=st.currentTime||0;const insertSample=Math.floor(insertTime*sr);const newBuffer=ctx.createBuffer(1,Math.max(1,data.length+clipBuf.length),sr);const newData=newBuffer.getChannelData(0);let idx=0;for(let i=0;i<insertSample;i++)newData[idx++]=data[i];const clipData=clipBuf.getChannelData(0);for(let i=0;i<clipBuf.length;i++)newData[idx++]=clipData[i];for(let i=insertSample;i<data.length;i++)newData[idx++]=data[i];setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,buffer:newBuffer,currentTime:insertTime+clipBuf.duration,selectionStart:null,selectionEnd:null}:s));showToast('Đã dán dữ liệu âm thanh.','success');};const handleSubTabDelete=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 để Delete.','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=endSample-startSample;const newBuffer=ctx.createBuffer(1,data.length-len,sr);const newData=newBuffer.getChannelData(0);let idx=0;for(let i=0;i<startSample;i++)newData[idx++]=data[i];for(let i=endSample;i<data.length;i++)newData[idx++]=data[i];setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,buffer:newBuffer,currentTime:left,selectionStart:null,selectionEnd:null}:s));showToast('Đã xóa vùng chọn.','success');};const handleSubTabLoop=(tabId,loopCount)=>{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 để Loop.','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=endSample-startSample;// Loop payload N times
|
||||
const segmentData=data.subarray(startSample,endSample);const addedSamples=len*(loopCount-1);const newBuffer=ctx.createBuffer(1,data.length+addedSamples,sr);const newData=newBuffer.getChannelData(0);let idx=0;for(let i=0;i<endSample;i++)newData[idx++]=data[i];for(let n=0;n<loopCount-1;n++){for(let i=0;i<len;i++)newData[idx++]=segmentData[i];}for(let i=endSample;i<data.length;i++)newData[idx++]=data[i];setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,buffer:newBuffer,selectionStart:null,selectionEnd:null}:s));showToast(`Đã lặp vùng chọn ${loopCount} lần.`,'success');};useEffect(()=>{const handler=e=>{// Bypass global hotkeys when typing inside input/textarea/contentEditable elements
|
||||
if(e.target&&(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.isContentEditable)){return;}const ctrl=e.ctrlKey||e.metaKey;const alt=e.altKey;// Global space play/pause shortcut for transport
|
||||
if(e.target&&(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.isContentEditable)){// PIANO ROLL tab: Space LUÔN play/stop (user yêu cầu — transport phải
|
||||
// capture được kể cả khi có input đang focus trong tab) — các phím
|
||||
// khác vẫn bypass để gõ liệu bình thường.
|
||||
if(!(e.key===' '||e.code==='Space')||!activeTabRef.current||!activeTabRef.current.startsWith('midi_')){return;}}const ctrl=e.ctrlKey||e.metaKey;const alt=e.altKey;// Global space play/pause shortcut for transport
|
||||
if(e.key===' '||e.code==='Space'){if(window.mediaExplorerActive)return;e.preventDefault();if(recordingStateRef.current==='RECORDING'||recordingStateRef.current==='COUNT_IN'){handleRecordClickRef.current();return;}if(handlePlayPauseRef.current)handlePlayPauseRef.current();return;}if(activeTabRef.current!=='main'&&!activeTabRef.current.startsWith('session_')){// Sub-Tab keyboard shortcuts mapping
|
||||
const curTabId=activeTabRef.current;if(ctrl&&alt&&e.key==='n'){e.preventDefault();handleSubTabNormalize(curTabId);return;}if(e.key==='f'||e.key==='F'){e.preventDefault();handleSubTabFade(curTabId,'in');return;}if(e.key==='g'||e.key==='G'){e.preventDefault();handleSubTabFade(curTabId,'out');return;}if(ctrl&&e.key==='l'){e.preventDefault();handleSubTabLoop(curTabId,4);return;}if(e.key==='v'||e.key==='V'){e.preventDefault();const val=prompt("Nhập Gain điều chỉnh (dB):","0");if(val)handleSubTabGain(curTabId,parseFloat(val)||0);return;}if(ctrl&&e.key==='x'){e.preventDefault();handleSubTabCut(curTabId);return;}if(ctrl&&e.key==='c'){e.preventDefault();handleSubTabCopy(curTabId);return;}if(ctrl&&e.key==='v'){e.preventDefault();handleSubTabPaste(curTabId);return;}if(e.key==='Delete'||e.key==='Backspace'||e.key==='Del'){e.preventDefault();if(subTabSelectedNodeTimeRef.current!==null){const selTime=subTabSelectedNodeTimeRef.current;setSubTabs(prev=>prev.map(s=>{if(s.id!==curTabId)return s;const curNodes=s.graphMode==='pan'?s.panningNodes||[]:s.volumeNodes||[];const updated=curNodes.filter(n=>n.time!==selTime);return{...s,[s.graphMode==='pan'?'panningNodes':'volumeNodes']:updated};}));setSubTabSelectedNodeTime(null);}else{handleSubTabDelete(curTabId);}return;}return;}if(ctrl&&e.key==='z'&&!e.shiftKey){const tag=document.activeElement?.tagName;if(tag==='INPUT'||tag==='TEXTAREA')return;e.preventDefault();handleUndoRef.current();return;}if(ctrl&&(e.key==='y'||e.key==='z'&&e.shiftKey)){const tag=document.activeElement?.tagName;if(tag==='INPUT'||tag==='TEXTAREA')return;e.preventDefault();handleRedoRef.current();return;}if(ctrl&&!alt&&e.key==='o'){e.preventDefault();handleImportSFS();return;}if(ctrl&&!alt&&e.key==='n'){e.preventDefault();// New project: đóng hết tab + reset MỌI trạng thái về mặc định
|
||||
try{stopAllPlayback();}catch(e){}setSubTabs([]);setSessionTabs([]);setActiveTab('main');trackMidiChannelsRef.current={};Object.keys(trackMidiBypassMap).forEach(function(k){delete trackMidiBypassMap[k];});Object.keys(trackAudioBypassMap).forEach(function(k){delete trackAudioBypassMap[k];});Object.keys(trackMasteringBypassMap).forEach(function(k){delete trackMasteringBypassMap[k];});setSelectedItemIds(new Set());setTracks([{id:'1',name:'Track 01',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#0f766e',markers:[],serverFileId:null},{id:'2',name:'Track 02',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#1d4ed8',markers:[],serverFileId:null}]);setSelectedTrackId('1');setProjectName('');setCurrentProjectId(null);localStorage.removeItem('sonic_project_name');localStorage.removeItem('sonic_project_id');showToast('New project created','info');return;}if(ctrl&&!alt&&!e.shiftKey&&e.key==='a'){e.preventDefault();const curTab=activeTabRef.current;if(curTab!=='main'&&!curTab.startsWith('session_'))return;captureSelectionUndo();const allIds=new Set();(activeTracksRef.current||[]).forEach(t=>{(t.sections||[]).forEach(s=>allIds.add(s.id));(t.midiItems||[]).forEach(m=>allIds.add(m.id));const clips=t.clips&&t.clips.length>0?t.clips:t.buffer?[{id:'default',buffer:t.buffer,startTime:t.startTime||0,name:t.name,speed:t.speed||1.0}]:[];clips.forEach(c=>allIds.add(c.id==='default'?'default_'+t.id:c.id));});setSelectedItemIds(allIds);pushSelectionUndo();return;}if(ctrl&&!alt&&e.key==='s'){e.preventDefault();const curTab=activeTabRef.current;if(curTab&&curTab.startsWith('session_')){// SECTION-TAB: Ctrl+S = Lưu section (như nút Lưu section)
|
||||
@@ -914,15 +928,11 @@ 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(beatTime<audioCtx.currentTime+lookahead){const isDownbeat=beatNum%4===0;playMetronomeClick(beatTime,isDownbeat);nextMetronomeBeatRef.current++;}else{break;}}// Throttled Live Preview compilation (every 200ms)
|
||||
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
|
||||
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)
|
||||
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
|
||||
// lại piano roll tab → playhead phải chạy theo âm đang phát); không
|
||||
// có play nào → đứng 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);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)
|
||||
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).
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<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/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608070405" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608070420" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user