FIX: sửa lỗi fluidsynth error khi toggle percussion kit

This commit is contained in:
2026-07-30 16:25:09 +07:00
parent 15193b13a7
commit 938c8d7f87
2 changed files with 27 additions and 10 deletions
+26 -9
View File
@@ -14708,15 +14708,32 @@ const App = () => {
setTimeout(() => lucide.createIcons(), 50); setTimeout(() => lucide.createIcons(), 50);
}; };
const toggleTrackDrum = trackId => { const toggleTrackDrum = trackId => {
setTracks(prev => prev.map(t => t.id === trackId ? { var mt = tracks || [];
...t, var tidx = mt.findIndex(function(tr) { return tr.id === trackId; });
is_percussion: !t.is_percussion, if (tidx < 0) tidx = 0;
soundfont_bank: !t.is_percussion ? 128 : (t._saved_sf_bank !== undefined ? t._saved_sf_bank : 0), setTracks(prev => prev.map(t => {
synth_engine: t.synth_engine ? { if (t.id !== trackId) return t;
...t.synth_engine, const becomingPercussion = !t.is_percussion;
soundfont_bank: !t.is_percussion ? 128 : 0 let newBank = becomingPercussion ? 128 : (t._saved_sf_bank !== undefined ? t._saved_sf_bank : 0);
} : t.synth_engine let newProgram = becomingPercussion ? 0 : (t._saved_sf_program !== undefined ? t._saved_sf_program : 0);
} : t)); let newCh = becomingPercussion ? 9 : (t._saved_sf_channel !== undefined ? t._saved_sf_channel : (tidx % 16));
return {
...t,
is_percussion: becomingPercussion,
soundfont_bank: newBank,
instrumentProgram: newProgram,
midiChannel: newCh,
_saved_sf_bank: becomingPercussion ? t.soundfont_bank : t._saved_sf_bank,
_saved_sf_program: becomingPercussion ? t.instrumentProgram : t._saved_sf_program,
_saved_sf_channel: becomingPercussion ? t.midiChannel : t._saved_sf_channel,
synth_engine: t.synth_engine ? {
...t.synth_engine,
soundfont_bank: newBank,
soundfont_program: newProgram
} : t.synth_engine
};
}));
setTimeout(() => lucide.createIcons(), 50); setTimeout(() => lucide.createIcons(), 50);
}; };
const updateTrackVolumeDb = (trackId, val) => { const updateTrackVolumeDb = (trackId, val) => {
+1 -1
View File
@@ -405,7 +405,7 @@ useEffect(()=>{const handleMouseMove=e=>{var pd=pendingDragRef.current;if(!pd)re
useEffect(()=>{const handleMouseMove=e=>{const resize=resizedSectionItemRef.current;if(!resize)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;autoScrollTimeline(e.clientX);const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);updateActiveTracks(prev=>prev.map(t=>{if(t.id!==resize.trackId)return t;const items=resize.itemType==='section'?[...(t.sections||[])]:[...(t.midiItems||[])];const idx=items.findIndex(it=>it.id===resize.itemId);if(idx===-1)return t;const item=items[idx];if(resize.side==='left'){const beatSec=60.0/(parseInt(bpm)||120);const newStart=Math.max(0,Math.min(time,resize.originalStart+resize.originalDuration-0.1));const end=resize.originalStart+resize.originalDuration;const newDuration=end-newStart;if(newDuration<0.1)return t;items[idx]=resize.itemType==='section'?{...item,start:newStart,duration:newDuration}:{...item,startTime:newStart,duration:newDuration};}else{const secondsPerBar=60.0/(parseInt(bpm)||120)*4;const marginBar=maxDurationRef.current-secondsPerBar;const clampedTime=Math.min(time,marginBar);const snappedDuration=snapValueRef.current!=='free'?snapTime(clampedTime-resize.originalStart,snapValueRef.current,bpm):clampedTime-resize.originalStart;const newDuration=Math.max(0.1,snappedDuration);items[idx]={...item,duration:newDuration};}return resize.itemType==='section'?{...t,sections:items}:{...t,midiItems:items};}));setCanvasRedrawCount(n=>n+1);const edgePx=(resize.side==='left'?Math.max(0,time):time)*zoom;const keepMargin=80;if(edgePx>scrollLeft+rect.width-keepMargin){wrapper.scrollLeft=edgePx-rect.width+keepMargin;setCanvasRedrawCount(n=>n+1);}else if(edgePx<scrollLeft+keepMargin){wrapper.scrollLeft=Math.max(0,edgePx-keepMargin);setCanvasRedrawCount(n=>n+1);}};const handleMouseUp=()=>{const resize=resizedSectionItemRef.current;if(!resize)return;setResizedSectionItem(null);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Sweep Select mousemove/mouseup ── useEffect(()=>{const handleMouseMove=e=>{const resize=resizedSectionItemRef.current;if(!resize)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;autoScrollTimeline(e.clientX);const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);updateActiveTracks(prev=>prev.map(t=>{if(t.id!==resize.trackId)return t;const items=resize.itemType==='section'?[...(t.sections||[])]:[...(t.midiItems||[])];const idx=items.findIndex(it=>it.id===resize.itemId);if(idx===-1)return t;const item=items[idx];if(resize.side==='left'){const beatSec=60.0/(parseInt(bpm)||120);const newStart=Math.max(0,Math.min(time,resize.originalStart+resize.originalDuration-0.1));const end=resize.originalStart+resize.originalDuration;const newDuration=end-newStart;if(newDuration<0.1)return t;items[idx]=resize.itemType==='section'?{...item,start:newStart,duration:newDuration}:{...item,startTime:newStart,duration:newDuration};}else{const secondsPerBar=60.0/(parseInt(bpm)||120)*4;const marginBar=maxDurationRef.current-secondsPerBar;const clampedTime=Math.min(time,marginBar);const snappedDuration=snapValueRef.current!=='free'?snapTime(clampedTime-resize.originalStart,snapValueRef.current,bpm):clampedTime-resize.originalStart;const newDuration=Math.max(0.1,snappedDuration);items[idx]={...item,duration:newDuration};}return resize.itemType==='section'?{...t,sections:items}:{...t,midiItems:items};}));setCanvasRedrawCount(n=>n+1);const edgePx=(resize.side==='left'?Math.max(0,time):time)*zoom;const keepMargin=80;if(edgePx>scrollLeft+rect.width-keepMargin){wrapper.scrollLeft=edgePx-rect.width+keepMargin;setCanvasRedrawCount(n=>n+1);}else if(edgePx<scrollLeft+keepMargin){wrapper.scrollLeft=Math.max(0,edgePx-keepMargin);setCanvasRedrawCount(n=>n+1);}};const handleMouseUp=()=>{const resize=resizedSectionItemRef.current;if(!resize)return;setResizedSectionItem(null);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Sweep Select mousemove/mouseup ──
useEffect(()=>{const handleMouseMove=e=>{if(!isSweepingRef.current)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);setSweepSelect(prev=>{var updated=prev?{...prev,endTime:time}:null;sweepSelectRef.current=updated;return updated;});};const handleMouseUp=()=>{if(!isSweepingRef.current)return;isSweepingRef.current=false;sweepTrackIdRef.current=null;const sweep=sweepSelectRef.current;sweepSelectRef.current=null;captureSelectionUndo();if(sweep){const start=Math.min(sweep.startTime,sweep.endTime);const end=Math.max(sweep.startTime,sweep.endTime);if(Math.abs(end-start)<0.02){setSelectedItemIds(new Set());setSweepSelect(null);pushSelectionUndo();return;}const curTracks=activeTracksRef.current||[];const found=new Set();curTracks.forEach(t=>{(t.midiItems||[]).forEach(m=>{if(m.startTime<end&&m.startTime+m.duration>start){found.add(m.id);}});(t.sections||[]).forEach(s=>{if(s.start<end&&s.start+s.duration>start){found.add(s.id);}});(t.clips||[]).forEach(c=>{const dur=c.buffer?c.buffer.duration/(c.speed||1.0):4;if(c.startTime<end&&c.startTime+dur>start){const cid=c.id==='default'?'default_'+t.id:c.id;found.add(cid);}});});setSelectedItemIds(prev=>{const next=new Set(prev);found.forEach(id=>{if(next.has(id))next.delete(id);else next.add(id);});return next;});setSweepSelect(null);sweepTrackIdRef.current=null;pushSelectionUndo();}};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom]);const handleSelectRange=(start,end,reset)=>{captureSelectionUndo();const maxLen=maxDuration;const cleanStart=Math.max(0,Math.min(maxLen,start));const cleanEnd=Math.max(0,Math.min(maxLen,end));if(reset){setSelectionStart(cleanStart);setSelectionEnd(cleanEnd);}else{setSelectionEnd(cleanEnd);}setSelectionCleared(false);pushSelectionUndo();};const handleSelectionInputChange=(field,val)=>{captureSelectionUndo();const numericVal=Math.max(0,parseFloat(val)||0);if(selectionMode==='local'){if(field==='start'){setLocalSelectionStart(numericVal);}else{setLocalSelectionEnd(numericVal);}}else{if(field==='start'){setSelectionStart(numericVal);}else{setSelectionEnd(numericVal);}}pushSelectionUndo();};const selectionStats=useMemo(()=>{if(selLeft===null||selRight===null){return{start:0,end:0,length:0};}const s=Math.min(selLeft,selRight);const e=Math.max(selLeft,selRight);return{start:parseFloat(s.toFixed(3)),end:parseFloat(e.toFixed(3)),length:parseFloat((e-s).toFixed(3))};},[selLeft,selRight]);// ── Handle Drag (selection resize) ── useEffect(()=>{const handleMouseMove=e=>{if(!isSweepingRef.current)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const time=Math.max(0,mouseX/zoom-leadInMarginRef.current);setSweepSelect(prev=>{var updated=prev?{...prev,endTime:time}:null;sweepSelectRef.current=updated;return updated;});};const handleMouseUp=()=>{if(!isSweepingRef.current)return;isSweepingRef.current=false;sweepTrackIdRef.current=null;const sweep=sweepSelectRef.current;sweepSelectRef.current=null;captureSelectionUndo();if(sweep){const start=Math.min(sweep.startTime,sweep.endTime);const end=Math.max(sweep.startTime,sweep.endTime);if(Math.abs(end-start)<0.02){setSelectedItemIds(new Set());setSweepSelect(null);pushSelectionUndo();return;}const curTracks=activeTracksRef.current||[];const found=new Set();curTracks.forEach(t=>{(t.midiItems||[]).forEach(m=>{if(m.startTime<end&&m.startTime+m.duration>start){found.add(m.id);}});(t.sections||[]).forEach(s=>{if(s.start<end&&s.start+s.duration>start){found.add(s.id);}});(t.clips||[]).forEach(c=>{const dur=c.buffer?c.buffer.duration/(c.speed||1.0):4;if(c.startTime<end&&c.startTime+dur>start){const cid=c.id==='default'?'default_'+t.id:c.id;found.add(cid);}});});setSelectedItemIds(prev=>{const next=new Set(prev);found.forEach(id=>{if(next.has(id))next.delete(id);else next.add(id);});return next;});setSweepSelect(null);sweepTrackIdRef.current=null;pushSelectionUndo();}};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom]);const handleSelectRange=(start,end,reset)=>{captureSelectionUndo();const maxLen=maxDuration;const cleanStart=Math.max(0,Math.min(maxLen,start));const cleanEnd=Math.max(0,Math.min(maxLen,end));if(reset){setSelectionStart(cleanStart);setSelectionEnd(cleanEnd);}else{setSelectionEnd(cleanEnd);}setSelectionCleared(false);pushSelectionUndo();};const handleSelectionInputChange=(field,val)=>{captureSelectionUndo();const numericVal=Math.max(0,parseFloat(val)||0);if(selectionMode==='local'){if(field==='start'){setLocalSelectionStart(numericVal);}else{setLocalSelectionEnd(numericVal);}}else{if(field==='start'){setSelectionStart(numericVal);}else{setSelectionEnd(numericVal);}}pushSelectionUndo();};const selectionStats=useMemo(()=>{if(selLeft===null||selRight===null){return{start:0,end:0,length:0};}const s=Math.min(selLeft,selRight);const e=Math.max(selLeft,selRight);return{start:parseFloat(s.toFixed(3)),end:parseFloat(e.toFixed(3)),length:parseFloat((e-s).toFixed(3))};},[selLeft,selRight]);// ── Handle Drag (selection resize) ──
const handleHandleDragStart=(e,side)=>{e.preventDefault();e.stopPropagation();const startX=e.clientX;const useLocal=selectionMode==='local';const currentStart=useLocal?localSelectionStart:selectionStart;const currentEnd=useLocal?localSelectionEnd:selectionEnd;const initialLeft=Math.min(currentStart,currentEnd);const initialRight=Math.max(currentStart,currentEnd);const setStart=useLocal?setLocalSelectionStart:setSelectionStart;const setEnd=useLocal?setLocalSelectionEnd:setSelectionEnd;const handleMouseMove=moveEvent=>{const deltaX=moveEvent.clientX-startX;const deltaSec=deltaX/zoom;if(side==='left'){const newLeft=Math.max(0,Math.min(initialRight-0.05,initialLeft+deltaSec));setStart(newLeft);setEnd(initialRight);}else{const newRight=Math.max(initialLeft+0.05,Math.min(maxDuration,initialRight+deltaSec));setStart(initialLeft);setEnd(newRight);}};const handleMouseUp=()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);};const handleSelectionBodyDragStart=e=>{e.preventDefault();e.stopPropagation();const startX=e.clientX;const useLocal=selectionMode==='local';const currentStart=useLocal?localSelectionStart:selectionStart;const currentEnd=useLocal?localSelectionEnd:selectionEnd;const initialLeft=Math.min(currentStart,currentEnd);const initialRight=Math.max(currentStart,currentEnd);const widthSec=initialRight-initialLeft;const setStart=useLocal?setLocalSelectionStart:setSelectionStart;const setEnd=useLocal?setLocalSelectionEnd:setSelectionEnd;const handleMouseMove=moveEvent=>{const deltaX=moveEvent.clientX-startX;const deltaSec=deltaX/zoom;let newLeft=initialLeft+deltaSec;let newRight=initialRight+deltaSec;if(newLeft<0){newLeft=0;newRight=widthSec;}if(newRight>maxDuration){newRight=maxDuration;newLeft=maxDuration-widthSec;}setStart(newLeft);setEnd(newRight);};const handleMouseUp=()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);};// ── Track Controls ── const handleHandleDragStart=(e,side)=>{e.preventDefault();e.stopPropagation();const startX=e.clientX;const useLocal=selectionMode==='local';const currentStart=useLocal?localSelectionStart:selectionStart;const currentEnd=useLocal?localSelectionEnd:selectionEnd;const initialLeft=Math.min(currentStart,currentEnd);const initialRight=Math.max(currentStart,currentEnd);const setStart=useLocal?setLocalSelectionStart:setSelectionStart;const setEnd=useLocal?setLocalSelectionEnd:setSelectionEnd;const handleMouseMove=moveEvent=>{const deltaX=moveEvent.clientX-startX;const deltaSec=deltaX/zoom;if(side==='left'){const newLeft=Math.max(0,Math.min(initialRight-0.05,initialLeft+deltaSec));setStart(newLeft);setEnd(initialRight);}else{const newRight=Math.max(initialLeft+0.05,Math.min(maxDuration,initialRight+deltaSec));setStart(initialLeft);setEnd(newRight);}};const handleMouseUp=()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);};const handleSelectionBodyDragStart=e=>{e.preventDefault();e.stopPropagation();const startX=e.clientX;const useLocal=selectionMode==='local';const currentStart=useLocal?localSelectionStart:selectionStart;const currentEnd=useLocal?localSelectionEnd:selectionEnd;const initialLeft=Math.min(currentStart,currentEnd);const initialRight=Math.max(currentStart,currentEnd);const widthSec=initialRight-initialLeft;const setStart=useLocal?setLocalSelectionStart:setSelectionStart;const setEnd=useLocal?setLocalSelectionEnd:setSelectionEnd;const handleMouseMove=moveEvent=>{const deltaX=moveEvent.clientX-startX;const deltaSec=deltaX/zoom;let newLeft=initialLeft+deltaSec;let newRight=initialRight+deltaSec;if(newLeft<0){newLeft=0;newRight=widthSec;}if(newRight>maxDuration){newRight=maxDuration;newLeft=maxDuration-widthSec;}setStart(newLeft);setEnd(newRight);};const handleMouseUp=()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);};// ── Track Controls ──
const toggleTrackSoloEvaluate=trackId=>{const wasPlaying=isPlaying;stopAllPlayback();setTracks(prev=>prev.map(t=>{if(t.id===trackId){return{...t,solo:!t.solo,muted:false};}return{...t,solo:false};}));if(wasPlaying){setTimeout(()=>{startOffsetTimeRef.current=currentTime;startAudioTimeRef.current=getAudioContext().currentTime;setIsPlaying(true);},50);}setTimeout(()=>lucide.createIcons(),50);};const toggleTrackMute=trackId=>{const beforeSnap=captureTrackSnapshot(trackId);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,muted:!t.muted}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'MUTE',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});setTimeout(()=>lucide.createIcons(),50);};const updateTrackProp=(trackId,props)=>{setTracks(prev=>prev.map(t=>t.id===trackId?{...t,...props}:t));setTimeout(()=>lucide.createIcons(),50);};const toggleTrackDrum=trackId=>{setTracks(prev=>prev.map(t=>t.id===trackId?{...t,is_percussion:!t.is_percussion,soundfont_bank:!t.is_percussion?128:t._saved_sf_bank!==undefined?t._saved_sf_bank:0,synth_engine:t.synth_engine?{...t.synth_engine,soundfont_bank:!t.is_percussion?128:0}:t.synth_engine}:t));setTimeout(()=>lucide.createIcons(),50);};const updateTrackVolumeDb=(trackId,val)=>{const beforeSnap=captureTrackSnapshot(trackId);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,volumeDb:val}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'VOLUME_CHANGE',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});// Real-time update during playback const toggleTrackSoloEvaluate=trackId=>{const wasPlaying=isPlaying;stopAllPlayback();setTracks(prev=>prev.map(t=>{if(t.id===trackId){return{...t,solo:!t.solo,muted:false};}return{...t,solo:false};}));if(wasPlaying){setTimeout(()=>{startOffsetTimeRef.current=currentTime;startAudioTimeRef.current=getAudioContext().currentTime;setIsPlaying(true);},50);}setTimeout(()=>lucide.createIcons(),50);};const toggleTrackMute=trackId=>{const beforeSnap=captureTrackSnapshot(trackId);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,muted:!t.muted}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'MUTE',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});setTimeout(()=>lucide.createIcons(),50);};const updateTrackProp=(trackId,props)=>{setTracks(prev=>prev.map(t=>t.id===trackId?{...t,...props}:t));setTimeout(()=>lucide.createIcons(),50);};const toggleTrackDrum=trackId=>{var mt=tracks||[];var tidx=mt.findIndex(function(tr){return tr.id===trackId;});if(tidx<0)tidx=0;setTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;const becomingPercussion=!t.is_percussion;let newBank=becomingPercussion?128:t._saved_sf_bank!==undefined?t._saved_sf_bank:0;let newProgram=becomingPercussion?0:t._saved_sf_program!==undefined?t._saved_sf_program:0;let newCh=becomingPercussion?9:t._saved_sf_channel!==undefined?t._saved_sf_channel:tidx%16;return{...t,is_percussion:becomingPercussion,soundfont_bank:newBank,instrumentProgram:newProgram,midiChannel:newCh,_saved_sf_bank:becomingPercussion?t.soundfont_bank:t._saved_sf_bank,_saved_sf_program:becomingPercussion?t.instrumentProgram:t._saved_sf_program,_saved_sf_channel:becomingPercussion?t.midiChannel:t._saved_sf_channel,synth_engine:t.synth_engine?{...t.synth_engine,soundfont_bank:newBank,soundfont_program:newProgram}:t.synth_engine};}));setTimeout(()=>lucide.createIcons(),50);};const updateTrackVolumeDb=(trackId,val)=>{const beforeSnap=captureTrackSnapshot(trackId);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,volumeDb:val}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'VOLUME_CHANGE',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});// Real-time update during playback
const nodes=activeTrackNodesRef.current[trackId];if(nodes){const volLinear=val<=-50?0:Math.pow(10,val/20);nodes.gainNode.gain.setValueAtTime(volLinear,getAudioContext().currentTime);}};const updateTrackPan=(trackId,val)=>{const beforeSnap=captureTrackSnapshot(trackId);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,pan:val}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'PAN_CHANGE',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});// Real-time update during playback const nodes=activeTrackNodesRef.current[trackId];if(nodes){const volLinear=val<=-50?0:Math.pow(10,val/20);nodes.gainNode.gain.setValueAtTime(volLinear,getAudioContext().currentTime);}};const updateTrackPan=(trackId,val)=>{const beforeSnap=captureTrackSnapshot(trackId);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,pan:val}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'PAN_CHANGE',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});// Real-time update during playback
const nodes=activeTrackNodesRef.current[trackId];if(nodes){nodes.pannerNode.pan.setValueAtTime(val/100,getAudioContext().currentTime);}};const updateTrackName=(trackId,name)=>{const beforeSnap=captureTrackSnapshot(trackId);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,name}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'RENAME',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});};const updateTrackColor=(trackId,color)=>{const beforeSnap=captureTrackSnapshot(trackId);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,color}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'RECOLOR',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});};const updateClipName=(trackId,clipId,newName)=>{setTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;const clips=t.clips&&t.clips.length>0?t.clips:[];return{...t,clips:clips.map(c=>c.id===clipId?{...c,name:newName}:c)};}));};// ── Load File on Track (with server upload) ── const nodes=activeTrackNodesRef.current[trackId];if(nodes){nodes.pannerNode.pan.setValueAtTime(val/100,getAudioContext().currentTime);}};const updateTrackName=(trackId,name)=>{const beforeSnap=captureTrackSnapshot(trackId);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,name}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'RENAME',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});};const updateTrackColor=(trackId,color)=>{const beforeSnap=captureTrackSnapshot(trackId);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,color}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'RECOLOR',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});};const updateClipName=(trackId,clipId,newName)=>{setTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;const clips=t.clips&&t.clips.length>0?t.clips:[];return{...t,clips:clips.map(c=>c.id===clipId?{...c,name:newName}:c)};}));};// ── Load File on Track (with server upload) ──
const loadFileOnTrack=async(trackId,file)=>{if(!file)return;showToast(`Đang nạp file ${file.name}...`,'info');try{// Upload to server const loadFileOnTrack=async(trackId,file)=>{if(!file)return;showToast(`Đang nạp file ${file.name}...`,'info');try{// Upload to server