FIX: cho phép copy/cut các notes được chọn trong PIANO ROLL TAB
This commit is contained in:
+24
-4
@@ -7141,8 +7141,15 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
e.preventDefault();
|
||||
if (!clipboardNotes || !clipboardNotes.length) { showToast('Clipboard trống — bấm Ctrl+C trước.', 'warning'); return; }
|
||||
pushToUndo(notes);
|
||||
setNotes(prev => [...(prev || []), ...clipboardNotes.map(function(n) { return { ...n, id: 'note_cp_' + Date.now() + '_' + Math.floor(Math.random() * 100000) }; })]);
|
||||
showToast('Đã paste ' + clipboardNotes.length + ' nốt.', 'success');
|
||||
// Dán bắt đầu tại vị trí CON TRỎ PLAYHEAD (user 09:25) — bù offset
|
||||
// sao cho note đầu tiên của clipboard nằm đúng playhead (beat).
|
||||
const pasteBeat = (st.currentTime || 0) / (60.0 / (parseInt(bpm) || 120));
|
||||
const minStart = Math.min(...clipboardNotes.map(n => n.start_beat));
|
||||
const offset = pasteBeat - minStart;
|
||||
setNotes(prev => [...(prev || []), ...clipboardNotes.map(function(n) {
|
||||
return { ...n, id: 'note_cp_' + Date.now() + '_' + Math.floor(Math.random() * 100000), start_beat: Math.max(0, n.start_beat + offset) };
|
||||
})]);
|
||||
showToast('Đã paste ' + clipboardNotes.length + ' nốt tại playhead.', 'success');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -7158,7 +7165,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, [notes, setSelectedNoteIds, clipboardNotes]);
|
||||
}, [notes, setSelectedNoteIds, clipboardNotes, selectedNoteIds]);
|
||||
|
||||
// Undo/redo stacks
|
||||
const undoStackRef = React.useRef([]);
|
||||
@@ -8242,6 +8249,19 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
}
|
||||
}
|
||||
setDraggedNote(null);
|
||||
// Marquee (Ctrl+drag): CHỌN notes nằm trong vùng (user 09:15 — trước đây
|
||||
// chỉ vẽ highlight, không set selectedNoteIds → Ctrl+X báo "chọn notes")
|
||||
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 inMarquee = notes.filter(n => {
|
||||
const center = n.start_beat + n.duration_beats / 2;
|
||||
return center >= minBeat && center <= maxBeat && n.pitch >= minPitch && n.pitch <= maxPitch;
|
||||
});
|
||||
setSelectedNoteIds(inMarquee.map(n => n.id));
|
||||
}
|
||||
setSelectionMarquee(null);
|
||||
rightClickDragRef.current = { active: false, startX: 0, startY: 0 };
|
||||
if (brushAutoScrollRef.current) {
|
||||
@@ -16574,7 +16594,7 @@ const App = () => {
|
||||
handleSubTabLoop(curTabId, 4);
|
||||
return;
|
||||
}
|
||||
if (e.key === 'v' || e.key === 'V') {
|
||||
if (!ctrl && !e.metaKey && !e.altKey && (e.key === 'v' || e.key === 'V')) {
|
||||
e.preventDefault();
|
||||
const val = prompt("Nhập Gain điều chỉnh (dB):", "0");
|
||||
if (val) handleSubTabGain(curTabId, parseFloat(val) || 0);
|
||||
|
||||
@@ -397,7 +397,9 @@ if(isPlaying)return[];if(!activeTracks||!st||!st.target_id)return[];var fn=windo
|
||||
// Shift+C chord stamp, Shift+S lock-scale toggle
|
||||
const inField=e.target&&(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.isContentEditable);// Ctrl+C/X/V — copy/paste/cut NOTES (thay 2 nút Copy/Paste đã bỏ —
|
||||
// user 08:40) — CHỈ tác động NOTES ĐƯỢC CHỌN (user 09:10)
|
||||
if((e.ctrlKey||e.metaKey)&&!e.altKey&&!inField){if(e.key==='c'){e.preventDefault();const sel=notes.filter(n=>selectedNoteIds.includes(n.id));if(sel.length){if(onCopyNotes)onCopyNotes(sel);showToast('Đã copy '+sel.length+' nốt được chọn.','success');}else{showToast('Chọn notes trước khi copy (Ctrl+C).','warning');}return;}if(e.key==='x'){e.preventDefault();const sel=notes.filter(n=>selectedNoteIds.includes(n.id));if(sel.length){if(onCopyNotes)onCopyNotes(sel);pushToUndo(notes);setNotes(prev=>(prev||[]).filter(n=>!selectedNoteIds.includes(n.id)));showToast('Đã cắt '+sel.length+' nốt được chọn.','success');}else{showToast('Chọn notes trước khi cắt (Ctrl+X).','warning');}return;}if(e.key==='v'){e.preventDefault();if(!clipboardNotes||!clipboardNotes.length){showToast('Clipboard trống — bấm Ctrl+C trước.','warning');return;}pushToUndo(notes);setNotes(prev=>[...(prev||[]),...clipboardNotes.map(function(n){return{...n,id:'note_cp_'+Date.now()+'_'+Math.floor(Math.random()*100000)};})]);showToast('Đã paste '+clipboardNotes.length+' nốt.','success');return;}}if(e.altKey&&!e.ctrlKey&&!inField&&e.key==='a'){e.preventDefault();setArpModal({pattern:'UP',rate:'1/16',octaves:2,gate:80,triplet:false,dotted:false});return;}if(e.altKey&&!e.ctrlKey&&!inField&&e.key==='s'){e.preventDefault();setStrumModal({ms:30,direction:'DOWN'});return;}if(e.altKey&&!e.ctrlKey&&!inField&&e.key==='r'){e.preventDefault();setHumanizeModal({timingMs:12,velRange:15,durRange:10});return;}if(e.shiftKey&&!e.ctrlKey&&!e.altKey&&!inField&&e.key==='c'){e.preventDefault();setChordStampMode(m=>!m);return;}if(e.shiftKey&&!e.ctrlKey&&!e.altKey&&!inField&&e.key==='s'){e.preventDefault();setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,snapToScale:!(s.snapToScale!==undefined?s.snapToScale:true)}:s));return;}};window.addEventListener('keydown',handler);return()=>window.removeEventListener('keydown',handler);},[notes,setSelectedNoteIds,clipboardNotes]);// Undo/redo stacks
|
||||
if((e.ctrlKey||e.metaKey)&&!e.altKey&&!inField){if(e.key==='c'){e.preventDefault();const sel=notes.filter(n=>selectedNoteIds.includes(n.id));if(sel.length){if(onCopyNotes)onCopyNotes(sel);showToast('Đã copy '+sel.length+' nốt được chọn.','success');}else{showToast('Chọn notes trước khi copy (Ctrl+C).','warning');}return;}if(e.key==='x'){e.preventDefault();const sel=notes.filter(n=>selectedNoteIds.includes(n.id));if(sel.length){if(onCopyNotes)onCopyNotes(sel);pushToUndo(notes);setNotes(prev=>(prev||[]).filter(n=>!selectedNoteIds.includes(n.id)));showToast('Đã cắt '+sel.length+' nốt được chọn.','success');}else{showToast('Chọn notes trước khi cắt (Ctrl+X).','warning');}return;}if(e.key==='v'){e.preventDefault();if(!clipboardNotes||!clipboardNotes.length){showToast('Clipboard trống — bấm Ctrl+C trước.','warning');return;}pushToUndo(notes);// Dán bắt đầu tại vị trí CON TRỎ PLAYHEAD (user 09:25) — bù offset
|
||||
// sao cho note đầu tiên của clipboard nằm đúng playhead (beat).
|
||||
const pasteBeat=(st.currentTime||0)/(60.0/(parseInt(bpm)||120));const minStart=Math.min(...clipboardNotes.map(n=>n.start_beat));const offset=pasteBeat-minStart;setNotes(prev=>[...(prev||[]),...clipboardNotes.map(function(n){return{...n,id:'note_cp_'+Date.now()+'_'+Math.floor(Math.random()*100000),start_beat:Math.max(0,n.start_beat+offset)};})]);showToast('Đã paste '+clipboardNotes.length+' nốt tại playhead.','success');return;}}if(e.altKey&&!e.ctrlKey&&!inField&&e.key==='a'){e.preventDefault();setArpModal({pattern:'UP',rate:'1/16',octaves:2,gate:80,triplet:false,dotted:false});return;}if(e.altKey&&!e.ctrlKey&&!inField&&e.key==='s'){e.preventDefault();setStrumModal({ms:30,direction:'DOWN'});return;}if(e.altKey&&!e.ctrlKey&&!inField&&e.key==='r'){e.preventDefault();setHumanizeModal({timingMs:12,velRange:15,durRange:10});return;}if(e.shiftKey&&!e.ctrlKey&&!e.altKey&&!inField&&e.key==='c'){e.preventDefault();setChordStampMode(m=>!m);return;}if(e.shiftKey&&!e.ctrlKey&&!e.altKey&&!inField&&e.key==='s'){e.preventDefault();setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,snapToScale:!(s.snapToScale!==undefined?s.snapToScale:true)}:s));return;}};window.addEventListener('keydown',handler);return()=>window.removeEventListener('keydown',handler);},[notes,setSelectedNoteIds,clipboardNotes,selectedNoteIds]);// Undo/redo stacks
|
||||
const undoStackRef=React.useRef([]);const redoStackRef=React.useRef([]);const notesBeforeDragRef=React.useRef(null);const pushToUndo=React.useCallback(prevNotes=>{undoStackRef.current.push(JSON.parse(JSON.stringify(prevNotes)));redoStackRef.current=[];if(undoStackRef.current.length>50)undoStackRef.current.shift();},[]);// ── Humanize: ngẫu nhiên hóa velocity + timing theo cường độ ──
|
||||
const[humanizeStrength,setHumanizeStrength]=React.useState(0.10);// 0.05 nhẹ / 0.10 vừa / 0.18 mạnh
|
||||
const applyHumanize=React.useCallback(()=>{if(!notes||!notes.length){showToast('Không có nốt nào để humanize.','warning');return;}const velAmt=humanizeStrength;const timeAmt=humanizeStrength*0.15;// ±0.015 beat @ vừa (~12ms @120bpm)
|
||||
@@ -454,7 +456,9 @@ return n.start_beat>=minBeat&¬eEnd<=maxBeat;}}).map(n=>n.id);setSelectedNoteI
|
||||
const rc=rightClickDragRef.current;if(rc.active&&(Math.abs(e.clientX-rc.startX)>5||Math.abs(e.clientY-rc.startY)>5)){rc.active=false;swallowContextMenuRef.current=true;notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));setDraggedNote({mode:'erase_sweep',visitedPitches:[]});return;}if(!draggedNote){let foundIdx=-1;for(let i=0;i<notes.length;i++){const n=notes[i];if(pitch===n.pitch){const noteRightBeat=n.start_beat+n.duration_beats;const pixelDist=Math.abs((noteRightBeat-beat)*pixelsPerBeat);if(pixelDist<6&&beat>=n.start_beat){foundIdx=i;break;}}}if(foundIdx!==-1){canvas.style.cursor='ew-resize';setHoveredResizeIdx(foundIdx);}else{canvas.style.cursor=activeRollTool==='eraser'?'pointer':'crosshair';setHoveredResizeIdx(-1);}return;}if(draggedNote.mode==='draw'){const snappedPitch=snapToScaleRef.current?snapPitchToScale(pitch,selectedScaleRef.current):pitch;const lastPitch=draggedNote.lastDrawnPitch!==undefined?draggedNote.lastDrawnPitch:draggedNote.startOffsetPitch;const pitchChanged=snappedPitch!==lastPitch;const noteBeats=draggedNote.noteStartBeats||[];const defaultDur=getSnapDuration(snapValue);function playDrawPreview(p,durMs){stopPreviewNote();if(window.SonicSF&&window.SonicSF.playNote){var pvCtx=getAudioContext();var pvTrk=activeTracks.find(function(t){return t.id===st.trackId;});var pvCtxInst=resolveTrackInstrumentCtx(pvTrk,activeTracks);var pvVel=Math.round(brushVelocityRef.current*127);// playNote (FluidSynth — nhạc cụ THẬT). _playNoteFallback = oscillator
|
||||
// beep sai âm (percussion/soundfont).
|
||||
window.SonicSF.playNote(p,pvVel,durMs,pvCtx.currentTime,pvCtxInst.program,null,pvCtxInst.ch,pvCtxInst.synthEngine);previewPitchRef.current=p;}}if(pitchChanged){const brushIds=draggedNote.brushIds||[];if(brushIds.length>0&¬eBeats.length>0){const prevNoteId=brushIds[brushIds.length-1];const prevNoteBeat=noteBeats[noteBeats.length-1];const prevDur=Math.max(0.125,beat-prevNoteBeat);setNotes(prev=>prev.map(n=>{if(n.id!==prevNoteId)return n;return{...n,duration_beats:prevDur};}));}const newNote={id:'note_'+Date.now()+Math.random().toString(36).substr(2,8)+'_'+(noteBeats.length+1),pitch:snappedPitch,start_beat:beat,duration_beats:defaultDur,velocity:brushVelocityRef.current,pan:0.0};setNotes(prev=>[...prev,newNote]);setSelectedNoteIds(prev=>[...prev,newNote.id]);draggedNote.brushIds=[...brushIds,newNote.id];draggedNote.lastDrawnPitch=snappedPitch;draggedNote.noteStartBeats=[...noteBeats,beat];playDrawPreview(snappedPitch,Math.max(100,Math.round(defaultDur*(60/bpm)*1000)));}else{const brushIds=draggedNote.brushIds||[];const lastBrushId=brushIds.length>0?brushIds[brushIds.length-1]:draggedNote.drawNoteId;const lastNoteBeat=noteBeats.length>0?noteBeats[noteBeats.length-1]:draggedNote.startOffsetBeat;if(lastBrushId){const extDur=Math.max(0.125,beat-lastNoteBeat);setNotes(prev=>prev.map(n=>{if(n.id!==lastBrushId)return n;return{...n,duration_beats:extDur};}));playDrawPreview(snappedPitch,Math.max(100,Math.round(extDur*(60/bpm)*1000)));}}const container=gridScrollRef.current;if(container){const cr=container.getBoundingClientRect();const visTop=container.scrollTop;const visBot=visTop+container.clientHeight;const pitchPixel=(127-snappedPitch)*NoteHeight;const safeMargin=NoteHeight*2;if(pitchPixel<visTop+safeMargin){const target=Math.max(0,pitchPixel-safeMargin);if(container.scrollTop!==target)container.scrollTop=target;if(!brushAutoScrollRef.current||brushAutoScrollRef.current.direction!=='up'){if(brushAutoScrollRef.current)clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current={direction:'up',id:setInterval(()=>{if(gridScrollRef.current)gridScrollRef.current.scrollTop=Math.max(0,gridScrollRef.current.scrollTop-Math.max(1,Math.floor(NoteHeight*0.5)));},16)};}}else if(pitchPixel+NoteHeight>visBot-safeMargin){const target=Math.min(container.scrollHeight-container.clientHeight,pitchPixel-container.clientHeight+safeMargin+NoteHeight);if(container.scrollTop!==target)container.scrollTop=target;if(!brushAutoScrollRef.current||brushAutoScrollRef.current.direction!=='down'){if(brushAutoScrollRef.current)clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current={direction:'down',id:setInterval(()=>{if(gridScrollRef.current)gridScrollRef.current.scrollTop=Math.min(gridScrollRef.current.scrollHeight-gridScrollRef.current.clientHeight,gridScrollRef.current.scrollTop+Math.max(1,Math.floor(NoteHeight*0.5)));},16)};}}else{if(brushAutoScrollRef.current){clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current=null;}}}return;}if(draggedNote.mode==='erase_sweep'){const erased=draggedNote.erasedIds||[];const target=notes.find(n=>n.pitch===pitch&&beat>=n.start_beat&&beat<n.start_beat+n.duration_beats);if(target&&!erased.includes(target.id)){draggedNote.erasedIds=[...erased,target.id];setNotes(prev=>prev.filter(n=>n.id!==target.id));setSelectedNoteIds(prev=>prev.filter(id=>id!==target.id));}return;}if(draggedNote.mode==='scale'){const newEnd=getSnapBeat(Math.max(draggedNote.firstStart+0.125,beat),snapValue);const range=draggedNote.originalEnd-draggedNote.firstStart;if(range<=0)return;const scaleFactor=Math.max(0.01,(newEnd-draggedNote.firstStart)/range);const ids=draggedNote.selectedNoteIds||[];const firstStart=draggedNote.firstStart;const notesBefore=notesBeforeDragRef.current;if(!notesBefore)return;setNotes(prev=>prev.map(n=>{if(!ids.includes(n.id))return n;const orig=notesBefore?notesBefore.find(o=>o.id===n.id):null;const origStart=orig?orig.start_beat:n.start_beat;const origDur=orig?orig.duration_beats:n.duration_beats;const relStart=origStart-firstStart;const relEnd=relStart+origDur;return{...n,start_beat:firstStart+relStart*scaleFactor,duration_beats:Math.max(0.125,relEnd*scaleFactor-relStart*scaleFactor)};}));return;}if(draggedNote.mode==='resize'){const newDuration=getSnapBeat(Math.max(0.125,beat-draggedNote.originalStart),snapValue);setNotes(prev=>prev.map((n,idx)=>{if(idx!==draggedNote.idx)return n;return{...n,duration_beats:newDuration};}));}else if(draggedNote.mode==='move'){const refOrigStart=draggedNote.clickedOriginalStartBeat;if(refOrigStart===undefined)return;const deltaBeat=getSnapBeat(beat-draggedNote.startOffsetBeat,snapValue)-refOrigStart;// Clamp so no note goes past beat 0
|
||||
const minOrigStart=Math.min(...draggedNote.selectedNotesOffset.map(o=>o.originalStartBeat));const clampedDeltaBeat=minOrigStart+deltaBeat<0?-minOrigStart:deltaBeat;const deltaPitch=Math.round(pitch-draggedNote.startOffsetPitch);setNotes(prev=>prev.map(n=>{const offset=draggedNote.selectedNotesOffset.find(o=>o.id===n.id);if(!offset)return n;return{...n,start_beat:getSnapBeat(Math.max(0,offset.originalStartBeat+clampedDeltaBeat),snapValue),pitch:Math.max(0,Math.min(127,offset.originalPitch+deltaPitch))};}));}};const handleGridMouseUp=()=>{if(draggedNote&&draggedNote.mode==='draw'){const dn=draggedNote;const brushIds=dn.brushIds||[];const lastBrushId=brushIds.length>0?brushIds[brushIds.length-1]:dn.drawNoteId;if(lastBrushId){const lastNote=notes.find(n=>n.id===lastBrushId);if(lastNote)lastNoteDurationRef.current=lastNote.duration_beats;}}setDraggedNote(null);setSelectionMarquee(null);rightClickDragRef.current={active:false,startX:0,startY:0};if(brushAutoScrollRef.current){clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current=null;}stopPreviewNote();previewPitchRef.current=null;};const handleContextMenu=e=>{e.preventDefault();if(swallowContextMenuRef.current){swallowContextMenuRef.current=false;return;}const pos={x:e.clientX,y:e.clientY,parentKey:null};scaleMenuOriginRef.current={x:pos.x,y:pos.y};setScaleMenuPos(pos);};const ccDragRef=React.useRef(null);const brushAutoScrollRef=React.useRef(null);const rightClickDragRef=React.useRef({active:false,startX:0,startY:0});const swallowContextMenuRef=React.useRef(false);const findCCNoteIndex=(b,mouseY,ccH)=>{const snapped=getSnapBeat(b,snapValue);const hits=[];notes.forEach((n,idx)=>{if(snapped>=n.start_beat&&snapped<=n.start_beat+n.duration_beats){const nv=ccMode==='pan'?(n.pan||0)*0.5+0.5:n.velocity!==undefined?n.velocity:0.8;const stemTop=ccH-(nv*(ccH-20)+10);hits.push({idx,dist:Math.abs(stemTop-mouseY)});}});if(hits.length>0){hits.sort((a,b)=>a.dist-b.dist);return hits[0].idx;}let nearest=-1;let minDist=Infinity;notes.forEach((n,idx)=>{const center=n.start_beat+n.duration_beats/2;const d=Math.abs(center-snapped);if(d<minDist){minDist=d;nearest=idx;}});return nearest;};// TẤT CẢ notes bắt đầu tại cùng beat (chord — velocity bars CÙNG cột) —
|
||||
const minOrigStart=Math.min(...draggedNote.selectedNotesOffset.map(o=>o.originalStartBeat));const clampedDeltaBeat=minOrigStart+deltaBeat<0?-minOrigStart:deltaBeat;const deltaPitch=Math.round(pitch-draggedNote.startOffsetPitch);setNotes(prev=>prev.map(n=>{const offset=draggedNote.selectedNotesOffset.find(o=>o.id===n.id);if(!offset)return n;return{...n,start_beat:getSnapBeat(Math.max(0,offset.originalStartBeat+clampedDeltaBeat),snapValue),pitch:Math.max(0,Math.min(127,offset.originalPitch+deltaPitch))};}));}};const handleGridMouseUp=()=>{if(draggedNote&&draggedNote.mode==='draw'){const dn=draggedNote;const brushIds=dn.brushIds||[];const lastBrushId=brushIds.length>0?brushIds[brushIds.length-1]:dn.drawNoteId;if(lastBrushId){const lastNote=notes.find(n=>n.id===lastBrushId);if(lastNote)lastNoteDurationRef.current=lastNote.duration_beats;}}setDraggedNote(null);// Marquee (Ctrl+drag): CHỌN notes nằm trong vùng (user 09:15 — trước đây
|
||||
// chỉ vẽ highlight, không set selectedNoteIds → Ctrl+X báo "chọn notes")
|
||||
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 inMarquee=notes.filter(n=>{const center=n.start_beat+n.duration_beats/2;return center>=minBeat&¢er<=maxBeat&&n.pitch>=minPitch&&n.pitch<=maxPitch;});setSelectedNoteIds(inMarquee.map(n=>n.id));}setSelectionMarquee(null);rightClickDragRef.current={active:false,startX:0,startY:0};if(brushAutoScrollRef.current){clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current=null;}stopPreviewNote();previewPitchRef.current=null;};const handleContextMenu=e=>{e.preventDefault();if(swallowContextMenuRef.current){swallowContextMenuRef.current=false;return;}const pos={x:e.clientX,y:e.clientY,parentKey:null};scaleMenuOriginRef.current={x:pos.x,y:pos.y};setScaleMenuPos(pos);};const ccDragRef=React.useRef(null);const brushAutoScrollRef=React.useRef(null);const rightClickDragRef=React.useRef({active:false,startX:0,startY:0});const swallowContextMenuRef=React.useRef(false);const findCCNoteIndex=(b,mouseY,ccH)=>{const snapped=getSnapBeat(b,snapValue);const hits=[];notes.forEach((n,idx)=>{if(snapped>=n.start_beat&&snapped<=n.start_beat+n.duration_beats){const nv=ccMode==='pan'?(n.pan||0)*0.5+0.5:n.velocity!==undefined?n.velocity:0.8;const stemTop=ccH-(nv*(ccH-20)+10);hits.push({idx,dist:Math.abs(stemTop-mouseY)});}});if(hits.length>0){hits.sort((a,b)=>a.dist-b.dist);return hits[0].idx;}let nearest=-1;let minDist=Infinity;notes.forEach((n,idx)=>{const center=n.start_beat+n.duration_beats/2;const d=Math.abs(center-snapped);if(d<minDist){minDist=d;nearest=idx;}});return nearest;};// TẤT CẢ notes bắt đầu tại cùng beat (chord — velocity bars CÙNG cột) —
|
||||
// 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;};const handleCCMouseDown=e=>{const canvas=ccCanvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat-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)
|
||||
@@ -858,7 +862,7 @@ if(e.key===' '||e.code==='Space'){if(window.mediaExplorerActive)return;e.prevent
|
||||
// CHUYỂN TAB (nhìn như space không play/stop). Blur button trước khi
|
||||
// play/stop — keyup sau đó target body → không click.
|
||||
try{const _ae=document.activeElement;if(_ae&&_ae.tagName==='BUTTON')_ae.blur();}catch(err){}if(recordingStateRef.current==='RECORDING'||recordingStateRef.current==='COUNT_IN'){handleRecordClickRef.current();return;}if(handlePlayPauseRef.current)handlePlayPauseRef.current();return;}if(activeTabRef.current!=='main'&&!activeTabRef.current.startsWith('session_')){// Sub-Tab keyboard shortcuts mapping
|
||||
const curTabId=activeTabRef.current;if(ctrl&&alt&&e.key==='n'){e.preventDefault();handleSubTabNormalize(curTabId);return;}if(e.key==='f'||e.key==='F'){e.preventDefault();handleSubTabFade(curTabId,'in');return;}if(e.key==='g'||e.key==='G'){e.preventDefault();handleSubTabFade(curTabId,'out');return;}if(ctrl&&e.key==='l'){e.preventDefault();handleSubTabLoop(curTabId,4);return;}if(e.key==='v'||e.key==='V'){e.preventDefault();const val=prompt("Nhập Gain điều chỉnh (dB):","0");if(val)handleSubTabGain(curTabId,parseFloat(val)||0);return;}if(ctrl&&e.key==='x'){e.preventDefault();handleSubTabCut(curTabId);return;}if(ctrl&&e.key==='c'){e.preventDefault();handleSubTabCopy(curTabId);return;}if(ctrl&&e.key==='v'){e.preventDefault();handleSubTabPaste(curTabId);return;}if(e.key==='Delete'||e.key==='Backspace'||e.key==='Del'){e.preventDefault();if(subTabSelectedNodeTimeRef.current!==null){const selTime=subTabSelectedNodeTimeRef.current;setSubTabs(prev=>prev.map(s=>{if(s.id!==curTabId)return s;const curNodes=s.graphMode==='pan'?s.panningNodes||[]:s.volumeNodes||[];const updated=curNodes.filter(n=>n.time!==selTime);return{...s,[s.graphMode==='pan'?'panningNodes':'volumeNodes']:updated};}));setSubTabSelectedNodeTime(null);}else{handleSubTabDelete(curTabId);}return;}return;}if(ctrl&&e.key==='z'&&!e.shiftKey){const tag=document.activeElement?.tagName;if(tag==='INPUT'||tag==='TEXTAREA')return;e.preventDefault();handleUndoRef.current();return;}if(ctrl&&(e.key==='y'||e.key==='z'&&e.shiftKey)){const tag=document.activeElement?.tagName;if(tag==='INPUT'||tag==='TEXTAREA')return;e.preventDefault();handleRedoRef.current();return;}if(ctrl&&!alt&&e.key==='o'){e.preventDefault();handleImportSFS();return;}if(ctrl&&!alt&&e.key==='n'){e.preventDefault();// New project: đóng hết tab + reset MỌI trạng thái về mặc định
|
||||
const curTabId=activeTabRef.current;if(ctrl&&alt&&e.key==='n'){e.preventDefault();handleSubTabNormalize(curTabId);return;}if(e.key==='f'||e.key==='F'){e.preventDefault();handleSubTabFade(curTabId,'in');return;}if(e.key==='g'||e.key==='G'){e.preventDefault();handleSubTabFade(curTabId,'out');return;}if(ctrl&&e.key==='l'){e.preventDefault();handleSubTabLoop(curTabId,4);return;}if(!ctrl&&!e.metaKey&&!e.altKey&&(e.key==='v'||e.key==='V')){e.preventDefault();const val=prompt("Nhập Gain điều chỉnh (dB):","0");if(val)handleSubTabGain(curTabId,parseFloat(val)||0);return;}if(ctrl&&e.key==='x'){e.preventDefault();handleSubTabCut(curTabId);return;}if(ctrl&&e.key==='c'){e.preventDefault();handleSubTabCopy(curTabId);return;}if(ctrl&&e.key==='v'){e.preventDefault();handleSubTabPaste(curTabId);return;}if(e.key==='Delete'||e.key==='Backspace'||e.key==='Del'){e.preventDefault();if(subTabSelectedNodeTimeRef.current!==null){const selTime=subTabSelectedNodeTimeRef.current;setSubTabs(prev=>prev.map(s=>{if(s.id!==curTabId)return s;const curNodes=s.graphMode==='pan'?s.panningNodes||[]:s.volumeNodes||[];const updated=curNodes.filter(n=>n.time!==selTime);return{...s,[s.graphMode==='pan'?'panningNodes':'volumeNodes']:updated};}));setSubTabSelectedNodeTime(null);}else{handleSubTabDelete(curTabId);}return;}return;}if(ctrl&&e.key==='z'&&!e.shiftKey){const tag=document.activeElement?.tagName;if(tag==='INPUT'||tag==='TEXTAREA')return;e.preventDefault();handleUndoRef.current();return;}if(ctrl&&(e.key==='y'||e.key==='z'&&e.shiftKey)){const tag=document.activeElement?.tagName;if(tag==='INPUT'||tag==='TEXTAREA')return;e.preventDefault();handleRedoRef.current();return;}if(ctrl&&!alt&&e.key==='o'){e.preventDefault();handleImportSFS();return;}if(ctrl&&!alt&&e.key==='n'){e.preventDefault();// New project: đóng hết tab + reset MỌI trạng thái về mặc định
|
||||
try{stopAllPlayback();}catch(e){}setSubTabs([]);setSessionTabs([]);setActiveTab('main');trackMidiChannelsRef.current={};Object.keys(trackMidiBypassMap).forEach(function(k){delete trackMidiBypassMap[k];});Object.keys(trackAudioBypassMap).forEach(function(k){delete trackAudioBypassMap[k];});Object.keys(trackMasteringBypassMap).forEach(function(k){delete trackMasteringBypassMap[k];});setSelectedItemIds(new Set());setTracks([{id:'1',name:'Track 01',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#0f766e',markers:[],serverFileId:null},{id:'2',name:'Track 02',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#1d4ed8',markers:[],serverFileId:null}]);setSelectedTrackId('1');setProjectName('');setCurrentProjectId(null);localStorage.removeItem('sonic_project_name');localStorage.removeItem('sonic_project_id');showToast('New project created','info');return;}if(ctrl&&!alt&&!e.shiftKey&&e.key==='a'){e.preventDefault();const curTab=activeTabRef.current;if(curTab!=='main'&&!curTab.startsWith('session_'))return;captureSelectionUndo();const allIds=new Set();(activeTracksRef.current||[]).forEach(t=>{(t.sections||[]).forEach(s=>allIds.add(s.id));(t.midiItems||[]).forEach(m=>allIds.add(m.id));const clips=t.clips&&t.clips.length>0?t.clips:t.buffer?[{id:'default',buffer:t.buffer,startTime:t.startTime||0,name:t.name,speed:t.speed||1.0}]:[];clips.forEach(c=>allIds.add(c.id==='default'?'default_'+t.id:c.id));});setSelectedItemIds(allIds);pushSelectionUndo();return;}if(ctrl&&!alt&&e.key==='s'){e.preventDefault();const curTab=activeTabRef.current;if(curTab&&curTab.startsWith('session_')){// SECTION-TAB: Ctrl+S = Lưu section (như nút Lưu section)
|
||||
handleSaveSectionTabRef.current(curTab);showToast('Đã lưu Section','success');}else{handleSaveProjectRef.current();}return;}if(ctrl&&alt&&e.key==='s'||ctrl&&e.shiftKey&&e.key==='s'){e.preventDefault();handleExportSFS();return;}if(ctrl&&!alt&&e.key==='i'){e.preventDefault();addNewTrack();return;}if(ctrl&&alt&&e.key==='i'){e.preventDefault();showToast('Import audio','info');return;}if(ctrl&&!alt&&e.key==='e'){e.preventDefault();openTempTab();return;}if(ctrl&&!alt&&e.key==='m'){e.preventDefault();handleMergeTracks();return;}if(ctrl&&!alt&&e.key==='c'){e.preventDefault();const selItems=selectedItemIdsRef.current;if(selItems&&selItems.size>0){// Copy ITEM đầu tiên được chọn (user 07:50 — Ctrl+C phải copy item)
|
||||
const firstId=selItems.values().next().value;const found=findItemContext(firstId);if(found&©ItemToClipboard(found.trackId,firstId,found.itemType)){showToast('Đã sao chép item.','info');}else{handleCopyTrack();}}else{handleCopyTrack();}return;}if(ctrl&&!alt&&e.key==='x'){e.preventDefault();e.stopPropagation();// chặn browser cut (user 07:55 — Ctrl-X bị capture)
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608070910" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608070925" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user