From 74dccaf54addd284b136d81a8851d6a27aa716b2 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Wed, 26 Aug 2026 11:48:13 +0700 Subject: [PATCH] feat(cc11): lane header CC dropdown + graph thread (2 fixed end nodes, preview-draw drag fix flicker) --- app/static/js/app.jsx | 179 ++++++++++++++++++++++--------- app/static/js/app.precompiled.js | 28 +++-- 2 files changed, 147 insertions(+), 60 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 4bbc673..45a973c 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -9631,6 +9631,66 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos const [showCC, setShowCC] = React.useState(true); const [ccHeight, setCcHeight] = React.useState(80); + // Ve CC11 lane (graph thread). Tach rieng de drag ve preview truc tiep + // (truoc day setNotes tung mousemove + doc notes tu closure stale -> node + // cu/moi xen ke -> flicker). previewNoteId != null: chi ve note do voi + // previewCurve thay cho expression_curve da commit. + const paintCC11Lane = (previewNoteId, previewCurve) => { + 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.setTransform(dpr, 0, 0, dpr, 0, 0); + 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(); + const yOf = v => h - ((v / 127) * (h - 20) + 10); + notes.forEach((note) => { + if (previewNoteId != null) { + if (note.id !== previewNoteId) return; + } else if (!selectedNoteIds.includes(note.id)) { + return; + } + const x0 = (renderBeatOffset + note.start_beat) * pixelsPerBeat; + const x1 = (renderBeatOffset + note.start_beat + (note.duration_beats || 1)) * pixelsPerBeat; + const curve = (previewNoteId != null && previewCurve ? previewCurve : (Array.isArray(note.expression_curve) ? note.expression_curve : [])).slice().sort((a, b) => a.position_ratio - b.position_ratio); + ctx.strokeStyle = '#34d399'; + ctx.lineWidth = 2; + ctx.beginPath(); + if (!curve.length) { + ctx.setLineDash([4, 4]); + ctx.moveTo(x0, yOf(127)); + ctx.lineTo(x1, yOf(127)); + ctx.setLineDash([]); + } else if (curve.length === 1) { + ctx.moveTo(x0, yOf(curve[0].value)); + ctx.lineTo(x1, yOf(curve[0].value)); + } else { + curve.forEach((p, i) => { + const x = x0 + p.position_ratio * (x1 - x0); + const y = yOf(p.value); + if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); + }); + } + ctx.stroke(); + curve.forEach((p) => { + const x = x0 + p.position_ratio * (x1 - x0); + ctx.fillStyle = '#a7f3d0'; + ctx.beginPath(); + ctx.arc(x, yOf(p.value), 4, 0, 2 * Math.PI); + ctx.fill(); + }); + }); + }; + React.useLayoutEffect(() => { const canvas = ccCanvasRef.current; if (!canvas) return; @@ -9652,41 +9712,15 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos ctx.stroke(); if (ccMode === 'cc11') { - // CC11 expression: curve anchors {position_ratio 0..1, value 0..127} - // ve cho note dang chon. Rong -> flat 127 (khong automation). - const yOf = v => h - ((v / 127) * (h - 20) + 10); - notes.forEach((note) => { - if (!selectedNoteIds.includes(note.id)) return; - const x0 = (renderBeatOffset + note.start_beat) * pixelsPerBeat; - const x1 = (renderBeatOffset + note.start_beat + (note.duration_beats || 1)) * pixelsPerBeat; - const curve = (Array.isArray(note.expression_curve) ? note.expression_curve : []).slice().sort((a, b) => a.position_ratio - b.position_ratio); - ctx.strokeStyle = '#34d399'; - ctx.lineWidth = 2; - ctx.beginPath(); - if (!curve.length) { - ctx.setLineDash([4, 4]); - ctx.moveTo(x0, yOf(127)); - ctx.lineTo(x1, yOf(127)); - ctx.setLineDash([]); - } else if (curve.length === 1) { - ctx.moveTo(x0, yOf(curve[0].value)); - ctx.lineTo(x1, yOf(curve[0].value)); - } else { - curve.forEach((p, i) => { - const x = x0 + p.position_ratio * (x1 - x0); - const y = yOf(p.value); - if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); - }); - } - ctx.stroke(); - curve.forEach((p) => { - const x = x0 + p.position_ratio * (x1 - x0); - ctx.fillStyle = '#a7f3d0'; - ctx.beginPath(); - ctx.arc(x, yOf(p.value), 4, 0, 2 * Math.PI); - ctx.fill(); - }); - }); + // CC11 expression: graph thread cho note dang chon; khi dang drag ve + // preview tu ccDragRef (khong phu thuoc state da commit). + const drag = ccDragRef.current; + if (drag && drag.mode === 'cc11' && drag.previewCurve) { + paintCC11Lane(drag.noteId, drag.previewCurve); + } else { + paintCC11Lane(); + } + return; } else { notes.forEach((note) => { const x = (renderBeatOffset + note.start_beat) * pixelsPerBeat; @@ -10485,16 +10519,21 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos const beat = x / pixelsPerBeat - renderBeatOffset; const noteIdx = findCC11NoteAtBeat(beat); if (noteIdx === -1) { ccDragRef.current = null; return; } - const note = notes[noteIdx]; + const note = notesRef.current[noteIdx]; + if (!note) return; // Auto-select note duoc click: draw branch chi ve curve cho note duoc chon // (selectedNoteIds) -> khong select -> anchor them nhung vo hinh (bug: khong ve duoc CC11) - if (note && note.id && !selectedNoteIds.includes(note.id)) { + if (note.id && !selectedNoteIds.includes(note.id)) { setSelectedNoteIds(prev => [...prev, note.id]); } const dur = note.duration_beats || 1; const ratio = Math.max(0, Math.min(1, (beat - note.start_beat) / dur)); const val = Math.max(0, Math.min(127, Math.round(((h - y) / h) * 127))); const curve = getCC11Curve(note).slice(); + // Curve rong -> khoi tao 2 node co dinh dau/cuoi note (graph thread) + if (!curve.length) { + curve.push({ position_ratio: 0, value: 127 }, { position_ratio: 1, value: 127 }); + } const x0 = (renderBeatOffset + note.start_beat) * pixelsPerBeat; const x1 = x0 + dur * pixelsPerBeat; const yOf = v => h - ((v / 127) * (h - 20) + 10); @@ -10509,12 +10548,15 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos if (dup !== -1) { curve[dup] = { position_ratio: ratio, value: val }; anchorIdx = dup; } else { curve.push({ position_ratio: ratio, value: val }); anchorIdx = curve.length - 1; } } else { - curve[anchorIdx] = { position_ratio: ratio, value: val }; + const cur = curve[anchorIdx]; + // Node dau/cuoi co dinh: giu ratio 0/1, chi doi value + curve[anchorIdx] = (cur.position_ratio <= 0.001 || cur.position_ratio >= 0.999) ? { position_ratio: cur.position_ratio, value: val } : { position_ratio: ratio, value: val }; } - const updatedNotes = notes.map((n, i) => (i === noteIdx ? { ...n, expression_curve: curve } : n)); - setNotes(updatedNotes); - if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes); - ccDragRef.current = { mode: 'cc11', active: true, noteIdx, anchorIdx, noteStart: note.start_beat, noteDur: dur }; + // Khong setNotes ngay (truoc day setNotes moi mousemove + doc notes tu + // closure stale -> node cu/moi xen ke -> flicker): ve preview truc tiep, + // commit vao notes chi khi mouseup/mouseleave. + ccDragRef.current = { mode: 'cc11', active: true, noteIdx, noteId: note.id, anchorIdx, noteStart: note.start_beat, noteDur: dur, previewCurve: curve }; + paintCC11Lane(note.id, curve); }; const handleCC11ContextMenu = (e) => { if (ccMode !== 'cc11') return; @@ -10542,11 +10584,26 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos if (d < delDist) { delDist = d; delIdx = i; } }); if (delIdx === -1 || delDist > 8) return; + // Node dau/cuoi co dinh (graph thread) — khong cho xoa + if (curve[delIdx].position_ratio <= 0.001 || curve[delIdx].position_ratio >= 0.999) return; curve.splice(delIdx, 1); const updatedNotes = notes.map((n, i) => (i === noteIdx ? { ...n, expression_curve: curve } : n)); setNotes(updatedNotes); if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes); }; + // Commit CC11 preview khi mouseup/mouseleave — truoc day drag setNotes moi + // mousemove (flicker); gio chi cap nhat state 1 lan khi ket thuc drag. + const commitCC11Drag = () => { + const drag = ccDragRef.current; + if (!drag) return; + if (drag.mode === 'cc11' && drag.previewCurve) { + const curNotes = notesRef.current; + const updatedNotes = curNotes.map((n, i) => (i === drag.noteIdx ? { ...n, expression_curve: drag.previewCurve } : n)); + setNotes(updatedNotes); + if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes); + } + ccDragRef.current = null; + }; const handleCCMouseDown = (e) => { if (ccMode === 'cc11') { handleCC11MouseDown(e); return; } const canvas = ccCanvasRef.current; @@ -10612,17 +10669,30 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos const y = e.clientY - rect.top; const h = rect.height; const beat = x / pixelsPerBeat - renderBeatOffset; - const note = notes[drag.noteIdx]; + const note = notesRef.current[drag.noteIdx]; if (!note) return; const dur = drag.noteDur || note.duration_beats || 1; const ratio = Math.max(0, Math.min(1, (beat - drag.noteStart) / dur)); const val = Math.max(0, Math.min(127, Math.round(((h - y) / h) * 127))); - const curve = getCC11Curve(note).slice(); + const curve = (drag.previewCurve || getCC11Curve(note)).slice(); const idx = Math.min(drag.anchorIdx, curve.length - 1); - curve[idx] = { position_ratio: ratio, value: val }; - const updatedNotes = notes.map((n, i) => (i === drag.noteIdx ? { ...n, expression_curve: curve } : n)); - setNotes(updatedNotes); - if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes); + const cur = curve[idx] || { position_ratio: ratio, value: val }; + if (cur.position_ratio <= 0.001 || cur.position_ratio >= 0.999) { + // Node dau/cuoi co dinh: chi doi value, giu ratio 0/1 + curve[idx] = { position_ratio: cur.position_ratio, value: val }; + } else { + // Node giua: doi ca ratio+value, clamp giua 2 node lan can + let minR = 0, maxR = 1; + for (let i = 0; i < curve.length; i++) { + if (i === idx) continue; + const r = curve[i].position_ratio; + if (r < cur.position_ratio && r > minR) minR = r; + if (r > cur.position_ratio && r < maxR) maxR = r; + } + curve[idx] = { position_ratio: Math.min(maxR, Math.max(minR, ratio)), value: val }; + } + drag.previewCurve = curve; + paintCC11Lane(drag.noteId, curve); return; } const canvas = ccCanvasRef.current; @@ -11657,9 +11727,12 @@ const beatSec = 60.0 / (parseInt(bpm) || 120); 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-[120px] shrink-0" - }), 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", { + }), React.createElement("select", { + value: ccMode, + onChange: e => setCcMode(e.target.value), + title: "CC control", + className: "w-[60px] bg-[#222222] border-r border-zinc-900 text-[10px] text-zinc-200 font-mono shrink-0 font-bold text-center cursor-pointer outline-none" + }, React.createElement("option", { value: "velocity" }, "Vel"), React.createElement("option", { value: "sustain" }, "Sus"), React.createElement("option", { value: "modulation" }, "Mod"), React.createElement("option", { value: "pitch_bend" }, "Bend"), React.createElement("option", { value: "pan" }, "Pan"), React.createElement("option", { value: "cc11" }, "Expr")), React.createElement("div", { ref: ccWrapperRef, className: "flex-1 overflow-x-hidden min-w-0" }, React.createElement("div", { @@ -11673,8 +11746,8 @@ const beatSec = 60.0 / (parseInt(bpm) || 120); onMouseDown: handleCCMouseDown, onMouseMove: handleCCMouseMove, onContextMenu: handleCC11ContextMenu, - onMouseUp: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; }, - onMouseLeave: () => { if (ccDragRef.current) ccDragRef.current.selectedMode = false; ccDragRef.current = null; }, + onMouseUp: commitCC11Drag, + onMouseLeave: commitCC11Drag, className: "absolute inset-0" }), loopStartBeat !== null && loopEndBeat !== null && loopEndBeat > loopStartBeat && React.createElement("div", { style: { diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 85d6b99..973913f 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -740,9 +740,13 @@ if(recordingState==='RECORDING'&&recTempMidiNotes&&recTempMidiNotes.length>0){re 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){const phBeat=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,snapValue,rollZoom,selectedNoteIds,selectionMarquee,st.currentTime,bpm,viewWidth,viewBeats,recordingState,recTempMidiNotes,showGhostNotes,sessionSyncMode,ghostLayers,renderBeatOffset,renderTick,activeTracks,focusItemId]);// showCC/ccHeight khai báo TRƯỚC useLayoutEffect vẽ CC (deps tham chiếu — // khai báo sau → TDZ error — user 08:50) -const[showCC,setShowCC]=React.useState(true);const[ccHeight,setCcHeight]=React.useState(80);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();if(ccMode==='cc11'){// CC11 expression: curve anchors {position_ratio 0..1, value 0..127} -// ve cho note dang chon. Rong -> flat 127 (khong automation). -const yOf=v=>h-(v/127*(h-20)+10);notes.forEach(note=>{if(!selectedNoteIds.includes(note.id))return;const x0=(renderBeatOffset+note.start_beat)*pixelsPerBeat;const x1=(renderBeatOffset+note.start_beat+(note.duration_beats||1))*pixelsPerBeat;const curve=(Array.isArray(note.expression_curve)?note.expression_curve:[]).slice().sort((a,b)=>a.position_ratio-b.position_ratio);ctx.strokeStyle='#34d399';ctx.lineWidth=2;ctx.beginPath();if(!curve.length){ctx.setLineDash([4,4]);ctx.moveTo(x0,yOf(127));ctx.lineTo(x1,yOf(127));ctx.setLineDash([]);}else if(curve.length===1){ctx.moveTo(x0,yOf(curve[0].value));ctx.lineTo(x1,yOf(curve[0].value));}else{curve.forEach((p,i)=>{const x=x0+p.position_ratio*(x1-x0);const y=yOf(p.value);if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);});}ctx.stroke();curve.forEach(p=>{const x=x0+p.position_ratio*(x1-x0);ctx.fillStyle='#a7f3d0';ctx.beginPath();ctx.arc(x,yOf(p.value),4,0,2*Math.PI);ctx.fill();});});}else{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,ccHeight,showCC]);// Follow playhead: khi PLAY — playhead luôn ở GIỮA view, notes trôi sang +const[showCC,setShowCC]=React.useState(true);const[ccHeight,setCcHeight]=React.useState(80);// Ve CC11 lane (graph thread). Tach rieng de drag ve preview truc tiep +// (truoc day setNotes tung mousemove + doc notes tu closure stale -> node +// cu/moi xen ke -> flicker). previewNoteId != null: chi ve note do voi +// previewCurve thay cho expression_curve da commit. +const paintCC11Lane=(previewNoteId,previewCurve)=>{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.setTransform(dpr,0,0,dpr,0,0);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();const yOf=v=>h-(v/127*(h-20)+10);notes.forEach(note=>{if(previewNoteId!=null){if(note.id!==previewNoteId)return;}else if(!selectedNoteIds.includes(note.id)){return;}const x0=(renderBeatOffset+note.start_beat)*pixelsPerBeat;const x1=(renderBeatOffset+note.start_beat+(note.duration_beats||1))*pixelsPerBeat;const curve=(previewNoteId!=null&&previewCurve?previewCurve:Array.isArray(note.expression_curve)?note.expression_curve:[]).slice().sort((a,b)=>a.position_ratio-b.position_ratio);ctx.strokeStyle='#34d399';ctx.lineWidth=2;ctx.beginPath();if(!curve.length){ctx.setLineDash([4,4]);ctx.moveTo(x0,yOf(127));ctx.lineTo(x1,yOf(127));ctx.setLineDash([]);}else if(curve.length===1){ctx.moveTo(x0,yOf(curve[0].value));ctx.lineTo(x1,yOf(curve[0].value));}else{curve.forEach((p,i)=>{const x=x0+p.position_ratio*(x1-x0);const y=yOf(p.value);if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);});}ctx.stroke();curve.forEach(p=>{const x=x0+p.position_ratio*(x1-x0);ctx.fillStyle='#a7f3d0';ctx.beginPath();ctx.arc(x,yOf(p.value),4,0,2*Math.PI);ctx.fill();});});};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();if(ccMode==='cc11'){// CC11 expression: graph thread cho note dang chon; khi dang drag ve +// preview tu ccDragRef (khong phu thuoc state da commit). +const drag=ccDragRef.current;if(drag&&drag.mode==='cc11'&&drag.previewCurve){paintCC11Lane(drag.noteId,drag.previewCurve);}else{paintCC11Lane();}return;}else{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,ccHeight,showCC]);// Follow playhead: khi PLAY — playhead luôn ở GIỮA view, notes trôi sang // trái (scroll theo st.currentTime); khi STOP — scroll về đầu (playhead ở // vị trí đầu piano roll) — user 09:40. Dùng CẢ isPlaying (main) LẪN // st.isPlaying (piano roll play — main isPlaying=false khi tab play — bug @@ -786,10 +790,20 @@ const prKeyStateRef=React.useRef({shift:false,ctrl:false});const prMouseInRef=Re // user 08:25: ctrl+draw qua vị trí → vẽ lại TẤT CẢ velocity cùng vị trí // (trước đây findCCNoteIndex chỉ trả 1 note → chord khó draw). const findCCNoteIndicesAtBeat=b=>{const snapped=getSnapBeat(b,snapValue);const out=[];notes.forEach((n,idx)=>{if(Math.abs(n.start_beat-snapped)<0.01)out.push(idx);});return out;};// ── CC11 EXPRESSION CURVE (piano roll lane) ── -const getCC11Curve=n=>Array.isArray(n&&n.expression_curve)?n.expression_curve:[];const cc11ValueAt=(curve,ratio)=>{if(!curve||!curve.length)return 127;if(curve.length===1)return curve[0].value;const pts=curve.slice().sort((a,b)=>a.position_ratio-b.position_ratio);if(ratio<=pts[0].position_ratio)return pts[0].value;if(ratio>=pts[pts.length-1].position_ratio)return pts[pts.length-1].value;for(let i=1;i{const snapped=getSnapBeat(b,snapValue);let best=-1,bestDist=Infinity;notes.forEach((n,idx)=>{if(snapped>=n.start_beat&&snapped<=n.start_beat+(n.duration_beats||1)){const d=Math.min(Math.abs(snapped-n.start_beat),Math.abs(snapped-(n.start_beat+(n.duration_beats||1))));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-renderBeatOffset;const noteIdx=findCC11NoteAtBeat(beat);if(noteIdx===-1){ccDragRef.current=null;return;}const note=notes[noteIdx];// Auto-select note duoc click: draw branch chi ve curve cho note duoc chon +const getCC11Curve=n=>Array.isArray(n&&n.expression_curve)?n.expression_curve:[];const cc11ValueAt=(curve,ratio)=>{if(!curve||!curve.length)return 127;if(curve.length===1)return curve[0].value;const pts=curve.slice().sort((a,b)=>a.position_ratio-b.position_ratio);if(ratio<=pts[0].position_ratio)return pts[0].value;if(ratio>=pts[pts.length-1].position_ratio)return pts[pts.length-1].value;for(let i=1;i{const snapped=getSnapBeat(b,snapValue);let best=-1,bestDist=Infinity;notes.forEach((n,idx)=>{if(snapped>=n.start_beat&&snapped<=n.start_beat+(n.duration_beats||1)){const d=Math.min(Math.abs(snapped-n.start_beat),Math.abs(snapped-(n.start_beat+(n.duration_beats||1))));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-renderBeatOffset;const noteIdx=findCC11NoteAtBeat(beat);if(noteIdx===-1){ccDragRef.current=null;return;}const note=notesRef.current[noteIdx];if(!note)return;// Auto-select note duoc click: draw branch chi ve curve cho note duoc chon // (selectedNoteIds) -> khong select -> anchor them nhung vo hinh (bug: khong ve duoc CC11) -if(note&¬e.id&&!selectedNoteIds.includes(note.id)){setSelectedNoteIds(prev=>[...prev,note.id]);}const dur=note.duration_beats||1;const ratio=Math.max(0,Math.min(1,(beat-note.start_beat)/dur));const val=Math.max(0,Math.min(127,Math.round((h-y)/h*127)));const curve=getCC11Curve(note).slice();const x0=(renderBeatOffset+note.start_beat)*pixelsPerBeat;const x1=x0+dur*pixelsPerBeat;const yOf=v=>h-(v/127*(h-20)+10);let anchorIdx=-1;curve.forEach((p,i)=>{const ax=x0+p.position_ratio*(x1-x0);const ay=yOf(p.value);if(Math.hypot(ax-x,ay-y)<=8)anchorIdx=i;});if(anchorIdx===-1){const dup=curve.findIndex(p=>Math.abs(p.position_ratio-ratio)<1e-6);if(dup!==-1){curve[dup]={position_ratio:ratio,value:val};anchorIdx=dup;}else{curve.push({position_ratio:ratio,value:val});anchorIdx=curve.length-1;}}else{curve[anchorIdx]={position_ratio:ratio,value:val};}const updatedNotes=notes.map((n,i)=>i===noteIdx?{...n,expression_curve:curve}:n);setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);ccDragRef.current={mode:'cc11',active:true,noteIdx,anchorIdx,noteStart:note.start_beat,noteDur:dur};};const handleCC11ContextMenu=e=>{if(ccMode!=='cc11')return;const canvas=ccCanvasRef.current;if(!canvas)return;e.preventDefault();const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat-renderBeatOffset;const noteIdx=findCC11NoteAtBeat(beat);if(noteIdx===-1)return;const note=notes[noteIdx];const dur=note.duration_beats||1;const x0=(renderBeatOffset+note.start_beat)*pixelsPerBeat;const x1=x0+dur*pixelsPerBeat;const yOf=v=>h-(v/127*(h-20)+10);const curve=getCC11Curve(note).slice();let delIdx=-1,delDist=Infinity;curve.forEach((p,i)=>{const ax=x0+p.position_ratio*(x1-x0);const ay=yOf(p.value);const d=Math.hypot(ax-x,ay-y);if(d8)return;curve.splice(delIdx,1);const updatedNotes=notes.map((n,i)=>i===noteIdx?{...n,expression_curve:curve}:n);setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);};const handleCCMouseDown=e=>{if(ccMode==='cc11'){handleCC11MouseDown(e);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-renderBeatOffset;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{// Không chọn notes: vẽ TẤT CẢ notes cùng beat (chord — user 08:25) -const idxs=findCCNoteIndicesAtBeat(beat);ccDragRef.current={active:true,lastBeat:beat,lastPainted:idxs};}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;if(ccDragRef.current.mode==='cc11'){const drag=ccDragRef.current;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-renderBeatOffset;const note=notes[drag.noteIdx];if(!note)return;const dur=drag.noteDur||note.duration_beats||1;const ratio=Math.max(0,Math.min(1,(beat-drag.noteStart)/dur));const val=Math.max(0,Math.min(127,Math.round((h-y)/h*127)));const curve=getCC11Curve(note).slice();const idx=Math.min(drag.anchorIdx,curve.length-1);curve[idx]={position_ratio:ratio,value:val};const updatedNotes=notes.map((n,i)=>i===drag.noteIdx?{...n,expression_curve:curve}:n);setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);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-renderBeatOffset;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;}// Vẽ TẤT CẢ notes cùng beat (chord — user 08:25) — trước đây chỉ 1 note +if(note.id&&!selectedNoteIds.includes(note.id)){setSelectedNoteIds(prev=>[...prev,note.id]);}const dur=note.duration_beats||1;const ratio=Math.max(0,Math.min(1,(beat-note.start_beat)/dur));const val=Math.max(0,Math.min(127,Math.round((h-y)/h*127)));const curve=getCC11Curve(note).slice();// Curve rong -> khoi tao 2 node co dinh dau/cuoi note (graph thread) +if(!curve.length){curve.push({position_ratio:0,value:127},{position_ratio:1,value:127});}const x0=(renderBeatOffset+note.start_beat)*pixelsPerBeat;const x1=x0+dur*pixelsPerBeat;const yOf=v=>h-(v/127*(h-20)+10);let anchorIdx=-1;curve.forEach((p,i)=>{const ax=x0+p.position_ratio*(x1-x0);const ay=yOf(p.value);if(Math.hypot(ax-x,ay-y)<=8)anchorIdx=i;});if(anchorIdx===-1){const dup=curve.findIndex(p=>Math.abs(p.position_ratio-ratio)<1e-6);if(dup!==-1){curve[dup]={position_ratio:ratio,value:val};anchorIdx=dup;}else{curve.push({position_ratio:ratio,value:val});anchorIdx=curve.length-1;}}else{const cur=curve[anchorIdx];// Node dau/cuoi co dinh: giu ratio 0/1, chi doi value +curve[anchorIdx]=cur.position_ratio<=0.001||cur.position_ratio>=0.999?{position_ratio:cur.position_ratio,value:val}:{position_ratio:ratio,value:val};}// Khong setNotes ngay (truoc day setNotes moi mousemove + doc notes tu +// closure stale -> node cu/moi xen ke -> flicker): ve preview truc tiep, +// commit vao notes chi khi mouseup/mouseleave. +ccDragRef.current={mode:'cc11',active:true,noteIdx,noteId:note.id,anchorIdx,noteStart:note.start_beat,noteDur:dur,previewCurve:curve};paintCC11Lane(note.id,curve);};const handleCC11ContextMenu=e=>{if(ccMode!=='cc11')return;const canvas=ccCanvasRef.current;if(!canvas)return;e.preventDefault();const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat-renderBeatOffset;const noteIdx=findCC11NoteAtBeat(beat);if(noteIdx===-1)return;const note=notes[noteIdx];const dur=note.duration_beats||1;const x0=(renderBeatOffset+note.start_beat)*pixelsPerBeat;const x1=x0+dur*pixelsPerBeat;const yOf=v=>h-(v/127*(h-20)+10);const curve=getCC11Curve(note).slice();let delIdx=-1,delDist=Infinity;curve.forEach((p,i)=>{const ax=x0+p.position_ratio*(x1-x0);const ay=yOf(p.value);const d=Math.hypot(ax-x,ay-y);if(d8)return;// Node dau/cuoi co dinh (graph thread) — khong cho xoa +if(curve[delIdx].position_ratio<=0.001||curve[delIdx].position_ratio>=0.999)return;curve.splice(delIdx,1);const updatedNotes=notes.map((n,i)=>i===noteIdx?{...n,expression_curve:curve}:n);setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);};// Commit CC11 preview khi mouseup/mouseleave — truoc day drag setNotes moi +// mousemove (flicker); gio chi cap nhat state 1 lan khi ket thuc drag. +const commitCC11Drag=()=>{const drag=ccDragRef.current;if(!drag)return;if(drag.mode==='cc11'&&drag.previewCurve){const curNotes=notesRef.current;const updatedNotes=curNotes.map((n,i)=>i===drag.noteIdx?{...n,expression_curve:drag.previewCurve}:n);setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}ccDragRef.current=null;};const handleCCMouseDown=e=>{if(ccMode==='cc11'){handleCC11MouseDown(e);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-renderBeatOffset;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{// Không chọn notes: vẽ TẤT CẢ notes cùng beat (chord — user 08:25) +const idxs=findCCNoteIndicesAtBeat(beat);ccDragRef.current={active:true,lastBeat:beat,lastPainted:idxs};}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;if(ccDragRef.current.mode==='cc11'){const drag=ccDragRef.current;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-renderBeatOffset;const note=notesRef.current[drag.noteIdx];if(!note)return;const dur=drag.noteDur||note.duration_beats||1;const ratio=Math.max(0,Math.min(1,(beat-drag.noteStart)/dur));const val=Math.max(0,Math.min(127,Math.round((h-y)/h*127)));const curve=(drag.previewCurve||getCC11Curve(note)).slice();const idx=Math.min(drag.anchorIdx,curve.length-1);const cur=curve[idx]||{position_ratio:ratio,value:val};if(cur.position_ratio<=0.001||cur.position_ratio>=0.999){// Node dau/cuoi co dinh: chi doi value, giu ratio 0/1 +curve[idx]={position_ratio:cur.position_ratio,value:val};}else{// Node giua: doi ca ratio+value, clamp giua 2 node lan can +let minR=0,maxR=1;for(let i=0;iminR)minR=r;if(r>cur.position_ratio&&r0){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;}// Vẽ TẤT CẢ notes cùng beat (chord — user 08:25) — trước đây chỉ 1 note const candidateIdxs=findCCNoteIndicesAtBeat(beat);const unpainted=candidateIdxs.filter(ci=>!painted.includes(ci));if(unpainted.length>0){const updatedNotes=notes.map((n,i)=>{if(!unpainted.includes(i))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,...unpainted];}};const renderKeybed=()=>{var kbTrk=activeTracks.find(function(t){return t.id===st.trackId;});var kbCtx=resolveTrackInstrumentCtx(kbTrk,activeTracks);ensureSonicInstrument(kbCtx);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{// Đồng bộ mastering + routing SF NGAY trước khi preview — âm // keybed PHẢI qua mastering FX của main out khi chain đang bật // (trước đây phải bật/tắt power mới có tác dụng). @@ -816,7 +830,7 @@ const se=st.synth_engine||{};const instrumentId=se.plugin_id||st.instrumentId||n // giữ Shift → select/unselect; giữ Ctrl → fast copy if(window.__setPrHint){window.__setPrHint(prKeyStateRef.current.ctrl?"Ctrl+drag: fast copy notes | Ctrl+click empty: marquee chọn notes":prKeyStateRef.current.shift?"Shift+Click: select/unselect notes | Shift+Click note: thêm/bớt vào nhóm chọn":"Scroll: Up/Down | Drag: Draw notes");}},onMouseLeave:function(){prMouseInRef.current=false;if(window.__setPrHint)window.__setPrHint(null);}},/* Track column */React.createElement("div",{className:"w-[120px] bg-[#16161a] border-r border-zinc-800 shrink-0 flex flex-col z-10",style:{height:KeybedPixelHeight+'px'}},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&&activePlayTrackIds.indexOf(m._trackId)!==-1;els.push(React.createElement("div",{key:m._trackId,className:"flex items-center gap-0.5 mx-1 my-[2px]"},React.createElement("button",{onClick:function(){// Click nút tên track → ACTIVE ghost notes của track đó thành MAIN // notes để chỉnh sửa (user 09:40) -handleSwitchMidiItem(m.id);},className:"flex items-center justify-center flex-1 h-[26px] min-w-0 border border-zinc-600 rounded-md cursor-pointer outline-none "+(isActive?'bg-yellow-600 text-black font-bold':'bg-zinc-700 text-zinc-300 hover:bg-zinc-600')},React.createElement("span",{className:"text-[13px] leading-none font-sans truncate px-1",title:track?track.name:m._trackName},track?track.name:m._trackName)),React.createElement("button",{onClick:function(e){e.stopPropagation();var prevList=activePlayTrackIds||[];var nextList=prevList.indexOf(m._trackId)!==-1?prevList.filter(function(id){return id!==m._trackId;}):prevList.concat([m._trackId]);setActivePlayTrackIds(nextList);if(onRealtimePlay)onRealtimePlay(nextList);},title:isPlayOn?"Unmute — ghost track play cùng main notes":"Mute (mặc định) — click để play ghost cùng main",className:"w-6 h-[26px] shrink-0 border border-zinc-600 rounded-md cursor-pointer text-[11px] font-bold outline-none flex items-center justify-center "+(isPlayOn?'bg-green-700 text-white':'bg-zinc-800 text-zinc-500 hover:text-zinc-300')},isPlayOn?"\u266A":"M")));});return els;}():null),React.createElement("div",{className:"w-[60px] shrink-0 flex flex-col border-r border-zinc-900 overflow-y-auto",ref:keybedRef,onScroll:handleKeybedScroll,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-[120px] shrink-0"}),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,onContextMenu:handleCC11ContextMenu,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 */arpModal&&React.createElement("div",{className:"absolute inset-0 z-40 flex items-center justify-center bg-black/60",onMouseDown:e=>e.stopPropagation(),onClick:e=>e.stopPropagation()},React.createElement("div",{className:"bg-zinc-900 border border-zinc-700 rounded-lg p-4 w-72 shadow-2xl"},React.createElement("div",{className:"text-sm font-bold text-cyan-300 mb-3"},"Arpeggiate"),React.createElement("div",{className:"grid grid-cols-2 gap-2 text-xs mb-3"},React.createElement("label",{className:"text-zinc-500"},"Pattern"),React.createElement("select",{value:arpModal.pattern,onChange:e=>setArpModal({...arpModal,pattern:e.target.value}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5"},['UP','DOWN','UP-DOWN','RANDOM','CHORD'].map(p=>React.createElement("option",{key:p,value:p},p))),React.createElement("label",{className:"text-zinc-500"},"Rate"),React.createElement("select",{value:arpModal.rate,onChange:e=>setArpModal({...arpModal,rate:e.target.value}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5"},['1/4','1/8','1/16','1/32'].map(r=>React.createElement("option",{key:r,value:r},r))),React.createElement("label",{className:"text-zinc-500"},"Octaves"),React.createElement("input",{type:"number",min:1,max:4,value:arpModal.octaves,onChange:e=>setArpModal({...arpModal,octaves:parseInt(e.target.value)||1}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-14"}),React.createElement("label",{className:"text-zinc-500"},"Gate %"),React.createElement("input",{type:"number",min:10,max:200,value:arpModal.gate,onChange:e=>setArpModal({...arpModal,gate:parseInt(e.target.value)||80}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-14"})),React.createElement("div",{className:"flex gap-1 mb-3"},React.createElement("label",{className:"flex items-center gap-1 text-[10px] text-zinc-400"},React.createElement("input",{type:"checkbox",checked:!!arpModal.triplet,onChange:e=>setArpModal({...arpModal,triplet:e.target.checked})}),"Triplet"),React.createElement("label",{className:"flex items-center gap-1 text-[10px] text-zinc-400"},React.createElement("input",{type:"checkbox",checked:!!arpModal.dotted,onChange:e=>setArpModal({...arpModal,dotted:e.target.checked})}),"Dotted")),React.createElement("div",{className:"flex gap-2"},React.createElement("button",{onClick:()=>applyArpeggiate(arpModal),className:"flex-1 px-3 py-1.5 rounded text-xs font-bold bg-cyan-700 hover:bg-cyan-600 text-white"},"Apply"),React.createElement("button",{onClick:()=>setArpModal(null),className:"px-3 py-1.5 rounded text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-300"},"Cancel")))),strumModal&&React.createElement("div",{className:"absolute inset-0 z-40 flex items-center justify-center bg-black/60",onMouseDown:e=>e.stopPropagation(),onClick:e=>e.stopPropagation()},React.createElement("div",{className:"bg-zinc-900 border border-zinc-700 rounded-lg p-4 w-72 shadow-2xl"},React.createElement("div",{className:"text-sm font-bold text-cyan-300 mb-3"},"Strum"),React.createElement("div",{className:"grid grid-cols-2 gap-2 text-xs mb-3"},React.createElement("label",{className:"text-zinc-500"},"Strum ms"),React.createElement("input",{type:"number",min:0,max:120,value:strumModal.ms,onChange:e=>setStrumModal({...strumModal,ms:parseInt(e.target.value)||0}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16"}),React.createElement("label",{className:"text-zinc-500"},"Direction"),React.createElement("select",{value:strumModal.direction,onChange:e=>setStrumModal({...strumModal,direction:e.target.value}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5"},['DOWN','UP','ALTERNATE'].map(d=>React.createElement("option",{key:d,value:d},d)))),React.createElement("div",{className:"flex gap-2"},React.createElement("button",{onClick:()=>applyStrum(strumModal),className:"flex-1 px-3 py-1.5 rounded text-xs font-bold bg-cyan-700 hover:bg-cyan-600 text-white"},"Apply"),React.createElement("button",{onClick:()=>setStrumModal(null),className:"px-3 py-1.5 rounded text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-300"},"Cancel")))),humanizeModal&&React.createElement("div",{className:"absolute inset-0 z-40 flex items-center justify-center bg-black/60",onMouseDown:e=>e.stopPropagation(),onClick:e=>e.stopPropagation()},React.createElement("div",{className:"bg-zinc-900 border border-zinc-700 rounded-lg p-4 w-72 shadow-2xl"},React.createElement("div",{className:"text-sm font-bold text-cyan-300 mb-3"},"Humanize"),React.createElement("div",{className:"grid grid-cols-2 gap-2 text-xs mb-3"},React.createElement("label",{className:"text-zinc-500"},"Timing ±ms"),React.createElement("input",{type:"number",min:0,max:30,value:humanizeModal.timingMs,onChange:e=>setHumanizeModal({...humanizeModal,timingMs:parseInt(e.target.value)||0}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16"}),React.createElement("label",{className:"text-zinc-500"},"Vel ±(0-127)"),React.createElement("input",{type:"number",min:0,max:20,value:humanizeModal.velRange,onChange:e=>setHumanizeModal({...humanizeModal,velRange:parseInt(e.target.value)||0}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16"}),React.createElement("label",{className:"text-zinc-500"},"Dur ±%"),React.createElement("input",{type:"number",min:0,max:50,value:humanizeModal.durRange,onChange:e=>setHumanizeModal({...humanizeModal,durRange:parseInt(e.target.value)||0}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16"})),React.createElement("div",{className:"flex gap-2"},React.createElement("button",{onClick:()=>applyHumanizeModal(humanizeModal),className:"flex-1 px-3 py-1.5 rounded text-xs font-bold bg-cyan-700 hover:bg-cyan-600 text-white"},"Apply"),React.createElement("button",{onClick:()=>setHumanizeModal(null),className:"px-3 py-1.5 rounded text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-300"},"Cancel")))),scaleMenuPos&&renderScaleContextMenu());};const serializeTracksList=(tracksList,secondsPerBar)=>{return(tracksList||[]).map(t=>{let trackType="AUDIO";if(t.type==='MIDI'||t.type==='soundfont'||t.type==='vst3')trackType="MIDI";else if(t.type==='SECTION')trackType="SECTION";else if(t.sections&&t.sections.length>0)trackType="SECTION";else if(t.midiItems&&t.midiItems.length>0)trackType="MIDI";const items=[];// Serialize EVERY item type present on the track (a track can hold audio +handleSwitchMidiItem(m.id);},className:"flex items-center justify-center flex-1 h-[26px] min-w-0 border border-zinc-600 rounded-md cursor-pointer outline-none "+(isActive?'bg-yellow-600 text-black font-bold':'bg-zinc-700 text-zinc-300 hover:bg-zinc-600')},React.createElement("span",{className:"text-[13px] leading-none font-sans truncate px-1",title:track?track.name:m._trackName},track?track.name:m._trackName)),React.createElement("button",{onClick:function(e){e.stopPropagation();var prevList=activePlayTrackIds||[];var nextList=prevList.indexOf(m._trackId)!==-1?prevList.filter(function(id){return id!==m._trackId;}):prevList.concat([m._trackId]);setActivePlayTrackIds(nextList);if(onRealtimePlay)onRealtimePlay(nextList);},title:isPlayOn?"Unmute — ghost track play cùng main notes":"Mute (mặc định) — click để play ghost cùng main",className:"w-6 h-[26px] shrink-0 border border-zinc-600 rounded-md cursor-pointer text-[11px] font-bold outline-none flex items-center justify-center "+(isPlayOn?'bg-green-700 text-white':'bg-zinc-800 text-zinc-500 hover:text-zinc-300')},isPlayOn?"\u266A":"M")));});return els;}():null),React.createElement("div",{className:"w-[60px] shrink-0 flex flex-col border-r border-zinc-900 overflow-y-auto",ref:keybedRef,onScroll:handleKeybedScroll,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-[120px] shrink-0"}),React.createElement("select",{value:ccMode,onChange:e=>setCcMode(e.target.value),title:"CC control",className:"w-[60px] bg-[#222222] border-r border-zinc-900 text-[10px] text-zinc-200 font-mono shrink-0 font-bold text-center cursor-pointer outline-none"},React.createElement("option",{value:"velocity"},"Vel"),React.createElement("option",{value:"sustain"},"Sus"),React.createElement("option",{value:"modulation"},"Mod"),React.createElement("option",{value:"pitch_bend"},"Bend"),React.createElement("option",{value:"pan"},"Pan"),React.createElement("option",{value:"cc11"},"Expr")),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,onContextMenu:handleCC11ContextMenu,onMouseUp:commitCC11Drag,onMouseLeave:commitCC11Drag,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 */arpModal&&React.createElement("div",{className:"absolute inset-0 z-40 flex items-center justify-center bg-black/60",onMouseDown:e=>e.stopPropagation(),onClick:e=>e.stopPropagation()},React.createElement("div",{className:"bg-zinc-900 border border-zinc-700 rounded-lg p-4 w-72 shadow-2xl"},React.createElement("div",{className:"text-sm font-bold text-cyan-300 mb-3"},"Arpeggiate"),React.createElement("div",{className:"grid grid-cols-2 gap-2 text-xs mb-3"},React.createElement("label",{className:"text-zinc-500"},"Pattern"),React.createElement("select",{value:arpModal.pattern,onChange:e=>setArpModal({...arpModal,pattern:e.target.value}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5"},['UP','DOWN','UP-DOWN','RANDOM','CHORD'].map(p=>React.createElement("option",{key:p,value:p},p))),React.createElement("label",{className:"text-zinc-500"},"Rate"),React.createElement("select",{value:arpModal.rate,onChange:e=>setArpModal({...arpModal,rate:e.target.value}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5"},['1/4','1/8','1/16','1/32'].map(r=>React.createElement("option",{key:r,value:r},r))),React.createElement("label",{className:"text-zinc-500"},"Octaves"),React.createElement("input",{type:"number",min:1,max:4,value:arpModal.octaves,onChange:e=>setArpModal({...arpModal,octaves:parseInt(e.target.value)||1}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-14"}),React.createElement("label",{className:"text-zinc-500"},"Gate %"),React.createElement("input",{type:"number",min:10,max:200,value:arpModal.gate,onChange:e=>setArpModal({...arpModal,gate:parseInt(e.target.value)||80}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-14"})),React.createElement("div",{className:"flex gap-1 mb-3"},React.createElement("label",{className:"flex items-center gap-1 text-[10px] text-zinc-400"},React.createElement("input",{type:"checkbox",checked:!!arpModal.triplet,onChange:e=>setArpModal({...arpModal,triplet:e.target.checked})}),"Triplet"),React.createElement("label",{className:"flex items-center gap-1 text-[10px] text-zinc-400"},React.createElement("input",{type:"checkbox",checked:!!arpModal.dotted,onChange:e=>setArpModal({...arpModal,dotted:e.target.checked})}),"Dotted")),React.createElement("div",{className:"flex gap-2"},React.createElement("button",{onClick:()=>applyArpeggiate(arpModal),className:"flex-1 px-3 py-1.5 rounded text-xs font-bold bg-cyan-700 hover:bg-cyan-600 text-white"},"Apply"),React.createElement("button",{onClick:()=>setArpModal(null),className:"px-3 py-1.5 rounded text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-300"},"Cancel")))),strumModal&&React.createElement("div",{className:"absolute inset-0 z-40 flex items-center justify-center bg-black/60",onMouseDown:e=>e.stopPropagation(),onClick:e=>e.stopPropagation()},React.createElement("div",{className:"bg-zinc-900 border border-zinc-700 rounded-lg p-4 w-72 shadow-2xl"},React.createElement("div",{className:"text-sm font-bold text-cyan-300 mb-3"},"Strum"),React.createElement("div",{className:"grid grid-cols-2 gap-2 text-xs mb-3"},React.createElement("label",{className:"text-zinc-500"},"Strum ms"),React.createElement("input",{type:"number",min:0,max:120,value:strumModal.ms,onChange:e=>setStrumModal({...strumModal,ms:parseInt(e.target.value)||0}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16"}),React.createElement("label",{className:"text-zinc-500"},"Direction"),React.createElement("select",{value:strumModal.direction,onChange:e=>setStrumModal({...strumModal,direction:e.target.value}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5"},['DOWN','UP','ALTERNATE'].map(d=>React.createElement("option",{key:d,value:d},d)))),React.createElement("div",{className:"flex gap-2"},React.createElement("button",{onClick:()=>applyStrum(strumModal),className:"flex-1 px-3 py-1.5 rounded text-xs font-bold bg-cyan-700 hover:bg-cyan-600 text-white"},"Apply"),React.createElement("button",{onClick:()=>setStrumModal(null),className:"px-3 py-1.5 rounded text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-300"},"Cancel")))),humanizeModal&&React.createElement("div",{className:"absolute inset-0 z-40 flex items-center justify-center bg-black/60",onMouseDown:e=>e.stopPropagation(),onClick:e=>e.stopPropagation()},React.createElement("div",{className:"bg-zinc-900 border border-zinc-700 rounded-lg p-4 w-72 shadow-2xl"},React.createElement("div",{className:"text-sm font-bold text-cyan-300 mb-3"},"Humanize"),React.createElement("div",{className:"grid grid-cols-2 gap-2 text-xs mb-3"},React.createElement("label",{className:"text-zinc-500"},"Timing ±ms"),React.createElement("input",{type:"number",min:0,max:30,value:humanizeModal.timingMs,onChange:e=>setHumanizeModal({...humanizeModal,timingMs:parseInt(e.target.value)||0}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16"}),React.createElement("label",{className:"text-zinc-500"},"Vel ±(0-127)"),React.createElement("input",{type:"number",min:0,max:20,value:humanizeModal.velRange,onChange:e=>setHumanizeModal({...humanizeModal,velRange:parseInt(e.target.value)||0}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16"}),React.createElement("label",{className:"text-zinc-500"},"Dur ±%"),React.createElement("input",{type:"number",min:0,max:50,value:humanizeModal.durRange,onChange:e=>setHumanizeModal({...humanizeModal,durRange:parseInt(e.target.value)||0}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16"})),React.createElement("div",{className:"flex gap-2"},React.createElement("button",{onClick:()=>applyHumanizeModal(humanizeModal),className:"flex-1 px-3 py-1.5 rounded text-xs font-bold bg-cyan-700 hover:bg-cyan-600 text-white"},"Apply"),React.createElement("button",{onClick:()=>setHumanizeModal(null),className:"px-3 py-1.5 rounded text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-300"},"Cancel")))),scaleMenuPos&&renderScaleContextMenu());};const serializeTracksList=(tracksList,secondsPerBar)=>{return(tracksList||[]).map(t=>{let trackType="AUDIO";if(t.type==='MIDI'||t.type==='soundfont'||t.type==='vst3')trackType="MIDI";else if(t.type==='SECTION')trackType="SECTION";else if(t.sections&&t.sections.length>0)trackType="SECTION";else if(t.midiItems&&t.midiItems.length>0)trackType="MIDI";const items=[];// Serialize EVERY item type present on the track (a track can hold audio // clips + MIDI items + section items at once). The old if/else-if chain // dropped all but one type per track — silent data loss on save. if(t.clips&&t.clips.length>0){t.clips.forEach(c=>{const durationSec=c.buffer?c.buffer.duration:4.0;const clipFileId=c.serverFileId||t.serverFileId;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:clipFileId?`/static/audio/uploads/${clipFileId}`:"",sample_rate:c.buffer?c.buffer.sampleRate:44100,channels:c.buffer?c.buffer.numberOfChannels:2,gain:1.0,server_file_id:clipFileId}});});}if(t.midiItems&&t.midiItems.length>0){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}))}});});}if(t.sections&&t.sections.length>0){t.sections.forEach(s=>{// BỎ section item thiếu sectionId (insertSectionAtPlayhead tạo section