fix: piano roll draw notes snapped to grid via getSnapBeat
note.start_beat + duration_beats snapped before rendering. Applies to active, ghost, and recording notes.
This commit is contained in:
+15
-6
@@ -6175,9 +6175,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
ctx.fillStyle = layer.track_color || '#888';
|
||||
ctx.strokeStyle = layer.track_color || '#888';
|
||||
layer.notes.forEach(function(note) {
|
||||
var x = (renderBeatOffset + note.relative_start_beat) * pixelsPerBeat;
|
||||
var snapStart = snapVal !== 'free' ? getSnapBeat(note.relative_start_beat, snapVal) : note.relative_start_beat;
|
||||
var rawEnd = note.relative_start_beat + note.duration_beats;
|
||||
var snapEnd = snapVal !== 'free' ? getSnapBeat(rawEnd, snapVal) : rawEnd;
|
||||
var x = (renderBeatOffset + snapStart) * pixelsPerBeat;
|
||||
var y = (127 - note.pitch) * NoteHeight;
|
||||
var w = note.duration_beats * pixelsPerBeat;
|
||||
var w = Math.max(2, (snapEnd - snapStart) * pixelsPerBeat);
|
||||
var h = NoteHeight - 1;
|
||||
ctx.fillRect(x, y, w, h);
|
||||
});
|
||||
@@ -6187,9 +6190,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
|
||||
// Layer 3: Active notes with velocity layer representation
|
||||
notes.forEach((note) => {
|
||||
const x = (renderBeatOffset + note.start_beat) * pixelsPerBeat;
|
||||
const snapStart = snapVal !== 'free' ? getSnapBeat(note.start_beat, snapVal) : note.start_beat;
|
||||
const rawEnd = note.start_beat + note.duration_beats;
|
||||
const snapEnd = snapVal !== 'free' ? getSnapBeat(rawEnd, snapVal) : rawEnd;
|
||||
const x = (renderBeatOffset + snapStart) * pixelsPerBeat;
|
||||
const y = (127 - note.pitch) * NoteHeight;
|
||||
const w = note.duration_beats * pixelsPerBeat;
|
||||
const w = Math.max(2, (snapEnd - snapStart) * pixelsPerBeat);
|
||||
const isSelected = selectedNoteIds.includes(note.id);
|
||||
|
||||
// Draw background of note
|
||||
@@ -6209,9 +6215,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
// Draw real-time recording notes
|
||||
if (recordingState === 'RECORDING' && recTempMidiNotes && recTempMidiNotes.length > 0) {
|
||||
recTempMidiNotes.forEach(note => {
|
||||
const x = (renderBeatOffset + note.start_beat) * pixelsPerBeat;
|
||||
const snapStart = snapVal !== 'free' ? getSnapBeat(note.start_beat, snapVal) : note.start_beat;
|
||||
const rawEnd = note.start_beat + (note.duration_beats || 0.25);
|
||||
const snapEnd = snapVal !== 'free' ? getSnapBeat(rawEnd, snapVal) : rawEnd;
|
||||
const x = (renderBeatOffset + snapStart) * pixelsPerBeat;
|
||||
const y = (127 - note.pitch) * NoteHeight;
|
||||
const w = (note.duration_beats || 0.25) * pixelsPerBeat;
|
||||
const w = Math.max(2, (snapEnd - snapStart) * pixelsPerBeat);
|
||||
ctx.fillStyle = 'rgba(255, 100, 100, 0.35)';
|
||||
ctx.strokeStyle = '#ff6464';
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
@@ -208,11 +208,11 @@ const scrollBeat=mx/pixelsPerBeat-renderBeatOffset;const clickedNote=notes.find(
|
||||
const container=gridScrollRef.current;if(container)container.scrollLeft+=e.deltaY;}return;}if(e.altKey){e.preventDefault();const scrollDelta=e.deltaY;const beatSec=60.0/(parseInt(bpm)||120);const step=scrollDelta<0?-0.25:0.25;const currentBeat=(st.currentTime||0)/beatSec;const maxBeats=totalBeats;const newBeat=Math.max(0,Math.min(maxBeats,currentBeat+step));const newTime=newBeat*beatSec;setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:newTime}:s));if(window.SonicSF){const ctx=getAudioContext();const playing=notes.filter(n=>currentBeat<n.start_beat&&newBeat>=n.start_beat);var pvTrk=activeTracks.find(function(t){return t.id===st.trackId;});var pvCh=window.SonicPianoRoll?window.SonicPianoRoll.getTrackMidiChannel(pvTrk,activeTracks):pvTrk?pvTrk.midiChannel:0;playing.forEach(n=>{window.SonicSF.playNote(n.pitch,(n.velocity||0.8)*127,200,ctx.currentTime,st.instrumentProgram,null,pvCh,pvTrk?pvTrk.synth_engine:undefined);});}}};const canvas=canvasRef.current;if(canvas){canvas.addEventListener('wheel',handleCanvasWheel,{passive:false});}return()=>{if(canvas){canvas.removeEventListener('wheel',handleCanvasWheel);}};},[notes,st.currentTime,pixelsPerBeat,st.id,totalBeats,bpm]);React.useLayoutEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=128*NoteHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);// Draw background rows
|
||||
for(let pitch=0;pitch<128;pitch++){const y=(127-pitch)*NoteHeight;const isBlack=[1,3,6,8,10].includes(pitch%12);ctx.fillStyle=isBlack?'#1a1a1e':'#25252a';ctx.fillRect(0,y,viewWidth,NoteHeight);ctx.strokeStyle='#2d2d35';ctx.lineWidth=0.5;ctx.beginPath();ctx.moveTo(0,y+NoteHeight);ctx.lineTo(viewWidth,y+NoteHeight);ctx.stroke();}// Draw snap lines
|
||||
let snapBeats=0.25;if(snapVal==='1')snapBeats=1.0;else if(snapVal==='1/2')snapBeats=0.5;else if(snapVal==='1/4')snapBeats=0.25;else if(snapVal==='1/8')snapBeats=0.125;else if(snapVal==='1/16')snapBeats=0.0625;else if(snapVal==='4')snapBeats=4.0;else if(snapVal==='1/32')snapBeats=0.03125;for(let beat=0;beat<=viewBeats;beat+=snapBeats){const x=beat*pixelsPerBeat;if(x>drawWidth)break;const isBar=beat%timeSigNum===0;ctx.strokeStyle=isBar?'#444450':'#2d2d35';ctx.lineWidth=isBar?1.2:0.6;ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,h);ctx.stroke();}// Layer 2: Ghost Notes (background reference from other tracks)
|
||||
if(showGhostNotes&&sessionSyncMode&&ghostLayers.length>0){ghostLayers.forEach(function(layer){ctx.save();ctx.globalAlpha=0.25;ctx.fillStyle=layer.track_color||'#888';ctx.strokeStyle=layer.track_color||'#888';layer.notes.forEach(function(note){var x=(renderBeatOffset+note.relative_start_beat)*pixelsPerBeat;var y=(127-note.pitch)*NoteHeight;var w=note.duration_beats*pixelsPerBeat;var h=NoteHeight-1;ctx.fillRect(x,y,w,h);});ctx.restore();});}// Layer 3: Active notes with velocity layer representation
|
||||
notes.forEach(note=>{const x=(renderBeatOffset+note.start_beat)*pixelsPerBeat;const y=(127-note.pitch)*NoteHeight;const w=note.duration_beats*pixelsPerBeat;const isSelected=selectedNoteIds.includes(note.id);// Draw background of note
|
||||
if(showGhostNotes&&sessionSyncMode&&ghostLayers.length>0){ghostLayers.forEach(function(layer){ctx.save();ctx.globalAlpha=0.25;ctx.fillStyle=layer.track_color||'#888';ctx.strokeStyle=layer.track_color||'#888';layer.notes.forEach(function(note){var snapStart=snapVal!=='free'?getSnapBeat(note.relative_start_beat,snapVal):note.relative_start_beat;var rawEnd=note.relative_start_beat+note.duration_beats;var snapEnd=snapVal!=='free'?getSnapBeat(rawEnd,snapVal):rawEnd;var x=(renderBeatOffset+snapStart)*pixelsPerBeat;var y=(127-note.pitch)*NoteHeight;var w=Math.max(2,(snapEnd-snapStart)*pixelsPerBeat);var h=NoteHeight-1;ctx.fillRect(x,y,w,h);});ctx.restore();});}// Layer 3: Active notes with velocity layer representation
|
||||
notes.forEach(note=>{const snapStart=snapVal!=='free'?getSnapBeat(note.start_beat,snapVal):note.start_beat;const rawEnd=note.start_beat+note.duration_beats;const snapEnd=snapVal!=='free'?getSnapBeat(rawEnd,snapVal):rawEnd;const x=(renderBeatOffset+snapStart)*pixelsPerBeat;const y=(127-note.pitch)*NoteHeight;const w=Math.max(2,(snapEnd-snapStart)*pixelsPerBeat);const isSelected=selectedNoteIds.includes(note.id);// Draw background of note
|
||||
ctx.fillStyle=isSelected?'rgba(59, 130, 246, 0.4)':'rgba(234, 179, 8, 0.25)';ctx.strokeStyle=isSelected?'#3b82f6':'#ca8a04';ctx.lineWidth=isSelected?1.5:1;ctx.fillRect(x+1,y+1,w-2,NoteHeight-2);ctx.strokeRect(x+1,y+1,w-2,NoteHeight-2);// Draw velocity layer (solid yellow/blue bar inside, proportional to velocity)
|
||||
const vel=note.velocity!==undefined?note.velocity:0.8;const velW=Math.max(2,(w-2)*vel);ctx.fillStyle=isSelected?'#3b82f6':'#eab308';ctx.fillRect(x+1,y+1,velW,NoteHeight-2);});// Draw real-time recording notes
|
||||
if(recordingState==='RECORDING'&&recTempMidiNotes&&recTempMidiNotes.length>0){recTempMidiNotes.forEach(note=>{const x=(renderBeatOffset+note.start_beat)*pixelsPerBeat;const y=(127-note.pitch)*NoteHeight;const w=(note.duration_beats||0.25)*pixelsPerBeat;ctx.fillStyle='rgba(255, 100, 100, 0.35)';ctx.strokeStyle='#ff6464';ctx.lineWidth=1;ctx.fillRect(x+1,y+1,Math.max(2,w-2),NoteHeight-2);ctx.strokeRect(x+1,y+1,Math.max(2,w-2),NoteHeight-2);const vel=Math.min(1,note.velocity||0.8);ctx.fillStyle='#ff6464';ctx.fillRect(x+1,y+1,Math.max(2,(w-2)*vel),NoteHeight-2);});}// Draw selection marquee if active
|
||||
if(recordingState==='RECORDING'&&recTempMidiNotes&&recTempMidiNotes.length>0){recTempMidiNotes.forEach(note=>{const snapStart=snapVal!=='free'?getSnapBeat(note.start_beat,snapVal):note.start_beat;const rawEnd=note.start_beat+(note.duration_beats||0.25);const snapEnd=snapVal!=='free'?getSnapBeat(rawEnd,snapVal):rawEnd;const x=(renderBeatOffset+snapStart)*pixelsPerBeat;const y=(127-note.pitch)*NoteHeight;const w=Math.max(2,(snapEnd-snapStart)*pixelsPerBeat);ctx.fillStyle='rgba(255, 100, 100, 0.35)';ctx.strokeStyle='#ff6464';ctx.lineWidth=1;ctx.fillRect(x+1,y+1,Math.max(2,w-2),NoteHeight-2);ctx.strokeRect(x+1,y+1,Math.max(2,w-2),NoteHeight-2);const vel=Math.min(1,note.velocity||0.8);ctx.fillStyle='#ff6464';ctx.fillRect(x+1,y+1,Math.max(2,(w-2)*vel),NoteHeight-2);});}// Draw selection marquee if active
|
||||
if(selectionMarquee){const minBeat=Math.min(selectionMarquee.startBeat,selectionMarquee.currentBeat);const maxBeat=Math.max(selectionMarquee.startBeat,selectionMarquee.currentBeat);const minPitch=Math.min(selectionMarquee.startPitch,selectionMarquee.currentPitch);const maxPitch=Math.max(selectionMarquee.startPitch,selectionMarquee.currentPitch);const mx=minBeat*pixelsPerBeat;const my=(127-maxPitch)*NoteHeight;const mw=(maxBeat-minBeat)*pixelsPerBeat;const mh=(maxPitch-minPitch+1)*NoteHeight;ctx.fillStyle='rgba(59, 130, 246, 0.15)';ctx.strokeStyle='#3b82f6';ctx.lineWidth=1;ctx.setLineDash([4,4]);ctx.fillRect(mx,my,mw,mh);ctx.strokeRect(mx,my,mw,mh);ctx.setLineDash([]);}// Draw playhead
|
||||
if(st.currentTime!==undefined&&st.currentTime!==null){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,snapVal,rollZoom,selectedNoteIds,selectionMarquee,st.currentTime,bpm,viewWidth,viewBeats,recordingState,recTempMidiNotes,showGhostNotes,sessionSyncMode,ghostLayers,renderBeatOffset]);React.useLayoutEffect(()=>{const canvas=ccCanvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=ccHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);ctx.fillStyle='#161616';ctx.fillRect(0,0,viewWidth,h);ctx.strokeStyle='#252525';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(0,h/2);ctx.lineTo(viewWidth,h/2);ctx.stroke();notes.forEach(note=>{const x=(renderBeatOffset+note.start_beat)*pixelsPerBeat;const isSelected=selectedNoteIds.includes(note.id);let val=note.velocity!==undefined?note.velocity:0.8;if(ccMode==='pan'){val=(note.pan!==undefined?note.pan:0.0)*0.5+0.5;}const stemH=val*(h-20)+10;const y=h-stemH;ctx.strokeStyle=ccMode==='pan'?isSelected?'#60a5fa':'#a78bfa':isSelected?'#3b82f6':'#fbbf24';ctx.lineWidth=2.5;ctx.beginPath();ctx.moveTo(x,h);ctx.lineTo(x,y);ctx.stroke();ctx.fillStyle=ccMode==='pan'?isSelected?'#3b82f6':'#c084fc':isSelected?'#3b82f6':'#fbbf24';ctx.beginPath();ctx.arc(x,y,3.5,0,2*Math.PI);ctx.fill();});},[notes,ccMode,rollZoom,viewWidth,selectedNoteIds,renderBeatOffset]);React.useEffect(()=>{const scrollToC3=()=>{if(gridScrollRef.current){const ch=gridScrollRef.current.clientHeight||400;gridScrollRef.current.scrollTop=Math.max(0,(127-48)*NoteHeight+NoteHeight-ch);}};scrollToC3();const timer=setTimeout(scrollToC3,100);return()=>clearTimeout(timer);},[]);// Sync ghost play data to subTab state for playback integration
|
||||
React.useEffect(function(){if(!sessionSyncMode||!showGhostNotes||!ghostLayers.length){setSubTabs(function(prev){return prev.map(function(s){if(s.id!==st.id)return s;return Object.assign({},s,{ghostPlayLayers:[]});});});return;}var layers=[];ghostLayers.forEach(function(layer){if(activePlayTrackIds===null||activePlayTrackIds!==layer.track_id)return;var trk=(activeTracks||[]).find(function(t){return t.id===layer.track_id;});layers.push({trackId:layer.track_id,notes:layer.notes.map(function(n){return{pitch:n.pitch,start_beat:n.relative_start_beat,duration_beats:n.duration_beats,velocity:n.velocity||0.8};}),instrumentProgram:trk?trk.instrumentProgram:undefined,instrumentName:trk?trk.instrumentName:undefined,synthEngine:trk?trk.synth_engine:undefined});});setSubTabs(function(prev){return prev.map(function(s){if(s.id!==st.id)return s;return Object.assign({},s,{ghostPlayLayers:layers});});});},[ghostLayers,activePlayTrackIds,sessionSyncMode,showGhostNotes,st.id,activeTracks]);const handleGridMouseDown=e=>{const canvas=canvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const beat=x/pixelsPerBeat-renderBeatOffset;const pitch=127-Math.floor(y/NoteHeight);if(scaleMenuPos)setScaleMenuPos(null);// Right click -> delete note (if on note) or prepare for sweep-drag
|
||||
|
||||
@@ -849,6 +849,12 @@
|
||||
- **Các file ảnh hưởng:** `app/templates/index.html`, `app/static/js/app.jsx`
|
||||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||||
|
||||
### [2026-07-30 21:15] Task: PIANO ROLL draw notes snapped to grid
|
||||
- **Tóm tắt thay đổi:** Note `start_beat` + `duration_beats` được snap bằng `getSnapBeat` trước khi vẽ lên canvas. Áp dụng cho active notes, ghost notes, recording notes.
|
||||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||||
- **Ghi chú/Test (nếu có):** `npm run build` pass. Note hiển thị thẳng hàng với grid lines theo snap value.
|
||||
---
|
||||
|
||||
### [2026-07-30 21:07] Task: Fix PIANO ROLL snap grid — snap values inverted in 3 places
|
||||
- **Tóm tắt thay đổi:** snapVal mapping bị đảo ngược: snap='1' (1 beat) cho q=4.0 (4 beats) → grid vẽ mỗi 4 beat, note snap mỗi 4 beat. Fix: `getSnapBeat`, `getSnapDuration`, grid drawing: snap='1'→1.0, '1/2'→0.5, '1/4'→0.25, '1/8'→0.125, '1/16'→0.0625, '1/32'→0.03125, '4'→4.0.
|
||||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
|
||||
|
||||
Reference in New Issue
Block a user