fix: piano roll click accuracy on zoom + Super Mario chiptune preset
- Fix mouse coordinate scaling in piano roll when canvas is zoomed (getBoundingClientRect returns visual size, now dividing by scale ratio) - Replace Megaman melody with Super Mario Bros overworld theme - Tune chiptune preset: faster BPM (200), snappier envelopes, brighter filter, less delay/distortion for cleaner NES sound Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,44 +10,73 @@ const BLACK_KEYS = [1, 3, 6, 8, 10];
|
||||
function midiToFreq(midi) { return 440 * Math.pow(2, (midi - 69) / 12); }
|
||||
function isBlack(midi) { return BLACK_KEYS.includes(midi % 12); }
|
||||
|
||||
// Chiptune melody: Megaman-style theme in C minor
|
||||
// Format: { note: MIDI, start: beats, duration: beats }
|
||||
const MEGA_MELODY = [
|
||||
// Bar 1: Opening riff
|
||||
{ note: 72, start: 0, duration: 0.25 }, // C5
|
||||
{ note: 72, start: 0.5, duration: 0.25 },
|
||||
{ note: 75, start: 1, duration: 0.5 }, // Eb5
|
||||
{ note: 72, start: 1.75, duration: 0.25 },
|
||||
{ note: 70, start: 2, duration: 0.5 }, // Bb4
|
||||
{ note: 67, start: 2.75, duration: 0.25 }, // G4
|
||||
{ note: 65, start: 3, duration: 0.5 }, // F4
|
||||
{ note: 63, start: 3.5, duration: 0.5 }, // Eb4
|
||||
// Super Mario Bros - Overworld Theme (NES, 1985)
|
||||
// BPM ~200, swing 8ths feel. Each beat = 1 quarter note.
|
||||
// s = eighth note unit (0.5 beats)
|
||||
const s = 0.5;
|
||||
const MARIO_MELODY = [
|
||||
// Bar 1: E5 E5 . E5 . C5 E5 .
|
||||
{ note: 76, start: 0, duration: s }, // E5
|
||||
{ note: 76, start: 1*s, duration: s }, // E5
|
||||
// rest
|
||||
{ note: 76, start: 3*s, duration: s }, // E5
|
||||
// rest
|
||||
{ note: 72, start: 5*s, duration: s }, // C5
|
||||
{ note: 76, start: 6*s, duration: 2*s }, // E5
|
||||
|
||||
// Bar 2: Response phrase
|
||||
{ note: 60, start: 4, duration: 0.5 }, // C4
|
||||
{ note: 63, start: 4.5, duration: 0.25 }, // Eb4
|
||||
{ note: 65, start: 5, duration: 0.25 }, // F4
|
||||
{ note: 67, start: 5.5, duration: 0.75 }, // G4
|
||||
{ note: 70, start: 6.5, duration: 0.5 }, // Bb4
|
||||
{ note: 72, start: 7, duration: 1 }, // C5
|
||||
// Bar 2: G5 . . . G4 . . .
|
||||
{ note: 79, start: 8*s, duration: 2*s }, // G5
|
||||
{ note: 67, start: 12*s, duration: 2*s }, // G4
|
||||
|
||||
// Bar 3: Climb up
|
||||
{ note: 67, start: 8, duration: 0.25 }, // G4
|
||||
{ note: 70, start: 8.5, duration: 0.25 }, // Bb4
|
||||
{ note: 72, start: 9, duration: 0.25 }, // C5
|
||||
{ note: 75, start: 9.5, duration: 0.5 }, // Eb5
|
||||
{ note: 77, start: 10, duration: 0.5 }, // F5
|
||||
{ note: 79, start: 10.5, duration: 0.5 }, // G5
|
||||
{ note: 77, start: 11, duration: 0.25 }, // F5
|
||||
{ note: 75, start: 11.5, duration: 0.5 }, // Eb5
|
||||
// Bar 3: C5 . . G4 . . E4 . .
|
||||
{ note: 72, start: 16*s, duration: s }, // C5
|
||||
{ note: 67, start: 18*s, duration: s }, // G4
|
||||
{ note: 64, start: 20*s, duration: s }, // E4
|
||||
|
||||
// Bar 4: Resolution
|
||||
{ note: 72, start: 12, duration: 0.5 }, // C5
|
||||
{ note: 70, start: 12.5, duration: 0.25 }, // Bb4
|
||||
{ note: 67, start: 13, duration: 0.5 }, // G4
|
||||
{ note: 63, start: 13.75, duration: 0.25 }, // Eb4
|
||||
{ note: 60, start: 14, duration: 1 }, // C4
|
||||
{ note: 60, start: 15.5, duration: 0.5 }, // C4 (pickup)
|
||||
// Bar 4: . A4 . B4 . Bb4 A4 .
|
||||
{ note: 69, start: 22*s, duration: s }, // A4
|
||||
{ note: 71, start: 24*s, duration: s }, // B4
|
||||
{ note: 70, start: 25*s, duration: s }, // Bb4
|
||||
{ note: 69, start: 26*s, duration: s }, // A4
|
||||
|
||||
// Bar 5: G4 E5 G5 A5 . F5 G5 .
|
||||
{ note: 67, start: 28*s, duration: 0.75 }, // G4 (triplet feel)
|
||||
{ note: 76, start: 30*s, duration: 0.75 }, // E5
|
||||
{ note: 79, start: 32*s, duration: s }, // G5
|
||||
{ note: 81, start: 33*s, duration: s }, // A5
|
||||
{ note: 77, start: 35*s, duration: s }, // F5
|
||||
{ note: 79, start: 36*s, duration: s }, // G5
|
||||
|
||||
// Bar 6: . E5 . C5 D5 B4 . .
|
||||
{ note: 76, start: 38*s, duration: s }, // E5
|
||||
{ note: 72, start: 40*s, duration: s }, // C5
|
||||
{ note: 74, start: 41*s, duration: s }, // D5
|
||||
{ note: 71, start: 42*s, duration: s }, // B4
|
||||
|
||||
// Bar 7: . . C5 . . G4 . . E4
|
||||
{ note: 72, start: 44*s, duration: s }, // C5
|
||||
{ note: 67, start: 46*s, duration: s }, // G4
|
||||
{ note: 64, start: 48*s, duration: s }, // E4
|
||||
|
||||
// Bar 8: . A4 . B4 . Bb4 A4 .
|
||||
{ note: 69, start: 50*s, duration: s }, // A4
|
||||
{ note: 71, start: 52*s, duration: s }, // B4
|
||||
{ note: 70, start: 53*s, duration: s }, // Bb4
|
||||
{ note: 69, start: 54*s, duration: s }, // A4
|
||||
|
||||
// Bar 9: G4 E5 G5 A5 . F5 G5 .
|
||||
{ note: 67, start: 56*s, duration: 0.75 }, // G4
|
||||
{ note: 76, start: 58*s, duration: 0.75 }, // E5
|
||||
{ note: 79, start: 60*s, duration: s }, // G5
|
||||
{ note: 81, start: 61*s, duration: s }, // A5
|
||||
{ note: 77, start: 63*s, duration: s }, // F5
|
||||
{ note: 79, start: 64*s, duration: s }, // G5
|
||||
|
||||
// Bar 10: . E5 . C5 D5 B4
|
||||
{ note: 76, start: 66*s, duration: s }, // E5
|
||||
{ note: 72, start: 68*s, duration: s }, // C5
|
||||
{ note: 74, start: 69*s, duration: s }, // D5
|
||||
{ note: 71, start: 70*s, duration: 2*s }, // B4
|
||||
];
|
||||
|
||||
const ROLL_W = 500;
|
||||
@@ -75,9 +104,9 @@ export default function PianoRollWidget({ moduleId }) {
|
||||
|
||||
// Init notes
|
||||
if (!mod?.params?._notes) {
|
||||
if (mod) mod.params._notes = [...MEGA_MELODY];
|
||||
if (mod) mod.params._notes = [...MARIO_MELODY];
|
||||
}
|
||||
const notes = mod?.params?._notes || MEGA_MELODY;
|
||||
const notes = mod?.params?._notes || MARIO_MELODY;
|
||||
const notesRef = useRef(notes);
|
||||
notesRef.current = notes;
|
||||
|
||||
@@ -258,8 +287,11 @@ export default function PianoRollWidget({ moduleId }) {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mx = e.clientX - rect.left;
|
||||
const my = e.clientY - rect.top;
|
||||
// Account for CSS transform scale (zoom) — rect is visual size, canvas is logical size
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
const mx = (e.clientX - rect.left) * scaleX;
|
||||
const my = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
if (mx < KEY_W) return; // Clicked on piano keys
|
||||
|
||||
@@ -298,7 +330,7 @@ export default function PianoRollWidget({ moduleId }) {
|
||||
|
||||
const handleMove = (me) => {
|
||||
if (!drawingRef.current) return;
|
||||
const mmx = me.clientX - rect.left;
|
||||
const mmx = (me.clientX - rect.left) * scaleX;
|
||||
const endBeat = Math.max(drawingRef.current.start + 0.25,
|
||||
Math.ceil(((mmx - KEY_W) / beatW) * 4) / 4);
|
||||
drawingRef.current.duration = endBeat - drawingRef.current.start;
|
||||
|
||||
Reference in New Issue
Block a user