diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index c6d985d..c52d8a7 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -4467,11 +4467,19 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot } else if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) { e.preventDefault(); handleRedo(); + } else if (e.key === 'Delete' || e.key === 'Backspace') { + if (selectedNoteIds.length > 0 && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') { + e.preventDefault(); + pushToUndo(notes); + setNotes(prev => prev.filter(n => !selectedNoteIds.includes(n.id))); + setSelectedNoteIds([]); + showToast(`Đã xóa ${selectedNoteIds.length} nốt!`, 'info'); + } } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); - }, [handleUndo, handleRedo]); + }, [handleUndo, handleRedo, notes, selectedNoteIds]); React.useEffect(() => { onUpdateNotes(st.id, notes); @@ -4739,7 +4747,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot const beat = x / pixelsPerBeat; const pitch = 127 - Math.floor(y / NoteHeight); - // Right click -> Quick delete note! + // Right click -> Quick delete note or start erase sweep if (e.button === 2) { e.preventDefault(); const clickedNote = notes.find(n => { @@ -4749,8 +4757,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot pushToUndo(notes); setNotes(prev => prev.filter(n => n.id !== clickedNote.id)); setSelectedNoteIds(prev => prev.filter(id => id !== clickedNote.id)); - showToast('Đã xóa nốt nhanh!', 'info'); + showToast('Đã xóa nốt!', 'info'); } + // Start erase sweep + notesBeforeDragRef.current = JSON.parse(JSON.stringify(notes)); + setDraggedNote({ mode: 'erase_sweep', visitedPitches: [pitch] }); return; } @@ -4769,12 +4780,32 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot } } - // Ctrl+click: duplicate or selection - if (e.ctrlKey && !e.altKey) { + // Ctrl+click: toggle selection (multi-select) + if (e.ctrlKey && !e.altKey && !e.shiftKey) { + if (clickedNoteIdx !== -1) { + const clickedNote = notes[clickedNoteIdx]; + if (selectedNoteIds.includes(clickedNote.id)) { + setSelectedNoteIds(prev => prev.filter(id => id !== clickedNote.id)); + } else { + setSelectedNoteIds(prev => [...prev, clickedNote.id]); + } + return; + } else { + // Ctrl+click on empty space: start selection marquee + setSelectedNoteIds([]); + setSelectionMarquee({ + startBeat: beat, startPitch: pitch, + currentBeat: beat, currentPitch: pitch + }); + return; + } + } + + // Ctrl+Shift+click: duplicate selected + clicked notes + if (e.ctrlKey && e.shiftKey) { if (clickedNoteIdx !== -1) { pushToUndo(notes); const clickedNote = notes[clickedNoteIdx]; - // Duplicate: clone all selected notes + clicked note const idsToClone = [...new Set([...selectedNoteIds, clickedNote.id])]; const clones = notes.filter(n => idsToClone.includes(n.id)).map(n => ({ ...JSON.parse(JSON.stringify(n)), @@ -4792,13 +4823,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot startOffsetPitch: pitch, selectedNotesOffset: cloneOffsets }); - } else { - // Ctrl+click on empty space: start selection marquee - setSelectedNoteIds([]); - setSelectionMarquee({ - startBeat: beat, startPitch: pitch, - currentBeat: beat, currentPitch: pitch - }); } return; } @@ -4985,9 +5009,19 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot } return; } + if (draggedNote.mode === 'erase_sweep') { + const erasePitches = draggedNote.visitedPitches || []; + if (!erasePitches.includes(pitch)) { + draggedNote.visitedPitches = [...erasePitches, pitch]; + setNotes(prev => prev.filter(n => !(n.pitch === pitch && beat >= n.start_beat && beat < n.start_beat + n.duration_beats))); + } + return; + } if (draggedNote.mode === 'scale') { const newEnd = getSnapBeat(Math.max(draggedNote.firstStart + 0.125, beat), snapVal); - const scaleFactor = (newEnd - draggedNote.firstStart) / (draggedNote.originalEnd - draggedNote.firstStart); + const range = draggedNote.originalEnd - draggedNote.firstStart; + if (range <= 0) return; + const scaleFactor = (newEnd - draggedNote.firstStart) / range; const ids = draggedNote.selectedNoteIds || []; const firstStart = draggedNote.firstStart; setNotes(prev => prev.map(n => { @@ -5012,9 +5046,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot }; })); } else if (draggedNote.mode === 'move') { - const baseOriginal = draggedNote.selectedNotesOffset.find(o => o.id === notes[draggedNote.idx].id); - const deltaBeat = getSnapBeat(beat - draggedNote.startOffsetBeat, snapVal) - baseOriginal.originalStartBeat; - const deltaPitch = Math.round(pitch - draggedNote.startOffsetPitch) - baseOriginal.originalPitch; + const firstOffset = draggedNote.selectedNotesOffset && draggedNote.selectedNotesOffset[0]; + if (!firstOffset) return; + const firstNote = notes.find(n => n.id === firstOffset.id); + if (!firstNote) return; + const deltaBeat = getSnapBeat(beat - draggedNote.startOffsetBeat, snapVal) - firstOffset.originalStartBeat; + const deltaPitch = Math.round(pitch - draggedNote.startOffsetPitch) - firstOffset.originalPitch; setNotes(prev => prev.map(n => { const offset = draggedNote.selectedNotesOffset.find(o => o.id === n.id); diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 3d7ae80..b2d87ae 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -130,7 +130,7 @@ const KeybedPixelHeight=(128-PITCH_START)*NoteHeight;const pixelsPerBeat=rollZoo const drawWidth=totalBeats*pixelsPerBeat;const[notes,setNotes]=React.useState(st.notes||[]);const[selectedNoteIds,setSelectedNoteIds]=React.useState([]);const[selectionMarquee,setSelectionMarquee]=React.useState(null);// { startBeat, startPitch, currentBeat, currentPitch } const[draggedNote,setDraggedNote]=React.useState(null);// { mode: 'move'|'resize', idx, startOffsetBeat, originalStart } const[hoveredResizeIdx,setHoveredResizeIdx]=React.useState(-1);// Undo/redo stacks -const undoStackRef=React.useRef([]);const redoStackRef=React.useRef([]);const notesBeforeDragRef=React.useRef(null);const pushToUndo=React.useCallback(prevNotes=>{undoStackRef.current.push(JSON.parse(JSON.stringify(prevNotes)));redoStackRef.current=[];if(undoStackRef.current.length>50)undoStackRef.current.shift();},[]);const handleUndo=React.useCallback(()=>{const prev=undoStackRef.current.pop();if(!prev)return;redoStackRef.current.push(JSON.parse(JSON.stringify(notes)));setNotes(prev);setSelectedNoteIds([]);},[notes]);const handleRedo=React.useCallback(()=>{const next=redoStackRef.current.pop();if(!next)return;undoStackRef.current.push(JSON.parse(JSON.stringify(notes)));setNotes(next);setSelectedNoteIds([]);},[notes]);React.useEffect(()=>{const handler=e=>{if((e.ctrlKey||e.metaKey)&&e.key==='z'&&!e.shiftKey){e.preventDefault();handleUndo();}else if((e.ctrlKey||e.metaKey)&&(e.key==='y'||e.key==='z'&&e.shiftKey)){e.preventDefault();handleRedo();}};window.addEventListener('keydown',handler);return()=>window.removeEventListener('keydown',handler);},[handleUndo,handleRedo]);React.useEffect(()=>{onUpdateNotes(st.id,notes);},[notes]);const getSnapBeat=(beat,mode)=>{let q=0.25;if(mode==='free')return beat;if(mode==='1')q=4.0;else if(mode==='1/2')q=2.0;else if(mode==='1/4')q=1.0;else if(mode==='1/8')q=0.5;else if(mode==='1/16')q=0.25;else if(mode==='1/32')q=0.125;return Math.round(beat/q)*q;};const getSnapDuration=mode=>{if(mode==='free')return 0.25;if(mode==='1')return 4.0;if(mode==='1/2')return 2.0;if(mode==='1/4')return 1.0;if(mode==='1/8')return 0.5;if(mode==='1/16')return 0.25;if(mode==='1/32')return 0.125;return 0.25;};// Local Zoom Wheel Event handler to block browser page zoom +const undoStackRef=React.useRef([]);const redoStackRef=React.useRef([]);const notesBeforeDragRef=React.useRef(null);const pushToUndo=React.useCallback(prevNotes=>{undoStackRef.current.push(JSON.parse(JSON.stringify(prevNotes)));redoStackRef.current=[];if(undoStackRef.current.length>50)undoStackRef.current.shift();},[]);const handleUndo=React.useCallback(()=>{const prev=undoStackRef.current.pop();if(!prev)return;redoStackRef.current.push(JSON.parse(JSON.stringify(notes)));setNotes(prev);setSelectedNoteIds([]);},[notes]);const handleRedo=React.useCallback(()=>{const next=redoStackRef.current.pop();if(!next)return;undoStackRef.current.push(JSON.parse(JSON.stringify(notes)));setNotes(next);setSelectedNoteIds([]);},[notes]);React.useEffect(()=>{const handler=e=>{if((e.ctrlKey||e.metaKey)&&e.key==='z'&&!e.shiftKey){e.preventDefault();handleUndo();}else if((e.ctrlKey||e.metaKey)&&(e.key==='y'||e.key==='z'&&e.shiftKey)){e.preventDefault();handleRedo();}else if(e.key==='Delete'||e.key==='Backspace'){if(selectedNoteIds.length>0&&e.target.tagName!=='INPUT'&&e.target.tagName!=='TEXTAREA'){e.preventDefault();pushToUndo(notes);setNotes(prev=>prev.filter(n=>!selectedNoteIds.includes(n.id)));setSelectedNoteIds([]);showToast(`Đã xóa ${selectedNoteIds.length} nốt!`,'info');}}};window.addEventListener('keydown',handler);return()=>window.removeEventListener('keydown',handler);},[handleUndo,handleRedo,notes,selectedNoteIds]);React.useEffect(()=>{onUpdateNotes(st.id,notes);},[notes]);const getSnapBeat=(beat,mode)=>{let q=0.25;if(mode==='free')return beat;if(mode==='1')q=4.0;else if(mode==='1/2')q=2.0;else if(mode==='1/4')q=1.0;else if(mode==='1/8')q=0.5;else if(mode==='1/16')q=0.25;else if(mode==='1/32')q=0.125;return Math.round(beat/q)*q;};const getSnapDuration=mode=>{if(mode==='free')return 0.25;if(mode==='1')return 4.0;if(mode==='1/2')return 2.0;if(mode==='1/4')return 1.0;if(mode==='1/8')return 0.5;if(mode==='1/16')return 0.25;if(mode==='1/32')return 0.125;return 0.25;};// Local Zoom Wheel Event handler to block browser page zoom React.useEffect(()=>{const handleWheelRaw=e=>{if(e.ctrlKey){e.preventDefault();const zoomFactor=e.deltaY<0?1.15:0.85;setRollZoom(prev=>Math.max(15,Math.min(250,prev*zoomFactor)));}};const container=gridScrollRef.current;if(container){container.addEventListener('wheel',handleWheelRaw,{passive:false});}return()=>{if(container){container.removeEventListener('wheel',handleWheelRaw);}};},[]);// Shift + Scroll event listener to adjust velocity React.useEffect(()=>{const handleCanvasWheel=e=>{if(e.shiftKey){e.preventDefault();const canvas=canvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const beat=x/pixelsPerBeat;const pitch=127-Math.floor(y/NoteHeight);// Find note under cursor const noteUnderCursor=notes.find(n=>{return pitch===n.pitch&&beat>=n.start_beat&&beatprev.map(n=>{if(n.id!==noteUnderCursor.id)return n;const newVel=Math.max(0.1,Math.min(1.0,(n.velocity??0.8)+delta));if(window.SonicSF){const ctx=getAudioContext();window.SonicSF.playNote(n.pitch,newVel*127,300,ctx.currentTime,st.instrumentProgram,null);}return{...n,velocity:newVel};}));}else if(selectedNoteIds.length>0){setNotes(prev=>prev.map(n=>{if(!selectedNoteIds.includes(n.id))return n;const newVel=Math.max(0.1,Math.min(1.0,(n.velocity??0.8)+delta));return{...n,velocity:newVel};}));}}};const canvas=canvasRef.current;if(canvas){canvas.addEventListener('wheel',handleCanvasWheel,{passive:false});}return()=>{if(canvas){canvas.removeEventListener('wheel',handleCanvasWheel);}};},[notes,selectedNoteIds,pixelsPerBeat]);React.useEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=128*NoteHeight;canvas.width=drawWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);// Draw background rows @@ -140,20 +140,21 @@ notes.forEach(note=>{const x=note.start_beat*pixelsPerBeat;const y=(127-note.pit ctx.fillStyle=isSelected?'rgba(59, 130, 246, 0.4)':'rgba(234, 179, 8, 0.25)';ctx.strokeStyle=isSelected?'#3b82f6':'#ca8a04';ctx.lineWidth=isSelected?1.5:1;ctx.fillRect(x+1,y+1,w-2,NoteHeight-2);ctx.strokeRect(x+1,y+1,w-2,NoteHeight-2);// Draw velocity layer (solid yellow/blue bar inside, proportional to velocity) const vel=note.velocity!==undefined?note.velocity:0.8;const velW=Math.max(2,(w-2)*vel);ctx.fillStyle=isSelected?'#3b82f6':'#eab308';ctx.fillRect(x+1,y+1,velW,NoteHeight-2);});// Draw selection marquee if active if(selectionMarquee){const minBeat=Math.min(selectionMarquee.startBeat,selectionMarquee.currentBeat);const maxBeat=Math.max(selectionMarquee.startBeat,selectionMarquee.currentBeat);const minPitch=Math.min(selectionMarquee.startPitch,selectionMarquee.currentPitch);const maxPitch=Math.max(selectionMarquee.startPitch,selectionMarquee.currentPitch);const mx=minBeat*pixelsPerBeat;const my=(127-maxPitch)*NoteHeight;const mw=(maxBeat-minBeat)*pixelsPerBeat;const mh=(maxPitch-minPitch+1)*NoteHeight;ctx.fillStyle='rgba(59, 130, 246, 0.15)';ctx.strokeStyle='#3b82f6';ctx.lineWidth=1;ctx.setLineDash([4,4]);ctx.fillRect(mx,my,mw,mh);ctx.strokeRect(mx,my,mw,mh);ctx.setLineDash([]);}// Draw playhead -if(st.currentTime!==undefined&&st.currentTime!==null&&st.currentTime>=0){const phBeat=st.currentTime/(60.0/(parseInt(bpm)||120));const phX=phBeat*pixelsPerBeat;if(phX>=0&&phX<=drawWidth){ctx.strokeStyle='#f59e0b';ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(phX,0);ctx.lineTo(phX,(128-PITCH_START)*NoteHeight);ctx.stroke();}}},[notes,snapVal,rollZoom,selectedNoteIds,selectionMarquee,st.currentTime,bpm]);React.useEffect(()=>{const canvas=ccCanvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=80;canvas.width=drawWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);ctx.fillStyle='#161616';ctx.fillRect(0,0,drawWidth,h);ctx.strokeStyle='#252525';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(0,h/2);ctx.lineTo(drawWidth,h/2);ctx.stroke();notes.forEach(note=>{const x=note.start_beat*pixelsPerBeat+note.duration_beats*pixelsPerBeat/2;let val=note.velocity!==undefined?note.velocity:0.8;if(ccMode==='pan'){val=(note.pan!==undefined?note.pan:0.0)*0.5+0.5;}const stemH=val*(h-20)+10;const y=h-stemH;ctx.strokeStyle=ccMode==='pan'?'#a78bfa':'#fbbf24';ctx.lineWidth=2.5;ctx.beginPath();ctx.moveTo(x,h);ctx.lineTo(x,y);ctx.stroke();ctx.fillStyle=ccMode==='pan'?'#c084fc':'#fbbf24';ctx.beginPath();ctx.arc(x,y,3.5,0,2*Math.PI);ctx.fill();});},[notes,ccMode,rollZoom]);React.useEffect(()=>{const scrollToC3=()=>{if(gridScrollRef.current){const ch=gridScrollRef.current.clientHeight||400;gridScrollRef.current.scrollTop=Math.max(0,(127-48)*NoteHeight+NoteHeight-ch);}};scrollToC3();const timer=setTimeout(scrollToC3,100);return()=>clearTimeout(timer);},[]);const handleGridMouseDown=e=>{const canvas=canvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const beat=x/pixelsPerBeat;const pitch=127-Math.floor(y/NoteHeight);// Right click -> Quick delete note! -if(e.button===2){e.preventDefault();const clickedNote=notes.find(n=>{return pitch===n.pitch&&beat>=n.start_beat&&beatprev.filter(n=>n.id!==clickedNote.id));setSelectedNoteIds(prev=>prev.filter(id=>id!==clickedNote.id));showToast('Đã xóa nốt nhanh!','info');}return;}if(e.button!==0)return;// Only handle left click +if(st.currentTime!==undefined&&st.currentTime!==null&&st.currentTime>=0){const phBeat=st.currentTime/(60.0/(parseInt(bpm)||120));const phX=phBeat*pixelsPerBeat;if(phX>=0&&phX<=drawWidth){ctx.strokeStyle='#f59e0b';ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(phX,0);ctx.lineTo(phX,(128-PITCH_START)*NoteHeight);ctx.stroke();}}},[notes,snapVal,rollZoom,selectedNoteIds,selectionMarquee,st.currentTime,bpm]);React.useEffect(()=>{const canvas=ccCanvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=80;canvas.width=drawWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);ctx.fillStyle='#161616';ctx.fillRect(0,0,drawWidth,h);ctx.strokeStyle='#252525';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(0,h/2);ctx.lineTo(drawWidth,h/2);ctx.stroke();notes.forEach(note=>{const x=note.start_beat*pixelsPerBeat+note.duration_beats*pixelsPerBeat/2;let val=note.velocity!==undefined?note.velocity:0.8;if(ccMode==='pan'){val=(note.pan!==undefined?note.pan:0.0)*0.5+0.5;}const stemH=val*(h-20)+10;const y=h-stemH;ctx.strokeStyle=ccMode==='pan'?'#a78bfa':'#fbbf24';ctx.lineWidth=2.5;ctx.beginPath();ctx.moveTo(x,h);ctx.lineTo(x,y);ctx.stroke();ctx.fillStyle=ccMode==='pan'?'#c084fc':'#fbbf24';ctx.beginPath();ctx.arc(x,y,3.5,0,2*Math.PI);ctx.fill();});},[notes,ccMode,rollZoom]);React.useEffect(()=>{const scrollToC3=()=>{if(gridScrollRef.current){const ch=gridScrollRef.current.clientHeight||400;gridScrollRef.current.scrollTop=Math.max(0,(127-48)*NoteHeight+NoteHeight-ch);}};scrollToC3();const timer=setTimeout(scrollToC3,100);return()=>clearTimeout(timer);},[]);const handleGridMouseDown=e=>{const canvas=canvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const beat=x/pixelsPerBeat;const pitch=127-Math.floor(y/NoteHeight);// Right click -> Quick delete note or start erase sweep +if(e.button===2){e.preventDefault();const clickedNote=notes.find(n=>{return pitch===n.pitch&&beat>=n.start_beat&&beatprev.filter(n=>n.id!==clickedNote.id));setSelectedNoteIds(prev=>prev.filter(id=>id!==clickedNote.id));showToast('Đã xóa nốt!','info');}// Start erase sweep +notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));setDraggedNote({mode:'erase_sweep',visitedPitches:[pitch]});return;}if(e.button!==0)return;// Only handle left click // Check if clicking on an existing note const clickedNoteIdx=notes.findIndex(n=>{return pitch===n.pitch&&beat>=n.start_beat&&beatidsToClone.includes(n.id)).map(n=>({...JSON.parse(JSON.stringify(n)),id:'note_'+Date.now()+Math.random().toString(36).substr(2,8)}));setNotes(prev=>[...prev,...clones]);const cloneIds=clones.map(c=>c.id);setSelectedNoteIds(cloneIds);notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));const cloneOffsets=clones.map(n=>({id:n.id,originalStartBeat:n.start_beat,originalPitch:n.pitch}));setDraggedNote({mode:'move',idx:-1,startOffsetBeat:beat,startOffsetPitch:pitch,selectedNotesOffset:cloneOffsets});}else{// Ctrl+click on empty space: start selection marquee -setSelectedNoteIds([]);setSelectionMarquee({startBeat:beat,startPitch:pitch,currentBeat:beat,currentPitch:pitch});}return;}// Hovered resize edge (Alt+resize for scaling) +if(clickedNoteIdx!==-1&&!e.ctrlKey&&!e.shiftKey&&!e.altKey){if(window.SonicSF){const ctx=getAudioContext();window.SonicSF.playNote(notes[clickedNoteIdx].pitch,100,300,ctx.currentTime,st.instrumentProgram,null);}}// Ctrl+click: toggle selection (multi-select) +if(e.ctrlKey&&!e.altKey&&!e.shiftKey){if(clickedNoteIdx!==-1){const clickedNote=notes[clickedNoteIdx];if(selectedNoteIds.includes(clickedNote.id)){setSelectedNoteIds(prev=>prev.filter(id=>id!==clickedNote.id));}else{setSelectedNoteIds(prev=>[...prev,clickedNote.id]);}return;}else{// Ctrl+click on empty space: start selection marquee +setSelectedNoteIds([]);setSelectionMarquee({startBeat:beat,startPitch:pitch,currentBeat:beat,currentPitch:pitch});return;}}// Ctrl+Shift+click: duplicate selected + clicked notes +if(e.ctrlKey&&e.shiftKey){if(clickedNoteIdx!==-1){pushToUndo(notes);const clickedNote=notes[clickedNoteIdx];const idsToClone=[...new Set([...selectedNoteIds,clickedNote.id])];const clones=notes.filter(n=>idsToClone.includes(n.id)).map(n=>({...JSON.parse(JSON.stringify(n)),id:'note_'+Date.now()+Math.random().toString(36).substr(2,8)}));setNotes(prev=>[...prev,...clones]);const cloneIds=clones.map(c=>c.id);setSelectedNoteIds(cloneIds);notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));const cloneOffsets=clones.map(n=>({id:n.id,originalStartBeat:n.start_beat,originalPitch:n.pitch}));setDraggedNote({mode:'move',idx:-1,startOffsetBeat:beat,startOffsetPitch:pitch,selectedNotesOffset:cloneOffsets});}return;}// Hovered resize edge (Alt+resize for scaling) if(hoveredResizeIdx!==-1&&e.altKey){pushToUndo(notes);notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));const allSelected=[...new Set(selectedNoteIds.length>0?selectedNoteIds:[notes[hoveredResizeIdx].id])];const selectedNotes=notes.filter(n=>allSelected.includes(n.id));const firstStart=Math.min(...selectedNotes.map(n=>n.start_beat));const draggedNote=notes[hoveredResizeIdx];setDraggedNote({mode:'scale',idx:hoveredResizeIdx,originalEnd:draggedNote.start_beat+draggedNote.duration_beats,firstStart:firstStart,selectedNoteIds:allSelected});return;}// Hovered resize edge (normal resize) if(hoveredResizeIdx!==-1){pushToUndo(notes);notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));setDraggedNote({mode:'resize',idx:hoveredResizeIdx,originalStart:notes[hoveredResizeIdx].start_beat});return;}if(clickedNoteIdx!==-1){// Click on existing note: drag-move const clickedNote=notes[clickedNoteIdx];let nextSelectedIds;if(!selectedNoteIds.includes(clickedNote.id)){if(e.shiftKey){nextSelectedIds=[...selectedNoteIds,clickedNote.id];setSelectedNoteIds(nextSelectedIds);}else{nextSelectedIds=[clickedNote.id];setSelectedNoteIds(nextSelectedIds);}}else{nextSelectedIds=selectedNoteIds;}pushToUndo(notes);notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));const selectedNotesOffset=notes.filter(n=>nextSelectedIds.includes(n.id)).map(n=>({id:n.id,originalStartBeat:n.start_beat,originalPitch:n.pitch}));setDraggedNote({mode:'move',idx:clickedNoteIdx,startOffsetBeat:beat-clickedNote.start_beat,startOffsetPitch:pitch-clickedNote.pitch,selectedNotesOffset:selectedNotesOffset});}else{// Click on empty space: DRAW a new note (brush mode with visitedPitches) pushToUndo(notes);const start=getSnapBeat(beat,snapVal);const initialDur=getSnapDuration(snapVal);const noteId='note_'+Date.now()+Math.random().toString(36).substr(2,5);const newNote={id:noteId,pitch:pitch,start_beat:start,duration_beats:initialDur,velocity:0.8,pan:0.0};setNotes(prev=>[...prev,newNote]);setSelectedNoteIds([noteId]);setDraggedNote({mode:'draw',idx:-1,startOffsetBeat:start,startOffsetPitch:pitch,drawNoteId:noteId,drawDuration:initialDur,visitedPitches:[pitch],initialBeat:start,initialPitch:pitch});// Play the note with SoundFont if(window.SonicSF){const ctx=getAudioContext();window.SonicSF.playNote(pitch,0.8,300,ctx.currentTime,undefined,null);}}};const handleGridMouseMove=e=>{const canvas=canvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const beat=x/pixelsPerBeat;const pitch=127-Math.floor(y/NoteHeight);if(selectionMarquee){const marquee={...selectionMarquee,currentBeat:beat,currentPitch:pitch};setSelectionMarquee(marquee);const minBeat=Math.min(marquee.startBeat,marquee.currentBeat);const maxBeat=Math.max(marquee.startBeat,marquee.currentBeat);const minPitch=Math.min(marquee.startPitch,marquee.currentPitch);const maxPitch=Math.max(marquee.startPitch,marquee.currentPitch);const insideIds=notes.filter(n=>n.start_beat>=minBeat&&n.start_beat<=maxBeat&&n.pitch>=minPitch&&n.pitch<=maxPitch).map(n=>n.id);setSelectedNoteIds(insideIds);return;}if(!draggedNote){let foundIdx=-1;for(let i=0;i=n.start_beat){foundIdx=i;break;}}}if(foundIdx!==-1){canvas.style.cursor='ew-resize';setHoveredResizeIdx(foundIdx);}else{canvas.style.cursor=activeRollTool==='eraser'?'pointer':'crosshair';setHoveredResizeIdx(-1);}return;}if(draggedNote.mode==='draw'){const rawDur=beat-draggedNote.startOffsetBeat;const newDur=getSnapBeat(Math.max(0.125,rawDur),snapVal);setNotes(prev=>prev.map(n=>{if(n.id!==draggedNote.drawNoteId)return n;return{...n,duration_beats:newDur};}));// Brush: track visited pitches and create evenly-spaced notes -const visited=draggedNote.visitedPitches||[];if(!visited.includes(pitch)){const newPitches=[...visited,pitch];const totalSpan=Math.max(0.125,beat-draggedNote.initialBeat);const perNoteDur=totalSpan/newPitches.length;const brushIds=draggedNote.brushIds||[];setNotes(prev=>{const cleaned=prev.filter(n=>!brushIds.includes(n.id));const brushNotes=newPitches.map((p,i)=>({id:'note_'+Date.now()+Math.random().toString(36).substr(2,8)+'_'+i,pitch:p,start_beat:draggedNote.initialBeat+i*perNoteDur,duration_beats:perNoteDur*0.9,velocity:0.8,pan:0.0}));const newBrushIds=brushNotes.map(bn=>bn.id);setSelectedNoteIds(newBrushIds);draggedNote.brushIds=newBrushIds;draggedNote.visitedPitches=newPitches;return[...cleaned,...brushNotes];});}return;}if(draggedNote.mode==='scale'){const newEnd=getSnapBeat(Math.max(draggedNote.firstStart+0.125,beat),snapVal);const scaleFactor=(newEnd-draggedNote.firstStart)/(draggedNote.originalEnd-draggedNote.firstStart);const ids=draggedNote.selectedNoteIds||[];const firstStart=draggedNote.firstStart;setNotes(prev=>prev.map(n=>{if(!ids.includes(n.id))return n;const relStart=n.start_beat-firstStart;const relEnd=relStart+n.duration_beats;return{...n,start_beat:firstStart+relStart*scaleFactor,duration_beats:Math.max(0.125,relEnd*scaleFactor-relStart*scaleFactor)};}));return;}if(draggedNote.mode==='resize'){const newDuration=getSnapBeat(Math.max(0.125,beat-draggedNote.originalStart),snapVal);setNotes(prev=>prev.map((n,idx)=>{if(idx!==draggedNote.idx)return n;return{...n,duration_beats:newDuration};}));}else if(draggedNote.mode==='move'){const baseOriginal=draggedNote.selectedNotesOffset.find(o=>o.id===notes[draggedNote.idx].id);const deltaBeat=getSnapBeat(beat-draggedNote.startOffsetBeat,snapVal)-baseOriginal.originalStartBeat;const deltaPitch=Math.round(pitch-draggedNote.startOffsetPitch)-baseOriginal.originalPitch;setNotes(prev=>prev.map(n=>{const offset=draggedNote.selectedNotesOffset.find(o=>o.id===n.id);if(!offset)return n;return{...n,start_beat:getSnapBeat(Math.max(0,offset.originalStartBeat+deltaBeat),snapVal),pitch:Math.max(0,Math.min(127,offset.originalPitch+deltaPitch))};}));}};const handleGridMouseUp=()=>{setDraggedNote(null);setSelectionMarquee(null);};const handleContextMenu=e=>{e.preventDefault();};const handleCCMouseDown=e=>{const canvas=ccCanvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat;let noteIdx=notes.findIndex(n=>beat>=n.start_beat&&beat<=n.start_beat+n.duration_beats);if(noteIdx===-1){let minDistance=Infinity;notes.forEach((n,idx)=>{const center=n.start_beat+n.duration_beats/2;const dist=Math.abs(center-beat);if(distprev.map((n,idx)=>{if(idx!==noteIdx)return n;if(ccMode==='pan'){return{...n,pan:(val-0.5)*2.0};}else{return{...n,velocity:val};}}));}};const renderKeybed=()=>{const keys=[];for(let pitch=127;pitch>=PITCH_START;pitch--){const isBlack=[1,3,6,8,10].includes(pitch%12);const notesArray=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;const label=`${notesArray[pitch%12]}${octave}`;const showLabel=pitch%12===0;keys.push(/*#__PURE__*/React.createElement("div",{key:pitch,style:{height:`${NoteHeight}px`},className:`w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition cursor-pointer ${isBlack?'bg-zinc-950 text-slate-500 border-zinc-900 hover:bg-zinc-800':'bg-white text-zinc-800 border-r border-zinc-400 hover:bg-amber-100'}`,onMouseDown:e=>{e.stopPropagation();try{if(window.SonicSF){window.SonicSF.playNote(pitch,100,500,undefined,st.instrumentProgram,null);}}catch(err){console.error('playNote error:',err);}}},showLabel&&label));}return keys;};const handleScroll=e=>{if(keybedRef.current){keybedRef.current.scrollTop=e.currentTarget.scrollTop;}if(rulerScrollRef.current){rulerScrollRef.current.scrollLeft=e.currentTarget.scrollLeft;}};const handleKeybedScroll=e=>{if(gridScrollRef.current){gridScrollRef.current.scrollTop=e.currentTarget.scrollTop;}};const renderBarLabels=()=>{const labels=[];const barsCount=Math.ceil(totalBeats/4);for(let bar=0;bar{e.stopPropagation();const barTime=bar*4*beatSec;setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:barTime}:s));}},`Bar ${bar}`));}return labels;};const beatSec=60.0/(parseInt(bpm)||120);const playHeadX=(st.currentTime||0)/beatSec*pixelsPerBeat;return React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col"},React.createElement("div",{className:"h-10 bg-[#282828] border-b border-zinc-900 flex items-center justify-between px-4 shrink-0 text-slate-200"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-4"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-yellow-500 uppercase flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"music",className:"w-3.5 h-3.5"}),st.label),/*#__PURE__*/React.createElement("div",{className:"flex bg-zinc-800 p-0.5 rounded border border-zinc-700 text-xs"},['select','pen','eraser'].map(tool=>/*#__PURE__*/React.createElement("button",{key:tool,onClick:()=>setActiveRollTool(tool),className:`px-2.5 py-1 rounded capitalize ${activeRollTool===tool?'bg-yellow-600 text-white font-bold':'text-zinc-400 hover:text-zinc-200'}`},tool))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 text-xs"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 font-semibold"},"Snap:"),/*#__PURE__*/React.createElement("select",{value:snapVal,onChange:e=>setSnapVal(e.target.value),className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"},['free','1','1/2','1/4','1/8','1/16','1/32'].map(v=>/*#__PURE__*/React.createElement("option",{key:v,value:v},v)))),/*#__PURE__*/React.createElement("div",{className:"flex bg-zinc-800 p-0.5 rounded border border-zinc-700 text-xs"},['velocity','pan'].map(mode=>/*#__PURE__*/React.createElement("button",{key:mode,onClick:()=>setCcMode(mode),className:`px-2.5 py-1 rounded capitalize ${ccMode===mode?'bg-purple-900/60 text-purple-300 font-bold border border-purple-700':'text-zinc-400 hover:text-zinc-200'}`},mode)))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>onSaveNotes(st.id,st.trackId,st.target_id,notes),className:"px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"},/*#__PURE__*/React.createElement("i",{"data-lucide":"save",className:"w-3 h-3"}),"Lưu"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs flex items-center gap-1 transition"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}),"Đóng"))),/*#__PURE__*/React.createElement("div",{className:"h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"},/*#__PURE__*/React.createElement("div",{className:"w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"}),/*#__PURE__*/React.createElement("div",{ref:rulerScrollRef,className:"flex-1 overflow-hidden",onMouseDown:e=>{const rect=e.currentTarget.getBoundingClientRect();const x=e.clientX-rect.left+e.currentTarget.scrollLeft;const clickBeat=x/pixelsPerBeat;const clickTime=clickBeat*beatSec;if(clickTime>=0){setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:clickTime}:s));}}},/*#__PURE__*/React.createElement("div",{style:{width:`${drawWidth}px`,height:'100%'},className:"relative h-full font-mono text-[9px] text-zinc-500 font-bold"},renderBarLabels()))),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0 relative"},/*#__PURE__*/React.createElement("div",{ref:keybedRef,onScroll:handleKeybedScroll,className:"w-[60px] overflow-y-auto flex flex-col border-r border-zinc-900 shrink-0",style:{scrollbarWidth:'none',msOverflowStyle:'none'}},renderKeybed()),/*#__PURE__*/React.createElement("div",{ref:gridScrollRef,onScroll:handleScroll,className:"flex-1 overflow-auto bg-[#141414] min-w-0"},/*#__PURE__*/React.createElement("div",{style:{width:`${drawWidth}px`,height:`${(128-PITCH_START)*NoteHeight}px`},className:"relative"},/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,onMouseDown:handleGridMouseDown,onMouseMove:handleGridMouseMove,onMouseUp:handleGridMouseUp,onMouseLeave:handleGridMouseUp,onContextMenu:handleContextMenu,className:"absolute inset-0 cursor-crosshair"})))),/*#__PURE__*/React.createElement("div",{className:"h-20 bg-[#161616] border-t border-zinc-900 flex shrink-0"},/*#__PURE__*/React.createElement("div",{className:"w-[60px] bg-[#222222] border-r border-zinc-900 flex items-center justify-center text-[10px] text-zinc-500 font-mono shrink-0 font-bold"},ccMode.toUpperCase()),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-x-hidden min-w-0"},/*#__PURE__*/React.createElement("div",{style:{width:`${drawWidth}px`,height:'100%'},className:"relative"},/*#__PURE__*/React.createElement("canvas",{ref:ccCanvasRef,onMouseDown:handleCCMouseDown,className:"absolute inset-0 cursor-ns-resize"})))));};const serializeTracksList=(tracksList,secondsPerBar)=>{return(tracksList||[]).map(t=>{let trackType="AUDIO";if(t.sections&&t.sections.length>0)trackType="SECTION";else if(t.midiItems&&t.midiItems.length>0)trackType="MIDI";const items=[];if(trackType==="AUDIO"&&t.clips){t.clips.forEach(c=>{const durationSec=c.buffer?c.buffer.duration:4.0;items.push({id:c.id,name:c.name||"Audio Clip",type:"AUDIO_ITEM",start_bar:c.startTime/secondsPerBar,duration_bars:durationSec/(c.speed||1.0)/secondsPerBar,clip_start_offset_bars:0.0,source_data:{audio_file_url:t.serverFileId?`/static/audio/uploads/${t.serverFileId}`:"",sample_rate:c.buffer?c.buffer.sampleRate:44100,channels:c.buffer?c.buffer.numberOfChannels:2,gain:1.0}});});}else if(trackType==="MIDI"&&t.midiItems){t.midiItems.forEach(m=>{items.push({id:m.id,name:m.name||"MIDI Item",type:"MIDI_ITEM",start_bar:m.startTime/secondsPerBar,duration_bars:m.duration?m.duration/secondsPerBar:4.0,clip_start_offset_bars:0.0,source_data:{total_buffer_bars:m.duration?m.duration/secondsPerBar:8.0,notes:(m.notes||[]).map(n=>({id:n.id||'note_'+Math.random().toString(36).substr(2,9),pitch:n.pitch||60,start_beat:n.start_beat||0.0,duration_beats:n.duration_beats||1.0,velocity:n.velocity||0.8,pan:n.pan||0.0}))}});});}else if(trackType==="SECTION"&&t.sections){t.sections.forEach(s=>{items.push({id:s.id,name:s.name||"Section Item",type:"SECTION_ITEM",start_bar:s.start/secondsPerBar,duration_bars:s.duration/secondsPerBar,clip_start_offset_bars:0.0,source_data:{referenced_section_id:s.sectionId||s.id}});});}return{id:t.id,name:t.name,type:trackType,volume_db:t.volumeDb||0.0,pan:t.pan||0.0,mute:t.muted||false,solo:t.solo||false,instrument_id:t.instrumentId||null,instrument_program:t.instrumentProgram!==undefined?t.instrumentProgram:null,instrument_name:t.instrumentName||null,items:items};});};const deserializeTracksList=(schemaTracks,secondsPerBar,sectionStore)=>{return(schemaTracks||[]).map(t=>{const clips=[];const sections=[];const midiItems=[];(t.items||[]).forEach(item=>{if(item.type==="AUDIO_ITEM"){const src=item.source_data||{};clips.push({id:item.id,name:item.name,startTime:item.start_bar*secondsPerBar,speed:1.0});}else if(item.type==="MIDI_ITEM"){const src=item.source_data||{};midiItems.push({id:item.id,name:item.name,startTime:item.start_bar*secondsPerBar,duration:item.duration_bars*secondsPerBar,notes:(src.notes||[]).map(n=>({id:n.id,pitch:n.pitch||60,start_beat:n.start_beat||0.0,duration_beats:n.duration_beats||1.0,velocity:n.velocity||0.8,pan:n.pan||0.0}))});}else if(item.type==="SECTION_ITEM"){const src=item.source_data||{};const secId=src.referenced_section_id;const secContainer=sectionStore?sectionStore[secId]:null;sections.push({id:item.id,name:item.name,start:item.start_bar*secondsPerBar,duration:item.duration_bars*secondsPerBar,sectionId:secId,tracks:secContainer?deserializeTracksList(secContainer.tracks,secondsPerBar,sectionStore):null});}});return{id:t.id,name:t.name,volumeDb:t.volume_db||0.0,pan:t.pan||0.0,muted:t.mute||false,solo:t.solo||false,color:t.id==='1'?'#0f766e':'#1d4ed8',markers:[],serverFileId:t.items&&t.items.find(i=>i.type==="AUDIO_ITEM")?.source_data?.audio_file_url?.split("/").pop()||null,clips:clips,sections:sections,midiItems:midiItems,instrumentId:t.instrument_id||null,instrumentProgram:t.instrument_program!==null?t.instrument_program:undefined,instrumentName:t.instrument_name||null};});};const serializeProjectToSchema=(projectId,name,bpmVal,tracksList,subTabsList,sessionTabsList)=>{const secondsPerBar=60.0/parseFloat(bpmVal||120)*4;const mainTracks=serializeTracksList(tracksList,secondsPerBar);const sectionStore={};// 1. Populate from sessionTabsList (open tabs) +const visited=draggedNote.visitedPitches||[];if(!visited.includes(pitch)){const newPitches=[...visited,pitch];const totalSpan=Math.max(0.125,beat-draggedNote.initialBeat);const perNoteDur=totalSpan/newPitches.length;const brushIds=draggedNote.brushIds||[];setNotes(prev=>{const cleaned=prev.filter(n=>!brushIds.includes(n.id));const brushNotes=newPitches.map((p,i)=>({id:'note_'+Date.now()+Math.random().toString(36).substr(2,8)+'_'+i,pitch:p,start_beat:draggedNote.initialBeat+i*perNoteDur,duration_beats:perNoteDur*0.9,velocity:0.8,pan:0.0}));const newBrushIds=brushNotes.map(bn=>bn.id);setSelectedNoteIds(newBrushIds);draggedNote.brushIds=newBrushIds;draggedNote.visitedPitches=newPitches;return[...cleaned,...brushNotes];});}return;}if(draggedNote.mode==='erase_sweep'){const erasePitches=draggedNote.visitedPitches||[];if(!erasePitches.includes(pitch)){draggedNote.visitedPitches=[...erasePitches,pitch];setNotes(prev=>prev.filter(n=>!(n.pitch===pitch&&beat>=n.start_beat&&beatprev.map(n=>{if(!ids.includes(n.id))return n;const relStart=n.start_beat-firstStart;const relEnd=relStart+n.duration_beats;return{...n,start_beat:firstStart+relStart*scaleFactor,duration_beats:Math.max(0.125,relEnd*scaleFactor-relStart*scaleFactor)};}));return;}if(draggedNote.mode==='resize'){const newDuration=getSnapBeat(Math.max(0.125,beat-draggedNote.originalStart),snapVal);setNotes(prev=>prev.map((n,idx)=>{if(idx!==draggedNote.idx)return n;return{...n,duration_beats:newDuration};}));}else if(draggedNote.mode==='move'){const firstOffset=draggedNote.selectedNotesOffset&&draggedNote.selectedNotesOffset[0];if(!firstOffset)return;const firstNote=notes.find(n=>n.id===firstOffset.id);if(!firstNote)return;const deltaBeat=getSnapBeat(beat-draggedNote.startOffsetBeat,snapVal)-firstOffset.originalStartBeat;const deltaPitch=Math.round(pitch-draggedNote.startOffsetPitch)-firstOffset.originalPitch;setNotes(prev=>prev.map(n=>{const offset=draggedNote.selectedNotesOffset.find(o=>o.id===n.id);if(!offset)return n;return{...n,start_beat:getSnapBeat(Math.max(0,offset.originalStartBeat+deltaBeat),snapVal),pitch:Math.max(0,Math.min(127,offset.originalPitch+deltaPitch))};}));}};const handleGridMouseUp=()=>{setDraggedNote(null);setSelectionMarquee(null);};const handleContextMenu=e=>{e.preventDefault();};const handleCCMouseDown=e=>{const canvas=ccCanvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat;let noteIdx=notes.findIndex(n=>beat>=n.start_beat&&beat<=n.start_beat+n.duration_beats);if(noteIdx===-1){let minDistance=Infinity;notes.forEach((n,idx)=>{const center=n.start_beat+n.duration_beats/2;const dist=Math.abs(center-beat);if(distprev.map((n,idx)=>{if(idx!==noteIdx)return n;if(ccMode==='pan'){return{...n,pan:(val-0.5)*2.0};}else{return{...n,velocity:val};}}));}};const renderKeybed=()=>{const keys=[];for(let pitch=127;pitch>=PITCH_START;pitch--){const isBlack=[1,3,6,8,10].includes(pitch%12);const notesArray=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;const label=`${notesArray[pitch%12]}${octave}`;const showLabel=pitch%12===0;keys.push(/*#__PURE__*/React.createElement("div",{key:pitch,style:{height:`${NoteHeight}px`},className:`w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition cursor-pointer ${isBlack?'bg-zinc-950 text-slate-500 border-zinc-900 hover:bg-zinc-800':'bg-white text-zinc-800 border-r border-zinc-400 hover:bg-amber-100'}`,onMouseDown:e=>{e.stopPropagation();try{if(window.SonicSF){window.SonicSF.playNote(pitch,100,500,undefined,st.instrumentProgram,null);}}catch(err){console.error('playNote error:',err);}}},showLabel&&label));}return keys;};const handleScroll=e=>{if(keybedRef.current){keybedRef.current.scrollTop=e.currentTarget.scrollTop;}if(rulerScrollRef.current){rulerScrollRef.current.scrollLeft=e.currentTarget.scrollLeft;}};const handleKeybedScroll=e=>{if(gridScrollRef.current){gridScrollRef.current.scrollTop=e.currentTarget.scrollTop;}};const renderBarLabels=()=>{const labels=[];const barsCount=Math.ceil(totalBeats/4);for(let bar=0;bar{e.stopPropagation();const barTime=bar*4*beatSec;setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:barTime}:s));}},`Bar ${bar}`));}return labels;};const beatSec=60.0/(parseInt(bpm)||120);const playHeadX=(st.currentTime||0)/beatSec*pixelsPerBeat;return React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col"},React.createElement("div",{className:"h-10 bg-[#282828] border-b border-zinc-900 flex items-center justify-between px-4 shrink-0 text-slate-200"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-4"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-yellow-500 uppercase flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"music",className:"w-3.5 h-3.5"}),st.label),/*#__PURE__*/React.createElement("div",{className:"flex bg-zinc-800 p-0.5 rounded border border-zinc-700 text-xs"},['select','pen','eraser'].map(tool=>/*#__PURE__*/React.createElement("button",{key:tool,onClick:()=>setActiveRollTool(tool),className:`px-2.5 py-1 rounded capitalize ${activeRollTool===tool?'bg-yellow-600 text-white font-bold':'text-zinc-400 hover:text-zinc-200'}`},tool))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 text-xs"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 font-semibold"},"Snap:"),/*#__PURE__*/React.createElement("select",{value:snapVal,onChange:e=>setSnapVal(e.target.value),className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"},['free','1','1/2','1/4','1/8','1/16','1/32'].map(v=>/*#__PURE__*/React.createElement("option",{key:v,value:v},v)))),/*#__PURE__*/React.createElement("div",{className:"flex bg-zinc-800 p-0.5 rounded border border-zinc-700 text-xs"},['velocity','pan'].map(mode=>/*#__PURE__*/React.createElement("button",{key:mode,onClick:()=>setCcMode(mode),className:`px-2.5 py-1 rounded capitalize ${ccMode===mode?'bg-purple-900/60 text-purple-300 font-bold border border-purple-700':'text-zinc-400 hover:text-zinc-200'}`},mode)))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>onSaveNotes(st.id,st.trackId,st.target_id,notes),className:"px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"},/*#__PURE__*/React.createElement("i",{"data-lucide":"save",className:"w-3 h-3"}),"Lưu"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs flex items-center gap-1 transition"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}),"Đóng"))),/*#__PURE__*/React.createElement("div",{className:"h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"},/*#__PURE__*/React.createElement("div",{className:"w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"}),/*#__PURE__*/React.createElement("div",{ref:rulerScrollRef,className:"flex-1 overflow-hidden",onMouseDown:e=>{const rect=e.currentTarget.getBoundingClientRect();const x=e.clientX-rect.left+e.currentTarget.scrollLeft;const clickBeat=x/pixelsPerBeat;const clickTime=clickBeat*beatSec;if(clickTime>=0){setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:clickTime}:s));}}},/*#__PURE__*/React.createElement("div",{style:{width:`${drawWidth}px`,height:'100%'},className:"relative h-full font-mono text-[9px] text-zinc-500 font-bold"},renderBarLabels()))),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0 relative"},/*#__PURE__*/React.createElement("div",{ref:keybedRef,onScroll:handleKeybedScroll,className:"w-[60px] overflow-y-auto flex flex-col border-r border-zinc-900 shrink-0",style:{scrollbarWidth:'none',msOverflowStyle:'none'}},renderKeybed()),/*#__PURE__*/React.createElement("div",{ref:gridScrollRef,onScroll:handleScroll,className:"flex-1 overflow-auto bg-[#141414] min-w-0"},/*#__PURE__*/React.createElement("div",{style:{width:`${drawWidth}px`,height:`${(128-PITCH_START)*NoteHeight}px`},className:"relative"},/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,onMouseDown:handleGridMouseDown,onMouseMove:handleGridMouseMove,onMouseUp:handleGridMouseUp,onMouseLeave:handleGridMouseUp,onContextMenu:handleContextMenu,className:"absolute inset-0 cursor-crosshair"})))),/*#__PURE__*/React.createElement("div",{className:"h-20 bg-[#161616] border-t border-zinc-900 flex shrink-0"},/*#__PURE__*/React.createElement("div",{className:"w-[60px] bg-[#222222] border-r border-zinc-900 flex items-center justify-center text-[10px] text-zinc-500 font-mono shrink-0 font-bold"},ccMode.toUpperCase()),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-x-hidden min-w-0"},/*#__PURE__*/React.createElement("div",{style:{width:`${drawWidth}px`,height:'100%'},className:"relative"},/*#__PURE__*/React.createElement("canvas",{ref:ccCanvasRef,onMouseDown:handleCCMouseDown,className:"absolute inset-0 cursor-ns-resize"})))));};const serializeTracksList=(tracksList,secondsPerBar)=>{return(tracksList||[]).map(t=>{let trackType="AUDIO";if(t.sections&&t.sections.length>0)trackType="SECTION";else if(t.midiItems&&t.midiItems.length>0)trackType="MIDI";const items=[];if(trackType==="AUDIO"&&t.clips){t.clips.forEach(c=>{const durationSec=c.buffer?c.buffer.duration:4.0;items.push({id:c.id,name:c.name||"Audio Clip",type:"AUDIO_ITEM",start_bar:c.startTime/secondsPerBar,duration_bars:durationSec/(c.speed||1.0)/secondsPerBar,clip_start_offset_bars:0.0,source_data:{audio_file_url:t.serverFileId?`/static/audio/uploads/${t.serverFileId}`:"",sample_rate:c.buffer?c.buffer.sampleRate:44100,channels:c.buffer?c.buffer.numberOfChannels:2,gain:1.0}});});}else if(trackType==="MIDI"&&t.midiItems){t.midiItems.forEach(m=>{items.push({id:m.id,name:m.name||"MIDI Item",type:"MIDI_ITEM",start_bar:m.startTime/secondsPerBar,duration_bars:m.duration?m.duration/secondsPerBar:4.0,clip_start_offset_bars:0.0,source_data:{total_buffer_bars:m.duration?m.duration/secondsPerBar:8.0,notes:(m.notes||[]).map(n=>({id:n.id||'note_'+Math.random().toString(36).substr(2,9),pitch:n.pitch||60,start_beat:n.start_beat||0.0,duration_beats:n.duration_beats||1.0,velocity:n.velocity||0.8,pan:n.pan||0.0}))}});});}else if(trackType==="SECTION"&&t.sections){t.sections.forEach(s=>{items.push({id:s.id,name:s.name||"Section Item",type:"SECTION_ITEM",start_bar:s.start/secondsPerBar,duration_bars:s.duration/secondsPerBar,clip_start_offset_bars:0.0,source_data:{referenced_section_id:s.sectionId||s.id}});});}return{id:t.id,name:t.name,type:trackType,volume_db:t.volumeDb||0.0,pan:t.pan||0.0,mute:t.muted||false,solo:t.solo||false,instrument_id:t.instrumentId||null,instrument_program:t.instrumentProgram!==undefined?t.instrumentProgram:null,instrument_name:t.instrumentName||null,items:items};});};const deserializeTracksList=(schemaTracks,secondsPerBar,sectionStore)=>{return(schemaTracks||[]).map(t=>{const clips=[];const sections=[];const midiItems=[];(t.items||[]).forEach(item=>{if(item.type==="AUDIO_ITEM"){const src=item.source_data||{};clips.push({id:item.id,name:item.name,startTime:item.start_bar*secondsPerBar,speed:1.0});}else if(item.type==="MIDI_ITEM"){const src=item.source_data||{};midiItems.push({id:item.id,name:item.name,startTime:item.start_bar*secondsPerBar,duration:item.duration_bars*secondsPerBar,notes:(src.notes||[]).map(n=>({id:n.id,pitch:n.pitch||60,start_beat:n.start_beat||0.0,duration_beats:n.duration_beats||1.0,velocity:n.velocity||0.8,pan:n.pan||0.0}))});}else if(item.type==="SECTION_ITEM"){const src=item.source_data||{};const secId=src.referenced_section_id;const secContainer=sectionStore?sectionStore[secId]:null;sections.push({id:item.id,name:item.name,start:item.start_bar*secondsPerBar,duration:item.duration_bars*secondsPerBar,sectionId:secId,tracks:secContainer?deserializeTracksList(secContainer.tracks,secondsPerBar,sectionStore):null});}});return{id:t.id,name:t.name,volumeDb:t.volume_db||0.0,pan:t.pan||0.0,muted:t.mute||false,solo:t.solo||false,color:t.id==='1'?'#0f766e':'#1d4ed8',markers:[],serverFileId:t.items&&t.items.find(i=>i.type==="AUDIO_ITEM")?.source_data?.audio_file_url?.split("/").pop()||null,clips:clips,sections:sections,midiItems:midiItems,instrumentId:t.instrument_id||null,instrumentProgram:t.instrument_program!==null?t.instrument_program:undefined,instrumentName:t.instrument_name||null};});};const serializeProjectToSchema=(projectId,name,bpmVal,tracksList,subTabsList,sessionTabsList)=>{const secondsPerBar=60.0/parseFloat(bpmVal||120)*4;const mainTracks=serializeTracksList(tracksList,secondsPerBar);const sectionStore={};// 1. Populate from sessionTabsList (open tabs) (sessionTabsList||[]).forEach(st=>{sectionStore[st.sectionId]={id:st.sectionId,name:st.name,is_root:false,length_bars:st.length_bars||16.0,auto_compute_length:st.auto_compute_length!==undefined?st.auto_compute_length:true,tracks:serializeTracksList(st.tracks,secondsPerBar),color:st.color||null};});// 2. Also populate from tracksList (closed tabs saved inside Section items) const scanForSections=tracks=>{(tracks||[]).forEach(t=>{if(t.sections){t.sections.forEach(s=>{const secId=s.sectionId||s.id;if(s.tracks&&!sectionStore[secId]){sectionStore[secId]={id:secId,name:s.name,is_root:false,length_bars:s.tracks[0]?.length_bars||16.0,auto_compute_length:true,tracks:serializeTracksList(s.tracks,secondsPerBar),color:s.color||null};}if(s.tracks){scanForSections(s.tracks);}});}});};scanForSections(tracksList);const subTabs=(subTabsList||[]).map(st=>{return{id:st.id,label:st.label,type:st.type,track_id:st.trackId,target_id:st.target_id,parent_tab_id:st.parent_tab_id,notes:st.notes||[],duration:st.duration||4,instrument_program:st.instrumentProgram,instrument_name:st.instrumentName,current_time:st.currentTime||0,color:st.color||null};});return{project_id:projectId||'proj_'+Date.now(),metadata:{title:name||"Dự án mới",bpm:parseFloat(bpmVal||120),time_signature_numerator:4,time_signature_denominator:4,sample_rate:44100},main_session:{id:"main",name:"MAIN SESSION",is_root:true,length_bars:16.0,auto_compute_length:true,tracks:mainTracks},sub_tabs:subTabs,section_store:sectionStore};};const deserializeProjectFromSchema=schemaObj=>{const bpmVal=schemaObj.metadata?schemaObj.metadata.bpm:120;const secondsPerBar=60.0/bpmVal*4;const sectionStore=schemaObj.section_store||{};const restoredTracks=deserializeTracksList(schemaObj.main_session.tracks,secondsPerBar,sectionStore);const restoredSessionTabs=[];Object.keys(sectionStore).forEach(secId=>{const secContainer=sectionStore[secId];const secTracks=deserializeTracksList(secContainer.tracks,secondsPerBar,sectionStore);restoredSessionTabs.push({id:'session_'+secId,name:secContainer.name,sectionId:secId,tracks:secTracks,length_bars:secContainer.length_bars||16.0,auto_compute_length:secContainer.auto_compute_length!==undefined?secContainer.auto_compute_length:true,color:secContainer.color||null});});const restoredSubTabs=(schemaObj.sub_tabs||[]).map(st=>{return{id:st.id,label:st.label,type:st.type,trackId:st.track_id,target_id:st.target_id,parent_tab_id:st.parent_tab_id,notes:st.notes||[],duration:st.duration||4,instrumentProgram:st.instrument_program,instrumentName:st.instrument_name,currentTime:st.current_time||0,color:st.color||null};});return{bpm:bpmVal,tracks:restoredTracks,sessionTabs:restoredSessionTabs,subTabs:restoredSubTabs};};const App=()=>{// ── State Definitions ── const[tracks,setTracks]=useState([{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:[],isArmed:false,monitoringEnabled:true,inputSource:{deviceType:'NONE',deviceId:''}},{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:[],isArmed:false,monitoringEnabled:true,inputSource:{deviceType:'NONE',deviceId:''}}]);const[appWarningModal,setAppWarningModal]=useState(null);const[bpm,setBpm]=useState(localStorage.getItem('studio_bpm')||'120');const[draggedClip,setDraggedClip]=useState(null);// { trackId, clickOffset, buffer, name, volume, color } diff --git a/app/storage/sonicforge.db b/app/storage/sonicforge.db index 47fd71f..7154ec7 100644 Binary files a/app/storage/sonicforge.db and b/app/storage/sonicforge.db differ diff --git a/app/storage/soundfonts/49d84fdb-ba20-4e7b-b03e-a9e07303df24.meta b/app/storage/soundfonts/49d84fdb-ba20-4e7b-b03e-a9e07303df24.meta new file mode 100644 index 0000000..9ddfeda --- /dev/null +++ b/app/storage/soundfonts/49d84fdb-ba20-4e7b-b03e-a9e07303df24.meta @@ -0,0 +1 @@ +{"original_name": "test.sf2", "uuid": "49d84fdb-ba20-4e7b-b03e-a9e07303df24", "file": "49d84fdb-ba20-4e7b-b03e-a9e07303df24.sf2"} \ No newline at end of file diff --git a/app/storage/soundfonts/49d84fdb-ba20-4e7b-b03e-a9e07303df24.sf2 b/app/storage/soundfonts/49d84fdb-ba20-4e7b-b03e-a9e07303df24.sf2 new file mode 100644 index 0000000..4693904 Binary files /dev/null and b/app/storage/soundfonts/49d84fdb-ba20-4e7b-b03e-a9e07303df24.sf2 differ