diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 7311715..03effcb 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -14852,11 +14852,142 @@ const App = () => { })); }; + // ── MIDI .mid file parser ── + const parseMidiFile = (arrayBuffer) => { + const data = new Uint8Array(arrayBuffer); + if (data.length < 14) return null; + var pos = 0; + var read32 = function() { var v = (data[pos] << 24) | (data[pos+1] << 16) | (data[pos+2] << 8) | data[pos+3]; pos += 4; return v; }; + var read16 = function() { var v = (data[pos] << 8) | data[pos+1]; pos += 2; return v; }; + var readVLQ = function() { var v = 0, b; do { b = data[pos++]; v = (v << 7) | (b & 0x7f); } while (b & 0x80); return v; }; + + var header = String.fromCharCode(data[0], data[1], data[2], data[3]); + if (header !== 'MThd') return null; + read32(); var fmt = read16(); var numTracks = read16(); var division = read16(); + var ticksPerBeat = (division & 0x8000) ? 480 : (division || 480); + var bpm = 120; + var result = []; + for (var t = 0; t < numTracks; t++) { + if (pos + 8 > data.length) break; + var trkId = String.fromCharCode(data[pos], data[pos+1], data[pos+2], data[pos+3]); + pos += 4; var trkLen = read32(); var endPos = Math.min(pos + trkLen, data.length); + if (trkId !== 'MTrk') { + pos = endPos; + continue; + } + var absTicks = 0; var runningStatus = 0; var trackName = 'MIDI Track ' + (t + 1); + var midiNotes = []; var pendingNotes = {}; + var maxAbsTick = 0; + while (pos < endPos) { + try { + var delta = readVLQ(); absTicks += delta; + var status = data[pos]; + if (status >= 0x80) { + if (status < 0xf0) { + runningStatus = status; + } + pos++; + } else { + status = runningStatus; + } + var cmd = status >> 4; + if (cmd === 0x9 || cmd === 0x8) { + var chan = status & 0x0F; + var pitch = data[pos++]; var vel = pos < endPos ? data[pos++] : 0; + var noteKey = chan + '_' + pitch; + if (cmd === 0x9 && vel > 0) { + pendingNotes[noteKey] = { tick: absTicks, vel: vel }; + if (absTicks > maxAbsTick) maxAbsTick = absTicks; + } else { + var pn = pendingNotes[noteKey]; + if (pn) { + var durTicks = absTicks - pn.tick; + if (durTicks <= 0) durTicks = 240; + midiNotes.push({ id: 'mn_' + t + '_' + pitch + '_' + pn.tick, pitch: pitch, start_beat: pn.tick / ticksPerBeat, duration_beats: durTicks / ticksPerBeat, velocity: Math.min(1, pn.vel / 127) }); + delete pendingNotes[noteKey]; + } + } + } else if (status >= 0xc0 && status < 0xe0) { if (pos < endPos) pos += 1; } + else if (status >= 0xe0 && status < 0xf0) { if (pos + 2 <= endPos) pos += 2; } + else if (status >= 0xa0 && status < 0xc0) { if (pos + 2 <= endPos) pos += 2; } + else if (status >= 0xf0 && status < 0xf8) { if (pos < endPos) { var sl = readVLQ(); pos += sl; } } + else if (status === 0xff) { + if (pos >= endPos) break; + var metaType = data[pos++]; var metaLen = readVLQ(); + if (metaType === 0x03) { try { trackName = String.fromCharCode.apply(null, Array.from(data.subarray(pos, pos + metaLen))); } catch(e) {} } + else if (metaType === 0x51 && pos + 3 <= endPos) { bpm = Math.round(60000000 / ((data[pos] << 16) | (data[pos+1] << 8) | data[pos+2])); } + pos += Math.min(metaLen, endPos - pos); + } + else { if (pos + 2 <= endPos) pos += 2; } + } catch(e) { pos = endPos; } + } + Object.keys(pendingNotes).forEach(function(k) { + var pn = pendingNotes[k]; + var parts = k.split('_'); + var p = parseInt(parts[1]); + var dur = Math.max(240, maxAbsTick - pn.tick); + midiNotes.push({ id: 'mn_' + t + '_' + p + '_' + pn.tick, pitch: p, start_beat: pn.tick / ticksPerBeat, duration_beats: dur / ticksPerBeat, velocity: Math.min(1, pn.vel / 127) }); + }); + if (midiNotes.length > 0) { + var lastEnd = 0; + midiNotes.forEach(function(n) { var e = (n.start_beat + n.duration_beats) * 60 / bpm; if (e > lastEnd) lastEnd = e; }); + result.push({ name: trackName, notes: midiNotes, duration: lastEnd || 4, startTime: 0, id: 'midi_' + t + '_' + Date.now() }); + } + pos = endPos; + } + return result.length > 0 ? result : null; + }; + // ── Load File on Track (with server upload) ── const loadFileOnTrack = async (trackId, file) => { if (!file) return; - showToast(`Đang nạp file ${file.name}...`, 'info'); + var fileName = file.name || ''; + var isMidi = /\.mid$|\.midi$/i.test(fileName); + showToast(`Đang nạp file ${fileName}...`, 'info'); try { + if (isMidi) { + var arrayBuffer = await file.arrayBuffer(); + var midiResult = parseMidiFile(arrayBuffer); + if (!midiResult || midiResult.length === 0) { + showToast('Không tìm thấy nốt nhạc trong file MIDI.', 'error'); + return; + } + if (midiResult.length === 1) { + updateActiveTracks(prev => prev.map(function(t) { + if (t.id !== trackId) return t; + return { ...t, name: fileName, midiItems: [midiResult[0]] }; + })); + showToast('Đã tải MIDI: ' + fileName, 'success'); + } else { + var colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309', '#ca8a04', '#dc2626', '#0891b2']; + var firstDone = false; + midiResult.forEach(function(midiItem, idx) { + if (!firstDone) { + updateActiveTracks(prev => prev.map(function(t) { + if (t.id !== trackId) return t; + return { ...t, name: midiItem.name || fileName, midiItems: [midiItem] }; + })); + firstDone = true; + } else { + var newId = 'midi_track_' + Date.now() + '_' + idx; + updateActiveTracks(function(prev) { + var curLen = prev.length; + return prev.concat([{ + id: newId, name: midiItem.name || 'MIDI Track ' + (idx + 1), + buffer: null, startTime: 0, volumeDb: 0, pan: 0, + muted: false, solo: false, color: colors[idx % colors.length], + markers: [], serverFileId: null, clips: [], sections: [], + midiItems: [midiItem], + isArmed: false, monitoringEnabled: true, + inputSource: { deviceType: 'NONE', deviceId: '' } + }]); + }); + } + }); + showToast('Đã tải MIDI: ' + midiResult.length + ' tracks from ' + fileName, 'success'); + } + return; + } // Upload to server uploadToServer(file, trackId); @@ -14867,13 +14998,13 @@ const App = () => { } = await window.SonicAudio.decodeAudioFile(file); setTracks(prev => prev.map(t => t.id === trackId ? { ...t, - name: file.name, + name: fileName, buffer: decodedBuffer, channelInfo: channelInfo } : t)); - showToast(`Nạp file thành công: ${file.name} (${channelInfo.label})`, 'success'); + showToast(`Nạp file thành công: ${fileName} (${channelInfo.label})`, 'success'); } catch (err) { - showToast("Lỗi giải mã âm thanh. Định dạng file không tương thích.", 'error'); + showToast(isMidi ? "Lỗi giải mã MIDI." : "Lỗi giải mã âm thanh. Định dạng file không tương thích.", 'error'); } }; diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 232fdec..6346534 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -407,10 +407,11 @@ useEffect(()=>{const handleMouseMove=e=>{if(!isSweepingRef.current)return;const 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=>{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){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 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)};}));};// ── MIDI .mid file parser ── +const parseMidiFile=arrayBuffer=>{const data=new Uint8Array(arrayBuffer);if(data.length<14)return null;var pos=0;var read32=function(){var v=data[pos]<<24|data[pos+1]<<16|data[pos+2]<<8|data[pos+3];pos+=4;return v;};var read16=function(){var v=data[pos]<<8|data[pos+1];pos+=2;return v;};var readVLQ=function(){var v=0,b;do{b=data[pos++];v=v<<7|b&0x7f;}while(b&0x80);return v;};var header=String.fromCharCode(data[0],data[1],data[2],data[3]);if(header!=='MThd')return null;read32();var fmt=read16();var numTracks=read16();var division=read16();var ticksPerBeat=division&0x8000?480:division||480;var bpm=120;var result=[];for(var t=0;tdata.length)break;var trkId=String.fromCharCode(data[pos],data[pos+1],data[pos+2],data[pos+3]);pos+=4;var trkLen=read32();var endPos=Math.min(pos+trkLen,data.length);if(trkId!=='MTrk'){pos=endPos;continue;}var absTicks=0;var runningStatus=0;var trackName='MIDI Track '+(t+1);var midiNotes=[];var pendingNotes={};var maxAbsTick=0;while(pos=0x80){if(status<0xf0){runningStatus=status;}pos++;}else{status=runningStatus;}var cmd=status>>4;if(cmd===0x9||cmd===0x8){var chan=status&0x0F;var pitch=data[pos++];var vel=pos0){pendingNotes[noteKey]={tick:absTicks,vel:vel};if(absTicks>maxAbsTick)maxAbsTick=absTicks;}else{var pn=pendingNotes[noteKey];if(pn){var durTicks=absTicks-pn.tick;if(durTicks<=0)durTicks=240;midiNotes.push({id:'mn_'+t+'_'+pitch+'_'+pn.tick,pitch:pitch,start_beat:pn.tick/ticksPerBeat,duration_beats:durTicks/ticksPerBeat,velocity:Math.min(1,pn.vel/127)});delete pendingNotes[noteKey];}}}else if(status>=0xc0&&status<0xe0){if(pos=0xe0&&status<0xf0){if(pos+2<=endPos)pos+=2;}else if(status>=0xa0&&status<0xc0){if(pos+2<=endPos)pos+=2;}else if(status>=0xf0&&status<0xf8){if(pos=endPos)break;var metaType=data[pos++];var metaLen=readVLQ();if(metaType===0x03){try{trackName=String.fromCharCode.apply(null,Array.from(data.subarray(pos,pos+metaLen)));}catch(e){}}else if(metaType===0x51&&pos+3<=endPos){bpm=Math.round(60000000/(data[pos]<<16|data[pos+1]<<8|data[pos+2]));}pos+=Math.min(metaLen,endPos-pos);}else{if(pos+2<=endPos)pos+=2;}}catch(e){pos=endPos;}}Object.keys(pendingNotes).forEach(function(k){var pn=pendingNotes[k];var parts=k.split('_');var p=parseInt(parts[1]);var dur=Math.max(240,maxAbsTick-pn.tick);midiNotes.push({id:'mn_'+t+'_'+p+'_'+pn.tick,pitch:p,start_beat:pn.tick/ticksPerBeat,duration_beats:dur/ticksPerBeat,velocity:Math.min(1,pn.vel/127)});});if(midiNotes.length>0){var lastEnd=0;midiNotes.forEach(function(n){var e=(n.start_beat+n.duration_beats)*60/bpm;if(e>lastEnd)lastEnd=e;});result.push({name:trackName,notes:midiNotes,duration:lastEnd||4,startTime:0,id:'midi_'+t+'_'+Date.now()});}pos=endPos;}return result.length>0?result:null;};// ── Load File on Track (with server upload) ── +const loadFileOnTrack=async(trackId,file)=>{if(!file)return;var fileName=file.name||'';var isMidi=/\.mid$|\.midi$/i.test(fileName);showToast(`Đang nạp file ${fileName}...`,'info');try{if(isMidi){var arrayBuffer=await file.arrayBuffer();var midiResult=parseMidiFile(arrayBuffer);if(!midiResult||midiResult.length===0){showToast('Không tìm thấy nốt nhạc trong file MIDI.','error');return;}if(midiResult.length===1){updateActiveTracks(prev=>prev.map(function(t){if(t.id!==trackId)return t;return{...t,name:fileName,midiItems:[midiResult[0]]};}));showToast('Đã tải MIDI: '+fileName,'success');}else{var colors=['#0f766e','#1d4ed8','#701a75','#a21caf','#b45309','#ca8a04','#dc2626','#0891b2'];var firstDone=false;midiResult.forEach(function(midiItem,idx){if(!firstDone){updateActiveTracks(prev=>prev.map(function(t){if(t.id!==trackId)return t;return{...t,name:midiItem.name||fileName,midiItems:[midiItem]};}));firstDone=true;}else{var newId='midi_track_'+Date.now()+'_'+idx;updateActiveTracks(function(prev){var curLen=prev.length;return prev.concat([{id:newId,name:midiItem.name||'MIDI Track '+(idx+1),buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:colors[idx%colors.length],markers:[],serverFileId:null,clips:[],sections:[],midiItems:[midiItem],isArmed:false,monitoringEnabled:true,inputSource:{deviceType:'NONE',deviceId:''}}]);});}});showToast('Đã tải MIDI: '+midiResult.length+' tracks from '+fileName,'success');}return;}// Upload to server uploadToServer(file,trackId);// Decode locally for playback + analyze channels (stereo/mono) -const{audioBuffer:decodedBuffer,channelInfo}=await window.SonicAudio.decodeAudioFile(file);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,name:file.name,buffer:decodedBuffer,channelInfo:channelInfo}:t));showToast(`Nạp file thành công: ${file.name} (${channelInfo.label})`,'success');}catch(err){showToast("Lỗi giải mã âm thanh. Định dạng file không tương thích.",'error');}};// ── Synth Generators ── +const{audioBuffer:decodedBuffer,channelInfo}=await window.SonicAudio.decodeAudioFile(file);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,name:fileName,buffer:decodedBuffer,channelInfo:channelInfo}:t));showToast(`Nạp file thành công: ${fileName} (${channelInfo.label})`,'success');}catch(err){showToast(isMidi?"Lỗi giải mã MIDI.":"Lỗi giải mã âm thanh. Định dạng file không tương thích.",'error');}};// ── Synth Generators ── const generateSynthToTrack=(trackId,type)=>{const sampleRate=44100;const duration=12.0;const context=getAudioContext();const frameCount=sampleRate*duration;const newBuffer=context.createBuffer(1,frameCount,sampleRate);const channelData=newBuffer.getChannelData(0);if(type==='kick'){for(let i=0;iprev.map(t=>t.id===trackId?{...t,name:`Demo_${type.toUpperCase()}.wav`,buffer:newBuffer}:t));showToast(`Đã nạp sóng âm tổng hợp: ${type.toUpperCase()}`,'success');};// ── Add Track ── const addNewTrack=()=>{const curTracks=activeTracks;const rearrangeNewId=(curTracks.length+1).toString();const colors=['#0f766e','#1d4ed8','#701a75','#a21caf','#b45309'];const selectColor=colors[curTracks.length%colors.length];updateActiveTracks(prev=>[...prev,{id:rearrangeNewId,name:`Track ${rearrangeNewId}`,buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:selectColor,markers:[],serverFileId:null,clips:[],sections:[],midiItems:[],isArmed:false,monitoringEnabled:true,inputSource:{deviceType:'NONE',deviceId:''}}]);showToast(`Đã thêm Track ${rearrangeNewId}.`,'info');setTimeout(()=>lucide.createIcons(),200);return rearrangeNewId;};// ── Update tracks in active context (main session or section tab) ── const updateActiveTracksRef=useCallback(updater=>{const currentSessionTabs=sessionTabsRef.current;const currentActiveTab=activeTabRef.current;var st=currentSessionTabs.find(s=>s.id===currentActiveTab);if(st){setSessionTabs(prev=>prev.map(s=>s.id===currentActiveTab?{...s,tracks:updater(s.tracks),isDirty:true}:s));return;}var pr=subTabsRef.current?subTabsRef.current.find(s=>s.id===currentActiveTab&&s.type==='PIANO_ROLL'):null;if(pr&&pr.parent_tab_id&&pr.parent_tab_id.startsWith('session_')){var parentSt=currentSessionTabs.find(s=>s.id===pr.parent_tab_id);if(parentSt){setSessionTabs(prev=>prev.map(s=>s.id===pr.parent_tab_id?{...s,tracks:updater(s.tracks),isDirty:true}:s));return;}}setTracks(updater);},[]);const updateActiveTracks=updateActiveTracksRef;const toggleTrackArm=trackId=>{updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,isArmed:!t.isArmed};}));setTimeout(()=>lucide.createIcons(),50);};const toggleTrackMonitor=trackId=>{updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,monitoringEnabled:!t.monitoringEnabled};}));setTimeout(()=>lucide.createIcons(),50);};const updateTrackInputSource=(trackId,type,deviceId)=>{updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,inputSource:{deviceType:type,deviceId}};}));};// ── MIDI Export: tạo file MIDI từ tất cả tracks ──