From c3182f6baa4a126fcffcf453dff9d165035eda7d Mon Sep 17 00:00:00 2001 From: 3dtours Date: Fri, 7 Aug 2026 18:58:29 +0700 Subject: [PATCH] =?UTF-8?q?FIX:=20s=E1=BB=ADa=20l=E1=BB=97i=20kh=C3=B4ng?= =?UTF-8?q?=20hi=E1=BB=83n=20th=E1=BB=8B=20=C4=91=C3=BAng=20n=E1=BB=99i=20?= =?UTF-8?q?dung=20c=E1=BB=A7a=20SECTION-TAB=20trong=20section=20item?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 272 +++++++++++++++++++++++++++---- app/static/js/app.precompiled.js | 104 +++++++++--- app/templates/index.html | 2 +- wiki.md | 89 ++++++++++ 4 files changed, 414 insertions(+), 53 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 65a7863..feb5c6a 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -2579,6 +2579,10 @@ const WaveformLane = ({ notes.forEach(note => { const beatSec = 60.0 / (parseInt(bpm) || 120); const noteStartSec = (note.start_beat || 0) * beatSec; + // CLIP theo item duration: item bị KÉO NGẮN (trim — duration 4 bars + // nhưng notes vẫn còn 8 bars) → note ngoài duration KHÔNG vẽ — + // canvas MAIN phải hiển thị đúng phần đã trim (user 08:20) + if (noteStartSec >= (item.duration || 0) - 0.01) return; const noteDurSec = Math.max(0.02, (note.duration_beats || 0.25) * beatSec); const noteStartLocal = itemStartLocal + noteStartSec * zoom; const nw = noteDurSec * zoom; @@ -2892,6 +2896,10 @@ const WaveformLane = ({ if (hitItem && !e.altKey && !e.shiftKey) { e.preventDefault(); e.stopPropagation(); + // Chọn TRACK chứa item (user 07:55: Ctrl+V phải paste vào track đang + // select — click item không qua onTrackLaneMouseDown → selectedTrackId + // giữ track cũ → paste sai track) + if (onSelectTrack) onSelectTrack(track.id); if (e.ctrlKey) { // Ctrl+Click: toggle selection (add/remove from group) + set pending copy-drag var preToggle = selectedItemIds ? new Set(selectedItemIds) : new Set(); @@ -3084,10 +3092,12 @@ const WaveformLane = ({ if (time >= sec.start && time < sec.start + sec.duration) { hitSectionId = sec.id; break; } } // Detect MIDI item dưới chuột (user 07:25 — Cut/Copy item phải copy - // ĐÚNG item — trước đây context menu chỉ biết track → copy nhầm buffer) + // ĐÚNG item — bounds × secondsPerBar (m.duration tính BEATS — như + // contextMenuDelete 17452) — thiếu × spb → detect miss → copy nhầm track) + const _spb = (60.0 / (parseInt(bpm) || 120)) * 4; let hitMidiId = null; for (const m of (track.midiItems || [])) { - if (time >= m.startTime && time < m.startTime + (m.duration || 4)) { hitMidiId = m.id; break; } + if (time >= m.startTime && time < m.startTime + (m.duration || 4) * _spb) { hitMidiId = m.id; break; } } // Detect clip dưới chuột let hitClipId = null; @@ -14949,10 +14959,12 @@ const App = () => { } const prevSub = subTabsRef.current.find(s => s.id === prevTab); if (prevSub && prevSub.isPlaying) { - // PIANO ROLL tab: KHÔNG stop âm khi rời tab (SF notes đã schedule — - // tiếp tục kêu tự nhiên — user: chuyển tab qua lại KHÔNG được câm). - // Audio sub-tab (buffer source riêng): vẫn stop như cũ. - if (prevSub.type !== 'PIANO_ROLL') { + // Chuyển sang SUB-TAB KHÁC (piano roll khác/audio tab) → STOP âm tab + // cũ — play ĐÚNG nội dung TỪNG TAB (user 07:30: piano roll tab 1 → + // tab 2 — không được nghe âm tab 1). Về MAIN/SECTION → giữ luật cũ + // (04:15+ — âm tiếp/resume). + const _newIsSub = activeTab && activeTab !== 'main' && !activeTab.startsWith('session_'); + if (prevSub.type !== 'PIANO_ROLL' || _newIsSub) { try { stopAllPlayback(); } catch (e) {} } setSubTabs(prev => prev.map(s => s.id === prevTab ? { ...s, isPlaying: false } : s)); @@ -15770,6 +15782,23 @@ const App = () => { const handleDeleteSelectedItemsRef = useRef(() => {}); handleDeleteSelectedItemsRef.current = (idsToDelete) => { const count = idsToDelete.size; + // Close piano roll tabs của midi items bị xóa (user 07:40 — áp cả + // SECTION-TAB: xóa midi item → close tab đã mở của item đó) + setSubTabs(prev => { + const next = prev.filter(s => !(s.target_id && idsToDelete.has(s.target_id))); + // CHỈ về main khi tab đang active là PIANO ROLL (midi_*) bị close — + // KHÔNG đá SECTION-TAB về main (user 07:45) + if (activeTabRef.current && activeTabRef.current.startsWith('midi_') && !next.some(s => s.id === activeTabRef.current)) { + setActiveTab('main'); + } + return next; + }); + // Sync nội dung section tab → section item trên MAIN (canvas vẽ lại — + // user 07:45) — setTimeout 0: chạy SAU updateActiveTracks (data mới) + if (activeTabRef.current && activeTabRef.current.startsWith('session_')) { + const _tabId = activeTabRef.current; + setTimeout(() => { try { syncSectionTabToMain(_tabId); } catch (e) {} }, 0); + } setSelectedItemIds(new Set()); updateActiveTracks(prev => prev.map(t => { let changed = false; @@ -16100,6 +16129,58 @@ const App = () => { } : s)); showToast(`Đã lặp vùng chọn ${loopCount} lần.`, 'success'); }; + + // ── Item copy/cut helpers (Ctrl+C/X — user 07:50: phải copy/cut ITEM được + // chọn, không phải track) ── + const findItemContext = (itemId) => { + for (const t of (activeTracksRef.current || [])) { + if ((t.midiItems || []).some(m => m.id === itemId)) return { trackId: t.id, itemType: 'midi' }; + if ((t.clips || []).some(c => c.id === itemId)) return { trackId: t.id, itemType: 'clip' }; + } + return null; + }; + const copyItemToClipboard = (trackId, itemId, itemType) => { + // Dùng REF (không phải activeTracks closure cũ): Ctrl+C/X chạy trong keydown + // effect deps [] → closure giữ activeTracks RENDER ĐẦU — item TẠO SAU phiên + // (vd midi_1786099252336) không tồn tại trong closure cũ → copy fail → cắt + // nhầm track (user 08:10 — log [CutItem] found= 2/midi nhưng không cắt). + const trk = (activeTracksRef.current || []).find(t => t.id === trackId); + if (!trk) return false; + const isMidi = itemType === 'midi' || (!itemType && (trk.midiItems || []).some(m => m.id === itemId)); + if (isMidi) { + const item = (trk.midiItems || []).find(m => m.id === itemId); + if (!item) return false; + // duration midiItem tính GIÂY (insertMidiItem = 4*spb — scheduling dùng + // seconds) — KHÔNG nhân (60/bpm) — nhân làm duration gấp đôi → block + // paste bị dài/ngắn sai (user 07:55). + clipboardRef.current = { + type: 'midi', + notes: (item.notes || []).map(n => ({ ...n })), + duration: item.duration || 4, + name: item.name || 'MIDI Item', + color: item.color || '#a855f7' + }; + window.globalStudioClipboard = clipboardRef.current; + return true; + } + const item = (trk.clips || []).find(c => c.id === itemId); + if (item && item.buffer) { + clipboardRef.current = { + buffer: item.buffer, + name: item.name || trk.name, + volumeDb: trk.volumeDb, + pan: trk.pan, + color: item.color || trk.color, + sampleRate: item.buffer.sampleRate, + channels: item.buffer.numberOfChannels, + speed: item.speed || 1.0 + }; + window.globalStudioClipboard = clipboardRef.current; + return true; + } + return false; + }; + useEffect(() => { const handler = e => { // Bypass global hotkeys when typing inside input/textarea/contentEditable elements @@ -16323,17 +16404,47 @@ const App = () => { } if (ctrl && !alt && e.key === 'c') { e.preventDefault(); - handleCopyTrack(); + const selItems = selectedItemIdsRef.current; + if (selItems && selItems.size > 0) { + // Copy ITEM đầu tiên được chọn (user 07:50 — Ctrl+C phải copy item) + const firstId = selItems.values().next().value; + const found = findItemContext(firstId); + if (found && copyItemToClipboard(found.trackId, firstId, found.itemType)) { + showToast('Đã sao chép item.', 'info'); + } else { + handleCopyTrack(); + } + } else { + handleCopyTrack(); + } return; } if (ctrl && !alt && e.key === 'x') { e.preventDefault(); - handleCutTrack(); + e.stopPropagation(); // chặn browser cut (user 07:55 — Ctrl-X bị capture) + const selItems = selectedItemIdsRef.current; + if (selItems && selItems.size > 0) { + // Cut ITEM đầu tiên được chọn: copy + xóa item (+ close piano roll + // tab + sync section — handleDeleteSelectedItemsRef đã xử lý) + const firstId = selItems.values().next().value; + const found = findItemContext(firstId); + console.log('[CutItem] Ctrl+X — itemId=', firstId, 'found=', found ? found.trackId + '/' + found.itemType : 'null', 'selSize=', selItems.size); + if (found && copyItemToClipboard(found.trackId, firstId, found.itemType)) { + handleDeleteSelectedItemsRef.current(new Set([firstId])); + showToast('Đã cắt item.', 'info'); + } else { + handleCutTrack(); + } + } else { + handleCutTrack(); + } return; } if (ctrl && !alt && e.key === 'v') { e.preventDefault(); - handlePasteTrack(); + // Dùng REF — effect deps [] → handlePasteTrack closure CŨ (selectedTrackId + // stale = '1' → paste sai track — user 08:05) + handlePasteTrackRef.current(); return; } if (e.key === 'Delete' || e.key === 'Backspace' || e.key === 'Del') { @@ -16386,10 +16497,10 @@ const App = () => { if (ctrl && !alt && e.key === 's') { e.preventDefault(); const curTab = activeTabRef.current; - if (curTab === 'main') { - // handled by main handler - } else if (curTab.startsWith('session_')) { - // Main session: save project + save all dirty sub-tabs + if (curTab === 'main' || (curTab && curTab.startsWith('session_'))) { + // MAIN (hoặc SECTION): save project + save TẤT CẢ dirty sub-tabs + // (piano roll/audio/section) — user 07:35: nhấn lưu ở MAIN phải lưu + // cả dirty tab (trước đây nhánh main RỖNG — dirty piano roll bị mất). handleSaveProject(); subTabsRef.current.filter(s => s.isDirty).forEach(st => { if (st.type === 'PIANO_ROLL') { @@ -16403,12 +16514,12 @@ const App = () => { updateActiveTracks(prev => prev.map(t => t.id === st.trackId ? { ...t, buffer: st.buffer } : t)); } } - showToast('Đã lưu', 'success'); }); - } else if (curTab.startsWith('session_')) { - // Section tab: save section - handleSaveSectionTabRef.current(curTab); - showToast('Đã lưu Section', 'success'); + if (curTab.startsWith('session_')) { + // SECTION-TAB: cũng lưu section hiện tại + handleSaveSectionTabRef.current(curTab); + } + showToast('Đã lưu dự án + các tab đã sửa', 'success'); } else { // Sub-tab: save current tab const st = subTabsRef.current.find(s => s.id === curTab); @@ -16737,10 +16848,25 @@ const App = () => { showToast('Đã lưu các chỉnh sửa nốt MIDI vào item cha!', 'success'); }; + // Sync nội dung SECTION-TAB → section item trên MAIN (sec.tracks) — cập nhật + // canvas section item ngay khi thay đổi nội dung tab (user 07:45 — xóa item + // trong SECTION-TAB → canvas section item ở MAIN phải vẽ lại). KHÔNG đổi + // duration (0505 — giữ kích thước user resize). + const syncSectionTabToMain = (tabId) => { + const tab = sessionTabsRef.current.find(s => s.id === tabId); + if (!tab || !tab.sectionId) return; + setTracks(prev => prev.map(t => ({ + ...t, + sections: (t.sections || []).map(s => { + if (s.sectionId !== tab.sectionId && s.id !== tab.sectionId) return s; + return { ...s, tracks: tab.tracks }; + }) + }))); + }; + const handleSaveSectionTab = async (tabId) => { const tab = sessionTabs.find(s => s.id === tabId); if (!tab) return; - // Upload buffer-only clips (chưa có serverFileId — upload sớm thất bại/ // race) TRƯỚC khi ghi vào section item → serialized AUDIO_ITEM có file → // reload không mất audioclip. @@ -17471,12 +17597,29 @@ const App = () => { const afterSnap = captureTrackSnapshot(tid); pushAction('DELETE_SECTION', tid, beforeSnap, afterSnap); closeContextMenu(); + // Sync section tab → section item trên MAIN (canvas vẽ lại — 07:45) + if (activeTabRef.current && activeTabRef.current.startsWith('session_')) { + setTimeout(() => { try { syncSectionTabToMain(activeTabRef.current); } catch (e) {} }, 0); + } showToast('Đã xoá section item.', 'info'); return; } if (clickedMidi) { const beforeSnap = captureTrackSnapshot(tid); + // Close piano roll tab mở của midi item này (user 07:35) + setSubTabs(prev => { + const next = prev.filter(s => s.target_id !== clickedMidi.id); + // CHỈ về main khi tab active là PIANO ROLL bị close — giữ SECTION-TAB + if (activeTabRef.current && activeTabRef.current.startsWith('midi_') && !next.some(s => s.id === activeTabRef.current)) { + setActiveTab('main'); + } + return next; + }); + // Sync section tab → section item trên MAIN (canvas vẽ lại — 07:45) + if (activeTabRef.current && activeTabRef.current.startsWith('session_')) { + setTimeout(() => { try { syncSectionTabToMain(activeTabRef.current); } catch (e) {} }, 0); + } updateActiveTracks(prev => prev.map(t => { if (t.id !== tid) return t; return { @@ -17507,6 +17650,10 @@ const App = () => { const afterSnap = captureTrackSnapshot(tid); pushAction('DELETE_CLIP', tid, beforeSnap, afterSnap); closeContextMenu(); + // Sync section tab → section item trên MAIN (canvas vẽ lại — 07:45) + if (activeTabRef.current && activeTabRef.current.startsWith('session_')) { + setTimeout(() => { try { syncSectionTabToMain(activeTabRef.current); } catch (e) {} }, 0); + } showToast('Đã xoá audio clip.', 'info'); return; } @@ -17559,11 +17706,10 @@ const App = () => { const trk = activeTracks.find(t => t.id === contextMenu.trackId); const item = trk && (trk.midiItems || []).find(m => m.id === contextMenu.itemId); if (item) { - const bpmVal = parseInt(bpm) || 120; clipboardRef.current = { type: 'midi', notes: (item.notes || []).map(n => ({ ...n })), - duration: (item.duration || 4) * (60.0 / bpmVal), + duration: item.duration || 4, name: item.name || 'MIDI Item', color: item.color || '#a855f7' }; @@ -17650,15 +17796,27 @@ const App = () => { const trk = activeTracks.find(t => t.id === contextMenu.trackId); const item = trk && (trk.midiItems || []).find(m => m.id === contextMenu.itemId); if (item) { - const bpmVal = parseInt(bpm) || 120; clipboardRef.current = { type: 'midi', notes: (item.notes || []).map(n => ({ ...n })), - duration: (item.duration || 4) * (60.0 / bpmVal), + duration: item.duration || 4, name: item.name || 'MIDI Item', color: item.color || '#a855f7' }; window.globalStudioClipboard = clipboardRef.current; + // Close piano roll tab mở của midi item này (user 07:35) + setSubTabs(prev => { + const next = prev.filter(s => s.target_id !== contextMenu.itemId); + // CHỈ về main khi tab active là PIANO ROLL bị close — giữ SECTION-TAB + if (activeTabRef.current && activeTabRef.current.startsWith('midi_') && !next.some(s => s.id === activeTabRef.current)) { + setActiveTab('main'); + } + return next; + }); + // Sync section tab → section item trên MAIN (canvas vẽ lại — 07:45) + if (activeTabRef.current && activeTabRef.current.startsWith('session_')) { + setTimeout(() => { try { syncSectionTabToMain(activeTabRef.current); } catch (e) {} }, 0); + } updateActiveTracks(prev => prev.map(t => t.id === contextMenu.trackId ? { ...t, midiItems: (t.midiItems || []).filter(m => m.id !== contextMenu.itemId) @@ -17888,7 +18046,28 @@ const App = () => { showToast('Đã dán track mới từ clipboard.', 'success'); return rearrangeNewId; }; - const handlePasteTrack = () => doPaste(selectedTrackId, currentTime); + const handlePasteTrack = () => { + // Paste vào TRACK của ITEM đang chọn (nếu có) — không phải track 1 mặc + // định (user 07:55: Ctrl+V dán vào track 1 dù track khác được chọn item). + let targetId = selectedTrackId; + try { + const selItems = selectedItemIdsRef.current; + if (selItems && selItems.size > 0) { + const firstId = selItems.values().next().value; + const found = findItemContext(firstId); + if (found) targetId = found.trackId; + } + } catch (e) {} + // Paste tại vị trí CLICK (bên phải playhead) — không phải playhead; + // click bên TRÁI playhead → paste tại playhead (user 08:05) + const _clickT = lastClickTimeRef.current; + const pasteTime = (_clickT != null && _clickT > currentTime) ? _clickT : currentTime; + doPaste(targetId, pasteTime); + }; + // Ref cho keydown handler (effect deps [] — closure STALE: selectedTrackId + // render đầu = '1' → Ctrl+V luôn paste track 1 — user 08:05) + const handlePasteTrackRef = useRef(handlePasteTrack); + handlePasteTrackRef.current = handlePasteTrack; const contextMenuPaste = () => { const result = doPaste(contextMenu.trackId, contextMenu.time || currentTime); closeContextMenu(); @@ -20500,7 +20679,12 @@ const App = () => { }; // ── Playhead set with seek+play ── + // Vị trí click gần nhất trên timeline — paste dùng vị trí CLICK (bên phải + // playhead) thay vì playhead (user 08:05: paste tại con trỏ click; click bên + // trái playhead → paste tại playhead) + const lastClickTimeRef = useRef(null); const handlePlayheadSet = (time, shiftKey) => { + lastClickTimeRef.current = time; setPlayheadWithUndo(time); }; const clearLocalSelection = () => { @@ -21381,6 +21565,12 @@ const App = () => { } } setDraggedSectionItem(null); + // Sync section tab → section item trên MAIN sau khi MOVE item (user + // 08:15 — vị trí item đổi → canvas section item ở MAIN phải theo) + if (activeTabRef.current && activeTabRef.current.startsWith('session_')) { + const _tabId = activeTabRef.current; + setTimeout(() => { try { syncSectionTabToMain(_tabId); } catch (e) {} }, 0); + } showToast('Đã di chuyển ' + (drag.itemType === 'section' ? 'section' : 'MIDI item') + '.', 'success'); }; document.addEventListener('mousemove', handleMouseMove); @@ -21472,6 +21662,13 @@ const App = () => { const resize = resizedSectionItemRef.current; if (!resize) return; setResizedSectionItem(null); + // Sync nội dung section tab → section item trên MAIN sau khi RESIZE item + // (user 08:15 — kéo ngắn midi item trong SECTION-TAB → canvas section + // item ở MAIN phải theo đúng duration mới — trước đây chỉ sync khi XÓA) + if (activeTabRef.current && activeTabRef.current.startsWith('session_')) { + const _tabId = activeTabRef.current; + setTimeout(() => { try { syncSectionTabToMain(_tabId); } catch (e) {} }, 0); + } }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); @@ -25365,11 +25562,28 @@ STRICT CONSTRAINTS: shortcut: 'Ctrl+O', action: () => setOpenProjectModalOpen(true) }, { - label: 'Save Project', - icon: 'upload-cloud', - shortcut: 'Ctrl+S', - action: () => handleSaveProject() - }, { + label: 'Save Project', + icon: 'upload-cloud', + shortcut: 'Ctrl+S', + action: () => { + // Save project + save TẤT CẢ dirty sub-tabs (user 07:35 — nhấn lưu ở + // MAIN phải lưu cả piano roll/audio tab đã sửa) + handleSaveProject(); + subTabsRef.current.filter(s => s.isDirty).forEach(st => { + if (st.type === 'PIANO_ROLL') { + handleSaveMidiNotes(st.id, st.trackId, st.target_id, st.notes || []); + } else if (st.type === 'SECTION') { + handleSaveSectionTab(st.id); + } else if (st.buffer) { + const subTrack = activeTracksRef.current.find(t => t.id === st.trackId); + if (subTrack) { + updateActiveTracks(prev => prev.map(t => t.id === st.trackId ? { ...t, buffer: st.buffer } : t)); + } + } + }); + showToast('Đã lưu dự án + các tab đã sửa', 'success'); + } + }, { label: 'Save As...', icon: 'download', shortcut: 'Ctrl+Alt+S', diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 36d8857..1befcc5 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -254,7 +254,10 @@ const secWidth=sec.duration*zoom;if(secStartLocal+secWidth<0||secStartLocal>draw // nạp đầy đủ bởi loadAudioBuffersForTracks recursion 19:00) const subTracks=sec.tracks||[];const subTrackCount=Math.min(subTracks.length,4);const subTrackHeight=(height-20)/Math.max(1,subTrackCount);const subColors=['#fbbf24','#a78bfa','#ec4899','#10b981'];ctx.save();ctx.beginPath();ctx.rect(secStartLocal,2,secWidth,height-4);ctx.clip();for(let stIdx=0;stIdx{if(!cl.buffer)return;const sr=cl.buffer.sampleRate;const bufData=cl.buffer.getChannelData(0);const bufLen=bufData.length;const clStartLocal=cl.startTime||0;const clDurLocal=bufLen/sr/(cl.speed||1.0);const clStartMain=secStartLocal+clStartLocal*zoom;const clW=Math.max(1,clDurLocal*zoom);const peakSamples=Math.max(10,Math.min(100,Math.floor(clW/3)));const step=Math.max(1,Math.floor(bufLen/peakSamples));for(let p=0;pmaxVal)maxVal=abs;}const bx=clStartMain+p/peakSamples*clW;const barH=Math.max(1,maxVal*(subTrackHeight*0.7));const barY=subY+(subTrackHeight-barH)/2;ctx.fillStyle=subColors[stIdx]+'99';ctx.fillRect(bx,barY,Math.max(1,clW/peakSamples),barH);}});// Draw MIDI items as colored note bars -const subMidi=sub.midiItems||[];subMidi.forEach(item=>{const itemStartLocal=secStartLocal+(item.startTime||0)*zoom;const itemDurLocal=(item.duration||1)*zoom;const notes=item.notes||[];const pitchMin=36;const pitchMax=84;notes.forEach(note=>{const beatSec=60.0/(parseInt(bpm)||120);const noteStartSec=(note.start_beat||0)*beatSec;const noteDurSec=Math.max(0.02,(note.duration_beats||0.25)*beatSec);const noteStartLocal=itemStartLocal+noteStartSec*zoom;const nw=noteDurSec*zoom;const pitchFrac=Math.max(0,Math.min(1,(note.pitch-pitchMin)/(pitchMax-pitchMin)));const ny=subY+2+(1.0-pitchFrac)*(subTrackHeight-6);const nh=Math.max(4,(subTrackHeight-6)/(pitchMax-pitchMin)*3);ctx.fillStyle=subColors[stIdx]+'cc';ctx.fillRect(Math.max(noteStartLocal,secStartLocal+2),ny,Math.max(2,nw),nh);});});}ctx.restore();});// Draw MIDI items +const subMidi=sub.midiItems||[];subMidi.forEach(item=>{const itemStartLocal=secStartLocal+(item.startTime||0)*zoom;const itemDurLocal=(item.duration||1)*zoom;const notes=item.notes||[];const pitchMin=36;const pitchMax=84;notes.forEach(note=>{const beatSec=60.0/(parseInt(bpm)||120);const noteStartSec=(note.start_beat||0)*beatSec;// CLIP theo item duration: item bị KÉO NGẮN (trim — duration 4 bars +// nhưng notes vẫn còn 8 bars) → note ngoài duration KHÔNG vẽ — +// canvas MAIN phải hiển thị đúng phần đã trim (user 08:20) +if(noteStartSec>=(item.duration||0)-0.01)return;const noteDurSec=Math.max(0.02,(note.duration_beats||0.25)*beatSec);const noteStartLocal=itemStartLocal+noteStartSec*zoom;const nw=noteDurSec*zoom;const pitchFrac=Math.max(0,Math.min(1,(note.pitch-pitchMin)/(pitchMax-pitchMin)));const ny=subY+2+(1.0-pitchFrac)*(subTrackHeight-6);const nh=Math.max(4,(subTrackHeight-6)/(pitchMax-pitchMin)*3);ctx.fillStyle=subColors[stIdx]+'cc';ctx.fillRect(Math.max(noteStartLocal,secStartLocal+2),ny,Math.max(2,nw),nh);});});}ctx.restore();});// Draw MIDI items const midiItems=track.midiItems||[];midiItems.forEach(midi=>{const midiStartLocal=midi.startTime*zoom-scrollLeftVal;const midiWidth=midi.duration*zoom;if(midiStartLocal+midiWidth<0||midiStartLocal>drawWidth)return;const isRecordingItem=midi.name==='Recording...';if(isRecordingItem&&Math.random()<0.05){console.log(`[DevLog] [Canvas Draw] Rendering MIDI item: ID=${midi.id}, Name="${midi.name}", Start=${midiStartLocal.toFixed(1)}px, Width=${midiWidth.toFixed(1)}px, NotesCount=${midi.notes?.length||0}`);}var isMidiSelected=selectedItemIds&&selectedItemIds.has(midi.id);ctx.fillStyle=isMidiSelected?'rgba(245, 158, 11, 0.35)':isRecordingItem?'rgba(239, 68, 68, 0.2)':(track.color||'#a78bfa')+'33';ctx.fillRect(midiStartLocal,2,midiWidth,height-4);ctx.strokeStyle=isMidiSelected?'#f59e0b':isRecordingItem?'#ef4444':track.color||'#a78bfa';ctx.lineWidth=isMidiSelected?2.5:1.5;ctx.strokeRect(midiStartLocal,2,midiWidth,height-4);ctx.fillStyle=isRecordingItem?'#fca5a5':track.color||'#a78bfa';ctx.font='bold 9px sans-serif';ctx.fillText(isRecordingItem?'[Ghi MIDI...]':midi.name||'MIDI',Math.max(midiStartLocal+4,4),14);const midiNotes=midi.notes||[];if(midiNotes.length>0){const secondsPerBeat=60.0/(parseInt(bpm)||120);const pitchMin=36;const pitchMax=84;midiNotes.forEach(note=>{const noteStartSec=(note.start_beat||0)*secondsPerBeat;const noteDurSec=Math.max(0.02,(note.duration_beats||0.25)*secondsPerBeat);const noteStartLocal=(midi.startTime+noteStartSec)*zoom-scrollLeftVal;const nw=noteDurSec*zoom;if(noteStartLocal+nwmidiStartLocal+midiWidth)return;const pitchFrac=Math.max(0,Math.min(1,(note.pitch-pitchMin)/(pitchMax-pitchMin)));const ny=18+(1.0-pitchFrac)*(height-26);const nh=Math.max(6,(height-26)/(pitchMax-pitchMin)*4);ctx.fillStyle=isRecordingItem?'#10b981':track.color||'#a78bfa';ctx.fillRect(Math.max(noteStartLocal,midiStartLocal+2),ny,Math.max(2,nw),nh);});}});// Selection highlight - local selection on this track if(selectionMode==='local'&&localSelectionTrackId===track.id&&localSelLeft!==null&&localSelRight!==null&&localSelRight>localSelLeft){const hlLeftLocal=localSelLeft*zoom-scrollLeftVal;const hlWidth=(localSelRight-localSelLeft)*zoom;ctx.fillStyle='rgba(245, 158, 11, 0.15)';ctx.fillRect(hlLeftLocal,0,hlWidth,height);ctx.strokeStyle='#f59e0b';ctx.lineWidth=1;ctx.strokeRect(hlLeftLocal,0,hlWidth,height);}},[track,zoom,timelineWidth,viewportWidth,isSelected,markers,selectionMode,localSelectionTrackId,localSelLeft,localSelRight,snapValue,bpm,scrollLeft,recordingState,recTempMidiNotes,recStartTimelineTime,canvasRedrawCount,currentTime,selectedItemIds]);return/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{key:"virtual-spacer",style:{width:`${timelineWidth}px`,height:'1px',pointerEvents:'none'}}),/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,style:{position:'sticky',left:0,imageRendering:'pixelated'},className:"cursor-crosshair",onMouseMove:e=>{if(!canvasRef.current)return;const rect=canvasRef.current.getBoundingClientRect();const x=e.clientX-rect.left+(scrollLeft||0);const time=Math.max(0,x/zoom-leadInMargin);const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];// 1. Shift+Drag (TOP PRIORITY): Range selection on track waveform if(e.shiftKey&&e.buttons>0){e.preventDefault();e.stopPropagation();const currentAnchor=getLocalAnchor?getLocalAnchor():null;const anchor=currentAnchor!==null&¤tAnchor!==undefined?currentAnchor:localSelectionStart!==null?localSelectionStart:currentTime;const selS=Math.min(anchor,time);const selE=Math.max(anchor,time);if(onSetSelectionMode)onSetSelectionMode('local');if(onSetLocalSelectionTrackId)onSetLocalSelectionTrackId(track.id);if(onSetLocalSelectionStart)onSetLocalSelectionStart(selS);if(onSetLocalSelectionEnd)onSetLocalSelectionEnd(selE);if(onSetSelectionStart)onSetSelectionStart(selS);if(onSetSelectionEnd)onSetSelectionEnd(selE);if(onSetCurrentTime)onSetCurrentTime(time);if(onSelectTrack)onSelectTrack(track.id);return;}// Check if hovering near local selection boundaries of this track @@ -268,7 +271,10 @@ if(e.ctrlKey&&selectionMode){e.preventDefault();e.stopPropagation();if(onClearLo if(e.shiftKey){e.preventDefault();e.stopPropagation();const currentAnchor=getLocalAnchor?getLocalAnchor():null;const anchor=currentAnchor!==null&¤tAnchor!==undefined?currentAnchor:localSelectionStart!==null?localSelectionStart:currentTime;const selS=Math.min(anchor,time);const selE=Math.max(anchor,time);if(onSetSelectionMode)onSetSelectionMode('local');if(onSetLocalSelectionTrackId)onSetLocalSelectionTrackId(track.id);if(onSetLocalSelectionStart)onSetLocalSelectionStart(selS);if(onSetLocalSelectionEnd)onSetLocalSelectionEnd(selE);if(onSetSelectionStart)onSetSelectionStart(selS);if(onSetSelectionEnd)onSetSelectionEnd(selE);if(onSetCurrentTime)onSetCurrentTime(time);if(onSelectTrack)onSelectTrack(track.id);return;}// Check if dragging selection boundaries (local mode) const isLocal=selectionMode==='local'&&localSelectionTrackId===track.id;if(isLocal&&localSelLeft!==null&&localSelRight!==null){const leftPx=localSelLeft*zoom;const rightPx=localSelRight*zoom;const distToLeft=Math.abs(x-leftPx);const distToRight=Math.abs(x-rightPx);if(distToLeft<=5){e.preventDefault();e.stopPropagation();if(onSelectionEdgeDragStart)onSelectionEdgeDragStart(e,track.id,'left');return;}else if(distToRight<=5){e.preventDefault();e.stopPropagation();if(onSelectionEdgeDragStart)onSelectionEdgeDragStart(e,track.id,'right');return;}}// Check if time-stretching (Alt + Right Edge) const toleranceSec=8/zoom;const rightEdgeClip=clips.find(c=>{if(!c.buffer)return false;const duration=c.buffer.duration/(c.speed||1.0);return Math.abs(time-(c.startTime+duration))<=toleranceSec;});if(rightEdgeClip&&e.altKey){e.preventDefault();e.stopPropagation();if(onClipStretchStart){onClipStretchStart(track.id,rightEdgeClip.id,time);}return;}// Check section/MIDI item edge for resize, then body for drag -const secItems=track.sections||[];const midiItems=track.midiItems||[];const secTol=8/zoom;let hitItem=null;let hitEdge=null;for(const sec of secItems){if(Math.abs(time-sec.start)<=secTol){hitItem={type:'section',id:sec.id,start:sec.start,dur:sec.duration};hitEdge='left';break;}if(Math.abs(time-(sec.start+sec.duration))<=secTol){hitItem={type:'section',id:sec.id,start:sec.start,dur:sec.duration};hitEdge='right';break;}}if(!hitItem){for(const midi of midiItems){if(Math.abs(time-midi.startTime)<=secTol){hitItem={type:'midiItem',id:midi.id,start:midi.startTime,dur:midi.duration};hitEdge='left';break;}if(Math.abs(time-(midi.startTime+midi.duration))<=secTol){hitItem={type:'midiItem',id:midi.id,start:midi.startTime,dur:midi.duration};hitEdge='right';break;}}}if(hitItem&&hitEdge){e.preventDefault();e.stopPropagation();if(onSectionItemResizeStart)onSectionItemResizeStart(track.id,hitItem.type,hitItem.id,hitEdge,time);return;}if(!hitItem){for(const sec of secItems){if(time>=sec.start&&time=midi.startTime&&time=sec.start&&time=midi.startTime&&time=sec.start&&time=midi.startTime&&timec.buffer&&time>=c.startTime&&time{e.preventDefault();e.stopPropagation();onSelectTrack(track.id);const rect=canvasRef.current.getBoundingClientRect();const x=e.clientX-rect.left+(scrollLeft||0);const time=Math.max(0,x/zoom-leadInMargin);// Detect section under cursor const secList=track.sections||[];let hitSectionId=null;for(const sec of secList){if(time>=sec.start&&time=m.startTime&&time=m.startTime&&time=c.startTime&&time{const canvasRef=useRef(null);const RULER_HEIGHT=40;const drawWidth=Math.min(timelineWidth,viewportWidth);useEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;let scrollLeftVal=scrollLeft||0;const height=RULER_HEIGHT;canvas.width=Math.min(Math.round(drawWidth*dpr),32768);canvas.height=Math.min(Math.round(height*dpr),32768);ctx.scale(dpr,dpr);ctx.imageSmoothingEnabled=false;canvas.style.width=`${drawWidth}px`;canvas.style.height=`${height}px`;ctx.fillStyle='#242424';ctx.fillRect(0,0,drawWidth,height);ctx.strokeStyle='rgba(255,255,255,0.08)';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(0,height-0.5);ctx.lineTo(drawWidth,height-0.5);ctx.stroke();const CLIP_BUFFER=400;const PADDING_LEFT=0;const tStart=scrollLeftVal/zoom-PADDING_LEFT;const tEnd=(scrollLeftVal+drawWidth)/zoom+CLIP_BUFFER/zoom;// Draw bar markers (aligned with TempoTrackLane) const beatDuration=60/bpm;const barDuration=beatDuration*4;const firstBarNum=Math.floor(tStart/barDuration);const lastBarNum=Math.ceil(tEnd/barDuration);for(let bn=firstBarNum;bn<=lastBarNum;bn++){const t=bn*barDuration;const localX=(t-tStart)*zoom;if(localX<-CLIP_BUFFER||localX>drawWidth+CLIP_BUFFER)continue;ctx.strokeStyle='rgba(255, 255, 255, 0.25)';ctx.lineWidth=1.2;ctx.beginPath();ctx.moveTo(localX,0);ctx.lineTo(localX,height);ctx.stroke();ctx.fillStyle='rgba(255, 255, 255, 0.8)';ctx.font='bold 10px Inter, sans-serif';ctx.textAlign='center';ctx.fillText(`${bn}`,localX,32);}// Draw time duration labels with drag-selection markers const minTimePx=60;const rawSecInt=Math.max(1,Math.ceil(minTimePx/zoom));const timePowers=[1,2,5,10,30,60];let secInterval=timePowers.find(p=>p>=rawSecInt)||120;if(secInterval*zoomdrawWidth+CLIP_BUFFER)continue;ctx.strokeStyle='rgba(255, 180, 100, 0.12)';ctx.lineWidth=0.8;ctx.beginPath();ctx.moveTo(localX,0);ctx.lineTo(localX,height);ctx.stroke();ctx.strokeStyle='rgba(255, 180, 100, 0.3)';ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(localX,0);ctx.lineTo(localX,10);ctx.stroke();ctx.fillStyle='rgba(255, 180, 100, 0.7)';ctx.font='bold 11px monospace';ctx.textAlign='center';ctx.fillText(formatTimeSimple(t),localX,24);}},[bpm,zoom,timelineWidth,viewportWidth,scrollLeft,canvasRedrawCount]);return React.createElement(React.Fragment,null,React.createElement("div",{key:"virtual-spacer-ruler",style:{width:`${timelineWidth}px`,height:'1px',pointerEvents:'none'}}),React.createElement("canvas",{ref:canvasRef,style:{position:'sticky',left:0,imageRendering:'pixelated'},className:"cursor-crosshair",onMouseDown:e=>{const rect=canvasRef.current.getBoundingClientRect();const x=e.clientX-rect.left+(scrollLeft||0);const time=Math.max(0,x/zoom);if(e.shiftKey){e.preventDefault();e.stopPropagation();}if(onRulerMouseDown)onRulerMouseDown(e);else onPlayheadSet(time,e.shiftKey);}}));};const TempoTrackLane=({bpm,zoom,timelineWidth,viewportWidth,onPlayheadSet,snapValue,onRulerMouseDown,scrollLeft,canvasRedrawCount,leadInMargin:propLeadIn})=>{const canvasRef=useRef(null);const drawWidth=Math.min(timelineWidth,viewportWidth);useEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;// Use scrollLeft prop directly (DOM traversal broken by sticky wrapper) @@ -729,10 +736,11 @@ if(prevTab!==activeTab){// Vào SECTION-TAB lúc MAIN đang play → CHỈ play // activeTracks = section clones (activeTab mới → activeTracksRef mới). if(prevTab==='main'&&activeTab&&activeTab.startsWith('session_')&&isPlaying){try{const _ctx=getAudioContext();// Vị trí play → LOCAL của section tab (trừ secStart — vị trí section // trên main): section tab items dùng time local (0-based) -let _secStart=0;const _tab=sessionTabsRef.current.find(st=>st.id===activeTab);if(_tab&&_tab.sectionId){(tracks||[]).forEach(_t=>(_t.sections||[]).forEach(_s=>{if(_s.sectionId===_tab.sectionId||_s.id===_tab.sectionId)_secStart=_s.start||0;}));}const _pt=Math.max(0,startOffsetTimeRef.current+(_ctx.currentTime-startAudioTimeRef.current)-_secStart);stopAllPlayback();startOffsetTimeRef.current=_pt;startAudioTimeRef.current=_ctx.currentTime;startTrackPlayback(_pt);setIsPlaying(true);console.log('[SectionTab] play CHỈ section items tại local',_pt.toFixed(2),'(secStart',_secStart.toFixed(2)+')');}catch(e){console.warn('[SectionTab] restart error:',e);}}const prevSub=subTabsRef.current.find(s=>s.id===prevTab);if(prevSub&&prevSub.isPlaying){// PIANO ROLL tab: KHÔNG stop âm khi rời tab (SF notes đã schedule — -// tiếp tục kêu tự nhiên — user: chuyển tab qua lại KHÔNG được câm). -// Audio sub-tab (buffer source riêng): vẫn stop như cũ. -if(prevSub.type!=='PIANO_ROLL'){try{stopAllPlayback();}catch(e){}}setSubTabs(prev=>prev.map(s=>s.id===prevTab?{...s,isPlaying:false}:s));// Resume main/session play khi rời PIANO ROLL tab đã play (Space): +let _secStart=0;const _tab=sessionTabsRef.current.find(st=>st.id===activeTab);if(_tab&&_tab.sectionId){(tracks||[]).forEach(_t=>(_t.sections||[]).forEach(_s=>{if(_s.sectionId===_tab.sectionId||_s.id===_tab.sectionId)_secStart=_s.start||0;}));}const _pt=Math.max(0,startOffsetTimeRef.current+(_ctx.currentTime-startAudioTimeRef.current)-_secStart);stopAllPlayback();startOffsetTimeRef.current=_pt;startAudioTimeRef.current=_ctx.currentTime;startTrackPlayback(_pt);setIsPlaying(true);console.log('[SectionTab] play CHỈ section items tại local',_pt.toFixed(2),'(secStart',_secStart.toFixed(2)+')');}catch(e){console.warn('[SectionTab] restart error:',e);}}const prevSub=subTabsRef.current.find(s=>s.id===prevTab);if(prevSub&&prevSub.isPlaying){// Chuyển sang SUB-TAB KHÁC (piano roll khác/audio tab) → STOP âm tab +// cũ — play ĐÚNG nội dung TỪNG TAB (user 07:30: piano roll tab 1 → +// tab 2 — không được nghe âm tab 1). Về MAIN/SECTION → giữ luật cũ +// (04:15+ — âm tiếp/resume). +const _newIsSub=activeTab&&activeTab!=='main'&&!activeTab.startsWith('session_');if(prevSub.type!=='PIANO_ROLL'||_newIsSub){try{stopAllPlayback();}catch(e){}}setSubTabs(prev=>prev.map(s=>s.id===prevTab?{...s,isPlaying:false}:s));// Resume main/session play khi rời PIANO ROLL tab đã play (Space): // play piano roll → stopAllPlayback dừng main → quay lại MAIN/SECTION // bị CÂM. Resume startTrackPlayback(offset + elapsed thật) — chỉ khi // trước đó mở tab lúc main đang play (mainResumeRef đã set). @@ -795,12 +803,27 @@ const startOffsetTimeRef=useRef(0);const startBufferOffsetRef=useRef(0);const st // quay lại MAIN/SECTION → CÂM. Lưu {offset, audioTime} lúc mở tab → khi // rời tab resume startTrackPlayback(offset + elapsed thật). const mainResumeRef=useRef(null);currentTimeRef.current=currentTime;// ── Keyboard Shortcuts ── -const handleUndoRef=useRef(handleUndo);const handleRedoRef=useRef(handleRedo);handleUndoRef.current=handleUndo;handleRedoRef.current=handleRedo;const handleDeleteSelectedItemsRef=useRef(()=>{});handleDeleteSelectedItemsRef.current=idsToDelete=>{const count=idsToDelete.size;setSelectedItemIds(new Set());updateActiveTracks(prev=>prev.map(t=>{let changed=false;const sections=(t.sections||[]).filter(s=>{if(idsToDelete.has(s.id)){changed=true;return false;}return true;});const midiItems=(t.midiItems||[]).filter(m=>{if(idsToDelete.has(m.id)){changed=true;return false;}return true;});let clips=t.clips||[];const hasVirtualClip=!t.clips||t.clips.length===0;if(hasVirtualClip&&t.buffer){const canonical='default_'+t.id;if(idsToDelete.has(canonical)){changed=true;return{...t,sections,midiItems,clips:[],buffer:null,startTime:0};}}const updatedClips=clips.filter(c=>{const canonical=c.id==='default'?'default_'+t.id:c.id;if(idsToDelete.has(canonical)){changed=true;return false;}return true;});if(!changed&§ions.length===(t.sections||[]).length&&midiItems.length===(t.midiItems||[]).length){return t;}return{...t,sections,midiItems,clips:updatedClips,buffer:updatedClips.length>0?updatedClips[0].buffer:null,startTime:updatedClips.length>0?updatedClips[0].startTime:0,name:updatedClips.length>0?updatedClips[0].name:t.name};}));showToast(count===1?'Đã xóa 1 item.':`Đã xóa ${count} items.`,'info');};const selectedClipIdRef=useRef(null);selectedClipIdRef.current=selectedClipId;const selectedTrackIdRef=useRef(selectedTrackId);selectedTrackIdRef.current=selectedTrackId;const handleDeleteTrackRef=useRef(()=>{});const handleSplitTrackRef=useRef(()=>{});const handleRecordClickRef=useRef(()=>{});const activeTabRef=useRef(activeTab);activeTabRef.current=activeTab;const activePlaybackSpeedRef=useRef(1.0);const subTabsRef=useRef(subTabs);subTabsRef.current=subTabs;const subTabSelectedNodeTimeRef=useRef(null);subTabSelectedNodeTimeRef.current=subTabSelectedNodeTime;const handleSubTabNormalize=tabId=>{setSubTabs(prev=>prev.map(st=>{if(st.id!==tabId||!st.buffer)return st;const ctx=getAudioContext();const data=st.buffer.getChannelData(0);const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):0;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):st.buffer.duration;const startSample=Math.floor(left*st.buffer.sampleRate);const endSample=Math.floor(right*st.buffer.sampleRate);let maxVal=0;for(let i=startSample;imaxVal)maxVal=abs;}if(maxVal===0)return st;const scale=1.0/maxVal;const clonedBuffer=ctx.createBuffer(1,Math.max(1,data.length),st.buffer.sampleRate);const clonedData=clonedBuffer.getChannelData(0);clonedData.set(data);for(let i=startSample;i{setSubTabs(prev=>prev.map(st=>{if(st.id!==tabId||!st.buffer)return st;const ctx=getAudioContext();const data=st.buffer.getChannelData(0);const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):0;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):st.buffer.duration;const startSample=Math.floor(left*st.buffer.sampleRate);const endSample=Math.floor(right*st.buffer.sampleRate);const scale=Math.pow(10,gainDb/20);const clonedBuffer=ctx.createBuffer(1,Math.max(1,data.length),st.buffer.sampleRate);const clonedData=clonedBuffer.getChannelData(0);clonedData.set(data);for(let i=startSample;i{setSubTabs(prev=>prev.map(st=>{if(st.id!==tabId||!st.buffer)return st;const ctx=getAudioContext();const data=st.buffer.getChannelData(0);const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):0;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):st.buffer.duration;const startSample=Math.floor(left*st.buffer.sampleRate);const endSample=Math.floor(right*st.buffer.sampleRate);const durationSamples=endSample-startSample;if(durationSamples<=0)return st;const clonedBuffer=ctx.createBuffer(1,Math.max(1,data.length),st.buffer.sampleRate);const clonedData=clonedBuffer.getChannelData(0);clonedData.set(data);if(type==='in'){for(let i=0;i{const st=subTabsRef.current.find(s=>s.id===tabId);if(!st||!st.buffer)return;const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):null;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):null;if(left===null||right===null||left===right){showToast('Vui lòng chọn vùng để Cut.','warning');return;}const ctx=getAudioContext();const sr=st.buffer.sampleRate;const data=st.buffer.getChannelData(0);const startSample=Math.floor(left*sr);const endSample=Math.floor(right*sr);const len=Math.max(1,endSample-startSample);// guard 0 frames → createBuffer crash +const handleUndoRef=useRef(handleUndo);const handleRedoRef=useRef(handleRedo);handleUndoRef.current=handleUndo;handleRedoRef.current=handleRedo;const handleDeleteSelectedItemsRef=useRef(()=>{});handleDeleteSelectedItemsRef.current=idsToDelete=>{const count=idsToDelete.size;// Close piano roll tabs của midi items bị xóa (user 07:40 — áp cả +// SECTION-TAB: xóa midi item → close tab đã mở của item đó) +setSubTabs(prev=>{const next=prev.filter(s=>!(s.target_id&&idsToDelete.has(s.target_id)));// CHỈ về main khi tab đang active là PIANO ROLL (midi_*) bị close — +// KHÔNG đá SECTION-TAB về main (user 07:45) +if(activeTabRef.current&&activeTabRef.current.startsWith('midi_')&&!next.some(s=>s.id===activeTabRef.current)){setActiveTab('main');}return next;});// Sync nội dung section tab → section item trên MAIN (canvas vẽ lại — +// user 07:45) — setTimeout 0: chạy SAU updateActiveTracks (data mới) +if(activeTabRef.current&&activeTabRef.current.startsWith('session_')){const _tabId=activeTabRef.current;setTimeout(()=>{try{syncSectionTabToMain(_tabId);}catch(e){}},0);}setSelectedItemIds(new Set());updateActiveTracks(prev=>prev.map(t=>{let changed=false;const sections=(t.sections||[]).filter(s=>{if(idsToDelete.has(s.id)){changed=true;return false;}return true;});const midiItems=(t.midiItems||[]).filter(m=>{if(idsToDelete.has(m.id)){changed=true;return false;}return true;});let clips=t.clips||[];const hasVirtualClip=!t.clips||t.clips.length===0;if(hasVirtualClip&&t.buffer){const canonical='default_'+t.id;if(idsToDelete.has(canonical)){changed=true;return{...t,sections,midiItems,clips:[],buffer:null,startTime:0};}}const updatedClips=clips.filter(c=>{const canonical=c.id==='default'?'default_'+t.id:c.id;if(idsToDelete.has(canonical)){changed=true;return false;}return true;});if(!changed&§ions.length===(t.sections||[]).length&&midiItems.length===(t.midiItems||[]).length){return t;}return{...t,sections,midiItems,clips:updatedClips,buffer:updatedClips.length>0?updatedClips[0].buffer:null,startTime:updatedClips.length>0?updatedClips[0].startTime:0,name:updatedClips.length>0?updatedClips[0].name:t.name};}));showToast(count===1?'Đã xóa 1 item.':`Đã xóa ${count} items.`,'info');};const selectedClipIdRef=useRef(null);selectedClipIdRef.current=selectedClipId;const selectedTrackIdRef=useRef(selectedTrackId);selectedTrackIdRef.current=selectedTrackId;const handleDeleteTrackRef=useRef(()=>{});const handleSplitTrackRef=useRef(()=>{});const handleRecordClickRef=useRef(()=>{});const activeTabRef=useRef(activeTab);activeTabRef.current=activeTab;const activePlaybackSpeedRef=useRef(1.0);const subTabsRef=useRef(subTabs);subTabsRef.current=subTabs;const subTabSelectedNodeTimeRef=useRef(null);subTabSelectedNodeTimeRef.current=subTabSelectedNodeTime;const handleSubTabNormalize=tabId=>{setSubTabs(prev=>prev.map(st=>{if(st.id!==tabId||!st.buffer)return st;const ctx=getAudioContext();const data=st.buffer.getChannelData(0);const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):0;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):st.buffer.duration;const startSample=Math.floor(left*st.buffer.sampleRate);const endSample=Math.floor(right*st.buffer.sampleRate);let maxVal=0;for(let i=startSample;imaxVal)maxVal=abs;}if(maxVal===0)return st;const scale=1.0/maxVal;const clonedBuffer=ctx.createBuffer(1,Math.max(1,data.length),st.buffer.sampleRate);const clonedData=clonedBuffer.getChannelData(0);clonedData.set(data);for(let i=startSample;i{setSubTabs(prev=>prev.map(st=>{if(st.id!==tabId||!st.buffer)return st;const ctx=getAudioContext();const data=st.buffer.getChannelData(0);const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):0;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):st.buffer.duration;const startSample=Math.floor(left*st.buffer.sampleRate);const endSample=Math.floor(right*st.buffer.sampleRate);const scale=Math.pow(10,gainDb/20);const clonedBuffer=ctx.createBuffer(1,Math.max(1,data.length),st.buffer.sampleRate);const clonedData=clonedBuffer.getChannelData(0);clonedData.set(data);for(let i=startSample;i{setSubTabs(prev=>prev.map(st=>{if(st.id!==tabId||!st.buffer)return st;const ctx=getAudioContext();const data=st.buffer.getChannelData(0);const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):0;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):st.buffer.duration;const startSample=Math.floor(left*st.buffer.sampleRate);const endSample=Math.floor(right*st.buffer.sampleRate);const durationSamples=endSample-startSample;if(durationSamples<=0)return st;const clonedBuffer=ctx.createBuffer(1,Math.max(1,data.length),st.buffer.sampleRate);const clonedData=clonedBuffer.getChannelData(0);clonedData.set(data);if(type==='in'){for(let i=0;i{const st=subTabsRef.current.find(s=>s.id===tabId);if(!st||!st.buffer)return;const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):null;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):null;if(left===null||right===null||left===right){showToast('Vui lòng chọn vùng để Cut.','warning');return;}const ctx=getAudioContext();const sr=st.buffer.sampleRate;const data=st.buffer.getChannelData(0);const startSample=Math.floor(left*sr);const endSample=Math.floor(right*sr);const len=Math.max(1,endSample-startSample);// guard 0 frames → createBuffer crash const cutBuffer=ctx.createBuffer(1,len,sr);cutBuffer.copyToChannel(data.subarray(startSample,endSample),0);clipboardRef.current={buffer:cutBuffer,name:'Subtab Clip'};const newBuffer=ctx.createBuffer(1,Math.max(1,data.length-len),sr);const newData=newBuffer.getChannelData(0);let idx=0;for(let i=0;iprev.map(s=>s.id===tabId?{...s,buffer:newBuffer,currentTime:left,selectionStart:null,selectionEnd:null}:s));showToast('Đã Cut vùng chọn.','success');};const handleSubTabCopy=tabId=>{const st=subTabsRef.current.find(s=>s.id===tabId);if(!st||!st.buffer)return;const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):null;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):null;if(left===null||right===null||left===right){showToast('Vui lòng chọn vùng để Copy.','warning');return;}const ctx=getAudioContext();const sr=st.buffer.sampleRate;const data=st.buffer.getChannelData(0);const startSample=Math.floor(left*sr);const endSample=Math.floor(right*sr);const len=Math.max(1,endSample-startSample);// guard 0 frames → createBuffer crash const copyBuffer=ctx.createBuffer(1,len,sr);copyBuffer.copyToChannel(data.subarray(startSample,endSample),0);clipboardRef.current={buffer:copyBuffer,name:'Subtab Clip',sampleRate:sr,channels:1,speed:1.0};showToast('Đã Copy vùng chọn.','success');};const handleSubTabPaste=tabId=>{const st=subTabsRef.current.find(s=>s.id===tabId);if(!st)return;// Check if Piano Roll Tab if(st.type==='PIANO_ROLL'){const clip=clipboardRef.current||window.globalStudioClipboard;if(!clip||clip.type!=='midi'||!clip.notes){showToast('Clipboard không chứa dữ liệu MIDI.','warning');return;}const secondsPerBeat=60/(bpm||120);const pasteBeat=(st.currentTime||0)/secondsPerBeat;const newNotes=clip.notes.map(n=>({id:'note_'+Math.random().toString(36).substr(2,9),pitch:n.pitch,start_beat:pasteBeat+n.start_beat,duration_beats:n.duration_beats,velocity:n.velocity||0.8}));setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,notes:[...(s.notes||[]),...newNotes]}:s));showToast('Đã dán note MIDI vào Piano Roll.','success');return;}// Default Waveform Audio Paste if(!st.buffer)return;const clip=clipboardRef.current||window.globalStudioClipboard;if(!clip||!clip.buffer){showToast('Clipboard trống hoặc không chứa dữ liệu âm thanh.','warning');return;}const ctx=getAudioContext();const clipBuf=clip.buffer;const sr=st.buffer.sampleRate;const data=st.buffer.getChannelData(0);const insertTime=st.currentTime||0;const insertSample=Math.floor(insertTime*sr);const newBuffer=ctx.createBuffer(1,Math.max(1,data.length+clipBuf.length),sr);const newData=newBuffer.getChannelData(0);let idx=0;for(let i=0;iprev.map(s=>s.id===tabId?{...s,buffer:newBuffer,currentTime:insertTime+clipBuf.duration,selectionStart:null,selectionEnd:null}:s));showToast('Đã dán dữ liệu âm thanh.','success');};const handleSubTabDelete=tabId=>{const st=subTabsRef.current.find(s=>s.id===tabId);if(!st||!st.buffer)return;const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):null;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):null;if(left===null||right===null||left===right){showToast('Vui lòng chọn vùng để Delete.','warning');return;}const ctx=getAudioContext();const sr=st.buffer.sampleRate;const data=st.buffer.getChannelData(0);const startSample=Math.floor(left*sr);const endSample=Math.floor(right*sr);const len=endSample-startSample;const newBuffer=ctx.createBuffer(1,data.length-len,sr);const newData=newBuffer.getChannelData(0);let idx=0;for(let i=0;iprev.map(s=>s.id===tabId?{...s,buffer:newBuffer,currentTime:left,selectionStart:null,selectionEnd:null}:s));showToast('Đã xóa vùng chọn.','success');};const handleSubTabLoop=(tabId,loopCount)=>{const st=subTabsRef.current.find(s=>s.id===tabId);if(!st||!st.buffer)return;const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):null;const right=st.selectionStart!==null&&st.selectionEnd!==null?Math.max(st.selectionStart,st.selectionEnd):null;if(left===null||right===null||left===right){showToast('Vui lòng chọn vùng để Loop.','warning');return;}const ctx=getAudioContext();const sr=st.buffer.sampleRate;const data=st.buffer.getChannelData(0);const startSample=Math.floor(left*sr);const endSample=Math.floor(right*sr);const len=endSample-startSample;// Loop payload N times -const segmentData=data.subarray(startSample,endSample);const addedSamples=len*(loopCount-1);const newBuffer=ctx.createBuffer(1,data.length+addedSamples,sr);const newData=newBuffer.getChannelData(0);let idx=0;for(let i=0;iprev.map(s=>s.id===tabId?{...s,buffer:newBuffer,selectionStart:null,selectionEnd:null}:s));showToast(`Đã lặp vùng chọn ${loopCount} lần.`,'success');};useEffect(()=>{const handler=e=>{// Bypass global hotkeys when typing inside input/textarea/contentEditable elements +const segmentData=data.subarray(startSample,endSample);const addedSamples=len*(loopCount-1);const newBuffer=ctx.createBuffer(1,data.length+addedSamples,sr);const newData=newBuffer.getChannelData(0);let idx=0;for(let i=0;iprev.map(s=>s.id===tabId?{...s,buffer:newBuffer,selectionStart:null,selectionEnd:null}:s));showToast(`Đã lặp vùng chọn ${loopCount} lần.`,'success');};// ── Item copy/cut helpers (Ctrl+C/X — user 07:50: phải copy/cut ITEM được +// chọn, không phải track) ── +const findItemContext=itemId=>{for(const t of activeTracksRef.current||[]){if((t.midiItems||[]).some(m=>m.id===itemId))return{trackId:t.id,itemType:'midi'};if((t.clips||[]).some(c=>c.id===itemId))return{trackId:t.id,itemType:'clip'};}return null;};const copyItemToClipboard=(trackId,itemId,itemType)=>{// Dùng REF (không phải activeTracks closure cũ): Ctrl+C/X chạy trong keydown +// effect deps [] → closure giữ activeTracks RENDER ĐẦU — item TẠO SAU phiên +// (vd midi_1786099252336) không tồn tại trong closure cũ → copy fail → cắt +// nhầm track (user 08:10 — log [CutItem] found= 2/midi nhưng không cắt). +const trk=(activeTracksRef.current||[]).find(t=>t.id===trackId);if(!trk)return false;const isMidi=itemType==='midi'||!itemType&&(trk.midiItems||[]).some(m=>m.id===itemId);if(isMidi){const item=(trk.midiItems||[]).find(m=>m.id===itemId);if(!item)return false;// duration midiItem tính GIÂY (insertMidiItem = 4*spb — scheduling dùng +// seconds) — KHÔNG nhân (60/bpm) — nhân làm duration gấp đôi → block +// paste bị dài/ngắn sai (user 07:55). +clipboardRef.current={type:'midi',notes:(item.notes||[]).map(n=>({...n})),duration:item.duration||4,name:item.name||'MIDI Item',color:item.color||'#a855f7'};window.globalStudioClipboard=clipboardRef.current;return true;}const item=(trk.clips||[]).find(c=>c.id===itemId);if(item&&item.buffer){clipboardRef.current={buffer:item.buffer,name:item.name||trk.name,volumeDb:trk.volumeDb,pan:trk.pan,color:item.color||trk.color,sampleRate:item.buffer.sampleRate,channels:item.buffer.numberOfChannels,speed:item.speed||1.0};window.globalStudioClipboard=clipboardRef.current;return true;}return false;};useEffect(()=>{const handler=e=>{// Bypass global hotkeys when typing inside input/textarea/contentEditable elements if(e.target&&(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.isContentEditable)){// PIANO ROLL tab: Space LUÔN play/stop (user yêu cầu — transport phải // capture được kể cả khi có input đang focus trong tab) — các phím // khác vẫn bypass để gõ liệu bình thường. @@ -812,11 +835,18 @@ if(e.key===' '||e.code==='Space'){if(window.mediaExplorerActive)return;e.prevent try{const _ae=document.activeElement;if(_ae&&_ae.tagName==='BUTTON')_ae.blur();}catch(err){}if(recordingStateRef.current==='RECORDING'||recordingStateRef.current==='COUNT_IN'){handleRecordClickRef.current();return;}if(handlePlayPauseRef.current)handlePlayPauseRef.current();return;}if(activeTabRef.current!=='main'&&!activeTabRef.current.startsWith('session_')){// Sub-Tab keyboard shortcuts mapping const curTabId=activeTabRef.current;if(ctrl&&alt&&e.key==='n'){e.preventDefault();handleSubTabNormalize(curTabId);return;}if(e.key==='f'||e.key==='F'){e.preventDefault();handleSubTabFade(curTabId,'in');return;}if(e.key==='g'||e.key==='G'){e.preventDefault();handleSubTabFade(curTabId,'out');return;}if(ctrl&&e.key==='l'){e.preventDefault();handleSubTabLoop(curTabId,4);return;}if(e.key==='v'||e.key==='V'){e.preventDefault();const val=prompt("Nhập Gain điều chỉnh (dB):","0");if(val)handleSubTabGain(curTabId,parseFloat(val)||0);return;}if(ctrl&&e.key==='x'){e.preventDefault();handleSubTabCut(curTabId);return;}if(ctrl&&e.key==='c'){e.preventDefault();handleSubTabCopy(curTabId);return;}if(ctrl&&e.key==='v'){e.preventDefault();handleSubTabPaste(curTabId);return;}if(e.key==='Delete'||e.key==='Backspace'||e.key==='Del'){e.preventDefault();if(subTabSelectedNodeTimeRef.current!==null){const selTime=subTabSelectedNodeTimeRef.current;setSubTabs(prev=>prev.map(s=>{if(s.id!==curTabId)return s;const curNodes=s.graphMode==='pan'?s.panningNodes||[]:s.volumeNodes||[];const updated=curNodes.filter(n=>n.time!==selTime);return{...s,[s.graphMode==='pan'?'panningNodes':'volumeNodes']:updated};}));setSubTabSelectedNodeTime(null);}else{handleSubTabDelete(curTabId);}return;}return;}if(ctrl&&e.key==='z'&&!e.shiftKey){const tag=document.activeElement?.tagName;if(tag==='INPUT'||tag==='TEXTAREA')return;e.preventDefault();handleUndoRef.current();return;}if(ctrl&&(e.key==='y'||e.key==='z'&&e.shiftKey)){const tag=document.activeElement?.tagName;if(tag==='INPUT'||tag==='TEXTAREA')return;e.preventDefault();handleRedoRef.current();return;}if(ctrl&&!alt&&e.key==='o'){e.preventDefault();handleImportSFS();return;}if(ctrl&&!alt&&e.key==='n'){e.preventDefault();// New project: đóng hết tab + reset MỌI trạng thái về mặc định try{stopAllPlayback();}catch(e){}setSubTabs([]);setSessionTabs([]);setActiveTab('main');trackMidiChannelsRef.current={};Object.keys(trackMidiBypassMap).forEach(function(k){delete trackMidiBypassMap[k];});Object.keys(trackAudioBypassMap).forEach(function(k){delete trackAudioBypassMap[k];});Object.keys(trackMasteringBypassMap).forEach(function(k){delete trackMasteringBypassMap[k];});setSelectedItemIds(new Set());setTracks([{id:'1',name:'Track 01',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#0f766e',markers:[],serverFileId:null},{id:'2',name:'Track 02',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#1d4ed8',markers:[],serverFileId:null}]);setSelectedTrackId('1');setProjectName('');setCurrentProjectId(null);localStorage.removeItem('sonic_project_name');localStorage.removeItem('sonic_project_id');showToast('New project created','info');return;}if(ctrl&&!alt&&!e.shiftKey&&e.key==='a'){e.preventDefault();const curTab=activeTabRef.current;if(curTab!=='main'&&!curTab.startsWith('session_'))return;captureSelectionUndo();const allIds=new Set();(activeTracksRef.current||[]).forEach(t=>{(t.sections||[]).forEach(s=>allIds.add(s.id));(t.midiItems||[]).forEach(m=>allIds.add(m.id));const clips=t.clips&&t.clips.length>0?t.clips:t.buffer?[{id:'default',buffer:t.buffer,startTime:t.startTime||0,name:t.name,speed:t.speed||1.0}]:[];clips.forEach(c=>allIds.add(c.id==='default'?'default_'+t.id:c.id));});setSelectedItemIds(allIds);pushSelectionUndo();return;}if(ctrl&&!alt&&e.key==='s'){e.preventDefault();const curTab=activeTabRef.current;if(curTab&&curTab.startsWith('session_')){// SECTION-TAB: Ctrl+S = Lưu section (như nút Lưu section) -handleSaveSectionTabRef.current(curTab);showToast('Đã lưu Section','success');}else{handleSaveProjectRef.current();}return;}if(ctrl&&alt&&e.key==='s'||ctrl&&e.shiftKey&&e.key==='s'){e.preventDefault();handleExportSFS();return;}if(ctrl&&!alt&&e.key==='i'){e.preventDefault();addNewTrack();return;}if(ctrl&&alt&&e.key==='i'){e.preventDefault();showToast('Import audio','info');return;}if(ctrl&&!alt&&e.key==='e'){e.preventDefault();openTempTab();return;}if(ctrl&&!alt&&e.key==='m'){e.preventDefault();handleMergeTracks();return;}if(ctrl&&!alt&&e.key==='c'){e.preventDefault();handleCopyTrack();return;}if(ctrl&&!alt&&e.key==='x'){e.preventDefault();handleCutTrack();return;}if(ctrl&&!alt&&e.key==='v'){e.preventDefault();handlePasteTrack();return;}if(e.key==='Delete'||e.key==='Backspace'||e.key==='Del'){const selItems=selectedItemIdsRef.current;if(selItems.size>0){e.preventDefault();const idsToDelete=new Set(selItems);handleDeleteSelectedItemsRef.current(idsToDelete);return;}const selClip=selectedClipIdRef.current;if(selClip){e.preventDefault();const{trackId,clipId}=selClip;setTracks(prev=>{const track=prev.find(t=>t.id===trackId);if(!track)return prev;const beforeSnap=captureTrackSnapshotRef.current?captureTrackSnapshotRef.current(trackId):null;const updatedClips=(track.clips||[]).filter(c=>c.id!==clipId);const updatedTracks=prev.map(t=>{if(t.id===trackId){return{...t,clips:updatedClips,buffer:updatedClips.length>0?updatedClips[0].buffer:null,startTime:updatedClips.length>0?updatedClips[0].startTime:0,name:updatedClips.length>0?updatedClips[0].name:`Track ${t.id}`};}return t;});setTimeout(()=>{const afterSnap=captureTrackSnapshotRef.current?captureTrackSnapshotRef.current(trackId):null;pushAction('DELETE_CLIP',trackId,beforeSnap,afterSnap);},50);return updatedTracks;});setSelectedClipId(null);showToast('Đã xóa clip.','info');return;}else{e.preventDefault();handleDeleteTrackRef.current();return;}}if(ctrl&&!alt&&e.key==='s'){e.preventDefault();const curTab=activeTabRef.current;if(curTab==='main'){// handled by main handler -}else if(curTab.startsWith('session_')){// Main session: save project + save all dirty sub-tabs +handleSaveSectionTabRef.current(curTab);showToast('Đã lưu Section','success');}else{handleSaveProjectRef.current();}return;}if(ctrl&&alt&&e.key==='s'||ctrl&&e.shiftKey&&e.key==='s'){e.preventDefault();handleExportSFS();return;}if(ctrl&&!alt&&e.key==='i'){e.preventDefault();addNewTrack();return;}if(ctrl&&alt&&e.key==='i'){e.preventDefault();showToast('Import audio','info');return;}if(ctrl&&!alt&&e.key==='e'){e.preventDefault();openTempTab();return;}if(ctrl&&!alt&&e.key==='m'){e.preventDefault();handleMergeTracks();return;}if(ctrl&&!alt&&e.key==='c'){e.preventDefault();const selItems=selectedItemIdsRef.current;if(selItems&&selItems.size>0){// Copy ITEM đầu tiên được chọn (user 07:50 — Ctrl+C phải copy item) +const firstId=selItems.values().next().value;const found=findItemContext(firstId);if(found&©ItemToClipboard(found.trackId,firstId,found.itemType)){showToast('Đã sao chép item.','info');}else{handleCopyTrack();}}else{handleCopyTrack();}return;}if(ctrl&&!alt&&e.key==='x'){e.preventDefault();e.stopPropagation();// chặn browser cut (user 07:55 — Ctrl-X bị capture) +const selItems=selectedItemIdsRef.current;if(selItems&&selItems.size>0){// Cut ITEM đầu tiên được chọn: copy + xóa item (+ close piano roll +// tab + sync section — handleDeleteSelectedItemsRef đã xử lý) +const firstId=selItems.values().next().value;const found=findItemContext(firstId);console.log('[CutItem] Ctrl+X — itemId=',firstId,'found=',found?found.trackId+'/'+found.itemType:'null','selSize=',selItems.size);if(found&©ItemToClipboard(found.trackId,firstId,found.itemType)){handleDeleteSelectedItemsRef.current(new Set([firstId]));showToast('Đã cắt item.','info');}else{handleCutTrack();}}else{handleCutTrack();}return;}if(ctrl&&!alt&&e.key==='v'){e.preventDefault();// Dùng REF — effect deps [] → handlePasteTrack closure CŨ (selectedTrackId +// stale = '1' → paste sai track — user 08:05) +handlePasteTrackRef.current();return;}if(e.key==='Delete'||e.key==='Backspace'||e.key==='Del'){const selItems=selectedItemIdsRef.current;if(selItems.size>0){e.preventDefault();const idsToDelete=new Set(selItems);handleDeleteSelectedItemsRef.current(idsToDelete);return;}const selClip=selectedClipIdRef.current;if(selClip){e.preventDefault();const{trackId,clipId}=selClip;setTracks(prev=>{const track=prev.find(t=>t.id===trackId);if(!track)return prev;const beforeSnap=captureTrackSnapshotRef.current?captureTrackSnapshotRef.current(trackId):null;const updatedClips=(track.clips||[]).filter(c=>c.id!==clipId);const updatedTracks=prev.map(t=>{if(t.id===trackId){return{...t,clips:updatedClips,buffer:updatedClips.length>0?updatedClips[0].buffer:null,startTime:updatedClips.length>0?updatedClips[0].startTime:0,name:updatedClips.length>0?updatedClips[0].name:`Track ${t.id}`};}return t;});setTimeout(()=>{const afterSnap=captureTrackSnapshotRef.current?captureTrackSnapshotRef.current(trackId):null;pushAction('DELETE_CLIP',trackId,beforeSnap,afterSnap);},50);return updatedTracks;});setSelectedClipId(null);showToast('Đã xóa clip.','info');return;}else{e.preventDefault();handleDeleteTrackRef.current();return;}}if(ctrl&&!alt&&e.key==='s'){e.preventDefault();const curTab=activeTabRef.current;if(curTab==='main'||curTab&&curTab.startsWith('session_')){// MAIN (hoặc SECTION): save project + save TẤT CẢ dirty sub-tabs +// (piano roll/audio/section) — user 07:35: nhấn lưu ở MAIN phải lưu +// cả dirty tab (trước đây nhánh main RỖNG — dirty piano roll bị mất). handleSaveProject();subTabsRef.current.filter(s=>s.isDirty).forEach(st=>{if(st.type==='PIANO_ROLL'){handleSaveMidiNotes(st.id,st.trackId,st.target_id,st.notes||[]);}else if(st.type==='SECTION'){handleSaveSectionTab(st.id);}else if(st.buffer){// Audio clip sub-tab: save buffer to track -const subTrack=activeTracksRef.current.find(t=>t.id===st.trackId);if(subTrack){updateActiveTracks(prev=>prev.map(t=>t.id===st.trackId?{...t,buffer:st.buffer}:t));}}showToast('Đã lưu','success');});}else if(curTab.startsWith('session_')){// Section tab: save section -handleSaveSectionTabRef.current(curTab);showToast('Đã lưu Section','success');}else{// Sub-tab: save current tab +const subTrack=activeTracksRef.current.find(t=>t.id===st.trackId);if(subTrack){updateActiveTracks(prev=>prev.map(t=>t.id===st.trackId?{...t,buffer:st.buffer}:t));}}});if(curTab.startsWith('session_')){// SECTION-TAB: cũng lưu section hiện tại +handleSaveSectionTabRef.current(curTab);}showToast('Đã lưu dự án + các tab đã sửa','success');}else{// Sub-tab: save current tab const st=subTabsRef.current.find(s=>s.id===curTab);if(st){if(st.type==='PIANO_ROLL'){handleSaveMidiNotes(st.id,st.trackId,st.target_id,st.notes||[]);showToast('Đã lưu Piano Roll','success');}else if(st.type==='SECTION'){handleSaveSectionTab(st.id);showToast('Đã lưu Section Tab','success');}else if(st.buffer){const subTrack=activeTracksRef.current.find(t=>t.id===st.trackId);if(subTrack){updateActiveTracks(prev=>prev.map(t=>t.id===st.trackId?{...t,buffer:st.buffer}:t));}showToast('Đã lưu Audio Tab','success');}}}return;}if(!ctrl&&!alt&&e.key==='s'){e.preventDefault();handleSplitTrackRef.current(selectedTrackIdRef.current);return;}if(e.key==='F7'){e.preventDefault();e.stopPropagation();window.__toggleMixerRef();return;}if(e.key==='F6'){e.preventDefault();e.stopPropagation();window.__toggleMediaExplorerRef();return;}};window.addEventListener('keydown',handler,{capture:true});return()=>window.removeEventListener('keydown',handler,{capture:true});},[]);// ── Temp Tab: draw isolated waveform ── useEffect(()=>{if(!tempTabActive||!tempTabBuffer||!tempTabCanvasRef.current)return;const canvas=tempTabCanvasRef.current;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const rect=canvas.getBoundingClientRect();canvas.width=rect.width*dpr;canvas.height=rect.height*dpr;ctx.scale(dpr,dpr);const w=rect.width;const h=rect.height;ctx.fillStyle='#181818';ctx.fillRect(0,0,w,h);const data=tempTabBuffer.getChannelData(0);const sr=tempTabBuffer.sampleRate;const totalSamples=data.length;if(totalSamples===0)return;ctx.strokeStyle='#6ee7b7';ctx.lineWidth=1;for(let px=0;pxmaxVal)maxVal=abs;}const mid=h/2;const peakHeight=maxVal*(h*0.4);ctx.beginPath();ctx.moveTo(px,mid-peakHeight);ctx.lineTo(px,mid+peakHeight);ctx.stroke();}},[tempTabActive,tempTabBuffer]);// ── Sub Tab: open as new tab instead of modal (LOOP_EDITOR_2.md §1) ── const openTempTab=()=>{const useLocal=selectionMode==='local'&&localSelectionTrackId;const trackId=useLocal?localSelectionTrackId:selectedTrackId;const t=tracks.find(x=>x.id===trackId);if(!t||!t.buffer){showToast('Vui lòng chọn track có dữ liệu âm thanh.','warning');return;}if(selLeft===null||selRight===null||selRight<=selLeft){showToast('Vui lòng chọn một khoảng thời gian trên sóng âm.','warning');return;}// Check if a subtab for this track+range already exists @@ -830,7 +860,11 @@ if(isPlaying){// Lưu điểm resume: nếu user bấm Space trong piano roll (p mainResumeRef.current={offset:startOffsetTimeRef.current,audioTime:getAudioContext().currentTime};}else{mainResumeRef.current=null;stopAllPlayback();}const ctx=getAudioContext();const silentBuffer=ctx.createBuffer(1,128,ctx.sampleRate);const existing=subTabs.find(s=>s.type==='PIANO_ROLL'&&s.target_id===midiItemId);if(existing){setSubTabs(prev=>prev.map(s=>s.id===existing.id?{...s,buffer:s.buffer||silentBuffer,instrumentProgram:track.instrumentProgram!==undefined?track.instrumentProgram:track.synth_engine?track.synth_engine.soundfont_program:undefined,instrumentName:track.instrumentName,instrumentId:track.instrumentId,synth_engine:track.synth_engine}:s));setActiveTab(existing.id);showToast('Piano Roll cho nốt MIDI đã được mở.','info');return;}const tabId='midi_'+Date.now();const tabLabel=`Piano Roll: ${midiItem.name||'MIDI'}`;const newTab={id:tabId,label:tabLabel,type:'PIANO_ROLL',trackId:trackId,target_id:midiItemId,parent_tab_id:activeTab==='main'?null:activeTab,notes:midiItem.notes||[],duration:midiItem.duration||4,buffer:silentBuffer,instrumentProgram:track.instrumentProgram!==undefined?track.instrumentProgram:track.synth_engine?track.synth_engine.soundfont_program:undefined,instrumentName:track.instrumentName,instrumentId:track.instrumentId,synth_engine:track.synth_engine,currentTime:0,isPlaying:false,isLooping:true,selectionStart:null,selectionEnd:null,viewport_start_bar:0.0,viewport_bar_width:8.0,scroll_y_pitch:60,snap_resolution:snapValue||"1/16",note_selection:[]};setSubTabs(prev=>[...prev,newTab]);setActiveTab(tabId);// KHÔNG loadSoundFont ở đây (kể cả nền): sfload SF mới bất đồng bộ làm // WASM heap 256MB đầy → FluidSynth stall → CÂM TOÀN CỤC (mọi âm thanh // chết sau khi mở tab). playNote TỰ load + retry đúng lúc note cần. -};const handleUpdateMidiNotes=(tabId,notes)=>{setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,notes:notes,isDirty:true}:s));};const handleSaveMidiNotes=(tabId,trackId,midiItemId,updatedNotes)=>{const pianoRollTab=subTabs.find(s=>s.id===tabId);const parentTabId=pianoRollTab?pianoRollTab.parent_tab_id:null;if(parentTabId&&parentTabId.startsWith('session_')){setSessionTabs(prev=>prev.map(s=>{if(s.id!==parentTabId)return s;return{...s,tracks:s.tracks.map(t=>{if(t.id!==trackId)return t;return{...t,midiItems:(t.midiItems||[]).map(m=>{if(m.id!==midiItemId)return m;return{...m,notes:updatedNotes};})};})};}));}else{setTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,midiItems:(t.midiItems||[]).map(m=>{if(m.id!==midiItemId)return m;return{...m,notes:updatedNotes};})};}));}setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,isDirty:false}:s));showToast('Đã lưu các chỉnh sửa nốt MIDI vào item cha!','success');};const handleSaveSectionTab=async tabId=>{const tab=sessionTabs.find(s=>s.id===tabId);if(!tab)return;// Upload buffer-only clips (chưa có serverFileId — upload sớm thất bại/ +};const handleUpdateMidiNotes=(tabId,notes)=>{setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,notes:notes,isDirty:true}:s));};const handleSaveMidiNotes=(tabId,trackId,midiItemId,updatedNotes)=>{const pianoRollTab=subTabs.find(s=>s.id===tabId);const parentTabId=pianoRollTab?pianoRollTab.parent_tab_id:null;if(parentTabId&&parentTabId.startsWith('session_')){setSessionTabs(prev=>prev.map(s=>{if(s.id!==parentTabId)return s;return{...s,tracks:s.tracks.map(t=>{if(t.id!==trackId)return t;return{...t,midiItems:(t.midiItems||[]).map(m=>{if(m.id!==midiItemId)return m;return{...m,notes:updatedNotes};})};})};}));}else{setTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,midiItems:(t.midiItems||[]).map(m=>{if(m.id!==midiItemId)return m;return{...m,notes:updatedNotes};})};}));}setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,isDirty:false}:s));showToast('Đã lưu các chỉnh sửa nốt MIDI vào item cha!','success');};// Sync nội dung SECTION-TAB → section item trên MAIN (sec.tracks) — cập nhật +// canvas section item ngay khi thay đổi nội dung tab (user 07:45 — xóa item +// trong SECTION-TAB → canvas section item ở MAIN phải vẽ lại). KHÔNG đổi +// duration (0505 — giữ kích thước user resize). +const syncSectionTabToMain=tabId=>{const tab=sessionTabsRef.current.find(s=>s.id===tabId);if(!tab||!tab.sectionId)return;setTracks(prev=>prev.map(t=>({...t,sections:(t.sections||[]).map(s=>{if(s.sectionId!==tab.sectionId&&s.id!==tab.sectionId)return s;return{...s,tracks:tab.tracks};})})));};const handleSaveSectionTab=async tabId=>{const tab=sessionTabs.find(s=>s.id===tabId);if(!tab)return;// Upload buffer-only clips (chưa có serverFileId — upload sớm thất bại/ // race) TRƯỚC khi ghi vào section item → serialized AUDIO_ITEM có file → // reload không mất audioclip. const ensureClipsUploaded=async tracksArr=>{for(const tr of tracksArr||[]){for(const c of tr.clips||[]){if(!c.serverFileId&&c.buffer&&c.buffer.duration>0.05){try{const blob=encodeWavBlob(c.buffer);const up=await uploadToServer(new File([blob],(c.name||'clip').replace(/\.[^.]+$/,'')+'.wav',{type:'audio/wav'}),tr.id);if(up&&up.file_id){c.serverFileId=up.file_id;}}catch(err){console.warn('section clip upload failed:',err);}}}}};// Gồm CẢ track buffer-only (audioclip vừa chèn, upload chưa tạo clip — @@ -877,19 +911,33 @@ const midiItems=track.midiItems||[];const clickedMidi=midiItems.find(m=>contextM const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];const clickedClip=clips.find(c=>contextMenu.time>=c.startTime&&contextMenu.time{closeContextMenu();handleSplitTrack(contextMenu.trackId);};const contextMenuDelete=()=>{const tid=contextMenu.trackId;const track=activeTracks.find(t=>t.id===tid);if(!track){closeContextMenu();return;}const secondsPerBeat=60.0/(parseInt(bpm)||120);const secondsPerBar=secondsPerBeat*4;const time=contextMenu.time;// Check for Section item under cursor const secList=track.sections||[];const clickedSec=secList.find(s=>time>=s.start&&timetime>=m.startTime&&time0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];const clickedClip=clips.find(c=>time>=c.startTime&&timeprev.map(t=>{if(t.id!==tid)return t;return{...t,sections:(t.sections||[]).filter(s=>s.id!==clickedSec.id)};}));const afterSnap=captureTrackSnapshot(tid);pushAction('DELETE_SECTION',tid,beforeSnap,afterSnap);closeContextMenu();showToast('Đã xoá section item.','info');return;}if(clickedMidi){const beforeSnap=captureTrackSnapshot(tid);updateActiveTracks(prev=>prev.map(t=>{if(t.id!==tid)return t;return{...t,midiItems:(t.midiItems||[]).filter(m=>m.id!==clickedMidi.id)};}));const afterSnap=captureTrackSnapshot(tid);pushAction('DELETE_MIDI',tid,beforeSnap,afterSnap);closeContextMenu();showToast('Đã xoá MIDI item.','info');return;}if(clickedClip){const beforeSnap=captureTrackSnapshot(tid);updateActiveTracks(prev=>prev.map(t=>{if(t.id!==tid)return t;const updatedClips=(t.clips||[]).filter(c=>c.id!==clickedClip.id);return{...t,clips:updatedClips,buffer:updatedClips[0]?.buffer||null,startTime:updatedClips[0]?.startTime||0,name:updatedClips[0]?.name||t.name};}));const afterSnap=captureTrackSnapshot(tid);pushAction('DELETE_CLIP',tid,beforeSnap,afterSnap);closeContextMenu();showToast('Đã xoá audio clip.','info');return;}// No item clicked under cursor -> Attempt to delete the track itself +const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];const clickedClip=clips.find(c=>time>=c.startTime&&timeprev.map(t=>{if(t.id!==tid)return t;return{...t,sections:(t.sections||[]).filter(s=>s.id!==clickedSec.id)};}));const afterSnap=captureTrackSnapshot(tid);pushAction('DELETE_SECTION',tid,beforeSnap,afterSnap);closeContextMenu();// Sync section tab → section item trên MAIN (canvas vẽ lại — 07:45) +if(activeTabRef.current&&activeTabRef.current.startsWith('session_')){setTimeout(()=>{try{syncSectionTabToMain(activeTabRef.current);}catch(e){}},0);}showToast('Đã xoá section item.','info');return;}if(clickedMidi){const beforeSnap=captureTrackSnapshot(tid);// Close piano roll tab mở của midi item này (user 07:35) +setSubTabs(prev=>{const next=prev.filter(s=>s.target_id!==clickedMidi.id);// CHỈ về main khi tab active là PIANO ROLL bị close — giữ SECTION-TAB +if(activeTabRef.current&&activeTabRef.current.startsWith('midi_')&&!next.some(s=>s.id===activeTabRef.current)){setActiveTab('main');}return next;});// Sync section tab → section item trên MAIN (canvas vẽ lại — 07:45) +if(activeTabRef.current&&activeTabRef.current.startsWith('session_')){setTimeout(()=>{try{syncSectionTabToMain(activeTabRef.current);}catch(e){}},0);}updateActiveTracks(prev=>prev.map(t=>{if(t.id!==tid)return t;return{...t,midiItems:(t.midiItems||[]).filter(m=>m.id!==clickedMidi.id)};}));const afterSnap=captureTrackSnapshot(tid);pushAction('DELETE_MIDI',tid,beforeSnap,afterSnap);closeContextMenu();showToast('Đã xoá MIDI item.','info');return;}if(clickedClip){const beforeSnap=captureTrackSnapshot(tid);updateActiveTracks(prev=>prev.map(t=>{if(t.id!==tid)return t;const updatedClips=(t.clips||[]).filter(c=>c.id!==clickedClip.id);return{...t,clips:updatedClips,buffer:updatedClips[0]?.buffer||null,startTime:updatedClips[0]?.startTime||0,name:updatedClips[0]?.name||t.name};}));const afterSnap=captureTrackSnapshot(tid);pushAction('DELETE_CLIP',tid,beforeSnap,afterSnap);closeContextMenu();// Sync section tab → section item trên MAIN (canvas vẽ lại — 07:45) +if(activeTabRef.current&&activeTabRef.current.startsWith('session_')){setTimeout(()=>{try{syncSectionTabToMain(activeTabRef.current);}catch(e){}},0);}showToast('Đã xoá audio clip.','info');return;}// No item clicked under cursor -> Attempt to delete the track itself // Item LỖI/rỗng (clip không buffer — khung viền "audio chưa được tải", // midi không notes, section không nội dung) KHÔNG chặn xoá track. const hasClips=track.clips&&track.clips.some(c=>c.buffer)||!!track.buffer;const hasMidi=track.midiItems&&track.midiItems.some(m=>m.notes&&m.notes.length>0);const hasSections=track.sections&&track.sections.some(s=>s.tracks&&s.tracks.some(st=>st.clips&&st.clips.some(c=>c.buffer)||st.midiItems&&st.midiItems.some(m=>m.notes&&m.notes.length>0)));const isTrackEmpty=!hasClips&&!hasMidi&&!hasSections;if(!isTrackEmpty){closeContextMenu();setAppWarningModal({title:'Không thể xoá Track',message:'Không thể xoá track chứa dữ liệu. Vui lòng xoá hết các item trước khi xoá track.',isAlert:true});return;}const sessionTab=sessionTabs.find(s=>s.id===activeTab);if(sessionTab){updateActiveTracks(prev=>{const filtered=prev.filter(t=>t.id!==tid);if(filtered.length>0)setSelectedTrackId(filtered[0].id);return filtered;});closeContextMenu();showToast('Đã xoá track.','info');}else{const beforeSnap=captureTrackSnapshot(tid);setTracks(prev=>{const filtered=prev.filter(t=>t.id!==tid);if(filtered.length>0)setSelectedTrackId(filtered[0].id||'1');return filtered;});const afterSnap=captureTrackSnapshot(tid);pushAction('DELETE',tid,beforeSnap,afterSnap);if(selectedTrackId===tid)setSelectedTrackId(tracks.filter(t=>t.id!==tid)[0]?.id||'1');closeContextMenu();showToast('Đã xoá track.','info');}};const contextMenuCopy=()=>{// ⚠️ Context menu trên ITEM → copy ĐÚNG item (user 07:25 — trước đây copy // nhầm track buffer → paste ra audio item dù cut/copy midi item). -if(contextMenu.itemType==='midi'&&contextMenu.itemId){const trk=activeTracks.find(t=>t.id===contextMenu.trackId);const item=trk&&(trk.midiItems||[]).find(m=>m.id===contextMenu.itemId);if(item){const bpmVal=parseInt(bpm)||120;clipboardRef.current={type:'midi',notes:(item.notes||[]).map(n=>({...n})),duration:(item.duration||4)*(60.0/bpmVal),name:item.name||'MIDI Item',color:item.color||'#a855f7'};window.globalStudioClipboard=clipboardRef.current;closeContextMenu();showToast(`Đã sao chép MIDI item "${item.name||''}".`,'info');return;}}if(contextMenu.itemType==='clip'&&contextMenu.itemId){const trk=activeTracks.find(t=>t.id===contextMenu.trackId);const item=trk&&(trk.clips||[]).find(c=>c.id===contextMenu.itemId);if(item&&item.buffer){clipboardRef.current={buffer:item.buffer,name:item.name||trk.name,volumeDb:trk.volumeDb,pan:trk.pan,color:item.color||trk.color,sampleRate:item.buffer.sampleRate,channels:item.buffer.numberOfChannels,speed:item.speed||1.0};window.globalStudioClipboard=clipboardRef.current;closeContextMenu();showToast(`Đã sao chép clip "${item.name||''}".`,'info');return;}}const track=activeTracks.find(t=>t.id===contextMenu.trackId);if(!track||!track.buffer)return;const sr=track.buffer.sampleRate;const data=track.buffer.getChannelData(0);// Copy selected region if selection exists +if(contextMenu.itemType==='midi'&&contextMenu.itemId){const trk=activeTracks.find(t=>t.id===contextMenu.trackId);const item=trk&&(trk.midiItems||[]).find(m=>m.id===contextMenu.itemId);if(item){clipboardRef.current={type:'midi',notes:(item.notes||[]).map(n=>({...n})),duration:item.duration||4,name:item.name||'MIDI Item',color:item.color||'#a855f7'};window.globalStudioClipboard=clipboardRef.current;closeContextMenu();showToast(`Đã sao chép MIDI item "${item.name||''}".`,'info');return;}}if(contextMenu.itemType==='clip'&&contextMenu.itemId){const trk=activeTracks.find(t=>t.id===contextMenu.trackId);const item=trk&&(trk.clips||[]).find(c=>c.id===contextMenu.itemId);if(item&&item.buffer){clipboardRef.current={buffer:item.buffer,name:item.name||trk.name,volumeDb:trk.volumeDb,pan:trk.pan,color:item.color||trk.color,sampleRate:item.buffer.sampleRate,channels:item.buffer.numberOfChannels,speed:item.speed||1.0};window.globalStudioClipboard=clipboardRef.current;closeContextMenu();showToast(`Đã sao chép clip "${item.name||''}".`,'info');return;}}const track=activeTracks.find(t=>t.id===contextMenu.trackId);if(!track||!track.buffer)return;const sr=track.buffer.sampleRate;const data=track.buffer.getChannelData(0);// Copy selected region if selection exists if(selLeft!==null&&selRight!==null&&selRight>selLeft){const trackStart=track.startTime||0;const relSelLeft=Math.max(0,selLeft-trackStart);const relSelRight=Math.max(0,selRight-trackStart);const startSample=Math.floor(relSelLeft*sr);const endSample=Math.min(data.length,Math.floor(relSelRight*sr));const len=endSample-startSample;if(len>0){const ctx=getAudioContext();const numCh=track.buffer.numberOfChannels||1;const clipBuffer=ctx.createBuffer(numCh,len,sr);for(let ch=0;ch{// ⚠️ Context menu trên ITEM → cut ĐÚNG item (copy + xóa item — trước đây // cut nhầm track buffer/audio — user 07:25). -if(contextMenu.itemType==='midi'&&contextMenu.itemId){const trk=activeTracks.find(t=>t.id===contextMenu.trackId);const item=trk&&(trk.midiItems||[]).find(m=>m.id===contextMenu.itemId);if(item){const bpmVal=parseInt(bpm)||120;clipboardRef.current={type:'midi',notes:(item.notes||[]).map(n=>({...n})),duration:(item.duration||4)*(60.0/bpmVal),name:item.name||'MIDI Item',color:item.color||'#a855f7'};window.globalStudioClipboard=clipboardRef.current;updateActiveTracks(prev=>prev.map(t=>t.id===contextMenu.trackId?{...t,midiItems:(t.midiItems||[]).filter(m=>m.id!==contextMenu.itemId)}:t));closeContextMenu();showToast(`Đã cắt MIDI item "${item.name||''}".`,'info');return;}}if(contextMenu.itemType==='clip'&&contextMenu.itemId){const trk=activeTracks.find(t=>t.id===contextMenu.trackId);const item=trk&&(trk.clips||[]).find(c=>c.id===contextMenu.itemId);if(item&&item.buffer){clipboardRef.current={buffer:item.buffer,name:item.name||trk.name,volumeDb:trk.volumeDb,pan:trk.pan,color:item.color||trk.color,sampleRate:item.buffer.sampleRate,channels:item.buffer.numberOfChannels,speed:item.speed||1.0};window.globalStudioClipboard=clipboardRef.current;updateActiveTracks(prev=>prev.map(t=>t.id===contextMenu.trackId?{...t,clips:(t.clips||[]).filter(c=>c.id!==contextMenu.itemId)}:t));closeContextMenu();showToast(`Đã cắt clip "${item.name||''}".`,'info');return;}}const t=tracks.find(x=>x.id===contextMenu.trackId);if(!t||!t.buffer){contextMenuDelete();return;}const beforeSnap=captureTrackSnapshot(contextMenu.trackId);const sr=t.buffer.sampleRate;const data=t.buffer.getChannelData(0);if(selLeft!==null&&selRight!==null&&selRight>selLeft){const trackStart=t.startTime||0;const relSelLeft=Math.max(0,selLeft-trackStart);const relSelRight=Math.max(0,selRight-trackStart);const startSample=Math.floor(relSelLeft*sr);const endSample=Math.min(data.length,Math.floor(relSelRight*sr));const len=endSample-startSample;if(len>0){const ctx=getAudioContext();const numCh=t.buffer.numberOfChannels||1;const clipBuffer=ctx.createBuffer(numCh,len,sr);for(let ch=0;chp.map(tr=>tr.id===contextMenu.trackId?{...tr,buffer:newBuffer}:tr));const afterSnap=captureTrackSnapshot(contextMenu.trackId);pushAction('CUT',contextMenu.trackId,beforeSnap,afterSnap);closeContextMenu();showToast('Đã cắt vùng chọn vào clipboard.','info');return;}}contextMenuCopy();contextMenuDelete();};const doPaste=(targetTrackId,pasteTime)=>{const clip=clipboardRef.current||window.globalStudioClipboard;if(!clip){showToast('Clipboard trống.','warning');return null;}// Support MIDI Clip Paste +if(contextMenu.itemType==='midi'&&contextMenu.itemId){const trk=activeTracks.find(t=>t.id===contextMenu.trackId);const item=trk&&(trk.midiItems||[]).find(m=>m.id===contextMenu.itemId);if(item){clipboardRef.current={type:'midi',notes:(item.notes||[]).map(n=>({...n})),duration:item.duration||4,name:item.name||'MIDI Item',color:item.color||'#a855f7'};window.globalStudioClipboard=clipboardRef.current;// Close piano roll tab mở của midi item này (user 07:35) +setSubTabs(prev=>{const next=prev.filter(s=>s.target_id!==contextMenu.itemId);// CHỈ về main khi tab active là PIANO ROLL bị close — giữ SECTION-TAB +if(activeTabRef.current&&activeTabRef.current.startsWith('midi_')&&!next.some(s=>s.id===activeTabRef.current)){setActiveTab('main');}return next;});// Sync section tab → section item trên MAIN (canvas vẽ lại — 07:45) +if(activeTabRef.current&&activeTabRef.current.startsWith('session_')){setTimeout(()=>{try{syncSectionTabToMain(activeTabRef.current);}catch(e){}},0);}updateActiveTracks(prev=>prev.map(t=>t.id===contextMenu.trackId?{...t,midiItems:(t.midiItems||[]).filter(m=>m.id!==contextMenu.itemId)}:t));closeContextMenu();showToast(`Đã cắt MIDI item "${item.name||''}".`,'info');return;}}if(contextMenu.itemType==='clip'&&contextMenu.itemId){const trk=activeTracks.find(t=>t.id===contextMenu.trackId);const item=trk&&(trk.clips||[]).find(c=>c.id===contextMenu.itemId);if(item&&item.buffer){clipboardRef.current={buffer:item.buffer,name:item.name||trk.name,volumeDb:trk.volumeDb,pan:trk.pan,color:item.color||trk.color,sampleRate:item.buffer.sampleRate,channels:item.buffer.numberOfChannels,speed:item.speed||1.0};window.globalStudioClipboard=clipboardRef.current;updateActiveTracks(prev=>prev.map(t=>t.id===contextMenu.trackId?{...t,clips:(t.clips||[]).filter(c=>c.id!==contextMenu.itemId)}:t));closeContextMenu();showToast(`Đã cắt clip "${item.name||''}".`,'info');return;}}const t=tracks.find(x=>x.id===contextMenu.trackId);if(!t||!t.buffer){contextMenuDelete();return;}const beforeSnap=captureTrackSnapshot(contextMenu.trackId);const sr=t.buffer.sampleRate;const data=t.buffer.getChannelData(0);if(selLeft!==null&&selRight!==null&&selRight>selLeft){const trackStart=t.startTime||0;const relSelLeft=Math.max(0,selLeft-trackStart);const relSelRight=Math.max(0,selRight-trackStart);const startSample=Math.floor(relSelLeft*sr);const endSample=Math.min(data.length,Math.floor(relSelRight*sr));const len=endSample-startSample;if(len>0){const ctx=getAudioContext();const numCh=t.buffer.numberOfChannels||1;const clipBuffer=ctx.createBuffer(numCh,len,sr);for(let ch=0;chp.map(tr=>tr.id===contextMenu.trackId?{...tr,buffer:newBuffer}:tr));const afterSnap=captureTrackSnapshot(contextMenu.trackId);pushAction('CUT',contextMenu.trackId,beforeSnap,afterSnap);closeContextMenu();showToast('Đã cắt vùng chọn vào clipboard.','info');return;}}contextMenuCopy();contextMenuDelete();};const doPaste=(targetTrackId,pasteTime)=>{const clip=clipboardRef.current||window.globalStudioClipboard;if(!clip){showToast('Clipboard trống.','warning');return null;}// Support MIDI Clip Paste if(clip.type==='midi'){const{notes,name,duration,color}=clip;const targetTrack=activeTracks.find(t=>t.id===targetTrackId);const newMidiItem={id:'midi_'+Date.now(),startTime:pasteTime,duration:duration||4,name:(name||'Pasted MIDI').replace(/\.\w+$/,'')+' (Pasted)',notes:notes.map(n=>({id:'note_'+Math.random().toString(36).substr(2,9),pitch:n.pitch,start_beat:n.start_beat,duration_beats:n.duration_beats,velocity:n.velocity||0.8}))};if(targetTrack){updateActiveTracks(p=>p.map(t=>{if(t.id===targetTrackId){const existingMidi=t.midiItems||[];return{...t,midiItems:[...existingMidi,newMidiItem]};}return t;}));setCurrentTime(pasteTime);showToast('Đã dán MIDI vào track.','success');return targetTrackId;}else{const colors=['#0f766e','#1d4ed8','#701a75','#a21caf','#b45309'];const rearrangeNewId='track_pasted_midi_'+Date.now();updateActiveTracks(prev=>[...prev,{id:rearrangeNewId,name:`Pasted_${name||'MIDI'}`,buffer:null,startTime:0,clips:[],midiItems:[newMidiItem],volumeDb:0,pan:0,muted:false,solo:false,color:color||colors[prev.length%colors.length],markers:[],serverFileId:null}]);setSelectedTrackId(rearrangeNewId);setCurrentTime(pasteTime);showToast('Đã dán track MIDI mới từ clipboard.','success');return rearrangeNewId;}}// Default Audio Clip Paste if(!clip.buffer){showToast('Clipboard không chứa dữ liệu âm thanh hợp lệ.','warning');return null;}const{buffer:clipBuffer,name,volumeDb,pan,color,sampleRate,channels,speed}=clip;const ctx=getAudioContext();const targetTrack=activeTracks.find(t=>t.id===targetTrackId);const newClip={id:nextClipId(),startTime:pasteTime,buffer:clipBuffer,name:(name||'Pasted Clip').replace(/\.\w+$/,'')+' (Pasted)',...(volumeDb!==undefined?{volumeDb}:{}),...(pan!==undefined?{pan}:{}),...(color?{color}:{}),...(speed!==undefined?{speed}:{})};if(targetTrack){updateActiveTracks(p=>p.map(t=>{if(t.id===targetTrackId){const existingClips=t.clips&&t.clips.length>0?t.clips:t.buffer?[{id:'default_'+t.id,buffer:t.buffer,startTime:t.startTime||0,name:t.name}]:[];const updatedClips=[...existingClips,newClip];return{...t,clips:updatedClips,buffer:updatedClips[0].buffer,startTime:updatedClips[0].startTime,name:name||t.name,volumeDb:volumeDb??t.volumeDb,pan:pan??t.pan,color:color||t.color};}return t;}));setCurrentTime(pasteTime);showToast('Đã dán clip vào track.','success');return targetTrackId;}// No matching track — create a new one -const colors=['#0f766e','#1d4ed8','#701a75','#a21caf','#b45309'];const rearrangeNewId='track_pasted_'+Date.now();updateActiveTracks(prev=>[...prev,{id:rearrangeNewId,name:`Pasted_${name||'track'}`,buffer:clipBuffer,startTime:pasteTime,clips:[newClip],volumeDb:volumeDb??0,pan:pan??0,muted:false,solo:false,color:color||colors[prev.length%colors.length],markers:[],serverFileId:null}]);setSelectedTrackId(rearrangeNewId);setCurrentTime(pasteTime);showToast('Đã dán track mới từ clipboard.','success');return rearrangeNewId;};const handlePasteTrack=()=>doPaste(selectedTrackId,currentTime);const contextMenuPaste=()=>{const result=doPaste(contextMenu.trackId,contextMenu.time||currentTime);closeContextMenu();};const contextMenuMerge=()=>{const activeTracks=tracks.filter(t=>t.buffer&&!t.muted);if(activeTracks.length<2){showToast('Cần ít nhất 2 track có dữ liệu để merge.','warning');closeContextMenu();return;}const ctx=getAudioContext();const maxDur=Math.max(...activeTracks.map(t=>(t.startTime||0)+t.buffer.duration));const sr=activeTracks[0].buffer.sampleRate;const merged=ctx.createBuffer(1,Math.ceil(maxDur*sr),sr);const mergedData=merged.getChannelData(0);activeTracks.forEach(t=>{const data=t.buffer.getChannelData(0);const startSample=Math.floor((t.startTime||0)*sr);const volLinear=(t.volumeDb??0)<=-50?0:Math.pow(10,(t.volumeDb??0)/20);for(let i=0;imaxPeak)maxPeak=abs;}if(maxPeak>1.0){for(let i=0;it.name).join('+').slice(0,30);setTracks(prev=>[...prev,{id:rearrangeNewId,name:`Merged_${names}.wav`,buffer:merged,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:colors[prev.length%colors.length],markers:[],serverFileId:null}]);setSelectedTrackId(rearrangeNewId);closeContextMenu();showToast(`Đã merge ${activeTracks.length} tracks.`,'success');};// Menu bar direct handlers (don't rely on contextMenu state) +const colors=['#0f766e','#1d4ed8','#701a75','#a21caf','#b45309'];const rearrangeNewId='track_pasted_'+Date.now();updateActiveTracks(prev=>[...prev,{id:rearrangeNewId,name:`Pasted_${name||'track'}`,buffer:clipBuffer,startTime:pasteTime,clips:[newClip],volumeDb:volumeDb??0,pan:pan??0,muted:false,solo:false,color:color||colors[prev.length%colors.length],markers:[],serverFileId:null}]);setSelectedTrackId(rearrangeNewId);setCurrentTime(pasteTime);showToast('Đã dán track mới từ clipboard.','success');return rearrangeNewId;};const handlePasteTrack=()=>{// Paste vào TRACK của ITEM đang chọn (nếu có) — không phải track 1 mặc +// định (user 07:55: Ctrl+V dán vào track 1 dù track khác được chọn item). +let targetId=selectedTrackId;try{const selItems=selectedItemIdsRef.current;if(selItems&&selItems.size>0){const firstId=selItems.values().next().value;const found=findItemContext(firstId);if(found)targetId=found.trackId;}}catch(e){}// Paste tại vị trí CLICK (bên phải playhead) — không phải playhead; +// click bên TRÁI playhead → paste tại playhead (user 08:05) +const _clickT=lastClickTimeRef.current;const pasteTime=_clickT!=null&&_clickT>currentTime?_clickT:currentTime;doPaste(targetId,pasteTime);};// Ref cho keydown handler (effect deps [] — closure STALE: selectedTrackId +// render đầu = '1' → Ctrl+V luôn paste track 1 — user 08:05) +const handlePasteTrackRef=useRef(handlePasteTrack);handlePasteTrackRef.current=handlePasteTrack;const contextMenuPaste=()=>{const result=doPaste(contextMenu.trackId,contextMenu.time||currentTime);closeContextMenu();};const contextMenuMerge=()=>{const activeTracks=tracks.filter(t=>t.buffer&&!t.muted);if(activeTracks.length<2){showToast('Cần ít nhất 2 track có dữ liệu để merge.','warning');closeContextMenu();return;}const ctx=getAudioContext();const maxDur=Math.max(...activeTracks.map(t=>(t.startTime||0)+t.buffer.duration));const sr=activeTracks[0].buffer.sampleRate;const merged=ctx.createBuffer(1,Math.ceil(maxDur*sr),sr);const mergedData=merged.getChannelData(0);activeTracks.forEach(t=>{const data=t.buffer.getChannelData(0);const startSample=Math.floor((t.startTime||0)*sr);const volLinear=(t.volumeDb??0)<=-50?0:Math.pow(10,(t.volumeDb??0)/20);for(let i=0;imaxPeak)maxPeak=abs;}if(maxPeak>1.0){for(let i=0;it.name).join('+').slice(0,30);setTracks(prev=>[...prev,{id:rearrangeNewId,name:`Merged_${names}.wav`,buffer:merged,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:colors[prev.length%colors.length],markers:[],serverFileId:null}]);setSelectedTrackId(rearrangeNewId);closeContextMenu();showToast(`Đã merge ${activeTracks.length} tracks.`,'success');};// Menu bar direct handlers (don't rely on contextMenu state) const handleMergeTracks=()=>{const at=tracks.filter(t=>t.buffer&&!t.muted);if(at.length<2){showToast('Cần 2+ tracks để merge.','warning');return;}const actx=getAudioContext();const maxDur=Math.max(...at.map(t=>(t.startTime||0)+t.buffer.duration));const sr=at[0].buffer.sampleRate;const mb=actx.createBuffer(1,Math.ceil(maxDur*sr),sr);const mdata=mb.getChannelData(0);at.forEach(t=>{const d=t.buffer.getChannelData(0);const startSample=Math.floor((t.startTime||0)*sr);const volLinear=(t.volumeDb??0)<=-50?0:Math.pow(10,(t.volumeDb??0)/20);for(let i=0;imp)mp=a;}if(mp>1.0)for(let i=0;i[...p,{id:'merged_'+Date.now(),name:'Merged_mix.wav',buffer:mb,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:['#0f766e','#1d4ed8'][p.length%2],markers:[],serverFileId:null}]);showToast('Merged all unmuted tracks.','success');};const handleCopyTrack=()=>{const t=activeTracks.find(x=>x.id===selectedTrackId);if(!t||!t.buffer)return;const sr=t.buffer.sampleRate;const data=t.buffer.getChannelData(0);// If selection exists, copy only the selected region if(selLeft!==null&&selRight!==null&&selRight>selLeft){const trackStart=t.startTime||0;const relSelLeft=Math.max(0,selLeft-trackStart);const relSelRight=Math.max(0,selRight-trackStart);const startSample=Math.floor(relSelLeft*sr);const endSample=Math.min(data.length,Math.floor(relSelRight*sr));const len=endSample-startSample;if(len>0){const ctx=getAudioContext();const clipBuffer=ctx.createBuffer(1,len,sr);clipBuffer.copyToChannel(data.subarray(startSample,endSample),0);clipboardRef.current={buffer:clipBuffer,name:t.name,volumeDb:t.volumeDb,color:t.color};showToast('Copied selection to clipboard.','info');return;}}// No selection: copy entire track clipboardRef.current={buffer:t.buffer,name:t.name,volumeDb:t.volumeDb,pan:t.pan,color:t.color,sampleRate:t.buffer.sampleRate,channels:t.buffer.numberOfChannels,speed:t.speed||1.0};showToast('Copied track to clipboard.','info');};const handleCutTrack=()=>{const t=activeTracks.find(x=>x.id===selectedTrackId);if(!t||!t.buffer)return;const beforeSnap=captureTrackSnapshot(selectedTrackId);const sr=t.buffer.sampleRate;const data=t.buffer.getChannelData(0);// If selection exists, cut only the selected region @@ -1185,7 +1233,10 @@ try{midiVuActivityRef.current={};}catch(e){}stopMidiCapture();if(window.SonicSF) if(window.SonicSF.panic){try{window.SonicSF.panic();}catch(e){console.warn('[Stop] panic error:',e);}}}}catch(e){console.warn('[Stop] stopAllPlayback error:',e);}setIsPlaying(false);setSubTabs(prev=>prev.map(s=>({...s,isPlaying:false})));updateSfRouting();};const seekPlaybackTo=time=>{const isSubTab=subTabs.some(sub=>sub.id===activeTab);if(isSubTab){const st=subTabs.find(s=>s.id===activeTab);if(!st)return;if(st.isPlaying){stopAllPlayback();window.SonicSF.stopAll();setSubTabs(prev=>prev.map(s=>s.id===activeTab?{...s,currentTime:time,isPlaying:true}:s));const ctx=getAudioContext();startOffsetTimeRef.current=time;startAudioTimeRef.current=ctx.currentTime;startBufferOffsetRef.current=time*(st.speed||1.0);if(st.type==='PIANO_ROLL'){schedulePianoRollMidi(st,time);}startSubTabPlayback(st,time);}else{setSubTabs(prev=>prev.map(s=>s.id===activeTab?{...s,currentTime:time}:s));}}else{if(isPlaying){stopAllPlayback();setCurrentTime(time);startOffsetTimeRef.current=time;startAudioTimeRef.current=getAudioContext().currentTime;startTrackPlayback(time);setIsPlaying(true);}else{setCurrentTime(time);}}};const handleStop=()=>{if(recordingStateRef.current==='RECORDING'||recordingStateRef.current==='COUNT_IN'){stopRecordingTake();return;}stopAllPlayback();if(activeTab!=='main'&&!activeTab.startsWith('session_')){setSubTabs(prev=>prev.map(s=>s.id===activeTab?{...s,currentTime:0}:s));}else{setCurrentTime(0);}};const drawVuMeter=(canvas,db)=>{if(!canvas)return;const ctx=canvas.getContext('2d');if(!ctx)return;const w=canvas.width;const h=canvas.height;ctx.clearRect(0,0,w,h);const minDb=-60;const maxDb=0;const frac=Math.max(0,Math.min(1,(db-minDb)/(maxDb-minDb)));ctx.fillStyle='#18181b';ctx.fillRect(0,0,w,h);const grad=ctx.createLinearGradient(0,0,w,0);grad.addColorStop(0,'#10b981');grad.addColorStop(0.7,'#eab308');grad.addColorStop(0.95,'#ef4444');ctx.fillStyle=grad;ctx.fillRect(0,0,w*frac,h);if(db>=-0.5){ctx.fillStyle='#ff0000';ctx.fillRect(w-6,0,6,h);}};const handleRecordClick=async()=>{if(recordingState==='RECORDING'||recordingState==='COUNT_IN'){await stopRecordingTake();return;}// Check if piano roll tab is active and armed const activePianoRoll=subTabs.find(s=>s.id===activeTab&&s.type==='PIANO_ROLL'&&s.isArmed);if(activePianoRoll&&selectedMidiInputId){setRecordingState('COUNT_IN');const secondsPerBeat=60.0/(parseInt(bpm)||120);const countInDuration=secondsPerBeat*4;const audioCtx=getAudioContext();const now=audioCtx.currentTime;showToast('Metronome Count-in: 4... 3... 2... 1... (MIDI Piano Roll)','info');for(let i=0;i<4;i++){playMetronomeClick(now+i*secondsPerBeat,i===0);}setTimeout(()=>{startPianoRollRecording(activePianoRoll);},countInDuration*1000);return;}const armed=activeTracks.filter(t=>t.isArmed&&t.inputSource?.deviceType&&t.inputSource.deviceType!=='NONE');if(armed.length===0){showToast('Vui lòng Arm (R) ít nhất một track và chọn cổng Input để ghi âm.','warning');return;}setRecordingState('COUNT_IN');const secondsPerBeat=60.0/(parseInt(bpm)||120);const countInDuration=secondsPerBeat*4;const audioCtx=getAudioContext();const now=audioCtx.currentTime;showToast('Metronome Count-in: 4... 3... 2... 1...','info');for(let i=0;i<4;i++){playMetronomeClick(now+i*secondsPerBeat,i===0);}setTimeout(()=>{startRecordingTake(armed);},countInDuration*1000);};const startPianoRollRecording=tab=>{try{const context=getAudioContext();const secondsPerBeat=60.0/(parseInt(bpm)||120);const startTime=currentTime;const startBeat=startTime/secondsPerBeat;nextMetronomeBeatRef.current=Math.ceil(startBeat);const midiRec=new ClientMIDIRecorder(context,parseInt(bpm)||120,4);midiRec.tempTabId=tab.id;midiRec.selectedMidiInputId=selectedMidiInputId||'ALL';midiRec.start(startTime/secondsPerBeat,midiRec.selectedMidiInputId);pianoRollRecorderRef.current=midiRec;activeMIDIRecordersRef.current['piano_roll']=midiRec;setRecStartTimelineTime(startTime);recordingStartTimeRef.current=startTime;setRecordingState('RECORDING');setRecTempMidiNotes([]);const ctx=getAudioContext();const silentBuf=ctx.createBuffer(1,128,ctx.sampleRate);setSubTabs(prev=>prev.map(s=>s.id===tab.id?{...s,buffer:silentBuf,isPlaying:true}:s));startSubTabPlayback({...tab,buffer:silentBuf},startTime);startOffsetTimeRef.current=startTime;startAudioTimeRef.current=context.currentTime;setIsPlaying(true);midiRec.onNoteOn=(pitch,currentBeat)=>{const elapsedBeats=Math.max(0,currentBeat);const sec=elapsedBeats*(60.0/(parseInt(bpm)||120));const activeNotes=Array.from(midiRec.activeNotes.values()).map(n=>({id:'rec_'+n.pitch+'_'+currentBeat,pitch:n.pitch,start_beat:n.start_beat,duration_beats:Math.max(0.125,currentBeat-n.start_beat),velocity:Math.min(1,n.velocity||0.8),pan:0.0}));const rec=midiRec.recordedNotes.map((n,i)=>({id:'rec_'+Date.now()+'_'+i,pitch:n.pitch,start_beat:n.start_beat,duration_beats:n.duration_beats,velocity:Math.min(1,n.velocity||0.8),pan:0.0}));const allNotes=[...rec,...activeNotes];setRecTempMidiNotes(allNotes);setCanvasRedrawCount(n=>n+1);setSubTabs(prev=>prev.map(s=>s.id===tab.id?{...s,currentTime:startTime+sec,isDirty:true}:s));};if(!tab._recordingStarted){tab._recordingStarted=true;}showToast('Recording MIDI to Piano Roll...','info');}catch(err){console.error('startPianoRollRecording error:',err);showToast('Lỗi khi bắt đầu ghi âm Piano Roll: '+err.message,'warning');setRecordingState('IDLE');}};handleRecordClickRef.current=handleRecordClick;const startRecordingTake=async armedTracks=>{const context=getAudioContext();if(context.state==='suspended'){await context.resume();}setRecordingState('RECORDING');setRecTempMidiNotes([]);setRecTempAudioBuffer(null);const startTimelineTime=currentTime;setRecStartTimelineTime(startTimelineTime);recordingStartTimeRef.current=startTimelineTime;const secondsPerBeat=60.0/(parseInt(bpm)||120);const startBeat=startTimelineTime/secondsPerBeat;nextMetronomeBeatRef.current=Math.ceil(startBeat);activeMIDIRecordersRef.current={};activeAudioRecordersRef.current={};recordingPCMDataRef.current={};startOffsetTimeRef.current=startTimelineTime;startAudioTimeRef.current=context.currentTime;startTrackPlayback(startTimelineTime);setIsPlaying(true);let midiRecList=[];for(let track of armedTracks){if(track.inputSource.deviceType==='MIDI_KEYBOARD'){const midiRec=new ClientMIDIRecorder(context,parseInt(bpm)||120,4);const tempMidiItemId='midi_rec_'+Date.now()+'_'+track.id;midiRec.tempMidiItemId=tempMidiItemId;updateActiveTracks(prev=>prev.map(t=>{if(t.id!==track.id)return t;return{...t,midiItems:[...(t.midiItems||[]),{id:tempMidiItemId,name:'Recording...',startTime:startTimelineTime,duration:4*(secondsPerBeat*4),notes:[]}]};}));midiRec.onNoteOn=(pitch,currentBeat)=>{const canvas=trackVuRefs.current[track.id];if(canvas){drawVuMeter(canvas,0);setTimeout(()=>drawVuMeter(canvas,-60),100);}const elapsedBeats=Math.max(0,currentBeat);const elapsedSec=elapsedBeats*secondsPerBeat;const durationSec=Math.max(4*(secondsPerBeat*4),elapsedSec);const allNotes=[...midiRec.recordedNotes,...Array.from(midiRec.activeNotes.values()).map(n=>({...n,duration_beats:currentBeat-n.start_beat}))];setRecTempMidiNotes(allNotes);setCanvasRedrawCount(n=>n+1);if(midiRec.tempMidiItemId){updateActiveTracks(prev=>prev.map(t=>{if(t.id!==track.id)return t;return{...t,midiItems:t.midiItems.map(m=>m.id===midiRec.tempMidiItemId?{...m,notes:allNotes,duration:durationSec}:m)};}));}};midiRec.onNoteOff=()=>{const currentBeat=(context.currentTime-midiRec.recStartAudioTime-midiRec.latencyCompSec)/(60.0/midiRec.bpm);const elapsedBeats=Math.max(0,currentBeat);const elapsedSec=elapsedBeats*secondsPerBeat;const durationSec=Math.max(4*(secondsPerBeat*4),elapsedSec);const activeNotesArray=Array.from(midiRec.activeNotes.values()).map(n=>({...n,duration_beats:currentBeat-n.start_beat}));const allNotes=[...midiRec.recordedNotes,...activeNotesArray];setRecTempMidiNotes(allNotes);setCanvasRedrawCount(n=>n+1);if(midiRec.tempMidiItemId){updateActiveTracks(prev=>prev.map(t=>{if(t.id!==track.id)return t;return{...t,midiItems:t.midiItems.map(m=>m.id===midiRec.tempMidiItemId?{...m,notes:allNotes,duration:durationSec}:m)};}));}};midiRec.start(startTimelineTime/(secondsPerBeat*4),track.inputSource.deviceId);activeMIDIRecordersRef.current[track.id]=midiRec;midiRecList.push({trackId:track.id,midiRec});}else if(track.inputSource.deviceType==='MICROPHONE'){const audioRec=new ClientAudioRecorder(context);try{await audioRec.initializeInput(track.inputSource.deviceId);recordingPCMDataRef.current[track.id]=[];audioRec.onLevelUpdate=db=>{const canvas=trackVuRefs.current[track.id];if(canvas){drawVuMeter(canvas,db);}};audioRec.onPCMChunk=chunk=>{if(recordingPCMDataRef.current[track.id]){const currentData=recordingPCMDataRef.current[track.id];const newData=new Float32Array(currentData.length+chunk.length);newData.set(currentData);newData.set(chunk,currentData.length);recordingPCMDataRef.current[track.id]=newData;}};const monitorGain=track.monitoringEnabled?masterBus?masterBus.input:context.destination:null;await audioRec.start(monitorGain,track.monitoringEnabled);activeAudioRecordersRef.current[track.id]=audioRec;}catch(err){console.error('Failed to initialize microphone:',err);showToast('Không khởi động được micro: '+err.message,'warning');}}}recordingSyncRef.current=setInterval(()=>{const secondsPerBeatInt=60.0/(parseInt(bpm)||120);for(let{trackId,midiRec}of midiRecList){if(!midiRec.isRecording||!midiRec.tempMidiItemId)continue;const currentTimeSec=Math.max(0,getAudioContext().currentTime-midiRec.recStartAudioTime-midiRec.latencyCompSec);const currentBeat=currentTimeSec/secondsPerBeatInt;const allNotes=[...midiRec.recordedNotes,...Array.from(midiRec.activeNotes.values()).map(n=>({...n,duration_beats:currentBeat-n.start_beat}))];const durationSec=Math.max(4*(secondsPerBeatInt*4),currentTimeSec);if(Math.random()<0.2){// Throttle log to prevent flooding (approx 2 logs/sec) console.log(`[DevLog] [MIDI Rec Sync] Temp Item ID: ${midiRec.tempMidiItemId}, Duration: ${durationSec.toFixed(2)}s, ActiveNotes: ${midiRec.activeNotes.size}, RecordedNotes: ${midiRec.recordedNotes.length}`);}setRecTempMidiNotes(allNotes);setCanvasRedrawCount(n=>n+1);updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,midiItems:t.midiItems.map(m=>m.id===midiRec.tempMidiItemId?{...m,notes:allNotes,duration:durationSec}:m)};}));}},100);showToast('Đang ghi âm...','info');};const stopRecordingTake=async()=>{setRecordingState('IDLE');stopAllPlayback();const context=getAudioContext();const secondsPerBeat=60.0/(parseInt(bpm)||120);const secondsPerBar=secondsPerBeat*4;const midiRecorders=activeMIDIRecordersRef.current;const audioRecorders=activeAudioRecordersRef.current;Object.keys(trackVuRefs.current).forEach(tid=>{const canvas=trackVuRefs.current[tid];if(canvas)drawVuMeter(canvas,-60);});let hasRecordedAnything=false;for(let trackId in midiRecorders){const midiRec=midiRecorders[trackId];const recordedNotes=midiRec.stop();if(midiRec.tempTabId){if(recordedNotes.length>0){const newNotes=recordedNotes.map((n,i)=>({id:'rec_'+Date.now()+'_'+i,pitch:n.pitch,start_beat:Math.max(0,n.start_beat||0),duration_beats:Math.max(0.125,n.duration_beats||0.25),velocity:Math.min(1,n.velocity||0.8),pan:0.0}));setSubTabs(prev=>prev.map(s=>s.id===midiRec.tempTabId?{...s,notes:[...(s.notes||[]),...newNotes],isDirty:true}:s));setCanvasRedrawCount(n=>n+1);showToast(`Đã ghi ${recordedNotes.length} notes vào Piano Roll.`,'success');}hasRecordedAnything=true;}else if(midiRec.tempMidiItemId){const recCurrentTimeSec=Math.max(0,context.currentTime-midiRec.recStartAudioTime-midiRec.latencyCompSec);const recElapsedBeats=recCurrentTimeSec/(60.0/midiRec.bpm);const totalDurationBeats=Math.max(4.0,recordedNotes.length>0?Math.max(recElapsedBeats,...recordedNotes.map(n=>n.start_beat+n.duration_beats)):recElapsedBeats);updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;const itemIndex=(t.midiItems||[]).findIndex(m=>m.id===midiRec.tempMidiItemId);if(itemIndex>=0){const updatedItems=[...t.midiItems];updatedItems[itemIndex]={...updatedItems[itemIndex],name:recordedNotes.length>0?'Recorded MIDI':'Empty MIDI',notes:recordedNotes,duration:Math.ceil(totalDurationBeats/4)*secondsPerBar};return{...t,midiItems:updatedItems};}const newMidiItem={id:'midi_rec_'+Date.now(),name:'Recorded MIDI',parent_track_id:t.id,startTime:recordingStartTimeRef.current,duration:Math.ceil(totalDurationBeats/4)*secondsPerBar,length_bars:Math.ceil(totalDurationBeats/4),notes:recordedNotes};return{...t,midiItems:[...(t.midiItems||[]),newMidiItem]};}));if(recordedNotes.length>0){hasRecordedAnything=true;}}else if(recordedNotes.length>0){hasRecordedAnything=true;const totalDurationBeats=Math.max(4.0,...recordedNotes.map(n=>n.start_beat+n.duration_beats));const newMidiItem={id:'midi_rec_'+Date.now(),name:'Recorded MIDI',parent_track_id:trackId,startTime:recordingStartTimeRef.current,duration:Math.ceil(totalDurationBeats/4)*secondsPerBar,length_bars:Math.ceil(totalDurationBeats/4),notes:recordedNotes};updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;return{...t,midiItems:[...(t.midiItems||[]),newMidiItem]};}));}}for(let trackId in audioRecorders){const audioRec=audioRecorders[trackId];const audioBuffer=await audioRec.stop();if(audioBuffer&&audioBuffer.duration>0.05){hasRecordedAnything=true;const newClip={id:'clip_rec_'+Date.now(),name:'Recorded Audio.wav',buffer:audioBuffer,startTime:recordingStartTimeRef.current,speed:1.0};updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;const updatedClips=[...(t.clips||[]),newClip];return{...t,clips:updatedClips,buffer:updatedClips[0]?.buffer||null,startTime:updatedClips[0]?.startTime||0,name:updatedClips[0]?.name||t.name};}));}}if(recordingSyncRef.current){clearInterval(recordingSyncRef.current);recordingSyncRef.current=null;}activeMIDIRecordersRef.current={};activeAudioRecordersRef.current={};recordingPCMDataRef.current={};setRecTempMidiNotes([]);setRecTempAudioBuffer(null);if(hasRecordedAnything){showToast('Đã thu và lưu bản ghi vào timeline.','success');}else{showToast('Đã dừng ghi âm (không phát hiện tín hiệu đầu vào).','info');}};const handleSubTabResizeMouseDown=e=>{e.preventDefault();e.stopPropagation();const startY=e.clientY;const startHeight=subTabHeight;const handleMouseMove=moveEvent=>{const deltaY=moveEvent.clientY-startY;const newHeight=Math.max(48,Math.min(400,startHeight+deltaY));setSubTabHeight(newHeight);};const handleMouseUp=()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);};// ── Playhead set with seek+play ── -const handlePlayheadSet=(time,shiftKey)=>{setPlayheadWithUndo(time);};const clearLocalSelection=()=>{setSelectionMode(null);setLocalSelectionTrackId(null);setLocalSelectionStart(null);setLocalSelectionEnd(null);};const handleRulerMouseDown=e=>{if(e.ctrlKey){e.preventDefault();e.stopPropagation();clearLocalSelection();const wrapper=timelineWrapperRef.current;if(wrapper){const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const rawTime=Math.max(0,(e.clientX-rect.left+scrollLeft)/zoom-leadInMargin);const time=snapValue!=='free'?snapTime(rawTime,snapValue,bpm):rawTime;rulerDragStartRef.current=time;rulerAnchorRef.current=time;isDraggingRulerRef.current=true;captureSelectionUndo();setSelectionMode('global');setSelectionStart(time);setSelectionEnd(time);}return;}const wrapper=timelineWrapperRef.current;if(!wrapper)return;const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const rawTime=Math.max(0,mouseX/zoom-leadInMargin);const time=snapValue!=='free'?snapTime(rawTime,snapValue,bpm):rawTime;clearLocalSelection();captureSelectionUndo();setSelectionMode('global');rulerDragStartRef.current=time;isDraggingRulerRef.current=true;if(e.shiftKey){e.preventDefault();e.stopPropagation();const anchor=rulerAnchorRef.current!==null&&rulerAnchorRef.current!==undefined?rulerAnchorRef.current:selectionStart!==null&&selectionStart!==undefined?selectionStart:currentTime;const selS=Math.max(0,Math.min(anchor,time));const selE=Math.max(0,Math.max(anchor,time));setSelectionStart(selS);setSelectionEnd(selE);}else{rulerAnchorRef.current=Math.max(0,time);}handlePlayheadSet(time);};// Global Ruler mousemove is tracked via document listener set up in useEffect +// Vị trí click gần nhất trên timeline — paste dùng vị trí CLICK (bên phải +// playhead) thay vì playhead (user 08:05: paste tại con trỏ click; click bên +// trái playhead → paste tại playhead) +const lastClickTimeRef=useRef(null);const handlePlayheadSet=(time,shiftKey)=>{lastClickTimeRef.current=time;setPlayheadWithUndo(time);};const clearLocalSelection=()=>{setSelectionMode(null);setLocalSelectionTrackId(null);setLocalSelectionStart(null);setLocalSelectionEnd(null);};const handleRulerMouseDown=e=>{if(e.ctrlKey){e.preventDefault();e.stopPropagation();clearLocalSelection();const wrapper=timelineWrapperRef.current;if(wrapper){const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const rawTime=Math.max(0,(e.clientX-rect.left+scrollLeft)/zoom-leadInMargin);const time=snapValue!=='free'?snapTime(rawTime,snapValue,bpm):rawTime;rulerDragStartRef.current=time;rulerAnchorRef.current=time;isDraggingRulerRef.current=true;captureSelectionUndo();setSelectionMode('global');setSelectionStart(time);setSelectionEnd(time);}return;}const wrapper=timelineWrapperRef.current;if(!wrapper)return;const rect=wrapper.getBoundingClientRect();const scrollLeft=wrapper.scrollLeft;const mouseX=e.clientX-rect.left+scrollLeft;const rawTime=Math.max(0,mouseX/zoom-leadInMargin);const time=snapValue!=='free'?snapTime(rawTime,snapValue,bpm):rawTime;clearLocalSelection();captureSelectionUndo();setSelectionMode('global');rulerDragStartRef.current=time;isDraggingRulerRef.current=true;if(e.shiftKey){e.preventDefault();e.stopPropagation();const anchor=rulerAnchorRef.current!==null&&rulerAnchorRef.current!==undefined?rulerAnchorRef.current:selectionStart!==null&&selectionStart!==undefined?selectionStart:currentTime;const selS=Math.max(0,Math.min(anchor,time));const selE=Math.max(0,Math.max(anchor,time));setSelectionStart(selS);setSelectionEnd(selE);}else{rulerAnchorRef.current=Math.max(0,time);}handlePlayheadSet(time);};// Global Ruler mousemove is tracked via document listener set up in useEffect useEffect(()=>{const handleMouseMove=e=>{if(!isDraggingRulerRef.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 rawTime=Math.max(0,Math.min(maxDuration,mouseX/zoom-leadInMarginRef.current));const time=snapValueRef.current!=='free'?snapTime(rawTime,snapValueRef.current,bpmRef.current):rawTime;const anchor=rulerAnchorRef.current??rulerDragStartRef.current??time;setSelectionStart(Math.min(anchor,time));setSelectionEnd(Math.max(anchor,time));};const handleMouseUp=()=>{if(isDraggingRulerRef.current){isDraggingRulerRef.current=false;rulerDragStartRef.current=null;pushSelectionUndo();}};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);// Sub-tab ruler drag (section/editor tabs) const handleSubTabMove=e=>{if(!isDraggingSubTabRef.current)return;const wrapper=timelineWrapperRef.current;if(!wrapper)return;const rect=wrapper.getBoundingClientRect();const sl=wrapper.scrollLeft;const raw=Math.max(0,(e.clientX-rect.left+sl)/zoom);const time=snapValueRef.current!=='free'?snapTime(raw,snapValueRef.current,bpmRef.current):raw;const anchor=subTabDragStartRef.current??0;setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,selectionStart:Math.min(anchor,time),selectionEnd:Math.max(anchor,time)}:s));};const handleSubTabUp=()=>{if(isDraggingSubTabRef.current){isDraggingSubTabRef.current=false;subTabDragStartRef.current=null;}};document.addEventListener('mousemove',handleSubTabMove);document.addEventListener('mouseup',handleSubTabUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);document.removeEventListener('mousemove',handleSubTabMove);document.removeEventListener('mouseup',handleSubTabUp);};},[zoom,maxDuration]);// ── Track Lane Local Selection Drag ── const localDragInProgressRef=useRef(false);const localDragTrackRef=useRef(null);const localDragStartTimeRef=useRef(0);const localSelectionAnchorRef=useRef(null);const handleTrackLaneMouseDown=(trackId,time)=>{setSelectedTrackId(trackId);captureSelectionUndo();clearLocalSelection();// A fresh region selection re-enables selection-looping (a previous @@ -1218,10 +1269,15 @@ var baseIdx=allTrks.findIndex(function(tr){return tr.id===dragBaseTrackId;});if( if(drag.multiIds){var dragOrigStart=drag.multiIds[drag.itemId]?drag.multiIds[drag.itemId].start:0;var delta=newStart-dragOrigStart;// Extend activeTracks if target track index exceeds track count var outTracks=[];Object.keys(drag.multiIds).forEach(function(mid){var inf=drag.multiIds[mid];var oIdx=allTrks.findIndex(function(tr){return tr.id===inf.trackId;});if(oIdx<0)oIdx=baseIdx;var needIdx=oIdx+crossOffset;if(needIdx>=allTrks.length){for(var ai=allTrks.length;ai<=needIdx;ai++){var exists=outTracks.some(function(ot){return ot.id===(ai+1).toString();});if(!exists){var colors=['#0f766e','#1d4ed8','#701a75','#a21caf','#b45309'];outTracks.push({id:(ai+1).toString(),name:'Track '+(ai+1),buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:colors[ai%colors.length],clips:[],midiItems:[],sections:[],markers:[],isArmed:false,height:140});}}}});updateActiveTracks(function(prev){var trkIds=prev.map(function(tr){return tr.id;});var merged=outTracks.length>0?prev.concat(outTracks.filter(function(ot){return!trkIds.includes(ot.id);})):prev;var allIds=merged.map(function(tr){return tr.id;});return merged.map(function(t){var resS=(t.sections||[]).slice();var resM=(t.midiItems||[]).slice();var resC=(t.clips||[]).slice();Object.keys(drag.multiIds).forEach(function(mid){var inf=drag.multiIds[mid];var newVal=inf.start+delta;var oIdx=allIds.indexOf(inf.trackId);if(oIdx<0)oIdx=baseIdx;var needIdx=Math.max(0,oIdx+crossOffset);var targetTid=needIdx{const drag=draggedSectionItemRef.current;if(!drag)return;var changed=false;var trackId=drag.trackId;var beforeSnap=drag.beforeSnap;var origs=drag.originalPositions||{};updateActiveTracks(function(prev){return prev.map(function(t){var allItemIds=Object.keys(origs);var updatedSections=(t.sections||[]).slice();var updatedMidi=(t.midiItems||[]).slice();var hasChange=false;for(var i=0;i0.001){hasChange=true;changed=true;}}}}if(!hasChange)return t;return{...t,sections:updatedSections,midiItems:updatedMidi};});});if(changed||drag.beforeSnap){var afterSnap=captureAllTracksSnapshot();pushAction('MOVE_ITEM','ALL_TRACKS',drag.beforeSnap||beforeSnap,afterSnap);}// Realtime: items vừa thả — re-schedule NGAY theo vị trí mới. -triggerRescheduleNow();if(drag.itemType==='midiItem'){let finalTrackId=null;const curTrks=activeTracksRef.current||activeTracks;for(const t of curTrks){if((t.midiItems||[]).some(m=>m.id===drag.itemId)){finalTrackId=t.id;break;}}if(finalTrackId){const targetTrack=curTrks.find(t=>t.id===finalTrackId);if(targetTrack){setSubTabs(prev=>prev.map(st=>{if(st.target_id===drag.itemId){return{...st,trackId:finalTrackId,instrumentProgram:targetTrack.instrumentProgram!==undefined?targetTrack.instrumentProgram:st.instrumentProgram,instrumentName:targetTrack.instrumentName||st.instrumentName};}return st;}));}}}setDraggedSectionItem(null);showToast('Đã di chuyển '+(drag.itemType==='section'?'section':'MIDI item')+'.','success');};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Pending drag: click selects; mousemove > 5px threshold starts the real drag ── +triggerRescheduleNow();if(drag.itemType==='midiItem'){let finalTrackId=null;const curTrks=activeTracksRef.current||activeTracks;for(const t of curTrks){if((t.midiItems||[]).some(m=>m.id===drag.itemId)){finalTrackId=t.id;break;}}if(finalTrackId){const targetTrack=curTrks.find(t=>t.id===finalTrackId);if(targetTrack){setSubTabs(prev=>prev.map(st=>{if(st.target_id===drag.itemId){return{...st,trackId:finalTrackId,instrumentProgram:targetTrack.instrumentProgram!==undefined?targetTrack.instrumentProgram:st.instrumentProgram,instrumentName:targetTrack.instrumentName||st.instrumentName};}return st;}));}}}setDraggedSectionItem(null);// Sync section tab → section item trên MAIN sau khi MOVE item (user +// 08:15 — vị trí item đổi → canvas section item ở MAIN phải theo) +if(activeTabRef.current&&activeTabRef.current.startsWith('session_')){const _tabId=activeTabRef.current;setTimeout(()=>{try{syncSectionTabToMain(_tabId);}catch(e){}},0);}showToast('Đã di chuyển '+(drag.itemType==='section'?'section':'MIDI item')+'.','success');};document.addEventListener('mousemove',handleMouseMove);document.addEventListener('mouseup',handleMouseUp);return()=>{document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);};},[zoom,activeTab,sessionTabs]);// ── Pending drag: click selects; mousemove > 5px threshold starts the real drag ── useEffect(()=>{const handleMouseMove=e=>{var pd=pendingDragRef.current;if(!pd)return;var dx=e.clientX-pd.startX;if(Math.abs(dx)>5){var pdSnap=pendingDragRef.current;pendingDragRef.current=null;if(pdSnap.itemType==='clip'&&!pdSnap.duplicate&&(!pdSnap.selectedIds||pdSnap.selectedIds.size<=1)){// Single clip move → clip drag machinery if(handleClipDragStartRef.current)handleClipDragStartRef.current(pdSnap.trackId,pdSnap.itemId,pdSnap.clickOffset,false);}else if(handleSectionItemDragStartRef.current){handleSectionItemDragStartRef.current(pdSnap.trackId,pdSnap.itemType,pdSnap.itemId,pdSnap.clickOffset,!!pdSnap.duplicate,pdSnap.selectedIds);}}};document.addEventListener('mousemove',handleMouseMove);var handleMouseUp=function(){pendingDragRef.current=null;};document.addEventListener('mouseup',handleMouseUp);return function(){document.removeEventListener('mousemove',handleMouseMove);document.removeEventListener('mouseup',handleMouseUp);pendingDragRef.current=null;};},[]);// ── Document-level mousemove/mouseup for Section/MIDI item resize ── -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(edgePxn+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(edgePxn+1);}};const handleMouseUp=()=>{const resize=resizedSectionItemRef.current;if(!resize)return;setResizedSectionItem(null);// Sync nội dung section tab → section item trên MAIN sau khi RESIZE item +// (user 08:15 — kéo ngắn midi item trong SECTION-TAB → canvas section +// item ở MAIN phải theo đúng duration mới — trước đây chỉ sync khi XÓA) +if(activeTabRef.current&&activeTabRef.current.startsWith('session_')){const _tabId=activeTabRef.current;setTimeout(()=>{try{syncSectionTabToMain(_tabId);}catch(e){}},0);}};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.startTimestart){found.add(m.id);}});(t.sections||[]).forEach(s=>{if(s.startstart){found.add(s.id);}});(t.clips||[]).forEach(c=>{const dur=c.buffer?c.buffer.duration/(c.speed||1.0):4;if(c.startTimestart){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 toggleTrackSoloEvaluate=trackId=>{const wasPlaying=isPlaying;const playingSubTab=subTabs.find(s=>s.isPlaying&&s.type==='PIANO_ROLL');stopAllPlayback();updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,solo:!t.solo,muted:false}:t));setTimeout(()=>{const soloCtx=getAudioContext();if(wasPlaying){startOffsetTimeRef.current=currentTime;startAudioTimeRef.current=soloCtx.currentTime;startBufferOffsetRef.current=currentTime;setIsPlaying(true);startTrackPlayback(currentTime);}if(playingSubTab){const prSt=subTabs.find(s=>s.id===playingSubTab.id);if(prSt){const prOffset=prSt.currentTime||0;startOffsetTimeRef.current=prOffset;startAudioTimeRef.current=soloCtx.currentTime;startBufferOffsetRef.current=prOffset*(prSt.speed||1.0);schedulePianoRollMidi(prSt,prOffset);startSubTabPlayback(prSt,prOffset);setSubTabs(prev=>prev.map(s=>s.id===prSt.id?Object.assign({},s,{isPlaying:true,currentTime:prOffset}):s));}}},60);setTimeout(()=>lucide.createIcons(),50);};const toggleTrackMute=trackId=>{const beforeSnap=captureTrackSnapshot(trackId);updateActiveTracks(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)=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,...props}:t));// Mixer fader dùng updateTrackProp({volumeDb}) — áp gain realtime cho node @@ -1432,7 +1488,9 @@ try{const _vf=window.__vuFrame=(window.__vuFrame||0)+1;if(_vf%45===0){console.lo // Explorer) — áp dụng mới giữ zoom hiện tại/localStorage. if(result.zoom>1&&setZoom)setZoom(result.zoom);if(result.masteringSettings)setMasteringSettings(result.masteringSettings);}else{restoredTracks=(parsed.tracks||[]).map(function(t){const rest=Object.assign({},t);delete rest.height;return Object.assign({},rest,{buffer:null,channelInfo:t.channelInfo||null,clips:t.clips||[],serverFileId:t.serverFileId||null});});}setTracks(restoredTracks);loadAudioBuffersForTracks(restoredTracks).catch(function(err){console.warn('loadAudioBuffersForTracks error:',err);});trackMidiChannelsRef.current={};preloadTrackInstruments(restoredTracks).catch(function(err){console.warn('preloadTrackInstruments error:',err);});setBpm(restoredBpm.toString());setSelectedTrackId(restoredTracks[0]?.id||'1');setProjectName(proj.name);setCurrentProjectId(proj.id);if(restoredSessionTabs.length>0)setSessionTabs(restoredSessionTabs);if(restoredSubTabs.length>0)setSubTabs(restoredSubTabs);localStorage.setItem('sonic_project_name',proj.name);localStorage.setItem('sonic_project_id',proj.id);showToast('Đã nạp dự án "'+proj.name+'" thành công!',"success");}catch(e){showToast(e.message||"Lỗi khi nạp dự án","error");}}});};return/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{className:"h-full w-full flex flex-col bg-[#1e1e1e]"},/*#__PURE__*/React.createElement("header",{className:"h-9 bg-[#2e2e2e] border-b border-[#181818] flex items-center px-1 shrink-0 select-none"},[{label:'File',items:[{label:'New Project',icon:'file-plus',shortcut:'Ctrl+N',action:()=>{// New project: đóng hết tab + reset MỌI trạng thái về mặc định try{stopAllPlayback();}catch(e){}setSubTabs([]);setSessionTabs([]);setActiveTab('main');// Reset instrument/channel/route context đã load -trackMidiChannelsRef.current={};Object.keys(trackMidiBypassMap).forEach(function(k){delete trackMidiBypassMap[k];});Object.keys(trackAudioBypassMap).forEach(function(k){delete trackAudioBypassMap[k];});Object.keys(trackMasteringBypassMap).forEach(function(k){delete trackMasteringBypassMap[k];});setSelectedItemIds(new Set());setTracks([{id:'1',name:'Track 01',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#0f766e',markers:[],serverFileId:null,clips:[],sections:[],midiItems:[]},{id:'2',name:'Track 02',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#1d4ed8',markers:[],serverFileId:null,clips:[],sections:[],midiItems:[]}]);setSelectedTrackId('1');setProjectName('');setCurrentProjectId(null);localStorage.removeItem('sonic_project_name');localStorage.removeItem('sonic_project_id');showToast('New project created','info');}},{label:'Open Project...',icon:'folder-open',shortcut:'Ctrl+O',action:()=>setOpenProjectModalOpen(true)},{label:'Save Project',icon:'upload-cloud',shortcut:'Ctrl+S',action:()=>handleSaveProject()},{label:'Save As...',icon:'download',shortcut:'Ctrl+Alt+S',action:()=>setSaveAsModalOpen(true)},{sep:true},{label:'Config AI Providers...',icon:'settings',action:()=>setAiConfigModalOpen(true)},{label:'Import Audio...',icon:'file-input',shortcut:'Ctrl+Alt+I',action:()=>{const input=document.createElement('input');input.type='file';input.accept='audio/*';input.onchange=async e=>{if(e.target.files[0]){addNewTrack();const rearrangeNewId=(tracks.length+1).toString();setTimeout(()=>loadFileOnTrack(rearrangeNewId,e.target.files[0]),100);}};input.click();showToast('Import audio','info');}},{label:'Export Mix...',icon:'file-output',action:()=>triggerWavExport()},{label:'Export MIDI...',icon:'music',action:()=>triggerMidiExport()},{label:'Mastering Suite',icon:'wand-2',shortcut:'Ctrl+Shift+M',action:()=>setShowMasteringModal(true)},{sep:true},...(currentUser?[{label:'Profile',icon:'user',action:()=>setProfileModalOpen(true)}]:[]),...(currentUser&¤tUser.role==='admin'?[{label:'System Manager',icon:'settings',action:()=>setSystemManagerModalOpen(true)}]:[]),{label:'Logout',icon:'log-out',action:()=>handleLogout()}]},{label:'Edit',items:[{label:'Insert New Track',icon:'plus',shortcut:'Ctrl+I',action:addNewTrack},{label:'Insert Music to Track',icon:'music',shortcut:'Ctrl+Alt+I',action:()=>showToast('Select music file to insert','info')},{sep:true},{label:'Edit in New Tab',icon:'file-edit',shortcut:'Ctrl+E',action:()=>openTempTab()},{label:'Split at Playhead',icon:'scissors',shortcut:'S',action:()=>handleSplitTrack(selectedTrackId)},{label:'Merge Tracks',icon:'combine',shortcut:'Ctrl+M',action:()=>{handleMergeTracks();}},{sep:true},{label:'Undo',icon:'undo',shortcut:'Ctrl+Z',action:()=>{handleUndo();}},{label:'Redo',icon:'redo',shortcut:'Ctrl+Y',action:()=>{handleRedo();}},{sep:true},{label:'Copy',icon:'copy',shortcut:'Ctrl+C',action:()=>{handleCopyTrack();}},{label:'Cut',icon:'scissors',shortcut:'Ctrl+X',action:()=>{handleCutTrack();}},{label:'Paste',icon:'clipboard',shortcut:'Ctrl+V',action:handlePasteTrack},{sep:true},{label:'Delete Track',icon:'trash-2',shortcut:'Del',action:()=>{handleDeleteTrack();}}]},{label:'Insert',items:[...(!sessionTabs.some(s=>s.id===activeTab)?[{label:'Insert Section',icon:'folder-plus',action:insertSectionAtPlayhead}]:[]),{label:'Insert MIDI item',icon:'music',action:insertMidiItemAtPlayhead},{label:'Insert sound clip',icon:'file-input',action:insertSoundClipAtCursor},{label:'Insert track',icon:'plus',action:insertTrackBelow}]},{label:'View',items:[{label:'Master Track',icon:'disc',action:()=>showToast('Master track view','info')},{label:'Maker View',icon:'layout',action:()=>showToast('Maker view','info')},{label:'Mixer',icon:'sliders',action:()=>window.__toggleMixerRef()},{label:'Tempo Track',icon:'timer',action:()=>showToast('Tempo track','info')},{label:'Video',icon:'film',action:()=>showToast('Video panel','info')},{label:'Media Explorer',icon:'folder-search',action:()=>window.__toggleMediaExplorerRef()}]},{label:'Tools',items:[{label:'Config AI Providers...',icon:'settings',action:()=>setAiConfigModalOpen(true)},{label:'AI MIDI Preset Manager...',icon:'sliders',action:()=>setAiPresetModalOpen(true)},{label:'DSP Tools Panel',icon:'wrench',action:()=>openPanel('python_tools')},{sep:true},{label:'Plugin Manager (SoundFont/VSTi)',icon:'zap',action:()=>{setPluginManagerModalOpen(true);window.SonicAPI.listPlugins().then(data=>setPluginsData(data)).catch(()=>{});}}]},{label:'Help',items:[{label:'About SonicForge',icon:'info',action:()=>showToast('SonicForge Studio v1.0 - Professional DAW','info')}]}].map(menu=>/*#__PURE__*/React.createElement("div",{key:menu.label,className:"relative"},/*#__PURE__*/React.createElement("button",{onClick:()=>setMenuOpen(menuOpen===menu.label?null:menu.label),className:`px-3 py-1 text-xs font-medium transition rounded ${menuOpen===menu.label?'bg-zinc-700 text-zinc-100':'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`},menu.label),menuOpen===menu.label&&/*#__PURE__*/React.createElement("div",{className:`absolute top-full left-0 mt-0.5 bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 z-50 ${menu.label==='Edit'?'w-72':'w-64'}`,onClick:()=>setMenuOpen(null)},menu.items.map((item,i)=>item.sep?/*#__PURE__*/React.createElement("div",{key:i,className:"h-px bg-zinc-700 my-1"}):/*#__PURE__*/React.createElement("button",{key:item.label,onClick:e=>{e.stopPropagation();item.action();setMenuOpen(null);},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":item.icon,className:"w-3.5 h-3.5 text-zinc-500 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},item.label),item.shortcut&&/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},item.shortcut)))))),/*#__PURE__*/React.createElement("div",{className:"flex-1"}),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 px-2"},/*#__PURE__*/React.createElement("span",{className:`text-xs font-bold uppercase px-1.5 py-0.5 rounded ${serverStatus==='connected'?'bg-emerald-950 text-emerald-400':serverStatus==='checking'?'bg-amber-950 text-amber-400':'bg-red-950 text-red-400'}`},"Server: ",serverStatus),/*#__PURE__*/React.createElement("button",{onClick:()=>setShowAIConfig(!showAIConfig),className:`px-1.5 py-0.5 rounded text-xs border transition ${showAIConfig?'bg-purple-900 text-purple-200 border-purple-700':'bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-zinc-200'}`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"cpu",className:"w-3 h-3"}))))),menuOpen&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-40",onClick:()=>setMenuOpen(null)}),/*#__PURE__*/React.createElement("div",{className:"h-7 bg-[#222] border-b border-zinc-800 flex items-stretch px-2 gap-0.5 shrink-0 overflow-x-auto"},/*#__PURE__*/React.createElement("button",{onClick:()=>setActiveTab('main'),className:`px-3 text-xs font-bold uppercase tracking-wider border-b-2 transition flex items-center gap-1 ${activeTab==='main'?'text-cyan-400 border-cyan-500 bg-zinc-800/50':'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'}`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"layout-dashboard",className:"w-3 h-3"}))," Main Session"),sessionTabs.map(st=>{const isTabActive=activeTab===st.id;const tabColor=st.color||(isTabActive?'#06b6d4':null);const borderStyle=isTabActive?{borderBottomColor:tabColor,color:tabColor}:st.color?{color:st.color,borderBottomColor:'transparent'}:{};return/*#__PURE__*/React.createElement("div",{key:st.id,className:"flex items-stretch"},/*#__PURE__*/React.createElement("button",{onClick:()=>setActiveTab(st.id),onAuxClick:e=>{if(e.button===1){e.preventDefault();closeSessionTab(st.id);}},onContextMenu:e=>{e.preventDefault();setTabContextMenu({x:e.clientX,y:e.clientY,tabId:st.id,tabType:'session'});},style:borderStyle,className:`px-2 text-xs font-medium border-b-2 transition flex items-center gap-1 ${isTabActive?'bg-zinc-800/50':'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'}`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"layers",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("span",{className:"max-w-[120px] truncate"},st.name),st.isDirty&&/*#__PURE__*/React.createElement("span",{className:"w-1.5 h-1.5 rounded-full bg-amber-500 ml-1",title:"Chưa lưu thay đổi"})),/*#__PURE__*/React.createElement("button",{onClick:()=>closeSessionTab(st.id),className:"px-1 text-zinc-600 hover:text-red-400 transition text-xs",title:"Close tab"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}))));}),subTabs.map(st=>{const isTabActive=activeTab===st.id;const tabColor=st.color||(isTabActive?'#f59e0b':null);const borderStyle=isTabActive?{borderBottomColor:tabColor,color:tabColor}:st.color?{color:st.color,borderBottomColor:'transparent'}:{};const iconName=st.type==='PIANO_ROLL'?'music':'file-edit';return/*#__PURE__*/React.createElement("div",{key:st.id,className:"flex items-stretch"},/*#__PURE__*/React.createElement("button",{onClick:()=>setActiveTab(st.id),onAuxClick:e=>{if(e.button===1){e.preventDefault();closeSubTab(st.id);}},onContextMenu:e=>{e.preventDefault();setTabContextMenu({x:e.clientX,y:e.clientY,tabId:st.id,tabType:'sub'});},style:borderStyle,className:`px-2 text-xs font-medium border-b-2 transition flex items-center gap-1 ${isTabActive?'bg-zinc-800/50':'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'}`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":iconName,className:"w-3 h-3"})),/*#__PURE__*/React.createElement("span",{className:"max-w-[100px] truncate"},st.label),st.isDirty&&/*#__PURE__*/React.createElement("span",{className:"w-1.5 h-1.5 rounded-full bg-amber-500 ml-1",title:"Chưa lưu thay đổi"})),/*#__PURE__*/React.createElement("button",{onClick:()=>closeSubTab(st.id),className:"px-1 text-zinc-600 hover:text-red-400 transition text-xs",title:"Close tab"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}))));})),showAIConfig&&/*#__PURE__*/React.createElement("div",{className:"bg-zinc-900 border-b border-purple-900 p-3 flex flex-col gap-2 transition-all"},/*#__PURE__*/React.createElement("div",{className:"text-xs font-bold text-purple-400 uppercase tracking-wider flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"cpu",className:"w-4 h-4"}))," Cấu hình cổng kết nối API"),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-2 text-xs"},/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-500 font-bold uppercase"},"Endpoint Base URL"),/*#__PURE__*/React.createElement("input",{type:"text",value:aiConfig.baseUrl,onChange:e=>setAiConfig(prev=>({...prev,baseUrl:e.target.value})),className:"bg-zinc-850 text-zinc-200 p-1.5 rounded border border-zinc-800 focus:outline-none focus:border-purple-600 font-mono text-xs",placeholder:"https://api.openai.com/v1"})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-500 font-bold uppercase"},"API Token Key"),/*#__PURE__*/React.createElement("input",{type:"password",value:aiConfig.apiKey,onChange:e=>setAiConfig(prev=>({...prev,apiKey:e.target.value})),className:"bg-zinc-850 text-zinc-200 p-1.5 rounded border border-zinc-800 focus:outline-none focus:border-purple-600 font-mono text-xs",placeholder:"sk-..."})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-500 font-bold uppercase"},"Model Name"),/*#__PURE__*/React.createElement("input",{type:"text",value:aiConfig.model,onChange:e=>setAiConfig(prev=>({...prev,model:e.target.value})),className:"bg-zinc-850 text-zinc-200 p-1.5 rounded border border-zinc-800 focus:outline-none focus:border-purple-600 font-mono text-xs",placeholder:"gpt-4o-mini"})))),/*#__PURE__*/React.createElement("div",{className:"h-9 bg-[#222] border-b border-zinc-800 flex items-center px-3 gap-2 shrink-0 select-none"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 bg-zinc-900 border border-zinc-700 rounded px-1.5 py-0.5 shadow-lg",title:"Kéo để di chuyển toolbar",style:{cursor:'grab'}},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-0.5 mr-1 text-zinc-600"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"grip-vertical",className:"w-3 h-3"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>{setActiveTool('select');showToast('Select Tool','info');},className:`w-7 h-7 flex items-center justify-center rounded ${activeTool==='select'?'bg-cyan-700 text-white':'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,title:"Select Tool (V)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"mouse-pointer",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>{setActiveTool('grab');showToast('Grab Tool','info');},className:`w-7 h-7 flex items-center justify-center rounded ${activeTool==='grab'?'bg-cyan-700 text-white':'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,title:"Grab Tool (H)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"hand",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-0.5 border border-zinc-700 rounded bg-zinc-850 px-0.5"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setActiveTool('razor');showToast('Razor Tool','info');},className:`w-7 h-7 flex items-center justify-center rounded ${activeTool==='razor'?'bg-cyan-700 text-white':'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,title:"Razor Tool (C)"},/*#__PURE__*/React.createElement("svg",{className:"w-3.5 h-3.5 text-orange-400",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round"},/*#__PURE__*/React.createElement("path",{d:"M6 3h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"}),/*#__PURE__*/React.createElement("path",{d:"M4 9h16l-3 9H7z"}),/*#__PURE__*/React.createElement("circle",{cx:"12",cy:"6",r:"1"}))),/*#__PURE__*/React.createElement("button",{onClick:handleGlueTracks,className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-purple-400 hover:bg-zinc-800",title:"Glue Clips"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"link",className:"w-3.5 h-3.5"})))),/*#__PURE__*/React.createElement("button",{onClick:()=>{setActiveTool('pen');showToast('Pen Tool','info');},className:`w-7 h-7 flex items-center justify-center rounded ${activeTool==='pen'?'bg-cyan-700 text-white':'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,title:"Pen Tool (P)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"pen-tool",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-700 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:handleCutTrack,className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-red-400 hover:bg-zinc-800",title:"Cut (Ctrl+X)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"scissors",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:handleCopyTrack,className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-blue-400 hover:bg-zinc-800",title:"Copy (Ctrl+C)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"copy",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:handlePasteTrack,className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-emerald-400 hover:bg-zinc-800",title:"Paste (Ctrl+V)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"clipboard",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-700 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:addNewTrack,className:"px-2 py-1 bg-cyan-600 hover:bg-cyan-500 text-white font-semibold rounded text-xs flex items-center gap-1 shadow transition",title:"Thêm Track Mới (Ctrl+I)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus",className:"w-3.5 h-3.5"})),/*#__PURE__*/React.createElement("span",null,"Track")),sessionTabs.some(s=>s.id===activeTab)&&/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-700 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:()=>handleSaveSectionTab(activeTab),className:"px-2 py-1 bg-emerald-600 hover:bg-emerald-500 text-white font-semibold rounded text-xs flex items-center gap-1 shadow transition",title:"Lưu Section vào Main Session"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"save",className:"w-3.5 h-3.5"})),/*#__PURE__*/React.createElement("span",null,"Lưu Section"))),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-700 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:handleUndo,disabled:undoStack.length===0&&(!window.UndoRedoEngine||!window.UndoRedoEngine.canUndo()),className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30",title:"Undo (Ctrl+Z)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"undo",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:handleRedo,disabled:redoStack.length===0&&(!window.UndoRedoEngine||!window.UndoRedoEngine.canRedo()),className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30",title:"Redo (Ctrl+Y)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"redo",className:"w-3.5 h-3.5"})))),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-800 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:()=>seekPlaybackTo(0),className:"w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",title:"Quay lại đầu"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"skip-back",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>{const isSubTab=subTabs.some(sub=>sub.id===activeTab);if(isSubTab){const st=subTabs.find(s=>s.id===activeTab);if(!st)return;const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):null;if(left!==null)seekPlaybackTo(left);}else{if(selLeft!==null)seekPlaybackTo(selLeft);}},className:"w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",title:"Đầu vùng chọn"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"step-back",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:handlePlayPause,className:`w-7 h-7 flex items-center justify-center rounded border transition ${isPlaying?'bg-emerald-600 text-black border-emerald-500 hover:bg-emerald-500':'bg-cyan-600 text-white border-cyan-500 hover:bg-cyan-500'}`,title:isPlaying?"Tạm dừng":"Play"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":isPlaying?"pause":"play",className:"w-3.5 h-3.5 fill-current"}))),/*#__PURE__*/React.createElement("button",{onClick:handleStop,className:"w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",title:"Stop"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"square",className:"w-3.5 h-3.5 fill-current"}))),/*#__PURE__*/React.createElement("button",{onClick:handleRecordClick,className:`w-7 h-7 flex items-center justify-center rounded border transition ${recordingState==='RECORDING'?'bg-red-600 text-white border-red-500 hover:bg-red-500 animate-pulse':recordingState==='COUNT_IN'?'bg-amber-500 text-black border-amber-400 hover:bg-amber-400 animate-pulse':'bg-zinc-800 text-red-500 border-zinc-700 hover:bg-zinc-700 hover:text-red-400'}`,title:recordingState==='RECORDING'?"Đang ghi âm...":recordingState==='COUNT_IN'?"Chuẩn bị ghi âm...":"Ghi âm (Record)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"circle",className:"w-3.5 h-3.5 fill-current"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>{const isSubTab=subTabs.some(sub=>sub.id===activeTab);if(isSubTab){setSubTabs(prev=>prev.map(s=>{if(s.id!==activeTab)return s;const right=s.selectionStart!==null&&s.selectionEnd!==null?Math.max(s.selectionStart,s.selectionEnd):null;return right!==null?{...s,currentTime:right}:s;}));}else{if(selRight!==null)setCurrentTime(selRight);}},className:"w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",title:"Cuối vùng chọn"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"step-forward",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>{const isSubTab=subTabs.some(sub=>sub.id===activeTab);if(isSubTab){setSubTabs(prev=>prev.map(s=>{if(s.id!==activeTab)return s;const duration=s.buffer?s.buffer.duration/(s.speed||1.0):0;return{...s,currentTime:duration};}));}else{setCurrentTime(maxDuration);}},className:"w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",title:"Đến cuối"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"skip-forward",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-800 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:()=>{setIsLoopingSelection(prev=>!prev);// Sync loop state with active sub-tab (piano roll / audio editor) +trackMidiChannelsRef.current={};Object.keys(trackMidiBypassMap).forEach(function(k){delete trackMidiBypassMap[k];});Object.keys(trackAudioBypassMap).forEach(function(k){delete trackAudioBypassMap[k];});Object.keys(trackMasteringBypassMap).forEach(function(k){delete trackMasteringBypassMap[k];});setSelectedItemIds(new Set());setTracks([{id:'1',name:'Track 01',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#0f766e',markers:[],serverFileId:null,clips:[],sections:[],midiItems:[]},{id:'2',name:'Track 02',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#1d4ed8',markers:[],serverFileId:null,clips:[],sections:[],midiItems:[]}]);setSelectedTrackId('1');setProjectName('');setCurrentProjectId(null);localStorage.removeItem('sonic_project_name');localStorage.removeItem('sonic_project_id');showToast('New project created','info');}},{label:'Open Project...',icon:'folder-open',shortcut:'Ctrl+O',action:()=>setOpenProjectModalOpen(true)},{label:'Save Project',icon:'upload-cloud',shortcut:'Ctrl+S',action:()=>{// Save project + save TẤT CẢ dirty sub-tabs (user 07:35 — nhấn lưu ở +// MAIN phải lưu cả piano roll/audio tab đã sửa) +handleSaveProject();subTabsRef.current.filter(s=>s.isDirty).forEach(st=>{if(st.type==='PIANO_ROLL'){handleSaveMidiNotes(st.id,st.trackId,st.target_id,st.notes||[]);}else if(st.type==='SECTION'){handleSaveSectionTab(st.id);}else if(st.buffer){const subTrack=activeTracksRef.current.find(t=>t.id===st.trackId);if(subTrack){updateActiveTracks(prev=>prev.map(t=>t.id===st.trackId?{...t,buffer:st.buffer}:t));}}});showToast('Đã lưu dự án + các tab đã sửa','success');}},{label:'Save As...',icon:'download',shortcut:'Ctrl+Alt+S',action:()=>setSaveAsModalOpen(true)},{sep:true},{label:'Config AI Providers...',icon:'settings',action:()=>setAiConfigModalOpen(true)},{label:'Import Audio...',icon:'file-input',shortcut:'Ctrl+Alt+I',action:()=>{const input=document.createElement('input');input.type='file';input.accept='audio/*';input.onchange=async e=>{if(e.target.files[0]){addNewTrack();const rearrangeNewId=(tracks.length+1).toString();setTimeout(()=>loadFileOnTrack(rearrangeNewId,e.target.files[0]),100);}};input.click();showToast('Import audio','info');}},{label:'Export Mix...',icon:'file-output',action:()=>triggerWavExport()},{label:'Export MIDI...',icon:'music',action:()=>triggerMidiExport()},{label:'Mastering Suite',icon:'wand-2',shortcut:'Ctrl+Shift+M',action:()=>setShowMasteringModal(true)},{sep:true},...(currentUser?[{label:'Profile',icon:'user',action:()=>setProfileModalOpen(true)}]:[]),...(currentUser&¤tUser.role==='admin'?[{label:'System Manager',icon:'settings',action:()=>setSystemManagerModalOpen(true)}]:[]),{label:'Logout',icon:'log-out',action:()=>handleLogout()}]},{label:'Edit',items:[{label:'Insert New Track',icon:'plus',shortcut:'Ctrl+I',action:addNewTrack},{label:'Insert Music to Track',icon:'music',shortcut:'Ctrl+Alt+I',action:()=>showToast('Select music file to insert','info')},{sep:true},{label:'Edit in New Tab',icon:'file-edit',shortcut:'Ctrl+E',action:()=>openTempTab()},{label:'Split at Playhead',icon:'scissors',shortcut:'S',action:()=>handleSplitTrack(selectedTrackId)},{label:'Merge Tracks',icon:'combine',shortcut:'Ctrl+M',action:()=>{handleMergeTracks();}},{sep:true},{label:'Undo',icon:'undo',shortcut:'Ctrl+Z',action:()=>{handleUndo();}},{label:'Redo',icon:'redo',shortcut:'Ctrl+Y',action:()=>{handleRedo();}},{sep:true},{label:'Copy',icon:'copy',shortcut:'Ctrl+C',action:()=>{handleCopyTrack();}},{label:'Cut',icon:'scissors',shortcut:'Ctrl+X',action:()=>{handleCutTrack();}},{label:'Paste',icon:'clipboard',shortcut:'Ctrl+V',action:handlePasteTrack},{sep:true},{label:'Delete Track',icon:'trash-2',shortcut:'Del',action:()=>{handleDeleteTrack();}}]},{label:'Insert',items:[...(!sessionTabs.some(s=>s.id===activeTab)?[{label:'Insert Section',icon:'folder-plus',action:insertSectionAtPlayhead}]:[]),{label:'Insert MIDI item',icon:'music',action:insertMidiItemAtPlayhead},{label:'Insert sound clip',icon:'file-input',action:insertSoundClipAtCursor},{label:'Insert track',icon:'plus',action:insertTrackBelow}]},{label:'View',items:[{label:'Master Track',icon:'disc',action:()=>showToast('Master track view','info')},{label:'Maker View',icon:'layout',action:()=>showToast('Maker view','info')},{label:'Mixer',icon:'sliders',action:()=>window.__toggleMixerRef()},{label:'Tempo Track',icon:'timer',action:()=>showToast('Tempo track','info')},{label:'Video',icon:'film',action:()=>showToast('Video panel','info')},{label:'Media Explorer',icon:'folder-search',action:()=>window.__toggleMediaExplorerRef()}]},{label:'Tools',items:[{label:'Config AI Providers...',icon:'settings',action:()=>setAiConfigModalOpen(true)},{label:'AI MIDI Preset Manager...',icon:'sliders',action:()=>setAiPresetModalOpen(true)},{label:'DSP Tools Panel',icon:'wrench',action:()=>openPanel('python_tools')},{sep:true},{label:'Plugin Manager (SoundFont/VSTi)',icon:'zap',action:()=>{setPluginManagerModalOpen(true);window.SonicAPI.listPlugins().then(data=>setPluginsData(data)).catch(()=>{});}}]},{label:'Help',items:[{label:'About SonicForge',icon:'info',action:()=>showToast('SonicForge Studio v1.0 - Professional DAW','info')}]}].map(menu=>/*#__PURE__*/React.createElement("div",{key:menu.label,className:"relative"},/*#__PURE__*/React.createElement("button",{onClick:()=>setMenuOpen(menuOpen===menu.label?null:menu.label),className:`px-3 py-1 text-xs font-medium transition rounded ${menuOpen===menu.label?'bg-zinc-700 text-zinc-100':'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`},menu.label),menuOpen===menu.label&&/*#__PURE__*/React.createElement("div",{className:`absolute top-full left-0 mt-0.5 bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 z-50 ${menu.label==='Edit'?'w-72':'w-64'}`,onClick:()=>setMenuOpen(null)},menu.items.map((item,i)=>item.sep?/*#__PURE__*/React.createElement("div",{key:i,className:"h-px bg-zinc-700 my-1"}):/*#__PURE__*/React.createElement("button",{key:item.label,onClick:e=>{e.stopPropagation();item.action();setMenuOpen(null);},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":item.icon,className:"w-3.5 h-3.5 text-zinc-500 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},item.label),item.shortcut&&/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},item.shortcut)))))),/*#__PURE__*/React.createElement("div",{className:"flex-1"}),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 px-2"},/*#__PURE__*/React.createElement("span",{className:`text-xs font-bold uppercase px-1.5 py-0.5 rounded ${serverStatus==='connected'?'bg-emerald-950 text-emerald-400':serverStatus==='checking'?'bg-amber-950 text-amber-400':'bg-red-950 text-red-400'}`},"Server: ",serverStatus),/*#__PURE__*/React.createElement("button",{onClick:()=>setShowAIConfig(!showAIConfig),className:`px-1.5 py-0.5 rounded text-xs border transition ${showAIConfig?'bg-purple-900 text-purple-200 border-purple-700':'bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-zinc-200'}`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"cpu",className:"w-3 h-3"}))))),menuOpen&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-40",onClick:()=>setMenuOpen(null)}),/*#__PURE__*/React.createElement("div",{className:"h-7 bg-[#222] border-b border-zinc-800 flex items-stretch px-2 gap-0.5 shrink-0 overflow-x-auto"},/*#__PURE__*/React.createElement("button",{onClick:()=>setActiveTab('main'),className:`px-3 text-xs font-bold uppercase tracking-wider border-b-2 transition flex items-center gap-1 ${activeTab==='main'?'text-cyan-400 border-cyan-500 bg-zinc-800/50':'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'}`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"layout-dashboard",className:"w-3 h-3"}))," Main Session"),sessionTabs.map(st=>{const isTabActive=activeTab===st.id;const tabColor=st.color||(isTabActive?'#06b6d4':null);const borderStyle=isTabActive?{borderBottomColor:tabColor,color:tabColor}:st.color?{color:st.color,borderBottomColor:'transparent'}:{};return/*#__PURE__*/React.createElement("div",{key:st.id,className:"flex items-stretch"},/*#__PURE__*/React.createElement("button",{onClick:()=>setActiveTab(st.id),onAuxClick:e=>{if(e.button===1){e.preventDefault();closeSessionTab(st.id);}},onContextMenu:e=>{e.preventDefault();setTabContextMenu({x:e.clientX,y:e.clientY,tabId:st.id,tabType:'session'});},style:borderStyle,className:`px-2 text-xs font-medium border-b-2 transition flex items-center gap-1 ${isTabActive?'bg-zinc-800/50':'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'}`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"layers",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("span",{className:"max-w-[120px] truncate"},st.name),st.isDirty&&/*#__PURE__*/React.createElement("span",{className:"w-1.5 h-1.5 rounded-full bg-amber-500 ml-1",title:"Chưa lưu thay đổi"})),/*#__PURE__*/React.createElement("button",{onClick:()=>closeSessionTab(st.id),className:"px-1 text-zinc-600 hover:text-red-400 transition text-xs",title:"Close tab"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}))));}),subTabs.map(st=>{const isTabActive=activeTab===st.id;const tabColor=st.color||(isTabActive?'#f59e0b':null);const borderStyle=isTabActive?{borderBottomColor:tabColor,color:tabColor}:st.color?{color:st.color,borderBottomColor:'transparent'}:{};const iconName=st.type==='PIANO_ROLL'?'music':'file-edit';return/*#__PURE__*/React.createElement("div",{key:st.id,className:"flex items-stretch"},/*#__PURE__*/React.createElement("button",{onClick:()=>setActiveTab(st.id),onAuxClick:e=>{if(e.button===1){e.preventDefault();closeSubTab(st.id);}},onContextMenu:e=>{e.preventDefault();setTabContextMenu({x:e.clientX,y:e.clientY,tabId:st.id,tabType:'sub'});},style:borderStyle,className:`px-2 text-xs font-medium border-b-2 transition flex items-center gap-1 ${isTabActive?'bg-zinc-800/50':'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'}`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":iconName,className:"w-3 h-3"})),/*#__PURE__*/React.createElement("span",{className:"max-w-[100px] truncate"},st.label),st.isDirty&&/*#__PURE__*/React.createElement("span",{className:"w-1.5 h-1.5 rounded-full bg-amber-500 ml-1",title:"Chưa lưu thay đổi"})),/*#__PURE__*/React.createElement("button",{onClick:()=>closeSubTab(st.id),className:"px-1 text-zinc-600 hover:text-red-400 transition text-xs",title:"Close tab"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}))));})),showAIConfig&&/*#__PURE__*/React.createElement("div",{className:"bg-zinc-900 border-b border-purple-900 p-3 flex flex-col gap-2 transition-all"},/*#__PURE__*/React.createElement("div",{className:"text-xs font-bold text-purple-400 uppercase tracking-wider flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"cpu",className:"w-4 h-4"}))," Cấu hình cổng kết nối API"),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-2 text-xs"},/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-500 font-bold uppercase"},"Endpoint Base URL"),/*#__PURE__*/React.createElement("input",{type:"text",value:aiConfig.baseUrl,onChange:e=>setAiConfig(prev=>({...prev,baseUrl:e.target.value})),className:"bg-zinc-850 text-zinc-200 p-1.5 rounded border border-zinc-800 focus:outline-none focus:border-purple-600 font-mono text-xs",placeholder:"https://api.openai.com/v1"})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-500 font-bold uppercase"},"API Token Key"),/*#__PURE__*/React.createElement("input",{type:"password",value:aiConfig.apiKey,onChange:e=>setAiConfig(prev=>({...prev,apiKey:e.target.value})),className:"bg-zinc-850 text-zinc-200 p-1.5 rounded border border-zinc-800 focus:outline-none focus:border-purple-600 font-mono text-xs",placeholder:"sk-..."})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-500 font-bold uppercase"},"Model Name"),/*#__PURE__*/React.createElement("input",{type:"text",value:aiConfig.model,onChange:e=>setAiConfig(prev=>({...prev,model:e.target.value})),className:"bg-zinc-850 text-zinc-200 p-1.5 rounded border border-zinc-800 focus:outline-none focus:border-purple-600 font-mono text-xs",placeholder:"gpt-4o-mini"})))),/*#__PURE__*/React.createElement("div",{className:"h-9 bg-[#222] border-b border-zinc-800 flex items-center px-3 gap-2 shrink-0 select-none"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 bg-zinc-900 border border-zinc-700 rounded px-1.5 py-0.5 shadow-lg",title:"Kéo để di chuyển toolbar",style:{cursor:'grab'}},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-0.5 mr-1 text-zinc-600"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"grip-vertical",className:"w-3 h-3"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>{setActiveTool('select');showToast('Select Tool','info');},className:`w-7 h-7 flex items-center justify-center rounded ${activeTool==='select'?'bg-cyan-700 text-white':'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,title:"Select Tool (V)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"mouse-pointer",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>{setActiveTool('grab');showToast('Grab Tool','info');},className:`w-7 h-7 flex items-center justify-center rounded ${activeTool==='grab'?'bg-cyan-700 text-white':'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,title:"Grab Tool (H)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"hand",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-0.5 border border-zinc-700 rounded bg-zinc-850 px-0.5"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setActiveTool('razor');showToast('Razor Tool','info');},className:`w-7 h-7 flex items-center justify-center rounded ${activeTool==='razor'?'bg-cyan-700 text-white':'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,title:"Razor Tool (C)"},/*#__PURE__*/React.createElement("svg",{className:"w-3.5 h-3.5 text-orange-400",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round"},/*#__PURE__*/React.createElement("path",{d:"M6 3h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"}),/*#__PURE__*/React.createElement("path",{d:"M4 9h16l-3 9H7z"}),/*#__PURE__*/React.createElement("circle",{cx:"12",cy:"6",r:"1"}))),/*#__PURE__*/React.createElement("button",{onClick:handleGlueTracks,className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-purple-400 hover:bg-zinc-800",title:"Glue Clips"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"link",className:"w-3.5 h-3.5"})))),/*#__PURE__*/React.createElement("button",{onClick:()=>{setActiveTool('pen');showToast('Pen Tool','info');},className:`w-7 h-7 flex items-center justify-center rounded ${activeTool==='pen'?'bg-cyan-700 text-white':'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,title:"Pen Tool (P)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"pen-tool",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-700 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:handleCutTrack,className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-red-400 hover:bg-zinc-800",title:"Cut (Ctrl+X)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"scissors",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:handleCopyTrack,className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-blue-400 hover:bg-zinc-800",title:"Copy (Ctrl+C)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"copy",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:handlePasteTrack,className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-emerald-400 hover:bg-zinc-800",title:"Paste (Ctrl+V)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"clipboard",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-700 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:addNewTrack,className:"px-2 py-1 bg-cyan-600 hover:bg-cyan-500 text-white font-semibold rounded text-xs flex items-center gap-1 shadow transition",title:"Thêm Track Mới (Ctrl+I)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus",className:"w-3.5 h-3.5"})),/*#__PURE__*/React.createElement("span",null,"Track")),sessionTabs.some(s=>s.id===activeTab)&&/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-700 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:()=>handleSaveSectionTab(activeTab),className:"px-2 py-1 bg-emerald-600 hover:bg-emerald-500 text-white font-semibold rounded text-xs flex items-center gap-1 shadow transition",title:"Lưu Section vào Main Session"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"save",className:"w-3.5 h-3.5"})),/*#__PURE__*/React.createElement("span",null,"Lưu Section"))),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-700 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:handleUndo,disabled:undoStack.length===0&&(!window.UndoRedoEngine||!window.UndoRedoEngine.canUndo()),className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30",title:"Undo (Ctrl+Z)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"undo",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:handleRedo,disabled:redoStack.length===0&&(!window.UndoRedoEngine||!window.UndoRedoEngine.canRedo()),className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30",title:"Redo (Ctrl+Y)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"redo",className:"w-3.5 h-3.5"})))),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-800 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:()=>seekPlaybackTo(0),className:"w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",title:"Quay lại đầu"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"skip-back",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>{const isSubTab=subTabs.some(sub=>sub.id===activeTab);if(isSubTab){const st=subTabs.find(s=>s.id===activeTab);if(!st)return;const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):null;if(left!==null)seekPlaybackTo(left);}else{if(selLeft!==null)seekPlaybackTo(selLeft);}},className:"w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",title:"Đầu vùng chọn"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"step-back",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:handlePlayPause,className:`w-7 h-7 flex items-center justify-center rounded border transition ${isPlaying?'bg-emerald-600 text-black border-emerald-500 hover:bg-emerald-500':'bg-cyan-600 text-white border-cyan-500 hover:bg-cyan-500'}`,title:isPlaying?"Tạm dừng":"Play"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":isPlaying?"pause":"play",className:"w-3.5 h-3.5 fill-current"}))),/*#__PURE__*/React.createElement("button",{onClick:handleStop,className:"w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",title:"Stop"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"square",className:"w-3.5 h-3.5 fill-current"}))),/*#__PURE__*/React.createElement("button",{onClick:handleRecordClick,className:`w-7 h-7 flex items-center justify-center rounded border transition ${recordingState==='RECORDING'?'bg-red-600 text-white border-red-500 hover:bg-red-500 animate-pulse':recordingState==='COUNT_IN'?'bg-amber-500 text-black border-amber-400 hover:bg-amber-400 animate-pulse':'bg-zinc-800 text-red-500 border-zinc-700 hover:bg-zinc-700 hover:text-red-400'}`,title:recordingState==='RECORDING'?"Đang ghi âm...":recordingState==='COUNT_IN'?"Chuẩn bị ghi âm...":"Ghi âm (Record)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"circle",className:"w-3.5 h-3.5 fill-current"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>{const isSubTab=subTabs.some(sub=>sub.id===activeTab);if(isSubTab){setSubTabs(prev=>prev.map(s=>{if(s.id!==activeTab)return s;const right=s.selectionStart!==null&&s.selectionEnd!==null?Math.max(s.selectionStart,s.selectionEnd):null;return right!==null?{...s,currentTime:right}:s;}));}else{if(selRight!==null)setCurrentTime(selRight);}},className:"w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",title:"Cuối vùng chọn"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"step-forward",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>{const isSubTab=subTabs.some(sub=>sub.id===activeTab);if(isSubTab){setSubTabs(prev=>prev.map(s=>{if(s.id!==activeTab)return s;const duration=s.buffer?s.buffer.duration/(s.speed||1.0):0;return{...s,currentTime:duration};}));}else{setCurrentTime(maxDuration);}},className:"w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",title:"Đến cuối"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"skip-forward",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-800 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:()=>{setIsLoopingSelection(prev=>!prev);// Sync loop state with active sub-tab (piano roll / audio editor) const activeSub=activeTab&&subTabs.find(s=>s.id===activeTab&&['PIANO_ROLL','AUDIO_CLIP_EDITOR','SECTION_EDITOR'].includes(s.type));if(activeSub){const newLoop=!activeSub.isLooping;setSubTabs(prev=>prev.map(s=>s.id===activeTab?{...s,isLooping:newLoop}:s));if(newLoop){const bpmVal=parseInt(bpm)||120;const beatSec=60.0/bpmVal;let maxEnd=0;if(activeSub.type==='PIANO_ROLL'){(activeSub.notes||[]).forEach(n=>{const end=(n.start_beat||0)+(n.duration_beats||1);if(end>maxEnd)maxEnd=end;});}else if(activeSub.type==='SECTION_EDITOR'){(activeSub.clips||[]).forEach(c=>{const end=(c.startTime||0)+(c.duration||0);if(end>maxEnd)maxEnd=end;});}else if(activeSub.type==='AUDIO_CLIP_EDITOR'){const dur=activeSub.buffer?.duration||0;if(dur>maxEnd)maxEnd=dur;}const loopEndTime=activeSub.type==='PIANO_ROLL'?Math.max(maxEnd,16)*beatSec+1.0:Math.max(maxEnd,1);setSubTabs(prev=>prev.map(s=>s.id===activeTab?{...s,selectionStart:0,selectionEnd:loopEndTime}:s));}}else{// Main timeline / section tab: nếu user đã chọn vùng loop riêng → // giữ nguyên (loop vùng chọn — nhánh updatePlayhead selLeft/ // selRight). KHÔNG auto-derive selection: LOOP bật → loop tại diff --git a/app/templates/index.html b/app/templates/index.html index cb526e6..3b3ec4f 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -24,7 +24,7 @@ - +