diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index c693e94..4bbc673 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -10486,6 +10486,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos 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 + // (selectedNoteIds) -> khong select -> anchor them nhung vo hinh (bug: khong ve duoc CC11) + if (note && 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))); diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 6307a23..85d6b99 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -786,7 +786,9 @@ 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];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 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 +// (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 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