FIX: sửa lỗi hiển thị animation của VU meter khi play MIDI

This commit is contained in:
2026-08-07 15:57:49 +07:00
parent 9e269d3b79
commit 4f34491f3e
5 changed files with 95 additions and 8 deletions
+24 -3
View File
@@ -378,7 +378,12 @@ try{const v=parseFloat(localStorage.getItem('sf_pr_zoom'));if(isFinite(v)&&v>=15
React.useEffect(()=>{try{localStorage.setItem('sf_pr_zoom',String(rollZoom));}catch(e){}},[rollZoom]);const[aiBarStart,setAiBarStart]=React.useState(0);const[aiBarEnd,setAiBarEnd]=React.useState(4);const canvasRef=React.useRef(null);const ccCanvasRef=React.useRef(null);const ccWrapperRef=React.useRef(null);const gridScrollRef=React.useRef(null);const keybedRef=React.useRef(null);const keybedMouseDownRef=React.useRef(false);const rulerScrollRef=React.useRef(null);React.useEffect(()=>{const up=()=>{keybedMouseDownRef.current=false;};window.addEventListener('mouseup',up);return()=>window.removeEventListener('mouseup',up);},[]);const NoteHeight=18;const PITCH_START=0;// C0 (render all 128 keys)
const KeybedPixelHeight=(128-PITCH_START)*NoteHeight;const pixelsPerBeat=rollZoom;const timeSigNum=4;const noteMaxBeat=(st.notes||[]).reduce((max,n)=>Math.max(max,(n.start_beat||0)+(n.duration_beats||1)),0);const[selectionMarquee,setSelectionMarquee]=React.useState(null);// { startBeat, startPitch, currentBeat, currentPitch }
const[draggedNote,setDraggedNote]=React.useState(null);// { mode: 'move'|'resize', idx, startOffsetBeat, originalStart }
const draggedNoteRef=React.useRef(draggedNote);draggedNoteRef.current=draggedNote;const[hoveredResizeIdx,setHoveredResizeIdx]=React.useState(-1);const[rollBeats,setRollBeats]=React.useState(Math.max(noteMaxBeat+16,64));const rollBeatsRef=React.useRef(rollBeats);rollBeatsRef.current=rollBeats;const[showGhostNotes,setShowGhostNotes]=React.useState(true);const[sessionSyncMode,setSessionSyncMode]=React.useState(true);const[activePlayTrackIds,setActivePlayTrackIds]=React.useState([]);const[focusItemId,setFocusItemId]=React.useState(st.target_id);const allMidiItems=React.useMemo(()=>{const result=[];(activeTracks||[]).forEach(t=>{if(!t.midiItems||!t.midiItems.length)return;t.midiItems.forEach(m=>{var extended=Object.assign({},m,{_trackId:t.id,_trackName:t.name});result.push(extended);});});return result;},[activeTracks]);const ghostLayers=React.useMemo(function(){if(!activeTracks||!st||!st.target_id)return[];var fn=window.SonicGhost&&window.SonicGhost.extractGhostLayers;return fn?fn(activeTracks,st.trackId,st.target_id,parseInt(bpm)||120):[];},[activeTracks,st.trackId,st.target_id,bpm]);const secondsPerBar=60.0/(parseInt(bpm)||120)*4;const activeTargetItem=React.useMemo(function(){if(!activeTracks||!st)return null;var trk=activeTracks.find(function(t){return t.id===st.trackId;});return trk?(trk.midiItems||[]).find(function(m){return m.id===st.target_id;}):null;},[activeTracks,st.trackId,st.target_id]);var activeParentTrackName='';if(st.target_id&&activeTracks){var aptTrk=window.SonicPianoRoll?window.SonicPianoRoll.getParentTrackByItemId(st.target_id,activeTracks):null;if(!aptTrk)aptTrk=activeTracks.find(function(t){return t.id===st.trackId;});if(!aptTrk&&activeTargetItem)aptTrk=activeTracks.find(function(t){return(t.midiItems||[]).some(function(m){return m.id===st.target_id;});});if(aptTrk)activeParentTrackName=aptTrk.name||aptTrk.id;}const sessionStartBar=0;const renderBeatOffset=sessionSyncMode&&activeTargetItem?activeTargetItem.startTime/secondsPerBar*timeSigNum:0;const sessionLengthBars=React.useMemo(function(){var maxSec=0;(activeTracks||[]).forEach(function(tr){(tr.midiItems||[]).forEach(function(m){var end=(m.startTime||0)+(m.duration||4);if(end>maxSec)maxSec=end;});});return Math.ceil((maxSec||4)/secondsPerBar);},[activeTracks,secondsPerBar]);const handleSwitchMidiItem=function(itemId){if(itemId===st.target_id)return;var match=allMidiItems.find(function(m){return m.id===itemId;});if(!match)return;var scope=window.SonicPianoRoll?window.SonicPianoRoll.buildActiveScope(itemId,activeTracks):null;var trk=scope?null:(activeTracks||[]).find(function(t){return t.id===match._trackId;});var newBeatOff=match.startTime/secondsPerBar*timeSigNum;var spb=60.0/(parseInt(bpm)||120);var newTime=0;setSubTabs(function(prev){return prev.map(function(s){if(s.id!==st.id)return s;return Object.assign({},s,{trackId:scope?scope.parent_track_id:match._trackId||trk?.id,target_id:match.id,label:'Piano Roll: '+(match.name||'MIDI'),notes:match.notes||[],duration:match.duration||4,instrumentProgram:scope?scope.instrument_program:trk?trk.instrumentProgram:undefined,instrumentName:scope?scope.instrument_name:trk?trk.instrumentName:undefined,active_scope:scope||null,note_selection:[],currentTime:newTime});});});setSelectedNoteIds([]);};const rawTotalBeats=Math.max(rollBeats,noteMaxBeat+16,64);const drawWidth=rawTotalBeats*pixelsPerBeat;const[rollViewWidth,setRollViewWidth]=React.useState(800);const viewWidth=Math.max(drawWidth,rollViewWidth);const viewBeats=Math.ceil(viewWidth/pixelsPerBeat)+4;const totalBeats=Math.max(rawTotalBeats,viewBeats+32);const[notes,setNotes]=React.useState(st.notes||[]);const notesRef=React.useRef(notes);notesRef.current=notes;React.useEffect(()=>{if(!draggedNoteRef.current)setNotes(st.notes||[]);},[st.notes]);const brushVelocityRef=React.useRef(0.8);const lastNoteDurationRef=React.useRef(null);const previewPitchRef=React.useRef(null);const previewNodesRef=React.useRef(null);var stopPreviewNote=function(){var pn=previewNodesRef.current;if(pn){try{pn.osc.stop();}catch(e){}try{pn.osc.disconnect();}catch(e){}try{pn.gain.disconnect();}catch(e){}previewNodesRef.current=null;}};const[selectedNoteIds,setSelectedNoteIds]=React.useState([]);const[loopStartBeat,setLoopStartBeat]=React.useState(null);const[loopEndBeat,setLoopEndBeat]=React.useState(null);const[isLooping,setIsLooping]=React.useState(false);const rulerDragRef=React.useRef(null);React.useEffect(()=>{const handler=e=>{if((e.ctrlKey||e.metaKey)&&e.key==='a'){const target=e.target;if(target&&(target.tagName==='INPUT'||target.tagName==='TEXTAREA'||target.isContentEditable))return;e.preventDefault();setSelectedNoteIds(notes.map(n=>n.id));}};window.addEventListener('keydown',handler);return()=>window.removeEventListener('keydown',handler);},[notes,setSelectedNoteIds]);// Undo/redo stacks
const draggedNoteRef=React.useRef(draggedNote);draggedNoteRef.current=draggedNote;const[hoveredResizeIdx,setHoveredResizeIdx]=React.useState(-1);const[rollBeats,setRollBeats]=React.useState(Math.max(noteMaxBeat+16,64));const rollBeatsRef=React.useRef(rollBeats);rollBeatsRef.current=rollBeats;const[showGhostNotes,setShowGhostNotes]=React.useState(true);const[sessionSyncMode,setSessionSyncMode]=React.useState(true);const[activePlayTrackIds,setActivePlayTrackIds]=React.useState([]);const[focusItemId,setFocusItemId]=React.useState(st.target_id);const allMidiItems=React.useMemo(()=>{const result=[];(activeTracks||[]).forEach(t=>{if(!t.midiItems||!t.midiItems.length)return;t.midiItems.forEach(m=>{var extended=Object.assign({},m,{_trackId:t.id,_trackName:t.name});result.push(extended);});});return result;},[activeTracks]);const ghostLayers=React.useMemo(function(){// Đang play (main/section) → BỎ ghost: extractGhostLayers duyệt TOÀN BỘ
// project (tracks + notes) — block main thread 100-500ms → noteon
// FluidSynth (scheduled qua setTimeout) TRỄ → âm bị lag 1-2 lần khi mở
// piano roll tab lúc đang play (user bug 06:30). Ghost chỉ là hiển thị
// tham chiếu — không cần khi âm đang chạy — tính lại khi dừng.
if(isPlaying)return[];if(!activeTracks||!st||!st.target_id)return[];var fn=window.SonicGhost&&window.SonicGhost.extractGhostLayers;return fn?fn(activeTracks,st.trackId,st.target_id,parseInt(bpm)||120):[];},[activeTracks,st.trackId,st.target_id,bpm,isPlaying]);const secondsPerBar=60.0/(parseInt(bpm)||120)*4;const activeTargetItem=React.useMemo(function(){if(!activeTracks||!st)return null;var trk=activeTracks.find(function(t){return t.id===st.trackId;});return trk?(trk.midiItems||[]).find(function(m){return m.id===st.target_id;}):null;},[activeTracks,st.trackId,st.target_id]);var activeParentTrackName='';if(st.target_id&&activeTracks){var aptTrk=window.SonicPianoRoll?window.SonicPianoRoll.getParentTrackByItemId(st.target_id,activeTracks):null;if(!aptTrk)aptTrk=activeTracks.find(function(t){return t.id===st.trackId;});if(!aptTrk&&activeTargetItem)aptTrk=activeTracks.find(function(t){return(t.midiItems||[]).some(function(m){return m.id===st.target_id;});});if(aptTrk)activeParentTrackName=aptTrk.name||aptTrk.id;}const sessionStartBar=0;const renderBeatOffset=sessionSyncMode&&activeTargetItem?activeTargetItem.startTime/secondsPerBar*timeSigNum:0;const sessionLengthBars=React.useMemo(function(){var maxSec=0;(activeTracks||[]).forEach(function(tr){(tr.midiItems||[]).forEach(function(m){var end=(m.startTime||0)+(m.duration||4);if(end>maxSec)maxSec=end;});});return Math.ceil((maxSec||4)/secondsPerBar);},[activeTracks,secondsPerBar]);const handleSwitchMidiItem=function(itemId){if(itemId===st.target_id)return;var match=allMidiItems.find(function(m){return m.id===itemId;});if(!match)return;var scope=window.SonicPianoRoll?window.SonicPianoRoll.buildActiveScope(itemId,activeTracks):null;var trk=scope?null:(activeTracks||[]).find(function(t){return t.id===match._trackId;});var newBeatOff=match.startTime/secondsPerBar*timeSigNum;var spb=60.0/(parseInt(bpm)||120);var newTime=0;setSubTabs(function(prev){return prev.map(function(s){if(s.id!==st.id)return s;return Object.assign({},s,{trackId:scope?scope.parent_track_id:match._trackId||trk?.id,target_id:match.id,label:'Piano Roll: '+(match.name||'MIDI'),notes:match.notes||[],duration:match.duration||4,instrumentProgram:scope?scope.instrument_program:trk?trk.instrumentProgram:undefined,instrumentName:scope?scope.instrument_name:trk?trk.instrumentName:undefined,active_scope:scope||null,note_selection:[],currentTime:newTime});});});setSelectedNoteIds([]);};const rawTotalBeats=Math.max(rollBeats,noteMaxBeat+16,64);const drawWidth=rawTotalBeats*pixelsPerBeat;const[rollViewWidth,setRollViewWidth]=React.useState(800);const viewWidth=Math.max(drawWidth,rollViewWidth);const viewBeats=Math.ceil(viewWidth/pixelsPerBeat)+4;const totalBeats=Math.max(rawTotalBeats,viewBeats+32);const[notes,setNotes]=React.useState(st.notes||[]);const notesRef=React.useRef(notes);notesRef.current=notes;React.useEffect(()=>{if(!draggedNoteRef.current)setNotes(st.notes||[]);},[st.notes]);const brushVelocityRef=React.useRef(0.8);const lastNoteDurationRef=React.useRef(null);const previewPitchRef=React.useRef(null);const previewNodesRef=React.useRef(null);var stopPreviewNote=function(){var pn=previewNodesRef.current;if(pn){try{pn.osc.stop();}catch(e){}try{pn.osc.disconnect();}catch(e){}try{pn.gain.disconnect();}catch(e){}previewNodesRef.current=null;}};const[selectedNoteIds,setSelectedNoteIds]=React.useState([]);const[loopStartBeat,setLoopStartBeat]=React.useState(null);const[loopEndBeat,setLoopEndBeat]=React.useState(null);const[isLooping,setIsLooping]=React.useState(false);const rulerDragRef=React.useRef(null);React.useEffect(()=>{const handler=e=>{if((e.ctrlKey||e.metaKey)&&e.key==='a'){const target=e.target;if(target&&(target.tagName==='INPUT'||target.tagName==='TEXTAREA'||target.isContentEditable))return;e.preventDefault();setSelectedNoteIds(notes.map(n=>n.id));}};window.addEventListener('keydown',handler);return()=>window.removeEventListener('keydown',handler);},[notes,setSelectedNoteIds]);// Undo/redo stacks
const undoStackRef=React.useRef([]);const redoStackRef=React.useRef([]);const notesBeforeDragRef=React.useRef(null);const pushToUndo=React.useCallback(prevNotes=>{undoStackRef.current.push(JSON.parse(JSON.stringify(prevNotes)));redoStackRef.current=[];if(undoStackRef.current.length>50)undoStackRef.current.shift();},[]);// ── Humanize: ngẫu nhiên hóa velocity + timing theo cường độ ──
const[humanizeStrength,setHumanizeStrength]=React.useState(0.10);// 0.05 nhẹ / 0.10 vừa / 0.18 mạnh
const applyHumanize=React.useCallback(()=>{if(!notes||!notes.length){showToast('Không có nốt nào để humanize.','warning');return;}const velAmt=humanizeStrength;const timeAmt=humanizeStrength*0.15;// ±0.015 beat @ vừa (~12ms @120bpm)
@@ -974,7 +979,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: 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)return;// PIANO ROLL KHÔNG cần buffer: tab restored từ phiên trước — AudioBuffer
// KHÔNG serialize → st.buffer undefined → return cũ làm playhead ĐỨNG IM
// (user bug 06:15 — mở lại piano roll đã mở phiên trước khi đang play).
// startSubTabPlayback tự tạo silent buffer khi play (18103).
if(!st.buffer&&st.type!=='PIANO_ROLL')return;if(!st.isPlaying){// PIANO ROLL / sub-tab KHÔNG play: playhead theo MAIN/SECTION nếu đang
// play — ĐỒNG BỘ 2 CHIỀU: vị trí play (section/main) → vị trí TRONG
// item (st.currentTime = playPos - item.startTime); playhead vượt ra
// ngoài duration item → bắt đầu từ đầu (0). Không có play nào → đứng
@@ -1039,7 +1048,19 @@ const updateSfRouting=()=>{try{const list=activeTracksRef.current&&activeTracksR
// TRACK đang edit (sfEntry → FX Rack riêng, fallback gainNode) để fader/
// pan/FX áp đúng cho notes đang nghe — kể cả khi project có >1 track MIDI
// audible (bình thường rơi về masterBus.input, mất FX track).
const _activeTabId=activeTabRef.current;const _activeSub=subTabsRef.current?subTabsRef.current.find(s=>s.id===_activeTabId):null;if(_activeSub&&_activeSub.type==='PIANO_ROLL'){// PIANO ROLL của track TRONG SECTION (parent_tab_id = session_*): id
const _activeTabId=activeTabRef.current;const _activeSub=subTabsRef.current?subTabsRef.current.find(s=>s.id===_activeTabId):null;// MAIN/SECTION đang play + đang ở SUB-TAB (piano roll/audio): KHÔNG đổi
// SF destination — âm đang phát thuộc MAIN context (route đã set ĐÚNG
// lúc schedule — masterBus.input — qua mastering). Đổi đích khi âm đang
// phát gây: (a) swap _gainNode → LAG ~1s (bug 06:20); (b) route vào node
// MAIN trùng id (midiAudible nhánh) → sfEntry/sfRouteGain theo ♪ main →
// âm dry NHỎ/không rõ (bug 06:25 — fix 0620). Giữ nguyên route hiện tại.
if(isPlayingRef.current&&_activeTabId&&_activeTabId!=='main'&&!_activeTabId.startsWith('session_')){return;}// MAIN/SECTION đang play (isPlayingRef) → BỎ QUA nhánh PIANO_ROLL:
// đổi SF destination (setOutputDestination — disconnect/reconnect
// _gainNode FluidSynth) khi âm đang phát → âm MIDI bị NGẮT ~1s (user
// bug 06:20 — mở lại piano roll tab restored lúc main play → lag 1s
// rồi mới phát tiếp). Chỉ route vào node piano roll khi KHÔNG play
// (preview tab) — handlePlayPause sub-tab tự stopAllPlayback trước.
if(_activeSub&&_activeSub.type==='PIANO_ROLL'&&!isPlayingRef.current){// PIANO ROLL của track TRONG SECTION (parent_tab_id = session_*): id
// track trùng main track id → lookup key = id trúng node MAIN (route
// theo ♪ main — sai context → MIDI bị kéo dry). ƯU TIÊN SUB-NODE
// (key <parent>_sub_<id> — route theo ♪ section + thẳng masterBus);