CC11: global curve from first to last note, manual node add/drag/remove
This commit is contained in:
+95
-112
@@ -9630,12 +9630,22 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
// khai báo sau → TDZ error — user 08:50)
|
||||
const [showCC, setShowCC] = React.useState(true);
|
||||
const [ccHeight, setCcHeight] = React.useState(80);
|
||||
// CC11 = GLOBAL curve tu note dau den note cuoi (khong per-note). Khi commit
|
||||
// sinh lai per-note expression_curve (engine doc per-note) bang cach sample
|
||||
// global tai vung tung note.
|
||||
const [cc11GlobalCurve, setCc11GlobalCurve] = React.useState([]);
|
||||
React.useEffect(() => {
|
||||
if (cc11GlobalCurve && cc11GlobalCurve.length) return;
|
||||
const n = notes.find(x => Array.isArray(x.expression_curve) && x.expression_curve.length);
|
||||
if (n) setCc11GlobalCurve(n.expression_curve.slice());
|
||||
// ponytail: chi khoi phuc khi project cu co curve; chua hop nhat migration
|
||||
}, [notes]);
|
||||
|
||||
// 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 paintCC11Lane = (previewCurve) => {
|
||||
const canvas = ccCanvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
@@ -9653,57 +9663,45 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
ctx.lineTo(viewWidth, h / 2);
|
||||
ctx.stroke();
|
||||
const yOf = v => h - ((v / 127) * (h - 20) + 10);
|
||||
// Dashed lien tuc tu note dau den note cuoi (default line) — khong per-note
|
||||
if (notes.length > 0) {
|
||||
let minS = Infinity, maxE = -Infinity;
|
||||
notes.forEach((n) => {
|
||||
minS = Math.min(minS, n.start_beat);
|
||||
maxE = Math.max(maxE, n.start_beat + (n.duration_beats || 1));
|
||||
});
|
||||
ctx.strokeStyle = '#2f6b57';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo((renderBeatOffset + minS) * pixelsPerBeat, yOf(127));
|
||||
ctx.lineTo((renderBeatOffset + maxE) * pixelsPerBeat, yOf(127));
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
notes.forEach((note) => {
|
||||
if (previewNoteId != null) {
|
||||
if (note.id !== previewNoteId) return;
|
||||
} else if (selectedNoteIds.length > 0) {
|
||||
// Co selection: chi ve curve cua note duoc chon
|
||||
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) {
|
||||
// chua ve node gi -> khong ve gi them (dashed nen da ve lien tuc)
|
||||
ctx.stroke();
|
||||
return;
|
||||
} 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) => {
|
||||
if (notes.length === 0) return;
|
||||
let minS = Infinity, maxE = -Infinity;
|
||||
notes.forEach((n) => {
|
||||
minS = Math.min(minS, n.start_beat);
|
||||
maxE = Math.max(maxE, n.start_beat + (n.duration_beats || 1));
|
||||
});
|
||||
const x0 = (renderBeatOffset + minS) * pixelsPerBeat;
|
||||
const x1 = (renderBeatOffset + maxE) * pixelsPerBeat;
|
||||
// Dashed lien tuc tu note dau den note cuoi (default line)
|
||||
ctx.strokeStyle = '#2f6b57';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x0, yOf(127));
|
||||
ctx.lineTo(x1, yOf(127));
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
const curve = (previewCurve || cc11GlobalCurve || []).slice().sort((a, b) => a.position_ratio - b.position_ratio);
|
||||
if (!curve.length) return;
|
||||
ctx.strokeStyle = '#34d399';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
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);
|
||||
ctx.fillStyle = '#a7f3d0';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, yOf(p.value), 4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
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();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -9732,7 +9730,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
// 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);
|
||||
paintCC11Lane(drag.previewCurve);
|
||||
} else {
|
||||
paintCC11Lane();
|
||||
}
|
||||
@@ -10498,7 +10496,6 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
};
|
||||
|
||||
// ── 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;
|
||||
@@ -10514,32 +10511,35 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
}
|
||||
return pts[pts.length - 1].value;
|
||||
};
|
||||
const findCC11NoteAtBeat = (b) => {
|
||||
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 < bestDist) { bestDist = d; best = idx; }
|
||||
}
|
||||
});
|
||||
return best;
|
||||
};
|
||||
const findCC11TargetNote = (b) => {
|
||||
// Chord: prefer note dang duoc chon duoi click de khong ve/anchor
|
||||
// nham cho note khac cung beat
|
||||
const cc11GlobalRange = () => {
|
||||
const cur = notesRef.current;
|
||||
for (let i = 0; i < cur.length; i++) {
|
||||
const n = cur[i];
|
||||
if (selectedNoteIds.includes(n.id) && b >= n.start_beat && b <= n.start_beat + (n.duration_beats || 1)) return i;
|
||||
}
|
||||
return findCC11NoteAtBeat(b);
|
||||
if (!cur.length) return null;
|
||||
let minS = Infinity, maxE = -Infinity;
|
||||
cur.forEach((n) => {
|
||||
minS = Math.min(minS, n.start_beat);
|
||||
maxE = Math.max(maxE, n.start_beat + (n.duration_beats || 1));
|
||||
});
|
||||
return { minS, maxE, span: (maxE - minS) || 1 };
|
||||
};
|
||||
const commitCC11Curve = (curve, shared, noteIdx) => {
|
||||
// GLOBAL curve -> per-note expression_curve: sample global tai vung tung note
|
||||
const commitCC11Curve = (curve) => {
|
||||
setCc11GlobalCurve(curve);
|
||||
const curNotes = notesRef.current;
|
||||
const updatedNotes = shared
|
||||
? curNotes.map(n => ({ ...n, expression_curve: curve }))
|
||||
: curNotes.map((n, i) => (i === noteIdx ? { ...n, expression_curve: curve } : n));
|
||||
const g = cc11GlobalRange();
|
||||
if (!g) { setNotes(curNotes); return; }
|
||||
const updatedNotes = curNotes.map((n) => {
|
||||
const s = n.start_beat;
|
||||
const e = s + (n.duration_beats || 1);
|
||||
const pts = [];
|
||||
const add = (ratio, value) => pts.push({ position_ratio: ratio, value });
|
||||
add(0, cc11ValueAt(curve, (s - g.minS) / g.span));
|
||||
(curve || []).forEach((p) => {
|
||||
const b = g.minS + p.position_ratio * g.span;
|
||||
if (b > s && b < e) add((b - s) / (e - s), p.value);
|
||||
});
|
||||
add(1, cc11ValueAt(curve, (e - g.minS) / g.span));
|
||||
return { ...n, expression_curve: pts.sort((a, b) => a.position_ratio - b.position_ratio) };
|
||||
});
|
||||
setNotes(updatedNotes);
|
||||
if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes);
|
||||
};
|
||||
@@ -10551,23 +10551,13 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
const y = e.clientY - rect.top;
|
||||
const h = rect.height;
|
||||
const beat = x / pixelsPerBeat - renderBeatOffset;
|
||||
const noteIdx = findCC11TargetNote(beat);
|
||||
if (noteIdx === -1) { ccDragRef.current = null; return; }
|
||||
const note = notesRef.current[noteIdx];
|
||||
if (!note) return;
|
||||
// Co selection -> chi sua note do; neu click trung note khac -> chuyen
|
||||
// selection sang note ay (REPLACE, khong accumulate). KHONG co selection
|
||||
// -> shared mode (CC11 dung chung cho tat ca notes), khong auto-select.
|
||||
const shared = selectedNoteIds.length === 0;
|
||||
if (!shared && note.id && !selectedNoteIds.includes(note.id)) {
|
||||
setSelectedNoteIds([note.id]);
|
||||
}
|
||||
const dur = note.duration_beats || 1;
|
||||
const ratio = Math.max(0, Math.min(1, (beat - note.start_beat) / dur));
|
||||
const g = cc11GlobalRange();
|
||||
if (!g || beat < g.minS || beat > g.maxE) { ccDragRef.current = null; return; }
|
||||
const ratio = Math.max(0, Math.min(1, (beat - g.minS) / g.span));
|
||||
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 curve = (cc11GlobalCurve || []).slice();
|
||||
const x0 = (renderBeatOffset + g.minS) * pixelsPerBeat;
|
||||
const x1 = (renderBeatOffset + g.maxE) * pixelsPerBeat;
|
||||
const yOf = v => h - ((v / 127) * (h - 20) + 10);
|
||||
let anchorIdx = -1;
|
||||
curve.forEach((p, i) => {
|
||||
@@ -10576,8 +10566,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
if (Math.hypot(ax - x, ay - y) <= 8) anchorIdx = i;
|
||||
});
|
||||
if (anchorIdx === -1) {
|
||||
// Click vao vi tri trong: THEM node tai do ngay lap tuc — khong keo
|
||||
// node moi theo cursor (user: chi click de them node, drag node co san)
|
||||
// Click trong: THEM node tai do ngay lap tuc — khong keo node moi
|
||||
const dup = curve.findIndex(p => Math.abs(p.position_ratio - ratio) < 1e-6);
|
||||
let newCurve;
|
||||
if (dup !== -1) {
|
||||
@@ -10586,15 +10575,14 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
} else {
|
||||
newCurve = curve.concat([{ position_ratio: ratio, value: val }]).sort((a, b) => a.position_ratio - b.position_ratio);
|
||||
}
|
||||
commitCC11Curve(newCurve, shared, noteIdx);
|
||||
commitCC11Curve(newCurve);
|
||||
ccDragRef.current = null;
|
||||
return;
|
||||
}
|
||||
const cur = curve[anchorIdx];
|
||||
curve[anchorIdx] = { position_ratio: ratio, value: val };
|
||||
// Khong setNotes ngay: ve preview truc tiep, commit khi mouseup/mouseleave
|
||||
ccDragRef.current = { mode: 'cc11', active: true, noteIdx, noteId: note.id, anchorIdx, noteStart: note.start_beat, noteDur: dur, previewCurve: curve, shared };
|
||||
paintCC11Lane(note.id, curve);
|
||||
ccDragRef.current = { mode: 'cc11', active: true, anchorIdx, gs: g.minS, ge: g.maxE, span: g.span, previewCurve: curve };
|
||||
paintCC11Lane(curve);
|
||||
};
|
||||
const handleCC11ContextMenu = (e) => {
|
||||
if (ccMode !== 'cc11') return;
|
||||
@@ -10606,14 +10594,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
const y = e.clientY - rect.top;
|
||||
const h = rect.height;
|
||||
const beat = x / pixelsPerBeat - renderBeatOffset;
|
||||
const noteIdx = findCC11TargetNote(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 g = cc11GlobalRange();
|
||||
if (!g || beat < g.minS || beat > g.maxE) return;
|
||||
const x0 = (renderBeatOffset + g.minS) * pixelsPerBeat;
|
||||
const x1 = (renderBeatOffset + g.maxE) * pixelsPerBeat;
|
||||
const yOf = v => h - ((v / 127) * (h - 20) + 10);
|
||||
const curve = getCC11Curve(note).slice();
|
||||
const curve = (cc11GlobalCurve || []).slice();
|
||||
let delIdx = -1, delDist = Infinity;
|
||||
curve.forEach((p, i) => {
|
||||
const ax = x0 + p.position_ratio * (x1 - x0);
|
||||
@@ -10623,7 +10609,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
});
|
||||
if (delIdx === -1 || delDist > 8) return;
|
||||
curve.splice(delIdx, 1);
|
||||
commitCC11Curve(curve, selectedNoteIds.length === 0, noteIdx);
|
||||
commitCC11Curve(curve);
|
||||
};
|
||||
// Commit CC11 preview khi mouseup/mouseleave — truoc day drag setNotes moi
|
||||
// mousemove (flicker); gio chi cap nhat state 1 lan khi ket thuc drag.
|
||||
@@ -10631,7 +10617,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
const drag = ccDragRef.current;
|
||||
if (!drag) return;
|
||||
if (drag.mode === 'cc11' && drag.previewCurve) {
|
||||
commitCC11Curve(drag.previewCurve, drag.shared, drag.noteIdx);
|
||||
commitCC11Curve(drag.previewCurve);
|
||||
}
|
||||
ccDragRef.current = null;
|
||||
};
|
||||
@@ -10700,12 +10686,9 @@ 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 = 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 ratio = Math.max(0, Math.min(1, (beat - drag.gs) / drag.span));
|
||||
const val = Math.max(0, Math.min(127, Math.round(((h - y) / h) * 127)));
|
||||
const curve = (drag.previewCurve || getCC11Curve(note)).slice();
|
||||
const curve = (drag.previewCurve || []).slice();
|
||||
const idx = Math.min(drag.anchorIdx, curve.length - 1);
|
||||
const cur = curve[idx] || { position_ratio: ratio, value: val };
|
||||
// Drag node: doi ca ratio+value, clamp giua 2 node lan can
|
||||
@@ -10718,7 +10701,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
}
|
||||
curve[idx] = { position_ratio: Math.min(maxR, Math.max(minR, ratio)), value: val };
|
||||
drag.previewCurve = curve;
|
||||
paintCC11Lane(drag.noteId, curve);
|
||||
paintCC11Lane(curve);
|
||||
return;
|
||||
}
|
||||
const canvas = ccCanvasRef.current;
|
||||
|
||||
@@ -740,16 +740,18 @@ 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);// Ve CC11 lane (graph thread). Tach rieng de drag ve preview truc tiep
|
||||
const[showCC,setShowCC]=React.useState(true);const[ccHeight,setCcHeight]=React.useState(80);// CC11 = GLOBAL curve tu note dau den note cuoi (khong per-note). Khi commit
|
||||
// sinh lai per-note expression_curve (engine doc per-note) bang cach sample
|
||||
// global tai vung tung note.
|
||||
const[cc11GlobalCurve,setCc11GlobalCurve]=React.useState([]);React.useEffect(()=>{if(cc11GlobalCurve&&cc11GlobalCurve.length)return;const n=notes.find(x=>Array.isArray(x.expression_curve)&&x.expression_curve.length);if(n)setCc11GlobalCurve(n.expression_curve.slice());// ponytail: chi khoi phuc khi project cu co curve; chua hop nhat migration
|
||||
},[notes]);// 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);// Dashed lien tuc tu note dau den note cuoi (default line) — khong per-note
|
||||
if(notes.length>0){let minS=Infinity,maxE=-Infinity;notes.forEach(n=>{minS=Math.min(minS,n.start_beat);maxE=Math.max(maxE,n.start_beat+(n.duration_beats||1));});ctx.strokeStyle='#2f6b57';ctx.lineWidth=1.5;ctx.setLineDash([4,4]);ctx.beginPath();ctx.moveTo((renderBeatOffset+minS)*pixelsPerBeat,yOf(127));ctx.lineTo((renderBeatOffset+maxE)*pixelsPerBeat,yOf(127));ctx.stroke();ctx.setLineDash([]);}notes.forEach(note=>{if(previewNoteId!=null){if(note.id!==previewNoteId)return;}else if(selectedNoteIds.length>0){// Co selection: chi ve curve cua note duoc chon
|
||||
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){// chua ve node gi -> khong ve gi them (dashed nen da ve lien tuc)
|
||||
ctx.stroke();return;}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
|
||||
const paintCC11Lane=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);if(notes.length===0)return;let minS=Infinity,maxE=-Infinity;notes.forEach(n=>{minS=Math.min(minS,n.start_beat);maxE=Math.max(maxE,n.start_beat+(n.duration_beats||1));});const x0=(renderBeatOffset+minS)*pixelsPerBeat;const x1=(renderBeatOffset+maxE)*pixelsPerBeat;// Dashed lien tuc tu note dau den note cuoi (default line)
|
||||
ctx.strokeStyle='#2f6b57';ctx.lineWidth=1.5;ctx.setLineDash([4,4]);ctx.beginPath();ctx.moveTo(x0,yOf(127));ctx.lineTo(x1,yOf(127));ctx.stroke();ctx.setLineDash([]);const curve=(previewCurve||cc11GlobalCurve||[]).slice().sort((a,b)=>a.position_ratio-b.position_ratio);if(!curve.length)return;ctx.strokeStyle='#34d399';ctx.lineWidth=2;ctx.beginPath();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
|
||||
const drag=ccDragRef.current;if(drag&&drag.mode==='cc11'&&drag.previewCurve){paintCC11Lane(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
|
||||
@@ -793,19 +795,14 @@ 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<pts.length;i++){if(ratio<=pts[i].position_ratio){const a=pts[i-1],b=pts[i];const t=(ratio-a.position_ratio)/(b.position_ratio-a.position_ratio||1);return Math.round(a.value+(b.value-a.value)*t);}}return pts[pts.length-1].value;};const findCC11NoteAtBeat=b=>{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<bestDist){bestDist=d;best=idx;}}});return best;};const findCC11TargetNote=b=>{// Chord: prefer note dang duoc chon duoi click de khong ve/anchor
|
||||
// nham cho note khac cung beat
|
||||
const cur=notesRef.current;for(let i=0;i<cur.length;i++){const n=cur[i];if(selectedNoteIds.includes(n.id)&&b>=n.start_beat&&b<=n.start_beat+(n.duration_beats||1))return i;}return findCC11NoteAtBeat(b);};const commitCC11Curve=(curve,shared,noteIdx)=>{const curNotes=notesRef.current;const updatedNotes=shared?curNotes.map(n=>({...n,expression_curve:curve})):curNotes.map((n,i)=>i===noteIdx?{...n,expression_curve:curve}:n);setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);};const handleCC11MouseDown=e=>{const canvas=ccCanvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat-renderBeatOffset;const noteIdx=findCC11TargetNote(beat);if(noteIdx===-1){ccDragRef.current=null;return;}const note=notesRef.current[noteIdx];if(!note)return;// Co selection -> chi sua note do; neu click trung note khac -> chuyen
|
||||
// selection sang note ay (REPLACE, khong accumulate). KHONG co selection
|
||||
// -> shared mode (CC11 dung chung cho tat ca notes), khong auto-select.
|
||||
const shared=selectedNoteIds.length===0;if(!shared&¬e.id&&!selectedNoteIds.includes(note.id)){setSelectedNoteIds([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){// Click vao vi tri trong: THEM node tai do ngay lap tuc — khong keo
|
||||
// node moi theo cursor (user: chi click de them node, drag node co san)
|
||||
const dup=curve.findIndex(p=>Math.abs(p.position_ratio-ratio)<1e-6);let newCurve;if(dup!==-1){newCurve=curve.slice();newCurve[dup]={position_ratio:ratio,value:val};}else{newCurve=curve.concat([{position_ratio:ratio,value:val}]).sort((a,b)=>a.position_ratio-b.position_ratio);}commitCC11Curve(newCurve,shared,noteIdx);ccDragRef.current=null;return;}const cur=curve[anchorIdx];curve[anchorIdx]={position_ratio:ratio,value:val};// Khong setNotes ngay: ve preview truc tiep, commit khi mouseup/mouseleave
|
||||
ccDragRef.current={mode:'cc11',active:true,noteIdx,noteId:note.id,anchorIdx,noteStart:note.start_beat,noteDur:dur,previewCurve:curve,shared};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=findCC11TargetNote(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(d<delDist){delDist=d;delIdx=i;}});if(delIdx===-1||delDist>8)return;curve.splice(delIdx,1);commitCC11Curve(curve,selectedNoteIds.length===0,noteIdx);};// Commit CC11 preview khi mouseup/mouseleave — truoc day drag setNotes moi
|
||||
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<pts.length;i++){if(ratio<=pts[i].position_ratio){const a=pts[i-1],b=pts[i];const t=(ratio-a.position_ratio)/(b.position_ratio-a.position_ratio||1);return Math.round(a.value+(b.value-a.value)*t);}}return pts[pts.length-1].value;};const cc11GlobalRange=()=>{const cur=notesRef.current;if(!cur.length)return null;let minS=Infinity,maxE=-Infinity;cur.forEach(n=>{minS=Math.min(minS,n.start_beat);maxE=Math.max(maxE,n.start_beat+(n.duration_beats||1));});return{minS,maxE,span:maxE-minS||1};};// GLOBAL curve -> per-note expression_curve: sample global tai vung tung note
|
||||
const commitCC11Curve=curve=>{setCc11GlobalCurve(curve);const curNotes=notesRef.current;const g=cc11GlobalRange();if(!g){setNotes(curNotes);return;}const updatedNotes=curNotes.map(n=>{const s=n.start_beat;const e=s+(n.duration_beats||1);const pts=[];const add=(ratio,value)=>pts.push({position_ratio:ratio,value});add(0,cc11ValueAt(curve,(s-g.minS)/g.span));(curve||[]).forEach(p=>{const b=g.minS+p.position_ratio*g.span;if(b>s&&b<e)add((b-s)/(e-s),p.value);});add(1,cc11ValueAt(curve,(e-g.minS)/g.span));return{...n,expression_curve:pts.sort((a,b)=>a.position_ratio-b.position_ratio)};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);};const handleCC11MouseDown=e=>{const canvas=ccCanvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat-renderBeatOffset;const g=cc11GlobalRange();if(!g||beat<g.minS||beat>g.maxE){ccDragRef.current=null;return;}const ratio=Math.max(0,Math.min(1,(beat-g.minS)/g.span));const val=Math.max(0,Math.min(127,Math.round((h-y)/h*127)));const curve=(cc11GlobalCurve||[]).slice();const x0=(renderBeatOffset+g.minS)*pixelsPerBeat;const x1=(renderBeatOffset+g.maxE)*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){// Click trong: THEM node tai do ngay lap tuc — khong keo node moi
|
||||
const dup=curve.findIndex(p=>Math.abs(p.position_ratio-ratio)<1e-6);let newCurve;if(dup!==-1){newCurve=curve.slice();newCurve[dup]={position_ratio:ratio,value:val};}else{newCurve=curve.concat([{position_ratio:ratio,value:val}]).sort((a,b)=>a.position_ratio-b.position_ratio);}commitCC11Curve(newCurve);ccDragRef.current=null;return;}curve[anchorIdx]={position_ratio:ratio,value:val};// Khong setNotes ngay: ve preview truc tiep, commit khi mouseup/mouseleave
|
||||
ccDragRef.current={mode:'cc11',active:true,anchorIdx,gs:g.minS,ge:g.maxE,span:g.span,previewCurve:curve};paintCC11Lane(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 g=cc11GlobalRange();if(!g||beat<g.minS||beat>g.maxE)return;const x0=(renderBeatOffset+g.minS)*pixelsPerBeat;const x1=(renderBeatOffset+g.maxE)*pixelsPerBeat;const yOf=v=>h-(v/127*(h-20)+10);const curve=(cc11GlobalCurve||[]).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(d<delDist){delDist=d;delIdx=i;}});if(delIdx===-1||delDist>8)return;curve.splice(delIdx,1);commitCC11Curve(curve);};// 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){commitCC11Curve(drag.previewCurve,drag.shared,drag.noteIdx);}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};// Drag node: 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;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 commitCC11Drag=()=>{const drag=ccDragRef.current;if(!drag)return;if(drag.mode==='cc11'&&drag.previewCurve){commitCC11Curve(drag.previewCurve);}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 ratio=Math.max(0,Math.min(1,(beat-drag.gs)/drag.span));const val=Math.max(0,Math.min(127,Math.round((h-y)/h*127)));const curve=(drag.previewCurve||[]).slice();const idx=Math.min(drag.anchorIdx,curve.length-1);const cur=curve[idx]||{position_ratio:ratio,value:val};// Drag node: 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(curve);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
|
||||
// (trước đây phải bật/tắt power mới có tác dụng).
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<link rel="stylesheet" href="/static/vendor/fontawesome.min.css">
|
||||
<script src="/static/vendor/react.production.min.js"></script>
|
||||
<script src="/static/vendor/react-dom.production.min.js"></script>
|
||||
<script src="/static/js/services/fluidsynthLoader.js?v=202607271245"></script>
|
||||
<script src="/static/js/services/fluidsynthLoader.js?v=20260826161545"></script>
|
||||
<script src="/static/js/services/runtime.js?v=202608101800"></script>
|
||||
<script src="/static/js/services/api.js?v=202608212400"></script>
|
||||
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
|
||||
|
||||
Reference in New Issue
Block a user