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:
+68
-12
@@ -4676,20 +4676,41 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Shift + Scroll event listener: fast‑forward playhead + play notes
|
||||
// Alt + Scroll event listener: fast‑forward 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
Reference in New Issue
Block a user