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:
+63
-7
@@ -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(() => {
|
React.useEffect(() => {
|
||||||
const handleCanvasWheel = (e) => {
|
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) {
|
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();
|
e.preventDefault();
|
||||||
const scrollDelta = e.deltaY;
|
const scrollDelta = e.deltaY;
|
||||||
const beatSec = 60.0 / (parseInt(bpm) || 120);
|
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 currentBeat = (st.currentTime || 0) / beatSec;
|
||||||
const maxBeats = totalBeats;
|
const maxBeats = totalBeats;
|
||||||
const newBeat = Math.max(0, Math.min(maxBeats, currentBeat + step));
|
const newBeat = Math.max(0, Math.min(maxBeats, currentBeat + step));
|
||||||
const newTime = newBeat * beatSec;
|
const newTime = newBeat * beatSec;
|
||||||
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: newTime } : s));
|
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) {
|
if (window.SonicSF) {
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
const playing = notes.filter(n =>
|
const playing = notes.filter(n =>
|
||||||
@@ -5326,6 +5347,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('playNote error:', err);
|
console.error('playNote error:', err);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onContextMenu: (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setScaleMenuPos({ x: e.clientX, y: e.clientY, parentKey: null });
|
||||||
}
|
}
|
||||||
}, showLabel && label)
|
}, showLabel && label)
|
||||||
);
|
);
|
||||||
@@ -5358,21 +5384,51 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
|
|
||||||
const SCALES = {
|
const SCALES = {
|
||||||
"None": null,
|
"None": null,
|
||||||
|
"Diatonic": {
|
||||||
"Major": [0, 2, 4, 5, 7, 9, 11],
|
"Major": [0, 2, 4, 5, 7, 9, 11],
|
||||||
"Minor": [0, 2, 3, 5, 7, 8, 10],
|
"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": {
|
"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],
|
"Chinese": [0, 2, 4, 7, 9],
|
||||||
"Vietnam": [0, 2, 6, 7, 10],
|
"Vietnam": [0, 2, 6, 7, 10],
|
||||||
"India": [0, 2, 5, 7, 9],
|
"India": [0, 2, 5, 7, 9],
|
||||||
"Japan": [0, 2, 5, 7, 9],
|
"Japan": [0, 2, 5, 7, 9],
|
||||||
"Africa": [0, 3, 5, 7, 10]
|
"Africa": [0, 3, 5, 7, 10]
|
||||||
},
|
},
|
||||||
"Blues": [0, 3, 5, 6, 7, 10],
|
"Church": {
|
||||||
"Dorian": [0, 2, 3, 5, 7, 9, 10],
|
"Dorian": [0, 2, 3, 5, 7, 9, 10],
|
||||||
"Phrygian": [0, 1, 3, 5, 7, 8, 10],
|
"Phrygian": [0, 1, 3, 5, 7, 8, 10],
|
||||||
"Lydian": [0, 2, 4, 6, 7, 9, 11],
|
"Lydian": [0, 2, 4, 6, 7, 9, 11],
|
||||||
"Mixolydian": [0, 2, 4, 5, 7, 9, 10],
|
"Mixolydian": [0, 2, 4, 5, 7, 9, 10]
|
||||||
"Locrian": [0, 1, 3, 5, 6, 8, 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 [selectedScale, setSelectedScale] = React.useState(null);
|
||||||
const [scaleMenuPos, setScaleMenuPos] = React.useState(null);
|
const [scaleMenuPos, setScaleMenuPos] = React.useState(null);
|
||||||
@@ -5622,7 +5678,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, onClose, onUpdateNot
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const startY = e.clientY;
|
const startY = e.clientY;
|
||||||
const startH = ccHeight;
|
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); };
|
const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
|
||||||
document.addEventListener('mousemove', onMove);
|
document.addEventListener('mousemove', onMove);
|
||||||
document.addEventListener('mouseup', onUp);
|
document.addEventListener('mouseup', onUp);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user