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
+29 -3
View File
@@ -6984,10 +6984,16 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
}, [activeTracks]);
const ghostLayers = React.useMemo(function() {
// Đang play (main/section) B ghost: extractGhostLayers duyt TOÀN B
// project (tracks + notes) block main thread 100-500ms noteon
// FluidSynth (scheduled qua setTimeout) TR âm b lag 1-2 ln khi m
// piano roll tab lúc đang play (user bug 06:30). Ghost ch là hin th
// tham chiếu không cn khi âm đang chy tính li khi dng.
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]);
}, [activeTracks, st.trackId, st.target_id, bpm, isPlaying]);
const secondsPerBar = (60.0 / (parseInt(bpm) || 120)) * 4;
const activeTargetItem = React.useMemo(function() {
@@ -18627,7 +18633,12 @@ const App = () => {
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) return;
// PIANO ROLL KHÔNG cn 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 li piano roll đã m phiên trưc khi đang play).
// startSubTabPlayback t to 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 CHIU: v trí play (section/main) v trí TRONG
@@ -19038,7 +19049,22 @@ const App = () => {
// audible (bình thưng rơi v masterBus.input, mt FX track).
const _activeTabId = activeTabRef.current;
const _activeSub = subTabsRef.current ? subTabsRef.current.find(s => s.id === _activeTabId) : null;
if (_activeSub && _activeSub.type === 'PIANO_ROLL') {
// MAIN/SECTION đang play + đang SUB-TAB (piano roll/audio): KHÔNG đi
// SF destination âm đang phát thuc 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 hin ti.
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 NGT ~1s (user
// bug 06:20 m li piano roll tab restored lúc main play lag 1s
// ri mi 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 ca 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
+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);
@@ -84,6 +84,11 @@
if (dest === _outputDestination) return;
_gainNode.disconnect();
_gainNode.connect(dest);
// Reconnect VU analyser tap (app.jsx VU tick gắn __vuAnalyser
// trên _gainNode): disconnect() không đối số ngắt MỌI kết
// nối kể cả analyser → sfAudioPeak = 0 mãi → track VU MIDI
// không nhảy khi có âm (user bug 06:35).
if (_gainNode.__vuAnalyser) _gainNode.connect(_gainNode.__vuAnalyser);
_outputDestination = dest;
console.log('[SonicSF] setOutputDestination to:', node ? 'track node (sfEntry)' : 'masterBus.input');
} else {
+2 -2
View File
@@ -16,7 +16,7 @@
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
<script src="/static/js/services/storage.js?v=202608038200"></script>
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608060630"></script>
<script src="/static/js/services/soundfontPlayer.js?v=202608070635"></script>
<script src="/static/js/services/aiGateway.js?v=202608037200"></script>
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
@@ -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=202608070610" defer></script>
<script src="/static/js/app.precompiled.js?v=202608070630" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+35
View File
@@ -2583,3 +2583,38 @@
- **FIX (app.jsx):** track audioPeak deadband — `if (audioPeak < 0.01) audioPeak = 0;` (sau khi gộp sfAudioPeak) — hết âm → VU tắt NGAY (peak 0 → db -60 → vẽ 0).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070610), `wiki.md`. Rebuild precompiled (build PASS — Deadband ×2: master + track).
- **Ghi chú/Test:** hard refresh → play → playhead ra khỏi vùng âm → VU TẮT NGAY (không còn đoạn 0.008); log `[VU]` không còn audio=0.008.
### [2026-08-07 06:15] Task: PIANO ROLL restored (phiên trước) — playhead đứng im khi play MAIN/SECTION
- **Báo cáo user:** đang play MAIN/SECTION → mở lại PIANO ROLL TAB đã mở từ phiên làm việc trước (restored từ project) → playhead đứng im — không đồng bộ vị trí play.
- **Nguyên nhân:** tab restored KHÔNG có `buffer` (AudioBuffer không serialize được) → nhánh updatePlayhead sub-tab `if (!st || !st.buffer) return;` → return sớm → playhead không cập nhật.
- **FIX (app.jsx updatePlayhead nhánh sub-tab):** `if (!st.buffer && st.type !== 'PIANO_ROLL') return;` — PIANO ROLL không cần buffer (chỉ hiển thị playhead — đồng bộ theo vị trí play MAIN/SECTION); `startSubTabPlayback` tự tạo silent buffer khi play. Audio sub-tab vẫn cần buffer (giữ return cũ).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070615), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → load project có piano roll tab mở sẵn → play MAIN/SECTION → mở lại tab piano roll → playhead DI CHUYỂN đúng vị trí play (đồng bộ); bấm play trong tab → vẫn play bình thường (silent buffer tự tạo).
### [2026-08-07 06:20] Task: Mở PIANO ROLL restored lúc MAIN đang play — âm lag ~1s
- **Báo cáo user:** đang play MAIN/SECTION → mở lại PIANO ROLL TAB (restored phiên trước) → âm thanh bị lag ~1s rồi mới phát tiếp.
- **Nguyên nhân:** effect đổi tab → `updateSfRouting` nhánh PIANO_ROLL → `setOutputDestination` (SF _gainNode FluidSynth) — disconnect + reconnect khi âm đang phát → âm MIDI đang kêu bị NGẮT (dedupe chỉ chặn khi đích KHÔNG đổi — đổi masterBus→node piano roll = swap → ngắt).
- **FIX (app.jsx updateSfRouting):** nhánh PIANO_ROLL thêm điều kiện `&& !isPlayingRef.current` — MAIN/SECTION đang play → BỎ QUA nhánh piano (giữ SF route hiện tại — âm tiếp tục); chỉ route vào node piano roll khi KHÔNG play (preview tab — handlePlayPause sub-tab tự stopAllPlayback trước).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070620), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (có âm MIDI) → mở lại piano roll tab restored → ÂM KHÔNG BỊ NGẮT (phát liên tục — không lag); bấm play trong piano roll tab (không play main) → route SF vào node piano roll như cũ (nghe FX track).
### [2026-08-07 06:25] Task: FIX hồi quy 0620 — âm nhỏ/dry khi mở piano roll restored lúc main play
- **Báo cáo user:** bản cũ (0615) lag nhưng âm TO (qua mastering); bản 0620 hết lag nhưng âm NHỎ/không rõ (mất bypass sai).
- **Nguyên nhân:** fix 0620 skip nhánh PIANO_ROLL → rơi xuống nhánh midiAudible (list = section clones khi activeTab=midi_*) → node lookup trúng NODE MAIN '1' (sfEntry) → SF (âm main đang phát) ĐỔI ĐÍCH → sfEntry main → sfRouteGain theo ♪ main (bypass) → âm DRY/nhỏ.
- **FIX (app.jsx updateSfRouting):** guard ĐẦU HÀM — `isPlayingRef.current && activeTab là sub-tab (midi_*/không main/không session)` → RETURN NGAY (KHÔNG đổi SF destination) — âm main đang phát giữ route đã set đúng lúc schedule (masterBus.input — qua mastering — TO). Hết lag (không swap) + hết bypass sai (không route vào node main).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070625), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (âm MIDI TO — qua mastering) → mở lại piano roll tab restored → ÂM VẪN TO (không nhỏ/dry) + KHÔNG lag; bấm play trong piano roll (main không play) → route SF vào node tab như cũ.
### [2026-08-07 06:30] Task: Mở PIANO ROLL vẫn lag (ngắn hơn, 2 lần) — ghost extract block main thread
- **Báo cáo user:** sau 0625 (SF route guard) vẫn lag khi chuyển sang piano roll restored — ngắn hơn + âm lag 2 lần.
- **Nguyên nhân:** PianoRollTabEditor mount → `ghostLayers` useMemo → `extractGhostLayers(activeTracks,...)` duyệt TOÀN BỘ project (tracks + notes) — block main thread 100-500ms → noteon FluidSynth (scheduled qua setTimeout) TRỄ → âm lag (2 noteon trễ = "lag 2 lần").
- **FIX (app.jsx PianoRollTabEditor ghostLayers):** đang play (`isPlaying` prop — main/section) → return [] (BỎ ghost — chỉ là hiển thị tham chiếu, không cần khi âm chạy) — không block → âm không lag. Khi dừng play → tính ghost bình thường (không âm — block OK). Deps thêm isPlaying.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070630), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN → mở piano roll tab restored → ÂM KHÔNG LAG (to, liên tục); dừng play → ghost notes hiển thị lại bình thường.
### [2026-08-07 06:35] Task: CHỐT VU MIDI — analyser tap bị disconnect() ngắt → VU không nhảy khi có âm
- **Báo cáo user:** (1) [lặp] click playhead → vị trí không âm → VU vẫn diễn (log cũ 0600 — velocity trigger giả); (2) MỚI — play MIDI trên track → KHÔNG có VU animation.
- **Nguyên nhân (chốt cả 2):** VU tick (0605) tap analyser trên `_gainNode` SF (âm MIDI thực) — NHƯNG `setOutputDestination` (soundfontPlayer.js 85) gọi `_gainNode.disconnect()` KHÔNG đối số → ngắt MỌI kết nối KỂ CẢ analyser tap → `sfAudioPeak` = 0 MÃI → (2) VU MIDI không nhảy khi có âm; và (1) `_sfTapped` vẫn true → velocity giả bị bỏ nhưng âm thực = 0 → VU chết hoàn toàn.
- **FIX (soundfontPlayer.js):** `setOutputDestination` — sau `_gainNode.connect(dest)``if (_gainNode.__vuAnalyser) _gainNode.connect(_gainNode.__vuAnalyser);` — analyser sống → sfAudioPeak đọc âm MIDI THỰC → VU nhảy khi có âm ✓; velocity giả đã bỏ (midiPeak=0 khi tapped) → hết nhảy giả khi click playhead vị trí không âm ✓.
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js` (bump ?v=202608070635 trong index.html), `wiki.md`. (app.precompiled giữ 0630 — không đổi logic app.jsx).
- **Ghi chú/Test:** hard refresh → (1) play MIDI track → VU nhảy theo âm thực; (2) click playhead tới vị trí không âm → VU tắt ngay; (3) mở piano roll tab → âm to không lag.