diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index b3d0fa1..1f46991 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -5052,6 +5052,27 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos return () => clearTimeout(timer); }, []); + // Sync ghost play data to subTab state for playback integration + React.useEffect(function() { + if (!sessionSyncMode || !showGhostNotes || !ghostLayers.length) { + setSubTabs(function(prev) { return prev.map(function(s) { if (s.id !== st.id) return s; return Object.assign({}, s, { ghostPlayLayers: [] }); }); }); + return; + } + var layers = []; + ghostLayers.forEach(function(layer) { + if (activePlayTrackIds !== null && !activePlayTrackIds.has(layer.track_id)) return; + var trk = (activeTracks || []).find(function(t) { return t.id === layer.track_id; }); + layers.push({ + trackId: layer.track_id, + notes: layer.notes.map(function(n) { return { pitch: n.pitch, start_beat: n.relative_start_beat, duration_beats: n.duration_beats, velocity: n.velocity || 0.8 }; }), + instrumentProgram: trk ? trk.instrumentProgram : undefined, + instrumentName: trk ? trk.instrumentName : undefined, + synthEngine: trk ? trk.synth_engine : undefined + }); + }); + setSubTabs(function(prev) { return prev.map(function(s) { if (s.id !== st.id) return s; return Object.assign({}, s, { ghostPlayLayers: layers }); }); }); + }, [ghostLayers, activePlayTrackIds, sessionSyncMode, showGhostNotes, st.id, activeTracks]); + const handleGridMouseDown = (e) => { const canvas = canvasRef.current; if (!canvas) return; @@ -6110,7 +6131,7 @@ const beatSec = 60.0 / (parseInt(bpm) || 120); className: "absolute left-0 top-0 w-[120px] bg-[#16161a] border-r border-zinc-800 z-10 flex flex-col overflow-hidden pointer-events-none", style: { height: KeybedPixelHeight + 'px' } }, React.createElement("div", { - className: "pointer-events-auto" + className: "pointer-events-auto flex flex-col" }, (allMidiItems.length > 0 ? function() { var seenTracks = {}; var els = []; @@ -6131,9 +6152,9 @@ const beatSec = 60.0 / (parseInt(bpm) || 120); return s.size === 0 ? null : s; }); }, - className: "flex items-center px-1.5 h-[18px] border-b border-zinc-900 cursor-pointer " + (isActive ? 'bg-yellow-900/20' : (isPlayOn ? 'bg-zinc-800/40' : 'bg-zinc-900/60')) + className: "flex items-center justify-center h-[18px] border-b border-zinc-900 cursor-pointer select-none " + (isActive ? 'bg-yellow-600 text-black font-bold' : (isPlayOn ? 'bg-purple-700 text-white font-semibold' : 'bg-zinc-800 text-zinc-500 hover:bg-zinc-700')) }, React.createElement("span", { - className: "text-[9px] font-mono truncate " + (isActive ? 'text-yellow-400 font-bold' : (isPlayOn ? 'text-zinc-300' : 'text-zinc-600')), + className: "text-[12px] font-sans truncate px-1", title: track ? track.name : m._trackName }, track ? track.name : m._trackName))); }); @@ -6765,7 +6786,11 @@ const App = () => { // Trigger SpessaSynth load + program change when soundfont instrument selected if (window.SonicSF && window.SonicSF.selectInstrument && instrumentId && isSfInstrument) { const sfId = instrumentId.replace('sf_', ''); - const ch = sfBank === 128 ? 9 : 0; + var allTracks = activeTracksRef.current || tracks; + var tidx = 0; + for (var i = 0; i < allTracks.length; i++) { if (allTracks[i].id === trackId) { tidx = i; break; } } + var ch = sfBank === 128 ? 9 : (tidx >= 9 ? tidx + 1 : tidx); + if (ch >= 16) ch = 0; window.SonicSF.selectInstrument(ch, sfBank || 0, sfProg || 0, sfId); } setSubTabs(prev => prev.map(s => { @@ -10372,6 +10397,9 @@ const App = () => { const destNode = getOrCreateTrackNode(track, context); const instrumentProgram = track ? track.instrumentProgram : undefined; const synthEngine = track ? track.synth_engine : undefined; + var allTracks = activeTracksRef.current || []; + var mainCh = 0; + for (var i = 0; i < allTracks.length; i++) { if (allTracks[i].id === st.trackId) { mainCh = i >= 9 ? i + 1 : i; if (mainCh >= 16) mainCh = 0; break; } } midiNotes.forEach(note => { const noteOnBeat = note.start_beat || 0; const noteDurBeat = note.duration_beats || 1; @@ -10383,10 +10411,35 @@ const App = () => { const scheduledTime = startWallTime + effectiveStart; const durMs = effectiveDur * 1000; if (window.SonicSF) { - window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, undefined, synthEngine); + window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, scheduledTime, instrumentProgram, destNode, mainCh, synthEngine); } } }); + // Play ghost notes from active tracks + var ghostLayers = st.ghostPlayLayers || []; + ghostLayers.forEach(function(layer) { + var ghostTrack = allTracks.find(function(t) { return t.id === layer.trackId; }); + var ghostProg = layer.instrumentProgram !== undefined ? layer.instrumentProgram : (ghostTrack ? ghostTrack.instrumentProgram : undefined); + var ghostSynth = layer.synthEngine || (ghostTrack ? ghostTrack.synth_engine : undefined); + var ghostDest = getOrCreateTrackNode(ghostTrack, context); + var ghostCh = 0; + for (var i = 0; i < allTracks.length; i++) { if (allTracks[i].id === layer.trackId) { ghostCh = i >= 9 ? i + 1 : i; if (ghostCh >= 16) ghostCh = 0; break; } } + layer.notes.forEach(function(note) { + var beat = note.start_beat || 0; + var dur = note.duration_beats || 1; + var startSec = beat * secondsPerBeat; + var durSec = dur * secondsPerBeat; + if (startSec + durSec > offsetSeconds) { + var effStart = Math.max(0, startSec - offsetSeconds); + var effDur = durSec - Math.max(0, offsetSeconds - startSec); + var schedTime = startWallTime + effStart; + var durMs = effDur * 1000; + if (window.SonicSF) { + window.SonicSF.playNote(note.pitch || 60, note.velocity || 0.8, durMs, schedTime, ghostProg, ghostDest, ghostCh, ghostSynth); + } + } + }); + }); }; const handlePlayPause = () => { if (activeTab !== 'main' && !activeTab.startsWith('session_')) { diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 4f0713d..13fe238 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -146,7 +146,8 @@ ctx.fillStyle=isSelected?'rgba(59, 130, 246, 0.4)':'rgba(234, 179, 8, 0.25)';ctx 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 real-time recording notes if(recordingState==='RECORDING'&&recTempMidiNotes&&recTempMidiNotes.length>0){recTempMidiNotes.forEach(note=>{const x=(renderBeatOffset+note.start_beat)*pixelsPerBeat;const y=(127-note.pitch)*NoteHeight;const w=(note.duration_beats||0.25)*pixelsPerBeat;ctx.fillStyle='rgba(255, 100, 100, 0.35)';ctx.strokeStyle='#ff6464';ctx.lineWidth=1;ctx.fillRect(x+1,y+1,Math.max(2,w-2),NoteHeight-2);ctx.strokeRect(x+1,y+1,Math.max(2,w-2),NoteHeight-2);const vel=Math.min(1,note.velocity||0.8);ctx.fillStyle='#ff6464';ctx.fillRect(x+1,y+1,Math.max(2,(w-2)*vel),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=renderBeatOffset+st.currentTime/(60.0/(parseInt(bpm)||120));const phX=phBeat*pixelsPerBeat;if(phX>=0&&phX<=viewWidth){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,viewWidth,viewBeats,recordingState,recTempMidiNotes,showGhostNotes,sessionSyncMode,ghostLayers,renderBeatOffset]);React.useLayoutEffect(()=>{const canvas=ccCanvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=ccHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);ctx.fillStyle='#161616';ctx.fillRect(0,0,viewWidth,h);ctx.strokeStyle='#252525';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(0,h/2);ctx.lineTo(viewWidth,h/2);ctx.stroke();notes.forEach(note=>{const x=(renderBeatOffset+note.start_beat)*pixelsPerBeat;const isSelected=selectedNoteIds.includes(note.id);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'?isSelected?'#60a5fa':'#a78bfa':isSelected?'#3b82f6':'#fbbf24';ctx.lineWidth=2.5;ctx.beginPath();ctx.moveTo(x,h);ctx.lineTo(x,y);ctx.stroke();ctx.fillStyle=ccMode==='pan'?isSelected?'#3b82f6':'#c084fc':isSelected?'#3b82f6':'#fbbf24';ctx.beginPath();ctx.arc(x,y,3.5,0,2*Math.PI);ctx.fill();});},[notes,ccMode,rollZoom,viewWidth,selectedNoteIds,renderBeatOffset]);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);if(scaleMenuPos)setScaleMenuPos(null);// Right click -> delete note (if on note) or prepare for sweep-drag +if(st.currentTime!==undefined&&st.currentTime!==null&&st.currentTime>=0){const phBeat=renderBeatOffset+st.currentTime/(60.0/(parseInt(bpm)||120));const phX=phBeat*pixelsPerBeat;if(phX>=0&&phX<=viewWidth){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,viewWidth,viewBeats,recordingState,recTempMidiNotes,showGhostNotes,sessionSyncMode,ghostLayers,renderBeatOffset]);React.useLayoutEffect(()=>{const canvas=ccCanvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=ccHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);ctx.fillStyle='#161616';ctx.fillRect(0,0,viewWidth,h);ctx.strokeStyle='#252525';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(0,h/2);ctx.lineTo(viewWidth,h/2);ctx.stroke();notes.forEach(note=>{const x=(renderBeatOffset+note.start_beat)*pixelsPerBeat;const isSelected=selectedNoteIds.includes(note.id);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'?isSelected?'#60a5fa':'#a78bfa':isSelected?'#3b82f6':'#fbbf24';ctx.lineWidth=2.5;ctx.beginPath();ctx.moveTo(x,h);ctx.lineTo(x,y);ctx.stroke();ctx.fillStyle=ccMode==='pan'?isSelected?'#3b82f6':'#c084fc':isSelected?'#3b82f6':'#fbbf24';ctx.beginPath();ctx.arc(x,y,3.5,0,2*Math.PI);ctx.fill();});},[notes,ccMode,rollZoom,viewWidth,selectedNoteIds,renderBeatOffset]);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);},[]);// Sync ghost play data to subTab state for playback integration +React.useEffect(function(){if(!sessionSyncMode||!showGhostNotes||!ghostLayers.length){setSubTabs(function(prev){return prev.map(function(s){if(s.id!==st.id)return s;return Object.assign({},s,{ghostPlayLayers:[]});});});return;}var layers=[];ghostLayers.forEach(function(layer){if(activePlayTrackIds!==null&&!activePlayTrackIds.has(layer.track_id))return;var trk=(activeTracks||[]).find(function(t){return t.id===layer.track_id;});layers.push({trackId:layer.track_id,notes:layer.notes.map(function(n){return{pitch:n.pitch,start_beat:n.relative_start_beat,duration_beats:n.duration_beats,velocity:n.velocity||0.8};}),instrumentProgram:trk?trk.instrumentProgram:undefined,instrumentName:trk?trk.instrumentName:undefined,synthEngine:trk?trk.synth_engine:undefined});});setSubTabs(function(prev){return prev.map(function(s){if(s.id!==st.id)return s;return Object.assign({},s,{ghostPlayLayers:layers});});});},[ghostLayers,activePlayTrackIds,sessionSyncMode,showGhostNotes,st.id,activeTracks]);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);if(scaleMenuPos)setScaleMenuPos(null);// Right click -> delete note (if on note) or prepare for sweep-drag 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));swallowContextMenuRef.current=true;showToast('Đã xóa nốt!','info');}else{rightClickDragRef.current={active:true,startX:e.clientX,startY:e.clientY};}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&&beat=minBeat;}else{// Right to left: select only if fully covered return n.start_beat>=minBeat&¬eEnd<=maxBeat;}}).map(n=>n.id);setSelectedNoteIds(insideIds);return;}// Right-click drag → erase sweep const rc=rightClickDragRef.current;if(rc.active&&(Math.abs(e.clientX-rc.startX)>5||Math.abs(e.clientY-rc.startY)>5)){rc.active=false;swallowContextMenuRef.current=true;notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));setDraggedNote({mode:'erase_sweep',visitedPitches:[]});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 snappedPitch=snapToScaleRef.current?snapPitchToScale(pitch,selectedScaleRef.current):pitch;const lastPitch=draggedNote.lastDrawnPitch!==undefined?draggedNote.lastDrawnPitch:draggedNote.startOffsetPitch;const pitchChanged=snappedPitch!==lastPitch;const noteBeats=draggedNote.noteStartBeats||[];const defaultDur=getSnapDuration(snapVal);if(pitchChanged){const brushIds=draggedNote.brushIds||[];if(brushIds.length>0&¬eBeats.length>0){const prevNoteId=brushIds[brushIds.length-1];const prevNoteBeat=noteBeats[noteBeats.length-1];const prevDur=Math.max(0.125,beat-prevNoteBeat);setNotes(prev=>prev.map(n=>{if(n.id!==prevNoteId)return n;return{...n,duration_beats:prevDur};}));}const newNote={id:'note_'+Date.now()+Math.random().toString(36).substr(2,8)+'_'+(noteBeats.length+1),pitch:snappedPitch,start_beat:beat,duration_beats:defaultDur,velocity:brushVelocityRef.current,pan:0.0};setNotes(prev=>[...prev,newNote]);setSelectedNoteIds(prev=>[...prev,newNote.id]);draggedNote.brushIds=[...brushIds,newNote.id];draggedNote.lastDrawnPitch=snappedPitch;draggedNote.noteStartBeats=[...noteBeats,beat];}else{const brushIds=draggedNote.brushIds||[];const lastBrushId=brushIds.length>0?brushIds[brushIds.length-1]:draggedNote.drawNoteId;const lastNoteBeat=noteBeats.length>0?noteBeats[noteBeats.length-1]:draggedNote.startOffsetBeat;if(lastBrushId){const extDur=Math.max(0.125,beat-lastNoteBeat);setNotes(prev=>prev.map(n=>{if(n.id!==lastBrushId)return n;return{...n,duration_beats:extDur};}));}}const container=gridScrollRef.current;if(container){const cr=container.getBoundingClientRect();const visTop=container.scrollTop;const visBot=visTop+container.clientHeight;const pitchPixel=(127-snappedPitch)*NoteHeight;const safeMargin=NoteHeight*2;if(pitchPixel{if(gridScrollRef.current)gridScrollRef.current.scrollTop=Math.max(0,gridScrollRef.current.scrollTop-Math.max(1,Math.floor(NoteHeight*0.5)));},16)};}}else if(pitchPixel+NoteHeight>visBot-safeMargin){const target=Math.min(container.scrollHeight-container.clientHeight,pitchPixel-container.clientHeight+safeMargin+NoteHeight);if(container.scrollTop!==target)container.scrollTop=target;if(!brushAutoScrollRef.current||brushAutoScrollRef.current.direction!=='down'){if(brushAutoScrollRef.current)clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current={direction:'down',id:setInterval(()=>{if(gridScrollRef.current)gridScrollRef.current.scrollTop=Math.min(gridScrollRef.current.scrollHeight-gridScrollRef.current.clientHeight,gridScrollRef.current.scrollTop+Math.max(1,Math.floor(NoteHeight*0.5)));},16)};}}else{if(brushAutoScrollRef.current){clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current=null;}}}return;}if(draggedNote.mode==='erase_sweep'){const erased=draggedNote.erasedIds||[];const target=notes.find(n=>n.pitch===pitch&&beat>=n.start_beat&&beatprev.filter(n=>n.id!==target.id));setSelectedNoteIds(prev=>prev.filter(id=>id!==target.id));}return;}if(draggedNote.mode==='scale'){const newEnd=getSnapBeat(Math.max(draggedNote.firstStart+0.125,beat),snapVal);const range=draggedNote.originalEnd-draggedNote.firstStart;if(range<=0)return;const scaleFactor=Math.max(0.01,(newEnd-draggedNote.firstStart)/range);const ids=draggedNote.selectedNoteIds||[];const firstStart=draggedNote.firstStart;const notesBefore=notesBeforeDragRef.current;if(!notesBefore)return;setNotes(prev=>prev.map(n=>{if(!ids.includes(n.id))return n;const orig=notesBefore?notesBefore.find(o=>o.id===n.id):null;const origStart=orig?orig.start_beat:n.start_beat;const origDur=orig?orig.duration_beats:n.duration_beats;const relStart=origStart-firstStart;const relEnd=relStart+origDur;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;// Clamp so no note goes past beat 0 -const minOrigStart=Math.min(...draggedNote.selectedNotesOffset.map(o=>o.originalStartBeat));const clampedDeltaBeat=minOrigStart+deltaBeat<0?-minOrigStart:deltaBeat;const deltaPitch=Math.round(pitch-draggedNote.startOffsetPitch);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+clampedDeltaBeat),snapVal),pitch:Math.max(0,Math.min(127,offset.originalPitch+deltaPitch))};}));}};const handleGridMouseUp=()=>{setDraggedNote(null);setSelectionMarquee(null);rightClickDragRef.current={active:false,startX:0,startY:0};if(brushAutoScrollRef.current){clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current=null;}};const handleContextMenu=e=>{e.preventDefault();if(swallowContextMenuRef.current){swallowContextMenuRef.current=false;return;}const pos={x:e.clientX,y:e.clientY,parentKey:null};scaleMenuOriginRef.current={x:pos.x,y:pos.y};setScaleMenuPos(pos);};const ccDragRef=React.useRef(null);const brushAutoScrollRef=React.useRef(null);const rightClickDragRef=React.useRef({active:false,startX:0,startY:0});const swallowContextMenuRef=React.useRef(false);const findCCNoteIndex=(b,mouseY,ccH)=>{const snapped=getSnapBeat(b,snapVal);const hits=[];notes.forEach((n,idx)=>{if(snapped>=n.start_beat&&snapped<=n.start_beat+n.duration_beats){const nv=ccMode==='pan'?(n.pan||0)*0.5+0.5:n.velocity!==undefined?n.velocity:0.8;const stemTop=ccH-(nv*(ccH-20)+10);hits.push({idx,dist:Math.abs(stemTop-mouseY)});}});if(hits.length>0){hits.sort((a,b)=>a.dist-b.dist);return hits[0].idx;}let nearest=-1;let minDist=Infinity;notes.forEach((n,idx)=>{const center=n.start_beat+n.duration_beats/2;const d=Math.abs(center-snapped);if(d{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;const noteIdx=findCCNoteIndex(beat,y,h);const val=Math.max(0,Math.min(1,(h-y)/h));if(e.ctrlKey){if(selectedNoteIds.length>0){const cursorNoteIdx=findCCNoteIndex(beat,y,h);if(cursorNoteIdx!==-1&&selectedNoteIds.includes(notes[cursorNoteIdx]?notes[cursorNoteIdx].id:-1)){const currentNote=notes[cursorNoteIdx];const currentVal=ccMode==='pan'?(currentNote.pan||0)/2.0+0.5:currentNote.velocity!==undefined?currentNote.velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==cursorNoteIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}ccDragRef.current={active:true,lastBeat:beat,selectedMode:true,lastPainted:[cursorNoteIdx]};}else{ccDragRef.current={active:true,lastBeat:beat,selectedMode:true,lastPainted:[]};}}else{ccDragRef.current={active:true,lastBeat:beat,lastPainted:noteIdx!==-1?[noteIdx]:[]};}return;}if(noteIdx!==-1){const currentVal=ccMode==='pan'?(notes[noteIdx].pan||0)/2.0+0.5:notes[noteIdx].velocity!==undefined?notes[noteIdx].velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==noteIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}}};const handleCCMouseMove=e=>{if(!ccDragRef.current||!ccDragRef.current.active)return;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;const val=Math.max(0,Math.min(1,(h-y)/h));const drag=ccDragRef.current;const painted=drag.lastPainted||[];if(drag.selectedMode&&selectedNoteIds.length>0){const cursorNoteIdx=findCCNoteIndex(beat,y,h);if(cursorNoteIdx!==-1){const cursorNote=notes[cursorNoteIdx];if(cursorNote&&selectedNoteIds.includes(cursorNote.id)&&!painted.includes(cursorNoteIdx)){const currentVal=ccMode==='pan'?(cursorNote.pan||0)/2.0+0.5:cursorNote.velocity!==undefined?cursorNote.velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==cursorNoteIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}drag.lastPainted=[...painted,cursorNoteIdx];}}return;}const candidateIdx=findCCNoteIndex(beat,y,h);if(candidateIdx!==-1&&!painted.includes(candidateIdx)){const currentVal=ccMode==='pan'?(notes[candidateIdx].pan||0)/2.0+0.5:notes[candidateIdx].velocity!==undefined?notes[candidateIdx].velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==candidateIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}drag.lastPainted=[...painted,candidateIdx];}};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 ${activeMidiPitches&&activeMidiPitches.has(pitch)?'bg-emerald-500 text-white border-emerald-400':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();keybedMouseDownRef.current=true;try{if(window.SonicSF){window.SonicSF.playNote(pitch,100,500,undefined,st.instrumentProgram,null);}}catch(err){console.error('playNote error:',err);}},onMouseEnter:e=>{if(keybedMouseDownRef.current){try{if(window.SonicSF){window.SonicSF.playNote(pitch,100,200,undefined,st.instrumentProgram,null);}}catch(err){console.error('playNote error:',err);}}},onMouseUp:()=>{keybedMouseDownRef.current=false;}},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;}if(ccWrapperRef.current){ccWrapperRef.current.scrollLeft=e.currentTarget.scrollLeft;}const el=e.currentTarget;const THRESHOLD=200;if(el.scrollLeft+el.clientWidth>=el.scrollWidth-THRESHOLD){const newBeats=rollBeatsRef.current+16;setRollBeats(newBeats);}};const handleKeybedScroll=e=>{if(gridScrollRef.current){gridScrollRef.current.scrollTop=e.currentTarget.scrollTop;}};const SCALES={"None":null,"Diatonic":{"Major":[0,2,4,5,7,9,11],"Minor":[0,2,3,5,7,8,10],"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10],"Locrian":[0,1,3,5,6,8,10]},"Pentatonic":{"Major":[0,2,4,7,9],"Minor":[0,3,5,7,10],"Blues":[0,3,5,6,7,10],"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"India":[0,2,5,7,9],"Japan":[0,2,5,7,9],"Africa":[0,3,5,7,10]},"Church":{"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10]},"Jazz":{"Blues":[0,3,5,6,7,10],"Bebop":[0,2,4,5,7,9,10,11],"Diminished":[0,2,3,5,6,8,9,11]},"Asian Traditional":{"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"Japan":[0,2,5,7,9],"India":[0,2,5,7,9],"Gamelan":[0,2,4,7,9],"Korea":[0,2,4,5,7,9]},"Middle Eastern":{"Hijaz":[0,1,4,5,7,8,11],"Nikriz":[0,1,4,5,7,8,10],"Rast":[0,2,4,5,7,8,10],"Saba":[0,1,3,4,7,8,10],"Bayati":[0,2,3,4,7,8,10]}};const[selectedScale,setSelectedScale]=React.useState(null);const selectedScaleRef=React.useRef(null);selectedScaleRef.current=selectedScale;const snapToScaleRef=React.useRef(true);snapToScaleRef.current=st.snapToScale!==undefined?st.snapToScale:true;const[scaleMenuPos,setScaleMenuPos]=React.useState(null);const scaleMenuOriginRef=React.useRef(null);const[showCC,setShowCC]=React.useState(true);const[ccHeight,setCcHeight]=React.useState(80);const snapPitchToScale=(pitch,scale)=>{if(!scale)return pitch;const octave=Math.floor(pitch/12);const noteInOctave=pitch%12;if(scale.includes(noteInOctave))return pitch;let best=noteInOctave;let minDist=12;scale.forEach(s=>{const dist=Math.abs(s-noteInOctave);if(dist{const closeMenu=()=>setScaleMenuPos(null);const origin=scaleMenuOriginRef.current||scaleMenuPos;const items=[];let subMenu=null;const isSameScale=(a,b)=>{if(!a||!b)return a===b;if(a.length!==b.length)return false;return a.every((v,i)=>v===b[i]);};const pushItem=(label,onClick,onHover)=>{const isActive=onClick._scale&&isSameScale(onClick._scale,selectedScale);items.push(React.createElement("div",{key:label,onClick:()=>{onClick();closeMenu();},onMouseEnter:onHover||undefined,className:"px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap "+(isActive?"bg-amber-800/40 text-amber-300":"text-zinc-300")},label));};Object.keys(SCALES).forEach(key=>{const val=SCALES[key];if(val===null){pushItem("None",()=>setSelectedScale(null));return;}if(Array.isArray(val)){pushItem(key,()=>setSelectedScale(val));}else{const parentKey=key;const isOpen=scaleMenuPos&&scaleMenuPos.parentKey===parentKey;pushItem(key+" ▸",()=>setSelectedScale(null),()=>{setScaleMenuPos({x:origin.x,y:origin.y,parentKey});});if(isOpen){const subs=[];Object.keys(val).forEach(subKey=>{subs.push(React.createElement("div",{key:subKey,onClick:()=>{setSelectedScale(val[subKey]);closeMenu();},className:"px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap "+(isSameScale(val[subKey],selectedScale)?"bg-amber-800/40 text-amber-300":"text-zinc-300")},subKey));});var subH=subs.length*30+16;var subTop=origin.y+subH+20>window.innerHeight?origin.y-subH:origin.y;subMenu=React.createElement("div",{style:{position:"fixed",left:origin.x+150,top:subTop,zIndex:10000},className:"bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[120px]"},...subs);}}});var menuH=items.length*30+16;var menuTop=origin.y+menuH+20>window.innerHeight?Math.max(10,origin.y-menuH):origin.y;return React.createElement(React.Fragment,null,React.createElement("div",{style:{position:"fixed",left:origin.x,top:menuTop,zIndex:9999},className:"bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[140px]"},...items),subMenu);};const renderBarLabels=()=>{const labels=[];const barsCount=Math.ceil(viewBeats/4);const barOffset=Math.floor(sessionStartBar);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 ${displayBar}`));}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 h-full"},/* 1. TOOLBAR HEADER */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"},React.createElement("div",{className:"flex items-center gap-4"},React.createElement("select",{value:st.target_id||'',onChange:function(e){handleSwitchMidiItem(e.target.value);},className:"bg-zinc-800 border border-zinc-700 text-yellow-500 font-bold text-xs rounded px-1.5 py-0.5 outline-none focus:border-yellow-500 max-w-[180px] uppercase"},allMidiItems.map(function(m){return React.createElement("option",{key:m.id,value:m.id},(m._trackName||'')+' - '+(m.name||'MIDI'));})),React.createElement("div",{className:"flex items-center gap-1 text-xs"},React.createElement("span",{className:"text-zinc-500 font-semibold"},"Snap to Scale"),React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,snapToScale:!(s.snapToScale!==undefined?s.snapToScale:true)}:s)),className:`w-7 h-4 rounded-full transition-colors relative ${(st.snapToScale!==undefined?st.snapToScale:true)?'bg-yellow-600':'bg-zinc-700'}`,style:{padding:0}},React.createElement("div",{className:`w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale!==undefined?st.snapToScale:true)?'translate-x-3.5':'translate-x-0.5'}`}))),React.createElement("div",{className:"flex items-center gap-1 text-xs"},React.createElement("span",{className:"text-zinc-500 font-semibold"},"Snap:"),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','4','1','1/2','1/4','1/8','1/16','1/32'].map(v=>React.createElement("option",{key:v,value:v},v)))),React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,isArmed:!s.isArmed}:s)),className:`px-2 py-1 rounded text-xs font-bold ${st.isArmed?'bg-red-600 text-white':'bg-zinc-800 text-zinc-400'}`},"ARM"),React.createElement("select",{value:selectedMidiInputId||'',onChange:e=>onMidiInputSelect(e.target.value),className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-xs outline-none max-w-[100px]"},React.createElement("option",{value:""},"Input"),React.createElement("option",{value:"ALL"},"Omni"),(midiDevices||[]).map(d=>React.createElement("option",{key:d.id,value:d.id},d.name||d.id))),React.createElement("button",{onClick:()=>onInstrumentSelect&&onInstrumentSelect(st.trackId),title:st.instrumentName||"Synth",className:`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[50px] ${st.instrumentName?'bg-violet-900 text-violet-300 border-violet-700':'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`},React.createElement("i",{"data-lucide":"music",className:"w-3 h-3 shrink-0"}),React.createElement("span",{className:"truncate text-[9px]"},st.instrumentName||'Synth')),React.createElement("div",{className:"flex items-center gap-1 ml-1 text-xs"},React.createElement("span",{className:"text-zinc-500"},"AI:"),React.createElement("input",{type:"number",value:aiBarStart,onChange:e=>setAiBarStart(parseInt(e.target.value)||0),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),React.createElement("span",{className:"text-zinc-500"},"-"),React.createElement("input",{type:"number",value:aiBarEnd,onChange:e=>setAiBarEnd(parseInt(e.target.value)||1),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),React.createElement("span",{className:"text-zinc-500"},"bar")),React.createElement("div",{className:"flex bg-zinc-800 p-0.5 rounded border border-zinc-700 text-xs"},['velocity','pan'].map(mode=>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)))),React.createElement("button",{onClick:()=>setShowCC(!showCC),className:`px-2 py-1 rounded text-xs ${showCC?'bg-purple-900/60 text-purple-300 border border-purple-700':'text-zinc-500 hover:text-zinc-300'}`},ccMode==='pan'?'Pan':'Vel'),React.createElement("button",{onClick:function(){setSessionSyncMode(function(p){return!p;});},className:function(){var base='px-2 py-1 rounded text-xs ';return sessionSyncMode?base+'bg-cyan-900/60 text-cyan-300 border border-cyan-700':base+'text-zinc-500 hover:text-zinc-300';}(),title:sessionSyncMode?"Session-synced mode (ghost visible)":"Isolated mode (bar 0, no ghost)"},sessionSyncMode?"\uD83C\uDF10 Session":"\uD83D\uDCCB Isolated"),React.createElement("button",{onClick:function(){setShowGhostNotes(function(p){return!p;});},disabled:!sessionSyncMode,className:function(){if(!sessionSyncMode)return'px-2 py-1 rounded text-xs opacity-30 cursor-not-allowed';var base='px-2 py-1 rounded text-xs ';return showGhostNotes?base+'bg-purple-900/60 text-purple-300 border border-purple-700':base+'text-zinc-500 hover:text-zinc-300';}(),title:"Toggle ghost notes visibility"},"\uD83D\uDC7B Ghost"),React.createElement("div",{className:"flex items-center gap-1"},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"},React.createElement("i",{"data-lucide":"save",className:"w-3 h-3"}),"Lưu"),React.createElement("button",{onClick:()=>{const ppq=480;const bpmNum=parseInt(bpm)||120;const ticksPerBeat=ppq;const events=[];(notes||[]).forEach(n=>{const startTick=Math.round((n.start_beat||0)*ticksPerBeat);const durTick=Math.round((n.duration_beats||1)*ticksPerBeat);const pitch=n.pitch||60;const vel=Math.round((n.velocity||0.8)*127);events.push({tick:startTick,type:'note_on',pitch,velocity:vel});events.push({tick:startTick+durTick,type:'note_off',pitch,velocity:0});});events.sort((a,b)=>a.tick-b.tick||(a.type==='note_off'?-1:1));const writeVLQ=(bytes,v)=>{let val=Math.max(0,v);const buf=[];buf.push(val&0x7F);while(val>0x7F){val>>=7;buf.push(0x80|val&0x7F);}for(let i=buf.length-1;i>=0;i--)bytes.push(buf[i]);};const trackBytes=[];let lastTick=0;events.forEach(ev=>{const delta=Math.max(0,ev.tick-lastTick);writeVLQ(trackBytes,delta);trackBytes.push(ev.type==='note_on'?0x90:0x80,ev.pitch,ev.velocity);lastTick=ev.tick;});writeVLQ(trackBytes,0);trackBytes.push(0xFF,0x2F,0x00);const trackData=[0x4D,0x54,0x72,0x6B];const len=trackBytes.length;trackData.push(len>>24&0xFF,len>>16&0xFF,len>>8&0xFF,len&0xFF);trackData.push(...trackBytes);const header=[0x4D,0x54,0x68,0x64,0x00,0x00,0x00,0x06,0x00,0x01,0x00,0x01,ppq>>8&0xFF,ppq&0xFF];const all=header.concat(trackData);const blob=new Blob([new Uint8Array(all)],{type:'audio/midi'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=(st.label||'midi')+'.mid';document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);showToast('Đã xuất file MIDI!','success');},className:"px-2.5 py-1 bg-amber-700 hover:bg-amber-600 text-white rounded text-xs flex items-center gap-1 transition font-semibold"},React.createElement("i",{"data-lucide":"file-down",className:"w-3 h-3"}),"Export"),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"},React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}),"Đóng"))),/* 2. BAR RULER */React.createElement("div",{className:"h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"},React.createElement("div",{className:"w-[120px] bg-[#1e1e22] border-r border-zinc-800 shrink-0 flex items-end"},React.createElement("span",{className:"text-[8px] text-zinc-600 font-mono px-1.5 pb-0.5 uppercase tracking-wider"},"Tracks")),React.createElement("div",{className:"w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"}),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;const clickInRange=loopStartBeat!==null&&loopEndBeat!==null&&clickBeat>=loopStartBeat&&clickBeat<=loopEndBeat;if(e.ctrlKey||e.metaKey){setLoopStartBeat(null);setLoopEndBeat(null);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:null,selectionEnd:null}:s));return;}if(e.shiftKey){const beatSnap=getSnapBeat(clickBeat,snapVal);if(loopStartBeat===null){setLoopStartBeat(Math.max(0,beatSnap-4));setLoopEndBeat(Math.max(4,beatSnap));}else{setLoopEndBeat(Math.max(loopStartBeat+4,beatSnap));}return;}if(clickTime>=0){if(onSeekPlayhead){onSeekPlayhead(clickTime);}else{setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:clickTime}:s));}}const snappedStartBeat=getSnapBeat(clickBeat,snapVal);rulerDragRef.current={startX:e.clientX,startBeat:snappedStartBeat,scrollLeft:e.currentTarget.scrollLeft};const onMove=ev=>{const r=rulerScrollRef.current;if(!r||!rulerDragRef.current)return;const rRect=r.getBoundingClientRect();const bx=ev.clientX-rRect.left+rulerDragRef.current.scrollLeft;const rawBeat=Math.max(0,bx/pixelsPerBeat);const beat=getSnapBeat(rawBeat,snapVal);if(Math.abs(ev.clientX-rulerDragRef.current.startX)>5){if(clickInRange){const rangeWidth=loopEndBeat-loopStartBeat;const offset=rulerDragRef.current.startBeat-loopStartBeat;const centerBeat=beat-offset;const halfRange=rangeWidth/2;const newStart=Math.max(0,centerBeat-halfRange);setLoopStartBeat(newStart);setLoopEndBeat(newStart+rangeWidth);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:newStart*beatSec,selectionEnd:(newStart+rangeWidth)*beatSec}:s));}else{const sBeat=Math.max(0,Math.min(rulerDragRef.current.startBeat,beat));const eBeat=Math.max(sBeat+1,Math.max(rulerDragRef.current.startBeat,beat));setLoopStartBeat(sBeat);setLoopEndBeat(eBeat);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:sBeat*beatSec,selectionEnd:eBeat*beatSec}:s));}}};const onUp=()=>{rulerDragRef.current=null;document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}},React.createElement("div",{style:{width:`${viewWidth}px`,height:'100%'},className:"relative h-full font-mono text-[9px] text-zinc-500 font-bold"},renderBarLabels(),loopStartBeat!==null&&loopEndBeat!==null&&loopEndBeat>loopStartBeat&&React.createElement("div",{style:{left:`${loopStartBeat*pixelsPerBeat}px`,width:`${(loopEndBeat-loopStartBeat)*pixelsPerBeat}px`,top:0,bottom:0},className:"absolute bg-emerald-500/15 border-l border-r border-emerald-400"},React.createElement("div",{style:{position:'absolute',left:0,top:0,bottom:0,width:'4px',cursor:'ew-resize'},onMouseDown:e=>{e.stopPropagation();const startBeat=loopStartBeat;const onMove=ev=>{const r=rulerScrollRef.current;if(!r)return;const rRect=r.getBoundingClientRect();const bx=ev.clientX-rRect.left+r.scrollLeft;const nBeat=Math.max(0,Math.min(loopEndBeat-1,getSnapBeat(bx/pixelsPerBeat,snapVal)));setLoopStartBeat(nBeat);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:nBeat*beatSec}:s));};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}}),React.createElement("div",{style:{position:'absolute',right:0,top:0,bottom:0,width:'4px',cursor:'ew-resize'},onMouseDown:e=>{e.stopPropagation();const onMove=ev=>{const r=rulerScrollRef.current;if(!r)return;const rRect=r.getBoundingClientRect();const bx=ev.clientX-rRect.left+r.scrollLeft;const nBeat=Math.max(loopStartBeat+1,getSnapBeat(bx/pixelsPerBeat,snapVal));setLoopEndBeat(nBeat);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionEnd:nBeat*beatSec}:s));};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}}))))),/* 3. MAIN PIANO ROLL GRID (KEYBOARD + TRACK COL + CANVAS) */React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0 relative"},/* Track column overlay — always visible */React.createElement("div",{className:"absolute left-0 top-0 w-[120px] bg-[#16161a] border-r border-zinc-800 z-10 flex flex-col overflow-hidden pointer-events-none",style:{height:KeybedPixelHeight+'px'}},React.createElement("div",{className:"pointer-events-auto"},allMidiItems.length>0?function(){var seenTracks={};var els=[];allMidiItems.forEach(function(m){if(seenTracks[m._trackId])return;seenTracks[m._trackId]=true;var track=(activeTracks||[]).find(function(t){return t.id===m._trackId;});var isActive=m._trackId===st.trackId&&m.id===st.target_id;var isPlayOn=activePlayTrackIds===null||activePlayTrackIds.has(m._trackId);els.push(React.createElement("div",{key:m._trackId,onClick:function(){if(isActive)return;setActivePlayTrackIds(function(prev){var s=prev===null?new Set():new Set(prev);if(isPlayOn)s.delete(m._trackId);else s.add(m._trackId);return s.size===0?null:s;});},className:"flex items-center px-1.5 h-[18px] border-b border-zinc-900 cursor-pointer "+(isActive?'bg-yellow-900/20':isPlayOn?'bg-zinc-800/40':'bg-zinc-900/60')},React.createElement("span",{className:"text-[9px] font-mono truncate "+(isActive?'text-yellow-400 font-bold':isPlayOn?'text-zinc-300':'text-zinc-600'),title:track?track.name:m._trackName},track?track.name:m._trackName)));});return els;}():null)),React.createElement("div",{className:"w-[120px] shrink-0"}),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()),React.createElement("div",{ref:gridScrollRef,onScroll:handleScroll,className:"flex-1 overflow-auto bg-[#141414] min-w-0"},React.createElement("div",{style:{width:`${viewWidth}px`,height:`${(128-PITCH_START)*NoteHeight}px`},className:"relative"},React.createElement("canvas",{ref:canvasRef,onMouseDown:handleGridMouseDown,onMouseMove:handleGridMouseMove,onMouseUp:handleGridMouseUp,onMouseLeave:handleGridMouseUp,onContextMenu:handleContextMenu,className:"absolute inset-0 cursor-crosshair"}),loopStartBeat!==null&&loopEndBeat!==null&&loopEndBeat>loopStartBeat&&React.createElement("div",{style:{left:`${loopStartBeat*pixelsPerBeat}px`,width:`${(loopEndBeat-loopStartBeat)*pixelsPerBeat}px`,top:0,bottom:0},className:"absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"})))),/* 4. CC / VELOCITY LANE (ĐẶT Ở ĐÁY) */showCC&&React.createElement("div",{style:{height:`${ccHeight}px`},className:"bg-[#161616] border-t border-zinc-900 flex shrink-0 relative"},React.createElement("div",{onMouseDown:e=>{e.preventDefault();const startY=e.clientY;const startH=ccHeight;const onMove=ev=>{setCcHeight(Math.max(40,startH+startY-ev.clientY));};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);},className:"absolute top-0 left-0 right-0 h-1.5 cursor-n-resize z-10 hover:bg-purple-600/30"}),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()),React.createElement("div",{ref:ccWrapperRef,className:"flex-1 overflow-x-hidden min-w-0"},React.createElement("div",{style:{width:`${viewWidth}px`,height:'100%'},className:"relative"},React.createElement("canvas",{ref:ccCanvasRef,onMouseDown:handleCCMouseDown,onMouseMove:handleCCMouseMove,onMouseUp:()=>{if(ccDragRef.current)ccDragRef.current.selectedMode=false;ccDragRef.current=null;},onMouseLeave:()=>{if(ccDragRef.current)ccDragRef.current.selectedMode=false;ccDragRef.current=null;},className:"absolute inset-0"}),loopStartBeat!==null&&loopEndBeat!==null&&loopEndBeat>loopStartBeat&&React.createElement("div",{style:{left:`${loopStartBeat*pixelsPerBeat}px`,width:`${(loopEndBeat-loopStartBeat)*pixelsPerBeat}px`,top:0,bottom:0},className:"absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"})))),/* 5. OVERLAY / CONTEXT MENU */scaleMenuPos&&renderScaleContextMenu());};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,instrument_source:t.instrument_source||(t.synth_engine?t.synth_engine.type:null),soundfont_id:t.soundfont_id||(t.synth_engine?t.synth_engine.soundfont_id:null),soundfont_bank:t.soundfont_bank!==undefined?t.soundfont_bank:t.synth_engine?t.synth_engine.soundfont_bank:null,soundfont_program:t.soundfont_program!==undefined?t.soundfont_program:t.synth_engine?t.synth_engine.soundfont_program:null,synth_engine:t.synth_engine||undefined,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,length_bars:item.duration_bars||4,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,length_bars:item.duration_bars||4,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,instrument_source:t.instrument_source||null,soundfont_id:t.soundfont_id||null,soundfont_bank:t.soundfont_bank!==null?t.soundfont_bank:undefined,soundfont_program:t.soundfont_program!==null?t.soundfont_program:undefined,synth_engine:t.synth_engine||undefined};});};const serializeProjectToSchema=(projectId,name,bpmVal,tracksList,subTabsList,sessionTabsList)=>{const secondsPerBar=60.0/parseFloat(bpmVal||120)*4;const mainTracks=serializeTracksList(tracksList,secondsPerBar);const sectionStore={};// Helper: compute length_bars from tracks content +const minOrigStart=Math.min(...draggedNote.selectedNotesOffset.map(o=>o.originalStartBeat));const clampedDeltaBeat=minOrigStart+deltaBeat<0?-minOrigStart:deltaBeat;const deltaPitch=Math.round(pitch-draggedNote.startOffsetPitch);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+clampedDeltaBeat),snapVal),pitch:Math.max(0,Math.min(127,offset.originalPitch+deltaPitch))};}));}};const handleGridMouseUp=()=>{setDraggedNote(null);setSelectionMarquee(null);rightClickDragRef.current={active:false,startX:0,startY:0};if(brushAutoScrollRef.current){clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current=null;}};const handleContextMenu=e=>{e.preventDefault();if(swallowContextMenuRef.current){swallowContextMenuRef.current=false;return;}const pos={x:e.clientX,y:e.clientY,parentKey:null};scaleMenuOriginRef.current={x:pos.x,y:pos.y};setScaleMenuPos(pos);};const ccDragRef=React.useRef(null);const brushAutoScrollRef=React.useRef(null);const rightClickDragRef=React.useRef({active:false,startX:0,startY:0});const swallowContextMenuRef=React.useRef(false);const findCCNoteIndex=(b,mouseY,ccH)=>{const snapped=getSnapBeat(b,snapVal);const hits=[];notes.forEach((n,idx)=>{if(snapped>=n.start_beat&&snapped<=n.start_beat+n.duration_beats){const nv=ccMode==='pan'?(n.pan||0)*0.5+0.5:n.velocity!==undefined?n.velocity:0.8;const stemTop=ccH-(nv*(ccH-20)+10);hits.push({idx,dist:Math.abs(stemTop-mouseY)});}});if(hits.length>0){hits.sort((a,b)=>a.dist-b.dist);return hits[0].idx;}let nearest=-1;let minDist=Infinity;notes.forEach((n,idx)=>{const center=n.start_beat+n.duration_beats/2;const d=Math.abs(center-snapped);if(d{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;const noteIdx=findCCNoteIndex(beat,y,h);const val=Math.max(0,Math.min(1,(h-y)/h));if(e.ctrlKey){if(selectedNoteIds.length>0){const cursorNoteIdx=findCCNoteIndex(beat,y,h);if(cursorNoteIdx!==-1&&selectedNoteIds.includes(notes[cursorNoteIdx]?notes[cursorNoteIdx].id:-1)){const currentNote=notes[cursorNoteIdx];const currentVal=ccMode==='pan'?(currentNote.pan||0)/2.0+0.5:currentNote.velocity!==undefined?currentNote.velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==cursorNoteIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}ccDragRef.current={active:true,lastBeat:beat,selectedMode:true,lastPainted:[cursorNoteIdx]};}else{ccDragRef.current={active:true,lastBeat:beat,selectedMode:true,lastPainted:[]};}}else{ccDragRef.current={active:true,lastBeat:beat,lastPainted:noteIdx!==-1?[noteIdx]:[]};}return;}if(noteIdx!==-1){const currentVal=ccMode==='pan'?(notes[noteIdx].pan||0)/2.0+0.5:notes[noteIdx].velocity!==undefined?notes[noteIdx].velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==noteIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}}};const handleCCMouseMove=e=>{if(!ccDragRef.current||!ccDragRef.current.active)return;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;const val=Math.max(0,Math.min(1,(h-y)/h));const drag=ccDragRef.current;const painted=drag.lastPainted||[];if(drag.selectedMode&&selectedNoteIds.length>0){const cursorNoteIdx=findCCNoteIndex(beat,y,h);if(cursorNoteIdx!==-1){const cursorNote=notes[cursorNoteIdx];if(cursorNote&&selectedNoteIds.includes(cursorNote.id)&&!painted.includes(cursorNoteIdx)){const currentVal=ccMode==='pan'?(cursorNote.pan||0)/2.0+0.5:cursorNote.velocity!==undefined?cursorNote.velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==cursorNoteIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}drag.lastPainted=[...painted,cursorNoteIdx];}}return;}const candidateIdx=findCCNoteIndex(beat,y,h);if(candidateIdx!==-1&&!painted.includes(candidateIdx)){const currentVal=ccMode==='pan'?(notes[candidateIdx].pan||0)/2.0+0.5:notes[candidateIdx].velocity!==undefined?notes[candidateIdx].velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==candidateIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}drag.lastPainted=[...painted,candidateIdx];}};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 ${activeMidiPitches&&activeMidiPitches.has(pitch)?'bg-emerald-500 text-white border-emerald-400':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();keybedMouseDownRef.current=true;try{if(window.SonicSF){window.SonicSF.playNote(pitch,100,500,undefined,st.instrumentProgram,null);}}catch(err){console.error('playNote error:',err);}},onMouseEnter:e=>{if(keybedMouseDownRef.current){try{if(window.SonicSF){window.SonicSF.playNote(pitch,100,200,undefined,st.instrumentProgram,null);}}catch(err){console.error('playNote error:',err);}}},onMouseUp:()=>{keybedMouseDownRef.current=false;}},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;}if(ccWrapperRef.current){ccWrapperRef.current.scrollLeft=e.currentTarget.scrollLeft;}const el=e.currentTarget;const THRESHOLD=200;if(el.scrollLeft+el.clientWidth>=el.scrollWidth-THRESHOLD){const newBeats=rollBeatsRef.current+16;setRollBeats(newBeats);}};const handleKeybedScroll=e=>{if(gridScrollRef.current){gridScrollRef.current.scrollTop=e.currentTarget.scrollTop;}};const SCALES={"None":null,"Diatonic":{"Major":[0,2,4,5,7,9,11],"Minor":[0,2,3,5,7,8,10],"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10],"Locrian":[0,1,3,5,6,8,10]},"Pentatonic":{"Major":[0,2,4,7,9],"Minor":[0,3,5,7,10],"Blues":[0,3,5,6,7,10],"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"India":[0,2,5,7,9],"Japan":[0,2,5,7,9],"Africa":[0,3,5,7,10]},"Church":{"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10]},"Jazz":{"Blues":[0,3,5,6,7,10],"Bebop":[0,2,4,5,7,9,10,11],"Diminished":[0,2,3,5,6,8,9,11]},"Asian Traditional":{"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"Japan":[0,2,5,7,9],"India":[0,2,5,7,9],"Gamelan":[0,2,4,7,9],"Korea":[0,2,4,5,7,9]},"Middle Eastern":{"Hijaz":[0,1,4,5,7,8,11],"Nikriz":[0,1,4,5,7,8,10],"Rast":[0,2,4,5,7,8,10],"Saba":[0,1,3,4,7,8,10],"Bayati":[0,2,3,4,7,8,10]}};const[selectedScale,setSelectedScale]=React.useState(null);const selectedScaleRef=React.useRef(null);selectedScaleRef.current=selectedScale;const snapToScaleRef=React.useRef(true);snapToScaleRef.current=st.snapToScale!==undefined?st.snapToScale:true;const[scaleMenuPos,setScaleMenuPos]=React.useState(null);const scaleMenuOriginRef=React.useRef(null);const[showCC,setShowCC]=React.useState(true);const[ccHeight,setCcHeight]=React.useState(80);const snapPitchToScale=(pitch,scale)=>{if(!scale)return pitch;const octave=Math.floor(pitch/12);const noteInOctave=pitch%12;if(scale.includes(noteInOctave))return pitch;let best=noteInOctave;let minDist=12;scale.forEach(s=>{const dist=Math.abs(s-noteInOctave);if(dist{const closeMenu=()=>setScaleMenuPos(null);const origin=scaleMenuOriginRef.current||scaleMenuPos;const items=[];let subMenu=null;const isSameScale=(a,b)=>{if(!a||!b)return a===b;if(a.length!==b.length)return false;return a.every((v,i)=>v===b[i]);};const pushItem=(label,onClick,onHover)=>{const isActive=onClick._scale&&isSameScale(onClick._scale,selectedScale);items.push(React.createElement("div",{key:label,onClick:()=>{onClick();closeMenu();},onMouseEnter:onHover||undefined,className:"px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap "+(isActive?"bg-amber-800/40 text-amber-300":"text-zinc-300")},label));};Object.keys(SCALES).forEach(key=>{const val=SCALES[key];if(val===null){pushItem("None",()=>setSelectedScale(null));return;}if(Array.isArray(val)){pushItem(key,()=>setSelectedScale(val));}else{const parentKey=key;const isOpen=scaleMenuPos&&scaleMenuPos.parentKey===parentKey;pushItem(key+" ▸",()=>setSelectedScale(null),()=>{setScaleMenuPos({x:origin.x,y:origin.y,parentKey});});if(isOpen){const subs=[];Object.keys(val).forEach(subKey=>{subs.push(React.createElement("div",{key:subKey,onClick:()=>{setSelectedScale(val[subKey]);closeMenu();},className:"px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap "+(isSameScale(val[subKey],selectedScale)?"bg-amber-800/40 text-amber-300":"text-zinc-300")},subKey));});var subH=subs.length*30+16;var subTop=origin.y+subH+20>window.innerHeight?origin.y-subH:origin.y;subMenu=React.createElement("div",{style:{position:"fixed",left:origin.x+150,top:subTop,zIndex:10000},className:"bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[120px]"},...subs);}}});var menuH=items.length*30+16;var menuTop=origin.y+menuH+20>window.innerHeight?Math.max(10,origin.y-menuH):origin.y;return React.createElement(React.Fragment,null,React.createElement("div",{style:{position:"fixed",left:origin.x,top:menuTop,zIndex:9999},className:"bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[140px]"},...items),subMenu);};const renderBarLabels=()=>{const labels=[];const barsCount=Math.ceil(viewBeats/4);const barOffset=Math.floor(sessionStartBar);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 ${displayBar}`));}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 h-full"},/* 1. TOOLBAR HEADER */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"},React.createElement("div",{className:"flex items-center gap-4"},React.createElement("select",{value:st.target_id||'',onChange:function(e){handleSwitchMidiItem(e.target.value);},className:"bg-zinc-800 border border-zinc-700 text-yellow-500 font-bold text-xs rounded px-1.5 py-0.5 outline-none focus:border-yellow-500 max-w-[180px] uppercase"},allMidiItems.map(function(m){return React.createElement("option",{key:m.id,value:m.id},(m._trackName||'')+' - '+(m.name||'MIDI'));})),React.createElement("div",{className:"flex items-center gap-1 text-xs"},React.createElement("span",{className:"text-zinc-500 font-semibold"},"Snap to Scale"),React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,snapToScale:!(s.snapToScale!==undefined?s.snapToScale:true)}:s)),className:`w-7 h-4 rounded-full transition-colors relative ${(st.snapToScale!==undefined?st.snapToScale:true)?'bg-yellow-600':'bg-zinc-700'}`,style:{padding:0}},React.createElement("div",{className:`w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale!==undefined?st.snapToScale:true)?'translate-x-3.5':'translate-x-0.5'}`}))),React.createElement("div",{className:"flex items-center gap-1 text-xs"},React.createElement("span",{className:"text-zinc-500 font-semibold"},"Snap:"),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','4','1','1/2','1/4','1/8','1/16','1/32'].map(v=>React.createElement("option",{key:v,value:v},v)))),React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,isArmed:!s.isArmed}:s)),className:`px-2 py-1 rounded text-xs font-bold ${st.isArmed?'bg-red-600 text-white':'bg-zinc-800 text-zinc-400'}`},"ARM"),React.createElement("select",{value:selectedMidiInputId||'',onChange:e=>onMidiInputSelect(e.target.value),className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-xs outline-none max-w-[100px]"},React.createElement("option",{value:""},"Input"),React.createElement("option",{value:"ALL"},"Omni"),(midiDevices||[]).map(d=>React.createElement("option",{key:d.id,value:d.id},d.name||d.id))),React.createElement("button",{onClick:()=>onInstrumentSelect&&onInstrumentSelect(st.trackId),title:st.instrumentName||"Synth",className:`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[50px] ${st.instrumentName?'bg-violet-900 text-violet-300 border-violet-700':'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`},React.createElement("i",{"data-lucide":"music",className:"w-3 h-3 shrink-0"}),React.createElement("span",{className:"truncate text-[9px]"},st.instrumentName||'Synth')),React.createElement("div",{className:"flex items-center gap-1 ml-1 text-xs"},React.createElement("span",{className:"text-zinc-500"},"AI:"),React.createElement("input",{type:"number",value:aiBarStart,onChange:e=>setAiBarStart(parseInt(e.target.value)||0),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),React.createElement("span",{className:"text-zinc-500"},"-"),React.createElement("input",{type:"number",value:aiBarEnd,onChange:e=>setAiBarEnd(parseInt(e.target.value)||1),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),React.createElement("span",{className:"text-zinc-500"},"bar")),React.createElement("div",{className:"flex bg-zinc-800 p-0.5 rounded border border-zinc-700 text-xs"},['velocity','pan'].map(mode=>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)))),React.createElement("button",{onClick:()=>setShowCC(!showCC),className:`px-2 py-1 rounded text-xs ${showCC?'bg-purple-900/60 text-purple-300 border border-purple-700':'text-zinc-500 hover:text-zinc-300'}`},ccMode==='pan'?'Pan':'Vel'),React.createElement("button",{onClick:function(){setSessionSyncMode(function(p){return!p;});},className:function(){var base='px-2 py-1 rounded text-xs ';return sessionSyncMode?base+'bg-cyan-900/60 text-cyan-300 border border-cyan-700':base+'text-zinc-500 hover:text-zinc-300';}(),title:sessionSyncMode?"Session-synced mode (ghost visible)":"Isolated mode (bar 0, no ghost)"},sessionSyncMode?"\uD83C\uDF10 Session":"\uD83D\uDCCB Isolated"),React.createElement("button",{onClick:function(){setShowGhostNotes(function(p){return!p;});},disabled:!sessionSyncMode,className:function(){if(!sessionSyncMode)return'px-2 py-1 rounded text-xs opacity-30 cursor-not-allowed';var base='px-2 py-1 rounded text-xs ';return showGhostNotes?base+'bg-purple-900/60 text-purple-300 border border-purple-700':base+'text-zinc-500 hover:text-zinc-300';}(),title:"Toggle ghost notes visibility"},"\uD83D\uDC7B Ghost"),React.createElement("div",{className:"flex items-center gap-1"},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"},React.createElement("i",{"data-lucide":"save",className:"w-3 h-3"}),"Lưu"),React.createElement("button",{onClick:()=>{const ppq=480;const bpmNum=parseInt(bpm)||120;const ticksPerBeat=ppq;const events=[];(notes||[]).forEach(n=>{const startTick=Math.round((n.start_beat||0)*ticksPerBeat);const durTick=Math.round((n.duration_beats||1)*ticksPerBeat);const pitch=n.pitch||60;const vel=Math.round((n.velocity||0.8)*127);events.push({tick:startTick,type:'note_on',pitch,velocity:vel});events.push({tick:startTick+durTick,type:'note_off',pitch,velocity:0});});events.sort((a,b)=>a.tick-b.tick||(a.type==='note_off'?-1:1));const writeVLQ=(bytes,v)=>{let val=Math.max(0,v);const buf=[];buf.push(val&0x7F);while(val>0x7F){val>>=7;buf.push(0x80|val&0x7F);}for(let i=buf.length-1;i>=0;i--)bytes.push(buf[i]);};const trackBytes=[];let lastTick=0;events.forEach(ev=>{const delta=Math.max(0,ev.tick-lastTick);writeVLQ(trackBytes,delta);trackBytes.push(ev.type==='note_on'?0x90:0x80,ev.pitch,ev.velocity);lastTick=ev.tick;});writeVLQ(trackBytes,0);trackBytes.push(0xFF,0x2F,0x00);const trackData=[0x4D,0x54,0x72,0x6B];const len=trackBytes.length;trackData.push(len>>24&0xFF,len>>16&0xFF,len>>8&0xFF,len&0xFF);trackData.push(...trackBytes);const header=[0x4D,0x54,0x68,0x64,0x00,0x00,0x00,0x06,0x00,0x01,0x00,0x01,ppq>>8&0xFF,ppq&0xFF];const all=header.concat(trackData);const blob=new Blob([new Uint8Array(all)],{type:'audio/midi'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=(st.label||'midi')+'.mid';document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);showToast('Đã xuất file MIDI!','success');},className:"px-2.5 py-1 bg-amber-700 hover:bg-amber-600 text-white rounded text-xs flex items-center gap-1 transition font-semibold"},React.createElement("i",{"data-lucide":"file-down",className:"w-3 h-3"}),"Export"),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"},React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}),"Đóng"))),/* 2. BAR RULER */React.createElement("div",{className:"h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"},React.createElement("div",{className:"w-[120px] bg-[#1e1e22] border-r border-zinc-800 shrink-0 flex items-end"},React.createElement("span",{className:"text-[8px] text-zinc-600 font-mono px-1.5 pb-0.5 uppercase tracking-wider"},"Tracks")),React.createElement("div",{className:"w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"}),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;const clickInRange=loopStartBeat!==null&&loopEndBeat!==null&&clickBeat>=loopStartBeat&&clickBeat<=loopEndBeat;if(e.ctrlKey||e.metaKey){setLoopStartBeat(null);setLoopEndBeat(null);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:null,selectionEnd:null}:s));return;}if(e.shiftKey){const beatSnap=getSnapBeat(clickBeat,snapVal);if(loopStartBeat===null){setLoopStartBeat(Math.max(0,beatSnap-4));setLoopEndBeat(Math.max(4,beatSnap));}else{setLoopEndBeat(Math.max(loopStartBeat+4,beatSnap));}return;}if(clickTime>=0){if(onSeekPlayhead){onSeekPlayhead(clickTime);}else{setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:clickTime}:s));}}const snappedStartBeat=getSnapBeat(clickBeat,snapVal);rulerDragRef.current={startX:e.clientX,startBeat:snappedStartBeat,scrollLeft:e.currentTarget.scrollLeft};const onMove=ev=>{const r=rulerScrollRef.current;if(!r||!rulerDragRef.current)return;const rRect=r.getBoundingClientRect();const bx=ev.clientX-rRect.left+rulerDragRef.current.scrollLeft;const rawBeat=Math.max(0,bx/pixelsPerBeat);const beat=getSnapBeat(rawBeat,snapVal);if(Math.abs(ev.clientX-rulerDragRef.current.startX)>5){if(clickInRange){const rangeWidth=loopEndBeat-loopStartBeat;const offset=rulerDragRef.current.startBeat-loopStartBeat;const centerBeat=beat-offset;const halfRange=rangeWidth/2;const newStart=Math.max(0,centerBeat-halfRange);setLoopStartBeat(newStart);setLoopEndBeat(newStart+rangeWidth);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:newStart*beatSec,selectionEnd:(newStart+rangeWidth)*beatSec}:s));}else{const sBeat=Math.max(0,Math.min(rulerDragRef.current.startBeat,beat));const eBeat=Math.max(sBeat+1,Math.max(rulerDragRef.current.startBeat,beat));setLoopStartBeat(sBeat);setLoopEndBeat(eBeat);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:sBeat*beatSec,selectionEnd:eBeat*beatSec}:s));}}};const onUp=()=>{rulerDragRef.current=null;document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}},React.createElement("div",{style:{width:`${viewWidth}px`,height:'100%'},className:"relative h-full font-mono text-[9px] text-zinc-500 font-bold"},renderBarLabels(),loopStartBeat!==null&&loopEndBeat!==null&&loopEndBeat>loopStartBeat&&React.createElement("div",{style:{left:`${loopStartBeat*pixelsPerBeat}px`,width:`${(loopEndBeat-loopStartBeat)*pixelsPerBeat}px`,top:0,bottom:0},className:"absolute bg-emerald-500/15 border-l border-r border-emerald-400"},React.createElement("div",{style:{position:'absolute',left:0,top:0,bottom:0,width:'4px',cursor:'ew-resize'},onMouseDown:e=>{e.stopPropagation();const startBeat=loopStartBeat;const onMove=ev=>{const r=rulerScrollRef.current;if(!r)return;const rRect=r.getBoundingClientRect();const bx=ev.clientX-rRect.left+r.scrollLeft;const nBeat=Math.max(0,Math.min(loopEndBeat-1,getSnapBeat(bx/pixelsPerBeat,snapVal)));setLoopStartBeat(nBeat);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:nBeat*beatSec}:s));};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}}),React.createElement("div",{style:{position:'absolute',right:0,top:0,bottom:0,width:'4px',cursor:'ew-resize'},onMouseDown:e=>{e.stopPropagation();const onMove=ev=>{const r=rulerScrollRef.current;if(!r)return;const rRect=r.getBoundingClientRect();const bx=ev.clientX-rRect.left+r.scrollLeft;const nBeat=Math.max(loopStartBeat+1,getSnapBeat(bx/pixelsPerBeat,snapVal));setLoopEndBeat(nBeat);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionEnd:nBeat*beatSec}:s));};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}}))))),/* 3. MAIN PIANO ROLL GRID (KEYBOARD + TRACK COL + CANVAS) */React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0 relative"},/* Track column overlay — always visible */React.createElement("div",{className:"absolute left-0 top-0 w-[120px] bg-[#16161a] border-r border-zinc-800 z-10 flex flex-col overflow-hidden pointer-events-none",style:{height:KeybedPixelHeight+'px'}},React.createElement("div",{className:"pointer-events-auto flex flex-col"},allMidiItems.length>0?function(){var seenTracks={};var els=[];allMidiItems.forEach(function(m){if(seenTracks[m._trackId])return;seenTracks[m._trackId]=true;var track=(activeTracks||[]).find(function(t){return t.id===m._trackId;});var isActive=m._trackId===st.trackId&&m.id===st.target_id;var isPlayOn=activePlayTrackIds===null||activePlayTrackIds.has(m._trackId);els.push(React.createElement("div",{key:m._trackId,onClick:function(){if(isActive)return;setActivePlayTrackIds(function(prev){var s=prev===null?new Set():new Set(prev);if(isPlayOn)s.delete(m._trackId);else s.add(m._trackId);return s.size===0?null:s;});},className:"flex items-center justify-center h-[18px] border-b border-zinc-900 cursor-pointer select-none "+(isActive?'bg-yellow-600 text-black font-bold':isPlayOn?'bg-purple-700 text-white font-semibold':'bg-zinc-800 text-zinc-500 hover:bg-zinc-700')},React.createElement("span",{className:"text-[12px] font-sans truncate px-1",title:track?track.name:m._trackName},track?track.name:m._trackName)));});return els;}():null)),React.createElement("div",{className:"w-[120px] shrink-0"}),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()),React.createElement("div",{ref:gridScrollRef,onScroll:handleScroll,className:"flex-1 overflow-auto bg-[#141414] min-w-0"},React.createElement("div",{style:{width:`${viewWidth}px`,height:`${(128-PITCH_START)*NoteHeight}px`},className:"relative"},React.createElement("canvas",{ref:canvasRef,onMouseDown:handleGridMouseDown,onMouseMove:handleGridMouseMove,onMouseUp:handleGridMouseUp,onMouseLeave:handleGridMouseUp,onContextMenu:handleContextMenu,className:"absolute inset-0 cursor-crosshair"}),loopStartBeat!==null&&loopEndBeat!==null&&loopEndBeat>loopStartBeat&&React.createElement("div",{style:{left:`${loopStartBeat*pixelsPerBeat}px`,width:`${(loopEndBeat-loopStartBeat)*pixelsPerBeat}px`,top:0,bottom:0},className:"absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"})))),/* 4. CC / VELOCITY LANE (ĐẶT Ở ĐÁY) */showCC&&React.createElement("div",{style:{height:`${ccHeight}px`},className:"bg-[#161616] border-t border-zinc-900 flex shrink-0 relative"},React.createElement("div",{onMouseDown:e=>{e.preventDefault();const startY=e.clientY;const startH=ccHeight;const onMove=ev=>{setCcHeight(Math.max(40,startH+startY-ev.clientY));};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);},className:"absolute top-0 left-0 right-0 h-1.5 cursor-n-resize z-10 hover:bg-purple-600/30"}),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()),React.createElement("div",{ref:ccWrapperRef,className:"flex-1 overflow-x-hidden min-w-0"},React.createElement("div",{style:{width:`${viewWidth}px`,height:'100%'},className:"relative"},React.createElement("canvas",{ref:ccCanvasRef,onMouseDown:handleCCMouseDown,onMouseMove:handleCCMouseMove,onMouseUp:()=>{if(ccDragRef.current)ccDragRef.current.selectedMode=false;ccDragRef.current=null;},onMouseLeave:()=>{if(ccDragRef.current)ccDragRef.current.selectedMode=false;ccDragRef.current=null;},className:"absolute inset-0"}),loopStartBeat!==null&&loopEndBeat!==null&&loopEndBeat>loopStartBeat&&React.createElement("div",{style:{left:`${loopStartBeat*pixelsPerBeat}px`,width:`${(loopEndBeat-loopStartBeat)*pixelsPerBeat}px`,top:0,bottom:0},className:"absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"})))),/* 5. OVERLAY / CONTEXT MENU */scaleMenuPos&&renderScaleContextMenu());};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,instrument_source:t.instrument_source||(t.synth_engine?t.synth_engine.type:null),soundfont_id:t.soundfont_id||(t.synth_engine?t.synth_engine.soundfont_id:null),soundfont_bank:t.soundfont_bank!==undefined?t.soundfont_bank:t.synth_engine?t.synth_engine.soundfont_bank:null,soundfont_program:t.soundfont_program!==undefined?t.soundfont_program:t.synth_engine?t.synth_engine.soundfont_program:null,synth_engine:t.synth_engine||undefined,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,length_bars:item.duration_bars||4,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,length_bars:item.duration_bars||4,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,instrument_source:t.instrument_source||null,soundfont_id:t.soundfont_id||null,soundfont_bank:t.soundfont_bank!==null?t.soundfont_bank:undefined,soundfont_program:t.soundfont_program!==null?t.soundfont_program:undefined,synth_engine:t.synth_engine||undefined};});};const serializeProjectToSchema=(projectId,name,bpmVal,tracksList,subTabsList,sessionTabsList)=>{const secondsPerBar=60.0/parseFloat(bpmVal||120)*4;const mainTracks=serializeTracksList(tracksList,secondsPerBar);const sectionStore={};// Helper: compute length_bars from tracks content const computeLengthBars=(tracksArr,spb)=>{let maxSec=0;(tracksArr||[]).forEach(tr=>{(tr.clips||[]).forEach(c=>{const end=(c.startTime||0)+(c.buffer?c.buffer.duration/(c.speed||1.0):4);if(end>maxSec)maxSec=end;});(tr.midiItems||[]).forEach(m=>{const end=(m.startTime||0)+(m.duration||4);if(end>maxSec)maxSec=end;});});return Math.ceil((maxSec||4)/spb);};// 1. Populate from sessionTabsList (open tabs) (sessionTabsList||[]).forEach(st=>{const serializedTracks=serializeTracksList(st.tracks,secondsPerBar);sectionStore[st.sectionId]={id:st.sectionId,name:st.name,is_root:false,length_bars:computeLengthBars(st.tracks,secondsPerBar),auto_compute_length:true,tracks:serializedTracks,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:computeLengthBars(s.tracks,secondsPerBar),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:(()=>{let maxBar=16.0;(mainTracks||[]).forEach(t=>{(t.items||[]).forEach(item=>{const end=(item.start_bar||0)+(item.duration_bars||4);if(end>maxBar)maxBar=end;});});return maxBar;})(),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 ── @@ -179,7 +180,7 @@ const[instrumentDropdownTrackId,setInstrumentDropdownTrackId]=useState(null);con try{const catResp=(await window.SonicAPI.getSoundfontCatalog?.())??(await fetch('/api/v1/plugins/soundfonts/catalog').then(r=>r.json()));const catalog=catResp.full_catalog||{};// Merge presets into each soundfont entry data.soundfonts=(data.soundfonts||[]).map(sf=>{const sfId=sf.id.replace('sf_','');// Catalog keys are lowercased by soundfont_inspector const catEntry=catalog[sfId.toLowerCase()]||catalog[sfId];if(catEntry&&catEntry.instruments){return{...sf,presets:catEntry.instruments};}return sf;});}catch(e){console.warn('Catalog fetch error:',e);}setInstrumentSelectorData(data);}).catch(()=>{});}},[]);useEffect(()=>{if(!instrumentDropdownTrackId)return;const handler=e=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setInstrumentSearchQuery('');};document.addEventListener('click',handler);return()=>document.removeEventListener('click',handler);},[instrumentDropdownTrackId]);const GM_INSTRUMENTS=["Acoustic Grand Piano","Bright Acoustic Piano","Electric Grand Piano","Honky-tonk Piano","Electric Piano 1","Electric Piano 2","Harpsichord","Clavi","Celesta","Glockenspiel","Music Box","Vibraphone","Marimba","Xylophone","Tubular Bells","Dulcimer","Drawbar Organ","Percussive Organ","Rock Organ","Church Organ","Reed Organ","Accordion","Harmonica","Tango Accordion","Acoustic Guitar (nylon)","Acoustic Guitar (steel)","Electric Guitar (jazz)","Electric Guitar (clean)","Electric Guitar (muted)","Overdriven Guitar","Distortion Guitar","Guitar harmonics","Acoustic Bass","Electric Bass (finger)","Electric Bass (pick)","Fretless Bass","Slap Bass 1","Slap Bass 2","Synth Bass 1","Synth Bass 2","Violin","Viola","Cello","Contrabass","Tremolo Strings","Pizzicato Strings","Orchestral Harp","Timpani","String Ensemble 1","String Ensemble 2","Synth Strings 1","Synth Strings 2","Choir Aahs","Voice Oohs","Synth Voice","Orchestra Hit","Trumpet","Trombone","Tuba","Muted Trumpet","French Horn","Brass Section","Synth Brass 1","Synth Brass 2","Soprano Sax","Alto Sax","Tenor Sax","Baritone Sax","Oboe","English Horn","Bassoon","Clarinet","Piccolo","Flute","Recorder","Pan Flute","Blown Bottle","Shakuhachi","Whistle","Ocarina","Lead 1 (square)","Lead 2 (sawtooth)","Lead 3 (calliope)","Lead 4 (chiff)","Lead 5 (charang)","Lead 6 (voice)","Lead 7 (fifths)","Lead 8 (bass+lead)","Pad 1 (new age)","Pad 2 (warm)","Pad 3 (polysynth)","Pad 4 (choir)","Pad 5 (bowed)","Pad 6 (metallic)","Pad 7 (halo)","Pad 8 (sweep)","FX 1 (rain)","FX 2 (soundtrack)","FX 3 (crystal)","FX 4 (atmosphere)","FX 5 (brightness)","FX 6 (goblins)","FX 7 (echoes)","FX 8 (sci-fi)","Sitar","Banjo","Shamisen","Koto","Kalimba","Bag pipe","Fiddle","Shanai","Tinkle Bell","Agogo","Steel Drums","Woodblock","Taiko Drum","Melodic Tom","Synth Drum","Reverse Cymbal","Guitar Fret Noise","Breath Noise","Seashore","Bird Tweet","Telephone Ring","Helicopter","Applause","Gunshot"];const setTrackInstrumentWithProgram=(trackId,instrumentId,programNumber,displayName,bankNumber)=>{const isSfInstrument=instrumentId&&typeof instrumentId==='string'&&instrumentId.startsWith('sf_');const sfBank=bankNumber!==undefined?bankNumber:isSfInstrument?0:undefined;const sfProg=programNumber!==undefined?programNumber:undefined;updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;const hasInstrument=!!instrumentId;const instrType=isSfInstrument?'soundfont':hasInstrument?'vst3':'default';const synthEngine=hasInstrument?{type:instrType,plugin_id:instrumentId,soundfont_bank:sfBank!==undefined?sfBank:0,soundfont_program:sfProg!==undefined?sfProg:0,soundfont_id:isSfInstrument?instrumentId.replace('sf_',''):''}:undefined;return{...t,instrumentId,instrumentProgram:sfProg,instrumentName:displayName,soundfont_bank:sfBank,soundfont_program:sfProg,synth_engine:synthEngine,type:hasInstrument?'MIDI':t.type==='MIDI'?'audio':t.type};}));setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setInstrumentSelectorTrackId(null);setSynthCategory(null);setSelectedSoundFontId(null);// Trigger SpessaSynth load + program change when soundfont instrument selected -if(window.SonicSF&&window.SonicSF.selectInstrument&&instrumentId&&isSfInstrument){const sfId=instrumentId.replace('sf_','');const ch=sfBank===128?9:0;window.SonicSF.selectInstrument(ch,sfBank||0,sfProg||0,sfId);}setSubTabs(prev=>prev.map(s=>{if(s.trackId!==trackId)return s;return{...s,instrumentProgram:programNumber!==undefined?programNumber:undefined,instrumentName:displayName,instrumentId};}));const playingSub=subTabs.find(s=>s.trackId===trackId&&s.type==='PIANO_ROLL'&&s.isPlaying);if(playingSub){const pid=playingSub.id;const ctx=getAudioContext();const tNode=activeTrackNodesRef.current[trackId];if(tNode&&tNode.gainNode){tNode.gainNode.gain.setValueAtTime(tNode.gainNode.gain.value||1,ctx.currentTime);tNode.gainNode.gain.linearRampToValueAtTime(0.001,ctx.currentTime+0.04);}setTimeout(()=>{stopAllPlayback();if(tNode&&tNode.gainNode){const trackData=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===trackId):null;const volDb=trackData?trackData.volumeDb??0:0;const volLinear=volDb<=-50?0:Math.pow(10,volDb/20);tNode.gainNode.gain.setValueAtTime(0.001,ctx.currentTime);tNode.gainNode.gain.linearRampToValueAtTime(volLinear||0.8,ctx.currentTime+0.015);}const context=getAudioContext();const offset=playingSub.currentTime||0;startOffsetTimeRef.current=offset;startAudioTimeRef.current=context.currentTime;startBufferOffsetRef.current=offset*(playingSub.speed||1.0);schedulePianoRollMidi(playingSub,offset);startSubTabPlayback(playingSub,offset);setSubTabs(prev=>prev.map(s=>s.id===pid?{...s,isPlaying:true}:s));if(subTabsRef.current){subTabsRef.current=subTabsRef.current.map(s=>s.id===pid?{...s,isPlaying:true}:s);}animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);},60);}setTimeout(()=>lucide.createIcons(),50);};const setTrackInstrument=(trackId,instrumentId,displayName)=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);if(instrumentId&&instrumentId.startsWith('sf_')){// Set instrument on track immediately so Synth button shows the name +if(window.SonicSF&&window.SonicSF.selectInstrument&&instrumentId&&isSfInstrument){const sfId=instrumentId.replace('sf_','');var allTracks=activeTracksRef.current||tracks;var tidx=0;for(var i=0;i=9?tidx+1:tidx;if(ch>=16)ch=0;window.SonicSF.selectInstrument(ch,sfBank||0,sfProg||0,sfId);}setSubTabs(prev=>prev.map(s=>{if(s.trackId!==trackId)return s;return{...s,instrumentProgram:programNumber!==undefined?programNumber:undefined,instrumentName:displayName,instrumentId};}));const playingSub=subTabs.find(s=>s.trackId===trackId&&s.type==='PIANO_ROLL'&&s.isPlaying);if(playingSub){const pid=playingSub.id;const ctx=getAudioContext();const tNode=activeTrackNodesRef.current[trackId];if(tNode&&tNode.gainNode){tNode.gainNode.gain.setValueAtTime(tNode.gainNode.gain.value||1,ctx.currentTime);tNode.gainNode.gain.linearRampToValueAtTime(0.001,ctx.currentTime+0.04);}setTimeout(()=>{stopAllPlayback();if(tNode&&tNode.gainNode){const trackData=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===trackId):null;const volDb=trackData?trackData.volumeDb??0:0;const volLinear=volDb<=-50?0:Math.pow(10,volDb/20);tNode.gainNode.gain.setValueAtTime(0.001,ctx.currentTime);tNode.gainNode.gain.linearRampToValueAtTime(volLinear||0.8,ctx.currentTime+0.015);}const context=getAudioContext();const offset=playingSub.currentTime||0;startOffsetTimeRef.current=offset;startAudioTimeRef.current=context.currentTime;startBufferOffsetRef.current=offset*(playingSub.speed||1.0);schedulePianoRollMidi(playingSub,offset);startSubTabPlayback(playingSub,offset);setSubTabs(prev=>prev.map(s=>s.id===pid?{...s,isPlaying:true}:s));if(subTabsRef.current){subTabsRef.current=subTabsRef.current.map(s=>s.id===pid?{...s,isPlaying:true}:s);}animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);},60);}setTimeout(()=>lucide.createIcons(),50);};const setTrackInstrument=(trackId,instrumentId,displayName)=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);if(instrumentId&&instrumentId.startsWith('sf_')){// Set instrument on track immediately so Synth button shows the name updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;const sfClean=instrumentId.replace('sf_','');const synthEngine={type:'soundfont',plugin_id:instrumentId,soundfont_bank:0,soundfont_program:0,soundfont_id:sfClean};return{...t,instrumentId,instrumentProgram:undefined,instrumentName:displayName,synth_engine:synthEngine};}));setSelectedSoundFontId(instrumentId);setSynthCategory('soundfont');setInstrumentSelectorTrackId(trackId);setSfPresets(null);setSfPresetSearchQuery('');const sfIdParam=instrumentId.replace('sf_','');// Use cached presets from instrumentSelectorData const cachedSf=(instrumentSelectorData?.soundfonts||[]).find(s=>s.id===instrumentId||s.id===sfIdParam);if(cachedSf&&cachedSf.presets){setSfPresets(cachedSf.presets);}else{window.SonicAPI.listSoundfontInstruments(sfIdParam).then(data=>setSfPresets(data.presets||[])).catch(e=>{console.error('listSoundfontInstruments failed:',e);setSfPresets([]);});}}else{setTrackInstrumentWithProgram(trackId,instrumentId,undefined,displayName);}};const[activeTool,setActiveTool]=useState('select');// 'select' | 'grab' | 'razor' const[snapValue,setSnapValue]=useState('free');// 'free', '1', '1/2', '1/4', '1/8', '1/16', '1/32' @@ -301,7 +302,8 @@ const subMidiItems=subTrack.midiItems||[];if(window.SonicSF&&subMidiItems.length if(noteStartMain>=secStart&¬eStartMain{const context=getAudioContext();const track=tracks.find(t=>t.id===trackId);if(!track)return;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}]:[];// Get or create persistent gain & panner for real-time control const gainNode=getOrCreateTrackNode(track,context);const pannerNode=activeTrackNodesRef.current[track.id].pannerNode;clips.forEach(clip=>{if(!clip.buffer)return;const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(gainNode);gainNode.connect(pannerNode);const clipStart=clip.startTime||0;const clipDuration=clip.buffer.duration/(clip.speed||1.0);const clipEnd=clipStart+clipDuration;if(offsetTime0)&&window.SonicSF){const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;midiItems.forEach(item=>{const notes=item.notes||[];notes.forEach(note=>{const noteStartSec=item.startTime+(note.start_beat||0)*secondsPerBeat;const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteEndSec=noteStartSec+noteDurSec;if(offsetTime{if(st.type!=='PIANO_ROLL')return;const context=getAudioContext();const midiNotes=notesOverride||st.notes||[];const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;const startWallTime=context.currentTime;const track=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===st.trackId):null;const destNode=getOrCreateTrackNode(track,context);const instrumentProgram=track?track.instrumentProgram:undefined;const synthEngine=track?track.synth_engine:undefined;midiNotes.forEach(note=>{const noteOnBeat=note.start_beat||0;const noteDurBeat=note.duration_beats||1;const noteStartSec=noteOnBeat*secondsPerBeat;const noteDurSec=noteDurBeat*secondsPerBeat;if(noteStartSec+noteDurSec>offsetSeconds){const effectiveStart=Math.max(0,noteStartSec-offsetSeconds);const effectiveDur=noteDurSec-Math.max(0,offsetSeconds-noteStartSec);const scheduledTime=startWallTime+effectiveStart;const durMs=effectiveDur*1000;if(window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,scheduledTime,instrumentProgram,destNode,undefined,synthEngine);}}});};const handlePlayPause=()=>{if(activeTab!=='main'&&!activeTab.startsWith('session_')){// Sub-tab playback transport +const midiItems=track.midiItems||[];if((track.type==='MIDI'||midiItems.length>0)&&window.SonicSF){const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;midiItems.forEach(item=>{const notes=item.notes||[];notes.forEach(note=>{const noteStartSec=item.startTime+(note.start_beat||0)*secondsPerBeat;const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteEndSec=noteStartSec+noteDurSec;if(offsetTime{if(st.type!=='PIANO_ROLL')return;const context=getAudioContext();const midiNotes=notesOverride||st.notes||[];const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;const startWallTime=context.currentTime;const track=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===st.trackId):null;const destNode=getOrCreateTrackNode(track,context);const instrumentProgram=track?track.instrumentProgram:undefined;const synthEngine=track?track.synth_engine:undefined;var allTracks=activeTracksRef.current||[];var mainCh=0;for(var i=0;i=9?i+1:i;if(mainCh>=16)mainCh=0;break;}}midiNotes.forEach(note=>{const noteOnBeat=note.start_beat||0;const noteDurBeat=note.duration_beats||1;const noteStartSec=noteOnBeat*secondsPerBeat;const noteDurSec=noteDurBeat*secondsPerBeat;if(noteStartSec+noteDurSec>offsetSeconds){const effectiveStart=Math.max(0,noteStartSec-offsetSeconds);const effectiveDur=noteDurSec-Math.max(0,offsetSeconds-noteStartSec);const scheduledTime=startWallTime+effectiveStart;const durMs=effectiveDur*1000;if(window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,scheduledTime,instrumentProgram,destNode,mainCh,synthEngine);}}});// Play ghost notes from active tracks +var ghostLayers=st.ghostPlayLayers||[];ghostLayers.forEach(function(layer){var ghostTrack=allTracks.find(function(t){return t.id===layer.trackId;});var ghostProg=layer.instrumentProgram!==undefined?layer.instrumentProgram:ghostTrack?ghostTrack.instrumentProgram:undefined;var ghostSynth=layer.synthEngine||(ghostTrack?ghostTrack.synth_engine:undefined);var ghostDest=getOrCreateTrackNode(ghostTrack,context);var ghostCh=0;for(var i=0;i=9?i+1:i;if(ghostCh>=16)ghostCh=0;break;}}layer.notes.forEach(function(note){var beat=note.start_beat||0;var dur=note.duration_beats||1;var startSec=beat*secondsPerBeat;var durSec=dur*secondsPerBeat;if(startSec+durSec>offsetSeconds){var effStart=Math.max(0,startSec-offsetSeconds);var effDur=durSec-Math.max(0,offsetSeconds-startSec);var schedTime=startWallTime+effStart;var durMs=effDur*1000;if(window.SonicSF){window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,schedTime,ghostProg,ghostDest,ghostCh,ghostSynth);}}});});};const handlePlayPause=()=>{if(activeTab!=='main'&&!activeTab.startsWith('session_')){// Sub-tab playback transport const st=subTabs.find(s=>s.id===activeTab);if(!st||!st.buffer&&st.type!=='PIANO_ROLL')return;if(st.isPlaying){stopAllPlayback();setSubTabs(prev=>prev.map(s=>s.id===activeTab?{...s,isPlaying:false}:s));}else{stopAllPlayback();const startOffset=st.currentTime||0;if(st.type==='PIANO_ROLL'){schedulePianoRollMidi(st,startOffset);startSubTabPlayback(st,startOffset);}else{startSubTabPlayback(st,startOffset);}setSubTabs(prev=>prev.map(s=>s.id===activeTab?{...s,isPlaying:true,currentTime:startOffset}:s));}return;}const context=getAudioContext();if(isPlaying){stopAllPlayback();}else{startOffsetTimeRef.current=currentTime;startAudioTimeRef.current=context.currentTime;startTrackPlayback(currentTime);setIsPlaying(true);}};handlePlayPauseRef.current=handlePlayPause;const handlePause=()=>{if(isPlaying||subTabs.some(s=>s.isPlaying))stopAllPlayback();};const stopAllPlayback=()=>{activeSourcesRef.current.forEach(src=>{try{src.stop();}catch(e){}});activeSourcesRef.current=[];Object.values(activeTrackNodesRef.current).forEach(n=>{if(n.fxStopFn)n.fxStopFn();});activeTrackNodesRef.current={};if(window.SonicSF){window.SonicSF.stopAll();}setIsPlaying(false);setSubTabs(prev=>prev.map(s=>({...s,isPlaying:false})));};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');}};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?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];if(midiRec.tempTabId){const recordedNotes=midiRec.stop();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',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',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 ──