feat: piano roll velocity scroll, alt+scroll play, scale menu

- Shift+scroll on note: change velocity; on empty space: horizontal scroll
- Alt+scroll: playhead scrub + play notes (was shift+scroll)
- Fix CC lane height drag direction (up=increase, down=decrease)
- Restructure SCALES hierarchy: Diatonic, Pentatonic, Church, Jazz, Asian, Middle Eastern
- Right-click on keybed pitch row opens scale context menu
This commit is contained in:
2026-07-25 11:56:50 +07:00
parent ac78d0aa7a
commit 51787e055e
2 changed files with 73 additions and 17 deletions
+68 -12
View File
@@ -4676,20 +4676,41 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
};
}, []);
// Shift + Scroll event listener: fastforward playhead + play notes
// Alt + Scroll event listener: fastforward playhead + play notes
React.useEffect(() => {
const handleCanvasWheel = (e) => {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const pitch = 127 - Math.floor(my / NoteHeight);
if (e.shiftKey) {
e.preventDefault();
// Shift+scroll on note change velocity
const clickedNote = notes.find(n => pitch === n.pitch && mx >= n.start_beat * pixelsPerBeat && mx < (n.start_beat + n.duration_beats) * pixelsPerBeat);
if (clickedNote) {
const delta = e.deltaY < 0 ? 0.05 : -0.05;
setNotes(prev => prev.map(n => n.id === clickedNote.id ? { ...n, velocity: Math.max(0.05, Math.min(1, (n.velocity || 0.8) + delta)) } : n));
} else {
// Shift+scroll on empty space horizontal scroll
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; // 1/4 beat per notch
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));
// Play notes only when playhead crosses a note start
if (window.SonicSF) {
const ctx = getAudioContext();
const playing = notes.filter(n =>
@@ -5326,6 +5347,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
} catch (err) {
console.error('playNote error:', err);
}
},
onContextMenu: (e) => {
e.preventDefault();
e.stopPropagation();
setScaleMenuPos({ x: e.clientX, y: e.clientY, parentKey: null });
}
}, showLabel && label)
);
@@ -5358,21 +5384,51 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
const SCALES = {
"None": null,
"Major": [0, 2, 4, 5, 7, 9, 11],
"Minor": [0, 2, 3, 5, 7, 8, 10],
"Diatonic": {
"Major": [0, 2, 4, 5, 7, 9, 11],
"Minor": [0, 2, 3, 5, 7, 8, 10],
"Dorian": [0, 2, 3, 5, 7, 9, 10],
"Phrygian": [0, 1, 3, 5, 7, 8, 10],
"Lydian": [0, 2, 4, 6, 7, 9, 11],
"Mixolydian": [0, 2, 4, 5, 7, 9, 10],
"Locrian": [0, 1, 3, 5, 6, 8, 10]
},
"Pentatonic": {
"Major": [0, 2, 4, 7, 9],
"Minor": [0, 3, 5, 7, 10],
"Blues": [0, 3, 5, 6, 7, 10],
"Chinese": [0, 2, 4, 7, 9],
"Vietnam": [0, 2, 6, 7, 10],
"India": [0, 2, 5, 7, 9],
"Japan": [0, 2, 5, 7, 9],
"Africa": [0, 3, 5, 7, 10]
},
"Blues": [0, 3, 5, 6, 7, 10],
"Dorian": [0, 2, 3, 5, 7, 9, 10],
"Phrygian": [0, 1, 3, 5, 7, 8, 10],
"Lydian": [0, 2, 4, 6, 7, 9, 11],
"Mixolydian": [0, 2, 4, 5, 7, 9, 10],
"Locrian": [0, 1, 3, 5, 6, 8, 10]
"Church": {
"Dorian": [0, 2, 3, 5, 7, 9, 10],
"Phrygian": [0, 1, 3, 5, 7, 8, 10],
"Lydian": [0, 2, 4, 6, 7, 9, 11],
"Mixolydian": [0, 2, 4, 5, 7, 9, 10]
},
"Jazz": {
"Blues": [0, 3, 5, 6, 7, 10],
"Bebop": [0, 2, 4, 5, 7, 9, 10, 11],
"Diminished": [0, 2, 3, 5, 6, 8, 9, 11]
},
"Asian Traditional": {
"Chinese": [0, 2, 4, 7, 9],
"Vietnam": [0, 2, 6, 7, 10],
"Japan": [0, 2, 5, 7, 9],
"India": [0, 2, 5, 7, 9],
"Gamelan": [0, 2, 4, 7, 9],
"Korea": [0, 2, 4, 5, 7, 9]
},
"Middle Eastern": {
"Hijaz": [0, 1, 4, 5, 7, 8, 11],
"Nikriz": [0, 1, 4, 5, 7, 8, 10],
"Rast": [0, 2, 4, 5, 7, 8, 10],
"Saba": [0, 1, 3, 4, 7, 8, 10],
"Bayati": [0, 2, 3, 4, 7, 8, 10]
}
};
const [selectedScale, setSelectedScale] = React.useState(null);
const [scaleMenuPos, setScaleMenuPos] = React.useState(null);
@@ -5622,7 +5678,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
e.preventDefault();
const startY = e.clientY;
const startH = ccHeight;
const onMove = ev => { setCcHeight(Math.max(40, startH + ev.clientY - startY)); };
const onMove = ev => { setCcHeight(Math.max(40, startH + startY - ev.clientY)); };
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
File diff suppressed because one or more lines are too long